diff --git a/internal/fsutil/create.go b/internal/fsutil/create.go index 0503c96f1..6d2ef3e29 100644 --- a/internal/fsutil/create.go +++ b/internal/fsutil/create.go @@ -57,6 +57,7 @@ func Create(options *CreateOptions) (*Entry, error) { } var hash string + var size int if o.MakeParents { if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { return nil, err @@ -73,9 +74,25 @@ func Create(options *CreateOptions) (*Entry, error) { } } err = createHardLink(o) + if err == nil { + // A link to a regular file carries that content, so hash it + // from disk and the entry can stand on its own if the target + // is not reported. A link to a symlink has no content. + var info fs.FileInfo + info, err = os.Lstat(path) + if err == nil && info.Mode().IsRegular() { + hash, size, err = hashFile(path) + if os.IsPermission(err) { + // The content is not readable, so the entry has no + // hash of its own. + hash, size, err = "", 0, nil + } + } + } } else { err = createFile(o) hash = hex.EncodeToString(rp.h.Sum(nil)) + size = rp.size } case fs.ModeDir: err = createDir(o) @@ -114,12 +131,27 @@ func Create(options *CreateOptions) (*Entry, error) { Path: path, Mode: mode, SHA256: hash, - Size: rp.size, + Size: size, Link: o.Link, } return entry, nil } +// hashFile returns the SHA256 and size of the content at path. +func hashFile(path string) (string, int, error) { + file, err := os.Open(path) + if err != nil { + return "", 0, err + } + defer file.Close() + h := sha256.New() + n, err := io.Copy(h, file) + if err != nil { + return "", 0, err + } + return hex.EncodeToString(h.Sum(nil)), int(n), nil +} + // CreateWriter handles the creation of a regular file and collects the // information recorded in Entry. The Hash and Size attributes are set on // calling Close() on the Writer. diff --git a/internal/fsutil/create_test.go b/internal/fsutil/create_test.go index 5e908fccc..cff0d20bb 100644 --- a/internal/fsutil/create_test.go +++ b/internal/fsutil/create_test.go @@ -2,6 +2,8 @@ package fsutil_test import ( "bytes" + "crypto/sha256" + "encoding/hex" "io/fs" "os" "path/filepath" @@ -332,16 +334,26 @@ func (s *S) TestCreate(c *C) { c.Assert(testutil.TreeDump(dir), DeepEquals, test.result) if options.Mode.IsRegular() && options.Link != "" { - // We should test hard link entries differently because - // fsutil.Create does not return hash or size when it creates hard - // links. + // Hard link entries are checked differently: the entry carries + // the hash and size of the content when the target is a regular + // file, and nothing when it is a symlink. c.Assert(entry.Link, Equals, options.Link) c.Assert(entry.Mode.IsRegular(), Equals, true) pathInfo, err := os.Lstat(entry.Path) c.Assert(err, IsNil) linkInfo, err := os.Lstat(entry.Link) c.Assert(err, IsNil) - os.SameFile(pathInfo, linkInfo) + c.Assert(os.SameFile(pathInfo, linkInfo), Equals, true) + if pathInfo.Mode().IsRegular() { + data, err := os.ReadFile(entry.Path) + c.Assert(err, IsNil) + sum := sha256.Sum256(data) + c.Assert(entry.SHA256, Equals, hex.EncodeToString(sum[:])) + c.Assert(entry.Size, Equals, len(data)) + } else { + c.Assert(entry.SHA256, Equals, "") + c.Assert(entry.Size, Equals, 0) + } } else { // [fsutil.Create] does not return information about parent directories // created implicitly. We only check for the requested path. diff --git a/internal/manifestutil/report.go b/internal/manifestutil/report.go index c294b1d22..98295af24 100644 --- a/internal/manifestutil/report.go +++ b/internal/manifestutil/report.go @@ -63,19 +63,25 @@ func (r *Report) Add(slice *setup.Slice, fsEntry *fsutil.Entry) error { if fsEntry.Mode.IsRegular() && fsEntry.Link != "" { // Hard link. relLinkPath, _ := r.sanitizeAbsPath(fsEntry.Link, false) - entry, ok := r.Entries[relLinkPath] - if !ok { + if entry, ok := r.Entries[relLinkPath]; ok { + if entry.Inode == 0 { + r.lastInode += 1 + entry.Inode = r.lastInode + r.Entries[relLinkPath] = entry + } + inode = entry.Inode + fsEntryCpy.SHA256 = entry.SHA256 + fsEntryCpy.Size = entry.Size + fsEntryCpy.Link = entry.Link + } else if fsEntry.SHA256 != "" { + // The target is not in the report, for instance because it is + // listed with "until: mutate", so the link is reported as a + // regular file with its own content. + fsEntryCpy.Link = "" + } else { + // A hard link to a symlink carries no content of its own. return fmt.Errorf("cannot add hard link %s to report: target %s not previously added", relPath, relLinkPath) } - if entry.Inode == 0 { - r.lastInode += 1 - entry.Inode = r.lastInode - r.Entries[relLinkPath] = entry - } - inode = entry.Inode - fsEntryCpy.SHA256 = entry.SHA256 - fsEntryCpy.Size = entry.Size - fsEntryCpy.Link = entry.Link } if entry, ok := r.Entries[relPath]; ok { diff --git a/internal/manifestutil/report_test.go b/internal/manifestutil/report_test.go index d56acddb6..6c76ce7cb 100644 --- a/internal/manifestutil/report_test.go +++ b/internal/manifestutil/report_test.go @@ -296,6 +296,38 @@ var reportTests = []struct { Inode: 1, }, }, +}, { + summary: "Hard link to a target that is not in the report", + add: []sliceAndEntry{{ + entry: fsutil.Entry{ + Path: "/base/example-hard-link", + Mode: 0777, + SHA256: "example-file_hash", + Size: 5678, + Link: "/base/example-file", + }, + slice: oneSlice, + }}, + expected: map[string]manifestutil.ReportEntry{ + "/example-hard-link": { + Path: "/example-hard-link", + Mode: 0777, + SHA256: "example-file_hash", + Size: 5678, + Slices: map[*setup.Slice]bool{oneSlice: true}, + }, + }, +}, { + summary: "Hard link without content to a target that is not in the report", + add: []sliceAndEntry{{ + entry: fsutil.Entry{ + Path: "/base/example-hard-link", + Mode: 0777, + Link: "/base/example-symlink", + }, + slice: oneSlice, + }}, + err: `cannot add hard link /example-hard-link to report: target /example-symlink not previously added`, }, { summary: "Multiple hard links groups", add: []sliceAndEntry{{ diff --git a/internal/slicer/slicer_test.go b/internal/slicer/slicer_test.go index d6ef9ca0d..018ab7367 100644 --- a/internal/slicer/slicer_test.go +++ b/internal/slicer/slicer_test.go @@ -1661,6 +1661,57 @@ var slicerTests = []slicerTest{{ "/symlink": "symlink ./file <1> {test-package_myslice}", "/hardlink": "symlink ./file <1> {test-package_myslice}", }, +}, { + summary: "Hard link whose target has until:mutate", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Reg(0644, "./file", "foo"), + testutil.Hrd(0644, "./hardlink", "./file"), + }), + }}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /file: {until: mutate} + /hardlink: + `, + }, + // The target is not in the manifest, so the link is a regular file there. + filesystem: map[string]string{ + "/hardlink": "file 0644 2c26b46b", + }, + manifestPaths: map[string]string{ + "/hardlink": "file 0644 2c26b46b {test-package_myslice}", + }, +}, { + summary: "Hard link to a symlink that has until:mutate", + slices: []setup.SliceKey{{"test-package", "myslice"}}, + pkgs: []*testutil.TestPackage{{ + Name: "test-package", + Data: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Reg(0644, "./file", "foo"), + testutil.Lnk(0644, "./symlink", "./file"), + testutil.Hrd(0644, "./hardlink", "./symlink"), + }), + }}, + release: map[string]string{ + "slices/mydir/test-package.yaml": ` + package: test-package + slices: + myslice: + contents: + /symlink: {until: mutate} + /hardlink: + `, + }, + error: `cannot extract from package "test-package": cannot add hard link /hardlink to report: target /symlink not previously added`, }, { summary: "Hard link identifiers are unique across packages", slices: []setup.SliceKey{