Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: handle hardlinks #17

Merged
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
0601fda
feat: handle hardlinks
zhijie-yang Jul 18, 2024
ffd9951
test: hard link against symlink
zhijie-yang Jul 18, 2024
85a4cf9
test: hard link testing for fsutil.create
zhijie-yang Jul 18, 2024
19fc4ff
test: make TreeDumpEntry handle hard link
zhijie-yang Jul 19, 2024
eefd744
test: combine into hackopt
zhijie-yang Jul 19, 2024
1237a08
test: extract all types of file
zhijie-yang Jul 19, 2024
bc02824
chore: add comments for hard link assumptions
zhijie-yang Jul 19, 2024
a8bd58e
test: comprehensive cases for hard link in create_test
zhijie-yang Jul 19, 2024
381f563
Revert "test: make TreeDumpEntry handle hard link"
zhijie-yang Jul 19, 2024
c8a7931
test: handle hard link test in createTest suite
zhijie-yang Jul 19, 2024
265c49e
chore: adjust error/test messages and test matching regex
zhijie-yang Jul 19, 2024
7b21a1e
chore: fix comments
zhijie-yang Jul 22, 2024
06ccac5
test: TreeDumpEntry to handle hard links
zhijie-yang Jul 22, 2024
4090b4c
fix: multiple adjusts according to code review
zhijie-yang Jul 23, 2024
d3a7921
chore: adjust comments for fsutil.Create
zhijie-yang Jul 23, 2024
a798078
feat: drop handling hard link in TreeDumpEntry
zhijie-yang Jul 23, 2024
2782eb8
Revert "feat: drop handling hard link in TreeDumpEntry"
zhijie-yang Jul 24, 2024
3e8d872
chore: change commits and test case summaries
zhijie-yang Jul 24, 2024
03ce4d3
chore: change variable naming
zhijie-yang Jul 24, 2024
5b29138
feat: add test for same hardlink in slicer_test
zhijie-yang Jul 24, 2024
0dca7a0
test: update duplicate hardlink in slicer_test
zhijie-yang Jul 25, 2024
acec1a7
test: treeDumpReport hardlink target uses abs path
zhijie-yang Jul 26, 2024
4313070
chore: adjust tree dump hardlink string
zhijie-yang Jul 29, 2024
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
8 changes: 7 additions & 1 deletion internal/deb/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,11 +246,17 @@ 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)
Expand Down
56 changes: 56 additions & 0 deletions internal/deb/extract_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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": link target does not exist: \/[^ ]*\/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",
zhijie-yang marked this conversation as resolved.
Show resolved Hide resolved
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) {
Expand Down
35 changes: 33 additions & 2 deletions internal/fsutil/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -121,6 +130,28 @@ func createSymlink(o *CreateOptions) error {
return os.Symlink(o.Link, o.Path)
}

func createHardLink(o *CreateOptions) error {
zhijie-yang marked this conversation as resolved.
Show resolved Hide resolved
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 {
Expand Down
110 changes: 95 additions & 15 deletions internal/fsutil/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand All @@ -33,6 +35,7 @@ var createTests = []createTest{{
"/foo/bar": "file 0444 5b41362b",
},
}, {
summary: "Create a symlink",
options: fsutil.CreateOptions{
Path: "foo/bar",
Link: "../baz",
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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) {
Expand All @@ -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 != "" {
Expand All @@ -122,14 +192,24 @@ 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 {
zhijie-yang marked this conversation as resolved.
Show resolved Hide resolved
// 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])
}
}
Loading
Loading