diff --git a/internal/archive/archive.go b/internal/archive/archive.go index 2a552084..06414a70 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, error) + Fetch(pkg string) (io.ReadSeekCloser, error) Exists(pkg string) bool } @@ -112,7 +112,7 @@ func (a *ubuntuArchive) selectPackage(pkg string) (control.Section, *ubuntuIndex return selectedSection, selectedIndex, nil } -func (a *ubuntuArchive) Fetch(pkg string) (io.ReadCloser, error) { +func (a *ubuntuArchive) Fetch(pkg string) (io.ReadSeekCloser, error) { section, index, err := a.selectPackage(pkg) if err != nil { return nil, err @@ -269,7 +269,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 406c1991..bea4c6b0 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,12 @@ func extractData(dataReader io.Reader, options *ExtractOptions) error { } } + // A mapping from the base file path to all the hard links that are pending + // We will put the entry only if the base file does not exist + // We will do the second pass if this map is not empty + // TODO we need both the modes and the extractInfo here + 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. // @@ -261,10 +280,37 @@ func extractData(dataReader io.Reader, options *ExtractOptions) error { } err := options.Create(extractInfos, createOptions) if err != nil { - return err + // Handles 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 + } } } } + // helperfunction() + + // 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)) @@ -279,6 +325,60 @@ 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 + } + // algorithm + // 1. read the contents of the sourcePath + createOption := &fsutil.CreateOptions{ + Path: filepath.Join(options.TargetDir, hardlinks[0].TargetPath), + Mode: 0644, //TODO change it to the recorded mode later + Data: tarReader, + } + + // TODO pass extractInfo into Create + err = options.Create(hardlinks[0].ExtractInfos, createOption) + if err != nil { + return err + } + delete(*pendingPaths, sourcePath) + + for _, hardlink := range hardlinks[1:] { + createOption := &fsutil.CreateOptions{ + Path: filepath.Join(options.TargetDir, hardlink.TargetPath), + Mode: 0644, //TODO change it to the recorded mode later + Link: filepath.Join(options.TargetDir, hardlinks[0].TargetPath), + } + // fmt.Println("====> ", "Creating", createOption) + // TODO pass extractInfo into Create + err := options.Create(hardlink.ExtractInfos, createOption) + if err != nil { + return err + } + } + } return nil } @@ -294,3 +394,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 1e78f641..316ee8c7 100644 --- a/internal/deb/extract_test.go +++ b/internal/deb/extract_test.go @@ -365,7 +365,7 @@ var extractTests = []extractTest{{ }}, }, }, - error: `cannot extract from package "test-package": link target does not exist: \/[^ ]*\/non-existing-target`, + 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{ @@ -433,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 @@ -544,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/slicer/report.go b/internal/slicer/report.go index 8897f518..798a56a3 100644 --- a/internal/slicer/report.go +++ b/internal/slicer/report.go @@ -11,13 +11,14 @@ import ( ) type ReportEntry struct { - Path string - Mode fs.FileMode - Hash string - Size int - Slices map[*setup.Slice]bool - Link string - FinalHash string + Path string + Mode fs.FileMode + Hash string + Size int + Slices map[*setup.Slice]bool + Link string + FinalHash 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 @@ -48,26 +50,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 + hash := fsEntry.Hash + size := fsEntry.Size + link := fsEntry.Link + if fsEntry.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 + hash = entry.Hash + size = entry.Size + link = relLinkPath + } else { // If the hardlink links to a symlink + link = entry.Link + } + } // else, this is a symlink + } // 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.Hash != entry.Hash { - return fmt.Errorf("path %s reported twice with diverging hash: %q != %q", relPath, fsEntry.Hash, entry.Hash) + } 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 hash != entry.Hash { + return fmt.Errorf("path %s reported twice with diverging hash: %q != %q", relPath, hash, entry.Hash) } entry.Slices[slice] = true r.Entries[relPath] = entry } else { r.Entries[relPath] = ReportEntry{ - Path: relPath, - Mode: fsEntry.Mode, - Hash: fsEntry.Hash, - Size: fsEntry.Size, - Slices: map[*setup.Slice]bool{slice: true}, - Link: fsEntry.Link, + Path: relPath, + Mode: fsEntry.Mode, + Hash: hash, + Size: size, + Slices: map[*setup.Slice]bool{slice: true}, + Link: link, + HardLinkId: hardLinkId, } } return nil diff --git a/internal/slicer/report_test.go b/internal/slicer/report_test.go index 762b35ad..b089eb61 100644 --- a/internal/slicer/report_test.go +++ b/internal/slicer/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, Hash: "example-file_hash", Size: 5678, Link: "/base/example-file", @@ -108,7 +108,7 @@ var reportTests = []struct { expected: map[string]slicer.ReportEntry{ "/example-link": { Path: "/example-link", - Mode: 0777, + Mode: fs.ModeSymlink | 0777, Hash: "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, - Hash: sampleFile.Hash, - Size: sampleFile.Size, + Path: sampleLink.Path, + Mode: sampleLink.Mode, + Hash: sampleLink.Hash, + 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/slicer/slicer.go b/internal/slicer/slicer.go index f164efd8..e177ff91 100644 --- a/internal/slicer/slicer.go +++ b/internal/slicer/slicer.go @@ -144,7 +144,7 @@ func Run(options *RunOptions) (*Report, error) { } // Fetch all packages, using the selection order. - packages := make(map[string]io.ReadCloser) + packages := make(map[string]io.ReadSeekCloser) for _, slice := range options.Selection.Slices { if packages[slice.Package] != nil { continue diff --git a/internal/slicer/slicer_test.go b/internal/slicer/slicer_test.go index cd40f3ce..1e7a6c92 100644 --- a/internal/slicer/slicer_test.go +++ b/internal/slicer/slicer_test.go @@ -98,11 +98,11 @@ var slicerTests = []slicerTest{{ "/other-dir/file": "symlink ../dir/file", }, report: 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", @@ -123,8 +123,8 @@ var slicerTests = []slicerTest{{ "/dir/other-file": "file 0644 63d5dd49", }, report: 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", @@ -144,7 +144,7 @@ var slicerTests = []slicerTest{{ "/parent/new": "file 0644 5b41362b", }, report: 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", @@ -165,7 +165,7 @@ var slicerTests = []slicerTest{{ "/parent/permissions/new": "file 0644 5b41362b", }, report: 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{{ report: 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", }, report: 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", @@ -272,7 +272,7 @@ var slicerTests = []slicerTest{{ "/usr/share/doc/test-package/copyright": "file 0644 c2fca2aa", }, report: map[string]string{ - "/dir/file": "file 0644 cc55e2ec {test-package_myslice}", + "/dir/file": "file 0644 cc55e2ec <0> {test-package_myslice}", }, }, { summary: "Install two packages", @@ -310,9 +310,9 @@ var slicerTests = []slicerTest{{ }, report: 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", @@ -350,7 +350,7 @@ var slicerTests = []slicerTest{{ }, report: 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", @@ -384,7 +384,7 @@ var slicerTests = []slicerTest{{ // Note: This is the only case where two slices can declare the same // file without conflicts. // TODO which slice(s) should own the file. - "/textFile": "file 0644 c6c83d10 {other-package_myslice}", + "/textFile": "file 0644 c6c83d10 <0> {other-package_myslice}", }, }, { summary: "Script: write a file", @@ -405,7 +405,7 @@ var slicerTests = []slicerTest{{ "/dir/text-file": "file 0644 d98cf53e", }, report: 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", @@ -430,8 +430,8 @@ var slicerTests = []slicerTest{{ "/foo/text-file-2": "file 0644 5b41362b", }, report: 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", @@ -455,7 +455,7 @@ var slicerTests = []slicerTest{{ "/foo/text-file-2": "file 0644 5b41362b", }, report: 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", @@ -494,7 +494,7 @@ var slicerTests = []slicerTest{{ "/dir/nested/file-copy": "file 0644 cc55e2ec", }, report: 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", @@ -515,7 +515,7 @@ var slicerTests = []slicerTest{{ "/dir/text-file": "file 0644 5b41362b", }, report: 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", @@ -790,7 +790,7 @@ var slicerTests = []slicerTest{{ "/dir/nested/file": "file 0644 84237a05", }, report: map[string]string{ - "/dir/nested/file": "file 0644 84237a05 {test-package_myslice}", + "/dir/nested/file": "file 0644 84237a05 <0> {test-package_myslice}", }, }, { summary: "Multiple slices of same package", @@ -824,11 +824,11 @@ var slicerTests = []slicerTest{{ "/other-dir/file": "symlink ../dir/file", }, report: 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", @@ -866,15 +866,15 @@ var slicerTests = []slicerTest{{ }, report: 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", @@ -912,15 +912,15 @@ var slicerTests = []slicerTest{{ }, report: 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", @@ -958,15 +958,15 @@ var slicerTests = []slicerTest{{ }, report: 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", @@ -995,7 +995,7 @@ var slicerTests = []slicerTest{{ "/dir/file": "file 0644 cc55e2ec", }, report: 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", @@ -1084,8 +1084,171 @@ var slicerTests = []slicerTest{{ "/hardlink": "file 0644 28121945", }, report: map[string]string{ - "/dir/file": "file 0644 28121945 {test-package_slice1,test-package_slice2}", - "/hardlink": "hardlink /dir/file {test-package_slice1,test-package_slice2}", + "/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: map[string][]byte{ + "test-package": 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", + }, + report: 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: map[string][]byte{ + "test-package": 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", + }, + report: 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: map[string][]byte{ + "test-package": 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", + }, + report: 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: map[string][]byte{ + "test-package": 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", + }, + report: 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: map[string][]byte{ + "test-package": 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", + }, + report: map[string]string{ + "/symlink": "symlink ./dir/file <1> {test-package_myslice}", + "/hardlink": "symlink ./dir/file <1> {test-package_myslice}", }, }} @@ -1111,9 +1274,9 @@ func (a *testArchive) Options() *archive.Options { return &a.options } -func (a *testArchive) Fetch(pkg string) (io.ReadCloser, error) { +func (a *testArchive) Fetch(pkg string) (io.ReadSeekCloser, error) { if data, ok := a.pkgs[pkg]; ok { - return io.NopCloser(bytes.NewBuffer(data)), nil + return testutil.ReadSeekerNopCloser(bytes.NewReader(data)), nil } return nil, fmt.Errorf("attempted to open %q package", pkg) } @@ -1231,21 +1394,15 @@ func treeDumpReport(report *slicer.Report) map[string]string { case fs.ModeDir: fsDump = fmt.Sprintf("dir %#o", fperm) case fs.ModeSymlink: - fsDump = fmt.Sprintf("symlink %s", entry.Link) + fsDump = fmt.Sprintf("symlink %s <%d>", entry.Link, entry.HardLinkId) case 0: - if entry.Link != "" { - // Hard link. - relLink := filepath.Clean("/" + strings.TrimPrefix(entry.Link, report.Root)) - fsDump = fmt.Sprintf("hardlink %s", relLink) + // Regular file. + if entry.Size == 0 { + fsDump = fmt.Sprintf("file %#o empty <%d>", entry.Mode.Perm(), entry.HardLinkId) + } else if entry.FinalHash != "" { + fsDump = fmt.Sprintf("file %#o %s %s <%d>", fperm, entry.Hash[:8], entry.FinalHash[:8], entry.HardLinkId) } else { - // Regular file. - if entry.Size == 0 { - fsDump = fmt.Sprintf("file %#o empty", entry.Mode.Perm()) - } else if entry.FinalHash != "" { - fsDump = fmt.Sprintf("file %#o %s %s", fperm, entry.Hash[:8], entry.FinalHash[:8]) - } else { - fsDump = fmt.Sprintf("file %#o %s", fperm, entry.Hash[:8]) - } + fsDump = fmt.Sprintf("file %#o %s <%d>", fperm, entry.Hash[:8], entry.HardLinkId) } default: panic(fmt.Errorf("unknown file type %d: %s", entry.Mode.Type(), entry.Path))