diff --git a/cmd/nardump/nardump.go b/cmd/nardump/nardump.go index 05be7b65a..f8947b02b 100644 --- a/cmd/nardump/nardump.go +++ b/cmd/nardump/nardump.go @@ -100,14 +100,13 @@ func (nw *narWriter) writeDir(dirPath string) error { sub := path.Join(dirPath, ent.Name()) var err error switch { - case mode.IsRegular(): - err = nw.writeRegular(sub) case mode.IsDir(): err = nw.writeDir(sub) + case mode.IsRegular(): + err = nw.writeRegular(sub) + case mode&os.ModeSymlink != 0: + err = nw.writeSymlink(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 { @@ -143,6 +142,23 @@ func (nw *narWriter) writeRegular(path string) error { return nil } +func (nw *narWriter) writeSymlink(path string) error { + nw.str("(") + nw.str("type") + nw.str("symlink") + nw.str("target") + // broken symlinks are valid in a nar + // given we do os.chdir(dir) and os.dirfs(".") above + // readlink now resolves relative links even if they are broken + link, err := os.Readlink(path) + if err != nil { + return err + } + nw.str(link) + nw.str(")") + return nil +} + func (nw *narWriter) str(s string) { if err := writeString(nw.w, s); err != nil { panic(writeNARError{err}) diff --git a/cmd/nardump/nardump_test.go b/cmd/nardump/nardump_test.go new file mode 100644 index 000000000..3b87e7962 --- /dev/null +++ b/cmd/nardump/nardump_test.go @@ -0,0 +1,52 @@ +// Copyright (c) Tailscale Inc & AUTHORS +// SPDX-License-Identifier: BSD-3-Clause + +package main + +import ( + "crypto/sha256" + "fmt" + "os" + "runtime" + "testing" +) + +// setupTmpdir sets up a known golden layout, covering all allowed file/folder types in a nar +func setupTmpdir(t *testing.T) string { + tmpdir := t.TempDir() + pwd, _ := os.Getwd() + os.Chdir(tmpdir) + defer os.Chdir(pwd) + os.MkdirAll("sub/dir", 0755) + os.Symlink("brokenfile", "brokenlink") + os.Symlink("sub/dir", "dirl") + os.Symlink("/abs/nonexistentdir", "dirb") + os.Create("sub/dir/file1") + f, _ := os.Create("file2m") + _ = f.Truncate(2 * 1024 * 1024) + f.Close() + os.Symlink("../file2m", "sub/goodlink") + return tmpdir +} + +func TestWriteNar(t *testing.T) { + if runtime.GOOS == "windows" { + // Skip test on Windows as the Nix package manager is not supported on this platform + t.Skip("nix package manager is not available on Windows") + } + dir := setupTmpdir(t) + t.Run("nar", func(t *testing.T) { + // obtained via `nix-store --dump /tmp/... | sha256sum` of the above test dir + expected := "727613a36f41030e93a4abf2649c3ec64a2757ccff364e3f6f7d544eb976e442" + h := sha256.New() + os.Chdir(dir) + err := writeNAR(h, os.DirFS(".")) + if err != nil { + t.Fatal(err) + } + hash := fmt.Sprintf("%x", h.Sum(nil)) + if expected != hash { + t.Fatal("sha256sum of nar not matched", hash, expected) + } + }) +}