depot/go/nix/nar/nar.go

196 lines
3.9 KiB
Go

package nar
import (
"encoding/binary"
"fmt"
"io"
"io/fs"
"path"
"sort"
)
type serializeWriter struct {
io.Writer
}
func (w serializeWriter) WritePadding(n int64) (int64, error) {
if n%8 > 0 {
n, err := w.Write(make([]byte, 8-(n%8)))
return int64(n), err
}
return 0, nil
}
func (w serializeWriter) WriteUint64(n uint64) (int64, error) {
buf := make([]byte, 8)
binary.LittleEndian.PutUint64(buf, n)
wrote, err := w.Write(buf)
return int64(wrote), err
}
func (w serializeWriter) WriteString(s string) (int64, error) {
nSize, err := w.WriteUint64(uint64(len(s)))
if err != nil {
return int64(nSize), err
}
nData, err := w.Write([]byte(s))
if err != nil {
return int64(nSize) + int64(nData), err
}
nPad, err := w.WritePadding(int64(len(s)))
return int64(nSize) + int64(nData) + int64(nPad), err
}
type FS interface {
Open(string) (fs.File, error)
Stat(string) (fs.FileInfo, error)
Lstat(string) (fs.FileInfo, error)
Readlink(string) (string, error)
}
func packFile(sw serializeWriter, root FS, fn string, stat fs.FileInfo) (int64, error) {
var nSoFar int64
write := func(data ...any) (int64, error) {
for _, datum := range data {
var n int64
var err error
switch datum := datum.(type) {
case string:
n, err = sw.WriteString(datum)
case uint64:
n, err = sw.WriteUint64(datum)
default:
return nSoFar, fmt.Errorf("unknown data type %T (%s)", datum, err)
}
if err != nil {
return nSoFar + n, err
}
nSoFar += n
}
return 0, nil
}
if n, err := write("("); err != nil {
return n, err
}
switch {
case stat.Mode()&fs.ModeDir != 0:
// Directory.
if n, err := write("type", "directory"); err != nil {
return n, err
}
f, err := root.Open(fn)
if err != nil {
return 0, err
}
defer f.Close()
dirF, ok := f.(fs.ReadDirFile)
if !ok {
return nSoFar, fmt.Errorf("%v didn't get me a ReadDirFile", fn)
}
dents, err := dirF.ReadDir(-1)
if err != nil {
return nSoFar, fmt.Errorf("reading dents from %v: %w", fn, err)
}
sort.Slice(dents, func(i, j int) bool {
return dents[i].Name() < dents[j].Name()
})
for _, dent := range dents {
if n, err := write("entry", "(", "name", dent.Name(), "node"); err != nil {
return n, err
}
dentStat, err := dent.Info()
if err != nil {
return nSoFar, fmt.Errorf("stat for %v: %w", path.Join(fn, dent.Name()), err)
}
n, err := packFile(sw, root, path.Join(fn, dent.Name()), dentStat)
if err != nil {
return nSoFar + n, err
}
nSoFar += n
if n, err := write(")"); err != nil {
return n, err
}
}
case stat.Mode()&fs.ModeSymlink != 0:
// Symlink.
target, err := root.Readlink(fn)
if err != nil {
return nSoFar, err
}
if n, err := write("type", "symlink", "target", target); err != nil {
return n, err
}
case stat.Mode().Type() != 0:
return 0, fmt.Errorf("not implemented (other: %s)", stat.Mode())
default:
// Regular file.
if n, err := write("type", "regular"); err != nil {
return n, err
}
if stat.Mode()&0o100 != 0 {
// Executable.
if n, err := write("executable", ""); err != nil {
return n, err
}
}
if n, err := write("contents", uint64(stat.Size())); err != nil {
return n, err
}
f, err := root.Open(fn)
if err != nil {
return 0, err
}
defer f.Close()
wrote, err := io.Copy(sw, f)
if err != nil {
return nSoFar + wrote, err
}
nSoFar += wrote
n, err := sw.WritePadding(wrote)
if err != nil {
return nSoFar + n, err
}
nSoFar += n
}
if n, err := write(")"); err != nil {
return n, err
}
return nSoFar, nil
}
func Pack(w io.Writer, fs FS, fn string) (int64, error) {
sw := serializeWriter{w}
n, err := sw.WriteString("nix-archive-1")
if err != nil {
return n, err
}
stat, err := fs.Lstat(fn)
if err != nil {
return n, fmt.Errorf("lstat(%q): %w", fn, err)
}
npf, err := packFile(sw, fs, fn, stat)
return npf + n, err
}