narinfo: implement parser :D
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Ic10f3e69f18308d620b1461fd62d46386a6a6964
This commit is contained in:
parent
2a0c30f953
commit
a4f4410e5b
2 changed files with 247 additions and 0 deletions
81
internal/narinfo/narinfo.go
Normal file
81
internal/narinfo/narinfo.go
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package narinfo
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Parsed representation of a Nix narinfo file.
|
||||
type NarInfo struct {
|
||||
StorePath string
|
||||
URL string
|
||||
Compression string
|
||||
FileHash string
|
||||
FileSize uint64
|
||||
NarHash string
|
||||
NarSize uint64
|
||||
References []string
|
||||
Deriver string
|
||||
Sig []string
|
||||
CA string
|
||||
}
|
||||
|
||||
// Parses a narinfo from r. Returns error on malformed input or missing StorePath.
|
||||
func Parse(r io.Reader) (*NarInfo, error) {
|
||||
ni := &NarInfo{}
|
||||
scanner := bufio.NewScanner(r)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
k, v, ok := strings.Cut(line, ": ")
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("malformed line: %q", line)
|
||||
}
|
||||
switch k {
|
||||
case "StorePath":
|
||||
ni.StorePath = v
|
||||
case "URL":
|
||||
ni.URL = v
|
||||
case "Compression":
|
||||
ni.Compression = v
|
||||
case "FileHash":
|
||||
ni.FileHash = v
|
||||
case "FileSize":
|
||||
n, err := strconv.ParseUint(v, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("FileSize: %w", err)
|
||||
}
|
||||
ni.FileSize = n
|
||||
case "NarHash":
|
||||
ni.NarHash = v
|
||||
case "NarSize":
|
||||
n, err := strconv.ParseUint(v, 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("NarSize: %w", err)
|
||||
}
|
||||
ni.NarSize = n
|
||||
case "References":
|
||||
if v != "" {
|
||||
ni.References = strings.Fields(v)
|
||||
}
|
||||
case "Deriver":
|
||||
ni.Deriver = v
|
||||
case "Sig":
|
||||
ni.Sig = append(ni.Sig, v)
|
||||
case "CA":
|
||||
ni.CA = v
|
||||
}
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if ni.StorePath == "" {
|
||||
return nil, fmt.Errorf("missing StorePath")
|
||||
}
|
||||
return ni, nil
|
||||
}
|
||||
166
internal/narinfo/narinfo_test.go
Normal file
166
internal/narinfo/narinfo_test.go
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
package narinfo_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"notashelf.dev/ncro/internal/narinfo"
|
||||
)
|
||||
|
||||
var realWorldNarinfo = `StorePath: /nix/store/s66mzxpvicwklp6cpph4dc53k5l6bfhe-hello-2.12.1
|
||||
URL: nar/1wwh37nhg4f5zhb2vsn1a81p3ixn69gkg5k6fvmw3nhcn19fg8xj.nar.xz
|
||||
Compression: xz
|
||||
FileHash: sha256:1wwh37nhg4f5zhb2vsn1a81p3ixn69gkg5k6fvmw3nhcn19fg8xj
|
||||
FileSize: 50088
|
||||
NarHash: sha256:04rrn5x6lnzrfkcy3bh7gf7x6hq3w1kap4wdss2n6n4s19pgbkr7
|
||||
NarSize: 226512
|
||||
References: s66mzxpvicwklp6cpph4dc53k5l6bfhe-hello-2.12.1 4nlgxhzzvsnr6bva0b9afnq8lbr9rk2b-glibc-2.38-23
|
||||
Sig: cache.nixos.org-1:abc123+base64signature=
|
||||
`
|
||||
|
||||
func TestParseRealWorld(t *testing.T) {
|
||||
ni, err := narinfo.Parse(strings.NewReader(realWorldNarinfo))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse: %v", err)
|
||||
}
|
||||
if ni.StorePath != "/nix/store/s66mzxpvicwklp6cpph4dc53k5l6bfhe-hello-2.12.1" {
|
||||
t.Errorf("StorePath = %q", ni.StorePath)
|
||||
}
|
||||
if ni.URL != "nar/1wwh37nhg4f5zhb2vsn1a81p3ixn69gkg5k6fvmw3nhcn19fg8xj.nar.xz" {
|
||||
t.Errorf("URL = %q", ni.URL)
|
||||
}
|
||||
if ni.Compression != "xz" {
|
||||
t.Errorf("Compression = %q, want xz", ni.Compression)
|
||||
}
|
||||
if ni.FileSize != 50088 {
|
||||
t.Errorf("FileSize = %d, want 50088", ni.FileSize)
|
||||
}
|
||||
if ni.NarHash != "sha256:04rrn5x6lnzrfkcy3bh7gf7x6hq3w1kap4wdss2n6n4s19pgbkr7" {
|
||||
t.Errorf("NarHash = %q", ni.NarHash)
|
||||
}
|
||||
if ni.NarSize != 226512 {
|
||||
t.Errorf("NarSize = %d, want 226512", ni.NarSize)
|
||||
}
|
||||
if len(ni.References) != 2 {
|
||||
t.Errorf("References len = %d, want 2", len(ni.References))
|
||||
}
|
||||
if len(ni.Sig) != 1 {
|
||||
t.Errorf("Sig len = %d, want 1", len(ni.Sig))
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNoneCompression(t *testing.T) {
|
||||
input := "StorePath: /nix/store/abc-test\nURL: nar/abc.nar\nCompression: none\n"
|
||||
ni, err := narinfo.Parse(strings.NewReader(input))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse: %v", err)
|
||||
}
|
||||
if ni.Compression != "none" {
|
||||
t.Errorf("Compression = %q, want none", ni.Compression)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMultipleReferences(t *testing.T) {
|
||||
input := "StorePath: /nix/store/abc-test\nReferences: pkg-a pkg-b pkg-c pkg-d\n"
|
||||
ni, err := narinfo.Parse(strings.NewReader(input))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse: %v", err)
|
||||
}
|
||||
if len(ni.References) != 4 {
|
||||
t.Errorf("References = %v, want 4 entries", ni.References)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseEmptyReferences(t *testing.T) {
|
||||
input := "StorePath: /nix/store/abc-test\nReferences: \n"
|
||||
ni, err := narinfo.Parse(strings.NewReader(input))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse: %v", err)
|
||||
}
|
||||
if len(ni.References) != 0 {
|
||||
t.Errorf("References = %v, want empty", ni.References)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMultipleSigs(t *testing.T) {
|
||||
input := "StorePath: /nix/store/abc-test\nSig: key1:aaa=\nSig: key2:bbb=\n"
|
||||
ni, err := narinfo.Parse(strings.NewReader(input))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse: %v", err)
|
||||
}
|
||||
if len(ni.Sig) != 2 {
|
||||
t.Errorf("Sig len = %d, want 2", len(ni.Sig))
|
||||
}
|
||||
if ni.Sig[0] != "key1:aaa=" || ni.Sig[1] != "key2:bbb=" {
|
||||
t.Errorf("Sig = %v", ni.Sig)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMissingStorePath(t *testing.T) {
|
||||
input := "URL: nar/abc.nar\nNarHash: sha256:abc\n"
|
||||
_, err := narinfo.Parse(strings.NewReader(input))
|
||||
if err == nil {
|
||||
t.Error("expected error for missing StorePath")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseMalformedLine(t *testing.T) {
|
||||
input := "StorePath: /nix/store/abc-test\nbadline\n"
|
||||
_, err := narinfo.Parse(strings.NewReader(input))
|
||||
if err == nil {
|
||||
t.Error("expected error for malformed line")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseNarSizeOverflow(t *testing.T) {
|
||||
// uint64 max: 18446744073709551615 — verify it parses correctly
|
||||
input := "StorePath: /nix/store/abc-test\nNarSize: 18446744073709551615\n"
|
||||
ni, err := narinfo.Parse(strings.NewReader(input))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse: %v", err)
|
||||
}
|
||||
if ni.NarSize != 18446744073709551615 {
|
||||
t.Errorf("NarSize = %d", ni.NarSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDeriverCA(t *testing.T) {
|
||||
input := "StorePath: /nix/store/abc-test\nDeriver: abc-drv\nCA: fixed:r:sha256:abc\n"
|
||||
ni, err := narinfo.Parse(strings.NewReader(input))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse: %v", err)
|
||||
}
|
||||
if ni.Deriver != "abc-drv" {
|
||||
t.Errorf("Deriver = %q", ni.Deriver)
|
||||
}
|
||||
if ni.CA != "fixed:r:sha256:abc" {
|
||||
t.Errorf("CA = %q", ni.CA)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIgnoresBlankLines(t *testing.T) {
|
||||
input := "\n\nStorePath: /nix/store/abc-test\n\nNarHash: sha256:abc\n\n"
|
||||
ni, err := narinfo.Parse(strings.NewReader(input))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse: %v", err)
|
||||
}
|
||||
if ni.StorePath == "" {
|
||||
t.Error("StorePath should be set")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInvalidNarSize(t *testing.T) {
|
||||
input := "StorePath: /nix/store/abc-test\nNarSize: not-a-number\n"
|
||||
_, err := narinfo.Parse(strings.NewReader(input))
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid NarSize")
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseInvalidFileSize(t *testing.T) {
|
||||
input := "StorePath: /nix/store/abc-test\nFileSize: not-a-number\n"
|
||||
_, err := narinfo.Parse(strings.NewReader(input))
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid FileSize")
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue