diff --git a/api/utils/blobaccess/git/access.go b/api/utils/blobaccess/git/access.go index 92de63de3a..43be22cf0f 100644 --- a/api/utils/blobaccess/git/access.go +++ b/api/utils/blobaccess/git/access.go @@ -90,7 +90,7 @@ func BlobAccess(opt ...Option) (_ bpi.BlobAccess, rerr error) { dw := iotools.NewDigestWriterWith(digest.SHA256, tgz) finalize.Close(dw) - if err := tarutils.TgzFs(filteredRepositoryFS, dw, tarutils.TarFileSystemOptions{ZeroModTime: true}); err != nil { + if err := tarutils.TgzFs(filteredRepositoryFS, dw, tarutils.TarFileSystemOptions{ZeroModTime: true, NormalizeHeaders: true}); err != nil { return nil, fmt.Errorf("failed to create tgz: %w", err) } diff --git a/api/utils/blobaccess/git/access_test.go b/api/utils/blobaccess/git/access_test.go index a919dfcabc..a23c82945b 100644 --- a/api/utils/blobaccess/git/access_test.go +++ b/api/utils/blobaccess/git/access_test.go @@ -1,6 +1,8 @@ package git_test import ( + "archive/tar" + "compress/gzip" "embed" _ "embed" "fmt" @@ -130,6 +132,31 @@ var _ = Describe("git Blob Access", func() { Expect(data1).ToNot(BeIdenticalTo(data2)) }) + + It("creates an archive without host-specific tar headers", func() { + b := Must(gitblob.BlobAccess( + gitblob.WithURL(url), + gitblob.WithLoggingContext(ctx), + gitblob.WithCachingContext(ctx), + )) + defer Close(b) + reader := Must(b.Reader()) + defer Close(reader) + gz := Must(gzip.NewReader(reader)) + tr := tar.NewReader(gz) + for { + h, err := tr.Next() + if err == io.EOF { + break + } + Expect(err).ToNot(HaveOccurred()) + Expect(h.Uid).To(BeZero(), h.Name) + Expect(h.Gid).To(BeZero(), h.Name) + Expect(h.Uname).To(BeEmpty(), h.Name) + Expect(h.Gname).To(BeEmpty(), h.Name) + Expect(h.Mode).To(BeElementOf(int64(0o644), int64(0o755)), h.Name) + } + }) }) Context("git http repository", func() { diff --git a/api/utils/tarutils/pack.go b/api/utils/tarutils/pack.go index 96b5f4a98f..924b5d1b23 100644 --- a/api/utils/tarutils/pack.go +++ b/api/utils/tarutils/pack.go @@ -53,6 +53,11 @@ type TarFileSystemOptions struct { // TAR archives at different timestamps, the mod time needs to be set to 0. ZeroModTime bool FollowSymlinks bool + // NormalizeHeaders drops host-specific metadata (owner, group, access and change time) + // and derives the permissions from the file type and executable bit only. Together with + // ZeroModTime the archive then no longer depends on the user, umask or temporary + // directory that produced it. + NormalizeHeaders bool root string } @@ -127,6 +132,21 @@ func addFileToTar(fs vfs.FileSystem, tw *tar.Writer, path string, realPath strin if opts.ZeroModTime { header.ModTime = time.Time{} } + if opts.NormalizeHeaders { + header.Uid, header.Gid = 0, 0 + header.Uname, header.Gname = "", "" + header.AccessTime, header.ChangeTime = time.Time{}, time.Time{} + switch { + case info.IsDir(): + header.Mode = 0o755 + case header.Typeflag == tar.TypeSymlink: + header.Mode = 0o777 + case info.Mode()&0o111 != 0: + header.Mode = 0o755 + default: + header.Mode = 0o644 + } + } switch { case info.IsDir(): diff --git a/api/utils/tarutils/pack_test.go b/api/utils/tarutils/pack_test.go index 698e21aeed..185534d02f 100644 --- a/api/utils/tarutils/pack_test.go +++ b/api/utils/tarutils/pack_test.go @@ -1,9 +1,12 @@ package tarutils_test import ( + "archive/tar" "bytes" + "io" "io/fs" "os" + "path/filepath" "runtime" . "github.com/mandelsoft/goutils/testutils" @@ -82,4 +85,42 @@ var _ = Describe("tar utils mapping", func() { Expect(tarutils.TgzFs(fs, &buf3, tarutils.TarFileSystemOptions{})).To(Succeed()) Expect(buf1.Bytes()).ToNot(Equal(buf3.Bytes())) }) + + It("normalizes host-specific tar headers", func() { + if runtime.GOOS == "windows" { + Skip("file permissions and symlinks differ on windows") + } + dir := GinkgoT().TempDir() + Expect(os.WriteFile(filepath.Join(dir, "regular"), []byte("regular"), 0o600)).To(Succeed()) + Expect(os.WriteFile(filepath.Join(dir, "executable"), []byte("executable"), 0o700)).To(Succeed()) + Expect(os.Mkdir(filepath.Join(dir, "subdir"), 0o700)).To(Succeed()) + Expect(os.Symlink("regular", filepath.Join(dir, "link"))).To(Succeed()) + + var buf bytes.Buffer + Expect(tarutils.PackFsIntoTar(osfs.New(), dir, &buf, tarutils.TarFileSystemOptions{ + ZeroModTime: true, + NormalizeHeaders: true, + })).To(Succeed()) + + modes := map[string]int64{} + tr := tar.NewReader(&buf) + for { + h, err := tr.Next() + if err == io.EOF { + break + } + Expect(err).ToNot(HaveOccurred()) + Expect(h.Uid).To(BeZero(), h.Name) + Expect(h.Gid).To(BeZero(), h.Name) + Expect(h.Uname).To(BeEmpty(), h.Name) + Expect(h.Gname).To(BeEmpty(), h.Name) + modes[h.Name] = h.Mode + } + Expect(modes).To(Equal(map[string]int64{ + "regular": 0o644, + "executable": 0o755, + "subdir": 0o755, + "link": 0o777, + })) + }) })