From df70b411ff1a7ab45b096313f2c6e15893d239e1 Mon Sep 17 00:00:00 2001 From: Zhijie Yang Date: Mon, 11 Nov 2024 14:51:57 +0100 Subject: [PATCH] feat: add support for hard links --- internal/archive/archive.go | 6 +- internal/cache/cache.go | 2 +- internal/deb/extract.go | 137 ++++++++++++-- internal/deb/extract_test.go | 60 +++++- internal/fsutil/create.go | 35 +++- internal/fsutil/create_test.go | 110 +++++++++-- internal/manifest/manifest.go | 2 + internal/manifest/report.go | 57 ++++-- internal/manifest/report_test.go | 16 +- internal/setup/setup.go | 2 +- internal/slicer/slicer.go | 2 +- internal/slicer/slicer_test.go | 311 +++++++++++++++++++++++++------ internal/testutil/archive.go | 4 +- internal/testutil/nopcloser.go | 19 ++ internal/testutil/pkgdata.go | 13 ++ internal/testutil/treedump.go | 7 +- 16 files changed, 666 insertions(+), 117 deletions(-) create mode 100644 internal/testutil/nopcloser.go diff --git a/internal/archive/archive.go b/internal/archive/archive.go index c09fbdfb..a5b731af 100644 --- a/internal/archive/archive.go +++ b/internal/archive/archive.go @@ -18,7 +18,7 @@ import ( type Archive interface { Options() *Options - Fetch(pkg string) (io.ReadCloser, *PackageInfo, error) + Fetch(pkg string) (io.ReadSeekCloser, *PackageInfo, error) Exists(pkg string) bool Info(pkg string) (*PackageInfo, error) } @@ -120,7 +120,7 @@ func (a *ubuntuArchive) selectPackage(pkg string) (control.Section, *ubuntuIndex return selectedSection, selectedIndex, nil } -func (a *ubuntuArchive) Fetch(pkg string) (io.ReadCloser, *PackageInfo, error) { +func (a *ubuntuArchive) Fetch(pkg string) (io.ReadSeekCloser, *PackageInfo, error) { section, index, err := a.selectPackage(pkg) if err != nil { return nil, nil, err @@ -287,7 +287,7 @@ func (index *ubuntuIndex) checkComponents(components []string) error { return nil } -func (index *ubuntuIndex) fetch(suffix, digest string, flags fetchFlags) (io.ReadCloser, error) { +func (index *ubuntuIndex) fetch(suffix, digest string, flags fetchFlags) (io.ReadSeekCloser, error) { reader, err := index.archive.cache.Open(digest) if err == nil { return reader, nil diff --git a/internal/cache/cache.go b/internal/cache/cache.go index 09d6df03..1dabc0d1 100644 --- a/internal/cache/cache.go +++ b/internal/cache/cache.go @@ -132,7 +132,7 @@ func (c *Cache) Write(digest string, data []byte) error { return err2 } -func (c *Cache) Open(digest string) (io.ReadCloser, error) { +func (c *Cache) Open(digest string) (io.ReadSeekCloser, error) { if c.Dir == "" || digest == "" { return nil, MissErr } diff --git a/internal/deb/extract.go b/internal/deb/extract.go index 07f219bd..4da6a90f 100644 --- a/internal/deb/extract.go +++ b/internal/deb/extract.go @@ -38,6 +38,11 @@ type ExtractInfo struct { Context any } +type PendingHardlink struct { + TargetPath string + ExtractInfos []ExtractInfo +} + func getValidOptions(options *ExtractOptions) (*ExtractOptions, error) { for extractPath, extractInfos := range options.Extract { isGlob := strings.ContainsAny(extractPath, "*?") @@ -62,7 +67,7 @@ func getValidOptions(options *ExtractOptions) (*ExtractOptions, error) { return options, nil } -func Extract(pkgReader io.Reader, options *ExtractOptions) (err error) { +func Extract(pkgReader io.ReadSeeker, options *ExtractOptions) (err error) { defer func() { if err != nil { err = fmt.Errorf("cannot extract from package %q: %w", options.Package, err) @@ -83,43 +88,51 @@ func Extract(pkgReader io.Reader, options *ExtractOptions) (err error) { return err } + return extractData(pkgReader, validOpts) +} + +func getDataReader(pkgReader io.ReadSeeker) (io.ReadCloser, error) { arReader := ar.NewReader(pkgReader) - var dataReader io.Reader + var dataReader io.ReadCloser for dataReader == nil { arHeader, err := arReader.Next() if err == io.EOF { - return fmt.Errorf("no data payload") + return nil, fmt.Errorf("no data payload") } if err != nil { - return err + return nil, err } switch arHeader.Name { case "data.tar.gz": gzipReader, err := gzip.NewReader(arReader) if err != nil { - return err + return nil, err } - defer gzipReader.Close() dataReader = gzipReader case "data.tar.xz": xzReader, err := xz.NewReader(arReader) if err != nil { - return err + return nil, err } - dataReader = xzReader + dataReader = io.NopCloser(xzReader) case "data.tar.zst": zstdReader, err := zstd.NewReader(arReader) if err != nil { - return err + return nil, err } - defer zstdReader.Close() - dataReader = zstdReader + dataReader = zstdReader.IOReadCloser() } } - return extractData(dataReader, validOpts) + + return dataReader, nil } -func extractData(dataReader io.Reader, options *ExtractOptions) error { +func extractData(pkgReader io.ReadSeeker, options *ExtractOptions) error { + + dataReader, err := getDataReader(pkgReader) + if err != nil { + return err + } oldUmask := syscall.Umask(0) defer func() { @@ -136,6 +149,8 @@ func extractData(dataReader io.Reader, options *ExtractOptions) error { } } + pendingHardlinks := make(map[string][]PendingHardlink) + // When creating a file we will iterate through its parent directories and // create them with the permissions defined in the tarball. // @@ -246,20 +261,52 @@ func extractData(dataReader io.Reader, options *ExtractOptions) error { } } // Create the entry itself. + link := tarHeader.Linkname + if tarHeader.Typeflag == tar.TypeLink { + // A hard link requires the real path of the target file. + link = filepath.Join(options.TargetDir, link) + } + createOptions := &fsutil.CreateOptions{ Path: filepath.Join(options.TargetDir, targetPath), Mode: tarHeader.FileInfo().Mode(), Data: pathReader, - Link: tarHeader.Linkname, + Link: link, MakeParents: true, } err := options.Create(extractInfos, createOptions) if err != nil { - return err + // Handle the hardlink where its counterpart is not extracted + if tarHeader.Typeflag == tar.TypeLink && strings.HasPrefix(err.Error(), "link target does not exist") { + basePath := sanitizePath(tarHeader.Linkname) + pendingHardlinks[basePath] = append(pendingHardlinks[basePath], + PendingHardlink{ + TargetPath: targetPath, + ExtractInfos: extractInfos, + }) + pendingPaths[basePath] = true + } else { + return err + } } } } + // Second pass to create hard links + if len(pendingHardlinks) > 0 { + pkgReader.Seek(0, io.SeekStart) + dataReader, err = getDataReader(pkgReader) + if err != nil { + return err + } + tarReader := tar.NewReader(dataReader) + err = handlePendingHardlinks(options, pendingHardlinks, tarReader, &pendingPaths) + if err != nil { + return err + } + + } + if len(pendingPaths) > 0 { pendingList := make([]string, 0, len(pendingPaths)) for pendingPath := range pendingPaths { @@ -273,6 +320,58 @@ func extractData(dataReader io.Reader, options *ExtractOptions) error { } } + dataReader.Close() + + return nil +} + +func handlePendingHardlinks(options *ExtractOptions, pendingHardlinks map[string][]PendingHardlink, + tarReader *tar.Reader, pendingPaths *map[string]bool) error { + for { + tarHeader, err := tarReader.Next() + if err == io.EOF { + break + } + if err != nil { + return err + } + + sourcePath := sanitizePath(tarHeader.Name) + if sourcePath == "" { + continue + } + + hardlinks, ok := pendingHardlinks[sourcePath] + if !ok { + continue + } + + // Write the content for the first file in the hard link group + createOption := &fsutil.CreateOptions{ + Path: filepath.Join(options.TargetDir, hardlinks[0].TargetPath), + Mode: tarHeader.FileInfo().Mode(), + Data: tarReader, + } + + err = options.Create(hardlinks[0].ExtractInfos, createOption) + if err != nil { + return err + } + delete(*pendingPaths, sourcePath) + + // Create the hard links for the rest of the group + for _, hardlink := range hardlinks[1:] { + createOption := &fsutil.CreateOptions{ + Path: filepath.Join(options.TargetDir, hardlink.TargetPath), + Mode: tarHeader.FileInfo().Mode(), + Link: filepath.Join(options.TargetDir, hardlinks[0].TargetPath), + } + err := options.Create(hardlink.ExtractInfos, createOption) + if err != nil { + return err + } + } + } return nil } @@ -288,3 +387,11 @@ func parentDirs(path string) []string { } return parents } + +func sanitizePath(path string) string { + if len(path) < 3 || path[0] != '.' || path[1] != '/' { + return "" + } + path = path[1:] + return path +} diff --git a/internal/deb/extract_test.go b/internal/deb/extract_test.go index 22a1fd18..316ee8c7 100644 --- a/internal/deb/extract_test.go +++ b/internal/deb/extract_test.go @@ -352,6 +352,62 @@ var extractTests = []extractTest{{ }, }, error: `cannot extract from package "test-package": path /dir/ requested twice with diverging mode: 0777 != 0000`, +}, { + summary: "Dangling hard link", + pkgdata: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Hln(0644, "./link", "./non-existing-target"), + }), + options: deb.ExtractOptions{ + Extract: map[string][]deb.ExtractInfo{ + "/**": []deb.ExtractInfo{{ + Path: "/**", + }}, + }, + }, + error: `cannot extract from package "test-package": no content at \/non-existing-target`, +}, { + summary: "Hard link to symlink does not follow symlink", + pkgdata: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Lnk(0644, "./symlink", "./file"), + testutil.Hln(0644, "./hardlink", "./symlink"), + }), + options: deb.ExtractOptions{ + Extract: map[string][]deb.ExtractInfo{ + "/**": []deb.ExtractInfo{{ + Path: "/**", + }}, + }, + }, + result: map[string]string{ + "/hardlink": "symlink ./file", + "/symlink": "symlink ./file", + }, + notCreated: []string{}, +}, { + summary: "Extract all types of files", + pkgdata: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Dir(0755, "./dir/"), + testutil.Reg(0644, "./dir/file", "text for file"), + testutil.Lnk(0644, "./symlink", "./dir/file"), + testutil.Hln(0644, "./hardlink", "./dir/file"), + }), + options: deb.ExtractOptions{ + Extract: map[string][]deb.ExtractInfo{ + "/**": []deb.ExtractInfo{{ + Path: "/**", + }}, + }, + }, + result: map[string]string{ + "/dir/": "dir 0755", + "/dir/file": "file 0644 28121945", + "/hardlink": "file 0644 28121945", + "/symlink": "symlink ./dir/file", + }, + notCreated: []string{}, }} func (s *S) TestExtract(c *C) { @@ -377,7 +433,7 @@ func (s *S) TestExtract(c *C) { test.hackopt(&options) } - err := deb.Extract(bytes.NewBuffer(test.pkgdata), &options) + err := deb.Extract(bytes.NewReader(test.pkgdata), &options) if test.error != "" { c.Assert(err, ErrorMatches, test.error) continue @@ -488,7 +544,7 @@ func (s *S) TestExtractCreateCallback(c *C) { return nil } - err := deb.Extract(bytes.NewBuffer(test.pkgdata), &options) + err := deb.Extract(bytes.NewReader(test.pkgdata), &options) c.Assert(err, IsNil) c.Assert(createExtractInfos, DeepEquals, test.calls) diff --git a/internal/fsutil/create.go b/internal/fsutil/create.go index f76271f1..5d7c2d8d 100644 --- a/internal/fsutil/create.go +++ b/internal/fsutil/create.go @@ -15,6 +15,9 @@ type CreateOptions struct { Path string Mode fs.FileMode Data io.Reader + // If Link is set and the symlink flag is set in Mode, a symlink is + // created. If the Mode is not set to symlink, a hard link is created + // instead. Link string // If MakeParents is true, missing parent directories of Path are // created with permissions 0755. @@ -48,8 +51,14 @@ func Create(options *CreateOptions) (*Entry, error) { switch o.Mode & fs.ModeType { case 0: - err = createFile(o) - hash = hex.EncodeToString(rp.h.Sum(nil)) + if o.Link != "" { + // Creating the hard link does not involve reading the file. + // Therefore, its size and hash is not calculated here. + err = createHardLink(o) + } else { + err = createFile(o) + hash = hex.EncodeToString(rp.h.Sum(nil)) + } case fs.ModeDir: err = createDir(o) case fs.ModeSymlink: @@ -150,6 +159,28 @@ func createSymlink(o *CreateOptions) error { return os.Symlink(o.Link, o.Path) } +func createHardLink(o *CreateOptions) error { + debugf("Creating hard link: %s => %s", o.Path, o.Link) + linkInfo, err := os.Lstat(o.Link) + if err != nil && os.IsNotExist(err) { + return fmt.Errorf("link target does not exist: %s", o.Link) + } else if err != nil { + return err + } + + pathInfo, err := os.Lstat(o.Path) + if err == nil || os.IsExist(err) { + if os.SameFile(linkInfo, pathInfo) { + return nil + } + return fmt.Errorf("path %s already exists", o.Path) + } else if !os.IsNotExist(err) { + return err + } + + return os.Link(o.Link, o.Path) +} + // readerProxy implements the io.Reader interface proxying the calls to its // inner io.Reader. On each read, the proxy keeps track of the file size and hash. type readerProxy struct { diff --git a/internal/fsutil/create_test.go b/internal/fsutil/create_test.go index d41bbf5a..fc61a2cb 100644 --- a/internal/fsutil/create_test.go +++ b/internal/fsutil/create_test.go @@ -15,13 +15,15 @@ import ( ) type createTest struct { + summary string options fsutil.CreateOptions - hackdir func(c *C, dir string) + hackopt func(c *C, targetDir string, options *fsutil.CreateOptions) result map[string]string error string } var createTests = []createTest{{ + summary: "Create a file and its parent directory", options: fsutil.CreateOptions{ Path: "foo/bar", Data: bytes.NewBufferString("data1"), @@ -33,6 +35,7 @@ var createTests = []createTest{{ "/foo/bar": "file 0444 5b41362b", }, }, { + summary: "Create a symlink", options: fsutil.CreateOptions{ Path: "foo/bar", Link: "../baz", @@ -44,6 +47,7 @@ var createTests = []createTest{{ "/foo/bar": "symlink ../baz", }, }, { + summary: "Create a directory", options: fsutil.CreateOptions{ Path: "foo/bar", Mode: fs.ModeDir | 0444, @@ -54,6 +58,7 @@ var createTests = []createTest{{ "/foo/bar/": "dir 0444", }, }, { + summary: "Create a directory with sticky bit", options: fsutil.CreateOptions{ Path: "tmp", Mode: fs.ModeDir | fs.ModeSticky | 0775, @@ -62,37 +67,101 @@ var createTests = []createTest{{ "/tmp/": "dir 01775", }, }, { + summary: "Cannot create a parent directory without MakeParents set", options: fsutil.CreateOptions{ Path: "foo/bar", Mode: fs.ModeDir | 0775, }, - error: `.*: no such file or directory`, + error: `mkdir \/[^ ]*\/foo/bar: no such file or directory`, }, { + summary: "Re-creating an existing directory keeps the original mode", options: fsutil.CreateOptions{ Path: "foo", Mode: fs.ModeDir | 0775, }, - hackdir: func(c *C, dir string) { - c.Assert(os.Mkdir(filepath.Join(dir, "foo/"), fs.ModeDir|0765), IsNil) + hackopt: func(c *C, targetDir string, options *fsutil.CreateOptions) { + c.Assert(os.Mkdir(filepath.Join(targetDir, "foo/"), fs.ModeDir|0765), IsNil) }, result: map[string]string{ // mode is not updated. "/foo/": "dir 0765", }, }, { + summary: "Re-creating an existing file keeps the original mode", options: fsutil.CreateOptions{ Path: "foo", // Mode should be ignored for existing entry. Mode: 0644, Data: bytes.NewBufferString("changed"), }, - hackdir: func(c *C, dir string) { - c.Assert(os.WriteFile(filepath.Join(dir, "foo"), []byte("data"), 0666), IsNil) + hackopt: func(c *C, targetDir string, options *fsutil.CreateOptions) { + c.Assert(os.WriteFile(filepath.Join(targetDir, "foo"), []byte("data"), 0666), IsNil) }, result: map[string]string{ // mode is not updated. "/foo": "file 0666 d67e2e94", }, +}, { + summary: "Create a hard link", + options: fsutil.CreateOptions{ + Path: "dir/hardlink", + Link: "file", + Mode: 0644, + MakeParents: true, + }, + hackopt: func(c *C, targetDir string, options *fsutil.CreateOptions) { + c.Assert(os.WriteFile(filepath.Join(targetDir, "file"), []byte("data"), 0644), IsNil) + // An absolute path is required to create a hard link. + options.Link = filepath.Join(targetDir, options.Link) + }, + result: map[string]string{ + "/file": "file 0644 3a6eb079", + "/dir/": "dir 0755", + "/dir/hardlink": "file 0644 3a6eb079", + }, +}, { + summary: "Cannot create a hard link if the link target does not exist", + options: fsutil.CreateOptions{ + Path: "dir/hardlink", + Link: "missing-file", + Mode: 0644, + MakeParents: true, + }, + hackopt: func(c *C, targetDir string, options *fsutil.CreateOptions) { + options.Link = filepath.Join(targetDir, options.Link) + }, + error: `link target does not exist: \/[^ ]*\/missing-file`, +}, { + summary: "Re-creating a duplicated hard link keeps the original link", + options: fsutil.CreateOptions{ + Path: "hardlink", + Link: "file", + Mode: 0644, + MakeParents: true, + }, + hackopt: func(c *C, targetDir string, options *fsutil.CreateOptions) { + c.Assert(os.WriteFile(filepath.Join(targetDir, "file"), []byte("data"), 0644), IsNil) + c.Assert(os.Link(filepath.Join(targetDir, "file"), filepath.Join(targetDir, "hardlink")), IsNil) + options.Link = filepath.Join(targetDir, options.Link) + }, + result: map[string]string{ + "/file": "file 0644 3a6eb079", + "/hardlink": "file 0644 3a6eb079", + }, +}, { + summary: "Cannot create a hard link if the link path exists and it is not a hard link to the target", + options: fsutil.CreateOptions{ + Path: "hardlink", + Link: "file", + Mode: 0644, + MakeParents: true, + }, + hackopt: func(c *C, targetDir string, options *fsutil.CreateOptions) { + c.Assert(os.WriteFile(filepath.Join(targetDir, "file"), []byte("data"), 0644), IsNil) + c.Assert(os.WriteFile(filepath.Join(targetDir, "hardlink"), []byte("data"), 0644), IsNil) + options.Link = filepath.Join(targetDir, options.Link) + }, + error: `path \/[^ ]*\/hardlink already exists`, }} func (s *S) TestCreate(c *C) { @@ -102,17 +171,18 @@ func (s *S) TestCreate(c *C) { }() for _, test := range createTests { + c.Logf("Test: %s", test.summary) if test.result == nil { // Empty map for no files created. test.result = make(map[string]string) } c.Logf("Options: %v", test.options) dir := c.MkDir() - if test.hackdir != nil { - test.hackdir(c, dir) - } options := test.options options.Path = filepath.Join(dir, options.Path) + if test.hackopt != nil { + test.hackopt(c, dir, &options) + } entry, err := fsutil.Create(&options) if test.error != "" { @@ -122,15 +192,25 @@ func (s *S) TestCreate(c *C) { c.Assert(err, IsNil) c.Assert(testutil.TreeDump(dir), DeepEquals, test.result) + // [fsutil.Create] does not return information about parent directories // created implicitly. We only check for the requested path. - entry.Path = strings.TrimPrefix(entry.Path, dir) - // Add the slashes that TreeDump adds to the path. - slashPath := "/" + test.options.Path - if test.options.Mode.IsDir() { - slashPath = slashPath + "/" + if entry.Link != "" && entry.Mode&fs.ModeSymlink == 0 { + // Entry is a hard link. + pathInfo, err := os.Lstat(entry.Path) + c.Assert(err, IsNil) + linkInfo, err := os.Lstat(entry.Link) + c.Assert(err, IsNil) + os.SameFile(pathInfo, linkInfo) + } else { + entry.Path = strings.TrimPrefix(entry.Path, dir) + // Add the slashes that TreeDump adds to the path. + slashPath := "/" + test.options.Path + if test.options.Mode.IsDir() { + slashPath = slashPath + "/" + } + c.Assert(testutil.TreeDumpEntry(entry), DeepEquals, test.result[slashPath]) } - c.Assert(testutil.TreeDumpEntry(entry), DeepEquals, test.result[slashPath]) } } diff --git a/internal/manifest/manifest.go b/internal/manifest/manifest.go index 273d3106..60275de5 100644 --- a/internal/manifest/manifest.go +++ b/internal/manifest/manifest.go @@ -39,6 +39,7 @@ type Path struct { FinalSHA256 string `json:"final_sha256,omitempty"` Size uint64 `json:"size,omitempty"` Link string `json:"link,omitempty"` + HardLinkId int `json:"hard_link_id,omitempty"` } type Content struct { @@ -291,6 +292,7 @@ func manifestAddReport(dbw *jsonwall.DBWriter, report *Report) error { FinalSHA256: entry.FinalSHA256, Size: uint64(entry.Size), Link: entry.Link, + HardLinkId: entry.HardLinkId, }) if err != nil { return err diff --git a/internal/manifest/report.go b/internal/manifest/report.go index 092ed823..c482641b 100644 --- a/internal/manifest/report.go +++ b/internal/manifest/report.go @@ -18,6 +18,7 @@ type ReportEntry struct { Slices map[*setup.Slice]bool Link string FinalSHA256 string + HardLinkId int } // Report holds the information about files and directories created when slicing @@ -26,7 +27,8 @@ type Report struct { // Root is the filesystem path where the all reported content is based. Root string // Entries holds all reported content, indexed by their path. - Entries map[string]ReportEntry + Entries map[string]ReportEntry + currHardLinkId int } // NewReport returns an empty report for content that will be based at the @@ -52,26 +54,55 @@ func (r *Report) Add(slice *setup.Slice, fsEntry *fsutil.Entry) error { return fmt.Errorf("cannot add path to report: %s", err) } + // Handle the hard link group + hardLinkId := 0 + sha256 := fsEntry.SHA256 + size := fsEntry.Size + link := fsEntry.Link + if link != "" { + // Having the link target in root is a necessary but insufficient condition for a hardlink. + if strings.HasPrefix(fsEntry.Link, r.Root) { + relLinkPath, _ := r.sanitizeAbsPath(fsEntry.Link, false) + // With this, a hardlink is found + if entry, ok := r.Entries[relLinkPath]; ok { + if entry.HardLinkId == 0 { + r.currHardLinkId++ + entry.HardLinkId = r.currHardLinkId + r.Entries[relLinkPath] = entry + } + hardLinkId = entry.HardLinkId + if fsEntry.Mode.IsRegular() { // If the hardlink links to a regular file + sha256 = entry.SHA256 + size = entry.Size + link = "" + } else { // If the hardlink links to a symlink + link = entry.Link + } + } + } // else, this is a symlink + } + if entry, ok := r.Entries[relPath]; ok { if fsEntry.Mode != entry.Mode { return fmt.Errorf("path %s reported twice with diverging mode: 0%03o != 0%03o", relPath, fsEntry.Mode, entry.Mode) - } else if fsEntry.Link != entry.Link { - return fmt.Errorf("path %s reported twice with diverging link: %q != %q", relPath, fsEntry.Link, entry.Link) - } else if fsEntry.Size != entry.Size { - return fmt.Errorf("path %s reported twice with diverging size: %d != %d", relPath, fsEntry.Size, entry.Size) - } else if fsEntry.SHA256 != entry.SHA256 { - return fmt.Errorf("path %s reported twice with diverging hash: %q != %q", relPath, fsEntry.SHA256, entry.SHA256) + } else if link != entry.Link { + return fmt.Errorf("path %s reported twice with diverging link: %q != %q", relPath, link, entry.Link) + } else if size != entry.Size { + return fmt.Errorf("path %s reported twice with diverging size: %d != %d", relPath, size, entry.Size) + } else if sha256 != entry.SHA256 { + return fmt.Errorf("path %s reported twice with diverging hash: %q != %q", relPath, sha256, entry.SHA256) } entry.Slices[slice] = true r.Entries[relPath] = entry } else { r.Entries[relPath] = ReportEntry{ - Path: relPath, - Mode: fsEntry.Mode, - SHA256: fsEntry.SHA256, - Size: fsEntry.Size, - Slices: map[*setup.Slice]bool{slice: true}, - Link: fsEntry.Link, + Path: relPath, + Mode: fsEntry.Mode, + SHA256: sha256, + Size: size, + Slices: map[*setup.Slice]bool{slice: true}, + Link: link, + HardLinkId: hardLinkId, } } return nil diff --git a/internal/manifest/report_test.go b/internal/manifest/report_test.go index 9c001509..af0373a7 100644 --- a/internal/manifest/report_test.go +++ b/internal/manifest/report_test.go @@ -42,7 +42,7 @@ var sampleFile = fsutil.Entry{ var sampleLink = fsutil.Entry{ Path: "/base/example-link", - Mode: 0777, + Mode: fs.ModeSymlink | 0777, SHA256: "example-file_hash", Size: 5678, Link: "/base/example-file", @@ -108,7 +108,7 @@ var reportTests = []struct { expected: map[string]manifest.ReportEntry{ "/example-link": { Path: "/example-link", - Mode: 0777, + Mode: fs.ModeSymlink | 0777, SHA256: "example-file_hash", Size: 5678, Slices: map[*setup.Slice]bool{oneSlice: true}, @@ -192,16 +192,16 @@ var reportTests = []struct { }, { summary: "Error for same path distinct link", add: []sliceAndEntry{ - {entry: sampleFile, slice: oneSlice}, + {entry: sampleLink, slice: oneSlice}, {entry: fsutil.Entry{ - Path: sampleFile.Path, - Mode: sampleFile.Mode, - SHA256: sampleFile.SHA256, - Size: sampleFile.Size, + Path: sampleLink.Path, + Mode: sampleLink.Mode, + SHA256: sampleLink.SHA256, + Size: sampleLink.Size, Link: "distinct link", }, slice: oneSlice}, }, - err: `path /example-file reported twice with diverging link: "distinct link" != ""`, + err: `path /example-link reported twice with diverging link: "distinct link" != "/base/example-file"`, }, { summary: "Error for path outside root", add: []sliceAndEntry{ diff --git a/internal/setup/setup.go b/internal/setup/setup.go index 00337bb6..1d569ffe 100644 --- a/internal/setup/setup.go +++ b/internal/setup/setup.go @@ -124,7 +124,7 @@ func (s SliceKey) String() string { return s.Package + "_" + s.Slice } // Selection holds the required configuration to create a Build for a selection // of slices from a Release. It's still an abstract proposal in the sense that -// the real information coming from pacakges is still unknown, so referenced +// the real information coming from packages is still unknown, so referenced // paths could potentially be missing, for example. type Selection struct { Release *Release diff --git a/internal/slicer/slicer.go b/internal/slicer/slicer.go index 2991e253..2566e07a 100644 --- a/internal/slicer/slicer.go +++ b/internal/slicer/slicer.go @@ -145,7 +145,7 @@ func Run(options *RunOptions) error { } // Fetch all packages, using the selection order. - packages := make(map[string]io.ReadCloser) + packages := make(map[string]io.ReadSeekCloser) var pkgInfos []*archive.PackageInfo for _, slice := range options.Selection.Slices { if packages[slice.Package] != nil { diff --git a/internal/slicer/slicer_test.go b/internal/slicer/slicer_test.go index 6cdb8f01..fd1bfba6 100644 --- a/internal/slicer/slicer_test.go +++ b/internal/slicer/slicer_test.go @@ -101,11 +101,11 @@ var slicerTests = []slicerTest{{ "/other-dir/file": "symlink ../dir/file", }, manifestPaths: map[string]string{ - "/dir/file": "file 0644 cc55e2ec {test-package_myslice}", - "/dir/file-copy": "file 0644 cc55e2ec {test-package_myslice}", + "/dir/file": "file 0644 cc55e2ec <0> {test-package_myslice}", + "/dir/file-copy": "file 0644 cc55e2ec <0> {test-package_myslice}", "/dir/foo/bar/": "dir 01777 {test-package_myslice}", - "/dir/text-file": "file 0644 5b41362b {test-package_myslice}", - "/other-dir/file": "symlink ../dir/file {test-package_myslice}", + "/dir/text-file": "file 0644 5b41362b <0> {test-package_myslice}", + "/other-dir/file": "symlink ../dir/file <0> {test-package_myslice}", }, }, { summary: "Glob extraction", @@ -126,8 +126,8 @@ var slicerTests = []slicerTest{{ "/dir/other-file": "file 0644 63d5dd49", }, manifestPaths: map[string]string{ - "/dir/nested/other-file": "file 0644 6b86b273 {test-package_myslice}", - "/dir/other-file": "file 0644 63d5dd49 {test-package_myslice}", + "/dir/nested/other-file": "file 0644 6b86b273 <0> {test-package_myslice}", + "/dir/other-file": "file 0644 63d5dd49 <0> {test-package_myslice}", }, }, { summary: "Create new file under extracted directory and preserve parent directory permissions", @@ -147,7 +147,7 @@ var slicerTests = []slicerTest{{ "/parent/new": "file 0644 5b41362b", }, manifestPaths: map[string]string{ - "/parent/new": "file 0644 5b41362b {test-package_myslice}", + "/parent/new": "file 0644 5b41362b <0> {test-package_myslice}", }, }, { summary: "Create new nested file under extracted directory and preserve parent directory permissions", @@ -168,7 +168,7 @@ var slicerTests = []slicerTest{{ "/parent/permissions/new": "file 0644 5b41362b", }, manifestPaths: map[string]string{ - "/parent/permissions/new": "file 0644 5b41362b {test-package_myslice}", + "/parent/permissions/new": "file 0644 5b41362b <0> {test-package_myslice}", }, }, { summary: "Create new directory under extracted directory and preserve parent directory permissions", @@ -211,7 +211,7 @@ var slicerTests = []slicerTest{{ manifestPaths: map[string]string{ "/parent/": "dir 01777 {test-package_myslice}", "/parent/permissions/": "dir 0764 {test-package_myslice}", - "/parent/permissions/file": "file 0755 722c14b3 {test-package_myslice}", + "/parent/permissions/file": "file 0755 722c14b3 <0> {test-package_myslice}", }, }, { summary: "Conditional architecture", @@ -240,10 +240,10 @@ var slicerTests = []slicerTest{{ "/dir/nested/copy-3": "file 0644 84237a05", }, manifestPaths: map[string]string{ - "/dir/nested/copy-1": "file 0644 84237a05 {test-package_myslice}", - "/dir/nested/copy-3": "file 0644 84237a05 {test-package_myslice}", - "/dir/text-file-1": "file 0644 5b41362b {test-package_myslice}", - "/dir/text-file-3": "file 0644 5b41362b {test-package_myslice}", + "/dir/nested/copy-1": "file 0644 84237a05 <0> {test-package_myslice}", + "/dir/nested/copy-3": "file 0644 84237a05 <0> {test-package_myslice}", + "/dir/text-file-1": "file 0644 5b41362b <0> {test-package_myslice}", + "/dir/text-file-3": "file 0644 5b41362b <0> {test-package_myslice}", }, }, { summary: "Copyright is installed", @@ -273,7 +273,7 @@ var slicerTests = []slicerTest{{ "/usr/share/doc/test-package/copyright": "file 0644 c2fca2aa", }, manifestPaths: map[string]string{ - "/dir/file": "file 0644 cc55e2ec {test-package_myslice}", + "/dir/file": "file 0644 cc55e2ec <0> {test-package_myslice}", }, }, { summary: "Install two packages", @@ -314,9 +314,9 @@ var slicerTests = []slicerTest{{ }, manifestPaths: map[string]string{ "/foo/": "dir 0755 {test-package_myslice}", - "/dir/file": "file 0644 cc55e2ec {test-package_myslice}", + "/dir/file": "file 0644 cc55e2ec <0> {test-package_myslice}", "/bar/": "dir 0755 {other-package_myslice}", - "/file": "file 0644 fc02ca0e {other-package_myslice}", + "/file": "file 0644 fc02ca0e <0> {other-package_myslice}", }, }, { summary: "Install two packages, explicit path has preference over implicit parent", @@ -357,7 +357,7 @@ var slicerTests = []slicerTest{{ }, manifestPaths: map[string]string{ "/dir/": "dir 01777 {explicit-dir_myslice}", - "/dir/file": "file 0644 a441b15f {implicit-parent_myslice}", + "/dir/file": "file 0644 a441b15f <0> {implicit-parent_myslice}", }, }, { summary: "Valid same file in two slices in different packages", @@ -391,7 +391,7 @@ var slicerTests = []slicerTest{{ "/textFile": "file 0644 c6c83d10", }, manifestPaths: map[string]string{ - "/textFile": "file 0644 c6c83d10 {other-package_myslice,test-package_myslice}", + "/textFile": "file 0644 c6c83d10 <0> {other-package_myslice,test-package_myslice}", }, }, { summary: "Script: write a file", @@ -412,7 +412,7 @@ var slicerTests = []slicerTest{{ "/dir/text-file": "file 0644 d98cf53e", }, manifestPaths: map[string]string{ - "/dir/text-file": "file 0644 5b41362b d98cf53e {test-package_myslice}", + "/dir/text-file": "file 0644 5b41362b d98cf53e <0> {test-package_myslice}", }, }, { summary: "Script: read a file", @@ -437,8 +437,8 @@ var slicerTests = []slicerTest{{ "/foo/text-file-2": "file 0644 5b41362b", }, manifestPaths: map[string]string{ - "/dir/text-file-1": "file 0644 5b41362b {test-package_myslice}", - "/foo/text-file-2": "file 0644 d98cf53e 5b41362b {test-package_myslice}", + "/dir/text-file-1": "file 0644 5b41362b <0> {test-package_myslice}", + "/foo/text-file-2": "file 0644 d98cf53e 5b41362b <0> {test-package_myslice}", }, }, { summary: "Script: use 'until' to remove file after mutate", @@ -462,7 +462,7 @@ var slicerTests = []slicerTest{{ "/foo/text-file-2": "file 0644 5b41362b", }, manifestPaths: map[string]string{ - "/foo/text-file-2": "file 0644 d98cf53e 5b41362b {test-package_myslice}", + "/foo/text-file-2": "file 0644 d98cf53e 5b41362b <0> {test-package_myslice}", }, }, { summary: "Script: use 'until' to remove wildcard after mutate", @@ -501,7 +501,7 @@ var slicerTests = []slicerTest{{ "/dir/nested/file-copy": "file 0644 cc55e2ec", }, manifestPaths: map[string]string{ - "/dir/nested/file-copy": "file 0644 cc55e2ec {test-package_myslice}", + "/dir/nested/file-copy": "file 0644 cc55e2ec <0> {test-package_myslice}", }, }, { summary: "Script: writing same contents to existing file does not set the final hash in report", @@ -522,7 +522,7 @@ var slicerTests = []slicerTest{{ "/dir/text-file": "file 0644 5b41362b", }, manifestPaths: map[string]string{ - "/dir/text-file": "file 0644 5b41362b {test-package_myslice}", + "/dir/text-file": "file 0644 5b41362b <0> {test-package_myslice}", }, }, { summary: "Script: cannot write non-mutable files", @@ -838,8 +838,8 @@ var slicerTests = []slicerTest{{ "/other-file": "file 0644 fa0c9cdb", }, manifestPaths: map[string]string{ - "/file": "file 0644 7a3e00f5 {test-package_myslice}", - "/other-file": "file 0644 fa0c9cdb {other-package_myslice}", + "/file": "file 0644 7a3e00f5 <0> {test-package_myslice}", + "/other-file": "file 0644 fa0c9cdb <0> {other-package_myslice}", }, manifestPkgs: map[string]string{ "test-package": "test-package v1 a1 h1", @@ -905,7 +905,7 @@ var slicerTests = []slicerTest{{ "/file": "file 0644 fa0c9cdb", }, manifestPaths: map[string]string{ - "/file": "file 0644 fa0c9cdb {test-package_myslice}", + "/file": "file 0644 fa0c9cdb <0> {test-package_myslice}", }, manifestPkgs: map[string]string{ "test-package": "test-package v2 a2 h2", @@ -1062,7 +1062,7 @@ var slicerTests = []slicerTest{{ "/file": "file 0644 7a3e00f5", }, manifestPaths: map[string]string{ - "/file": "file 0644 7a3e00f5 {test-package_myslice}", + "/file": "file 0644 7a3e00f5 <0> {test-package_myslice}", }, manifestPkgs: map[string]string{ "test-package": "test-package v1 a1 h1", @@ -1099,11 +1099,11 @@ var slicerTests = []slicerTest{{ "/other-dir/file": "symlink ../dir/file", }, manifestPaths: map[string]string{ - "/dir/file": "file 0644 cc55e2ec {test-package_myslice1}", - "/dir/file-copy": "file 0644 cc55e2ec {test-package_myslice1}", + "/dir/file": "file 0644 cc55e2ec <0> {test-package_myslice1}", + "/dir/file-copy": "file 0644 cc55e2ec <0> {test-package_myslice1}", "/dir/foo/bar/": "dir 01777 {test-package_myslice1}", - "/dir/other-file": "file 0644 63d5dd49 {test-package_myslice2}", - "/other-dir/file": "symlink ../dir/file {test-package_myslice1}", + "/dir/other-file": "file 0644 63d5dd49 <0> {test-package_myslice2}", + "/other-dir/file": "symlink ../dir/file <0> {test-package_myslice1}", }, }, { summary: "Same glob in several entries with until:mutate and reading from script", @@ -1141,15 +1141,15 @@ var slicerTests = []slicerTest{{ }, manifestPaths: map[string]string{ "/dir/": "dir 0755 {test-package_myslice2}", - "/dir/file": "file 0644 cc55e2ec {test-package_myslice2}", + "/dir/file": "file 0644 cc55e2ec <0> {test-package_myslice2}", "/dir/nested/": "dir 0755 {test-package_myslice2}", - "/dir/nested/file": "file 0644 84237a05 {test-package_myslice2}", - "/dir/nested/other-file": "file 0644 6b86b273 {test-package_myslice2}", - "/dir/other-file": "file 0644 63d5dd49 {test-package_myslice2}", + "/dir/nested/file": "file 0644 84237a05 <0> {test-package_myslice2}", + "/dir/nested/other-file": "file 0644 6b86b273 <0> {test-package_myslice2}", + "/dir/other-file": "file 0644 63d5dd49 <0> {test-package_myslice2}", "/dir/several/": "dir 0755 {test-package_myslice2}", "/dir/several/levels/": "dir 0755 {test-package_myslice2}", "/dir/several/levels/deep/": "dir 0755 {test-package_myslice2}", - "/dir/several/levels/deep/file": "file 0644 6bc26dff {test-package_myslice2}", + "/dir/several/levels/deep/file": "file 0644 6bc26dff <0> {test-package_myslice2}", }, }, { summary: "Overlapping globs, until:mutate and reading from script", @@ -1187,15 +1187,15 @@ var slicerTests = []slicerTest{{ }, manifestPaths: map[string]string{ "/dir/": "dir 0755 {test-package_myslice1}", - "/dir/file": "file 0644 cc55e2ec {test-package_myslice1}", + "/dir/file": "file 0644 cc55e2ec <0> {test-package_myslice1}", "/dir/nested/": "dir 0755 {test-package_myslice1}", - "/dir/nested/file": "file 0644 84237a05 {test-package_myslice1}", - "/dir/nested/other-file": "file 0644 6b86b273 {test-package_myslice1}", - "/dir/other-file": "file 0644 63d5dd49 {test-package_myslice1}", + "/dir/nested/file": "file 0644 84237a05 <0> {test-package_myslice1}", + "/dir/nested/other-file": "file 0644 6b86b273 <0> {test-package_myslice1}", + "/dir/other-file": "file 0644 63d5dd49 <0> {test-package_myslice1}", "/dir/several/": "dir 0755 {test-package_myslice1}", "/dir/several/levels/": "dir 0755 {test-package_myslice1}", "/dir/several/levels/deep/": "dir 0755 {test-package_myslice1}", - "/dir/several/levels/deep/file": "file 0644 6bc26dff {test-package_myslice1}", + "/dir/several/levels/deep/file": "file 0644 6bc26dff <0> {test-package_myslice1}", }, }, { summary: "Overlapping glob and single entry, until:mutate on entry and reading from script", @@ -1233,15 +1233,15 @@ var slicerTests = []slicerTest{{ }, manifestPaths: map[string]string{ "/dir/": "dir 0755 {test-package_myslice1}", - "/dir/file": "file 0644 cc55e2ec {test-package_myslice1}", + "/dir/file": "file 0644 cc55e2ec <0> {test-package_myslice1}", "/dir/nested/": "dir 0755 {test-package_myslice1}", - "/dir/nested/file": "file 0644 84237a05 {test-package_myslice1}", - "/dir/nested/other-file": "file 0644 6b86b273 {test-package_myslice1}", - "/dir/other-file": "file 0644 63d5dd49 {test-package_myslice1}", + "/dir/nested/file": "file 0644 84237a05 <0> {test-package_myslice1}", + "/dir/nested/other-file": "file 0644 6b86b273 <0> {test-package_myslice1}", + "/dir/other-file": "file 0644 63d5dd49 <0> {test-package_myslice1}", "/dir/several/": "dir 0755 {test-package_myslice1}", "/dir/several/levels/": "dir 0755 {test-package_myslice1}", "/dir/several/levels/deep/": "dir 0755 {test-package_myslice1}", - "/dir/several/levels/deep/file": "file 0644 6bc26dff {test-package_myslice1}", + "/dir/several/levels/deep/file": "file 0644 6bc26dff <0> {test-package_myslice1}", }, }, { summary: "Overlapping glob and single entry, until:mutate on glob and reading from script", @@ -1270,7 +1270,7 @@ var slicerTests = []slicerTest{{ "/dir/file": "file 0644 cc55e2ec", }, manifestPaths: map[string]string{ - "/dir/file": "file 0644 cc55e2ec {test-package_myslice2}", + "/dir/file": "file 0644 cc55e2ec <0> {test-package_myslice2}", }, }, { summary: "Overlapping glob and single entry, until:mutate on both and reading from script", @@ -1319,7 +1319,7 @@ var slicerTests = []slicerTest{{ `, }, filesystem: map[string]string{"/file": "file 0644 2c26b46b"}, - manifestPaths: map[string]string{"/file": "file 0644 2c26b46b {test-package_myslice1,test-package_myslice2}"}, + manifestPaths: map[string]string{"/file": "file 0644 2c26b46b <0> {test-package_myslice1,test-package_myslice2}"}, }, { summary: "Install two packages, both are recorded", slices: []setup.SliceKey{ @@ -1445,6 +1445,211 @@ var slicerTests = []slicerTest{{ contents: `, }, +}, { + summary: "Valid hard link in two slices in the same package", + slices: []setup.SliceKey{ + {"test-package", "slice1"}, + {"test-package", "slice2"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Dir(0755, "./dir/"), + testutil.Reg(0644, "./dir/file", "text for file"), + testutil.Hln(0644, "./hardlink", "./dir/file"), + }), + }}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + slice1: + contents: + /dir/file: + /hardlink: + slice2: + contents: + /dir/file: + /hardlink: + `, + }, + filesystem: map[string]string{ + "/dir/": "dir 0755", + "/dir/file": "file 0644 28121945", + "/hardlink": "file 0644 28121945", + }, + manifestPaths: map[string]string{ + "/dir/file": "file 0644 28121945 <1> {test-package_slice1,test-package_slice2}", + "/hardlink": "file 0644 28121945 <1> {test-package_slice1,test-package_slice2}", + }, +}, { + summary: "Empty hard link is inflated with its counterpart", + slices: []setup.SliceKey{ + {"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Dir(0755, "./dir/"), + testutil.Reg(0644, "./dir/file", "text for file"), + testutil.Hln(0644, "./hardlink1", "./dir/file"), + testutil.Hln(0644, "./hardlink2", "./dir/file"), + }), + }}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /hardlink1: + /hardlink2: + `, + }, + filesystem: map[string]string{ + "/hardlink1": "file 0644 28121945", + "/hardlink2": "file 0644 28121945", + }, + manifestPaths: map[string]string{ + "/hardlink1": "file 0644 28121945 <1> {test-package_myslice}", + "/hardlink2": "file 0644 28121945 <1> {test-package_myslice}", + }, +}, { + summary: "Single hard link has 0 hardlink identifier", + slices: []setup.SliceKey{ + {"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Dir(0755, "./dir/"), + testutil.Reg(0644, "./dir/file", "text for file"), + testutil.Hln(0644, "./hardlink", "./dir/file"), + }), + }}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /hardlink: + `, + }, + filesystem: map[string]string{ + "/hardlink": "file 0644 28121945", + }, + manifestPaths: map[string]string{ + "/hardlink": "file 0644 28121945 <0> {test-package_myslice}", + }, +}, { + summary: "Hard link identifier distinguishes different hard links", + slices: []setup.SliceKey{ + {"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Dir(0755, "./dir/"), + testutil.Reg(0644, "./dir/file1", "text for file1"), + testutil.Reg(0644, "./dir/file2", "text for file2"), + testutil.Hln(0644, "./hardlink1", "./dir/file1"), + testutil.Hln(0644, "./hardlink2", "./dir/file2"), + }), + }}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /dir/file1: + /dir/file2: + /hardlink1: + /hardlink2: + `, + }, + filesystem: map[string]string{ + "/dir/": "dir 0755", + "/dir/file1": "file 0644 df82bbbd", + "/dir/file2": "file 0644 dcddda2e", + "/hardlink1": "file 0644 df82bbbd", + "/hardlink2": "file 0644 dcddda2e", + }, + manifestPaths: map[string]string{ + "/dir/file1": "file 0644 df82bbbd <1> {test-package_myslice}", + "/dir/file2": "file 0644 dcddda2e <2> {test-package_myslice}", + "/hardlink1": "file 0644 df82bbbd <1> {test-package_myslice}", + "/hardlink2": "file 0644 dcddda2e <2> {test-package_myslice}", + }, +}, { + summary: "Hard links handled with wildcard", + slices: []setup.SliceKey{ + {"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Dir(0755, "./dir/"), + testutil.Reg(0644, "./file1.txt", "text for file1"), + testutil.Reg(0644, "./dir/file2.txt", "text for file2"), + testutil.Hln(0644, "./dir/hardlink1.txt", "./file1.txt"), + testutil.Hln(0644, "./hardlink2.txt", "./dir/file2.txt"), + }), + }}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /**.txt: + `, + }, + filesystem: map[string]string{ + "/dir/": "dir 0755", + "/dir/file2.txt": "file 0644 dcddda2e", + "/dir/hardlink1.txt": "file 0644 df82bbbd", + "/file1.txt": "file 0644 df82bbbd", + "/hardlink2.txt": "file 0644 dcddda2e", + }, + manifestPaths: map[string]string{ + "/file1.txt": "file 0644 df82bbbd <1> {test-package_myslice}", + "/dir/file2.txt": "file 0644 dcddda2e <2> {test-package_myslice}", + "/dir/hardlink1.txt": "file 0644 df82bbbd <1> {test-package_myslice}", + "/hardlink2.txt": "file 0644 dcddda2e <2> {test-package_myslice}", + }, +}, { + summary: "Symlink is a valid hard link base file", slices: []setup.SliceKey{ + {"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Dir(0755, "./dir/"), + testutil.Reg(0644, "./dir/file", "text for file"), + testutil.Lnk(0644, "./symlink", "./dir/file"), + testutil.Hln(0644, "./hardlink", "./symlink"), + }), + }}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /hardlink: + /symlink: + `, + }, + filesystem: map[string]string{ + "/hardlink": "symlink ./dir/file", + "/symlink": "symlink ./dir/file", + }, + manifestPaths: map[string]string{ + "/symlink": "symlink ./dir/file <1> {test-package_myslice}", + "/hardlink": "symlink ./dir/file <1> {test-package_myslice}", + }, }} var defaultChiselYaml = ` @@ -1601,14 +1806,14 @@ func treeDumpManifestPaths(mfest *manifest.Manifest) (map[string]string, error) case strings.HasSuffix(path.Path, "/"): fsDump = fmt.Sprintf("dir %s", path.Mode) case path.Link != "": - fsDump = fmt.Sprintf("symlink %s", path.Link) + fsDump = fmt.Sprintf("symlink %s <%d>", path.Link, path.HardLinkId) default: // Regular if path.Size == 0 { - fsDump = fmt.Sprintf("file %s empty", path.Mode) + fsDump = fmt.Sprintf("file %s empty <%d>", path.Mode, path.HardLinkId) } else if path.FinalSHA256 != "" { - fsDump = fmt.Sprintf("file %s %s %s", path.Mode, path.SHA256[:8], path.FinalSHA256[:8]) + fsDump = fmt.Sprintf("file %s %s %s <%d>", path.Mode, path.SHA256[:8], path.FinalSHA256[:8], path.HardLinkId) } else { - fsDump = fmt.Sprintf("file %s %s", path.Mode, path.SHA256[:8]) + fsDump = fmt.Sprintf("file %s %s <%d>", path.Mode, path.SHA256[:8], path.HardLinkId) } } diff --git a/internal/testutil/archive.go b/internal/testutil/archive.go index ae8258c2..041130c2 100644 --- a/internal/testutil/archive.go +++ b/internal/testutil/archive.go @@ -26,7 +26,7 @@ func (a *TestArchive) Options() *archive.Options { return &a.Opts } -func (a *TestArchive) Fetch(pkgName string) (io.ReadCloser, *archive.PackageInfo, error) { +func (a *TestArchive) Fetch(pkgName string) (io.ReadSeekCloser, *archive.PackageInfo, error) { pkg, ok := a.Packages[pkgName] if !ok { return nil, nil, fmt.Errorf("cannot find package %q in archive", pkgName) @@ -37,7 +37,7 @@ func (a *TestArchive) Fetch(pkgName string) (io.ReadCloser, *archive.PackageInfo SHA256: pkg.Hash, Arch: pkg.Arch, } - return io.NopCloser(bytes.NewBuffer(pkg.Data)), info, nil + return ReadSeekerNopCloser(bytes.NewReader(pkg.Data)), info, nil } func (a *TestArchive) Exists(pkg string) bool { diff --git a/internal/testutil/nopcloser.go b/internal/testutil/nopcloser.go new file mode 100644 index 00000000..79a9839f --- /dev/null +++ b/internal/testutil/nopcloser.go @@ -0,0 +1,19 @@ +package testutil + +import ( + "io" +) + +// NopSeekCloser is an io.Reader that does nothing on Close, and +// seeks to the beginning of the stream on Seek. +// It is an extension of io.NopCloser that also implements io.Seeker. +type readSeekerNopCloser struct { + io.ReadSeeker +} + +// Close does nothing. +func (readSeekerNopCloser) Close() error { return nil } + +func ReadSeekerNopCloser(r io.ReadSeeker) io.ReadSeekCloser { + return readSeekerNopCloser{r} +} diff --git a/internal/testutil/pkgdata.go b/internal/testutil/pkgdata.go index 11bc9028..d151cd6c 100644 --- a/internal/testutil/pkgdata.go +++ b/internal/testutil/pkgdata.go @@ -197,3 +197,16 @@ func Lnk(mode int64, path, target string) TarEntry { }, } } + +// Hln is a shortcut for creating a hard link TarEntry structure (with +// tar.Typeflag set to tar.TypeLink). Hln stands for "Hard LiNk". +func Hln(mode int64, path, target string) TarEntry { + return TarEntry{ + Header: tar.Header{ + Typeflag: tar.TypeLink, + Name: path, + Mode: mode, + Linkname: target, + }, + } +} diff --git a/internal/testutil/treedump.go b/internal/testutil/treedump.go index 83b275e3..e4dee2d4 100644 --- a/internal/testutil/treedump.go +++ b/internal/testutil/treedump.go @@ -74,7 +74,12 @@ func TreeDumpEntry(entry *fsutil.Entry) string { return fmt.Sprintf("dir %#o", fperm) case fs.ModeSymlink: return fmt.Sprintf("symlink %s", entry.Link) - case 0: // Regular + case 0: + // Hard link. + if entry.Link != "" { + return fmt.Sprintf("hardlink %s", entry.Link) + } + // Regular file. if entry.Size == 0 { return fmt.Sprintf("file %#o empty", entry.Mode.Perm()) } else {