Skip to content

Commit

Permalink
Implementing the methods to detect hardlinks on windows
Browse files Browse the repository at this point in the history
Signed-off-by: Juan Bustamante <[email protected]>
  • Loading branch information
jjbustamante committed Nov 15, 2023
1 parent 9c53229 commit 5f5f2af
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 23 deletions.
41 changes: 28 additions & 13 deletions pkg/archive/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,19 +219,8 @@ func WriteDirToTar(tw TarWriter, srcDir, basePath string, uid, gid int, mode int
}

header.Name = getHeaderNameFromBaseAndRelPath(basePath, relPath)
if hasHardlinks(fi) {
inode, err := getInodeFromStat(fi.Sys())
if err != nil {
return err
}

if processedPath, ok := hardLinkFiles[inode]; ok {
header.Typeflag = tar.TypeLink
header.Linkname = processedPath
header.Size = 0
} else {
hardLinkFiles[inode] = header.Name
}
if err = processHardLinks(file, fi, hardLinkFiles, header); err != nil {
return err
}

Check warning on line 224 in pkg/archive/archive.go

View check run for this annotation

Codecov / codecov/patch

pkg/archive/archive.go#L223-L224

Added lines #L223 - L224 were not covered by tests

err = writeHeader(header, uid, gid, mode, normalizeModTime, tw)
Expand All @@ -255,6 +244,32 @@ func WriteDirToTar(tw TarWriter, srcDir, basePath string, uid, gid int, mode int
})
}

func processHardLinks(file string, fi os.FileInfo, hardLinkFiles map[uint64]string, header *tar.Header) error {
var (
err error
hardlinks bool
inode uint64
)
if hardlinks, err = hasHardlinks(fi, file); err != nil {
return err
}

Check warning on line 255 in pkg/archive/archive.go

View check run for this annotation

Codecov / codecov/patch

pkg/archive/archive.go#L254-L255

Added lines #L254 - L255 were not covered by tests
if hardlinks {
inode, err = getInodeFromStat(fi.Sys(), file)
if err != nil {
return err
}

Check warning on line 260 in pkg/archive/archive.go

View check run for this annotation

Codecov / codecov/patch

pkg/archive/archive.go#L259-L260

Added lines #L259 - L260 were not covered by tests

if processedPath, ok := hardLinkFiles[inode]; ok {
header.Typeflag = tar.TypeLink
header.Linkname = processedPath
header.Size = 0
} else {
hardLinkFiles[inode] = header.Name
}
}
return nil
}

// WriteZipToTar writes the contents of a zip file to a tar writer.
func WriteZipToTar(tw TarWriter, srcZip, basePath string, uid, gid int, mode int64, normalizeModTime bool, fileFilter func(string) bool) error {
zipReader, err := zip.OpenReader(srcZip)
Expand Down
9 changes: 7 additions & 2 deletions pkg/archive/archive_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ func testArchive(t *testing.T, when spec.G, it spec.S) {

it.After(func() {
if err := os.RemoveAll(tmpDir); err != nil {
t.Fatalf("failed to clean up tmp dir %s: %s", tmpDir, err)
if runtime.GOOS != "windows" {
// skip "The process cannot access the file because it is being used by another process" on windows
t.Fatalf("failed to clean up tmp dir %s: %s", tmpDir, err)
}
}
})

Expand Down Expand Up @@ -445,8 +448,10 @@ func testArchive(t *testing.T, when spec.G, it spec.S) {

when("hard link files are present", func() {
it.Before(func() {
h.SkipIf(t, runtime.GOOS == "windows", "Skipping on windows")
src = filepath.Join("testdata", "dir-to-tar-with-hardlink")
if runtime.GOOS == "windows" {
src = filepath.Join(".", "testdata", "dir-to-tar-with-hardlink")
}
// create a hard link
err := os.Link(filepath.Join(src, "original-file"), filepath.Join(src, "original-file-2"))
h.AssertNil(t, err)
Expand Down
6 changes: 3 additions & 3 deletions pkg/archive/archive_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ import (
"syscall"
)

func hasHardlinks(fi os.FileInfo) bool {
return fi.Sys().(*syscall.Stat_t).Nlink > 1
func hasHardlinks(fi os.FileInfo, path string) (bool, error) {
return fi.Sys().(*syscall.Stat_t).Nlink > 1, nil
}

func getInodeFromStat(stat interface{}) (inode uint64, err error) {
func getInodeFromStat(stat interface{}, path string) (inode uint64, err error) {
s, ok := stat.(*syscall.Stat_t)
if ok {
inode = s.Ino
Expand Down
59 changes: 56 additions & 3 deletions pkg/archive/archive_windows.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,66 @@
//go:build windows

package archive

import (
"os"
"syscall"

"golang.org/x/sys/windows"
)

func hasHardlinks(fi os.FileInfo) bool {
return false
func hasHardlinks(fi os.FileInfo, path string) (bool, error) {
var numberOfLinks uint32
switch v := fi.Sys().(type) {
case *syscall.ByHandleFileInformation:
numberOfLinks = v.NumberOfLinks

Check warning on line 16 in pkg/archive/archive_windows.go

View check run for this annotation

Codecov / codecov/patch

pkg/archive/archive_windows.go#L15-L16

Added lines #L15 - L16 were not covered by tests
default:
// We need an instance of a ByHandleFileInformation to read NumberOfLinks
info, err := open(path)
if err != nil {
return false, err
}

Check warning on line 22 in pkg/archive/archive_windows.go

View check run for this annotation

Codecov / codecov/patch

pkg/archive/archive_windows.go#L21-L22

Added lines #L21 - L22 were not covered by tests
numberOfLinks = info.NumberOfLinks
}
return numberOfLinks > 1, nil
}

func getInodeFromStat(stat interface{}) (inode uint64, err error) {
func getInodeFromStat(stat interface{}, path string) (inode uint64, err error) {
s, ok := stat.(*syscall.ByHandleFileInformation)
if ok {
inode = (uint64(s.FileIndexHigh) << 32) | uint64(s.FileIndexLow)

Check warning on line 31 in pkg/archive/archive_windows.go

View check run for this annotation

Codecov / codecov/patch

pkg/archive/archive_windows.go#L31

Added line #L31 was not covered by tests
} else {
s, err = open(path)
if err == nil {
inode = (uint64(s.FileIndexHigh) << 32) | uint64(s.FileIndexLow)
}
}
return
}

func open(path string) (*syscall.ByHandleFileInformation, error) {
fPath, err := syscall.UTF16PtrFromString(path)
if err != nil {
return nil, err
}

Check warning on line 45 in pkg/archive/archive_windows.go

View check run for this annotation

Codecov / codecov/patch

pkg/archive/archive_windows.go#L44-L45

Added lines #L44 - L45 were not covered by tests

handle, err := syscall.CreateFile(
fPath,
windows.FILE_READ_ATTRIBUTES,
syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
nil,
syscall.OPEN_EXISTING,
syscall.FILE_FLAG_BACKUP_SEMANTICS,
0)
if err != nil {
return nil, err
}

Check warning on line 57 in pkg/archive/archive_windows.go

View check run for this annotation

Codecov / codecov/patch

pkg/archive/archive_windows.go#L56-L57

Added lines #L56 - L57 were not covered by tests
defer syscall.CloseHandle(handle)

var info syscall.ByHandleFileInformation
err = syscall.GetFileInformationByHandle(handle, &info)
if err != nil {
return nil, err
}

Check warning on line 64 in pkg/archive/archive_windows.go

View check run for this annotation

Codecov / codecov/patch

pkg/archive/archive_windows.go#L63-L64

Added lines #L63 - L64 were not covered by tests
return &info, nil
}
3 changes: 1 addition & 2 deletions pkg/buildpack/buildpack_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -517,7 +517,6 @@ version = "1.2.3"
var bpRootFolder string

it.Before(func() {
h.SkipIf(t, runtime.GOOS == "windows", "Skipping on windows")
bpRootFolder = filepath.Join("testdata", "buildpack-with-hardlink")
// create a hard link
err := os.Link(filepath.Join(bpRootFolder, "original-file"), filepath.Join(bpRootFolder, "original-file-2"))
Expand All @@ -528,7 +527,7 @@ version = "1.2.3"
os.RemoveAll(filepath.Join(bpRootFolder, "original-file-2"))
})

it("hardlink is preserved in the output tar file", func() {
it.Focus("hardlink is preserved in the output tar file", func() {
bp, err := buildpack.FromBuildpackRootBlob(
blob.NewBlob(bpRootFolder),
archive.DefaultTarWriterFactory(),
Expand Down

0 comments on commit 5f5f2af

Please sign in to comment.