185 lines
3.8 KiB
Go
185 lines
3.8 KiB
Go
// Copyright (c) Tailscale Inc & AUTHORS
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
// nardump is like nix-store --dump, but in Go, writing a NAR
|
|
// file (tar-like, but focused on being reproducible) to stdout
|
|
// or to a hash with the --sri flag.
|
|
//
|
|
// It lets us calculate a Nix sha256 without the person running
|
|
// git-pull-oss.sh having Nix available.
|
|
package main
|
|
|
|
// For the format, see:
|
|
// See https://gist.github.com/jbeda/5c79d2b1434f0018d693
|
|
|
|
import (
|
|
"bufio"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/binary"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"log"
|
|
"os"
|
|
"path"
|
|
"sort"
|
|
)
|
|
|
|
var sri = flag.Bool("sri", false, "print SRI")
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
if flag.NArg() != 1 {
|
|
log.Fatal("usage: nardump <dir>")
|
|
}
|
|
arg := flag.Arg(0)
|
|
if err := os.Chdir(arg); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
if *sri {
|
|
hash := sha256.New()
|
|
if err := writeNAR(hash, os.DirFS(".")); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
fmt.Printf("sha256-%s\n", base64.StdEncoding.EncodeToString(hash.Sum(nil)))
|
|
return
|
|
}
|
|
bw := bufio.NewWriter(os.Stdout)
|
|
if err := writeNAR(bw, os.DirFS(".")); err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
bw.Flush()
|
|
}
|
|
|
|
// writeNARError is a sentinel panic type that's recovered by writeNAR
|
|
// and converted into the wrapped error.
|
|
type writeNARError struct{ err error }
|
|
|
|
// narWriter writes NAR files.
|
|
type narWriter struct {
|
|
w io.Writer
|
|
fs fs.FS
|
|
}
|
|
|
|
// writeNAR writes a NAR file to w from the root of fs.
|
|
func writeNAR(w io.Writer, fs fs.FS) (err error) {
|
|
defer func() {
|
|
if e := recover(); e != nil {
|
|
if we, ok := e.(writeNARError); ok {
|
|
err = we.err
|
|
return
|
|
}
|
|
panic(e)
|
|
}
|
|
}()
|
|
nw := &narWriter{w: w, fs: fs}
|
|
nw.str("nix-archive-1")
|
|
return nw.writeDir(".")
|
|
}
|
|
|
|
func (nw *narWriter) writeDir(dirPath string) error {
|
|
ents, err := fs.ReadDir(nw.fs, dirPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
sort.Slice(ents, func(i, j int) bool {
|
|
return ents[i].Name() < ents[j].Name()
|
|
})
|
|
nw.str("(")
|
|
nw.str("type")
|
|
nw.str("directory")
|
|
for _, ent := range ents {
|
|
nw.str("entry")
|
|
nw.str("(")
|
|
nw.str("name")
|
|
nw.str(ent.Name())
|
|
nw.str("node")
|
|
mode := ent.Type()
|
|
sub := path.Join(dirPath, ent.Name())
|
|
var err error
|
|
switch {
|
|
case mode.IsRegular():
|
|
err = nw.writeRegular(sub)
|
|
case mode.IsDir():
|
|
err = nw.writeDir(sub)
|
|
default:
|
|
// TODO(bradfitz): symlink, but requires fighting io/fs a bit
|
|
// to get at Readlink or the osFS via fs. But for now
|
|
// we don't need symlinks because they're not in Go's archive.
|
|
return fmt.Errorf("unsupported file type %v at %q", sub, mode)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nw.str(")")
|
|
}
|
|
nw.str(")")
|
|
return nil
|
|
}
|
|
|
|
func (nw *narWriter) writeRegular(path string) error {
|
|
nw.str("(")
|
|
nw.str("type")
|
|
nw.str("regular")
|
|
fi, err := fs.Stat(nw.fs, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if fi.Mode()&0111 != 0 {
|
|
nw.str("executable")
|
|
nw.str("")
|
|
}
|
|
contents, err := fs.ReadFile(nw.fs, path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nw.str("contents")
|
|
if err := writeBytes(nw.w, contents); err != nil {
|
|
return err
|
|
}
|
|
nw.str(")")
|
|
return nil
|
|
}
|
|
|
|
func (nw *narWriter) str(s string) {
|
|
if err := writeString(nw.w, s); err != nil {
|
|
panic(writeNARError{err})
|
|
}
|
|
}
|
|
|
|
func writeString(w io.Writer, s string) error {
|
|
var buf [8]byte
|
|
binary.LittleEndian.PutUint64(buf[:], uint64(len(s)))
|
|
if _, err := w.Write(buf[:]); err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.WriteString(w, s); err != nil {
|
|
return err
|
|
}
|
|
return writePad(w, len(s))
|
|
}
|
|
|
|
func writeBytes(w io.Writer, b []byte) error {
|
|
var buf [8]byte
|
|
binary.LittleEndian.PutUint64(buf[:], uint64(len(b)))
|
|
if _, err := w.Write(buf[:]); err != nil {
|
|
return err
|
|
}
|
|
if _, err := w.Write(b); err != nil {
|
|
return err
|
|
}
|
|
return writePad(w, len(b))
|
|
}
|
|
|
|
func writePad(w io.Writer, n int) error {
|
|
pad := n % 8
|
|
if pad == 0 {
|
|
return nil
|
|
}
|
|
var zeroes [8]byte
|
|
_, err := w.Write(zeroes[:8-pad])
|
|
return err
|
|
}
|