├── .envrc ├── .gitattributes ├── .github └── workflows │ ├── ci.yml │ └── golangci-lint.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── README.md ├── cmd └── gonix │ ├── drv │ └── cmd.go │ ├── drv_store_uri_mapper.go │ ├── main.go │ └── nar │ ├── cat.go │ ├── cmd.go │ ├── dump.go │ └── ls.go ├── default.nix ├── devshell.nix ├── flake.lock ├── flake.nix ├── formatter.nix ├── go.mod ├── go.sum ├── pkg ├── derivation │ ├── derivation.go │ ├── derivation_test.go │ ├── drv_path.go │ ├── encode.go │ ├── hashes.go │ ├── helpers.go │ ├── json_test.go │ ├── output.go │ ├── parser.go │ ├── store.go │ └── store │ │ ├── badger.go │ │ ├── filesystem.go │ │ ├── filesystem_test.go │ │ ├── http.go │ │ ├── map.go │ │ ├── store_test.go │ │ ├── uri.go │ │ └── util.go ├── nar │ ├── doc.go │ ├── dump.go │ ├── dump_nonwindows_test.go │ ├── dump_test.go │ ├── fixtures_test.go │ ├── header.go │ ├── header_mode.go │ ├── header_mode_windows.go │ ├── header_test.go │ ├── ls │ │ ├── doc.go │ │ ├── list.go │ │ └── list_test.go │ ├── reader.go │ ├── reader_test.go │ ├── types.go │ ├── util.go │ ├── util_test.go │ ├── writer.go │ └── writer_test.go ├── narinfo │ ├── check.go │ ├── fingerprint.go │ ├── narinfo_test.go │ ├── parser.go │ ├── signature │ │ ├── public_key.go │ │ ├── secret_key.go │ │ ├── signature.go │ │ ├── signature_test.go │ │ └── util.go │ ├── splitonce_test.go │ └── types.go ├── nixbase32 │ ├── nixbase32.go │ └── nixbase32_test.go ├── nixhash │ ├── algo.go │ ├── algo_test.go │ ├── encoding.go │ ├── hash.go │ ├── hash_test.go │ ├── hash_with_encoding.go │ ├── parse.go │ └── util.go ├── sqlite │ ├── README.md │ ├── binary_cache_v6 │ │ ├── db.go │ │ ├── models.go │ │ ├── query.sql │ │ ├── query.sql.go │ │ └── schema.sql │ ├── eval_cache_v5 │ │ ├── db.go │ │ ├── models.go │ │ ├── query.sql │ │ ├── query.sql.go │ │ └── schema.sql │ ├── fetcher_cache_v2 │ │ ├── db.go │ │ ├── models.go │ │ ├── query.sql │ │ ├── query.sql.go │ │ └── schema.sql │ ├── nix_v10 │ │ ├── db.go │ │ ├── models.go │ │ ├── query.sql │ │ ├── query.sql.go │ │ └── schema.sql │ ├── sqlite.go │ └── sqlite_test.go ├── storepath │ ├── references │ │ ├── refs.go │ │ └── refs_test.go │ ├── storepath.go │ └── storepath_test.go └── wire │ ├── bytes_reader.go │ ├── bytes_writer.go │ ├── read.go │ ├── read_test.go │ ├── wire.go │ ├── write.go │ └── write_test.go ├── shell.nix ├── sqlc.yml └── test └── testdata ├── 0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv ├── 0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv.json ├── 0zhkga32apid60mm7nh92z2970im5837-bootstrap-tools.drv ├── 292w8yzv5nn7nhdpxcs8b7vby2p27s09-nested-json.drv ├── 292w8yzv5nn7nhdpxcs8b7vby2p27s09-nested-json.drv.json ├── 385bniikgs469345jfsbw24kjfhxrsi0-foo-file.drv ├── 4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv ├── 4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv.json ├── 52a9id8hx688hvlnz4d1n25ml1jdykz0-unicode.drv ├── 52a9id8hx688hvlnz4d1n25ml1jdykz0-unicode.drv.json ├── 9lj1lkjm2ag622mh4h9rpy6j607an8g2-structured-attrs.drv ├── 9lj1lkjm2ag622mh4h9rpy6j607an8g2-structured-attrs.drv.json ├── big.narinfo ├── build-fixtures.go ├── ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv ├── ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv.json ├── cl5fr6hlr6hdqza2vgb9qqy5s26wls8i-jq-1.6.drv ├── cp1252.nix ├── derivation_multi-outputs.nix ├── derivation_nested-json.nix ├── derivation_sha1.nix ├── derivation_sha256.nix ├── derivation_structured.nix ├── h32dahq0bx5rp1krcdx3a53asj21jvhk-has-multi-out.drv ├── h32dahq0bx5rp1krcdx3a53asj21jvhk-has-multi-out.drv.json ├── latin1.nix ├── m1vfixn8iprlf0v9abmlrz7mjw1xj8kp-cp1252.drv ├── m1vfixn8iprlf0v9abmlrz7mjw1xj8kp-cp1252.drv.json ├── m5j1yp47lw1psd9n6bzina1167abbprr-bash44-023.drv ├── nar_1094wph9z4nwlgvsd53abfz8i117ykiv5dwnq9nnhz846s7xqd7d.nar ├── nar_1094wph9z4nwlgvsd53abfz8i117ykiv5dwnq9nnhz846s7xqd7d.nar_bin_arp ├── ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv ├── ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv.json ├── unicode.nix ├── x6p0hg79i3wg0kkv7699935f7rrj9jf3-latin1.drv ├── x6p0hg79i3wg0kkv7699935f7rrj9jf3-latin1.drv.json └── z8dajq053b2bxc3ncqp8p8y3nfwafh3p-foo-file.drv /.envrc: -------------------------------------------------------------------------------- 1 | watch_file devshell.nix 2 | 3 | if nix flake metadata &>/dev/null; then 4 | use flake 5 | else 6 | use nix 7 | fi 8 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.nar binary 2 | # Mark testdata directory contents as not text, 3 | # so Git won't replace \n with \r\n on Windows. 4 | # We make a few exceptions below. 5 | **/testdata/** -text 6 | /test/testdata/build-fixtures.go text 7 | /test/testdata/nar_1094wph9z4nwlgvsd53abfz8i117ykiv5dwnq9nnhz846s7xqd7d.nar_bin_arp binary 8 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | fixtures: 11 | name: fixtures-up-to-date 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: cachix/install-nix-action@v22 16 | with: 17 | install_url: https://releases.nixos.org/nix/nix-2.12.1/install 18 | - uses: actions/setup-go@v4 19 | with: 20 | go-version: "1.21" 21 | - name: Build fixtures 22 | run: bash -c 'cd test/testdata && ./build-fixtures.go' 23 | - name: Diff fixtures 24 | run: git diff --exit-code test/testdata 25 | 26 | build: 27 | strategy: 28 | matrix: 29 | os: ["ubuntu-latest", "macos-latest", "windows-latest"] 30 | go: ["1.20", "1.21"] 31 | runs-on: ${{ matrix.os }} 32 | 33 | name: Build (Go ${{ matrix.go }}, OS ${{ matrix.os }}) 34 | steps: 35 | - uses: actions/checkout@v3 36 | - uses: actions/setup-go@v4 37 | with: 38 | go-version: ${{ matrix.go }} 39 | - name: go test -race -bench='.+' -v ./... 40 | run: go test -race -bench='.+' -v ./... 41 | -------------------------------------------------------------------------------- /.github/workflows/golangci-lint.yml: -------------------------------------------------------------------------------- 1 | name: golangci-lint 2 | on: 3 | push: 4 | tags: 5 | - v* 6 | branches: 7 | - main 8 | pull_request: 9 | permissions: 10 | contents: read 11 | # Optional: allow read access to pull request. Use with `only-new-issues` option. 12 | # pull-requests: read 13 | jobs: 14 | golangci: 15 | name: lint 16 | runs-on: ubuntu-22.04 17 | steps: 18 | - uses: actions/checkout@v3 19 | - name: golangci-lint 20 | uses: golangci/golangci-lint-action@v3 21 | with: 22 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 23 | version: v1.61.0 24 | 25 | # Optional: working directory, useful for monorepos 26 | # working-directory: somedir 27 | 28 | # Optional: golangci-lint command line arguments. 29 | args: --timeout=10m 30 | 31 | # Optional: show only new issues if it's a pull request. The default value is `false`. 32 | # only-new-issues: true 33 | 34 | # Optional: if set to true then the action will use pre-installed Go. 35 | # skip-go-installation: true 36 | 37 | # Optional: if set to true then the action don't cache or restore ~/go/pkg. 38 | # skip-pkg-cache: true 39 | 40 | # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. 41 | # skip-build-cache: true 42 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /gonix 2 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable: 3 | - errname 4 | - exhaustive 5 | - gci 6 | - gochecknoglobals 7 | - gochecknoinits 8 | - goconst 9 | - godot 10 | - gofumpt 11 | - goheader 12 | - goimports 13 | - gosec 14 | - importas 15 | - ireturn 16 | - lll 17 | - makezero 18 | - misspell 19 | - nakedret 20 | - nestif 21 | - nilerr 22 | - nilnil 23 | - nlreturn 24 | - noctx 25 | - nolintlint 26 | - prealloc 27 | - predeclared 28 | - revive 29 | - rowserrcheck 30 | - stylecheck 31 | - tagliatelle 32 | - tenv 33 | - testpackage 34 | - unconvert 35 | - unparam 36 | - wastedassign 37 | - whitespace 38 | - wsl 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # go-nix - Nix experiments written in go 2 | 3 | _STATUS_: experimental 4 | 5 | This repository holds a bunch of experiments written in Go. 6 | 7 | ## `cmd/gonix` 8 | 9 | A command line entrypoint called `gonix`, currently implementing the nar 10 | {cat,dump-path,ls} commands. 11 | 12 | They're not meant to be 100% compatible, but are documented in the `--help` 13 | output. 14 | 15 | ## `pkg/derivation` 16 | 17 | A parser for Nix `.drv` files. 18 | Functions to calculate derivation paths and output hashes. 19 | 20 | ## `pkg/derivation/store` 21 | 22 | A Structure to hold derivation graphs. 23 | 24 | ## `pkg/nixhash` 25 | 26 | Methods to serialize and deserialize some of the hashes used in nix code and 27 | `.narinfo` files. 28 | 29 | ## `pkg/nar` 30 | 31 | A Nix ARchive (NAR) file Reader and Writer, with an interface similar to 32 | `archive/tar` from the stdlib, as well as a `DumpPath` method, which 33 | will assemble a NAR representation of a local file system path. 34 | 35 | ## `pkg/nar/ls` 36 | 37 | A parser for .ls files (providing an index for .nar files) 38 | 39 | ## `pkg/nar/narinfo` 40 | 41 | A parser and generator for `.narinfo` files. 42 | 43 | ## `pkg/nixbase32` 44 | 45 | An implementation of the slightly odd "base32" encoding that's used in Nix, 46 | providing some of the functions in `encoding/base32.Encoding`. 47 | 48 | ## `pkg/storepath` 49 | 50 | A parser and regexes for Nix Store Paths. 51 | 52 | ## `pkg/storepath/references` 53 | 54 | A Nix Store path reference scanner. 55 | 56 | ## `pkg/sqlite` 57 | 58 | A collection of interfaces and utilities for writing to and querying various `sqlite` databases that Nix uses. 59 | 60 | [sqlc]: https://github.com/sqlc-dev/sqlc 61 | 62 | ## `pkg/sqlite/binary_cache_v6` 63 | 64 | [SQLC] generated code for querying the Nar Info Disk Cache, typically located at `$XDG_CACHE_HOME/nix/binary-cache-v6.sqlite`. 65 | 66 | ## `pkg/sqlite/eval_cache_v5` 67 | 68 | [SQLC] generated code for querying an instance of the Eval Cache, typically located at `$XDG_CACHE_HOME/nix/eval-cache-v5/*.sqlite`. 69 | 70 | ## `pkg/sqlite/fetcher_cache_v2` 71 | 72 | [SQLC] generated code for querying the fetcher cache, typically located in `$XDG_CACHE_HOME/nix/fetcher-cache-v2.sqlite`. 73 | 74 | ## `pkg/sqlite/nix_v10` 75 | 76 | [SQLC] generated code for querying the main Nix database, typically located in `/nix/var/nix/db.sqlite`. 77 | 78 | ## `pkg/wire` 79 | 80 | Methods to parse and produce fields used in the low-level Nix wire protocol. 81 | -------------------------------------------------------------------------------- /cmd/gonix/drv/cmd.go: -------------------------------------------------------------------------------- 1 | package drv 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/nix-community/go-nix/pkg/derivation" 10 | ) 11 | 12 | type Cmd struct { 13 | DrvStore derivation.Store `kong:"type='drv-store-uri',default='',help='Path where derivations are read from.'"` 14 | 15 | Show ShowCmd `kong:"cmd,name='show',help='Show a derivation'"` 16 | } 17 | 18 | type ShowCmd struct { 19 | Drv string `kong:"arg,type='string',help='Path to the Derivation'"` 20 | Format string `kong:"default='json-pretty',help='The format to use to show (aterm,json-pretty,json)'"` 21 | } 22 | 23 | func (cmd *ShowCmd) Run(drvCmd *Cmd) error { 24 | drvStore := drvCmd.DrvStore 25 | 26 | drv, err := drvStore.Get(context.Background(), cmd.Drv) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | // Keep in mind `nix show-derivation` started sorting all of the JSON alphabetically, 32 | // while this still preserves the previous order of keys, as encoding/json 33 | // preserves struct element definition order when serializing. 34 | switch cmd.Format { 35 | case "json": 36 | enc := json.NewEncoder(os.Stdout) 37 | err = enc.Encode(drv) 38 | case "json-pretty": 39 | enc := json.NewEncoder(os.Stdout) 40 | enc.SetIndent("", " ") 41 | err = enc.Encode(drv) 42 | case "aterm": 43 | err = drv.WriteDerivation(os.Stdout) 44 | default: 45 | err = fmt.Errorf("invalid format: %v", cmd.Format) 46 | } 47 | 48 | if err != nil { 49 | return err 50 | } 51 | 52 | return nil 53 | } 54 | -------------------------------------------------------------------------------- /cmd/gonix/drv_store_uri_mapper.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "reflect" 6 | 7 | "github.com/alecthomas/kong" 8 | derivationStore "github.com/nix-community/go-nix/pkg/derivation/store" 9 | ) 10 | 11 | func drvStoreURIDecoder() kong.MapperFunc { 12 | return func(ctx *kong.DecodeContext, target reflect.Value) error { 13 | var drvStoreURI string 14 | 15 | err := ctx.Scan.PopValueInto("value", &drvStoreURI) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | drvStore, err := derivationStore.NewFromURI(drvStoreURI) 21 | if err != nil { 22 | return fmt.Errorf("error creating store from URI: %w", err) 23 | } 24 | 25 | target.Set(reflect.ValueOf(drvStore)) 26 | 27 | return nil 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /cmd/gonix/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/alecthomas/kong" 7 | "github.com/nix-community/go-nix/cmd/gonix/drv" 8 | "github.com/nix-community/go-nix/cmd/gonix/nar" 9 | ) 10 | 11 | //nolint:gochecknoglobals 12 | var cli struct { 13 | Nar nar.Cmd `kong:"cmd,name='nar',help='Create or inspect NAR files'"` 14 | Drv drv.Cmd `kong:"cmd,name='drv',help='Inspect NAR files'"` 15 | } 16 | 17 | func main() { 18 | parser, err := kong.New(&cli, kong.NamedMapper("drv-store-uri", drvStoreURIDecoder())) 19 | if err != nil { 20 | panic(err) 21 | } 22 | 23 | ctx, err := parser.Parse(os.Args[1:]) 24 | if err != nil { 25 | panic(err) 26 | } 27 | // Call the Run() method of the selected parsed command. 28 | err = ctx.Run() 29 | 30 | ctx.FatalIfErrorf(err) 31 | } 32 | -------------------------------------------------------------------------------- /cmd/gonix/nar/cat.go: -------------------------------------------------------------------------------- 1 | package nar 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "os" 8 | 9 | "github.com/nix-community/go-nix/pkg/nar" 10 | ) 11 | 12 | type CatCmd struct { 13 | Nar string `kong:"arg,type='existingfile',help='Path to the NAR'"` 14 | Path string `kong:"arg,type='string',help='Path inside the NAR, starting with \"/\".'"` 15 | } 16 | 17 | func (cmd *CatCmd) Run() error { 18 | f, err := os.Open(cmd.Nar) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | nr, err := nar.NewReader(f) 24 | if err != nil { 25 | return err 26 | } 27 | 28 | for { 29 | hdr, err := nr.Next() 30 | if err != nil { 31 | // io.EOF means we didn't find the requested path 32 | if err == io.EOF { 33 | return fmt.Errorf("requested path not found") 34 | } 35 | // relay other errors 36 | return err 37 | } 38 | 39 | if hdr.Path == cmd.Path { 40 | // we can't cat directories and symlinks 41 | if hdr.Type != nar.TypeRegular { 42 | return fmt.Errorf("unable to cat non-regular file") 43 | } 44 | 45 | w := bufio.NewWriter(os.Stdout) 46 | 47 | _, err := io.Copy(w, nr) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | return w.Flush() 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /cmd/gonix/nar/cmd.go: -------------------------------------------------------------------------------- 1 | package nar 2 | 3 | type Cmd struct { 4 | Cat CatCmd `kong:"cmd,name='cat',help='Print the contents of a file inside a NAR file'"` 5 | DumpPath DumpPathCmd `kong:"cmd,name='dump-path',help='Serialise a path to stdout in NAR format'"` 6 | Ls LsCmd `kong:"cmd,name='ls',help='Show information about a path inside a NAR file'"` 7 | } 8 | -------------------------------------------------------------------------------- /cmd/gonix/nar/dump.go: -------------------------------------------------------------------------------- 1 | package nar 2 | 3 | import ( 4 | "bufio" 5 | "os" 6 | 7 | "github.com/nix-community/go-nix/pkg/nar" 8 | ) 9 | 10 | type DumpPathCmd struct { 11 | Path string `kong:"arg,type:'path',help:'The path to dump'"` 12 | } 13 | 14 | func (cmd *DumpPathCmd) Run() error { 15 | // grab stdout 16 | w := bufio.NewWriter(os.Stdout) 17 | 18 | err := nar.DumpPath(w, cmd.Path) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | return w.Flush() 24 | } 25 | -------------------------------------------------------------------------------- /cmd/gonix/nar/ls.go: -------------------------------------------------------------------------------- 1 | package nar 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "strings" 8 | 9 | "github.com/nix-community/go-nix/pkg/nar" 10 | ) 11 | 12 | type LsCmd struct { 13 | Nar string `kong:"arg,type:'existingfile',help='Path to the NAR'"` 14 | Path string `kong:"arg,optional,type='string',default='/',help='Path inside the NAR. Defaults to \"/\".'"` 15 | Recursive bool `kong:"short='R',help='Whether to list recursively, or only the current level.'"` 16 | } 17 | 18 | // headerLineString returns a one-line string describing a header. 19 | // hdr.Validate() is assumed to be true. 20 | func headerLineString(hdr *nar.Header) string { 21 | var sb strings.Builder 22 | 23 | sb.WriteString(hdr.FileInfo().Mode().String()) 24 | sb.WriteString(" ") 25 | sb.WriteString(hdr.Path) 26 | 27 | // if regular file, show size in parantheses. We don't bother about aligning it nicely, 28 | // as that'd require reading in all headers first before printing them out. 29 | if hdr.Size > 0 { 30 | sb.WriteString(fmt.Sprintf(" (%v bytes)", hdr.Size)) 31 | } 32 | 33 | // if LinkTarget, show it 34 | if hdr.LinkTarget != "" { 35 | sb.WriteString(" -> ") 36 | sb.WriteString(hdr.LinkTarget) 37 | } 38 | 39 | sb.WriteString("\n") 40 | 41 | return sb.String() 42 | } 43 | 44 | func (cmd *LsCmd) Run() error { 45 | f, err := os.Open(cmd.Nar) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | nr, err := nar.NewReader(f) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | for { 56 | hdr, err := nr.Next() 57 | if err != nil { 58 | // io.EOF means we're done 59 | if err == io.EOF { 60 | return nil 61 | } 62 | // relay other errors 63 | return err 64 | } 65 | 66 | // if the yielded path starts with the path specified 67 | if strings.HasPrefix(hdr.Path, cmd.Path) { 68 | remainder := hdr.Path[len(cmd.Path):] 69 | // If recursive was requested, return all these elements. 70 | // Else, look at the remainder - There may be no other slashes. 71 | if cmd.Recursive || !strings.Contains(remainder, "/") { 72 | // fmt.Printf("%v type %v\n", hdr.Type, hdr.Path) 73 | print(headerLineString(hdr)) 74 | } 75 | } else { 76 | // We can exit early as soon as we receive a header whose path doesn't have the prefix we're searching for, 77 | // and the path is lexicographically bigger than our search prefix 78 | if hdr.Path > cmd.Path { 79 | return nil 80 | } 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /default.nix: -------------------------------------------------------------------------------- 1 | # This file provides backward compatibility to nix < 2.4 clients 2 | { 3 | system ? builtins.currentSystem, 4 | }: 5 | let 6 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 7 | 8 | root = lock.nodes.${lock.root}; 9 | inherit (lock.nodes.${root.inputs.flake-compat}.locked) 10 | owner 11 | repo 12 | rev 13 | narHash 14 | ; 15 | 16 | flake-compat = fetchTarball { 17 | url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; 18 | sha256 = narHash; 19 | }; 20 | 21 | flake = import flake-compat { 22 | inherit system; 23 | src = ./.; 24 | }; 25 | in 26 | flake.defaultNix 27 | -------------------------------------------------------------------------------- /devshell.nix: -------------------------------------------------------------------------------- 1 | { 2 | perSystem, 3 | pkgs, 4 | ... 5 | }: 6 | pkgs.mkShell { 7 | env.GOROOT = "${pkgs.go}/share/go"; 8 | 9 | packages = 10 | (with pkgs; [ 11 | delve 12 | pprof 13 | go 14 | gotools 15 | golangci-lint 16 | lazysql 17 | sqlc 18 | ]) 19 | ++ (with perSystem; [ 20 | gomod2nix.default 21 | ]); 22 | } 23 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "blueprint": { 4 | "inputs": { 5 | "nixpkgs": [ 6 | "nixpkgs" 7 | ], 8 | "systems": [ 9 | "systems" 10 | ] 11 | }, 12 | "locked": { 13 | "lastModified": 1727084436, 14 | "narHash": "sha256-H5rbzYDlQD/lmTKvvfyohnhB+zdoZfykghjFHi2rS7o=", 15 | "owner": "numtide", 16 | "repo": "blueprint", 17 | "rev": "77e32417d97959e3d81d22211cba7c8ba44c0079", 18 | "type": "github" 19 | }, 20 | "original": { 21 | "owner": "numtide", 22 | "repo": "blueprint", 23 | "type": "github" 24 | } 25 | }, 26 | "flake-compat": { 27 | "locked": { 28 | "lastModified": 1717312683, 29 | "narHash": "sha256-FrlieJH50AuvagamEvWMIE6D2OAnERuDboFDYAED/dE=", 30 | "owner": "nix-community", 31 | "repo": "flake-compat", 32 | "rev": "38fd3954cf65ce6faf3d0d45cd26059e059f07ea", 33 | "type": "github" 34 | }, 35 | "original": { 36 | "owner": "nix-community", 37 | "repo": "flake-compat", 38 | "type": "github" 39 | } 40 | }, 41 | "flake-utils": { 42 | "inputs": { 43 | "systems": "systems" 44 | }, 45 | "locked": { 46 | "lastModified": 1726560853, 47 | "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=", 48 | "owner": "numtide", 49 | "repo": "flake-utils", 50 | "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a", 51 | "type": "github" 52 | }, 53 | "original": { 54 | "owner": "numtide", 55 | "repo": "flake-utils", 56 | "type": "github" 57 | } 58 | }, 59 | "gomod2nix": { 60 | "inputs": { 61 | "flake-utils": [ 62 | "flake-utils" 63 | ], 64 | "nixpkgs": [ 65 | "nixpkgs" 66 | ] 67 | }, 68 | "locked": { 69 | "lastModified": 1727668934, 70 | "narHash": "sha256-nPpQ/4k6Fjaq2CHNSdO6j1ikiuWApuk/S6lU6ISp5SQ=", 71 | "owner": "nix-community", 72 | "repo": "gomod2nix", 73 | "rev": "066e0dd2afde263f547cb0905b77cea00521d86c", 74 | "type": "github" 75 | }, 76 | "original": { 77 | "owner": "nix-community", 78 | "repo": "gomod2nix", 79 | "type": "github" 80 | } 81 | }, 82 | "nixpkgs": { 83 | "locked": { 84 | "lastModified": 1727348695, 85 | "narHash": "sha256-J+PeFKSDV+pHL7ukkfpVzCOO7mBSrrpJ3svwBFABbhI=", 86 | "owner": "nixos", 87 | "repo": "nixpkgs", 88 | "rev": "1925c603f17fc89f4c8f6bf6f631a802ad85d784", 89 | "type": "github" 90 | }, 91 | "original": { 92 | "owner": "nixos", 93 | "ref": "nixos-unstable", 94 | "repo": "nixpkgs", 95 | "type": "github" 96 | } 97 | }, 98 | "root": { 99 | "inputs": { 100 | "blueprint": "blueprint", 101 | "flake-compat": "flake-compat", 102 | "flake-utils": "flake-utils", 103 | "gomod2nix": "gomod2nix", 104 | "nixpkgs": "nixpkgs", 105 | "systems": "systems_2", 106 | "treefmt-nix": "treefmt-nix" 107 | } 108 | }, 109 | "systems": { 110 | "locked": { 111 | "lastModified": 1681028828, 112 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 113 | "owner": "nix-systems", 114 | "repo": "default", 115 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 116 | "type": "github" 117 | }, 118 | "original": { 119 | "owner": "nix-systems", 120 | "repo": "default", 121 | "type": "github" 122 | } 123 | }, 124 | "systems_2": { 125 | "locked": { 126 | "lastModified": 1681028828, 127 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 128 | "owner": "nix-systems", 129 | "repo": "default", 130 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 131 | "type": "github" 132 | }, 133 | "original": { 134 | "owner": "nix-systems", 135 | "repo": "default", 136 | "type": "github" 137 | } 138 | }, 139 | "treefmt-nix": { 140 | "inputs": { 141 | "nixpkgs": [ 142 | "nixpkgs" 143 | ] 144 | }, 145 | "locked": { 146 | "lastModified": 1727431250, 147 | "narHash": "sha256-uGRlRT47ecicF9iLD1G3g43jn2e+b5KaMptb59LHnvM=", 148 | "owner": "numtide", 149 | "repo": "treefmt-nix", 150 | "rev": "879b29ae9a0378904fbbefe0dadaed43c8905754", 151 | "type": "github" 152 | }, 153 | "original": { 154 | "owner": "numtide", 155 | "repo": "treefmt-nix", 156 | "type": "github" 157 | } 158 | } 159 | }, 160 | "root": "root", 161 | "version": 7 162 | } 163 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Elements of Nix re-implemented as Go libraries"; 3 | 4 | inputs = { 5 | blueprint = { 6 | url = "github:numtide/blueprint"; 7 | inputs.nixpkgs.follows = "nixpkgs"; 8 | inputs.systems.follows = "systems"; 9 | }; 10 | gomod2nix = { 11 | url = "github:nix-community/gomod2nix"; 12 | inputs.nixpkgs.follows = "nixpkgs"; 13 | inputs.flake-utils.follows = "flake-utils"; 14 | }; 15 | systems.url = "github:nix-systems/default"; 16 | nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; 17 | flake-utils.url = "github:numtide/flake-utils"; 18 | treefmt-nix = { 19 | url = "github:numtide/treefmt-nix"; 20 | inputs.nixpkgs.follows = "nixpkgs"; 21 | }; 22 | flake-compat.url = "github:nix-community/flake-compat"; 23 | }; 24 | 25 | # Keep the magic invocations to minimum. 26 | outputs = 27 | inputs: 28 | inputs.blueprint { 29 | inherit inputs; 30 | }; 31 | } 32 | -------------------------------------------------------------------------------- /formatter.nix: -------------------------------------------------------------------------------- 1 | { 2 | pkgs, 3 | inputs, 4 | ... 5 | }: 6 | inputs.treefmt-nix.lib.mkWrapper pkgs { 7 | projectRootFile = ".git/config"; 8 | 9 | programs = { 10 | nixfmt.enable = true; 11 | deadnix.enable = true; 12 | gofumpt.enable = true; 13 | prettier.enable = true; 14 | statix.enable = true; 15 | }; 16 | 17 | settings = { 18 | global.excludes = [ 19 | "LICENSE" 20 | ".gitattributes" 21 | "test/testdata/*" 22 | # unsupported extensions 23 | "*.{gif,png,svg,tape,mts,lock,mod,sum,toml,env,envrc,gitignore,sql}" 24 | ]; 25 | 26 | formatter = { 27 | deadnix.priority = 1; 28 | statix.priority = 2; 29 | nixfmt.priority = 3; 30 | 31 | prettier = { 32 | options = [ 33 | "--tab-width" 34 | "4" 35 | ]; 36 | includes = [ "*.{css,html,js,json,jsx,md,mdx,scss,ts,yaml}" ]; 37 | }; 38 | }; 39 | }; 40 | } 41 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nix-community/go-nix 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/adrg/xdg v0.5.0 7 | github.com/alecthomas/kong v0.5.0 8 | github.com/dgraph-io/badger/v3 v3.2103.2 9 | github.com/mattn/go-sqlite3 v1.14.23 10 | github.com/multiformats/go-multihash v0.2.1 11 | github.com/nsf/jsondiff v0.0.0-20210926074059-1e845ec5d249 12 | github.com/stretchr/testify v1.9.0 13 | ) 14 | 15 | require ( 16 | github.com/cespare/xxhash v1.1.0 // indirect 17 | github.com/cespare/xxhash/v2 v2.1.1 // indirect 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/dgraph-io/ristretto v0.1.0 // indirect 20 | github.com/dustin/go-humanize v1.0.0 // indirect 21 | github.com/gogo/protobuf v1.3.2 // indirect 22 | github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b // indirect 23 | github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6 // indirect 24 | github.com/golang/protobuf v1.3.1 // indirect 25 | github.com/golang/snappy v0.0.3 // indirect 26 | github.com/google/flatbuffers v1.12.1 // indirect 27 | github.com/klauspost/compress v1.12.3 // indirect 28 | github.com/klauspost/cpuid/v2 v2.0.9 // indirect 29 | github.com/minio/sha256-simd v1.0.0 // indirect 30 | github.com/mr-tron/base58 v1.2.0 // indirect 31 | github.com/multiformats/go-varint v0.0.6 // indirect 32 | github.com/pkg/errors v0.9.1 // indirect 33 | github.com/pmezard/go-difflib v1.0.0 // indirect 34 | github.com/spaolacci/murmur3 v1.1.0 // indirect 35 | go.opencensus.io v0.22.5 // indirect 36 | golang.org/x/crypto v0.17.0 // indirect 37 | golang.org/x/net v0.17.0 // indirect 38 | golang.org/x/sys v0.22.0 // indirect 39 | gopkg.in/yaml.v3 v3.0.1 // indirect 40 | lukechampine.com/blake3 v1.1.6 // indirect 41 | ) 42 | -------------------------------------------------------------------------------- /pkg/derivation/derivation.go: -------------------------------------------------------------------------------- 1 | package derivation 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/nix-community/go-nix/pkg/storepath" 7 | ) 8 | 9 | // Derivation describes all data in a .drv, which canonically is expressed in ATerm format. 10 | // Nix requires some stronger properties w.r.t. order of elements, so we can internally use 11 | // maps for some of the fields, and convert to the canonical representation when encoding back 12 | // to ATerm format. 13 | // The field names (and order of fields) also match the json structure 14 | // that the `nix show-derivation /path/to.drv` is using, 15 | // even though this might change in the future. 16 | type Derivation struct { 17 | // Structured don't have the env name right in the regular spot but in the nested JSON object. 18 | // This is an internal variable only used for structured attrs derivations, which can currently only be created 19 | // from an existing drv file. 20 | name string 21 | 22 | // Outputs are always lexicographically sorted by their name (key in this map) 23 | Outputs map[string]*Output `json:"outputs"` 24 | 25 | // InputSources are always lexicographically sorted. 26 | InputSources []string `json:"inputSrcs"` 27 | 28 | // InputDerivations are always lexicographically sorted by their path (key in this map) 29 | // the []string returns the output names (out, …) of this input derivation that are used. 30 | InputDerivations map[string][]string `json:"inputDrvs"` 31 | 32 | Platform string `json:"system"` 33 | 34 | Builder string `json:"builder"` 35 | 36 | Arguments []string `json:"args"` 37 | 38 | // Env must be lexicographically sorted by their key. 39 | Env map[string]string `json:"env"` 40 | } 41 | 42 | func (d *Derivation) Validate() error { 43 | numberOfOutputs := len(d.Outputs) 44 | 45 | if numberOfOutputs == 0 { 46 | return fmt.Errorf("at least one output must be defined") 47 | } 48 | 49 | for outputName, output := range d.Outputs { 50 | if outputName == "" { 51 | return fmt.Errorf("empty output name") 52 | } 53 | 54 | // TODO: are there more restrictions on output names? 55 | 56 | // we encountered a fixed-output output 57 | // In these derivations, there may be only one output, 58 | // which needs to be called out 59 | if output.HashAlgorithm != "" { 60 | if numberOfOutputs != 1 { 61 | return fmt.Errorf("encountered fixed-output, but there's more than 1 output in total") 62 | } 63 | 64 | if outputName != "out" { 65 | return fmt.Errorf("the fixed-output output name must be called 'out'") 66 | } 67 | 68 | // we confirmed above there's only one output, so we're done with the loop 69 | break 70 | } 71 | 72 | err := output.Validate() 73 | if err != nil { 74 | return fmt.Errorf("error validating output '%s': %w", outputName, err) 75 | } 76 | } 77 | 78 | for inputDerivationPath := range d.InputDerivations { 79 | err := storepath.Validate(inputDerivationPath) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | outputNames := d.InputDerivations[inputDerivationPath] 85 | if len(outputNames) == 0 { 86 | return fmt.Errorf("output names list for '%s' empty", inputDerivationPath) 87 | } 88 | 89 | for i, o := range outputNames { 90 | if i > 0 && o < outputNames[i-1] { 91 | return fmt.Errorf("invalid input derivation output order: %s < %s", o, outputNames[i-1]) 92 | } 93 | 94 | if o == "" { 95 | return fmt.Errorf("Output name entry for '%s' empty", inputDerivationPath) 96 | } 97 | } 98 | } 99 | 100 | for i, is := range d.InputSources { 101 | err := storepath.Validate(is) 102 | if err != nil { 103 | return fmt.Errorf("error validating input source '%s': %w", is, err) 104 | } 105 | 106 | if i > 0 && is < d.InputSources[i-1] { 107 | return fmt.Errorf("invalid input source order: %s < %s", is, d.InputSources[i-1]) 108 | } 109 | } 110 | 111 | if d.Platform == "" { 112 | return fmt.Errorf("required attribute 'platform' missing") 113 | } 114 | 115 | if d.Builder == "" { 116 | return fmt.Errorf("required attribute 'builder' missing") 117 | } 118 | 119 | // there has to be an env variable with key `name`. 120 | hasNameEnv := false 121 | 122 | for k := range d.Env { 123 | if k == "" { 124 | return fmt.Errorf("empty environment variable key") 125 | } 126 | 127 | if k == "name" { 128 | hasNameEnv = true 129 | } 130 | 131 | // Structured attrs 132 | if k == "__json" { 133 | hasNameEnv = d.name != "" 134 | } 135 | } 136 | 137 | if !hasNameEnv { 138 | return fmt.Errorf("env 'name' not found") 139 | } 140 | 141 | return nil 142 | } 143 | 144 | func (d *Derivation) Name() string { 145 | if _, ok := d.Env["__json"]; ok { 146 | return d.name 147 | } 148 | 149 | name, ok := d.Env["name"] 150 | if ok { 151 | return name 152 | } 153 | 154 | return "" 155 | } 156 | -------------------------------------------------------------------------------- /pkg/derivation/drv_path.go: -------------------------------------------------------------------------------- 1 | package derivation 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "sort" 7 | 8 | "github.com/nix-community/go-nix/pkg/nixhash" 9 | "github.com/nix-community/go-nix/pkg/storepath" 10 | ) 11 | 12 | //nolint:gochecknoglobals 13 | var ( 14 | textColon = []byte("text:") 15 | sha256Colon = []byte("sha256:") 16 | storeDirColon = []byte(storepath.StoreDir + ":") 17 | dotDrv = []byte(".drv") 18 | ) 19 | 20 | // Returns the path of a Derivation struct, or an error. 21 | // The path is calculated like this: 22 | // - Write the fingerprint of the Derivation to the sha256 hash function. 23 | // This is: `text:`, 24 | // all d.InputDerivations and d.InputSources (sorted, separated by a `:`), 25 | // a `:`, 26 | // a `sha256:`, followed by the sha256 digest of the ATerm representation (hex-encoded) 27 | // a `:`, 28 | // the storeDir, followed by a `:`, 29 | // the name of a derivation, 30 | // a `.drv`. 31 | // - Write the .drv A-Term contents to a hash function 32 | // - Take the digest, run hash.CompressHash(digest, 20) on it. 33 | // - Encode it with nixbase32 34 | // - Construct the full path $storeDir/$nixbase32EncodedCompressedHash-$name.drv 35 | func (d *Derivation) DrvPath() (string, error) { 36 | // calculate the sha256 digest of the ATerm representation 37 | h := sha256.New() 38 | 39 | if err := d.WriteDerivation(h); err != nil { 40 | return "", err 41 | } 42 | 43 | // store the atermDigest, we'll use it later 44 | atermDigest := h.Sum(nil) 45 | 46 | // reset the sha256 calculator 47 | h.Reset() 48 | 49 | h.Write(textColon) 50 | 51 | // Write references (lexicographically ordered) 52 | { 53 | references := make([]string, len(d.InputDerivations)+len(d.InputSources)) 54 | 55 | n := 0 56 | 57 | for inputDrvPath := range d.InputDerivations { 58 | references[n] = inputDrvPath 59 | n++ 60 | } 61 | 62 | for _, inputSrc := range d.InputSources { 63 | references[n] = inputSrc 64 | n++ 65 | } 66 | 67 | sort.Strings(references) 68 | 69 | for _, ref := range references { 70 | h.Write(unsafeBytes(ref)) 71 | h.Write(colon) 72 | } 73 | } 74 | 75 | h.Write(sha256Colon) 76 | 77 | { 78 | encoded := make([]byte, hex.EncodedLen(sha256.Size)) 79 | hex.Encode(encoded, atermDigest) 80 | h.Write(encoded) 81 | } 82 | 83 | h.Write(colon) 84 | h.Write(storeDirColon) 85 | 86 | name := d.Name() 87 | if name == "" { 88 | // asserted by Validate 89 | panic("env 'name' not found") 90 | } 91 | 92 | h.Write(unsafeBytes(name)) 93 | h.Write(dotDrv) 94 | 95 | atermDigest = h.Sum(nil) 96 | 97 | np := storepath.StorePath{ 98 | Name: name + ".drv", 99 | Digest: nixhash.CompressHash(atermDigest, 20), 100 | } 101 | 102 | return np.Absolute(), nil 103 | } 104 | -------------------------------------------------------------------------------- /pkg/derivation/hashes.go: -------------------------------------------------------------------------------- 1 | package derivation 2 | 3 | import ( 4 | "crypto/sha256" 5 | "encoding/hex" 6 | "fmt" 7 | chash "hash" 8 | 9 | "github.com/nix-community/go-nix/pkg/nixhash" 10 | "github.com/nix-community/go-nix/pkg/storepath" 11 | ) 12 | 13 | //nolint:gochecknoglobals 14 | var colon = []byte{':'} 15 | 16 | // getMaskedATermHash returns the hex-representation of 17 | // In case the Derivation is not just a fixed-output derivation, 18 | // calculating the output hashes includes all inputs derivations. 19 | // 20 | // This is done by hashing a special ATerm variant. 21 | // In this variant, all output paths, and environment variables 22 | // named like output names are set to an empty string, 23 | // aka "not calculated yet". 24 | // 25 | // Input derivation are replaced with a hex-replacement string, 26 | // which is calculated by CalculateDrvReplacement, 27 | // but passed in as a map here (we don't want to always recurse, but precompute). 28 | func (d *Derivation) getMaskedATermHash(inputDrvReplacements map[string]string) (string, error) { 29 | h := sha256.New() 30 | 31 | err := d.writeDerivation(h, true, inputDrvReplacements) 32 | if err != nil { 33 | return "", fmt.Errorf("error writing masked ATerm: %w", err) 34 | } 35 | 36 | return hex.EncodeToString(h.Sum(nil)), nil 37 | } 38 | 39 | func hashStrings(h chash.Hash, strings ...string) []byte { 40 | h.Write(unsafeBytes(strings[0])) 41 | 42 | for _, s := range strings[1:] { 43 | h.Write(colon) 44 | h.Write(unsafeBytes(s)) 45 | } 46 | 47 | return h.Sum(nil) 48 | } 49 | 50 | // CalculateOutputPaths calculates the output paths of all outputs 51 | // It consumes a list of input derivation path replacements. 52 | func (d *Derivation) CalculateOutputPaths(inputDrvReplacements map[string]string) (map[string]string, error) { 53 | derivationName := d.Name() 54 | 55 | if derivationName == "" { 56 | // asserted by Validate 57 | panic("env 'name' not found") 58 | } 59 | 60 | outputPaths := make(map[string]string, len(d.Outputs)) 61 | 62 | h := sha256.New() 63 | 64 | for outputName, o := range d.Outputs { 65 | // calculate the part of an output path that comes after the hash 66 | var outputPathName string 67 | if outputName == "out" { 68 | outputPathName = derivationName 69 | } else { 70 | outputPathName = derivationName + "-" + outputName 71 | } 72 | 73 | var storeHash []byte 74 | 75 | if o.HashAlgorithm != "" { 76 | // This code is _weird_ but it is what Nix is doing. See: 77 | // https://github.com/NixOS/nix/blob/1385b2007804c8a0370f2a6555045a00e34b07c7/src/libstore/store-api.cc#L178-L196 78 | if o.HashAlgorithm == "r:sha256" { 79 | storeHash = hashStrings( 80 | h, 81 | "source", 82 | "sha256", 83 | o.Hash, 84 | storepath.StoreDir, 85 | derivationName, 86 | ) 87 | } else { 88 | fixedHex := hex.EncodeToString(hashStrings(h, "fixed", "out", o.HashAlgorithm, o.Hash, "")) 89 | 90 | h.Reset() 91 | 92 | storeHash = hashStrings( 93 | h, 94 | "output", 95 | "out", 96 | "sha256", 97 | fixedHex, 98 | storepath.StoreDir, 99 | derivationName, 100 | ) 101 | } 102 | } else { 103 | maskedATermHash, err := d.getMaskedATermHash(inputDrvReplacements) 104 | if err != nil { 105 | return nil, fmt.Errorf("failed to calculate masked ATerm hash: %w", err) 106 | } 107 | 108 | storeHash = hashStrings( 109 | h, 110 | "output", 111 | outputName, 112 | "sha256", 113 | maskedATermHash, 114 | storepath.StoreDir, 115 | outputPathName, 116 | ) 117 | } 118 | 119 | calculatedPath := storepath.StorePath{ 120 | Name: outputPathName, 121 | Digest: nixhash.CompressHash(storeHash, 20), 122 | } 123 | 124 | outputPaths[outputName] = calculatedPath.Absolute() 125 | 126 | h.Reset() 127 | } 128 | 129 | return outputPaths, nil 130 | } 131 | 132 | // CalculateDrvReplacement calculates the hex-replacement string for a derivation. 133 | // When calculating output paths with Derivation.CalculateOutputPaths(), 134 | // for a non-fixed-output derivation, a map of replacements (each calculated by this function) 135 | // needs to be passed in. 136 | // 137 | // To calculate replacement strings of non-fixed-output derivations, 138 | // *their* input derivation replacements also need to be known - so 139 | // the calculation would be recursive. 140 | // 141 | // We solve this having calculateDrvReplacement accept a map of 142 | // /its/ replacements, instead of recursing. 143 | func (d *Derivation) CalculateDrvReplacement(inputDrvReplacements map[string]string) (string, error) { 144 | // Check if we're a fixed output 145 | if len(d.Outputs) == 1 { 146 | // Is it fixed output? 147 | if o, ok := d.Outputs["out"]; ok && o.HashAlgorithm != "" { 148 | return hex.EncodeToString(hashStrings( 149 | sha256.New(), 150 | "fixed", 151 | "out", 152 | o.HashAlgorithm, 153 | o.Hash, 154 | o.Path, 155 | )), nil 156 | } 157 | } 158 | 159 | h := sha256.New() 160 | 161 | err := d.writeDerivation(h, false, inputDrvReplacements) 162 | if err != nil { 163 | return "", fmt.Errorf("error hashing ATerm: %w", err) 164 | } 165 | 166 | return hex.EncodeToString(h.Sum(nil)), nil 167 | } 168 | -------------------------------------------------------------------------------- /pkg/derivation/helpers.go: -------------------------------------------------------------------------------- 1 | package derivation 2 | 3 | import ( 4 | "unsafe" 5 | ) 6 | 7 | // unsafeString takes a byte slice and returns it as a string. 8 | // Consider this method as passing ownership of the byte slice, 9 | // do not mutate it afterwards. 10 | func unsafeString(b []byte) string { 11 | return *(*string)(unsafe.Pointer(&b)) 12 | } 13 | 14 | // unsafeBytes returns the byte slice backing the string s. 15 | // It's safe to use in situations like hash calculations or 16 | // writing into buffers. 17 | func unsafeBytes(s string) []byte { 18 | return unsafe.Slice(unsafe.StringData(s), len(s)) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/derivation/json_test.go: -------------------------------------------------------------------------------- 1 | package derivation_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "io" 7 | "os" 8 | "path/filepath" 9 | "testing" 10 | 11 | "github.com/nix-community/go-nix/pkg/derivation" 12 | "github.com/nsf/jsondiff" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | // TestJSONSerialize serializes a Derivation to a JSON, 17 | // and verifies it matches what `nix show-derivation` shows. 18 | // As the Nix output uses the Derivation Path as a key, we 19 | // serialize the map instead. 20 | func TestJSONSerialize(t *testing.T) { 21 | drvs := []string{"0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv", "4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv"} 22 | 23 | for _, drvBasename := range drvs { 24 | container := make(map[string]*derivation.Derivation) 25 | 26 | derivationFile, err := os.Open(filepath.FromSlash("../../test/testdata/" + drvBasename)) 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | derivationBytes, err := io.ReadAll(derivationFile) 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | drv, err := derivation.ReadDerivation(bytes.NewReader(derivationBytes)) 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | drvPath, err := drv.DrvPath() 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | container[drvPath] = drv 47 | 48 | var buf bytes.Buffer 49 | 50 | enc := json.NewEncoder(&buf) 51 | enc.SetIndent("", " ") 52 | 53 | err = enc.Encode(container) 54 | assert.NoError(t, err, "encoding a derivation to JSON shouldn't error") 55 | 56 | // compare the output with the prerecorded json output 57 | derivationJSONFile, err := os.Open(filepath.FromSlash("../../test/testdata/" + drvBasename + ".json")) 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | derivationJSONBytes, err := io.ReadAll(derivationJSONFile) 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | // encoding/json serializes in struct key definition order, not alphabetic. 68 | // So we can't just compare the raw bytes unfortunately. 69 | diffOpts := jsondiff.DefaultConsoleOptions() 70 | 71 | diff, str := jsondiff.Compare(derivationJSONBytes, buf.Bytes(), &diffOpts) 72 | 73 | assert.Equal(t, jsondiff.FullMatch, diff, "produced json should be equal") 74 | 75 | if diff != jsondiff.FullMatch { 76 | panic(str) 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /pkg/derivation/output.go: -------------------------------------------------------------------------------- 1 | package derivation 2 | 3 | import ( 4 | "github.com/nix-community/go-nix/pkg/storepath" 5 | ) 6 | 7 | type Output struct { 8 | Path string `json:"path"` 9 | HashAlgorithm string `json:"hashAlgo,omitempty"` 10 | Hash string `json:"hash,omitempty"` 11 | } 12 | 13 | func (o *Output) Validate() error { 14 | return storepath.Validate(o.Path) 15 | } 16 | -------------------------------------------------------------------------------- /pkg/derivation/store.go: -------------------------------------------------------------------------------- 1 | package derivation 2 | 3 | import "context" 4 | 5 | // Store describes the interface a Derivation store needs to implement 6 | // to be used from here. 7 | // Note we use pointers to Derivation structs here, so be careful modifying these. 8 | // Look in the store/ subfolder for implementations. 9 | type Store interface { 10 | // Put inserts a new Derivation into the Derivation Store. 11 | // All referred derivation paths should have been Put() before. 12 | // The resulting derivation path is returned, or an error. 13 | Put(context.Context, *Derivation) (string, error) 14 | 15 | // Get retrieves a derivation by drv path. 16 | // The second return argument specifies if the derivation could be found, 17 | // similar to how acessing from a map works. 18 | Get(context.Context, string) (*Derivation, error) 19 | 20 | // Has returns whether the derivation (by drv path) exists. 21 | Has(context.Context, string) (bool, error) 22 | 23 | // Close closes the store. 24 | Close() error 25 | } 26 | -------------------------------------------------------------------------------- /pkg/derivation/store/badger.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | 8 | badger "github.com/dgraph-io/badger/v3" 9 | "github.com/nix-community/go-nix/pkg/derivation" 10 | ) 11 | 12 | var _ derivation.Store = &BadgerStore{} 13 | 14 | func buildDefaultBadgerOptions(path string) badger.Options { 15 | // set log level for badger to WARN, as it spams with INFO: 16 | // https://github.com/dgraph-io/badger/issues/556#issuecomment-536145162 17 | return badger.DefaultOptions(path).WithLoggingLevel(badger.WARNING) 18 | } 19 | 20 | // NewBadgerStore opens a store that stores its data 21 | // in the path specified by path. 22 | func NewBadgerStore(path string) (*BadgerStore, error) { 23 | db, err := badger.Open(buildDefaultBadgerOptions(path)) 24 | if err != nil { 25 | return nil, fmt.Errorf("error opening badger store: %w", err) 26 | } 27 | 28 | return &BadgerStore{ 29 | db: db, 30 | }, nil 31 | } 32 | 33 | // NewBadgerMemoryStore opens a store that entirely resides in memory. 34 | func NewBadgerMemoryStore() (*BadgerStore, error) { 35 | db, err := badger.Open(buildDefaultBadgerOptions("").WithInMemory(true)) 36 | if err != nil { 37 | return nil, fmt.Errorf("error opening badger store: %w", err) 38 | } 39 | 40 | return &BadgerStore{ 41 | db: db, 42 | }, nil 43 | } 44 | 45 | // BadgerStore stores data using badger. 46 | // All derivations are stored in ATerm format, at `drv:$drvPath`. 47 | // The replacement string for a drv is stored at `replacement:$drvPath`. 48 | // The interface should be thread-safe. 49 | type BadgerStore struct { 50 | db *badger.DB 51 | } 52 | 53 | // Put inserts a new Derivation into the Derivation Store. 54 | func (bs *BadgerStore) Put(ctx context.Context, drv *derivation.Derivation) (string, error) { 55 | if err := validateDerivationInStore(ctx, drv, bs); err != nil { 56 | return "", err 57 | } 58 | 59 | drvReplacements := make(map[string]string, len(drv.InputDerivations)) 60 | 61 | if len(drv.InputDerivations) > 0 { 62 | err := bs.db.View(func(txn *badger.Txn) error { 63 | for inputDrvPath := range drv.InputDerivations { 64 | item, err := txn.Get([]byte("replacement:" + inputDrvPath)) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | return item.Value(func(val []byte) error { 70 | // store the replacement string in drvReplacements 71 | drvReplacements[inputDrvPath] = string(val) 72 | 73 | return nil 74 | }) 75 | } 76 | 77 | return nil 78 | }) 79 | if err != nil { 80 | return "", fmt.Errorf("unable to get input derivations: %w", err) 81 | } 82 | 83 | if err != nil { 84 | return "", fmt.Errorf("error retrieving replacements: %w", err) 85 | } 86 | } 87 | 88 | if err := checkOutputPaths(drv, drvReplacements); err != nil { 89 | return "", err 90 | } 91 | 92 | // Calculate the drv path of the drv we're about to insert 93 | drvPath, err := drv.DrvPath() 94 | if err != nil { 95 | return "", err 96 | } 97 | 98 | // serialize the derivation to ATerm 99 | var buf bytes.Buffer 100 | 101 | err = drv.WriteDerivation(&buf) 102 | if err != nil { 103 | return "", err 104 | } 105 | 106 | // create a transaction 107 | err = bs.db.Update(func(txn *badger.Txn) error { 108 | // store derivation itself 109 | drvEntry := badger.NewEntry([]byte("drv:"+drvPath), buf.Bytes()) 110 | 111 | err := txn.SetEntry(drvEntry) 112 | if err != nil { 113 | return fmt.Errorf("unable to store derivation: %w", err) 114 | } 115 | 116 | // calculate replacement string 117 | drvReplacement, err := drv.CalculateDrvReplacement(drvReplacements) 118 | if err != nil { 119 | return fmt.Errorf("unable to calculate replacement string: %w", err) 120 | } 121 | 122 | // Store replacement string 123 | replacementEntry := badger.NewEntry([]byte("replacement:"+drvPath), []byte(drvReplacement)) 124 | 125 | err = txn.SetEntry(replacementEntry) 126 | if err != nil { 127 | return fmt.Errorf("unable to store replacement string: %w", err) 128 | } 129 | 130 | return nil 131 | }) 132 | if err != nil { 133 | return "", err 134 | } 135 | 136 | return drvPath, nil 137 | } 138 | 139 | // Get retrieves a Derivation by drv path from the Derivation Store. 140 | func (bs *BadgerStore) Get(_ context.Context, derivationPath string) (*derivation.Derivation, error) { 141 | var drv *derivation.Derivation 142 | 143 | err := bs.db.View(func(txn *badger.Txn) error { 144 | item, err := txn.Get([]byte("drv:" + derivationPath)) 145 | if err != nil { 146 | return err 147 | } 148 | 149 | return item.Value(func(val []byte) error { 150 | // parse the derivation from ATerm, store it in drv 151 | drv, err = derivation.ReadDerivation(bytes.NewReader(val)) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | return nil 157 | }) 158 | }) 159 | if err != nil { 160 | if err == badger.ErrKeyNotFound { 161 | return nil, fmt.Errorf("derivation path not found: %s", derivationPath) 162 | } 163 | 164 | return nil, err 165 | } 166 | 167 | return drv, nil 168 | } 169 | 170 | // Has returns whether the derivation (by drv path) exists. 171 | // This is done by using the Badger iterator with ValidForPrefix. 172 | func (bs *BadgerStore) Has(_ context.Context, derivationPath string) (bool, error) { 173 | found := false 174 | 175 | err := bs.db.View(func(txn *badger.Txn) error { 176 | opts := badger.DefaultIteratorOptions 177 | opts.PrefetchValues = false 178 | 179 | it := txn.NewIterator(opts) 180 | defer it.Close() 181 | 182 | key := []byte("drv:" + derivationPath) 183 | 184 | for it.Seek(key); it.Valid(); it.Next() { 185 | item := it.Item() 186 | k := item.Key() 187 | 188 | if bytes.Equal(k, key) { 189 | found = true 190 | 191 | break 192 | } 193 | } 194 | 195 | return nil 196 | }) 197 | if err != nil { 198 | return false, fmt.Errorf("unable to check if we have a derivation: %w", err) 199 | } 200 | 201 | return found, nil 202 | } 203 | 204 | // Close closes the store. 205 | func (bs *BadgerStore) Close() error { 206 | return bs.db.Close() 207 | } 208 | -------------------------------------------------------------------------------- /pkg/derivation/store/filesystem.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | 11 | "github.com/nix-community/go-nix/pkg/derivation" 12 | "github.com/nix-community/go-nix/pkg/storepath" 13 | ) 14 | 15 | // FSStore implements derivation.Store. 16 | var _ derivation.Store = &FSStore{} 17 | 18 | // NewFSStore returns a store exposing all `.drv` files in the directory 19 | // specified by storageDir. 20 | // If storageDir is set to an empty string, storepath.StoreDir is used as a directory. 21 | func NewFSStore(storageDir string) (*FSStore, error) { 22 | if storageDir == "" { 23 | storageDir = storepath.StoreDir 24 | } 25 | 26 | return &FSStore{ 27 | StorageDir: storageDir, 28 | }, nil 29 | } 30 | 31 | // FSStore provides a derivation.Store interface, 32 | // that exposes all .drv files in a given folder. 33 | // These files need to be regular files, not symlinks. 34 | // It doesn't do any output path validation and consistency checks, 35 | // meaning you usually want to wrap this in a validating store. 36 | // Right now, Put() is not implemented. 37 | type FSStore struct { 38 | // The path containing the .drv files on disk 39 | StorageDir string 40 | } 41 | 42 | // Put is not implemented right now. 43 | func (fs *FSStore) Put(_ context.Context, _ *derivation.Derivation) (string, error) { 44 | return "", fmt.Errorf("not implemented") 45 | } 46 | 47 | // getFilepath returns the path to a .drv file, 48 | // with respect to the configured StorageDir. 49 | func (fs *FSStore) getFilepath(derivationPath string) string { 50 | return filepath.Join(fs.StorageDir, path.Base(derivationPath)) 51 | } 52 | 53 | // Get retrieves a Derivation by drv path from the Derivation Store. 54 | func (fs *FSStore) Get(_ context.Context, derivationPath string) (*derivation.Derivation, error) { 55 | path := fs.getFilepath(derivationPath) 56 | 57 | f, err := os.Open(path) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | drv, err := derivation.ReadDerivation(f) 63 | if err != nil { 64 | return nil, fmt.Errorf("unable to parse derivation: %w", err) 65 | } 66 | 67 | return drv, nil 68 | } 69 | 70 | // Has returns whether the derivation (by drv path) exists. 71 | // We only need pass this down to the cache, as everything 72 | // we did Get() is stored in there. 73 | func (fs *FSStore) Has(_ context.Context, derivationPath string) (bool, error) { 74 | path := fs.getFilepath(derivationPath) 75 | 76 | // Stat the file. We do an lstat here, to not follow symlinks. 77 | fi, err := os.Lstat(path) 78 | if err != nil { 79 | // if stat returns os.ErrNotExits, this means the file doesn't exist. 80 | if errors.Is(err, os.ErrNotExist) { 81 | return false, nil 82 | } 83 | 84 | // in all other cases, stat returned an error. 85 | return false, fmt.Errorf("unable to stat %s: %w", path, err) 86 | } 87 | 88 | // We already have `fi`, so do a quick check the file is regular. 89 | if isRegular := fi.Mode().IsRegular(); !isRegular { 90 | return false, fmt.Errorf("file at %s is not regular", path) 91 | } 92 | 93 | // otherwise, assume it's fine. 94 | return true, nil 95 | } 96 | 97 | // Close is a no-op. 98 | func (fs *FSStore) Close() error { 99 | return nil 100 | } 101 | -------------------------------------------------------------------------------- /pkg/derivation/store/filesystem_test.go: -------------------------------------------------------------------------------- 1 | package store_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/nix-community/go-nix/pkg/derivation/store" 8 | "github.com/nix-community/go-nix/pkg/storepath" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestFSStore(t *testing.T) { 13 | cases := []struct { 14 | Title string 15 | DerivationFile string 16 | }{ 17 | { 18 | Title: "fixed-sha256", 19 | DerivationFile: "0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv", 20 | }, 21 | { 22 | // Has a single fixed-output dependency 23 | Title: "simple-sha256", 24 | DerivationFile: "4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv", 25 | }, 26 | } 27 | 28 | // Initialize the FSStore 29 | drvStore, err := store.NewFSStore("../../../test/testdata/") 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | for _, c := range cases { 35 | t.Run(c.Title, func(t *testing.T) { 36 | drvPath, err := storepath.FromString(c.DerivationFile) 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | _, err = drvStore.Get(context.Background(), drvPath.Absolute()) 42 | assert.NoError(t, err, "Get(%v) shouldn't error", c.DerivationFile) 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /pkg/derivation/store/http.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | "net/http" 9 | "net/url" 10 | "path" 11 | 12 | "github.com/nix-community/go-nix/pkg/derivation" 13 | ) 14 | 15 | // HTTPStore implements derivation.Store. 16 | var _ derivation.Store = &HTTPStore{} 17 | 18 | // NewHTTPStore returns a HTTPStore with a given base URL. 19 | func NewHTTPStore(baseURL *url.URL) *HTTPStore { 20 | return &HTTPStore{ 21 | Client: &http.Client{}, 22 | BaseURL: baseURL, 23 | } 24 | } 25 | 26 | // HTTPStore provides a store exposing all .drv files 27 | // directly hosted below the a HTTP path specified by baseURL 28 | // aka ${baseURl}/${base derivationPath}. 29 | // It doesn't do any output path validation and consistency checks, 30 | // meaning you usually want to wrap this in a validating store. 31 | // Right now, Put() is not implemented. 32 | type HTTPStore struct { 33 | Client *http.Client 34 | // The base URL 35 | BaseURL *url.URL 36 | } 37 | 38 | // Put is not implemented right now. 39 | func (hs *HTTPStore) Put(_ context.Context, _ *derivation.Derivation) (string, error) { 40 | return "", fmt.Errorf("not implemented") 41 | } 42 | 43 | // getURL returns the full url to a derivation path, 44 | // with respect to the configured BaseURL. 45 | // It constructs the URL by appending the derivation path, 46 | // cleaned by storepath.StoreDir. 47 | func (hs *HTTPStore) getURL(derivationPath string) url.URL { 48 | // copy the base url 49 | url := *hs.BaseURL 50 | url.Path = path.Join(url.Path, path.Base(derivationPath)) 51 | 52 | return url 53 | } 54 | 55 | // constructRequest constructs a http.Request, based on a derivation path. 56 | func (hs *HTTPStore) constructRequest( 57 | ctx context.Context, 58 | method string, 59 | derivationPath string, 60 | ) (*http.Request, error) { 61 | u := hs.getURL(derivationPath) 62 | 63 | // construct the request 64 | req, err := http.NewRequestWithContext(ctx, method, u.String(), nil) 65 | if err != nil { 66 | return nil, fmt.Errorf("error constructing request: %w", err) 67 | } 68 | 69 | return req, nil 70 | } 71 | 72 | // Get retrieves a Derivation by drv path from the Derivation Store. 73 | func (hs *HTTPStore) Get(ctx context.Context, derivationPath string) (*derivation.Derivation, error) { 74 | req, err := hs.constructRequest(ctx, "GET", derivationPath) 75 | if err != nil { 76 | return nil, err 77 | } 78 | 79 | resp, err := hs.Client.Do(req) 80 | if err != nil { 81 | return nil, fmt.Errorf("error doing request: %w", err) 82 | } 83 | defer resp.Body.Close() 84 | 85 | if resp.StatusCode < 200 || resp.StatusCode >= 300 { 86 | return nil, fmt.Errorf("bad status code: %v", resp.StatusCode) 87 | } 88 | 89 | // prepare a buffer to receive the body in 90 | var buf bytes.Buffer 91 | 92 | // copy body into buffer 93 | _, err = io.Copy(&buf, resp.Body) 94 | if err != nil { 95 | return nil, fmt.Errorf("error reading body: %w", err) 96 | } 97 | 98 | // parse derivation from the buffer 99 | drv, err := derivation.ReadDerivation(&buf) 100 | if err != nil { 101 | return nil, fmt.Errorf("error parsing derivation: %w", err) 102 | } 103 | 104 | return drv, nil 105 | } 106 | 107 | // Has returns whether the derivation (by drv path) exists. 108 | // It does this by doing a HEAD request to the http endpoint. 109 | func (hs *HTTPStore) Has(ctx context.Context, derivationPath string) (bool, error) { 110 | req, err := hs.constructRequest(ctx, "HEAD", derivationPath) 111 | if err != nil { 112 | return false, err 113 | } 114 | 115 | resp, err := hs.Client.Do(req) 116 | if err != nil { 117 | return false, fmt.Errorf("error doing request: %w", err) 118 | } 119 | defer resp.Body.Close() 120 | 121 | // if we get back a plain 404, this means the file doesn't exist. 122 | if resp.StatusCode == 404 { 123 | return false, nil 124 | } 125 | 126 | // if we get back something in the 2xx range, this means 127 | // the file exists 128 | if resp.StatusCode >= 200 && resp.StatusCode < 300 { 129 | return true, nil 130 | } 131 | 132 | // else, return an error 133 | return false, fmt.Errorf("bad status code: %v", resp.StatusCode) 134 | } 135 | 136 | // Close is a no-op. 137 | func (hs *HTTPStore) Close() error { 138 | return nil 139 | } 140 | -------------------------------------------------------------------------------- /pkg/derivation/store/map.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/nix-community/go-nix/pkg/derivation" 8 | ) 9 | 10 | // MapStore implements derivation.Store. 11 | var _ derivation.Store = &MapStore{} 12 | 13 | func NewMapStore() *MapStore { 14 | return &MapStore{ 15 | drvs: make(map[string]*derivation.Derivation), 16 | drvReplacements: make(map[string]string), 17 | } 18 | } 19 | 20 | // MapStore provides a simple implementation of derivation.Store, 21 | // that's just a hashmap mapping drv paths to Derivation objects. 22 | // The interface is not thread-safe. 23 | type MapStore struct { 24 | // drvs stores all derivation structs, indexed by their drv path 25 | drvs map[string]*derivation.Derivation 26 | 27 | // drvReplacements stores the replacement strings for a derivation (indexed by drv path, too) 28 | drvReplacements map[string]string 29 | } 30 | 31 | // Put inserts a new Derivation into the Derivation Store. 32 | func (ms *MapStore) Put(ctx context.Context, drv *derivation.Derivation) (string, error) { 33 | if err := validateDerivationInStore(ctx, drv, ms); err != nil { 34 | return "", err 35 | } 36 | 37 | if err := checkOutputPaths(drv, ms.drvReplacements); err != nil { 38 | return "", err 39 | } 40 | 41 | // Calculate the drv path of the drv we're about to insert 42 | drvPath, err := drv.DrvPath() 43 | if err != nil { 44 | return "", err 45 | } 46 | 47 | // We might already have one in here, and overwrite it. 48 | // But as it's fully validated, it'll be the same. 49 | ms.drvs[drvPath] = drv 50 | 51 | // (Pre-)calculate the replacement string, so it's available 52 | // once we refer to it from other derivations inserted later. 53 | drvReplacement, err := drv.CalculateDrvReplacement(ms.drvReplacements) 54 | if err != nil { 55 | return "", fmt.Errorf("unable to calculate drv replacement: %w", err) 56 | } 57 | 58 | ms.drvReplacements[drvPath] = drvReplacement 59 | 60 | return drvPath, nil 61 | } 62 | 63 | // Get retrieves a Derivation by drv path from the Derivation Store. 64 | func (ms *MapStore) Get(_ context.Context, derivationPath string) (*derivation.Derivation, error) { 65 | if drv, ok := ms.drvs[derivationPath]; ok { 66 | return drv, nil 67 | } 68 | 69 | return nil, fmt.Errorf("derivation path not found: %s", derivationPath) 70 | } 71 | 72 | // Has returns whether the derivation (by drv path) exists. 73 | func (ms *MapStore) Has(_ context.Context, derivationPath string) (bool, error) { 74 | if _, ok := ms.drvs[derivationPath]; ok { 75 | return true, nil 76 | } 77 | 78 | return false, nil 79 | } 80 | 81 | // Close is a no-op. 82 | func (ms *MapStore) Close() error { 83 | return nil 84 | } 85 | -------------------------------------------------------------------------------- /pkg/derivation/store/store_test.go: -------------------------------------------------------------------------------- 1 | package store_test 2 | 3 | import ( 4 | "context" 5 | "os" 6 | "path/filepath" 7 | "testing" 8 | 9 | "github.com/nix-community/go-nix/pkg/derivation" 10 | "github.com/nix-community/go-nix/pkg/derivation/store" 11 | "github.com/nix-community/go-nix/pkg/storepath" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | //nolint:gochecknoglobals 16 | var stores = []struct { 17 | Title string 18 | // a function that creates a new store on the fly 19 | // a temporary folder (t.TempDir()) is passed to it 20 | NewStore (func(string) derivation.Store) 21 | }{ 22 | { 23 | Title: "MemoryStore", 24 | NewStore: func(_ string) derivation.Store { 25 | return store.NewMapStore() 26 | }, 27 | }, { 28 | Title: "BadgerStore (tmpdir)", 29 | NewStore: func(tmpDir string) derivation.Store { 30 | store, err := store.NewBadgerStore(tmpDir) 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | return store 36 | }, 37 | }, { 38 | Title: "Badger Store (memory)", 39 | NewStore: func(_ string) derivation.Store { 40 | store, err := store.NewBadgerMemoryStore() 41 | if err != nil { 42 | panic(err) 43 | } 44 | 45 | return store 46 | }, 47 | }, 48 | } 49 | 50 | //nolint:gochecknoglobals 51 | var cases = []struct { 52 | Title string 53 | DerivationFile string 54 | }{ 55 | { 56 | Title: "fixed-sha256", 57 | DerivationFile: "0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv", 58 | }, 59 | { 60 | // Has a single fixed-output dependency 61 | Title: "simple-sha256", 62 | DerivationFile: "4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv", 63 | }, 64 | { 65 | Title: "fixed-sha1", 66 | DerivationFile: "ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv", 67 | }, 68 | { 69 | // Has a single fixed-output dependency 70 | Title: "simple-sha1", 71 | DerivationFile: "ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv", 72 | }, 73 | { 74 | Title: "multiple-outputs", 75 | DerivationFile: "h32dahq0bx5rp1krcdx3a53asj21jvhk-has-multi-out.drv", 76 | }, 77 | { 78 | Title: "structured-attrs", 79 | DerivationFile: "9lj1lkjm2ag622mh4h9rpy6j607an8g2-structured-attrs.drv", 80 | }, 81 | } 82 | 83 | // fixtureToDrvStruct opens a fixture from //test/testdata, and returns a *Derivation struct 84 | // it panics in case of parsing errors. 85 | func fixtureToDrvStruct(fixtureFilename string) *derivation.Derivation { 86 | derivationFile, err := os.Open(filepath.FromSlash("../../../test/testdata/" + fixtureFilename)) 87 | if err != nil { 88 | panic(err) 89 | } 90 | 91 | drv, err := derivation.ReadDerivation(derivationFile) 92 | if err != nil { 93 | panic(err) 94 | } 95 | 96 | return drv 97 | } 98 | 99 | func TestStores(t *testing.T) { 100 | for _, s := range stores { 101 | t.Run(s.Title, func(t *testing.T) { 102 | t.Run("open and close", func(t *testing.T) { 103 | store := s.NewStore(t.TempDir()) 104 | assert.NoError(t, store.Close(), "closing the store shouldn't error") 105 | }) 106 | 107 | t.Run("normal Put", func(t *testing.T) { 108 | store := s.NewStore(t.TempDir()) 109 | defer store.Close() 110 | 111 | for _, c := range cases { 112 | t.Run(c.Title, func(t *testing.T) { 113 | drv := fixtureToDrvStruct(c.DerivationFile) 114 | 115 | drvPath, err := store.Put(context.Background(), drv) 116 | 117 | assert.NoError(t, err, "Put()'ing the derivation shouldn't cause an error") 118 | 119 | spExpected, err := storepath.FromString(c.DerivationFile) 120 | if err != nil { 121 | panic(err) 122 | } 123 | 124 | assert.Equal(t, spExpected.Absolute(), drvPath) 125 | }) 126 | } 127 | }) 128 | 129 | // This tries to retrieve "simple-sha256", even if it was never inserted 130 | t.Run("Get() without Put()", func(t *testing.T) { 131 | store := s.NewStore(t.TempDir()) 132 | defer store.Close() 133 | 134 | drv := fixtureToDrvStruct(cases[0].DerivationFile) 135 | 136 | drvPath, err := drv.DrvPath() 137 | if err != nil { 138 | panic(err) 139 | } 140 | 141 | _, err = store.Get(context.Background(), drvPath) 142 | assert.Error(t, err, "retrieving a derivation that doesn't exist should error") 143 | assert.Containsf(t, err.Error(), "derivation path not found", "error should complain about not found") 144 | }) 145 | 146 | // This inserts "simple-sha256", which depends on "fixed-sha256", which isn't inserted. 147 | t.Run("missing input derivation", func(t *testing.T) { 148 | store := s.NewStore(t.TempDir()) 149 | defer store.Close() 150 | 151 | drv := fixtureToDrvStruct(cases[1].DerivationFile) 152 | 153 | _, err := store.Put(context.Background(), drv) 154 | assert.Error(t, err, "inserting a derivation without the dependency being inserted should error") 155 | }) 156 | 157 | // This inserts "simple-sha256", but with miscalculated output path 158 | t.Run("wrong output paths", func(t *testing.T) { 159 | store := s.NewStore(t.TempDir()) 160 | defer store.Close() 161 | 162 | drv := fixtureToDrvStruct(cases[0].DerivationFile) 163 | 164 | // was /nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar 165 | drv.Outputs["out"].Path = "/nix/store/1q0pg5zpfmznxscq3avycvf9xdvx50n3-bar" 166 | 167 | _, err := store.Put(context.Background(), drv) 168 | assert.Error(t, err, "inserting a derivation with wrongly calculated output path should error") 169 | }) 170 | 171 | // This inserts "simple-sha256", but we renamed outputs["out"] to outputs["foo"], 172 | // so it should already fail validation 173 | t.Run("wrong output name", func(t *testing.T) { 174 | store := s.NewStore(t.TempDir()) 175 | defer store.Close() 176 | 177 | drv := fixtureToDrvStruct(cases[0].DerivationFile) 178 | 179 | outOutput := drv.Outputs["out"] 180 | delete(drv.Outputs, "out") 181 | drv.Outputs["foo"] = outOutput 182 | 183 | _, err := store.Put(context.Background(), drv) 184 | assert.Error(t, err, "inserting a derivation should fail validation already") 185 | assert.Containsf(t, err.Error(), "unable to validate derivation", "error should complain about validate") 186 | }) 187 | }) 188 | } 189 | } 190 | 191 | func BenchmarkStores(b *testing.B) { 192 | for _, s := range stores { 193 | s := s 194 | b.Run(s.Title, func(b *testing.B) { 195 | store := s.NewStore(b.TempDir()) 196 | defer store.Close() 197 | 198 | b.Run("Put", func(b *testing.B) { 199 | for _, c := range cases { 200 | ctx := context.Background() 201 | drv := fixtureToDrvStruct(c.DerivationFile) 202 | 203 | b.Run(c.Title, func(b *testing.B) { 204 | for i := 0; i < b.N; i++ { 205 | _, err := store.Put(ctx, drv) 206 | if err != nil { 207 | panic(err) 208 | } 209 | } 210 | }) 211 | } 212 | }) 213 | }) 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /pkg/derivation/store/uri.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | "github.com/nix-community/go-nix/pkg/derivation" 8 | ) 9 | 10 | // NewFromURI returns a derivation.Store by consuming a URI: 11 | // - if no scheme is specified, FSStore is assumed 12 | // - file:// also uses FSStore. 13 | // - http:// and https:// initialize an HTTPStore 14 | // - badger:// initializes an in-memory badger store. 15 | // - badger:///path/to/badger initializes an on-disk badger store. 16 | func NewFromURI(uri string) (derivation.Store, error) { //nolint:ireturn 17 | u, err := url.Parse(uri) 18 | if err != nil { 19 | return nil, fmt.Errorf("unable to parse uri: %w", err) 20 | } 21 | 22 | switch u.Scheme { 23 | case "": 24 | return NewFSStore(u.Path) 25 | case "badger": 26 | return NewBadgerStore("") 27 | case "file": 28 | return NewFSStore(u.Path) 29 | case "http": 30 | return NewHTTPStore(u), nil 31 | case "https": 32 | return NewHTTPStore(u), nil 33 | default: 34 | return nil, fmt.Errorf("unknown scheme: %v", u.Scheme) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /pkg/derivation/store/util.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/nix-community/go-nix/pkg/derivation" 8 | ) 9 | 10 | // validateDerivationInStore validates a function standalone, 11 | // and checks if all the derivations it refers to exist in the store. 12 | func validateDerivationInStore(ctx context.Context, drv *derivation.Derivation, store derivation.Store) error { 13 | // Validate the derivation, we don't bother with costly calculations 14 | // if it's obviously wrong. 15 | if err := drv.Validate(); err != nil { 16 | return fmt.Errorf("unable to validate derivation: %w", err) 17 | } 18 | 19 | // Check if all InputDerivations already exist. 20 | // It's easy to check, and this means we detect 21 | // inconsistencies when inserting Drvs early, and not 22 | // when we try to use them from a child. 23 | for inputDerivationPath := range drv.InputDerivations { 24 | found, err := store.Has(ctx, inputDerivationPath) 25 | if err != nil { 26 | return fmt.Errorf("error checking if input derivation exists: %w", err) 27 | } 28 | 29 | if !found { 30 | return fmt.Errorf("unable to find referred input drv path %v", inputDerivationPath) 31 | } 32 | } 33 | 34 | return nil 35 | } 36 | 37 | // checkOutputPaths re-calculates the paths of a derivation, and returns an error if they don't match. 38 | // It needs some (usually pre-calculated) values for input derivations. 39 | func checkOutputPaths(drv *derivation.Derivation, drvReplacements map[string]string) error { 40 | // (Re-)calculate the output paths of the derivation that we're about to insert. 41 | // pass in all of ms.drvReplacements, to look up replacements from there. 42 | outputPaths, err := drv.CalculateOutputPaths(drvReplacements) 43 | if err != nil { 44 | return fmt.Errorf("unable to calculate output paths: %w", err) 45 | } 46 | 47 | // Compare calculated output paths with what has been passed 48 | for outputName, calculatedOutputPath := range outputPaths { 49 | if calculatedOutputPath != drv.Outputs[outputName].Path { 50 | return fmt.Errorf( 51 | "calculated output path (%s) doesn't match sent output path (%s)", 52 | calculatedOutputPath, drv.Outputs[outputName].Path, 53 | ) 54 | } 55 | } 56 | 57 | return nil 58 | } 59 | -------------------------------------------------------------------------------- /pkg/nar/doc.go: -------------------------------------------------------------------------------- 1 | // Package nar implements access to .nar files. 2 | // 3 | // Nix Archive (nar) is a file format for storing a directory or a single file 4 | // in a binary reproducible format. This is the format that is being used to 5 | // pack and distribute Nix build results. It doesn't store any timestamps or 6 | // similar fields available in conventional filesystems. .nar files can be read 7 | // and written in a streaming manner. 8 | package nar 9 | -------------------------------------------------------------------------------- /pkg/nar/dump.go: -------------------------------------------------------------------------------- 1 | package nar 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "syscall" 9 | ) 10 | 11 | // SourceFilterFunc is the interface for creating source filters. 12 | // If the function returns true, the file is copied to the Nix store, otherwise it is omitted, 13 | // this mimics the behaviour of the Nix function builtins.filterSource. 14 | type SourceFilterFunc func(path string, nodeType NodeType) bool 15 | 16 | // DumpPath will serialize a path on the local file system to NAR format, 17 | // and write it to the passed writer. 18 | func DumpPath(w io.Writer, path string) error { 19 | return DumpPathFilter(w, path, nil) 20 | } 21 | 22 | // DumpPathFilter will serialize a path on the local file system to NAR format, 23 | // and write it to the passed writer, filtering out any files where the filter 24 | // function returns false. 25 | func DumpPathFilter(w io.Writer, path string, filter SourceFilterFunc) error { 26 | // initialize the nar writer 27 | nw, err := NewWriter(w) 28 | if err != nil { 29 | return err 30 | } 31 | 32 | // make sure the NAR writer is always closed, so the underlying goroutine is stopped 33 | defer nw.Close() 34 | 35 | err = dumpPath(nw, path, "/", filter) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | return nw.Close() 41 | } 42 | 43 | // dumpPath recursively calls itself for every node in the path. 44 | func dumpPath(nw *Writer, path string, subpath string, filter SourceFilterFunc) error { 45 | // assemble the full path. 46 | p := filepath.Join(path, subpath) 47 | 48 | // peek at the path 49 | fi, err := os.Lstat(p) 50 | if err != nil { 51 | return err 52 | } 53 | 54 | var nodeType NodeType 55 | if fi.Mode()&os.ModeSymlink == os.ModeSymlink { 56 | nodeType = TypeSymlink 57 | } else if fi.IsDir() { 58 | nodeType = TypeDirectory 59 | } else if fi.Mode().IsRegular() { 60 | nodeType = TypeRegular 61 | } else { 62 | return fmt.Errorf("unknown type for %v", p) 63 | } 64 | 65 | if filter != nil && !filter(p, nodeType) { 66 | return nil 67 | } 68 | 69 | switch nodeType { 70 | case TypeSymlink: 71 | linkTarget, err := os.Readlink(p) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | // write the symlink node 77 | err = nw.WriteHeader(&Header{ 78 | Path: subpath, 79 | Type: TypeSymlink, 80 | LinkTarget: linkTarget, 81 | }) 82 | if err != nil { 83 | return err 84 | } 85 | 86 | return nil 87 | 88 | case TypeDirectory: 89 | // write directory node 90 | err := nw.WriteHeader(&Header{ 91 | Path: subpath, 92 | Type: TypeDirectory, 93 | }) 94 | if err != nil { 95 | return err 96 | } 97 | 98 | // look at the children 99 | files, err := os.ReadDir(filepath.Join(path, subpath)) 100 | if err != nil { 101 | return err 102 | } 103 | 104 | // loop over all elements 105 | for _, file := range files { 106 | err := dumpPath(nw, path, filepath.Join(subpath, file.Name()), filter) 107 | if err != nil { 108 | return err 109 | } 110 | } 111 | 112 | return nil 113 | 114 | case TypeRegular: 115 | // write regular node 116 | err := nw.WriteHeader(&Header{ 117 | Path: subpath, 118 | Type: TypeRegular, 119 | Size: fi.Size(), 120 | // If it's executable by the user, it'll become executable. 121 | // This matches nix's dump() function behaviour. 122 | Executable: fi.Mode()&syscall.S_IXUSR != 0, 123 | }) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | // open the file 129 | f, err := os.Open(p) 130 | if err != nil { 131 | return err 132 | } 133 | defer f.Close() 134 | 135 | // read in contents 136 | n, err := io.Copy(nw, f) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | // check if read bytes matches fi.Size() 142 | if n != fi.Size() { 143 | return fmt.Errorf("read %v, expected %v bytes while reading %v", n, fi.Size(), p) 144 | } 145 | 146 | return nil 147 | } 148 | 149 | return fmt.Errorf("unknown type for file %v", p) 150 | } 151 | -------------------------------------------------------------------------------- /pkg/nar/dump_nonwindows_test.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package nar_test 5 | 6 | import ( 7 | "bytes" 8 | "path/filepath" 9 | "syscall" 10 | "testing" 11 | 12 | "github.com/nix-community/go-nix/pkg/nar" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | // TestDumpPathUnknown makes sure calling DumpPath on a path with a fifo 17 | // doesn't panic, but returns an error. 18 | func TestDumpPathUnknown(t *testing.T) { 19 | tmpDir := t.TempDir() 20 | p := filepath.Join(tmpDir, "a") 21 | 22 | err := syscall.Mkfifo(p, 0o644) 23 | if err != nil { 24 | panic(err) 25 | } 26 | 27 | var buf bytes.Buffer 28 | 29 | err = nar.DumpPath(&buf, p) 30 | assert.Error(t, err) 31 | assert.Containsf(t, err.Error(), "unknown type", "error should complain about unknown type") 32 | } 33 | -------------------------------------------------------------------------------- /pkg/nar/dump_test.go: -------------------------------------------------------------------------------- 1 | package nar_test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "os" 7 | "path/filepath" 8 | "runtime" 9 | "syscall" 10 | "testing" 11 | 12 | "github.com/nix-community/go-nix/pkg/nar" 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestDumpPathEmptyDir(t *testing.T) { 17 | var buf bytes.Buffer 18 | 19 | err := nar.DumpPath(&buf, t.TempDir()) 20 | if assert.NoError(t, err) { 21 | assert.Equal(t, genEmptyDirectoryNar(), buf.Bytes()) 22 | } 23 | } 24 | 25 | func TestDumpPathOneByteRegular(t *testing.T) { 26 | t.Run("non-executable", func(t *testing.T) { 27 | tmpDir := t.TempDir() 28 | p := filepath.Join(tmpDir, "a") 29 | 30 | err := os.WriteFile(p, []byte{0x1}, os.ModePerm&syscall.S_IRUSR) 31 | if err != nil { 32 | panic(err) 33 | } 34 | 35 | var buf bytes.Buffer 36 | 37 | err = nar.DumpPath(&buf, p) 38 | if assert.NoError(t, err) { 39 | assert.Equal(t, genOneByteRegularNar(), buf.Bytes()) 40 | } 41 | }) 42 | 43 | t.Run("executable", func(t *testing.T) { 44 | // This writes to the filesystem and looks at the attributes. 45 | // As you can't represent the executable bit on windows, it would fail. 46 | if runtime.GOOS == "windows" { 47 | return 48 | } 49 | 50 | tmpDir := t.TempDir() 51 | p := filepath.Join(tmpDir, "a") 52 | 53 | err := os.WriteFile(p, []byte{0x1}, os.ModePerm&(syscall.S_IRUSR|syscall.S_IXUSR)) 54 | if err != nil { 55 | panic(err) 56 | } 57 | 58 | var buf bytes.Buffer 59 | 60 | // call dump path on it again 61 | err = nar.DumpPath(&buf, p) 62 | if assert.NoError(t, err) { 63 | // We don't have a fixture with executable bit set, 64 | // so pipe the nar into a reader and check the returned first header. 65 | nr, err := nar.NewReader(&buf) 66 | if err != nil { 67 | panic(err) 68 | } 69 | 70 | hdr, err := nr.Next() 71 | if err != nil { 72 | panic(err) 73 | } 74 | 75 | assert.True(t, hdr.Executable, "regular should be executable") 76 | } 77 | }) 78 | } 79 | 80 | func TestDumpPathSymlink(t *testing.T) { 81 | tmpDir := t.TempDir() 82 | p := filepath.Join(tmpDir, "a") 83 | 84 | err := os.Symlink("/nix/store/somewhereelse", p) 85 | if err != nil { 86 | panic(err) 87 | } 88 | 89 | var buf bytes.Buffer 90 | 91 | err = nar.DumpPath(&buf, p) 92 | if assert.NoError(t, err) { 93 | assert.Equal(t, genSymlinkNar(), buf.Bytes()) 94 | } 95 | } 96 | 97 | func TestDumpPathRecursion(t *testing.T) { 98 | tmpDir := t.TempDir() 99 | p := filepath.Join(tmpDir, "a") 100 | 101 | err := os.WriteFile(p, []byte{0x1}, os.ModePerm&syscall.S_IRUSR) 102 | if err != nil { 103 | panic(err) 104 | } 105 | 106 | var buf bytes.Buffer 107 | 108 | err = nar.DumpPath(&buf, tmpDir) 109 | if assert.NoError(t, err) { 110 | // We don't have a fixture for the created path 111 | // so pipe the nar into a reader and check the headers returned. 112 | nr, err := nar.NewReader(&buf) 113 | if err != nil { 114 | panic(err) 115 | } 116 | 117 | // read in first node 118 | hdr, err := nr.Next() 119 | assert.NoError(t, err) 120 | assert.Equal(t, &nar.Header{ 121 | Path: "/", 122 | Type: nar.TypeDirectory, 123 | }, hdr) 124 | 125 | // read in second node 126 | hdr, err = nr.Next() 127 | assert.NoError(t, err) 128 | assert.Equal(t, &nar.Header{ 129 | Path: "/a", 130 | Type: nar.TypeRegular, 131 | Size: 1, 132 | }, hdr) 133 | 134 | // read in contents 135 | contents, err := io.ReadAll(nr) 136 | assert.NoError(t, err) 137 | assert.Equal(t, []byte{0x1}, contents) 138 | 139 | // we should be done 140 | _, err = nr.Next() 141 | assert.Equal(t, io.EOF, err) 142 | } 143 | } 144 | 145 | func TestDumpPathFilter(t *testing.T) { 146 | t.Run("unfiltered", func(t *testing.T) { 147 | tmpDir := t.TempDir() 148 | p := filepath.Join(tmpDir, "a") 149 | 150 | err := os.WriteFile(p, []byte{0x1}, os.ModePerm&syscall.S_IRUSR) 151 | if err != nil { 152 | panic(err) 153 | } 154 | 155 | var buf bytes.Buffer 156 | 157 | err = nar.DumpPathFilter(&buf, p, func(name string, nodeType nar.NodeType) bool { 158 | assert.Equal(t, name, p) 159 | assert.Equal(t, nodeType, nar.TypeRegular) 160 | 161 | return true 162 | }) 163 | if assert.NoError(t, err) { 164 | assert.Equal(t, genOneByteRegularNar(), buf.Bytes()) 165 | } 166 | }) 167 | 168 | t.Run("filtered", func(t *testing.T) { 169 | tmpDir := t.TempDir() 170 | p := filepath.Join(tmpDir, "a") 171 | 172 | err := os.WriteFile(p, []byte{0x1}, os.ModePerm&syscall.S_IRUSR) 173 | if err != nil { 174 | panic(err) 175 | } 176 | 177 | var buf bytes.Buffer 178 | 179 | err = nar.DumpPathFilter(&buf, tmpDir, func(name string, _ nar.NodeType) bool { 180 | return name != p 181 | }) 182 | if assert.NoError(t, err) { 183 | assert.NotEqual(t, genOneByteRegularNar(), buf.Bytes()) 184 | } 185 | }) 186 | } 187 | 188 | func BenchmarkDumpPath(b *testing.B) { 189 | b.Run("testdata", func(b *testing.B) { 190 | for i := 0; i < b.N; i++ { 191 | err := nar.DumpPath(io.Discard, "../../test/testdata") 192 | if err != nil { 193 | panic(err) 194 | } 195 | } 196 | }) 197 | } 198 | -------------------------------------------------------------------------------- /pkg/nar/fixtures_test.go: -------------------------------------------------------------------------------- 1 | package nar_test 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/nix-community/go-nix/pkg/wire" 7 | ) 8 | 9 | // genEmptyNar returns just the magic header, without any actual nodes 10 | // this is no valid NAR file, as it needs to contain at least a root. 11 | func genEmptyNar() []byte { 12 | var expectedBuf bytes.Buffer 13 | 14 | err := wire.WriteString(&expectedBuf, "nix-archive-1") 15 | if err != nil { 16 | panic(err) 17 | } 18 | 19 | return expectedBuf.Bytes() 20 | } 21 | 22 | // genEmptyDirectoryNar returns the bytes of a NAR file only containing an empty directory. 23 | func genEmptyDirectoryNar() []byte { 24 | var expectedBuf bytes.Buffer 25 | 26 | err := wire.WriteString(&expectedBuf, "nix-archive-1") 27 | if err != nil { 28 | panic(err) 29 | } 30 | 31 | err = wire.WriteString(&expectedBuf, "(") 32 | if err != nil { 33 | panic(err) 34 | } 35 | 36 | err = wire.WriteString(&expectedBuf, "type") 37 | if err != nil { 38 | panic(err) 39 | } 40 | 41 | err = wire.WriteString(&expectedBuf, "directory") 42 | if err != nil { 43 | panic(err) 44 | } 45 | 46 | err = wire.WriteString(&expectedBuf, ")") 47 | if err != nil { 48 | panic(err) 49 | } 50 | 51 | return expectedBuf.Bytes() 52 | } 53 | 54 | // genOneByteRegularNar returns the bytes of a NAR only containing a single file at the root. 55 | func genOneByteRegularNar() []byte { 56 | var expectedBuf bytes.Buffer 57 | 58 | err := wire.WriteString(&expectedBuf, "nix-archive-1") 59 | if err != nil { 60 | panic(err) 61 | } 62 | 63 | err = wire.WriteString(&expectedBuf, "(") 64 | if err != nil { 65 | panic(err) 66 | } 67 | 68 | err = wire.WriteString(&expectedBuf, "type") 69 | if err != nil { 70 | panic(err) 71 | } 72 | 73 | err = wire.WriteString(&expectedBuf, "regular") 74 | if err != nil { 75 | panic(err) 76 | } 77 | 78 | err = wire.WriteString(&expectedBuf, "contents") 79 | if err != nil { 80 | panic(err) 81 | } 82 | 83 | err = wire.WriteBytes(&expectedBuf, []byte{0x1}) 84 | if err != nil { 85 | panic(err) 86 | } 87 | 88 | err = wire.WriteString(&expectedBuf, ")") 89 | if err != nil { 90 | panic(err) 91 | } 92 | 93 | return expectedBuf.Bytes() 94 | } 95 | 96 | // genSymlinkNar returns the bytes of a NAR only containing a single symlink at the root. 97 | func genSymlinkNar() []byte { 98 | var expectedBuf bytes.Buffer 99 | 100 | err := wire.WriteString(&expectedBuf, "nix-archive-1") 101 | if err != nil { 102 | panic(err) 103 | } 104 | 105 | err = wire.WriteString(&expectedBuf, "(") 106 | if err != nil { 107 | panic(err) 108 | } 109 | 110 | err = wire.WriteString(&expectedBuf, "type") 111 | if err != nil { 112 | panic(err) 113 | } 114 | 115 | err = wire.WriteString(&expectedBuf, "symlink") 116 | if err != nil { 117 | panic(err) 118 | } 119 | 120 | err = wire.WriteString(&expectedBuf, "target") 121 | if err != nil { 122 | panic(err) 123 | } 124 | 125 | err = wire.WriteString(&expectedBuf, "/nix/store/somewhereelse") 126 | if err != nil { 127 | panic(err) 128 | } 129 | 130 | err = wire.WriteString(&expectedBuf, ")") 131 | if err != nil { 132 | panic(err) 133 | } 134 | 135 | return expectedBuf.Bytes() 136 | } 137 | 138 | // genInvalidOrderNAR returns the bytes of a NAR file that contains a folder 139 | // with a and b directories inside, but in the wrong order (b comes first). 140 | func genInvalidOrderNAR() []byte { 141 | var expectedBuf bytes.Buffer 142 | 143 | err := wire.WriteString(&expectedBuf, "nix-archive-1") 144 | if err != nil { 145 | panic(err) 146 | } 147 | 148 | err = wire.WriteString(&expectedBuf, "(") 149 | if err != nil { 150 | panic(err) 151 | } 152 | 153 | err = wire.WriteString(&expectedBuf, "type") 154 | if err != nil { 155 | panic(err) 156 | } 157 | 158 | err = wire.WriteString(&expectedBuf, "directory") 159 | if err != nil { 160 | panic(err) 161 | } 162 | 163 | // first entry begin 164 | err = wire.WriteString(&expectedBuf, "entry") 165 | if err != nil { 166 | panic(err) 167 | } 168 | 169 | err = wire.WriteString(&expectedBuf, "(") 170 | if err != nil { 171 | panic(err) 172 | } 173 | 174 | err = wire.WriteString(&expectedBuf, "name") 175 | if err != nil { 176 | panic(err) 177 | } 178 | 179 | err = wire.WriteString(&expectedBuf, "b") 180 | if err != nil { 181 | panic(err) 182 | } 183 | 184 | err = wire.WriteString(&expectedBuf, "node") 185 | if err != nil { 186 | panic(err) 187 | } 188 | 189 | // begin 190 | err = wire.WriteString(&expectedBuf, "(") 191 | if err != nil { 192 | panic(err) 193 | } 194 | 195 | err = wire.WriteString(&expectedBuf, "type") 196 | if err != nil { 197 | panic(err) 198 | } 199 | 200 | err = wire.WriteString(&expectedBuf, "directory") 201 | if err != nil { 202 | panic(err) 203 | } 204 | 205 | err = wire.WriteString(&expectedBuf, ")") 206 | if err != nil { 207 | panic(err) 208 | } 209 | // end 210 | 211 | err = wire.WriteString(&expectedBuf, ")") 212 | if err != nil { 213 | panic(err) 214 | } 215 | // first entry end 216 | 217 | // second entry begin 218 | err = wire.WriteString(&expectedBuf, "entry") 219 | if err != nil { 220 | panic(err) 221 | } 222 | 223 | err = wire.WriteString(&expectedBuf, "(") 224 | if err != nil { 225 | panic(err) 226 | } 227 | 228 | err = wire.WriteString(&expectedBuf, "name") 229 | if err != nil { 230 | panic(err) 231 | } 232 | 233 | err = wire.WriteString(&expectedBuf, "a") 234 | if err != nil { 235 | panic(err) 236 | } 237 | 238 | err = wire.WriteString(&expectedBuf, "node") 239 | if err != nil { 240 | panic(err) 241 | } 242 | 243 | // begin 244 | err = wire.WriteString(&expectedBuf, "(") 245 | if err != nil { 246 | panic(err) 247 | } 248 | 249 | err = wire.WriteString(&expectedBuf, "type") 250 | if err != nil { 251 | panic(err) 252 | } 253 | 254 | err = wire.WriteString(&expectedBuf, "directory") 255 | if err != nil { 256 | panic(err) 257 | } 258 | 259 | err = wire.WriteString(&expectedBuf, ")") 260 | if err != nil { 261 | panic(err) 262 | } 263 | // end 264 | 265 | err = wire.WriteString(&expectedBuf, ")") 266 | if err != nil { 267 | panic(err) 268 | } 269 | // second entry end 270 | 271 | err = wire.WriteString(&expectedBuf, ")") 272 | if err != nil { 273 | panic(err) 274 | } 275 | 276 | return expectedBuf.Bytes() 277 | } 278 | -------------------------------------------------------------------------------- /pkg/nar/header.go: -------------------------------------------------------------------------------- 1 | package nar 2 | 3 | import ( 4 | "fmt" 5 | "io/fs" 6 | "path/filepath" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | // Header represents a single header in a NAR archive. Some fields may not 12 | // be populated depending on the Type. 13 | type Header struct { 14 | Path string // Path of the file entry, relative inside the NAR 15 | Type NodeType // Typeflag is the type of header entry. 16 | LinkTarget string // Target of symlink (valid for TypeSymlink) 17 | Size int64 // Logical file size in bytes 18 | Executable bool // Set to true for files that are executable 19 | } 20 | 21 | // Validate does some consistency checking of the header structure, such as 22 | // checking for valid paths and inconsistent fields, and returns an error if it 23 | // fails validation. 24 | func (h *Header) Validate() error { 25 | // Path needs to start with a /, and must not contain null bytes 26 | // as we might get passed windows paths, ToSlash them first. 27 | if p := filepath.ToSlash(h.Path); len(h.Path) < 1 || p[0:1] != "/" { 28 | return fmt.Errorf("path must start with a /") 29 | } 30 | 31 | if strings.ContainsAny(h.Path, "\u0000") { 32 | return fmt.Errorf("path may not contain null bytes") 33 | } 34 | 35 | // Regular files and directories may not have LinkTarget set. 36 | if h.Type == TypeRegular || h.Type == TypeDirectory { 37 | if h.LinkTarget != "" { 38 | return fmt.Errorf("type is %v, but LinkTarget is not empty", h.Type.String()) 39 | } 40 | } 41 | 42 | // Directories and Symlinks may not have Size and Executable set. 43 | if h.Type == TypeDirectory || h.Type == TypeSymlink { 44 | if h.Size != 0 { 45 | return fmt.Errorf("type is %v, but Size is not 0", h.Type.String()) 46 | } 47 | 48 | if h.Executable { 49 | return fmt.Errorf("type is %v, but Executable is true", h.Type.String()) 50 | } 51 | } 52 | 53 | // Symlinks need to specify a target. 54 | if h.Type == TypeSymlink { 55 | if h.LinkTarget == "" { 56 | return fmt.Errorf("type is symlink, but LinkTarget is empty") 57 | } 58 | } 59 | 60 | return nil 61 | } 62 | 63 | // FileInfo returns an fs.FileInfo for the Header. 64 | func (h *Header) FileInfo() fs.FileInfo { 65 | return headerFileInfo{h} 66 | } 67 | 68 | type headerFileInfo struct { 69 | h *Header 70 | } 71 | 72 | func (fi headerFileInfo) Size() int64 { return fi.h.Size } 73 | func (fi headerFileInfo) IsDir() bool { return fi.h.Type == TypeDirectory } 74 | func (fi headerFileInfo) ModTime() time.Time { return time.Unix(0, 0) } 75 | func (fi headerFileInfo) Sys() interface{} { return fi.h } 76 | 77 | // Name of the file. 78 | // Will be an empty string, if this describes the root of a NAR. 79 | func (fi headerFileInfo) Name() string { return fi.h.Path } 80 | -------------------------------------------------------------------------------- /pkg/nar/header_mode.go: -------------------------------------------------------------------------------- 1 | //go:build !windows 2 | // +build !windows 3 | 4 | package nar 5 | 6 | import ( 7 | "io/fs" 8 | "syscall" 9 | ) 10 | 11 | func (fi headerFileInfo) Mode() fs.FileMode { 12 | // everything in the nix store is readable by user, group and other. 13 | var mode fs.FileMode 14 | 15 | switch fi.h.Type { 16 | case TypeRegular: 17 | mode = syscall.S_IRUSR | syscall.S_IRGRP | syscall.S_IROTH 18 | if fi.h.Executable { 19 | mode |= (syscall.S_IXUSR | syscall.S_IXGRP | syscall.S_IXOTH) 20 | } 21 | case TypeDirectory: 22 | mode = syscall.S_IRUSR | syscall.S_IRGRP | syscall.S_IROTH 23 | mode |= (syscall.S_IXUSR | syscall.S_IXGRP | syscall.S_IXOTH) 24 | case TypeSymlink: 25 | mode = fs.ModePerm | fs.ModeSymlink 26 | } 27 | 28 | return mode 29 | } 30 | -------------------------------------------------------------------------------- /pkg/nar/header_mode_windows.go: -------------------------------------------------------------------------------- 1 | package nar 2 | 3 | import ( 4 | "io/fs" 5 | ) 6 | 7 | func (fi headerFileInfo) Mode() fs.FileMode { 8 | // On Windows, create a very basic variant of Mode(). 9 | // we use fs.FileMode and clear the 0200 bit. 10 | // Per https://golang.org/pkg/os/#Chmod: 11 | // “On Windows, only the 0200 bit (owner writable) of mode is used; it 12 | // controls whether the file's read-only attribute is set or cleared.” 13 | var mode fs.FileMode 14 | 15 | switch fi.h.Type { 16 | case TypeRegular: 17 | mode = fs.ModePerm 18 | case TypeDirectory: 19 | mode = fs.ModeDir 20 | case TypeSymlink: 21 | mode = fs.ModeSymlink 22 | } 23 | 24 | return mode & ^fs.FileMode(0o200) 25 | } 26 | -------------------------------------------------------------------------------- /pkg/nar/header_test.go: -------------------------------------------------------------------------------- 1 | package nar_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nix-community/go-nix/pkg/nar" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestHeaderValidate(t *testing.T) { 11 | headerRegular := &nar.Header{ 12 | Path: "/foo/bar", 13 | Type: nar.TypeRegular, 14 | LinkTarget: "", 15 | Size: 0, 16 | Executable: false, 17 | } 18 | 19 | t.Run("valid", func(t *testing.T) { 20 | vHeader := *headerRegular 21 | assert.NoError(t, vHeader.Validate()) 22 | }) 23 | 24 | t.Run("invalid path", func(t *testing.T) { 25 | invHeader := *headerRegular 26 | invHeader.Path = "foo/bar" 27 | assert.Error(t, invHeader.Validate()) 28 | 29 | invHeader.Path = "/foo/bar\000/" 30 | assert.Error(t, invHeader.Validate()) 31 | }) 32 | 33 | t.Run("LinkTarget set on regulars or directories", func(t *testing.T) { 34 | invHeader := *headerRegular 35 | invHeader.LinkTarget = "foo" 36 | 37 | assert.Error(t, invHeader.Validate()) 38 | 39 | invHeader.Type = nar.TypeDirectory 40 | assert.Error(t, invHeader.Validate()) 41 | }) 42 | 43 | t.Run("Size set on directories or symlinks", func(t *testing.T) { 44 | invHeader := *headerRegular 45 | invHeader.Type = nar.TypeDirectory 46 | invHeader.Size = 1 47 | assert.Error(t, invHeader.Validate()) 48 | 49 | invHeader = *headerRegular 50 | invHeader.Type = nar.TypeSymlink 51 | invHeader.Size = 1 52 | assert.Error(t, invHeader.Validate()) 53 | }) 54 | 55 | t.Run("Executable set on directories or symlinks", func(t *testing.T) { 56 | invHeader := *headerRegular 57 | invHeader.Type = nar.TypeDirectory 58 | invHeader.Executable = true 59 | assert.Error(t, invHeader.Validate()) 60 | 61 | invHeader = *headerRegular 62 | invHeader.Type = nar.TypeSymlink 63 | invHeader.Executable = true 64 | assert.Error(t, invHeader.Validate()) 65 | }) 66 | 67 | t.Run("No LinkTarget set on symlinks", func(t *testing.T) { 68 | invHeader := *headerRegular 69 | invHeader.Type = nar.TypeSymlink 70 | assert.Error(t, invHeader.Validate()) 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /pkg/nar/ls/doc.go: -------------------------------------------------------------------------------- 1 | // Package ls implements a parser for the .ls file format, which provides an 2 | // index into .nar files. 3 | 4 | // It is provided on cache.nixos.org, and more generally, written when 5 | // write-nar-listing=1 is passed while copying build results into a binary 6 | // cache. 7 | package ls 8 | -------------------------------------------------------------------------------- /pkg/nar/ls/list.go: -------------------------------------------------------------------------------- 1 | package ls 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io" 7 | 8 | "github.com/nix-community/go-nix/pkg/nar" 9 | ) 10 | 11 | // Root represents the .ls file root entry. 12 | type Root struct { 13 | Version int `json:"version"` 14 | Root Node 15 | } 16 | 17 | // Node represents one of the entries in a .ls file. 18 | type Node struct { 19 | Type nar.NodeType `json:"type"` 20 | Entries map[string]*Node `json:"entries"` 21 | Size int64 `json:"size"` 22 | LinkTarget string `json:"target"` 23 | Executable bool `json:"executable"` 24 | NAROffset int64 `json:"narOffset"` 25 | } 26 | 27 | // validateNode runs some consistency checks on a node and all its child 28 | // entries. It returns an error on failure. 29 | func validateNode(node *Node) error { 30 | // ensure the name of each entry is valid 31 | for k, v := range node.Entries { 32 | if !nar.IsValidNodeName(k) { 33 | return fmt.Errorf("invalid entry name: %v", k) 34 | } 35 | 36 | // Regular files and directories may not have LinkTarget set. 37 | if node.Type == nar.TypeRegular || node.Type == nar.TypeDirectory { 38 | if node.LinkTarget != "" { 39 | return fmt.Errorf("type is %v, but LinkTarget is not empty", node.Type.String()) 40 | } 41 | } 42 | 43 | // Directories and Symlinks may not have Size and Executable set. 44 | if node.Type == nar.TypeDirectory || node.Type == nar.TypeSymlink { 45 | if node.Size != 0 { 46 | return fmt.Errorf("type is %v, but Size is not 0", node.Type.String()) 47 | } 48 | 49 | if node.Executable { 50 | return fmt.Errorf("type is %v, but Executable is true", node.Type.String()) 51 | } 52 | } 53 | 54 | // Symlinks need to specify a target. 55 | if node.Type == nar.TypeSymlink { 56 | if node.LinkTarget == "" { 57 | return fmt.Errorf("type is symlink, but LinkTarget is empty") 58 | } 59 | } 60 | 61 | // verify children 62 | err := validateNode(v) 63 | if err != nil { 64 | return err 65 | } 66 | } 67 | 68 | return nil 69 | } 70 | 71 | // ParseLS parses the NAR .ls file format. 72 | // It returns a tree-like structure for all the entries. 73 | func ParseLS(r io.Reader) (*Root, error) { 74 | root := Root{} 75 | 76 | dec := json.NewDecoder(r) 77 | dec.DisallowUnknownFields() 78 | 79 | err := dec.Decode(&root) 80 | if err != nil { 81 | return nil, err 82 | } 83 | 84 | if root.Version != 1 { 85 | return nil, fmt.Errorf("invalid version %d", root.Version) 86 | } 87 | 88 | // ensure the nodes are valid 89 | err = validateNode(&root.Root) 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | return &root, err 95 | } 96 | -------------------------------------------------------------------------------- /pkg/nar/ls/list_test.go: -------------------------------------------------------------------------------- 1 | package ls_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/nix-community/go-nix/pkg/nar" 8 | "github.com/nix-community/go-nix/pkg/nar/ls" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | const fixture = ` 13 | { 14 | "version": 1, 15 | "root": { 16 | "type": "directory", 17 | "entries": { 18 | "bin": { 19 | "type": "directory", 20 | "entries": { 21 | "curl": { 22 | "type": "regular", 23 | "size": 182520, 24 | "executable": true, 25 | "narOffset": 400 26 | } 27 | } 28 | } 29 | } 30 | } 31 | } 32 | ` 33 | 34 | func TestLS(t *testing.T) { 35 | r := strings.NewReader(fixture) 36 | root, err := ls.ParseLS(r) 37 | assert.NoError(t, err) 38 | 39 | expectedRoot := &ls.Root{ 40 | Version: 1, 41 | Root: ls.Node{ 42 | Type: nar.TypeDirectory, 43 | Entries: map[string]*ls.Node{ 44 | "bin": { 45 | Type: nar.TypeDirectory, 46 | Entries: map[string]*ls.Node{ 47 | "curl": { 48 | Type: nar.TypeRegular, 49 | Size: 182520, 50 | Executable: true, 51 | NAROffset: 400, 52 | }, 53 | }, 54 | }, 55 | }, 56 | }, 57 | } 58 | assert.Equal(t, expectedRoot, root) 59 | } 60 | -------------------------------------------------------------------------------- /pkg/nar/types.go: -------------------------------------------------------------------------------- 1 | package nar 2 | 3 | const narVersionMagic1 = "nix-archive-1" 4 | 5 | // Enum of all the node types possible. 6 | type NodeType string 7 | 8 | const ( 9 | // TypeRegular represents a regular file. 10 | TypeRegular = NodeType("regular") 11 | // TypeDirectory represents a directory entry. 12 | TypeDirectory = NodeType("directory") 13 | // TypeSymlink represents a file symlink. 14 | TypeSymlink = NodeType("symlink") 15 | ) 16 | 17 | func (t NodeType) String() string { 18 | return string(t) 19 | } 20 | -------------------------------------------------------------------------------- /pkg/nar/util.go: -------------------------------------------------------------------------------- 1 | package nar 2 | 3 | import "strings" 4 | 5 | // IsValidNodeName checks the name of a node 6 | // it may not contain null bytes or slashes. 7 | func IsValidNodeName(nodeName string) bool { 8 | return !strings.Contains(nodeName, "/") && !strings.ContainsAny(nodeName, "\u0000") 9 | } 10 | 11 | // PathIsLexicographicallyOrdered checks if two paths are lexicographically ordered component by component. 12 | func PathIsLexicographicallyOrdered(path1 string, path2 string) bool { 13 | if path1 <= path2 { 14 | return true 15 | } 16 | 17 | // n is the lower number of characters of the two paths. 18 | var n int 19 | if len(path1) < len(path2) { 20 | n = len(path1) 21 | } else { 22 | n = len(path2) 23 | } 24 | 25 | for i := 0; i < n; i++ { 26 | if path1[i] == path2[i] { 27 | continue 28 | } 29 | 30 | if path1[i] == '/' && path2[i] != '/' { 31 | return true 32 | } 33 | 34 | return path1[i] < path2[i] 35 | } 36 | 37 | // Cover cases like where path1 is a prefix of path2 (path1=/arp-foo path2=/arp) 38 | return len(path2) >= len(path1) 39 | } 40 | -------------------------------------------------------------------------------- /pkg/nar/util_test.go: -------------------------------------------------------------------------------- 1 | package nar_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/nix-community/go-nix/pkg/nar" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | //nolint:gochecknoglobals 12 | var cases = []struct { 13 | path1 string 14 | path2 string 15 | expected bool 16 | }{ 17 | { 18 | path1: "/foo", 19 | path2: "/foo", 20 | expected: true, 21 | }, 22 | { 23 | path1: "/fooa", 24 | path2: "/foob", 25 | expected: true, 26 | }, 27 | { 28 | path1: "/foob", 29 | path2: "/fooa", 30 | expected: false, 31 | }, 32 | { 33 | path1: "/cmd/structlayout/main.go", 34 | path2: "/cmd/structlayout-optimize", 35 | expected: true, 36 | }, 37 | { 38 | path1: "/cmd/structlayout-optimize", 39 | path2: "/cmd/structlayout-ao/main.go", 40 | expected: false, 41 | }, 42 | } 43 | 44 | func TestLexicographicallyOrdered(t *testing.T) { 45 | for i, testCase := range cases { 46 | t.Run(fmt.Sprint(i), func(t *testing.T) { 47 | result := nar.PathIsLexicographicallyOrdered(testCase.path1, testCase.path2) 48 | assert.Equal(t, result, testCase.expected) 49 | }) 50 | } 51 | } 52 | 53 | func BenchmarkLexicographicallyOrdered(b *testing.B) { 54 | for i, testCase := range cases { 55 | b.Run(fmt.Sprint(i), func(b *testing.B) { 56 | for i := 0; i < b.N; i++ { 57 | nar.PathIsLexicographicallyOrdered(testCase.path1, testCase.path2) 58 | } 59 | }) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /pkg/narinfo/check.go: -------------------------------------------------------------------------------- 1 | package narinfo 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/nix-community/go-nix/pkg/storepath" 8 | ) 9 | 10 | // Check does some sanity checking on a NarInfo struct, such as: 11 | // 12 | // - ensuring the paths in StorePath, References and Deriver are syntactically valid 13 | // (references and deriver first need to be made absolute) 14 | // - when no compression is present, ensuring File{Hash,Size} and 15 | // Nar{Hash,Size} are equal 16 | func (n *NarInfo) Check() error { 17 | _, err := storepath.FromAbsolutePath(n.StorePath) 18 | if err != nil { 19 | return fmt.Errorf("invalid StorePath: %v: %s", n.StorePath, err) 20 | } 21 | 22 | for i, r := range n.References { 23 | _, err = storepath.FromString(r) 24 | if err != nil { 25 | return fmt.Errorf("invalid Reference[%d]: %v", i, r) 26 | } 27 | } 28 | 29 | if n.Deriver != "" { 30 | _, err = storepath.FromString(n.Deriver) 31 | if err != nil { 32 | return fmt.Errorf("invalid Deriver: %v", n.Deriver) 33 | } 34 | } 35 | 36 | if n.Compression != "none" { 37 | return nil 38 | } 39 | 40 | if n.FileSize > 0 && n.FileSize != n.NarSize { 41 | return fmt.Errorf("compression is none, FileSize/NarSize differs: %d, %d", n.FileSize, n.NarSize) 42 | } 43 | 44 | if n.FileHash == nil || n.NarHash == nil { 45 | return nil 46 | } 47 | 48 | if n.FileHash.Algo() != n.NarHash.Algo() { 49 | return fmt.Errorf("FileHash/NarHash type differ: %v, %v", n.FileHash.Algo().String(), n.NarHash.Algo().String()) 50 | } 51 | 52 | if !bytes.Equal(n.FileHash.Digest(), n.NarHash.Digest()) { 53 | return fmt.Errorf("compression is none, FileHash/NarHash differs: %v, %v", n.FileHash, n.NarHash) 54 | } 55 | 56 | return nil 57 | } 58 | -------------------------------------------------------------------------------- /pkg/narinfo/fingerprint.go: -------------------------------------------------------------------------------- 1 | package narinfo 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/nix-community/go-nix/pkg/nixhash" 7 | "github.com/nix-community/go-nix/pkg/storepath" 8 | ) 9 | 10 | // Fingerprint is the digest that will be used with a private key to generate 11 | // one of the signatures. 12 | func (n NarInfo) Fingerprint() string { 13 | f := "1;" + 14 | n.StorePath + ";" + 15 | n.NarHash.Format(nixhash.NixBase32, true) + ";" + 16 | strconv.FormatUint(n.NarSize, 10) + ";" 17 | 18 | if len(n.References) == 0 { 19 | return f 20 | } 21 | 22 | for _, ref := range n.References { 23 | f += storepath.StoreDir + "/" + ref + "," 24 | } 25 | 26 | return f[:len(f)-1] 27 | } 28 | -------------------------------------------------------------------------------- /pkg/narinfo/parser.go: -------------------------------------------------------------------------------- 1 | package narinfo 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "io" 7 | "strconv" 8 | "strings" 9 | 10 | "github.com/nix-community/go-nix/pkg/narinfo/signature" 11 | "github.com/nix-community/go-nix/pkg/nixhash" 12 | ) 13 | 14 | // Parse reads a .narinfo file content 15 | // and returns a NarInfo struct with the parsed data. 16 | func Parse(r io.Reader) (*NarInfo, error) { 17 | narInfo := &NarInfo{} 18 | scanner := bufio.NewScanner(r) 19 | 20 | // Increase the buffer size. 21 | // Some .narinfo files have a lot of entries in References, 22 | // and bufio.Scanner will error bufio.ErrTooLong otherwise. 23 | const maxCapacity = 1048576 24 | buf := make([]byte, maxCapacity) 25 | scanner.Buffer(buf, maxCapacity) 26 | 27 | for scanner.Scan() { 28 | var err error 29 | 30 | line := scanner.Text() 31 | // skip empty lines (like, an empty line at EOF) 32 | if line == "" { 33 | continue 34 | } 35 | 36 | k, v, err := splitOnce(line, ": ") 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | switch k { 42 | case "StorePath": 43 | narInfo.StorePath = v 44 | case "URL": 45 | narInfo.URL = v 46 | case "Compression": 47 | narInfo.Compression = v 48 | case "FileHash": 49 | narInfo.FileHash, err = nixhash.ParseAny(v, nil) 50 | if err != nil { 51 | return nil, err 52 | } 53 | case "FileSize": 54 | narInfo.FileSize, err = strconv.ParseUint(v, 10, 0) 55 | if err != nil { 56 | return nil, err 57 | } 58 | case "NarHash": 59 | narInfo.NarHash, err = nixhash.ParseAny(v, nil) 60 | if err != nil { 61 | return nil, err 62 | } 63 | case "NarSize": 64 | narInfo.NarSize, err = strconv.ParseUint(v, 10, 0) 65 | if err != nil { 66 | return nil, err 67 | } 68 | case "References": 69 | if v == "" { 70 | continue 71 | } 72 | 73 | narInfo.References = append(narInfo.References, strings.Split(v, " ")...) 74 | case "Deriver": 75 | if v != "unknown-deriver" { 76 | narInfo.Deriver = v 77 | } 78 | case "System": 79 | narInfo.System = v 80 | case "Sig": 81 | signature, e := signature.ParseSignature(v) 82 | if e != nil { 83 | return nil, fmt.Errorf("unable to parse signature line %v: %v", v, err) 84 | } 85 | 86 | narInfo.Signatures = append(narInfo.Signatures, signature) 87 | case "CA": 88 | narInfo.CA = v 89 | default: 90 | return nil, fmt.Errorf("unknown key %v", k) 91 | } 92 | 93 | if err != nil { 94 | return nil, fmt.Errorf("unable to parse line %v", line) 95 | } 96 | } 97 | 98 | if err := scanner.Err(); err != nil { 99 | return nil, err 100 | } 101 | 102 | // An empty/non-existrent compression field is considered to mean bzip2 103 | if narInfo.Compression == "" { 104 | narInfo.Compression = "bzip2" 105 | } 106 | 107 | return narInfo, nil 108 | } 109 | 110 | // splitOnce - Split a string and make sure it's only splittable once. 111 | func splitOnce(s string, sep string) (string, string, error) { 112 | idx := strings.Index(s, sep) 113 | if idx == -1 { 114 | return "", "", fmt.Errorf("unable to find separator '%s' in %v", sep, s) 115 | } 116 | 117 | if strings.Contains(s[idx+1:], sep) { 118 | return "", "", fmt.Errorf("found separator '%s' twice or more in %v", sep, s) 119 | } 120 | 121 | return s[0:idx], s[idx+len(sep):], nil 122 | } 123 | -------------------------------------------------------------------------------- /pkg/narinfo/signature/public_key.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "fmt" 6 | ) 7 | 8 | // PublicKey represents a named ed25519 public key. 9 | type PublicKey struct { 10 | Name string 11 | Data ed25519.PublicKey 12 | } 13 | 14 | // String outputs a string representation as name + ":" + base64(data). 15 | func (pk PublicKey) String() string { 16 | return encode(pk.Name, pk.Data) 17 | } 18 | 19 | // Verify that the fingerprint with the signature against the public key. If the 20 | // signature and public key don't have the same name, just return false. 21 | func (pk PublicKey) Verify(fingerprint string, sig Signature) bool { 22 | if pk.Name != sig.Name { 23 | return false 24 | } 25 | 26 | return ed25519.Verify(pk.Data, []byte(fingerprint), sig.Data) 27 | } 28 | 29 | // ParsePublicKey decodes a serialized string, and returns a PublicKey struct, or an error. 30 | func ParsePublicKey(s string) (PublicKey, error) { 31 | name, data, err := decode(s, ed25519.PublicKeySize) 32 | if err != nil { 33 | return PublicKey{}, fmt.Errorf("public key is corrupt: %w", err) 34 | } 35 | 36 | return PublicKey{name, data}, nil 37 | } 38 | -------------------------------------------------------------------------------- /pkg/narinfo/signature/secret_key.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ed25519" 6 | "fmt" 7 | "io" 8 | ) 9 | 10 | // GenerateKeypair creates a new nix-store compatible keypair 11 | // 12 | // rand: uses crypto/rand.Reader if nil 13 | // name: key identifier used by Nix 14 | func GenerateKeypair(name string, rand io.Reader) (secretKey SecretKey, publicKey PublicKey, err error) { 15 | pub, sec, err := ed25519.GenerateKey(rand) 16 | if err != nil { 17 | return SecretKey{}, PublicKey{}, err 18 | } 19 | 20 | return SecretKey{name, sec}, PublicKey{name, pub}, nil 21 | } 22 | 23 | // SecretKey represents a named ed25519 private key. 24 | type SecretKey struct { 25 | name string 26 | data ed25519.PrivateKey 27 | } 28 | 29 | // String outputs a string representation as name + ":" + base64(data). 30 | func (sk SecretKey) String() string { 31 | return encode(sk.name, sk.data) 32 | } 33 | 34 | // LoadSecretKey decodes a : pair into a SecretKey. 35 | func LoadSecretKey(s string) (SecretKey, error) { 36 | name, data, err := decode(s, ed25519.PrivateKeySize) 37 | if err != nil { 38 | return SecretKey{}, fmt.Errorf("secret key is corrupt: %w", err) 39 | } 40 | 41 | return SecretKey{name, data}, nil 42 | } 43 | 44 | // ToPublicKey derives the PublicKey from the SecretKey. 45 | func (sk SecretKey) ToPublicKey() PublicKey { 46 | pub := sk.data.Public().(ed25519.PublicKey) 47 | 48 | return PublicKey{sk.name, []byte(pub)} 49 | } 50 | 51 | // Sign generates a signature for the fingerprint. 52 | // If rand is nil, it will use rand.Reader. 53 | func (sk SecretKey) Sign(rand io.Reader, fingerprint string) (Signature, error) { 54 | // passing crypto.Hash(0) as ed25519 doesn't support pre-hashed messages 55 | // (see docs) 56 | data, err := sk.data.Sign(rand, []byte(fingerprint), crypto.Hash(0)) 57 | if err != nil { 58 | return Signature{}, err 59 | } 60 | 61 | return Signature{sk.name, data}, nil 62 | } 63 | -------------------------------------------------------------------------------- /pkg/narinfo/signature/signature.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "crypto/ed25519" 5 | "fmt" 6 | ) 7 | 8 | // Signature represents a named ed25519 signature. 9 | type Signature struct { 10 | Name string 11 | Data []byte 12 | } 13 | 14 | // String returns the encoded :. 15 | func (s Signature) String() string { 16 | return encode(s.Name, s.Data) 17 | } 18 | 19 | // ParseSignature decodes a : 20 | // and returns a *Signature, or an error. 21 | func ParseSignature(s string) (Signature, error) { 22 | name, data, err := decode(s, ed25519.SignatureSize) 23 | if err != nil { 24 | return Signature{}, fmt.Errorf("signature is corrupt: %w", err) 25 | } 26 | 27 | return Signature{name, data}, nil 28 | } 29 | 30 | // VerifyFirst returns the result of the first signature that matches a public 31 | // key. If no matching public key was found, it returns false. 32 | func VerifyFirst(fingerprint string, signatures []Signature, pubKeys []PublicKey) bool { 33 | for _, key := range pubKeys { 34 | for _, sig := range signatures { 35 | if key.Name == sig.Name { 36 | return key.Verify(fingerprint, sig) 37 | } 38 | } 39 | } 40 | 41 | return false 42 | } 43 | -------------------------------------------------------------------------------- /pkg/narinfo/signature/signature_test.go: -------------------------------------------------------------------------------- 1 | package signature_test 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/nix-community/go-nix/pkg/narinfo" 8 | "github.com/nix-community/go-nix/pkg/narinfo/signature" 9 | "github.com/stretchr/testify/assert" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | const ( 14 | nixosPublicKey = "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" 15 | test1PublicKey = "test1:tLAEn+EeaBUJYqEpTd2yeerr7Ic6+0vWe+aXL/vYUpE=" 16 | //nolint:gosec 17 | test1SecretKey = "test1:jbX9NxZp8WB/coK8k7yLf0gNYmBbIbCrOFwgJgI7OV+0sASf4R5oFQlioSlN3bJ56uvshzr7S9Z75pcv+9hSkQ==" 18 | ) 19 | 20 | func TestPublicKeyLoad(t *testing.T) { 21 | pub, err := signature.ParsePublicKey(nixosPublicKey) 22 | require.NoError(t, err) 23 | assert.Equal(t, nixosPublicKey, pub.String()) 24 | 25 | pub2, err := signature.ParsePublicKey(test1PublicKey) 26 | require.NoError(t, err) 27 | assert.Equal(t, test1PublicKey, pub2.String()) 28 | } 29 | 30 | func TestSecretKeyLoad(t *testing.T) { 31 | sec, err := signature.LoadSecretKey(test1SecretKey) 32 | require.NoError(t, err) 33 | assert.Equal(t, test1SecretKey, sec.String()) 34 | 35 | pub := sec.ToPublicKey() 36 | pub2, err := signature.ParsePublicKey(test1PublicKey) 37 | require.NoError(t, err) 38 | assert.Equal(t, pub, pub2) 39 | } 40 | 41 | func TestGenerate(t *testing.T) { 42 | sec, pub, err := signature.GenerateKeypair("test2", nil) 43 | require.NoError(t, err) 44 | 45 | sec2, err := signature.LoadSecretKey(sec.String()) 46 | require.NoError(t, err) 47 | assert.Equal(t, sec, sec2) 48 | 49 | pub2, err := signature.ParsePublicKey(pub.String()) 50 | require.NoError(t, err) 51 | assert.Equal(t, pub, pub2) 52 | } 53 | 54 | func TestSignature(t *testing.T) { 55 | sigStr := "test1:519iiVLx/c4Rdt5DNt6Y2Jm6hcWE9+XY69ygiWSZCNGVcmOcyL64uVAJ3cV8vaTusIZdbTnYo9Y7vDNeTmmMBQ==" 56 | 57 | sig, err := signature.ParseSignature(sigStr) 58 | require.NoError(t, err) 59 | assert.Equal(t, sigStr, sig.String()) 60 | } 61 | 62 | func TestSignVerify(t *testing.T) { 63 | //nolint:lll 64 | strNarinfoSample := ` 65 | StorePath: /nix/store/syd87l2rxw8cbsxmxl853h0r6pdwhwjr-curl-7.82.0-bin 66 | URL: nar/05ra3y72i3qjri7xskf9qj8kb29r6naqy1sqpbs3azi3xcigmj56.nar.xz 67 | Compression: xz 68 | FileHash: sha256:05ra3y72i3qjri7xskf9qj8kb29r6naqy1sqpbs3azi3xcigmj56 69 | FileSize: 68852 70 | NarHash: sha256:1b4sb93wp679q4zx9k1ignby1yna3z7c4c2ri3wphylbc2dwsys0 71 | NarSize: 196040 72 | References: 0jqd0rlxzra1rs38rdxl43yh6rxchgc6-curl-7.82.0 6w8g7njm4mck5dmjxws0z1xnrxvl81xa-glibc-2.34-115 j5jxw3iy7bbz4a57fh9g2xm2gxmyal8h-zlib-1.2.12 yxvjs9drzsphm9pcf42a4byzj1kb9m7k-openssl-1.1.1n 73 | Deriver: 5rwxzi7pal3qhpsyfc16gzkh939q1np6-curl-7.82.0.drv 74 | Sig: cache.nixos.org-1:TsTTb3WGTZKphvYdBHXwo6weVILmTytUjLB+vcX89fOjjRicCHmKA4RCPMVLkj6TMJ4GMX3HPVWRdD1hkeKZBQ== 75 | Sig: test1:519iiVLx/c4Rdt5DNt6Y2Jm6hcWE9+XY69ygiWSZCNGVcmOcyL64uVAJ3cV8vaTusIZdbTnYo9Y7vDNeTmmMBQ== 76 | ` 77 | ni, err := narinfo.Parse(strings.NewReader(strNarinfoSample)) 78 | require.NoError(t, err) 79 | secKeyTest1, err := signature.LoadSecretKey(test1SecretKey) 80 | require.NoError(t, err) 81 | pubKeyTest1, err := signature.ParsePublicKey(test1PublicKey) 82 | require.NoError(t, err) 83 | pubKeyNixOS, err := signature.ParsePublicKey(nixosPublicKey) 84 | require.NoError(t, err) 85 | 86 | t.Run("verify sig and verify", func(t *testing.T) { 87 | fingerprint := ni.Fingerprint() 88 | 89 | // Check the signature 90 | sig, err := secKeyTest1.Sign(nil, fingerprint) 91 | require.NoError(t, err) 92 | 93 | // Check you can verify the signature 94 | require.True(t, pubKeyTest1.Verify(fingerprint, sig)) 95 | }) 96 | 97 | t.Run("verifyFirst narinfo signature", func(t *testing.T) { 98 | // Test our own generated key 99 | assert.True(t, signature.VerifyFirst(ni.Fingerprint(), ni.Signatures, []signature.PublicKey{pubKeyTest1})) 100 | 101 | // Try the official public key 102 | assert.True(t, signature.VerifyFirst(ni.Fingerprint(), ni.Signatures, []signature.PublicKey{pubKeyNixOS})) 103 | }) 104 | } 105 | -------------------------------------------------------------------------------- /pkg/narinfo/signature/util.go: -------------------------------------------------------------------------------- 1 | package signature 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | // decode parses a ":" string into a name, and data pair. 10 | // And then checks that the data is of dataSize. 11 | // 12 | // This is used internally for all the data structures below in this file. 13 | func decode(s string, dataSize int) (name string, data []byte, err error) { 14 | kv := strings.SplitN(s, ":", 2) 15 | name = kv[0] 16 | 17 | var dataStr string 18 | 19 | if len(kv) != 2 { 20 | return "", nil, fmt.Errorf("encountered invalid number of fields: %v", len(kv)) 21 | } 22 | 23 | dataStr = kv[1] 24 | 25 | if name == "" { 26 | return "", nil, fmt.Errorf("name is missing") 27 | } 28 | 29 | if dataStr == "" { 30 | return "", nil, fmt.Errorf("data is missing") 31 | } 32 | 33 | data, err = base64.StdEncoding.DecodeString(dataStr) 34 | if err != nil { 35 | return "", nil, fmt.Errorf("data is corrupt: %w", err) 36 | } 37 | 38 | if len(data) != dataSize { 39 | return "", nil, fmt.Errorf("data is not the right size: expected %d but got %d", dataSize, len(data)) 40 | } 41 | 42 | return name, data, nil 43 | } 44 | 45 | // encode is the counterpart of the decode function above. Generate a 46 | // ":" string from the underlying data structures. 47 | func encode(name string, data []byte) string { 48 | return name + ":" + base64.StdEncoding.EncodeToString(data) 49 | } 50 | -------------------------------------------------------------------------------- /pkg/narinfo/splitonce_test.go: -------------------------------------------------------------------------------- 1 | //nolint:testpackage 2 | package narinfo 3 | 4 | import ( 5 | "fmt" 6 | "testing" 7 | ) 8 | 9 | func TestSplitOnce(t *testing.T) { 10 | tests := []struct { 11 | s string 12 | sep string 13 | str1 string 14 | str2 string 15 | err string 16 | }{ 17 | {"hello:world", ":", "hello", "world", ""}, 18 | {":helloworld", ":", "", "helloworld", ""}, 19 | {"helloworld:", ":", "helloworld", "", ""}, 20 | {"helloworld", ":", "", "", "unable to find separator ':' in helloworld"}, 21 | {"hello:wo:rld", ":", "", "", "found separator ':' twice or more in hello:wo:rld"}, 22 | {"hello::world", ":", "", "", "found separator ':' twice or more in hello::world"}, 23 | } 24 | 25 | for _, ltest := range tests { 26 | // TODO: This is not necessary on Go >=1.23. Remove this assignment and use 27 | // test instead of ltest above. 28 | test := ltest 29 | 30 | tName := fmt.Sprintf("splitOnce(%q, %q) -> (%q, %q, %s)", 31 | test.s, test.sep, test.str1, test.str2, test.err) 32 | 33 | t.Run(tName, func(t *testing.T) { 34 | t.Parallel() 35 | 36 | str1, str2, err := splitOnce(test.s, test.sep) 37 | 38 | if test.err == "" && err != nil { 39 | t.Fatalf("expected no error but got %s", err) 40 | } else if test.err != "" && err == nil { 41 | t.Fatalf("expected an error but got none") 42 | } else if test.err != "" && err != nil { 43 | if want, got := test.err, err.Error(); want != got { 44 | t.Errorf("want %q got %q", want, got) 45 | } 46 | } 47 | 48 | if want, got := test.str1, str1; want != got { 49 | t.Errorf("want %q got %q", want, got) 50 | } 51 | 52 | if want, got := test.str2, str2; want != got { 53 | t.Errorf("want %q got %q", want, got) 54 | } 55 | }) 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /pkg/narinfo/types.go: -------------------------------------------------------------------------------- 1 | package narinfo 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | 7 | "github.com/nix-community/go-nix/pkg/narinfo/signature" 8 | "github.com/nix-community/go-nix/pkg/nixhash" 9 | ) 10 | 11 | // NarInfo represents a parsed .narinfo file. 12 | type NarInfo struct { 13 | StorePath string // The full nix store path (/nix/store/…-pname-version) 14 | 15 | URL string // The relative location to the .nar[.xz,…] file. Usually nar/$fileHash.nar[.xz] 16 | Compression string // The compression method file at URL is compressed with (none,xz,…) 17 | 18 | FileHash *nixhash.HashWithEncoding // The hash of the file at URL 19 | FileSize uint64 // The size of the file at URL, in bytes 20 | 21 | // The hash of the .nar file, after possible decompression 22 | // Identical to FileHash if no compression is used. 23 | NarHash *nixhash.HashWithEncoding 24 | // The size of the .nar file, after possible decompression, in bytes. 25 | // Identical to FileSize if no compression is used. 26 | NarSize uint64 27 | 28 | // References to other store paths, contained in the .nar file 29 | References []string 30 | 31 | // Path of the .drv for this store path 32 | Deriver string 33 | 34 | // This doesn't seem to be used at all? 35 | System string 36 | 37 | // Signatures, if any. 38 | Signatures []signature.Signature 39 | 40 | // TODO: Figure out the meaning of this 41 | CA string 42 | } 43 | 44 | func (n *NarInfo) String() string { 45 | var buf bytes.Buffer 46 | 47 | fmt.Fprintf(&buf, "StorePath: %v\n", n.StorePath) 48 | fmt.Fprintf(&buf, "URL: %v\n", n.URL) 49 | fmt.Fprintf(&buf, "Compression: %v\n", n.Compression) 50 | 51 | if n.FileHash != nil && n.FileSize != 0 { 52 | fmt.Fprintf(&buf, "FileHash: %s\n", n.FileHash.String()) 53 | fmt.Fprintf(&buf, "FileSize: %d\n", n.FileSize) 54 | } 55 | 56 | fmt.Fprintf(&buf, "NarHash: %s\n", n.NarHash.String()) 57 | 58 | fmt.Fprintf(&buf, "NarSize: %d\n", n.NarSize) 59 | 60 | buf.WriteString("References:") 61 | 62 | if len(n.References) == 0 { 63 | buf.WriteByte(' ') 64 | } else { 65 | for _, r := range n.References { 66 | buf.WriteByte(' ') 67 | buf.WriteString(r) 68 | } 69 | } 70 | 71 | buf.WriteByte('\n') 72 | 73 | if n.Deriver != "" { 74 | fmt.Fprintf(&buf, "Deriver: %v\n", n.Deriver) 75 | } 76 | 77 | if n.System != "" { 78 | fmt.Fprintf(&buf, "System: %v\n", n.System) 79 | } 80 | 81 | for _, s := range n.Signatures { 82 | fmt.Fprintf(&buf, "Sig: %v\n", s) 83 | } 84 | 85 | if n.CA != "" { 86 | fmt.Fprintf(&buf, "CA: %v\n", n.CA) 87 | } 88 | 89 | return buf.String() 90 | } 91 | 92 | // ContentType returns the mime content type of the object. 93 | func (n NarInfo) ContentType() string { 94 | return "text/x-nix-narinfo" 95 | } 96 | -------------------------------------------------------------------------------- /pkg/nixbase32/nixbase32.go: -------------------------------------------------------------------------------- 1 | /* 2 | Package nixbase32 implements the slightly odd "base32" encoding that's used 3 | in Nix. 4 | 5 | Nix uses a custom alphabet. Contrary to other implementations (RFC4648), 6 | encoding to "nix base32" also reads in characters in reverse order (and 7 | doesn't use any padding), which makes adopting encoding/base32 hard. 8 | */ 9 | package nixbase32 10 | 11 | import ( 12 | "errors" 13 | "fmt" 14 | "strings" 15 | ) 16 | 17 | // ErrInvalidHash is returned if the hash is not valid. 18 | var ErrInvalidHash = errors.New("hash is not valid") 19 | 20 | // Alphabet contains the list of valid characters for the Nix base32 Alphabet. 21 | const Alphabet = "0123456789abcdfghijklmnpqrsvwxyz" 22 | 23 | // DecodeString returns the bytes represented by the nixbase32 string s or 24 | // returns an error. 25 | func DecodeString(s string) ([]byte, error) { 26 | dst := make([]byte, DecodedLen(len(s))) 27 | n, err := Decode(dst, []byte(s)) 28 | 29 | return dst[:n], err 30 | } 31 | 32 | // Decode decodes src using nixbase32. 33 | // It writes at most [DecodedLen] of len(src) bytes to dst 34 | // and returns the number of bytes written. 35 | func Decode(dst, src []byte) (n int, err error) { 36 | maxDstSize := DecodedLen(len(src)) 37 | 38 | for n := 0; n < len(src); n++ { 39 | b := uint64(n) * 5 //nolint:gosec 40 | i := int(b / 8) //nolint:gosec 41 | j := int(b % 8) //nolint:gosec 42 | 43 | c := src[len(src)-n-1] 44 | digit := strings.IndexByte(Alphabet, c) 45 | 46 | if digit == -1 { 47 | return i, fmt.Errorf("decode base32: character %q not in Nix alphabet", c) 48 | } 49 | 50 | if i >= len(dst) { 51 | return i, ErrInvalidHash 52 | } 53 | 54 | // OR the main pattern 55 | dst[i] |= byte(digit) << j 56 | // calculate the "carry pattern" 57 | carry := byte(digit) >> (8 - j) 58 | if i+1 < maxDstSize { 59 | dst[i+1] |= carry 60 | } else if carry != 0 { 61 | // but have a nonzero carry, the encoding is invalid. 62 | return i, fmt.Errorf("decode base32: non-zero padding") 63 | } 64 | } 65 | 66 | return maxDstSize, nil 67 | } 68 | 69 | // ValidateString returns an error if s is not valid nixbase32. 70 | func ValidateString(src string) error { 71 | maxDstSize := DecodedLen(len(src)) 72 | 73 | for n := 0; n < len(src); n++ { 74 | b := uint64(n) * 5 //nolint:gosec 75 | i := int(b / 8) //nolint:gosec 76 | j := int(b % 8) //nolint:gosec 77 | 78 | c := src[len(src)-n-1] 79 | digit := strings.IndexByte(Alphabet, c) 80 | 81 | if digit == -1 { 82 | return fmt.Errorf("decode base32: character %q not in Nix alphabet", c) 83 | } 84 | 85 | if i+1 >= maxDstSize { 86 | if carry := byte(digit) >> (8 - j); carry != 0 { 87 | // but have a nonzero carry, the encoding is invalid. 88 | return fmt.Errorf("decode base32: non-zero padding") 89 | } 90 | } 91 | } 92 | 93 | return nil 94 | } 95 | 96 | // EncodedLen returns the length in bytes of the base32 encoding of an input 97 | // buffer of length n. 98 | func EncodedLen(n int) int { 99 | return (n*8 + 4) / 5 100 | } 101 | 102 | // DecodedLen returns the length in bytes of the decoded data 103 | // corresponding to n bytes of base32-encoded data. 104 | // If we have bits that don't fit into here, they are padding and must 105 | // be 0. 106 | func DecodedLen(n int) int { 107 | return (n * 5) / 8 108 | } 109 | 110 | // Encode encodes src using nixbase32, 111 | // writing [EncodedLen] of len(src) bytes to dst. 112 | func Encode(dst, src []byte) { 113 | n := EncodedLen(len(src)) 114 | dst = dst[:0:n] 115 | 116 | for n = n - 1; n >= 0; n-- { 117 | b := uint64(n) * 5 118 | i := int(b / 8) //nolint:gosec 119 | j := int(b % 8) //nolint:gosec 120 | c := src[i] >> j 121 | 122 | if i+1 < len(src) { 123 | c |= src[i+1] << (8 - j) 124 | } 125 | 126 | dst = append(dst, Alphabet[c&0x1f]) 127 | } 128 | } 129 | 130 | // EncodeToString returns the nixbase32 encoding of src. 131 | func EncodeToString(src []byte) string { 132 | n := EncodedLen(len(src)) 133 | 134 | var dst strings.Builder 135 | 136 | dst.Grow(n) 137 | 138 | for n = n - 1; n >= 0; n-- { 139 | b := uint64(n) * 5 140 | i := int(b / 8) //nolint:gosec 141 | j := int(b % 8) //nolint:gosec 142 | c := src[i] >> j 143 | 144 | if i+1 < len(src) { 145 | c |= src[i+1] << (8 - j) 146 | } 147 | 148 | dst.WriteByte(Alphabet[c&0x1f]) 149 | } 150 | 151 | return dst.String() 152 | } 153 | 154 | // Is reports whether the given byte is part of the nixbase32 alphabet. 155 | func Is(c byte) bool { 156 | return '0' <= c && c <= '9' || 157 | 'a' <= c && c <= 'z' && c != 'e' && c != 'o' && c != 'u' && c != 't' 158 | } 159 | -------------------------------------------------------------------------------- /pkg/nixhash/algo.go: -------------------------------------------------------------------------------- 1 | package nixhash 2 | 3 | import ( 4 | "crypto" 5 | "fmt" 6 | ) 7 | 8 | // Algorithm represent the hashing algorithm used to digest the data. 9 | type Algorithm uint8 10 | 11 | const ( 12 | _ = iota 13 | 14 | // All the algorithms that Nix understands. 15 | MD5 = Algorithm(iota) 16 | SHA1 = Algorithm(iota) 17 | SHA256 = Algorithm(iota) 18 | SHA512 = Algorithm(iota) 19 | ) 20 | 21 | func ParseAlgorithm(s string) (Algorithm, error) { 22 | switch s { 23 | case "md5": 24 | return MD5, nil 25 | case "sha1": 26 | return SHA1, nil 27 | case "sha256": 28 | return SHA256, nil 29 | case "sha512": 30 | return SHA512, nil 31 | default: 32 | return 0, fmt.Errorf("unknown algorithm: %s", s) 33 | } 34 | } 35 | 36 | func (a Algorithm) String() string { 37 | switch a { 38 | case MD5: 39 | return "md5" 40 | case SHA1: 41 | return "sha1" 42 | case SHA256: 43 | return "sha256" 44 | case SHA512: 45 | return "sha512" 46 | default: 47 | panic(fmt.Sprintf("bug: unknown algorithm %d", a)) 48 | } 49 | } 50 | 51 | // Func returns the cryptographic hash function for the Algorithm (implementing crypto.Hash) 52 | // It panics when encountering an invalid Algorithm, as these can only occur by 53 | // manually filling the struct. 54 | func (a Algorithm) Func() crypto.Hash { 55 | switch a { 56 | case MD5: 57 | return crypto.MD5 58 | case SHA1: 59 | return crypto.SHA1 60 | case SHA256: 61 | return crypto.SHA256 62 | case SHA512: 63 | return crypto.SHA512 64 | default: 65 | panic(fmt.Sprintf("Invalid hash type: %v", a)) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /pkg/nixhash/algo_test.go: -------------------------------------------------------------------------------- 1 | package nixhash_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nix-community/go-nix/pkg/nixhash" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestAlgo(t *testing.T) { 11 | cases := []struct { 12 | Title string 13 | Str string 14 | Algo nixhash.Algorithm 15 | }{ 16 | { 17 | "valid md5", 18 | "md5", 19 | nixhash.MD5, 20 | }, 21 | { 22 | "valid sha1", 23 | "sha1", 24 | nixhash.SHA1, 25 | }, 26 | { 27 | "valid sha256", 28 | "sha256", 29 | nixhash.SHA256, 30 | }, 31 | { 32 | "valid sha512", 33 | "sha512", 34 | nixhash.SHA512, 35 | }, 36 | } 37 | 38 | t.Run("ParseAlgorithm", func(t *testing.T) { 39 | for _, c := range cases { 40 | t.Run(c.Title, func(t *testing.T) { 41 | algo, err := nixhash.ParseAlgorithm(c.Str) 42 | assert.NoError(t, err) 43 | assert.Equal(t, c.Algo, algo) 44 | assert.Equal(t, c.Str, algo.String()) 45 | }) 46 | } 47 | }) 48 | 49 | t.Run("ParseInvalidAlgo", func(t *testing.T) { 50 | _, err := nixhash.ParseAlgorithm("woot") 51 | assert.Error(t, err) 52 | }) 53 | 54 | t.Run("PrintInalidAlgo", func(t *testing.T) { 55 | assert.Panics(t, func() { 56 | _ = nixhash.Algorithm(0).String() 57 | }) 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /pkg/nixhash/encoding.go: -------------------------------------------------------------------------------- 1 | package nixhash 2 | 3 | import ( 4 | "encoding/base64" 5 | ) 6 | 7 | // Encoding is the string representation of the hashed data. 8 | type Encoding uint8 9 | 10 | const ( 11 | _ = iota // ignore zero value 12 | 13 | // All the encodings that Nix understands. 14 | Base16 = Encoding(iota) // Lowercase hexadecimal encoding. 15 | Base64 = Encoding(iota) // [IETF RFC 4648, section 4](https://datatracker.ietf.org/doc/html/rfc4648#section-4). 16 | NixBase32 = Encoding(iota) // Nix-specific base-32 encoding. 17 | SRI = Encoding(iota) // W3C recommendation [Subresource Intergrity](https://www.w3.org/TR/SRI/) 18 | ) 19 | 20 | // b64 is the specific base64 encoding that we are using. 21 | // 22 | //nolint:gochecknoglobals 23 | var b64 = base64.StdEncoding 24 | -------------------------------------------------------------------------------- /pkg/nixhash/hash.go: -------------------------------------------------------------------------------- 1 | // Package nixhash provides methods to serialize and deserialize some of the 2 | // hashes used in nix code and .narinfo files. 3 | // 4 | // Nix uses different representation of hashes depending on the context 5 | // and history of the project. This package provides the utilities to handle them. 6 | package nixhash 7 | 8 | import ( 9 | "encoding/hex" 10 | "fmt" 11 | 12 | "github.com/nix-community/go-nix/pkg/nixbase32" 13 | ) 14 | 15 | type Hash struct { 16 | algo Algorithm 17 | digest []byte 18 | } 19 | 20 | func NewHash(algo Algorithm, digest []byte) (*Hash, error) { 21 | if algo.Func().Size() != len(digest) { 22 | return nil, fmt.Errorf("algo length doesn't match digest size") 23 | } 24 | 25 | return &Hash{algo, digest}, nil 26 | } 27 | 28 | func MustNewHash(algo Algorithm, digest []byte) *Hash { 29 | h, err := NewHash(algo, digest) 30 | if err != nil { 31 | panic(err) 32 | } 33 | 34 | return h 35 | } 36 | 37 | func (h Hash) Algo() Algorithm { 38 | return h.algo 39 | } 40 | 41 | func (h Hash) Digest() []byte { 42 | return h.digest 43 | } 44 | 45 | // Format converts the hash to a string of the given encoding. 46 | func (h Hash) Format(e Encoding, includeAlgo bool) string { 47 | var s string 48 | if e == SRI || includeAlgo { 49 | s += h.algo.String() 50 | if e == SRI { 51 | s += "-" 52 | } else { 53 | s += ":" 54 | } 55 | } 56 | 57 | switch e { 58 | case Base16: 59 | s += hex.EncodeToString(h.digest) 60 | case NixBase32: 61 | s += nixbase32.EncodeToString(h.digest) 62 | case Base64, SRI: 63 | s += b64.EncodeToString(h.digest) 64 | default: 65 | panic(fmt.Sprintf("bug: unknown encoding: %v", e)) 66 | } 67 | 68 | return s 69 | } 70 | -------------------------------------------------------------------------------- /pkg/nixhash/hash_test.go: -------------------------------------------------------------------------------- 1 | package nixhash_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nix-community/go-nix/pkg/nixhash" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestDigest(t *testing.T) { 11 | cases := []struct { 12 | Title string 13 | EncodedHash string 14 | Algo nixhash.Algorithm 15 | Encoding nixhash.Encoding 16 | IncludePrefix bool 17 | Digest []byte 18 | }{ 19 | { 20 | "valid sha256", 21 | "sha256:1rjs6c23nyf8zkmf7yxglz2q2m7v5kp51nc2m0lk4h998d0qiixs", 22 | nixhash.SHA256, 23 | nixhash.NixBase32, 24 | true, 25 | []byte{ 26 | 0xba, 0xc7, 0x88, 0x41, 0x43, 0x29, 0x41, 0x32, 27 | 0x29, 0xa8, 0x82, 0xd9, 0x50, 0xee, 0x2c, 0xfb, 28 | 0x54, 0x81, 0xc5, 0xa7, 0xaf, 0xfb, 0xe3, 0xea, 29 | 0xfc, 0xc8, 0x79, 0x3b, 0x04, 0x33, 0x5a, 0xe6, 30 | }, 31 | }, 32 | { 33 | "valid sha512", 34 | "sha512:37iwwa5iw4m6pkd6qs2c5lw13q7y16hw2rv4i1cx6jax6yibhn6fgajbwc8p4j1fc6iicpy5r1vi7hpfq3n6z1ikhm5kcyz2b1frk80", 35 | nixhash.SHA512, 36 | nixhash.NixBase32, 37 | true, 38 | []byte{ 39 | 0x00, 0xcd, 0xec, 0xc2, 0x12, 0xdf, 0xb3, 0x59, 40 | 0x2a, 0x9c, 0x31, 0x7c, 0x63, 0x07, 0x76, 0x17, 41 | 0x9e, 0xb8, 0x43, 0x2e, 0xfe, 0xb2, 0x18, 0x0d, 42 | 0x73, 0x41, 0x92, 0x8b, 0x18, 0x5f, 0x52, 0x3d, 43 | 0x67, 0x2c, 0x5c, 0xd1, 0x9b, 0xae, 0xa4, 0xe9, 44 | 0x2c, 0x44, 0xb2, 0xb3, 0xe0, 0xd0, 0x04, 0x7f, 45 | 0xf0, 0x08, 0x9c, 0x16, 0x26, 0x34, 0x36, 0x6d, 46 | 0x5e, 0x53, 0x09, 0x8f, 0x45, 0x71, 0x1e, 0xcf, 47 | }, 48 | }, 49 | { 50 | "invalid base32", 51 | "sha256:1rjs6c2tnyf8zkmf7yxglz2q2m7v5kp51nc2m0lk4h998d0qiixs", 52 | nixhash.SHA256, 53 | nixhash.NixBase32, 54 | true, 55 | nil, // means no result 56 | }, 57 | { 58 | "invalid digest length", 59 | "", // means this should panic 60 | nixhash.SHA256, 61 | nixhash.NixBase32, 62 | true, 63 | []byte{ 64 | 0xba, 0xc7, 0x88, 0x41, 0x43, 0x29, 0x41, 0x32, 65 | 0x29, 0xa8, 0x82, 0xd9, 0x50, 0xee, 0x2c, 0xfb, 66 | 0x54, 0x81, 0xc5, 0xa7, 0xaf, 0xfb, 0xe3, 0xea, 67 | 0xfc, 0xc8, 0x79, 0x3b, 0x04, 0x33, 0x5a, 68 | }, 69 | }, 70 | { 71 | "invalid encoded digest length", 72 | "sha256:37iwwa5iw4m6pkd6qs2c5lw13q7y16hw2rv4i1cx6jax6yibhn6fgajbwc8p4j1fc6iicpy5r1vi7hpfq3n6z1ikhm5kcyz2b1frk80", 73 | nixhash.SHA256, 74 | nixhash.Base64, 75 | true, 76 | nil, 77 | }, 78 | } 79 | 80 | t.Run("ParseAny", func(t *testing.T) { 81 | for _, c := range cases { 82 | t.Run(c.Title, func(t *testing.T) { 83 | if c.EncodedHash == "" { 84 | return // there is no valid string representation to parse 85 | } 86 | 87 | algo := c.Algo 88 | hash, err := nixhash.ParseAny(c.EncodedHash, &algo) 89 | 90 | if c.Digest != nil { 91 | if assert.NoError(t, err, "shouldn't error") { 92 | h, err := nixhash.NewHashWithEncoding(c.Algo, c.Digest, c.Encoding, c.IncludePrefix) 93 | assert.NoError(t, err) 94 | assert.Equal(t, h, hash) 95 | } 96 | } else { 97 | assert.Error(t, err, "should error") 98 | } 99 | }) 100 | } 101 | }) 102 | 103 | t.Run("Format", func(t *testing.T) { 104 | for _, c := range cases { 105 | t.Run(c.Title, func(t *testing.T) { 106 | if c.Digest == nil { 107 | return // there is no valid parsed representation to stringify 108 | } 109 | 110 | hash, err := nixhash.NewHashWithEncoding(c.Algo, c.Digest, c.Encoding, c.IncludePrefix) 111 | 112 | if c.EncodedHash == "" { 113 | assert.Error(t, err) 114 | } else { 115 | assert.NoError(t, err) 116 | assert.Equal(t, c.EncodedHash, hash.String()) 117 | } 118 | }) 119 | } 120 | }) 121 | } 122 | -------------------------------------------------------------------------------- /pkg/nixhash/hash_with_encoding.go: -------------------------------------------------------------------------------- 1 | package nixhash 2 | 3 | // HashWithEncoding stores the original encoding so the user can get error messages with the same encoding. 4 | type HashWithEncoding struct { 5 | Hash 6 | encoding Encoding 7 | includeAlgo bool 8 | } 9 | 10 | func NewHashWithEncoding( 11 | algo Algorithm, 12 | digest []byte, 13 | encoding Encoding, 14 | includeAlgo bool, 15 | ) (*HashWithEncoding, error) { 16 | h, err := NewHash(algo, digest) 17 | if err != nil { 18 | return nil, err 19 | } 20 | 21 | return &HashWithEncoding{ 22 | Hash: *h, 23 | encoding: encoding, 24 | includeAlgo: includeAlgo, 25 | }, nil 26 | } 27 | 28 | func MustNewHashWithEncoding(algo Algorithm, digest []byte, encoding Encoding, includeAlgo bool) *HashWithEncoding { 29 | h := MustNewHash(algo, digest) 30 | 31 | return &HashWithEncoding{ 32 | Hash: *h, 33 | encoding: encoding, 34 | includeAlgo: includeAlgo, 35 | } 36 | } 37 | 38 | // String return the previous representation of a given hash. 39 | func (h HashWithEncoding) String() string { 40 | return h.Format(h.encoding, h.includeAlgo) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/nixhash/parse.go: -------------------------------------------------------------------------------- 1 | package nixhash 2 | 3 | import ( 4 | "encoding/hex" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/nix-community/go-nix/pkg/nixbase32" 9 | ) 10 | 11 | // Parse the hash from a string representation in the format 12 | // "[:]" or "-" (a 13 | // Subresource Integrity hash expression). If the 'optAlgo' argument 14 | // is not present, then the hash algorithm must be specified in the 15 | // string. 16 | func ParseAny(s string, optAlgo *Algorithm) (*HashWithEncoding, error) { 17 | var ( 18 | isSRI = false 19 | err error 20 | ) 21 | 22 | h := &HashWithEncoding{} 23 | 24 | // Look for prefix 25 | i := strings.IndexByte(s, ':') 26 | if i <= 0 { 27 | i = strings.IndexByte(s, '-') 28 | if i > 0 { 29 | isSRI = true 30 | } 31 | } 32 | 33 | // If has prefix, get the algo 34 | if i > 0 { 35 | h.includeAlgo = true 36 | 37 | h.algo, err = ParseAlgorithm(s[:i]) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | if optAlgo != nil && h.algo != *optAlgo { 43 | return nil, fmt.Errorf("algo doesn't match expected algo: %v, %v", h.algo, optAlgo) 44 | } 45 | 46 | // keep the remainder for the encoding 47 | s = s[i+1:] 48 | } else if optAlgo != nil { 49 | h.algo = *optAlgo 50 | } else { 51 | return nil, fmt.Errorf("unable to find separator in %v", s) 52 | } 53 | 54 | // Decode the string. Because we know the algo, and each encoding has a different size, we 55 | // can find out which of the encoding was used to represent the hash. 56 | digestLenBytes := h.algo.Func().Size() 57 | 58 | switch len(s) { 59 | case hex.EncodedLen(digestLenBytes): 60 | h.encoding = Base16 61 | h.digest, err = hex.DecodeString(s) 62 | case nixbase32.EncodedLen(digestLenBytes): 63 | h.encoding = NixBase32 64 | h.digest, err = nixbase32.DecodeString(s) 65 | case b64.EncodedLen(digestLenBytes): 66 | h.encoding = Base64 67 | h.digest, err = b64.DecodeString(s) 68 | default: 69 | return h, fmt.Errorf("unknown encoding for %v", s) 70 | } 71 | 72 | if err != nil { 73 | return h, err 74 | } 75 | 76 | // Post-processing for SRI 77 | if isSRI { 78 | if h.encoding == Base64 { 79 | h.encoding = SRI 80 | } else { 81 | return h, fmt.Errorf("invalid encoding for SRI: %v", h.encoding) 82 | } 83 | } 84 | 85 | return h, nil 86 | } 87 | 88 | // ParseNixBase32 returns a new Hash struct, by parsing a hashtype:nixbase32 string, or an error. 89 | func ParseNixBase32(s string) (*Hash, error) { 90 | h, err := ParseAny(s, nil) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | if h.encoding != NixBase32 { 96 | return nil, fmt.Errorf("expected NixBase32 encoding but got %v", h.encoding) 97 | } 98 | 99 | return &h.Hash, nil 100 | } 101 | 102 | // MustParseNixBase32 returns a new Hash struct, by parsing a hashtype:nixbase32 string, or panics on error. 103 | func MustParseNixBase32(s string) *Hash { 104 | h, err := ParseNixBase32(s) 105 | if err != nil { 106 | panic(err) 107 | } 108 | 109 | return h 110 | } 111 | -------------------------------------------------------------------------------- /pkg/nixhash/util.go: -------------------------------------------------------------------------------- 1 | package nixhash 2 | 3 | // CompressHash takes an arbitrary long sequence of bytes (usually a hash digest), 4 | // and returns a sequence of bytes of length newSize. 5 | // It's calculated by rotating through the bytes in the output buffer (zero-initialized), 6 | // and XOR'ing with each byte in the passed input 7 | // It consumes 1 byte at a time, and XOR's it with the current value in the output buffer. 8 | func CompressHash(input []byte, outputSize int) []byte { 9 | buf := make([]byte, outputSize) 10 | for i := 0; i < len(input); i++ { 11 | buf[i%outputSize] ^= input[i] 12 | } 13 | 14 | return buf 15 | } 16 | -------------------------------------------------------------------------------- /pkg/sqlite/README.md: -------------------------------------------------------------------------------- 1 | # Sqlite Packages 2 | 3 | Each subpackage targets a specific version of an internal Nix [sqlite] database. 4 | 5 | The user should only edit `schema.sql` and `query.sql`. All other files are generated by [sqlc] via `sqlc generate` from 6 | the root of the repository. 7 | 8 | > Note: when adding a new package, you must edit the `sqlc.yml` file at the root of the repository. 9 | 10 | [sqlite]: https://www.sqlite.org/ 11 | [sqlc]: https://github.com/sqlc-dev/sqlc 12 | -------------------------------------------------------------------------------- /pkg/sqlite/binary_cache_v6/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.27.0 4 | 5 | package binary_cache_v6 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | ) 11 | 12 | type DBTX interface { 13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 14 | PrepareContext(context.Context, string) (*sql.Stmt, error) 15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 17 | } 18 | 19 | func New(db DBTX) *Queries { 20 | return &Queries{db: db} 21 | } 22 | 23 | type Queries struct { 24 | db DBTX 25 | } 26 | 27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 28 | return &Queries{ 29 | db: tx, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/sqlite/binary_cache_v6/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.27.0 4 | 5 | package binary_cache_v6 6 | 7 | import ( 8 | "database/sql" 9 | ) 10 | 11 | type BinaryCach struct { 12 | ID int64 13 | Url string 14 | Timestamp int64 15 | Storedir string 16 | Wantmassquery int64 17 | Priority int64 18 | } 19 | 20 | type LastPurge struct { 21 | Dummy string 22 | Value sql.NullInt64 23 | } 24 | 25 | type NAR struct { 26 | Cache int64 27 | Hashpart string 28 | Namepart sql.NullString 29 | Url sql.NullString 30 | Compression sql.NullString 31 | Filehash sql.NullString 32 | Filesize sql.NullInt64 33 | Narhash sql.NullString 34 | Narsize sql.NullInt64 35 | Refs sql.NullString 36 | Deriver sql.NullString 37 | Sigs sql.NullString 38 | Ca sql.NullString 39 | Timestamp int64 40 | Present int64 41 | } 42 | 43 | type Realisation struct { 44 | Cache int64 45 | Outputid string 46 | Content []byte 47 | Timestamp int64 48 | } 49 | 50 | type SqliteSequence struct { 51 | Name interface{} 52 | Seq interface{} 53 | } 54 | -------------------------------------------------------------------------------- /pkg/sqlite/binary_cache_v6/query.sql: -------------------------------------------------------------------------------- 1 | -- name: InsertCache :one 2 | insert into BinaryCaches(url, timestamp, storeDir, wantMassQuery, priority) 3 | values (?1, ?2, ?3, ?4, ?5) 4 | on conflict (url) 5 | do update set timestamp = ?2, storeDir = ?3, wantMassQuery = ?4, priority = ?5 6 | returning id; 7 | 8 | -- name: QueryCache :many 9 | select id, storeDir, wantMassQuery, priority from BinaryCaches where url = ? and timestamp > ?; 10 | 11 | -- name: InsertNar :exec 12 | insert or replace into NARs( 13 | cache, hashPart, namePart, url, compression, fileHash, fileSize, narHash, narSize, refs, deriver, sigs, ca, 14 | timestamp, present 15 | ) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1); 16 | 17 | -- name: InsertMissingNAR :exec 18 | insert or replace into NARs(cache, hashPart, timestamp, present) values (?, ?, ?, 0); 19 | 20 | -- name: QueryNar :many 21 | select present, namePart, url, compression, fileHash, fileSize, narHash, narSize, refs, deriver, sigs, ca from NARs 22 | where cache = ? and hashPart = ? and ((present = 0 and timestamp > ?) or (present = 1 and timestamp > ?)); 23 | 24 | -- name: InsertRealisation :exec 25 | insert or replace into Realisations(cache, outputId, content, timestamp) 26 | values (?, ?, ?, ?); 27 | 28 | -- name: InsertMissingRealisation :exec 29 | insert or replace into Realisations(cache, outputId, timestamp) 30 | values (?, ?, ?); 31 | 32 | -- name: QueryRealisation :many 33 | select content from Realisations 34 | where cache = ? and outputId = ? and 35 | ( 36 | (content is null and timestamp > ?) or 37 | (content is not null and timestamp > ?) 38 | ); 39 | 40 | -- name: QueryLastPurge :one 41 | select value from LastPurge; 42 | 43 | -- name: UpdateLastPurge :exec 44 | insert or replace into LastPurge(dummy, value) values ('', ?); 45 | 46 | -- name: PurgeNars :exec 47 | delete from NARs where ((present = 0 and timestamp < ?) or (present = 1 and timestamp < ?)); 48 | 49 | -------------------------------------------------------------------------------- /pkg/sqlite/binary_cache_v6/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE BinaryCaches ( 2 | id integer primary key autoincrement not null, 3 | url text unique not null, 4 | timestamp integer not null, 5 | storeDir text not null, 6 | wantMassQuery integer not null, 7 | priority integer not null 8 | ); 9 | CREATE TABLE sqlite_sequence(name,seq); 10 | CREATE TABLE NARs ( 11 | cache integer not null, 12 | hashPart text not null, 13 | namePart text, 14 | url text, 15 | compression text, 16 | fileHash text, 17 | fileSize integer, 18 | narHash text, 19 | narSize integer, 20 | refs text, 21 | deriver text, 22 | sigs text, 23 | ca text, 24 | timestamp integer not null, 25 | present integer not null, 26 | primary key (cache, hashPart), 27 | foreign key (cache) references BinaryCaches(id) on delete cascade 28 | ); 29 | CREATE TABLE Realisations ( 30 | cache integer not null, 31 | outputId text not null, 32 | content blob, -- Json serialisation of the realisation, or null if the realisation is absent 33 | timestamp integer not null, 34 | primary key (cache, outputId), 35 | foreign key (cache) references BinaryCaches(id) on delete cascade 36 | ); 37 | CREATE TABLE LastPurge ( 38 | dummy text primary key, 39 | value integer 40 | ); 41 | -------------------------------------------------------------------------------- /pkg/sqlite/eval_cache_v5/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.27.0 4 | 5 | package eval_cache_v5 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | ) 11 | 12 | type DBTX interface { 13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 14 | PrepareContext(context.Context, string) (*sql.Stmt, error) 15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 17 | } 18 | 19 | func New(db DBTX) *Queries { 20 | return &Queries{db: db} 21 | } 22 | 23 | type Queries struct { 24 | db DBTX 25 | } 26 | 27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 28 | return &Queries{ 29 | db: tx, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/sqlite/eval_cache_v5/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.27.0 4 | 5 | package eval_cache_v5 6 | 7 | import ( 8 | "database/sql" 9 | ) 10 | 11 | type Attribute struct { 12 | Parent int64 13 | Name sql.NullString 14 | Type int64 15 | Value sql.NullString 16 | Context sql.NullString 17 | } 18 | -------------------------------------------------------------------------------- /pkg/sqlite/eval_cache_v5/query.sql: -------------------------------------------------------------------------------- 1 | -- name: InsertAttribute :exec 2 | insert or replace into Attributes(parent, name, type, value) values (?, ?, ?, ?); 3 | 4 | -- name: InsertAttributeWithContext :exec 5 | insert or replace into Attributes(parent, name, type, value, context) values (?, ?, ?, ?, ?); 6 | 7 | -- todo sqlc doesn't like the rowid column being included below 8 | -- name: QueryAttribute :one 9 | select type, value, context from Attributes where parent = ? and name = ?; 10 | 11 | -- name: QueryAttributes :many 12 | select name from Attributes where parent = ?; -------------------------------------------------------------------------------- /pkg/sqlite/eval_cache_v5/query.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.27.0 4 | // source: query.sql 5 | 6 | package eval_cache_v5 7 | 8 | import ( 9 | "context" 10 | "database/sql" 11 | ) 12 | 13 | const insertAttribute = `-- name: InsertAttribute :exec 14 | insert or replace into Attributes(parent, name, type, value) values (?, ?, ?, ?) 15 | ` 16 | 17 | type InsertAttributeParams struct { 18 | Parent int64 19 | Name sql.NullString 20 | Type int64 21 | Value sql.NullString 22 | } 23 | 24 | func (q *Queries) InsertAttribute(ctx context.Context, arg InsertAttributeParams) error { 25 | _, err := q.db.ExecContext(ctx, insertAttribute, 26 | arg.Parent, 27 | arg.Name, 28 | arg.Type, 29 | arg.Value, 30 | ) 31 | return err 32 | } 33 | 34 | const insertAttributeWithContext = `-- name: InsertAttributeWithContext :exec 35 | insert or replace into Attributes(parent, name, type, value, context) values (?, ?, ?, ?, ?) 36 | ` 37 | 38 | type InsertAttributeWithContextParams struct { 39 | Parent int64 40 | Name sql.NullString 41 | Type int64 42 | Value sql.NullString 43 | Context sql.NullString 44 | } 45 | 46 | func (q *Queries) InsertAttributeWithContext(ctx context.Context, arg InsertAttributeWithContextParams) error { 47 | _, err := q.db.ExecContext(ctx, insertAttributeWithContext, 48 | arg.Parent, 49 | arg.Name, 50 | arg.Type, 51 | arg.Value, 52 | arg.Context, 53 | ) 54 | return err 55 | } 56 | 57 | const queryAttribute = `-- name: QueryAttribute :one 58 | select type, value, context from Attributes where parent = ? and name = ? 59 | ` 60 | 61 | type QueryAttributeParams struct { 62 | Parent int64 63 | Name sql.NullString 64 | } 65 | 66 | type QueryAttributeRow struct { 67 | Type int64 68 | Value sql.NullString 69 | Context sql.NullString 70 | } 71 | 72 | // todo sqlc doesn't like the rowid column being included below 73 | func (q *Queries) QueryAttribute(ctx context.Context, arg QueryAttributeParams) (QueryAttributeRow, error) { 74 | row := q.db.QueryRowContext(ctx, queryAttribute, arg.Parent, arg.Name) 75 | var i QueryAttributeRow 76 | err := row.Scan(&i.Type, &i.Value, &i.Context) 77 | return i, err 78 | } 79 | 80 | const queryAttributes = `-- name: QueryAttributes :many 81 | select name from Attributes where parent = ? 82 | ` 83 | 84 | func (q *Queries) QueryAttributes(ctx context.Context, parent int64) ([]sql.NullString, error) { 85 | rows, err := q.db.QueryContext(ctx, queryAttributes, parent) 86 | if err != nil { 87 | return nil, err 88 | } 89 | defer rows.Close() 90 | var items []sql.NullString 91 | for rows.Next() { 92 | var name sql.NullString 93 | if err := rows.Scan(&name); err != nil { 94 | return nil, err 95 | } 96 | items = append(items, name) 97 | } 98 | if err := rows.Close(); err != nil { 99 | return nil, err 100 | } 101 | if err := rows.Err(); err != nil { 102 | return nil, err 103 | } 104 | return items, nil 105 | } 106 | -------------------------------------------------------------------------------- /pkg/sqlite/eval_cache_v5/schema.sql: -------------------------------------------------------------------------------- 1 | create table if not exists Attributes ( 2 | parent integer not null, 3 | name text, 4 | type integer not null, 5 | value text, 6 | context text, 7 | primary key (parent, name) 8 | ); -------------------------------------------------------------------------------- /pkg/sqlite/fetcher_cache_v2/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.27.0 4 | 5 | package fetcher_cache_v2 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | ) 11 | 12 | type DBTX interface { 13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 14 | PrepareContext(context.Context, string) (*sql.Stmt, error) 15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 17 | } 18 | 19 | func New(db DBTX) *Queries { 20 | return &Queries{db: db} 21 | } 22 | 23 | type Queries struct { 24 | db DBTX 25 | } 26 | 27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 28 | return &Queries{ 29 | db: tx, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/sqlite/fetcher_cache_v2/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.27.0 4 | 5 | package fetcher_cache_v2 6 | 7 | type Cache struct { 8 | Domain string 9 | Key string 10 | Value string 11 | Timestamp int64 12 | } 13 | -------------------------------------------------------------------------------- /pkg/sqlite/fetcher_cache_v2/query.sql: -------------------------------------------------------------------------------- 1 | -- name: UpsertCache :exec 2 | insert or replace into Cache(domain, key, value, timestamp) values (?, ?, ?, ?); 3 | 4 | -- name: QueryCache :many 5 | select value, timestamp from Cache where domain = ? and key = ?; -------------------------------------------------------------------------------- /pkg/sqlite/fetcher_cache_v2/query.sql.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.27.0 4 | // source: query.sql 5 | 6 | package fetcher_cache_v2 7 | 8 | import ( 9 | "context" 10 | ) 11 | 12 | const queryCache = `-- name: QueryCache :many 13 | select value, timestamp from Cache where domain = ? and key = ? 14 | ` 15 | 16 | type QueryCacheParams struct { 17 | Domain string 18 | Key string 19 | } 20 | 21 | type QueryCacheRow struct { 22 | Value string 23 | Timestamp int64 24 | } 25 | 26 | func (q *Queries) QueryCache(ctx context.Context, arg QueryCacheParams) ([]QueryCacheRow, error) { 27 | rows, err := q.db.QueryContext(ctx, queryCache, arg.Domain, arg.Key) 28 | if err != nil { 29 | return nil, err 30 | } 31 | defer rows.Close() 32 | var items []QueryCacheRow 33 | for rows.Next() { 34 | var i QueryCacheRow 35 | if err := rows.Scan(&i.Value, &i.Timestamp); err != nil { 36 | return nil, err 37 | } 38 | items = append(items, i) 39 | } 40 | if err := rows.Close(); err != nil { 41 | return nil, err 42 | } 43 | if err := rows.Err(); err != nil { 44 | return nil, err 45 | } 46 | return items, nil 47 | } 48 | 49 | const upsertCache = `-- name: UpsertCache :exec 50 | insert or replace into Cache(domain, key, value, timestamp) values (?, ?, ?, ?) 51 | ` 52 | 53 | type UpsertCacheParams struct { 54 | Domain string 55 | Key string 56 | Value string 57 | Timestamp int64 58 | } 59 | 60 | func (q *Queries) UpsertCache(ctx context.Context, arg UpsertCacheParams) error { 61 | _, err := q.db.ExecContext(ctx, upsertCache, 62 | arg.Domain, 63 | arg.Key, 64 | arg.Value, 65 | arg.Timestamp, 66 | ) 67 | return err 68 | } 69 | -------------------------------------------------------------------------------- /pkg/sqlite/fetcher_cache_v2/schema.sql: -------------------------------------------------------------------------------- 1 | create table if not exists Cache ( 2 | domain text not null, 3 | key text not null, 4 | value text not null, 5 | timestamp integer not null, 6 | primary key (domain, key) 7 | ); -------------------------------------------------------------------------------- /pkg/sqlite/nix_v10/db.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.27.0 4 | 5 | package nix_v10 6 | 7 | import ( 8 | "context" 9 | "database/sql" 10 | ) 11 | 12 | type DBTX interface { 13 | ExecContext(context.Context, string, ...interface{}) (sql.Result, error) 14 | PrepareContext(context.Context, string) (*sql.Stmt, error) 15 | QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) 16 | QueryRowContext(context.Context, string, ...interface{}) *sql.Row 17 | } 18 | 19 | func New(db DBTX) *Queries { 20 | return &Queries{db: db} 21 | } 22 | 23 | type Queries struct { 24 | db DBTX 25 | } 26 | 27 | func (q *Queries) WithTx(tx *sql.Tx) *Queries { 28 | return &Queries{ 29 | db: tx, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pkg/sqlite/nix_v10/models.go: -------------------------------------------------------------------------------- 1 | // Code generated by sqlc. DO NOT EDIT. 2 | // versions: 3 | // sqlc v1.27.0 4 | 5 | package nix_v10 6 | 7 | import ( 8 | "database/sql" 9 | ) 10 | 11 | type DerivationOutput struct { 12 | Drv int64 13 | ID string 14 | Path string 15 | } 16 | 17 | type Ref struct { 18 | Referrer int64 19 | Reference int64 20 | } 21 | 22 | type SqliteSequence struct { 23 | Name interface{} 24 | Seq interface{} 25 | } 26 | 27 | type ValidPath struct { 28 | ID int64 29 | Path string 30 | Hash string 31 | Registrationtime int64 32 | Deriver sql.NullString 33 | Narsize sql.NullInt64 34 | Ultimate sql.NullInt64 35 | Sigs sql.NullString 36 | Ca sql.NullString 37 | } 38 | -------------------------------------------------------------------------------- /pkg/sqlite/nix_v10/query.sql: -------------------------------------------------------------------------------- 1 | -- name: RegisterValidPath :exec 2 | insert into ValidPaths (path, hash, registrationTime, deriver, narSize, ultimate, sigs, ca) 3 | values (?, ?, ?, ?, ?, ?, ?, ?); 4 | 5 | -- name: UpdatePathInfo :exec 6 | update ValidPaths set narSize = ?, hash = ?, ultimate = ?, sigs = ?, ca = ? where path = ?; 7 | 8 | -- name: AddReference :exec 9 | insert or replace into Refs (referrer, reference) values (?, ?); 10 | 11 | -- name: QueryPathInfo :one 12 | select id, hash, registrationTime, deriver, narSize, ultimate, sigs, ca from ValidPaths where path = ?; 13 | 14 | -- name: QueryReferences :many 15 | select path from Refs join ValidPaths on reference = id where referrer = ?; 16 | 17 | -- name: QueryReferrers :many 18 | select path from Refs join ValidPaths on referrer = id where reference = (select vp.id from ValidPaths as vp where vp.path = ?); 19 | 20 | -- name: InvalidatePath :exec 21 | delete from ValidPaths where path = ?; 22 | 23 | -- name: AddDerivationOutput :exec 24 | insert or replace into DerivationOutputs (drv, id, path) values (?, ?, ?); 25 | 26 | -- name: QueryValidDerivers :many 27 | select v.id, v.path from DerivationOutputs d join ValidPaths v on d.drv = v.id where d.path = ?; 28 | 29 | -- name: QueryDerivationOutputs :many 30 | select id, path from DerivationOutputs where drv = ?; 31 | 32 | -- name: QueryPathFromHashPart :one 33 | select path from ValidPaths where path >= ? limit 1; -------------------------------------------------------------------------------- /pkg/sqlite/nix_v10/schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE ValidPaths ( 2 | id integer primary key autoincrement not null, 3 | path text unique not null, 4 | hash text not null, -- base16 representation 5 | registrationTime integer not null, 6 | deriver text, 7 | narSize integer, 8 | ultimate integer, -- null implies "false" 9 | sigs text, -- space-separated 10 | ca text -- if not null, an assertion that the path is content-addressed; see ValidPathInfo 11 | ); 12 | CREATE TABLE sqlite_sequence(name,seq); 13 | CREATE TABLE Refs ( 14 | referrer integer not null, 15 | reference integer not null, 16 | primary key (referrer, reference), 17 | foreign key (referrer) references ValidPaths(id) on delete cascade, 18 | foreign key (reference) references ValidPaths(id) on delete restrict 19 | ); 20 | CREATE INDEX IndexReferrer on Refs(referrer); 21 | CREATE INDEX IndexReference on Refs(reference); 22 | CREATE TRIGGER DeleteSelfRefs before delete on ValidPaths 23 | begin 24 | delete from Refs where referrer = old.id and reference = old.id; 25 | end; 26 | CREATE TABLE DerivationOutputs ( 27 | drv integer not null, 28 | id text not null, -- symbolic output id, usually "out" 29 | path text not null, 30 | primary key (drv, id), 31 | foreign key (drv) references ValidPaths(id) on delete cascade 32 | ); 33 | CREATE INDEX IndexDerivationOutputs on DerivationOutputs(path); 34 | -------------------------------------------------------------------------------- /pkg/sqlite/sqlite.go: -------------------------------------------------------------------------------- 1 | package sqlite 2 | 3 | import ( 4 | "database/sql" 5 | "fmt" 6 | 7 | // enable the sqlite3 driver. 8 | _ "github.com/mattn/go-sqlite3" 9 | "github.com/nix-community/go-nix/pkg/sqlite/binary_cache_v6" 10 | "github.com/nix-community/go-nix/pkg/sqlite/eval_cache_v5" 11 | "github.com/nix-community/go-nix/pkg/sqlite/fetcher_cache_v2" 12 | "github.com/nix-community/go-nix/pkg/sqlite/nix_v10" 13 | ) 14 | 15 | func BinaryCacheV6(dsn string) (*sql.DB, *binary_cache_v6.Queries, error) { 16 | db, err := sql.Open("sqlite3", dsn) 17 | if err != nil { 18 | return nil, nil, fmt.Errorf("failed to open database: %w", err) 19 | } 20 | 21 | return db, binary_cache_v6.New(db), nil 22 | } 23 | 24 | func EvalCacheV5(dsn string) (*sql.DB, *eval_cache_v5.Queries, error) { 25 | db, err := sql.Open("sqlite3", dsn) 26 | if err != nil { 27 | return nil, nil, fmt.Errorf("failed to open database: %w", err) 28 | } 29 | 30 | return db, eval_cache_v5.New(db), nil 31 | } 32 | 33 | func FetcherCacheV2(dsn string) (*sql.DB, *fetcher_cache_v2.Queries, error) { 34 | db, err := sql.Open("sqlite3", dsn) 35 | if err != nil { 36 | return nil, nil, fmt.Errorf("failed to open database: %w", err) 37 | } 38 | 39 | return db, fetcher_cache_v2.New(db), nil 40 | } 41 | 42 | func NixV10(dsn string) (*sql.DB, *nix_v10.Queries, error) { 43 | db, err := sql.Open("sqlite3", dsn) 44 | if err != nil { 45 | return nil, nil, fmt.Errorf("failed to open database: %w", err) 46 | } 47 | 48 | return db, nix_v10.New(db), nil 49 | } 50 | -------------------------------------------------------------------------------- /pkg/sqlite/sqlite_test.go: -------------------------------------------------------------------------------- 1 | //go:build integration 2 | 3 | package sqlite 4 | 5 | import ( 6 | "context" 7 | "fmt" 8 | "os/exec" 9 | "testing" 10 | 11 | "github.com/adrg/xdg" 12 | "github.com/nix-community/go-nix/pkg/sqlite/fetcher_cache_v2" 13 | 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | func TestBinaryCacheV6(t *testing.T) { 18 | as := require.New(t) 19 | 20 | // open our user-specific binary cache db 21 | path, err := xdg.CacheFile("nix/binary-cache-v6.sqlite") 22 | as.NoError(err, "failed to resolve binary cache file") 23 | as.FileExists(path) 24 | 25 | // open the sqlite db 26 | db, queries, err := BinaryCacheV6(fmt.Sprintf("file:%s?mode=ro", path)) 27 | as.NoError(err) 28 | defer db.Close() 29 | 30 | // perform a basic query, we aren't interested in the result 31 | _, err = queries.QueryLastPurge(context.Background()) 32 | as.NoError(err) 33 | } 34 | 35 | func TestFetcherCacheV2(t *testing.T) { 36 | as := require.New(t) 37 | 38 | // open our user-specific binary cache db 39 | path, err := xdg.CacheFile("nix/fetcher-cache-v2.sqlite") 40 | as.NoError(err, "failed to resolve fetcher cache file") 41 | as.FileExists(path) 42 | 43 | // open the sqlite db 44 | db, queries, err := FetcherCacheV2(fmt.Sprintf("file:%s?mode=ro", path)) 45 | as.NoError(err) 46 | defer db.Close() 47 | 48 | // perform a basic query, we aren't interested in the result 49 | _, err = queries.QueryCache(context.Background(), fetcher_cache_v2.QueryCacheParams{}) 50 | as.NoError(err) 51 | } 52 | 53 | func TestNixV10(t *testing.T) { 54 | as := require.New(t) 55 | 56 | // pull down a known path 57 | path := "/nix/store/kz5clxh7s1n0fnx6d37c1wc2cs9qm53q-hello-2.12.1" 58 | as.NoError(exec.Command("nix", "build", "--no-link", "--refresh", path).Run(), "failed to pull hello path") 59 | 60 | // open the sqlite db 61 | db, queries, err := NixV10("file:/nix/var/nix/db/db.sqlite?mode=ro") 62 | as.NoError(err) 63 | defer db.Close() 64 | 65 | // query the path we just pulled down 66 | info, err := queries.QueryPathInfo(context.Background(), path) 67 | as.NoError(err) 68 | as.Equal("sha256:f8340af15f7996faded748bea9e2d0b82a6f7c96417b03f7fa8e1a6a873748e8", info.Hash) 69 | as.Equal("/nix/store/qnavcbp5ydyd12asgz7rpr7is7hlswaz-hello-2.12.1.drv", info.Deriver.String) 70 | as.Equal(int64(226560), info.Narsize.Int64) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/storepath/references/refs.go: -------------------------------------------------------------------------------- 1 | package references 2 | 3 | import ( 4 | "fmt" 5 | "sort" 6 | "strings" 7 | 8 | "github.com/nix-community/go-nix/pkg/nixbase32" 9 | "github.com/nix-community/go-nix/pkg/storepath" 10 | ) 11 | 12 | const ( 13 | storePrefixLength = len(storepath.StoreDir) + 1 14 | refLength = len(nixbase32.Alphabet) // Store path hash prefix length 15 | ) 16 | 17 | // ReferenceScanner scans a stream of data for references to store paths to extract run time dependencies. 18 | type ReferenceScanner struct { 19 | // Map of store path hashes to full store paths. 20 | hashes map[string]string 21 | 22 | // Set of hits. 23 | hits map[string]struct{} 24 | 25 | // Buffer for current partial hit. 26 | buf [refLength]byte 27 | 28 | // How far into buf is currently written. 29 | n int 30 | } 31 | 32 | func NewReferenceScanner(storePathCandidates []string) (*ReferenceScanner, error) { 33 | var buf [refLength]byte 34 | 35 | hashes := make(map[string]string) 36 | 37 | for _, storePath := range storePathCandidates { 38 | if !strings.HasPrefix(storePath, storepath.StoreDir) { 39 | return nil, fmt.Errorf("missing store path prefix: %s", storePath) 40 | } 41 | 42 | // Check length is a valid store path length including dashes 43 | if len(storePath) < len(storepath.StoreDir)+refLength+3 { 44 | return nil, fmt.Errorf("invalid store path length: %d for store path '%s'", len(storePath), storePath) 45 | } 46 | 47 | hashes[storePath[storePrefixLength:storePrefixLength+refLength]] = storePath 48 | } 49 | 50 | return &ReferenceScanner{ 51 | hits: make(map[string]struct{}), 52 | hashes: hashes, 53 | buf: buf, 54 | n: 0, 55 | }, nil 56 | } 57 | 58 | func (r *ReferenceScanner) References() []string { 59 | paths := make([]string, len(r.hits)) 60 | 61 | i := 0 62 | 63 | for hash := range r.hits { 64 | paths[i] = r.hashes[hash] 65 | i++ 66 | } 67 | 68 | sort.Strings(paths) 69 | 70 | return paths 71 | } 72 | 73 | func (r *ReferenceScanner) Write(s []byte) (int, error) { 74 | for _, c := range s { 75 | if !nixbase32.Is(c) { 76 | r.n = 0 77 | 78 | continue 79 | } 80 | 81 | r.buf[r.n] = c 82 | r.n++ 83 | 84 | if r.n == refLength { 85 | hash := string(r.buf[:]) 86 | if _, ok := r.hashes[hash]; ok { 87 | r.hits[hash] = struct{}{} 88 | } 89 | 90 | r.n = 0 91 | } 92 | } 93 | 94 | return len(s), nil 95 | } 96 | -------------------------------------------------------------------------------- /pkg/storepath/references/refs_test.go: -------------------------------------------------------------------------------- 1 | package references_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nix-community/go-nix/pkg/storepath/references" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | //nolint:gochecknoglobals 11 | var cases = []struct { 12 | Title string 13 | Chunks []string 14 | Expected []string 15 | }{ 16 | { 17 | Title: "Basic", 18 | Chunks: []string{ 19 | "/nix/store/knn6wc1a89c47yb70qwv56rmxylia6wx-hello-2.12/bin/hello", 20 | }, 21 | Expected: []string{ 22 | "/nix/store/knn6wc1a89c47yb70qwv56rmxylia6wx-hello-2.12", 23 | }, 24 | }, 25 | { 26 | Title: "PartialWrites", 27 | Chunks: []string{ 28 | "/nix/store/knn6wc1a89c47yb70", 29 | "qwv56rmxylia6wx-hello-2.12/bin/hello", 30 | }, 31 | Expected: []string{ 32 | "/nix/store/knn6wc1a89c47yb70qwv56rmxylia6wx-hello-2.12", 33 | }, 34 | }, 35 | { 36 | Title: "IgnoredPaths", 37 | Chunks: []string{ 38 | "/nix/store/knn6wc1a89c47yb70qwv56rmxylia6wx-hello-2.12/bin/hello", 39 | "/nix/store/c4pcgriqgiwz8vxrjxg7p38q3y7w3ni3-go-1.18.2/bin/go", 40 | }, 41 | Expected: []string{ 42 | "/nix/store/knn6wc1a89c47yb70qwv56rmxylia6wx-hello-2.12", 43 | }, 44 | }, 45 | } 46 | 47 | func TestReferences(t *testing.T) { 48 | t.Run("ScanReferences", func(t *testing.T) { 49 | for _, c := range cases { 50 | t.Run(c.Title, func(t *testing.T) { 51 | refScanner, err := references.NewReferenceScanner(c.Expected) 52 | if err != nil { 53 | panic(err) 54 | } 55 | 56 | for _, line := range c.Chunks { 57 | _, err = refScanner.Write([]byte(line)) 58 | if err != nil { 59 | panic(err) 60 | } 61 | } 62 | 63 | assert.Equal(t, c.Expected, refScanner.References()) 64 | }) 65 | } 66 | }) 67 | } 68 | 69 | func BenchmarkReferences(b *testing.B) { 70 | for _, c := range cases { 71 | c := c 72 | 73 | refScanner, err := references.NewReferenceScanner(c.Expected) 74 | if err != nil { 75 | panic(err) 76 | } 77 | 78 | chunks := make([][]byte, len(c.Chunks)) 79 | for i, c := range c.Chunks { 80 | chunks[i] = []byte(c) 81 | } 82 | 83 | b.Run(c.Title, func(b *testing.B) { 84 | for i := 0; i < b.N; i++ { 85 | for _, chunk := range chunks { 86 | _, err = refScanner.Write(chunk) 87 | if err != nil { 88 | panic(err) 89 | } 90 | } 91 | } 92 | 93 | assert.Equal(b, c.Expected, refScanner.References()) 94 | }) 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /pkg/storepath/storepath.go: -------------------------------------------------------------------------------- 1 | // Package storepath parses and renders Nix store paths. 2 | package storepath 3 | 4 | import ( 5 | "fmt" 6 | "path" 7 | "regexp" 8 | 9 | "github.com/nix-community/go-nix/pkg/nixbase32" 10 | ) 11 | 12 | const ( 13 | StoreDir = "/nix/store" 14 | PathHashSize = 20 15 | ) 16 | 17 | //nolint:gochecknoglobals 18 | var ( 19 | NameRe = regexp.MustCompile(`[a-zA-Z0-9+\-_?=][.a-zA-Z0-9+\-_?=]*`) 20 | PathRe = regexp.MustCompile(fmt.Sprintf( 21 | `^%v/([%v]{%d})-(%v)$`, 22 | regexp.QuoteMeta(StoreDir), 23 | nixbase32.Alphabet, 24 | nixbase32.EncodedLen(PathHashSize), 25 | NameRe, 26 | )) 27 | 28 | // Length of the hash portion of the store path in base32. 29 | encodedPathHashSize = nixbase32.EncodedLen(PathHashSize) 30 | 31 | // Offset in absolute string to hash. 32 | hashOffset = len(StoreDir) + 1 33 | // Offset in relative path string to name. 34 | nameOffset = encodedPathHashSize + 1 35 | ) 36 | 37 | // StorePath represents a bare Nix store path, without any paths underneath `/nix/store/…-…`. 38 | type StorePath struct { 39 | Name string 40 | Digest []byte 41 | } 42 | 43 | // String returns a Store without StoreDir. 44 | // It starts with a digest (20 bytes), nixbase32-encoded, 45 | // followed by a `-`, and ends with the name. 46 | func (n *StorePath) String() string { 47 | return nixbase32.EncodeToString(n.Digest) + "-" + n.Name 48 | } 49 | 50 | // Absolute returns a StorePath with StoreDir and slash prepended. 51 | // We use forward slashes on all architectures (including Windows), to be 52 | // consistent in hashing contexts. 53 | func (n *StorePath) Absolute() string { 54 | return path.Join(StoreDir, n.String()) 55 | } 56 | 57 | // Validate validates a StorePath, verifying it's syntactically valid. 58 | func (n *StorePath) Validate() error { 59 | return Validate(n.Absolute()) 60 | } 61 | 62 | // FromString parses a Nix store path without store prefix into a StorePath, 63 | // verifying it's syntactically valid. 64 | // It returns an error if it fails to parse. 65 | func FromString(s string) (*StorePath, error) { 66 | if err := Validate(path.Join(StoreDir, s)); err != nil { 67 | return nil, err 68 | } 69 | 70 | digest, err := nixbase32.DecodeString(s[:nameOffset-1]) 71 | if err != nil { 72 | return nil, fmt.Errorf("unable to decode hash: %v", err) 73 | } 74 | 75 | return &StorePath{ 76 | Name: s[nameOffset:], 77 | Digest: digest, 78 | }, nil 79 | } 80 | 81 | // FromAbsolutePath parses an absolute Nix Store path including store prefix) 82 | // into a StorePath, verifying it's syntactically valid. 83 | // It returns an error if it fails to parse. 84 | func FromAbsolutePath(s string) (*StorePath, error) { 85 | if len(s) < hashOffset+nameOffset+1 { 86 | return nil, fmt.Errorf("unable to parse path: invalid path length %d for path %v", len(s), s) 87 | } 88 | 89 | return FromString(s[hashOffset:]) 90 | } 91 | 92 | // Validate validates an absolute Nix Store Path string. 93 | func Validate(s string) error { 94 | if len(s) < hashOffset+encodedPathHashSize+1 { 95 | return fmt.Errorf("unable to parse path: invalid path length %d for path %v", len(s), s) 96 | } 97 | 98 | if s[:len(StoreDir)] != StoreDir { 99 | return fmt.Errorf("unable to parse path: mismatching store path prefix for path %v", s) 100 | } 101 | 102 | if err := nixbase32.ValidateString(s[hashOffset : hashOffset+encodedPathHashSize]); err != nil { 103 | return fmt.Errorf("unable to parse path: error validating path nixbase32 %v: %v", err, s) 104 | } 105 | 106 | for _, c := range s[nameOffset:] { 107 | if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') { 108 | switch c { 109 | case '-': 110 | continue 111 | case '_': 112 | continue 113 | case '.': 114 | continue 115 | case '+': 116 | continue 117 | case '?': 118 | continue 119 | case '=': 120 | continue 121 | } 122 | 123 | return fmt.Errorf("unable to parse path: invalid character in path: %v", s) 124 | } 125 | } 126 | 127 | return nil 128 | } 129 | -------------------------------------------------------------------------------- /pkg/storepath/storepath_test.go: -------------------------------------------------------------------------------- 1 | package storepath_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/nix-community/go-nix/pkg/storepath" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestStorePath(t *testing.T) { 11 | t.Run("happy path", func(t *testing.T) { 12 | exampleAbsolutePath := "/nix/store/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432" 13 | exampleNonAbsolutePath := "00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432" 14 | 15 | t.Run("FromString", func(t *testing.T) { 16 | storePath, err := storepath.FromString(exampleNonAbsolutePath) 17 | 18 | if assert.NoError(t, err) { 19 | assert.Equal(t, "net-tools-1.60_p20170221182432", storePath.Name) 20 | assert.Equal(t, []byte{ 21 | 0x8a, 0x12, 0x32, 0x15, 0x22, 0xfd, 0x91, 0xef, 0xbd, 0x60, 0xeb, 0xb2, 0x48, 0x1a, 0xf8, 0x85, 22 | 0x80, 0xf6, 0x16, 0x00, 23 | }, storePath.Digest) 24 | } 25 | 26 | // Test String() and Absolute() 27 | assert.Equal(t, exampleNonAbsolutePath, storePath.String()) 28 | assert.Equal(t, exampleAbsolutePath, storePath.Absolute()) 29 | }) 30 | 31 | t.Run("FromAbsolutePath", func(t *testing.T) { 32 | storePath, err := storepath.FromAbsolutePath(exampleAbsolutePath) 33 | 34 | if assert.NoError(t, err) { 35 | assert.Equal(t, "net-tools-1.60_p20170221182432", storePath.Name) 36 | assert.Equal(t, []byte{ 37 | 0x8a, 0x12, 0x32, 0x15, 0x22, 0xfd, 0x91, 0xef, 0xbd, 0x60, 0xeb, 0xb2, 0x48, 0x1a, 0xf8, 0x85, 38 | 0x80, 0xf6, 0x16, 0x00, 39 | }, storePath.Digest) 40 | } 41 | 42 | // Test String() and Absolute() 43 | assert.Equal(t, exampleNonAbsolutePath, storePath.String()) 44 | assert.Equal(t, exampleAbsolutePath, storePath.Absolute()) 45 | }) 46 | }) 47 | 48 | t.Run("invalid hash length", func(t *testing.T) { 49 | s := "00bgd045z0d4icpbc2yy-net-tools-1.60_p20170221182432" 50 | 51 | _, err := storepath.FromString(s) 52 | assert.Error(t, err) 53 | 54 | err = storepath.Validate(s) 55 | assert.Error(t, err) 56 | }) 57 | 58 | t.Run("invalid encoding in hash", func(t *testing.T) { 59 | s := "00bgd045z0d4icpbc2yyz4gx48aku4la-net-tools-1.60_p20170221182432" 60 | 61 | _, err := storepath.FromString(s) 62 | assert.Error(t, err) 63 | 64 | err = storepath.Validate(s) 65 | assert.Error(t, err) 66 | }) 67 | 68 | t.Run("more than just the bare nix store path", func(t *testing.T) { 69 | s := "00bgd045z0d4icpbc2yyz4gx48aku4la-net-tools-1.60_p20170221182432/bin/arp" 70 | 71 | _, err := storepath.FromString(s) 72 | assert.Error(t, err) 73 | 74 | err = storepath.Validate(s) 75 | assert.Error(t, err) 76 | }) 77 | } 78 | 79 | func BenchmarkStorePath(b *testing.B) { 80 | path := "00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432" 81 | pathAbsolute := storepath.StoreDir + "/00bgd045z0d4icpbc2yyz4gx48ak44la-net-tools-1.60_p20170221182432" 82 | 83 | b.Run("FromString", func(b *testing.B) { 84 | for i := 0; i < b.N; i++ { 85 | _, err := storepath.FromString(path) 86 | if err != nil { 87 | b.Fatal(err) 88 | } 89 | } 90 | }) 91 | 92 | b.Run("FromAbsolutePath", func(b *testing.B) { 93 | for i := 0; i < b.N; i++ { 94 | _, err := storepath.FromAbsolutePath(pathAbsolute) 95 | if err != nil { 96 | b.Fatal(err) 97 | } 98 | } 99 | }) 100 | 101 | b.Run("Validate", func(b *testing.B) { 102 | for i := 0; i < b.N; i++ { 103 | err := storepath.Validate(pathAbsolute) 104 | if err != nil { 105 | b.Fatal(err) 106 | } 107 | } 108 | }) 109 | 110 | { 111 | p, err := storepath.FromString(path) 112 | if err != nil { 113 | b.Fatal(err) 114 | } 115 | 116 | b.Run("ValidateStruct", func(b *testing.B) { 117 | for i := 0; i < b.N; i++ { 118 | err := p.Validate() 119 | if err != nil { 120 | b.Fatal(err) 121 | } 122 | } 123 | }) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /pkg/wire/bytes_reader.go: -------------------------------------------------------------------------------- 1 | package wire 2 | 3 | import ( 4 | "io" 5 | ) 6 | 7 | // BytesReader implements io.ReadCloser. 8 | var _ io.ReadCloser = &BytesReader{} 9 | 10 | // BytesReader implements reading from bytes fields. 11 | // It'll return a limited reader to the actual contents. 12 | // Closing the reader will seek to the end of the packet (including padding). 13 | // It's fine to not close, in case you don't want to seek to the end. 14 | type BytesReader struct { 15 | contentLength uint64 // the total length of the field 16 | lr io.Reader // a reader limited to the actual contents of the field 17 | r io.Reader // the underlying real reader, used when seeking over the padding. 18 | } 19 | 20 | // NewBytesReader constructs a Reader of a bytes packet. 21 | // Closing the reader will skip over any padding. 22 | func NewBytesReader(r io.Reader, contentLength uint64) *BytesReader { 23 | return &BytesReader{ 24 | contentLength: contentLength, 25 | lr: io.LimitReader(r, int64(contentLength)), //nolint:gosec 26 | r: r, 27 | } 28 | } 29 | 30 | // Read will read into b until all bytes from the field have been read 31 | // Keep in mind there might be some padding at the end still, 32 | // which can be seek'ed over by closing the reader. 33 | func (br *BytesReader) Read(b []byte) (int, error) { 34 | n, err := br.lr.Read(b) 35 | 36 | return n, err 37 | } 38 | 39 | // Close will skip to the end and consume any remaining padding. 40 | // It'll return an error if the padding contains something else than null 41 | // bytes. 42 | // It's fine to not close, in case you don't want to seek to the end. 43 | func (br *BytesReader) Close() error { 44 | // seek to the end of the limited reader 45 | for { 46 | buf := make([]byte, 1024) 47 | 48 | _, err := br.lr.Read(buf) 49 | if err != nil { 50 | if err == io.EOF { 51 | break 52 | } 53 | 54 | return err 55 | } 56 | } 57 | // skip over padding 58 | return readPadding(br.r, br.contentLength) 59 | } 60 | -------------------------------------------------------------------------------- /pkg/wire/bytes_writer.go: -------------------------------------------------------------------------------- 1 | package wire 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | var _ io.WriteCloser = &BytesWriter{} 9 | 10 | // BytesWriter implements writing bytes fields. 11 | // It'll return a io.WriteCloser that can be written to. 12 | // On Write(), it'll verify we don't write more than was initially specified. 13 | // On Close(), it'll verify exactly the previously specified number of bytes were written, 14 | // then write any necessary padding. 15 | type BytesWriter struct { 16 | w io.Writer 17 | bytesWritten uint64 // the number of bytes written so far 18 | totalLength uint64 // the expected length of the contents, without padding 19 | paddingWritten bool 20 | } 21 | 22 | func NewBytesWriter(w io.Writer, contentLength uint64) (*BytesWriter, error) { 23 | // write the size field 24 | n := contentLength 25 | if err := WriteUint64(w, n); err != nil { 26 | return nil, err 27 | } 28 | 29 | bytesWriter := &BytesWriter{ 30 | w: w, 31 | bytesWritten: 0, 32 | totalLength: contentLength, 33 | paddingWritten: false, 34 | } 35 | 36 | return bytesWriter, nil 37 | } 38 | 39 | func (bw *BytesWriter) Write(p []byte) (n int, err error) { 40 | l := len(p) 41 | 42 | if bw.bytesWritten+uint64(l) > bw.totalLength { 43 | return 0, fmt.Errorf("maximum number of bytes exceeded") 44 | } 45 | 46 | bytesWritten, err := bw.w.Write(p) 47 | bw.bytesWritten += uint64(bytesWritten) //nolint:gosec 48 | 49 | return bytesWritten, err 50 | } 51 | 52 | // Close ensures the previously specified number of bytes were written, then writes padding. 53 | func (bw *BytesWriter) Close() error { 54 | // if we already closed once, don't close again 55 | if bw.paddingWritten { 56 | return nil 57 | } 58 | 59 | if bw.bytesWritten != bw.totalLength { 60 | return fmt.Errorf("wrote %v bytes in total, but expected %v", bw.bytesWritten, bw.totalLength) 61 | } 62 | 63 | // write padding 64 | err := writePadding(bw.w, bw.totalLength) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | bw.paddingWritten = true 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /pkg/wire/read.go: -------------------------------------------------------------------------------- 1 | package wire 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | ) 7 | 8 | // ReadUint64 consumes exactly 8 bytes and returns a uint64. 9 | func ReadUint64(r io.Reader) (n uint64, err error) { 10 | buf := bufPool.Get().(*[8]byte) 11 | defer bufPool.Put(buf) 12 | 13 | if _, err := io.ReadFull(r, buf[:]); err != nil { 14 | return 0, err 15 | } 16 | 17 | return byteOrder.Uint64(buf[:]), nil 18 | } 19 | 20 | // ReadBool consumes a boolean in nix wire format. 21 | func ReadBool(r io.Reader) (v bool, err error) { 22 | n, err := ReadUint64(r) 23 | if err != nil { 24 | return false, err 25 | } 26 | 27 | if n != 0 && n != 1 { 28 | return false, fmt.Errorf("invalid value for boolean: %v", n) 29 | } 30 | 31 | return n == 1, nil 32 | } 33 | 34 | // readPadding consumes the remaining padding, if any, and errors out if it's not null bytes. 35 | // In nix archive format, byte packets are padded to 8 byte blocks each. 36 | func readPadding(r io.Reader, contentLength uint64) error { 37 | // n marks the position inside the last block 38 | n := contentLength % 8 39 | if n == 0 { 40 | return nil 41 | } 42 | 43 | buf := bufPool.Get().(*[8]byte) 44 | defer bufPool.Put(buf) 45 | 46 | // we read the padding contents into the tail of the buf slice 47 | if _, err := io.ReadFull(r, buf[n:]); err != nil { 48 | return err 49 | } 50 | // … and check if it's only null bytes 51 | for _, b := range buf[n:] { 52 | if b != 0 { 53 | return fmt.Errorf("invalid padding, should be null bytes, found %v", buf[n:]) 54 | } 55 | } 56 | 57 | return nil 58 | } 59 | 60 | // ReadBytes parses the size field, and returns a ReadCloser to its contents. 61 | // That reader is limited to the actual contents of the bytes field. 62 | // Closing the reader will skip to the end of the last byte packet, including the padding. 63 | func ReadBytes(r io.Reader) (uint64, io.ReadCloser, error) { 64 | // read content length 65 | contentLength, err := ReadUint64(r) 66 | if err != nil { 67 | return 0, nil, err 68 | } 69 | 70 | return contentLength, NewBytesReader(r, contentLength), nil 71 | } 72 | 73 | // ReadBytesFull reads a byte packet, and will return its content, or an error. 74 | // A maximum number of bytes can be specified in max. 75 | // In the case of a packet exceeding the maximum number of bytes, 76 | // the reader won't seek to the end of the packet. 77 | func ReadBytesFull(r io.Reader, maxBytes uint64) ([]byte, error) { 78 | contentLength, rd, err := ReadBytes(r) 79 | if err != nil { 80 | return []byte{}, err 81 | } 82 | 83 | if contentLength > maxBytes { 84 | return nil, fmt.Errorf("content length of %v bytes exceeds maximum of %v bytes", contentLength, maxBytes) 85 | } 86 | 87 | defer rd.Close() 88 | 89 | // consume content 90 | buf := make([]byte, contentLength) 91 | if _, err := io.ReadFull(rd, buf); err != nil { 92 | return nil, err 93 | } 94 | 95 | return buf, nil 96 | } 97 | 98 | // ReadString reads a bytes packet and converts it to string. 99 | func ReadString(r io.Reader, maxBytes uint64) (string, error) { 100 | buf, err := ReadBytesFull(r, maxBytes) 101 | 102 | return string(buf), err 103 | } 104 | -------------------------------------------------------------------------------- /pkg/wire/read_test.go: -------------------------------------------------------------------------------- 1 | package wire_test 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "testing" 7 | 8 | "github.com/nix-community/go-nix/pkg/wire" 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | //nolint:gochecknoglobals 13 | var ( 14 | wireBytesFalse = []byte{0, 0, 0, 0, 0, 0, 0, 0} 15 | wireBytesTrue = []byte{1, 0, 0, 0, 0, 0, 0, 0} 16 | wireBytesInvalidBool = []byte{2, 0, 0, 0, 0, 0, 0, 0} 17 | 18 | contents8Bytes = []byte{ 19 | 42, 23, 42, 23, 42, 23, 42, 23, // the actual data 20 | } 21 | wire8Bytes = []byte{ 22 | 8, 0, 0, 0, 0, 0, 0, 0, // length field - 8 bytes 23 | 42, 23, 42, 23, 42, 23, 42, 23, // the actual data 24 | } 25 | 26 | contents10Bytes = []byte{ 27 | 42, 23, 42, 23, 42, 23, 42, 23, // the actual data 28 | 42, 23, 29 | } 30 | wire10Bytes = []byte{ 31 | 10, 0, 0, 0, 0, 0, 0, 0, // length field - 8 bytes 32 | 42, 23, 42, 23, 42, 23, 42, 23, // the actual data 33 | 42, 23, 0, 0, 0, 0, 0, 0, // more actual data (2 bytes), then padding 34 | } 35 | 36 | wireStringFoo = []byte{ 37 | 3, 0, 0, 0, 0, 0, 0, 0, // length field - 3 bytes 38 | 0x46, 0x6F, 0x6F, 0, 0, 0, 0, 0, // contents, Foo, then 5 bytes padding 39 | } 40 | ) 41 | 42 | // hesitantReader implements an io.Reader. 43 | type hesitantReader struct { 44 | data [][]byte 45 | } 46 | 47 | // Read returns the topmost []byte in data, or io.EOF if empty. 48 | func (r *hesitantReader) Read(p []byte) (n int, err error) { 49 | if len(r.data) == 0 { 50 | return 0, io.EOF 51 | } 52 | 53 | copy(p, r.data[0]) 54 | lenRead := len(r.data[0]) 55 | 56 | // pop first element in r.data 57 | r.data = r.data[1:] 58 | 59 | return lenRead, nil 60 | } 61 | 62 | // TestReadUint64 tests a reading a single uint64 field. 63 | func TestReadUint64(t *testing.T) { 64 | bs := []byte{13, 0, 0, 0, 0, 0, 0, 0} 65 | r := bytes.NewReader(bs) 66 | 67 | num, err := wire.ReadUint64(r) 68 | 69 | assert.NoError(t, err) 70 | assert.Equal(t, num, uint64(13)) 71 | } 72 | 73 | // TestReadLongLongPartial tests reading a single uint64 field, but through a 74 | // reader not returning everything at once. 75 | func TestReadUint64Slow(t *testing.T) { 76 | r := &hesitantReader{data: [][]byte{ 77 | {13}, 78 | {}, 79 | {0, 0, 0, 0, 0, 0, 0}, 80 | }} 81 | 82 | num, err := wire.ReadUint64(r) 83 | assert.NoError(t, err) 84 | assert.Equal(t, num, uint64(13)) 85 | } 86 | 87 | // TestReadBool tests reading boolean values works. 88 | func TestReadBool(t *testing.T) { 89 | rdBytesFalse := bytes.NewReader(wireBytesFalse) 90 | rdBytesTrue := bytes.NewReader(wireBytesTrue) 91 | rdBytesInvalidBool := bytes.NewReader(wireBytesInvalidBool) 92 | 93 | v, err := wire.ReadBool(rdBytesFalse) 94 | if assert.NoError(t, err) { 95 | assert.Equal(t, v, false) 96 | } 97 | 98 | v, err = wire.ReadBool(rdBytesTrue) 99 | if assert.NoError(t, err) { 100 | assert.Equal(t, v, true) 101 | } 102 | 103 | _, err = wire.ReadBool(rdBytesInvalidBool) 104 | assert.Error(t, err) 105 | } 106 | 107 | func TestReadBytes(t *testing.T) { 108 | buf, err := wire.ReadBytesFull(bytes.NewReader(wire8Bytes), 1024) 109 | if assert.NoError(t, err) { 110 | assert.Equal(t, 8, len(buf)) 111 | assert.Equal(t, buf, contents8Bytes) 112 | } 113 | 114 | buf, err = wire.ReadBytesFull(bytes.NewReader(wire10Bytes), 1024) 115 | if assert.NoError(t, err) { 116 | assert.Equal(t, 10, len(buf)) 117 | assert.Equal(t, buf, contents10Bytes) 118 | } 119 | 120 | // concatenate the 10 bytes, then 8 bytes dummy data together, 121 | // and see if we can get out both bytes. This will test we properly skip over the padding. 122 | payloadCombined := []byte{} 123 | payloadCombined = append(payloadCombined, wire10Bytes...) 124 | payloadCombined = append(payloadCombined, wire8Bytes...) 125 | 126 | rd := bytes.NewReader(payloadCombined) 127 | 128 | buf, err = wire.ReadBytesFull(rd, 1024) 129 | if assert.NoError(t, err) { 130 | assert.Equal(t, 10, len(buf)) 131 | assert.Equal(t, buf, contents10Bytes) 132 | } 133 | 134 | buf, err = wire.ReadBytesFull(rd, 1024) 135 | if assert.NoError(t, err) { 136 | assert.Equal(t, 8, len(buf)) 137 | assert.Equal(t, buf, contents8Bytes) 138 | } 139 | } 140 | 141 | func TestReadString(t *testing.T) { 142 | s, err := wire.ReadString(bytes.NewReader(wireStringFoo), 1024) 143 | if assert.NoError(t, err) { 144 | assert.Equal(t, s, "Foo") 145 | } 146 | 147 | // exceeding max should error 148 | rd := bytes.NewReader(wireStringFoo) 149 | _, err = wire.ReadString(rd, 2) 150 | assert.Error(t, err) 151 | 152 | // the reader should not have seeked to the end of the packet 153 | buf, err := io.ReadAll(rd) 154 | if assert.NoError(t, err, "reading the rest shouldn't error") { 155 | assert.Equal(t, wireStringFoo[8:], buf, "the reader should not have seeked to the end of the packet") 156 | } 157 | } 158 | -------------------------------------------------------------------------------- /pkg/wire/wire.go: -------------------------------------------------------------------------------- 1 | // Package wire provides methods to parse and produce fields used in the 2 | // low-level Nix wire protocol, operating on io.Reader and io.Writer 3 | // When reading fields with arbitrary lengths, a maximum number of bytes needs 4 | // to be specified. 5 | package wire 6 | 7 | import ( 8 | "encoding/binary" 9 | ) 10 | 11 | //nolint:gochecknoglobals 12 | var byteOrder = binary.LittleEndian 13 | -------------------------------------------------------------------------------- /pkg/wire/write.go: -------------------------------------------------------------------------------- 1 | package wire 2 | 3 | import ( 4 | "io" 5 | "sync" 6 | ) 7 | 8 | //nolint:gochecknoglobals 9 | var ( 10 | padding [8]byte 11 | 12 | bufPool = sync.Pool{ 13 | New: func() interface{} { 14 | return new([8]byte) 15 | }, 16 | } 17 | ) 18 | 19 | // WriteUint64 writes an uint64 in Nix wire format. 20 | func WriteUint64(w io.Writer, n uint64) error { 21 | buf := bufPool.Get().(*[8]byte) 22 | defer bufPool.Put(buf) 23 | 24 | byteOrder.PutUint64(buf[:], n) 25 | _, err := w.Write(buf[:]) 26 | 27 | return err 28 | } 29 | 30 | // WriteBool writes a boolean in Nix wire format. 31 | func WriteBool(w io.Writer, v bool) error { 32 | if v { 33 | return WriteUint64(w, 1) 34 | } 35 | 36 | return WriteUint64(w, 0) 37 | } 38 | 39 | // WriteBytes writes a bytes packet. See ReadBytes for its structure. 40 | func WriteBytes(w io.Writer, buf []byte) error { 41 | n := uint64(len(buf)) 42 | if err := WriteUint64(w, n); err != nil { 43 | return err 44 | } 45 | 46 | if _, err := w.Write(buf); err != nil { 47 | return err 48 | } 49 | 50 | return writePadding(w, n) 51 | } 52 | 53 | // WriteString writes a bytes packet. 54 | func WriteString(w io.Writer, s string) error { 55 | n := uint64(len(s)) 56 | if err := WriteUint64(w, n); err != nil { 57 | return err 58 | } 59 | 60 | if _, err := io.WriteString(w, s); err != nil { 61 | return err 62 | } 63 | 64 | return writePadding(w, n) 65 | } 66 | 67 | // writePadding writes the appropriate amount of padding. 68 | func writePadding(w io.Writer, contentLength uint64) error { 69 | if m := contentLength % 8; m != 0 { 70 | _, err := w.Write(padding[m:]) 71 | 72 | return err 73 | } 74 | 75 | return nil 76 | } 77 | -------------------------------------------------------------------------------- /pkg/wire/write_test.go: -------------------------------------------------------------------------------- 1 | package wire_test 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | 7 | "github.com/nix-community/go-nix/pkg/wire" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestWriteUint64(t *testing.T) { 12 | var buf bytes.Buffer 13 | 14 | err := wire.WriteUint64(&buf, 1) 15 | assert.NoError(t, err) 16 | assert.Equal(t, wireBytesTrue, buf.Bytes()) 17 | } 18 | 19 | func TestWriteBool(t *testing.T) { 20 | var buf bytes.Buffer 21 | 22 | err := wire.WriteBool(&buf, true) 23 | assert.NoError(t, err) 24 | assert.Equal(t, wireBytesTrue, buf.Bytes()) 25 | 26 | buf.Reset() 27 | err = wire.WriteBool(&buf, false) 28 | assert.NoError(t, err) 29 | assert.Equal(t, wireBytesFalse, buf.Bytes()) 30 | } 31 | 32 | func TestWriteBytes(t *testing.T) { 33 | var buf bytes.Buffer 34 | 35 | err := wire.WriteBytes(&buf, contents8Bytes) 36 | assert.NoError(t, err) 37 | assert.Equal(t, wire8Bytes, buf.Bytes()) 38 | 39 | buf.Reset() 40 | 41 | err = wire.WriteBytes(&buf, contents10Bytes) 42 | assert.NoError(t, err) 43 | assert.Equal(t, wire10Bytes, buf.Bytes()) 44 | } 45 | 46 | func TestWriteString(t *testing.T) { 47 | var buf bytes.Buffer 48 | 49 | err := wire.WriteString(&buf, "Foo") 50 | assert.NoError(t, err) 51 | assert.Equal(t, wireStringFoo, buf.Bytes()) 52 | } 53 | 54 | func TestBytesWriter8Bytes(t *testing.T) { 55 | var buf bytes.Buffer 56 | 57 | bw, err := wire.NewBytesWriter(&buf, uint64(len(contents8Bytes))) 58 | assert.NoError(t, err) 59 | 60 | n, err := bw.Write(contents8Bytes[:4]) 61 | assert.NoError(t, err) 62 | assert.Equal(t, 4, n) 63 | n, err = bw.Write(contents8Bytes[4:]) 64 | assert.NoError(t, err) 65 | assert.Equal(t, 4, n) 66 | 67 | err = bw.Close() 68 | assert.NoError(t, err) 69 | 70 | assert.Equal(t, wire8Bytes, buf.Bytes()) 71 | } 72 | 73 | func TestBytesWriter10Bytes(t *testing.T) { 74 | var buf bytes.Buffer 75 | 76 | bw, err := wire.NewBytesWriter(&buf, uint64(len(contents10Bytes))) 77 | assert.NoError(t, err) 78 | 79 | n, err := bw.Write(contents10Bytes[:4]) 80 | assert.NoError(t, err) 81 | assert.Equal(t, 4, n) 82 | n, err = bw.Write(contents10Bytes[4:]) 83 | assert.NoError(t, err) 84 | assert.Equal(t, 6, n) 85 | 86 | err = bw.Close() 87 | assert.NoError(t, err) 88 | 89 | // closing again shouldn't panic 90 | assert.NotPanics(t, func() { 91 | bw.Close() 92 | }) 93 | 94 | assert.Equal(t, wire10Bytes, buf.Bytes()) 95 | } 96 | 97 | func TestBytesWriterError(t *testing.T) { 98 | var buf bytes.Buffer 99 | 100 | // initialize a bytes writer with a len of 9 101 | bw, err := wire.NewBytesWriter(&buf, 9) 102 | assert.NoError(t, err) 103 | 104 | // try to write 10 bytes into it 105 | _, err = bw.Write(contents10Bytes) 106 | assert.Error(t, err) 107 | 108 | buf.Reset() 109 | 110 | // initialize a bytes writer with a len of 11 111 | bw, err = wire.NewBytesWriter(&buf, 11) 112 | assert.NoError(t, err) 113 | 114 | // write 10 bytes into it 115 | n, err := bw.Write(contents10Bytes) 116 | assert.NoError(t, err) 117 | assert.Equal(t, 10, n) 118 | 119 | err = bw.Close() 120 | assert.Error(t, err, "closing should fail, as one byte is still missing") 121 | } 122 | -------------------------------------------------------------------------------- /shell.nix: -------------------------------------------------------------------------------- 1 | # This file provides backward compatibility to nix < 2.4 clients 2 | { 3 | system ? builtins.currentSystem, 4 | }: 5 | let 6 | lock = builtins.fromJSON (builtins.readFile ./flake.lock); 7 | 8 | root = lock.nodes.${lock.root}; 9 | inherit (lock.nodes.${root.inputs.flake-compat}.locked) 10 | owner 11 | repo 12 | rev 13 | narHash 14 | ; 15 | 16 | flake-compat = fetchTarball { 17 | url = "https://github.com/${owner}/${repo}/archive/${rev}.tar.gz"; 18 | sha256 = narHash; 19 | }; 20 | 21 | flake = import flake-compat { 22 | inherit system; 23 | src = ./.; 24 | }; 25 | in 26 | flake.shellNix 27 | -------------------------------------------------------------------------------- /sqlc.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | sql: 3 | - engine: "sqlite" 4 | queries: "pkg/sqlite/binary_cache_v6/query.sql" 5 | schema: "pkg/sqlite/binary_cache_v6/schema.sql" 6 | gen: 7 | go: 8 | package: "binary_cache_v6" 9 | out: "pkg/sqlite/binary_cache_v6" 10 | - engine: "sqlite" 11 | queries: "pkg/sqlite/eval_cache_v5/query.sql" 12 | schema: "pkg/sqlite/eval_cache_v5/schema.sql" 13 | gen: 14 | go: 15 | package: "eval_cache_v5" 16 | out: "pkg/sqlite/eval_cache_v5" 17 | - engine: "sqlite" 18 | queries: "pkg/sqlite/fetcher_cache_v2/query.sql" 19 | schema: "pkg/sqlite/fetcher_cache_v2/schema.sql" 20 | gen: 21 | go: 22 | package: "fetcher_cache_v2" 23 | out: "pkg/sqlite/fetcher_cache_v2" 24 | - engine: "sqlite" 25 | queries: "pkg/sqlite/nix_v10/query.sql" 26 | schema: "pkg/sqlite/nix_v10/schema.sql" 27 | gen: 28 | go: 29 | package: "nix_v10" 30 | out: "pkg/sqlite/nix_v10" 31 | -------------------------------------------------------------------------------- /test/testdata/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv: -------------------------------------------------------------------------------- 1 | Derive([("out","/nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar","r:sha256","08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba")],[],[],":",":",[],[("builder",":"),("name","bar"),("out","/nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar"),("outputHash","08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba"),("outputHashAlgo","sha256"),("outputHashMode","recursive"),("system",":")]) -------------------------------------------------------------------------------- /test/testdata/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv.json: -------------------------------------------------------------------------------- 1 | { 2 | "/nix/store/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv": { 3 | "args": [], 4 | "builder": ":", 5 | "env": { 6 | "builder": ":", 7 | "name": "bar", 8 | "out": "/nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar", 9 | "outputHash": "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba", 10 | "outputHashAlgo": "sha256", 11 | "outputHashMode": "recursive", 12 | "system": ":" 13 | }, 14 | "inputDrvs": {}, 15 | "inputSrcs": [], 16 | "outputs": { 17 | "out": { 18 | "hash": "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba", 19 | "hashAlgo": "r:sha256", 20 | "path": "/nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar" 21 | } 22 | }, 23 | "system": ":" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/testdata/0zhkga32apid60mm7nh92z2970im5837-bootstrap-tools.drv: -------------------------------------------------------------------------------- 1 | Derive([("out","/nix/store/p4s4jf7aq6v6z9iazll1aiqwb34aqxq9-bootstrap-tools","","")],[("/nix/store/b7irlwi2wjlx5aj1dghx4c8k3ax6m56q-busybox.drv",["out"]),("/nix/store/bzq60ip2z5xgi7jk6jgdw8cngfiwjrcm-bootstrap-tools.tar.xz.drv",["out"])],["/nix/store/wzdwpgqf2384hr2npma78mqillg5lv08-unpack-bootstrap-tools.sh"],"x86_64-linux","/nix/store/lan2w3ab1mvpxj3ppiw2sizh8i7rpz7s-busybox",["ash","-e","/nix/store/wzdwpgqf2384hr2npma78mqillg5lv08-unpack-bootstrap-tools.sh"],[("builder","/nix/store/lan2w3ab1mvpxj3ppiw2sizh8i7rpz7s-busybox"),("isGNU","1"),("langC","1"),("langCC","1"),("name","bootstrap-tools"),("out","/nix/store/p4s4jf7aq6v6z9iazll1aiqwb34aqxq9-bootstrap-tools"),("system","x86_64-linux"),("tarball","/nix/store/cijs9ypwccmdfjhkq9a35nin7qizg6jm-bootstrap-tools.tar.xz")]) -------------------------------------------------------------------------------- /test/testdata/292w8yzv5nn7nhdpxcs8b7vby2p27s09-nested-json.drv: -------------------------------------------------------------------------------- 1 | Derive([("out","/nix/store/pzr7lsd3q9pqsnb42r9b23jc5sh8irvn-nested-json","","")],[],[],":",":",[],[("builder",":"),("json","{\"hello\":\"moto\\n\"}"),("name","nested-json"),("out","/nix/store/pzr7lsd3q9pqsnb42r9b23jc5sh8irvn-nested-json"),("system",":")]) -------------------------------------------------------------------------------- /test/testdata/292w8yzv5nn7nhdpxcs8b7vby2p27s09-nested-json.drv.json: -------------------------------------------------------------------------------- 1 | { 2 | "/nix/store/292w8yzv5nn7nhdpxcs8b7vby2p27s09-nested-json.drv": { 3 | "args": [], 4 | "builder": ":", 5 | "env": { 6 | "builder": ":", 7 | "json": "{\"hello\":\"moto\\n\"}", 8 | "name": "nested-json", 9 | "out": "/nix/store/pzr7lsd3q9pqsnb42r9b23jc5sh8irvn-nested-json", 10 | "system": ":" 11 | }, 12 | "inputDrvs": {}, 13 | "inputSrcs": [], 14 | "outputs": { 15 | "out": { 16 | "path": "/nix/store/pzr7lsd3q9pqsnb42r9b23jc5sh8irvn-nested-json" 17 | } 18 | }, 19 | "system": ":" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/testdata/385bniikgs469345jfsbw24kjfhxrsi0-foo-file.drv: -------------------------------------------------------------------------------- 1 | Derive([("out","/nix/store/hb42ifgavm0d783l9xr0l3ydl76f1hss-foo-file","","")],[],["/nix/store/gy295yl6dvm27wv7rsa6gswiq14zk3za-foofile"],":",":",[],[("builder",":"),("file","/nix/store/gy295yl6dvm27wv7rsa6gswiq14zk3za-foofile"),("name","foo-file"),("out","/nix/store/hb42ifgavm0d783l9xr0l3ydl76f1hss-foo-file"),("system",":")]) -------------------------------------------------------------------------------- /test/testdata/4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv: -------------------------------------------------------------------------------- 1 | Derive([("out","/nix/store/5vyvcwah9l9kf07d52rcgdk70g2f4y13-foo","","")],[("/nix/store/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv",["out"])],[],":",":",[],[("bar","/nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar"),("builder",":"),("name","foo"),("out","/nix/store/5vyvcwah9l9kf07d52rcgdk70g2f4y13-foo"),("system",":")]) -------------------------------------------------------------------------------- /test/testdata/4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv.json: -------------------------------------------------------------------------------- 1 | { 2 | "/nix/store/4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv": { 3 | "args": [], 4 | "builder": ":", 5 | "env": { 6 | "bar": "/nix/store/4q0pg5zpfmznxscq3avycvf9xdvx50n3-bar", 7 | "builder": ":", 8 | "name": "foo", 9 | "out": "/nix/store/5vyvcwah9l9kf07d52rcgdk70g2f4y13-foo", 10 | "system": ":" 11 | }, 12 | "inputDrvs": { 13 | "/nix/store/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv": [ 14 | "out" 15 | ] 16 | }, 17 | "inputSrcs": [], 18 | "outputs": { 19 | "out": { 20 | "path": "/nix/store/5vyvcwah9l9kf07d52rcgdk70g2f4y13-foo" 21 | } 22 | }, 23 | "system": ":" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/testdata/52a9id8hx688hvlnz4d1n25ml1jdykz0-unicode.drv: -------------------------------------------------------------------------------- 1 | Derive([("out","/nix/store/vgvdj6nf7s8kvfbl2skbpwz9kc7xjazc-unicode","","")],[],[],":",":",[],[("builder",":"),("letters","räksmörgås\nrødgrød med fløde\nLübeck\n肥猪\nこんにちは / 今日は\n🌮\n"),("name","unicode"),("out","/nix/store/vgvdj6nf7s8kvfbl2skbpwz9kc7xjazc-unicode"),("system",":")]) -------------------------------------------------------------------------------- /test/testdata/52a9id8hx688hvlnz4d1n25ml1jdykz0-unicode.drv.json: -------------------------------------------------------------------------------- 1 | { 2 | "/nix/store/52a9id8hx688hvlnz4d1n25ml1jdykz0-unicode.drv": { 3 | "outputs": { 4 | "out": { 5 | "path": "/nix/store/vgvdj6nf7s8kvfbl2skbpwz9kc7xjazc-unicode" 6 | } 7 | }, 8 | "inputSrcs": [], 9 | "inputDrvs": {}, 10 | "system": ":", 11 | "builder": ":", 12 | "args": [], 13 | "env": { 14 | "builder": ":", 15 | "letters": "räksmörgås\nrødgrød med fløde\nLübeck\n肥猪\nこんにちは / 今日は\n🌮\n", 16 | "name": "unicode", 17 | "out": "/nix/store/vgvdj6nf7s8kvfbl2skbpwz9kc7xjazc-unicode", 18 | "system": ":" 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /test/testdata/9lj1lkjm2ag622mh4h9rpy6j607an8g2-structured-attrs.drv: -------------------------------------------------------------------------------- 1 | Derive([("out","/nix/store/6a39dl014j57bqka7qx25k0vb20vkqm6-structured-attrs","","")],[],[],":",":",[],[("__json","{\"builder\":\":\",\"name\":\"structured-attrs\",\"system\":\":\"}"),("out","/nix/store/6a39dl014j57bqka7qx25k0vb20vkqm6-structured-attrs")]) -------------------------------------------------------------------------------- /test/testdata/9lj1lkjm2ag622mh4h9rpy6j607an8g2-structured-attrs.drv.json: -------------------------------------------------------------------------------- 1 | { 2 | "/nix/store/9lj1lkjm2ag622mh4h9rpy6j607an8g2-structured-attrs.drv": { 3 | "args": [], 4 | "builder": ":", 5 | "env": { 6 | "__json": "{\"builder\":\":\",\"name\":\"structured-attrs\",\"system\":\":\"}", 7 | "out": "/nix/store/6a39dl014j57bqka7qx25k0vb20vkqm6-structured-attrs" 8 | }, 9 | "inputDrvs": {}, 10 | "inputSrcs": [], 11 | "outputs": { 12 | "out": { 13 | "path": "/nix/store/6a39dl014j57bqka7qx25k0vb20vkqm6-structured-attrs" 14 | } 15 | }, 16 | "system": ":" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/testdata/build-fixtures.go: -------------------------------------------------------------------------------- 1 | //usr/bin/env go run $0 $@ ; exit 2 | 3 | package main 4 | 5 | // This (re-)builds a bunch of fixture files from this folder. 6 | 7 | // It requires the following binaries to be in $PATH: 8 | // - nix-instantiate / nix 9 | 10 | import ( 11 | "fmt" 12 | "io" 13 | "os" 14 | "os/exec" 15 | "path/filepath" 16 | "strings" 17 | ) 18 | 19 | type fixture struct { 20 | file string 21 | attr string 22 | path string 23 | } 24 | 25 | // nolint:gochecknoglobals 26 | var fixtures = []*fixture{ 27 | { 28 | path: "/nix/store/0hm2f1psjpcwg8fijsmr4wwxrx59s092-bar.drv", 29 | file: "derivation_sha256.nix", 30 | attr: "bar", 31 | }, 32 | { 33 | path: "/nix/store/4wvvbi4jwn0prsdxb7vs673qa5h9gr7x-foo.drv", 34 | file: "derivation_sha256.nix", 35 | attr: "foo", 36 | }, 37 | { 38 | path: "/nix/store/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv", 39 | file: "derivation_sha1.nix", 40 | attr: "bar", 41 | }, 42 | { 43 | path: "/nix/store/ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv", 44 | file: "derivation_sha1.nix", 45 | attr: "foo", 46 | }, 47 | { 48 | path: "/nix/store/h32dahq0bx5rp1krcdx3a53asj21jvhk-has-multi-out.drv", 49 | file: "derivation_multi-outputs.nix", 50 | }, 51 | { 52 | path: "/nix/store/292w8yzv5nn7nhdpxcs8b7vby2p27s09-nested-json.drv", 53 | file: "derivation_nested-json.nix", 54 | }, 55 | { 56 | path: "/nix/store/9lj1lkjm2ag622mh4h9rpy6j607an8g2-structured-attrs.drv", 57 | file: "derivation_structured.nix", 58 | }, 59 | } 60 | 61 | func buildFixture(fixture *fixture) error { 62 | // nolint:gosec 63 | cmd := exec.Command("nix-instantiate", fixture.file, "-A", fixture.attr) 64 | cmd.Stderr = os.Stderr 65 | 66 | out, err := cmd.Output() 67 | if err != nil { 68 | return err 69 | } 70 | 71 | drvPath := strings.TrimSpace(string(out)) 72 | 73 | if fixture.path != "" && fixture.path != drvPath { 74 | return fmt.Errorf("mismatch in expected drv path: %s != %s", fixture.path, drvPath) 75 | } 76 | 77 | // Copy drv contents 78 | { 79 | fin, err := os.Open(drvPath) 80 | if err != nil { 81 | return err 82 | } 83 | defer fin.Close() 84 | 85 | fout, err := os.Create(filepath.Base(drvPath)) 86 | if err != nil { 87 | return err 88 | } 89 | defer fout.Close() 90 | 91 | _, err = io.Copy(fout, fin) 92 | if err != nil { 93 | return err 94 | } 95 | } 96 | 97 | // Get JSON contents 98 | { 99 | cmd := exec.Command("nix", "show-derivation", drvPath) 100 | cmd.Stderr = os.Stderr 101 | 102 | fout, err := os.Create(filepath.Base(drvPath) + ".json") 103 | if err != nil { 104 | return err 105 | } 106 | defer fout.Close() 107 | 108 | cmd.Stdout = fout 109 | 110 | err = cmd.Start() 111 | if err != nil { 112 | return err 113 | } 114 | 115 | err = cmd.Wait() 116 | if err != nil { 117 | return err 118 | } 119 | } 120 | 121 | return nil 122 | } 123 | 124 | func main() { 125 | for _, fixture := range fixtures { 126 | err := buildFixture(fixture) 127 | if err != nil { 128 | panic(err) 129 | } 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /test/testdata/ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv: -------------------------------------------------------------------------------- 1 | Derive([("out","/nix/store/fhaj6gmwns62s6ypkcldbaj2ybvkhx3p-foo","","")],[("/nix/store/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv",["out"])],[],":",":",[],[("bar","/nix/store/mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar"),("builder",":"),("name","foo"),("out","/nix/store/fhaj6gmwns62s6ypkcldbaj2ybvkhx3p-foo"),("system",":")]) -------------------------------------------------------------------------------- /test/testdata/ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv.json: -------------------------------------------------------------------------------- 1 | { 2 | "/nix/store/ch49594n9avinrf8ip0aslidkc4lxkqv-foo.drv": { 3 | "args": [], 4 | "builder": ":", 5 | "env": { 6 | "bar": "/nix/store/mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar", 7 | "builder": ":", 8 | "name": "foo", 9 | "out": "/nix/store/fhaj6gmwns62s6ypkcldbaj2ybvkhx3p-foo", 10 | "system": ":" 11 | }, 12 | "inputDrvs": { 13 | "/nix/store/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv": [ 14 | "out" 15 | ] 16 | }, 17 | "inputSrcs": [], 18 | "outputs": { 19 | "out": { 20 | "path": "/nix/store/fhaj6gmwns62s6ypkcldbaj2ybvkhx3p-foo" 21 | } 22 | }, 23 | "system": ":" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/testdata/cl5fr6hlr6hdqza2vgb9qqy5s26wls8i-jq-1.6.drv: -------------------------------------------------------------------------------- 1 | Derive([("bin","/nix/store/amh6f24qs9809zg9xzckfi90ysfi8r2a-jq-1.6-bin","",""),("dev","/nix/store/0jmbidsi4asvlqlgnsqrcfyddx7icq2h-jq-1.6-dev","",""),("doc","/nix/store/q5pywa8m8zz0d5v4b3f17pafqwia81yd-jq-1.6-doc","",""),("lib","/nix/store/95mivp8m5gsv88ar0apd0xb0jvlzzd83-jq-1.6-lib","",""),("man","/nix/store/dhk7c8fbzzlhcpb2c7fdrwqsz761msrl-jq-1.6-man","",""),("out","/nix/store/gz5wackiq656d26w298hkqf2494c21kr-jq-1.6","","")],[("/nix/store/073gancjdr3z1scm2p553v0k3cxj2cpy-fix-tests-when-building-without-regex-supports.patch.drv",["out"]),("/nix/store/15qnffsb7c5qn6577b1g36d8blvasp8x-source.drv",["out"]),("/nix/store/77krna4j969zayr43hwxy7srrg76m7zp-bash-5.1-p16.drv",["out"]),("/nix/store/gmv4lkgbmjl90lpqn66cv5gyzghdhivr-stdenv-linux.drv",["out"]),("/nix/store/h1xi8g0jf5l5kyjh9kyq9l5d4dxp5y2i-onig-6.9.7.1.drv",["out"]),("/nix/store/zim5sj6nfl1784x5w74yigc6451jnriq-hook.drv",["out"])],["/nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh"],"x86_64-linux","/nix/store/fcd0m68c331j7nkdxvnnpb8ggwsaiqac-bash-5.1-p16/bin/bash",["-e","/nix/store/9krlzvny65gdc8s7kpb6lkx8cd02c25b-default-builder.sh"],[("bin","/nix/store/amh6f24qs9809zg9xzckfi90ysfi8r2a-jq-1.6-bin"),("buildInputs","/nix/store/80ggmgv0wsy9qmdp1y0mdrpla4y291m5-onig-6.9.7.1"),("builder","/nix/store/fcd0m68c331j7nkdxvnnpb8ggwsaiqac-bash-5.1-p16/bin/bash"),("configureFlags","--bindir=${bin}/bin --sbindir=${bin}/bin --datadir=${doc}/share --mandir=${man}/share/man LDFLAGS=-Wl,-rpath,\\${libdir}"),("depsBuildBuild",""),("depsBuildBuildPropagated",""),("depsBuildTarget",""),("depsBuildTargetPropagated",""),("depsHostHost",""),("depsHostHostPropagated",""),("depsTargetTarget",""),("depsTargetTargetPropagated",""),("dev","/nix/store/0jmbidsi4asvlqlgnsqrcfyddx7icq2h-jq-1.6-dev"),("doCheck",""),("doInstallCheck","1"),("doc","/nix/store/q5pywa8m8zz0d5v4b3f17pafqwia81yd-jq-1.6-doc"),("installCheckTarget","check"),("lib","/nix/store/95mivp8m5gsv88ar0apd0xb0jvlzzd83-jq-1.6-lib"),("man","/nix/store/dhk7c8fbzzlhcpb2c7fdrwqsz761msrl-jq-1.6-man"),("name","jq-1.6"),("nativeBuildInputs","/nix/store/0ph6nm5iscn8vv6ihl2azbk2d7997345-hook"),("out","/nix/store/gz5wackiq656d26w298hkqf2494c21kr-jq-1.6"),("outputs","bin doc man dev lib out"),("patches","/nix/store/s0nsdqgd0x6ivb2kzgdzxz700irvvi69-fix-tests-when-building-without-regex-supports.patch"),("pname","jq"),("postInstallCheck","$bin/bin/jq --help >/dev/null\n$bin/bin/jq -r '.values[1]' <<< '{\"values\":[\"hello\",\"world\"]}' | grep '^world$' > /dev/null\n"),("preBuild","rm -r ./modules/oniguruma\n"),("preConfigure","echo \"#!/bin/sh\" > scripts/version\necho \"echo 1.6\" >> scripts/version\npatchShebangs scripts/version\n"),("propagatedBuildInputs",""),("propagatedNativeBuildInputs",""),("src","/nix/store/6ncacqfpl29xv67cilz4axhygyyz1brr-source"),("stdenv","/nix/store/pmyiksh5sgqzakbr84qsfxqy8fgirmic-stdenv-linux"),("strictDeps",""),("system","x86_64-linux"),("version","1.6")]) -------------------------------------------------------------------------------- /test/testdata/cp1252.nix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nix-community/go-nix/4bdde671e0a123e7ac8df2bbd2bcbb2f316f5f55/test/testdata/cp1252.nix -------------------------------------------------------------------------------- /test/testdata/derivation_multi-outputs.nix: -------------------------------------------------------------------------------- 1 | builtins.derivation { 2 | name = "has-multi-out"; 3 | builder = ":"; 4 | system = ":"; 5 | outputs = [ "out" "lib" ]; 6 | } 7 | -------------------------------------------------------------------------------- /test/testdata/derivation_nested-json.nix: -------------------------------------------------------------------------------- 1 | builtins.derivation { 2 | name = "nested-json"; 3 | builder = ":"; 4 | system = ":"; 5 | json = builtins.toJSON { 6 | hello = "moto\n"; 7 | }; 8 | } 9 | -------------------------------------------------------------------------------- /test/testdata/derivation_sha1.nix: -------------------------------------------------------------------------------- 1 | rec { 2 | bar = builtins.derivation { 3 | name = "bar"; 4 | builder = ":"; 5 | system = ":"; 6 | outputHash = "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"; 7 | outputHashAlgo = "sha1"; 8 | outputHashMode = "recursive"; 9 | }; 10 | 11 | foo = builtins.derivation { 12 | name = "foo"; 13 | builder = ":"; 14 | system = ":"; 15 | inherit bar; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /test/testdata/derivation_sha256.nix: -------------------------------------------------------------------------------- 1 | rec { 2 | bar = builtins.derivation { 3 | name = "bar"; 4 | builder = ":"; 5 | system = ":"; 6 | outputHash = "08813cbee9903c62be4c5027726a418a300da4500b2d369d3af9286f4815ceba"; 7 | outputHashAlgo = "sha256"; 8 | outputHashMode = "recursive"; 9 | }; 10 | 11 | foo = builtins.derivation { 12 | name = "foo"; 13 | builder = ":"; 14 | system = ":"; 15 | inherit bar; 16 | }; 17 | } 18 | -------------------------------------------------------------------------------- /test/testdata/derivation_structured.nix: -------------------------------------------------------------------------------- 1 | builtins.derivation { 2 | name = "structured-attrs"; 3 | builder = ":"; 4 | system = ":"; 5 | __structuredAttrs = true; 6 | } 7 | -------------------------------------------------------------------------------- /test/testdata/h32dahq0bx5rp1krcdx3a53asj21jvhk-has-multi-out.drv: -------------------------------------------------------------------------------- 1 | Derive([("lib","/nix/store/2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out-lib","",""),("out","/nix/store/55lwldka5nyxa08wnvlizyqw02ihy8ic-has-multi-out","","")],[],[],":",":",[],[("builder",":"),("lib","/nix/store/2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out-lib"),("name","has-multi-out"),("out","/nix/store/55lwldka5nyxa08wnvlizyqw02ihy8ic-has-multi-out"),("outputs","out lib"),("system",":")]) -------------------------------------------------------------------------------- /test/testdata/h32dahq0bx5rp1krcdx3a53asj21jvhk-has-multi-out.drv.json: -------------------------------------------------------------------------------- 1 | { 2 | "/nix/store/h32dahq0bx5rp1krcdx3a53asj21jvhk-has-multi-out.drv": { 3 | "args": [], 4 | "builder": ":", 5 | "env": { 6 | "builder": ":", 7 | "lib": "/nix/store/2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out-lib", 8 | "name": "has-multi-out", 9 | "out": "/nix/store/55lwldka5nyxa08wnvlizyqw02ihy8ic-has-multi-out", 10 | "outputs": "out lib", 11 | "system": ":" 12 | }, 13 | "inputDrvs": {}, 14 | "inputSrcs": [], 15 | "outputs": { 16 | "lib": { 17 | "path": "/nix/store/2vixb94v0hy2xc6p7mbnxxcyc095yyia-has-multi-out-lib" 18 | }, 19 | "out": { 20 | "path": "/nix/store/55lwldka5nyxa08wnvlizyqw02ihy8ic-has-multi-out" 21 | } 22 | }, 23 | "system": ":" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/testdata/latin1.nix: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nix-community/go-nix/4bdde671e0a123e7ac8df2bbd2bcbb2f316f5f55/test/testdata/latin1.nix -------------------------------------------------------------------------------- /test/testdata/m1vfixn8iprlf0v9abmlrz7mjw1xj8kp-cp1252.drv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nix-community/go-nix/4bdde671e0a123e7ac8df2bbd2bcbb2f316f5f55/test/testdata/m1vfixn8iprlf0v9abmlrz7mjw1xj8kp-cp1252.drv -------------------------------------------------------------------------------- /test/testdata/m1vfixn8iprlf0v9abmlrz7mjw1xj8kp-cp1252.drv.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nix-community/go-nix/4bdde671e0a123e7ac8df2bbd2bcbb2f316f5f55/test/testdata/m1vfixn8iprlf0v9abmlrz7mjw1xj8kp-cp1252.drv.json -------------------------------------------------------------------------------- /test/testdata/m5j1yp47lw1psd9n6bzina1167abbprr-bash44-023.drv: -------------------------------------------------------------------------------- 1 | Derive([("out","/nix/store/x9cyj78gzd1wjf0xsiad1pa3ricbj566-bash44-023","sha256","4fec236f3fbd3d0c47b893fdfa9122142a474f6ef66c20ffb6c0f4864dd591b6")],[],[],"builtin","builtin:fetchurl",[],[("builder","builtin:fetchurl"),("executable",""),("impureEnvVars","http_proxy https_proxy ftp_proxy all_proxy no_proxy"),("name","bash44-023"),("out","/nix/store/x9cyj78gzd1wjf0xsiad1pa3ricbj566-bash44-023"),("outputHash","1dlism6qdx60nvzj0v7ndr7lfahl4a8zmzckp13hqgdx7xpj7v2g"),("outputHashAlgo","sha256"),("outputHashMode","flat"),("preferLocalBuild","1"),("system","builtin"),("unpack",""),("url","https://ftpmirror.gnu.org/bash/bash-4.4-patches/bash44-023"),("urls","https://ftpmirror.gnu.org/bash/bash-4.4-patches/bash44-023")]) -------------------------------------------------------------------------------- /test/testdata/nar_1094wph9z4nwlgvsd53abfz8i117ykiv5dwnq9nnhz846s7xqd7d.nar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nix-community/go-nix/4bdde671e0a123e7ac8df2bbd2bcbb2f316f5f55/test/testdata/nar_1094wph9z4nwlgvsd53abfz8i117ykiv5dwnq9nnhz846s7xqd7d.nar -------------------------------------------------------------------------------- /test/testdata/nar_1094wph9z4nwlgvsd53abfz8i117ykiv5dwnq9nnhz846s7xqd7d.nar_bin_arp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nix-community/go-nix/4bdde671e0a123e7ac8df2bbd2bcbb2f316f5f55/test/testdata/nar_1094wph9z4nwlgvsd53abfz8i117ykiv5dwnq9nnhz846s7xqd7d.nar_bin_arp -------------------------------------------------------------------------------- /test/testdata/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv: -------------------------------------------------------------------------------- 1 | Derive([("out","/nix/store/mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar","r:sha1","0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33")],[],[],":",":",[],[("builder",":"),("name","bar"),("out","/nix/store/mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar"),("outputHash","0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"),("outputHashAlgo","sha1"),("outputHashMode","recursive"),("system",":")]) -------------------------------------------------------------------------------- /test/testdata/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv.json: -------------------------------------------------------------------------------- 1 | { 2 | "/nix/store/ss2p4wmxijn652haqyd7dckxwl4c7hxx-bar.drv": { 3 | "args": [], 4 | "builder": ":", 5 | "env": { 6 | "builder": ":", 7 | "name": "bar", 8 | "out": "/nix/store/mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar", 9 | "outputHash": "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33", 10 | "outputHashAlgo": "sha1", 11 | "outputHashMode": "recursive", 12 | "system": ":" 13 | }, 14 | "inputDrvs": {}, 15 | "inputSrcs": [], 16 | "outputs": { 17 | "out": { 18 | "hash": "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33", 19 | "hashAlgo": "r:sha1", 20 | "path": "/nix/store/mp57d33657rf34lzvlbpfa1gjfv5gmpg-bar" 21 | } 22 | }, 23 | "system": ":" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/testdata/unicode.nix: -------------------------------------------------------------------------------- 1 | builtins.derivation { 2 | name = "unicode"; 3 | builder = ":"; 4 | system = ":"; 5 | letters = '' 6 | räksmörgås 7 | rødgrød med fløde 8 | Lübeck 9 | 肥猪 10 | こんにちは / 今日は 11 | 🌮 12 | ''; 13 | } 14 | -------------------------------------------------------------------------------- /test/testdata/x6p0hg79i3wg0kkv7699935f7rrj9jf3-latin1.drv: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nix-community/go-nix/4bdde671e0a123e7ac8df2bbd2bcbb2f316f5f55/test/testdata/x6p0hg79i3wg0kkv7699935f7rrj9jf3-latin1.drv -------------------------------------------------------------------------------- /test/testdata/x6p0hg79i3wg0kkv7699935f7rrj9jf3-latin1.drv.json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nix-community/go-nix/4bdde671e0a123e7ac8df2bbd2bcbb2f316f5f55/test/testdata/x6p0hg79i3wg0kkv7699935f7rrj9jf3-latin1.drv.json -------------------------------------------------------------------------------- /test/testdata/z8dajq053b2bxc3ncqp8p8y3nfwafh3p-foo-file.drv: -------------------------------------------------------------------------------- 1 | Derive([("out","/nix/store/v9n02914nf8hjggiqskx46r9lbajhxw9-foo-file","","")],[("/nix/store/hr30xfxq6c5dc4mxndmh603nfyc4d1ms-bar.drv",["out"])],["/nix/store/8kh9rwg8fjrahlyycfn1k8k1mpxcpiv2-foofile"],":",":",[],[("bar","/nix/store/p5lra3zh34yj34162lh0ib5kssnp7fqc-bar"),("builder",":"),("file","/nix/store/8kh9rwg8fjrahlyycfn1k8k1mpxcpiv2-foofile"),("name","foo-file"),("out","/nix/store/v9n02914nf8hjggiqskx46r9lbajhxw9-foo-file"),("system",":")]) --------------------------------------------------------------------------------