diff --git a/pkg/narv2/README.md b/pkg/narv2/README.md new file mode 100644 index 0000000..9cd5f64 --- /dev/null +++ b/pkg/narv2/README.md @@ -0,0 +1,160 @@ +# Fast NAR Reader (fastnar) + +This package provides a high-performance NAR (Nix Archive) reader implementation, thanks to the genius brain of @edef. It improves upon the original `pkg/nar` package with a synchronous, state-machine based approach. + +### Performance Improvements + +1. **Synchronous Processing**: No goroutines or channels overhead +2. **Buffered Reading**: Uses `bufio.Reader` with peek/discard operations +3. **Pre-computed Tokens**: Binary token matching for faster parsing +4. **Lower Memory Allocation**: Minimal object creation during parsing +5. **State Machine**: Direct state transitions without intermediate objects + +## Usage Examples + +### Basic Usage + +```go +package main + +import ( + "fmt" + "io" + "os" + + "github.com/nix-community/go-nix/pkg/fastnar" +) + +func main() { + file, err := os.Open("archive.nar") + if err != nil { + panic(err) + } + defer file.Close() + + reader := nar.NewReader(file) + + for { + tag, err := reader.Next() + if err == io.EOF { + break + } + if err != nil { + panic(err) + } + + switch tag { + case nar.TagDir: + fmt.Printf("Directory: %s\n", reader.Path()) + case nar.TagReg: + fmt.Printf("File: %s (%d bytes)\n", reader.Path(), reader.Size()) + case nar.TagExe: + fmt.Printf("Executable: %s (%d bytes)\n", reader.Path(), reader.Size()) + case nar.TagSym: + fmt.Printf("Symlink: %s -> %s\n", reader.Path(), reader.Target()) + } + } +} +``` + +### Copying NAR Archives + +```go +// High-performance NAR copying +func copyNAR(dst io.Writer, src io.Reader) error { + reader := nar.NewReader(src) + writer := nar.NewWriter(dst) + return nar.Copy(writer, reader) +} +``` + +### Reading File Contents + +```go +for { + tag, err := reader.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + if tag == nar.TagReg || tag == nar.TagExe { + // Read file content + content := make([]byte, reader.Size()) + _, err := io.ReadFull(reader, content) + if err != nil { + return err + } + + fmt.Printf("File %s: %s\n", reader.Path(), string(content)) + } +} +``` + +## Migration Guide + +### From pkg/nar to pkg/fastnar + +**Before:** +```go +import "github.com/nix-community/go-nix/pkg/nar" + +reader, err := nar.NewReader(file) +if err != nil { + return err +} +defer reader.Close() + +for { + header, err := reader.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + switch header.Type { + case nar.TypeDirectory: + fmt.Printf("Dir: %s\n", header.Path) + case nar.TypeRegular: + fmt.Printf("File: %s\n", header.Path) + if header.Executable { + fmt.Printf(" (executable)\n") + } + case nar.TypeSymlink: + fmt.Printf("Link: %s -> %s\n", header.Path, header.LinkTarget) + } +} +``` + +**After:** +```go +import "github.com/nix-community/go-nix/pkg/fastnar" + +reader := nar.NewReader(file) + +for { + tag, err := reader.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + switch tag { + case nar.TagDir: + fmt.Printf("Dir: %s\n", reader.Path()) + case nar.TagReg: + fmt.Printf("File: %s\n", reader.Path()) + case nar.TagExe: + fmt.Printf("File: %s\n", reader.Path()) + fmt.Printf(" (executable)\n") + case nar.TagSym: + fmt.Printf("Link: %s -> %s\n", reader.Path(), reader.Target()) + } +} +``` diff --git a/pkg/narv2/copy.go b/pkg/narv2/copy.go new file mode 100644 index 0000000..7254298 --- /dev/null +++ b/pkg/narv2/copy.go @@ -0,0 +1,47 @@ +package narv2 + +import "io" + +func Copy(dst Writer, src Reader) error { + tag, err := src.Next() + if err != nil { + return err + } + return copyNAR(dst, src, tag) +} + +func copyNAR(dst Writer, src Reader, tag Tag) error { + switch tag { + default: + panic("invalid tag") + case TagSym: + return dst.Link(src.Target()) + case TagReg, TagExe: + if err := dst.File(tag == TagExe, src.Size()); err != nil { + return err + } + if _, err := io.Copy(dst, src); err != nil { + return err + } + return dst.Close() + case TagDir: + if err := dst.Directory(); err != nil { + return err + } + for { + tag, err := src.Next() + if err == io.EOF { + return dst.Close() + } + if err != nil { + return err + } + if err := dst.Entry(src.Name()); err != nil { + return err + } + if err := copyNAR(dst, src, tag); err != nil { + return err + } + } + } +} diff --git a/pkg/narv2/copy_test.go b/pkg/narv2/copy_test.go new file mode 100644 index 0000000..b1d095d --- /dev/null +++ b/pkg/narv2/copy_test.go @@ -0,0 +1,88 @@ +package narv2_test + +import ( + "bytes" + "io" + "os" + "testing" + + "github.com/nix-community/go-nix/pkg/narv2" +) + +func TestRoundtrip(t *testing.T) { + // Use the test data file that exists + f, err := os.Open("../../test/testdata/nar_1094wph9z4nwlgvsd53abfz8i117ykiv5dwnq9nnhz846s7xqd7d.nar") + if err != nil { + t.Fatal(err) + } + defer f.Close() + + // Read the original file into memory + original, err := io.ReadAll(f) + if err != nil { + t.Fatal(err) + } + + // Copy original through narv2 + var outputBuf bytes.Buffer + if err := narv2.Copy(narv2.NewWriter(&outputBuf), narv2.NewReader(bytes.NewReader(original))); err != nil { + t.Fatalf("Copy: %v", err) + } + + // Test logical equivalence: read both files and compare their structure + originalEntries := readAllEntries(t, bytes.NewReader(original)) + outputEntries := readAllEntries(t, bytes.NewReader(outputBuf.Bytes())) + + if len(originalEntries) != len(outputEntries) { + t.Fatalf("Entry count mismatch: original=%d, output=%d", len(originalEntries), len(outputEntries)) + } + + for i, orig := range originalEntries { + out := outputEntries[i] + if orig.Path != out.Path || orig.Type != out.Type || orig.Size != out.Size || orig.Target != out.Target { + t.Errorf("Entry %d mismatch:\n original: %+v\n output: %+v", i, orig, out) + } + } +} + +type EntryInfo struct { + Path string + Type string + Size uint64 + Target string +} + +func readAllEntries(t *testing.T, r io.Reader) []EntryInfo { + var entries []EntryInfo + reader := narv2.NewReader(r) + + for { + tag, err := reader.Next() + if err == io.EOF { + break + } + if err != nil { + t.Fatalf("Reader error: %v", err) + } + + entry := EntryInfo{Path: reader.Path()} + switch tag { + case narv2.TagDir: + entry.Type = "directory" + case narv2.TagReg: + entry.Type = "regular" + entry.Size = reader.Size() + io.Copy(io.Discard, reader) // consume content + case narv2.TagExe: + entry.Type = "executable" + entry.Size = reader.Size() + io.Copy(io.Discard, reader) // consume content + case narv2.TagSym: + entry.Type = "symlink" + entry.Target = reader.Target() + } + entries = append(entries, entry) + } + + return entries +} diff --git a/pkg/narv2/example_test.go b/pkg/narv2/example_test.go new file mode 100644 index 0000000..9ff3307 --- /dev/null +++ b/pkg/narv2/example_test.go @@ -0,0 +1,174 @@ +package narv2_test + +import ( + "bytes" + "fmt" + "io" + "log" + + "github.com/nix-community/go-nix/pkg/narv2" + oldnar "github.com/nix-community/go-nix/pkg/nar" +) + +func ExampleReader() { + // Create a simple NAR for demonstration + var buf bytes.Buffer + w := narv2.NewWriter(&buf) + + // Build a simple directory structure + w.Directory() + + w.Entry("file.txt") + w.File(false, 5) + w.Write([]byte("hello")) + w.Close() + + w.Entry("link") + w.Link("file.txt") + + w.Entry("script.sh") + w.File(true, 11) + w.Write([]byte("#!/bin/bash")) + w.Close() + + w.Close() // Close root directory + + narData := buf.Bytes() + + // Read with Reader + fmt.Println("=== FastReader ===") + r := narv2.NewReader(bytes.NewReader(narData)) + + for { + tag, err := r.Next() + if err == io.EOF { + break + } + if err != nil { + log.Fatal(err) + } + + switch tag { + case narv2.TagDir: + fmt.Printf("Directory: %s\n", r.Path()) + case narv2.TagReg: + fmt.Printf("Regular file: %s (size: %d)\n", r.Path(), r.Size()) + // Read file content + content := make([]byte, r.Size()) + io.ReadFull(r, content) + fmt.Printf(" Content: %s\n", string(content)) + case narv2.TagExe: + fmt.Printf("Executable: %s (size: %d)\n", r.Path(), r.Size()) + // Read file content + content := make([]byte, r.Size()) + io.ReadFull(r, content) + fmt.Printf(" Content: %s\n", string(content)) + case narv2.TagSym: + fmt.Printf("Symlink: %s -> %s\n", r.Path(), r.Target()) + } + } + + // Read with traditional NAR reader for comparison + fmt.Println("\n=== Traditional Reader ===") + oldReader, err := oldnar.NewReader(bytes.NewReader(narData)) + if err != nil { + log.Fatal(err) + } + defer oldReader.Close() + + for { + hdr, err := oldReader.Next() + if err == io.EOF { + break + } + if err != nil { + log.Fatal(err) + } + + switch hdr.Type { + case oldnar.TypeDirectory: + fmt.Printf("Directory: %s\n", hdr.Path) + case oldnar.TypeRegular: + if hdr.Executable { + fmt.Printf("Executable: %s (size: %d)\n", hdr.Path, hdr.Size) + } else { + fmt.Printf("Regular file: %s (size: %d)\n", hdr.Path, hdr.Size) + } + // Read file content + if hdr.Size > 0 { + content := make([]byte, hdr.Size) + io.ReadFull(oldReader, content) + fmt.Printf(" Content: %s\n", string(content)) + } + case oldnar.TypeSymlink: + fmt.Printf("Symlink: %s -> %s\n", hdr.Path, hdr.LinkTarget) + } + } + + // Output: + // === FastReader === + // Directory: / + // Regular file: /file.txt (size: 5) + // Content: hello + // Symlink: /link -> file.txt + // Executable: /script.sh (size: 11) + // Content: #!/bin/bash + // + // === Traditional Reader === + // Directory: / + // Regular file: /file.txt (size: 5) + // Content: hello + // Symlink: /link -> file.txt + // Executable: /script.sh (size: 11) + // Content: #!/bin/bash +} + +func ExampleReader_performance() { + // Performance-focused usage example + var buf bytes.Buffer + w := narv2.NewWriter(&buf) + + // Create a larger directory structure + w.Directory() + for i := 0; i < 100; i++ { + w.Entry(fmt.Sprintf("file%d.txt", i)) + w.File(false, 10) + w.Write([]byte(fmt.Sprintf("content%03d", i))) + w.Close() + } + w.Close() + + narData := buf.Bytes() + + // Reader - synchronous, low overhead + fmt.Println("=== Reader (synchronous) ===") + r := narv2.NewReader(bytes.NewReader(narData)) + + fileCount := 0 + totalSize := uint64(0) + + for { + tag, err := r.Next() + if err == io.EOF { + break + } + if err != nil { + log.Fatal(err) + } + + if tag == narv2.TagReg || tag == narv2.TagExe { + fileCount++ + totalSize += r.Size() + // Skip reading content for performance + io.Copy(io.Discard, r) + } + } + + fmt.Printf("Files processed: %d\n", fileCount) + fmt.Printf("Total size: %d bytes\n", totalSize) + + // Output: + // === Reader (synchronous) === + // Files processed: 100 + // Total size: 1000 bytes +} \ No newline at end of file diff --git a/pkg/narv2/reader.go b/pkg/narv2/reader.go new file mode 100644 index 0000000..3d3a3c5 --- /dev/null +++ b/pkg/narv2/reader.go @@ -0,0 +1,373 @@ +package narv2 + +import ( + "bufio" + "bytes" + "encoding/binary" + "fmt" + "io" + "path" + "strings" +) + +var encoding = binary.LittleEndian +var zero [8]byte + +func token(parts ...string) []byte { + var buf bytes.Buffer + for _, part := range parts { + binary.Write(&buf, encoding, uint64(len(part))) + buf.WriteString(part) + n := len(part) & 7 + if n != 0 { + buf.Write(zero[n:]) + } + } + return buf.Bytes() +} + +var ( + tokNar = token("nix-archive-1", "(", "type") + tokReg = token("regular", "contents") + tokExe = token("regular", "executable", "", "contents") + tokSym = token("symlink", "target") + tokDir = token("directory") + tokEnt = token("entry", "(", "name") + tokNod = token("node", "(", "type") + tokPar = token(")") +) + +type Tag byte + +const ( + TagSym = 6 + TagReg = 8 + TagExe = 10 + TagDir = 'y' +) + +type Reader interface { + Next() (Tag, error) + Name() string + Path() string + Target() string + Size() uint64 + io.Reader +} + +func NewReader(rd io.Reader) Reader { + return &reader{ + r: bufio.NewReader(rd), + path: "/", + } +} + +type reader struct { + r *bufio.Reader + err error + depth uint32 + name string + path string + target string + size uint64 + pad byte + + // path construction state + pathStack []string +} + +func (r *reader) fail(err error) error { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + if r.err == nil { + r.err = err + } + return r.err +} + +var ( + errInvalid = fmt.Errorf("nar: invalid input") + errSize = fmt.Errorf("nar: rejecting excessively large input") +) + +func (r *reader) Next() (Tag, error) { + if r.err != nil { + return 0, r.err + } + + // Skip remaining file content if not fully read + if r.size != 0 { + _, err := io.Copy(io.Discard, r) + if err != nil { + r.fail(err) + return 0, r.err + } + } + + for { + if r.depth == 0 { + // Check if we've already processed the root node + buf := r.peek(16) + if buf == nil { + // If we can't peek and depth is 0, we're at EOF + if r.err == io.ErrUnexpectedEOF { + r.err = io.EOF + } + return 0, r.err + } + + // Check if this is a closing paren at root level (end of NAR) + if buf[0] == 1 { // ")" + r.readEnd() + if r.err == nil { + r.err = io.EOF + } + return 0, r.err + } + + // Initialize by consuming NAR header + r.consume(tokNar) + if r.err != nil { + return 0, r.err + } + } else { + // Check for directory end or entry + buf := r.peek(16) + if buf == nil { + return 0, r.err + } + + switch buf[0] { + default: + r.fail(errInvalid) + return 0, r.err + case 1: // ")" - end of directory + r.depth-- + r.readEnd() + + // Pop path component + if len(r.pathStack) > 0 { + r.pathStack = r.pathStack[:len(r.pathStack)-1] + } + r.updatePath() + + // Return EOF for directory closure + if r.depth == 0 && r.err == nil { + r.err = io.EOF + return 0, r.err + } + // For non-root directory closures, return EOF but don't store it + return 0, io.EOF + case 5: // "entry" - directory entry + r.consume(tokEnt) + if r.err != nil { + return 0, r.err + } + + r.name = r.readString(255) + if r.err != nil { + return 0, r.err + } + + // For directories, push path component to stack + // For files/symlinks, just store the name for Path() method + + r.consume(tokNod) + if r.err != nil { + return 0, r.err + } + } + } + break + } + + // Read node type + buf := r.peek(32) + if buf == nil { + return 0, r.err + } + + switch buf[16] { + default: + r.fail(errInvalid) + return 0, r.err + case TagSym: + r.consume(tokSym) + if r.err != nil { + return 0, r.err + } + r.target = r.readString(4095) + if r.err != nil { + return 0, r.err + } + r.readEnd() + return TagSym, r.err + case TagReg: + r.consume(tokReg) + if r.err != nil { + return 0, r.err + } + r.readFile() + return TagReg, r.err + case TagExe: + r.consume(tokExe) + if r.err != nil { + return 0, r.err + } + r.readFile() + return TagExe, r.err + case TagDir: + r.consume(tokDir) + if r.err != nil { + return 0, r.err + } + r.depth++ + // Push directory name to path stack only for directories + r.pathStack = append(r.pathStack, r.name) + r.updatePath() + return TagDir, r.err + } +} + +func (r *reader) updatePath() { + if len(r.pathStack) == 0 { + r.path = "/" + } else { + r.path = "/" + path.Join(r.pathStack...) + } +} + +func (r *reader) Path() string { + // For directories, the name is already included in r.path + // For files and symlinks, we need to append the name + if len(r.pathStack) > 0 && r.path != "/" && strings.HasSuffix(r.path, "/"+r.name) { + // Directory case: name already in path + return r.path + } + // File/symlink case: append name to path + if r.name == "" { + return r.path + } + if r.path == "/" { + return "/" + r.name + } + return r.path + "/" + r.name +} + +func (r *reader) readFile() { + r.size, _ = r.readInt() + r.pad = byte(r.size & 7) + if r.size > 1<<40 { + r.fail(errSize) + } + if r.size == 0 { + r.readEnd() + } +} + +func (r *reader) readEnd() { + r.consume(tokPar) + if r.depth > 0 { + r.consume(tokPar) + } +} + +func (r *reader) Name() string { + return r.name +} + +func (r *reader) Target() string { + return r.target +} + +func (r *reader) Size() uint64 { + return r.size +} + +func (r *reader) Read(buf []byte) (n int, err error) { + if r.size == 0 { + return 0, io.EOF + } + if uint64(len(buf)) > r.size { + buf = buf[:r.size] + } + n, err = r.r.Read(buf) + r.size -= uint64(n) + if err != nil { + r.fail(err) + } else if r.size == 0 { + r.consumePadding(int(r.pad)) + r.pad = 0 + r.readEnd() + } + return +} + +func (r *reader) peek(n int) []byte { + if r.err != nil { + return nil + } + buf, err := r.r.Peek(n) + if err != nil { + r.fail(err) + return nil + } + return buf +} + +func (r *reader) take(n int) []byte { + buf := r.peek(n) + if buf == nil { + return nil + } + r.r.Discard(n) + return buf +} + +func (r *reader) consume(tok []byte) { + buf := r.peek(len(tok)) + if buf == nil { + return + } + if !bytes.Equal(buf, tok) { + r.fail(errInvalid) + return + } + r.r.Discard(len(tok)) +} + +func (r *reader) readInt() (n uint64, ok bool) { + nbuf := r.take(8) + if nbuf == nil { + return 0, false + } + return encoding.Uint64(nbuf), true +} + +func (r *reader) consumePadding(n int) { + n &= 7 + if n != 0 { + r.consume(zero[n:]) + } +} + +func (r *reader) readString(max int) (s string) { + n, ok := r.readInt() + if !ok { + return + } + if n > uint64(max) { + r.fail(errSize) + return + } + if n == 0 { + r.fail(errInvalid) + return + } + + s = string(r.take(int(n))) + r.consumePadding(int(n)) + + return s +} \ No newline at end of file diff --git a/pkg/narv2/reader_test.go b/pkg/narv2/reader_test.go new file mode 100644 index 0000000..51c00dc --- /dev/null +++ b/pkg/narv2/reader_test.go @@ -0,0 +1,259 @@ +package narv2_test + +import ( + "bytes" + "io" + "testing" + + "github.com/nix-community/go-nix/pkg/narv2" + "github.com/nix-community/go-nix/pkg/wire" +) + +func TestReader(t *testing.T) { + // Test with simple directory NAR + narData := genEmptyDirectoryNar() + r := narv2.NewReader(bytes.NewReader(narData)) + + tag, err := r.Next() + if err != nil { + t.Fatalf("Next() failed: %v", err) + } + if tag != narv2.TagDir { + t.Errorf("Expected TagDir, got %v", tag) + } + if r.Path() != "/" { + t.Errorf("Expected path '/', got '%s'", r.Path()) + } + + // Should get EOF on next call + _, err = r.Next() + if err != io.EOF { + t.Errorf("Expected EOF, got %v", err) + } +} + +func TestReaderRegularFile(t *testing.T) { + narData := genOneByteRegularNar() + r := narv2.NewReader(bytes.NewReader(narData)) + + tag, err := r.Next() + if err != nil { + t.Fatalf("Next() failed: %v", err) + } + if tag != narv2.TagReg { + t.Errorf("Expected TagReg, got %v", tag) + } + if r.Size() != 1 { + t.Errorf("Expected size 1, got %d", r.Size()) + } + + // Read the file content + buf := make([]byte, 1) + n, err := r.Read(buf) + if err != nil { + t.Fatalf("Read() failed: %v", err) + } + if n != 1 || buf[0] != 0x1 { + t.Errorf("Expected to read byte 0x1, got %v", buf[:n]) + } + + // Should get EOF on next call + _, err = r.Next() + if err != io.EOF { + t.Errorf("Expected EOF, got %v", err) + } +} + +func TestReaderSymlink(t *testing.T) { + narData := genSymlinkNar() + r := narv2.NewReader(bytes.NewReader(narData)) + + tag, err := r.Next() + if err != nil { + t.Fatalf("Next() failed: %v", err) + } + if tag != narv2.TagSym { + t.Errorf("Expected TagSym, got %v", tag) + } + if r.Target() != "/nix/store/somewhereelse" { + t.Errorf("Expected target '/nix/store/somewhereelse', got '%s'", r.Target()) + } + + // Should get EOF on next call + _, err = r.Next() + if err != io.EOF { + t.Errorf("Expected EOF, got %v", err) + } +} + +func TestReaderRoundtrip(t *testing.T) { + // Use the existing test data file + narData := genComplexNar() + r := narv2.NewReader(bytes.NewReader(narData)) + + var buf bytes.Buffer + w := narv2.NewWriter(&buf) + + if err := narv2.Copy(w, r); err != nil { + t.Fatalf("Copy failed: %v", err) + } + + // The output should match the input + if !bytes.Equal(narData, buf.Bytes()) { + t.Error("Roundtrip failed: output doesn't match input") + } +} + +// genComplexNar creates a more complex NAR for testing +func genComplexNar() []byte { + var buf bytes.Buffer + w := narv2.NewWriter(&buf) + + // Create a directory with files + w.Directory() + + // Add a regular file + w.Entry("file.txt") + w.File(false, 5) + w.Write([]byte("hello")) + w.Close() + + // Add a symlink (must come before script.sh for lexicographic order) + w.Entry("link") + w.Link("file.txt") + + // Add an executable file + w.Entry("script.sh") + w.File(true, 11) + w.Write([]byte("#!/bin/bash")) + w.Close() + + // Add a subdirectory + w.Entry("subdir") + w.Directory() + w.Entry("nested.txt") + w.File(false, 4) + w.Write([]byte("test")) + w.Close() + w.Close() // Close subdirectory + + w.Close() // Close root directory + + return buf.Bytes() +} + +// genEmptyDirectoryNar returns the bytes of a NAR file only containing an empty directory. +func genEmptyDirectoryNar() []byte { + var expectedBuf bytes.Buffer + + err := wire.WriteString(&expectedBuf, "nix-archive-1") + if err != nil { + panic(err) + } + + err = wire.WriteString(&expectedBuf, "(") + if err != nil { + panic(err) + } + + err = wire.WriteString(&expectedBuf, "type") + if err != nil { + panic(err) + } + + err = wire.WriteString(&expectedBuf, "directory") + if err != nil { + panic(err) + } + + err = wire.WriteString(&expectedBuf, ")") + if err != nil { + panic(err) + } + + return expectedBuf.Bytes() +} + +// genOneByteRegularNar returns the bytes of a NAR only containing a single file at the root. +func genOneByteRegularNar() []byte { + var expectedBuf bytes.Buffer + + err := wire.WriteString(&expectedBuf, "nix-archive-1") + if err != nil { + panic(err) + } + + err = wire.WriteString(&expectedBuf, "(") + if err != nil { + panic(err) + } + + err = wire.WriteString(&expectedBuf, "type") + if err != nil { + panic(err) + } + + err = wire.WriteString(&expectedBuf, "regular") + if err != nil { + panic(err) + } + + err = wire.WriteString(&expectedBuf, "contents") + if err != nil { + panic(err) + } + + err = wire.WriteBytes(&expectedBuf, []byte{0x1}) + if err != nil { + panic(err) + } + + err = wire.WriteString(&expectedBuf, ")") + if err != nil { + panic(err) + } + + return expectedBuf.Bytes() +} + +// genSymlinkNar returns the bytes of a NAR only containing a single symlink at the root. +func genSymlinkNar() []byte { + var expectedBuf bytes.Buffer + + err := wire.WriteString(&expectedBuf, "nix-archive-1") + if err != nil { + panic(err) + } + + err = wire.WriteString(&expectedBuf, "(") + if err != nil { + panic(err) + } + + err = wire.WriteString(&expectedBuf, "type") + if err != nil { + panic(err) + } + + err = wire.WriteString(&expectedBuf, "symlink") + if err != nil { + panic(err) + } + + err = wire.WriteString(&expectedBuf, "target") + if err != nil { + panic(err) + } + + err = wire.WriteString(&expectedBuf, "/nix/store/somewhereelse") + if err != nil { + panic(err) + } + + err = wire.WriteString(&expectedBuf, ")") + if err != nil { + panic(err) + } + + return expectedBuf.Bytes() +} \ No newline at end of file diff --git a/pkg/narv2/writer.go b/pkg/narv2/writer.go new file mode 100644 index 0000000..4a648c1 --- /dev/null +++ b/pkg/narv2/writer.go @@ -0,0 +1,121 @@ +package narv2 + +import ( + "fmt" + "io" +) + +type Writer interface { + Directory() error + Entry(name string) error + Link(target string) error + File(executable bool, size uint64) error + io.WriteCloser +} + +func NewWriter(w io.Writer) Writer { + nw := &writer{w: w} + nw.write(tokNar) + return nw +} + +type writer struct { + w io.Writer + err error + size uint64 // pending file bytes + pad byte // pending padding + buf [8]byte // scratch pad for lengths + depth uint32 + file bool +} + +func (w *writer) write(data []byte) (n int) { + if w.err == nil { + n, w.err = w.w.Write(data) + } + return +} + +func (w *writer) Directory() error { + w.write(tokDir) + w.depth += 1 + return w.err +} + +func (w *writer) Entry(name string) error { + if name == "" { + return fmt.Errorf("nar: entries must have non-empty names") + } + w.write(tokEnt) + w.write(token(name)) + w.write(tokNod) + return w.err +} + +func (w *writer) Link(target string) error { + w.write(tokSym) + w.write(token(target)) + w.write(tokPar) + if w.depth != 0 { + w.write(tokPar) + } + return w.err +} + +func (w *writer) File(executable bool, size uint64) error { + if w.err != nil { + return w.err + } + w.file = true + w.size = size + w.pad = byte(size & 7) + if executable { + w.write(tokExe) + } else { + w.write(tokReg) + } + encoding.PutUint64(w.buf[:], size) + w.write(w.buf[:]) + return w.err +} + +func (w *writer) Write(data []byte) (n int, err error) { + if w.err != nil { + return 0, w.err + } + if uint64(len(data)) > w.size { + w.err = fmt.Errorf("nar: did not expect (more) file data") + return 0, w.err + } + n = w.write(data) + w.size -= uint64(n) + return n, w.err +} + +func (w *writer) Close() error { + if w.err != nil { + return w.err + } + if !w.file && w.depth == 0 { + w.err = fmt.Errorf("nar: close at depth 0") + return w.err + } + if w.size != 0 { + w.err = fmt.Errorf("nar: incomplete file write") + return w.err + } + if w.pad != 0 { + w.write(zero[w.pad:]) + w.pad = 0 + } + if w.file { + w.file = false + } else { + w.depth -= 1 + } + w.write(tokPar) + if w.depth != 0 { + w.write(tokPar) + } + return w.err +}