diff --git a/internal/deb/extract.go b/internal/deb/extract.go index 07f219bd..6cb67c1c 100644 --- a/internal/deb/extract.go +++ b/internal/deb/extract.go @@ -246,11 +246,17 @@ func extractData(dataReader io.Reader, options *ExtractOptions) error { } } // Create the entry itself. + link := tarHeader.Linkname + if tarHeader.Typeflag == tar.TypeLink { + // The 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) diff --git a/internal/deb/extract_test.go b/internal/deb/extract_test.go index 22a1fd18..31d19373 100644 --- a/internal/deb/extract_test.go +++ b/internal/deb/extract_test.go @@ -352,6 +352,40 @@ var extractTests = []extractTest{{ }, }, error: `cannot extract from package "test-package": path /dir/ requested twice with diverging mode: 0777 != 0000`, +}, { + summary: "Hard link must be created if specified in the tarball", + pkgdata: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + testutil.Reg(0644, "./file1.txt", "text for file1.txt"), + testutil.Hln(0644, "./file2.txt", "./file1.txt"), + }), + options: deb.ExtractOptions{ + Extract: map[string][]deb.ExtractInfo{ + "/*.txt": []deb.ExtractInfo{{ + Path: "/*.txt", + }}, + }, + }, + result: map[string]string{ + "/file1.txt": "file 0644 e926a7fb", + "/file2.txt": "file 0644 e926a7fb", + }, + notCreated: []string{}, +}, { + summary: "Dangling hard link must raise an error", + pkgdata: testutil.MustMakeDeb([]testutil.TarEntry{ + testutil.Dir(0755, "./"), + // testutil.Reg(0644, "./file1.txt", "text for file1.txt"), + testutil.Hln(0644, "./file2.txt", "./file1.txt"), + }), + options: deb.ExtractOptions{ + Extract: map[string][]deb.ExtractInfo{ + "/*.txt": []deb.ExtractInfo{{ + Path: "/*.txt", + }}, + }, + }, + error: "cannot extract from package \"test-package\": the target file does not exist: .*/file1.txt", }} func (s *S) TestExtract(c *C) { diff --git a/internal/fsutil/create.go b/internal/fsutil/create.go index 48b12cb7..f135eef5 100644 --- a/internal/fsutil/create.go +++ b/internal/fsutil/create.go @@ -48,8 +48,12 @@ 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 != "" { + err = createHardLink(o) + } else { + err = createFile(o) + hash = hex.EncodeToString(rp.h.Sum(nil)) + } case fs.ModeDir: err = createDir(o) case fs.ModeSymlink: @@ -121,6 +125,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) + targetInfo, err := os.Lstat(o.Link) + if err != nil && os.IsNotExist(err) { + return fmt.Errorf("the target file does not exist: %s", o.Link) + } else if err != nil { + return err + } + + linkInfo, err := os.Lstat(o.Path) + if err == nil || os.IsExist(err) { + if os.SameFile(targetInfo, linkInfo) { + return nil + } + return fmt.Errorf("the link already exists: %s", 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/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, + }, + } +}