Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 33 additions & 1 deletion internal/fsutil/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
20 changes: 16 additions & 4 deletions internal/fsutil/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package fsutil_test

import (
"bytes"
"crypto/sha256"
"encoding/hex"
"io/fs"
"os"
"path/filepath"
Expand Down Expand Up @@ -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.
Expand Down
28 changes: 17 additions & 11 deletions internal/manifestutil/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
32 changes: 32 additions & 0 deletions internal/manifestutil/report_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{{
Expand Down
51 changes: 51 additions & 0 deletions internal/slicer/slicer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
Loading