From a60b605002ddbb28a2625d4a8bbb3507cb7f073f Mon Sep 17 00:00:00 2001 From: Rebecca Mahany-Horton Date: Wed, 10 Apr 2024 16:34:18 -0400 Subject: [PATCH] Add UntarBundleWithRequiredFilePermission --- fsutil/filesystem.go | 45 +++++++++++++++++++++++++++++++++++++++ fsutil/filesystem_test.go | 37 ++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+) diff --git a/fsutil/filesystem.go b/fsutil/filesystem.go index 4d64873..292eaca 100644 --- a/fsutil/filesystem.go +++ b/fsutil/filesystem.go @@ -134,6 +134,51 @@ func UntarBundle(destination string, source string) error { return nil } +// UntarBundleWithRequiredFilePermission performs the same operation as UntarBundle, +// but enforces `requiredFilePerm` for all files in the bundle. +func UntarBundleWithRequiredFilePermission(destination string, source string, requiredFilePerm fs.FileMode) error { + f, err := os.Open(source) + if err != nil { + return fmt.Errorf("opening source: %w", err) + } + defer f.Close() + + gzr, err := gzip.NewReader(f) + if err != nil { + return fmt.Errorf("creating gzip reader from %s: %w", source, err) + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + for { + header, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("reading tar file: %w", err) + } + + if err := sanitizeExtractPath(filepath.Dir(destination), header.Name); err != nil { + return fmt.Errorf("checking filename: %w", err) + } + + destPath := filepath.Join(filepath.Dir(destination), header.Name) + info := header.FileInfo() + if info.IsDir() { + if err = os.MkdirAll(destPath, info.Mode()); err != nil { + return fmt.Errorf("creating directory %s for tar file: %w", destPath, err) + } + continue + } + + if err := writeBundleFile(destPath, requiredFilePerm, tr); err != nil { + return fmt.Errorf("writing file: %w", err) + } + } + return nil +} + func writeBundleFile(destPath string, perm fs.FileMode, srcReader io.Reader) error { file, err := os.OpenFile(destPath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, perm) if err != nil { diff --git a/fsutil/filesystem_test.go b/fsutil/filesystem_test.go index e63e2cb..9aa0ff3 100644 --- a/fsutil/filesystem_test.go +++ b/fsutil/filesystem_test.go @@ -40,6 +40,43 @@ func TestUntarBundle(t *testing.T) { require.FileExists(t, filepath.Join(newDir, "some", "path", "to", filepath.Base(nestedFile))) } +func TestUntarBundleWithRequiredFilePermission(t *testing.T) { + t.Parallel() + + // Create tarball contents + originalDir := t.TempDir() + topLevelFile := filepath.Join(originalDir, "testfile.txt") + require.NoError(t, os.WriteFile(topLevelFile, []byte("test1"), 0655)) + internalDir := filepath.Join(originalDir, "some", "path", "to") + require.NoError(t, os.MkdirAll(internalDir, 0755)) + nestedFile := filepath.Join(internalDir, "anotherfile.txt") + require.NoError(t, os.WriteFile(nestedFile, []byte("test2"), 0744)) + + // Create test tarball + tarballDir := t.TempDir() + tarballFile := filepath.Join(tarballDir, "test.gz") + createTar(t, tarballFile, originalDir) + + // Confirm we can untar the tarball successfully + newDir := t.TempDir() + var requiredFileMode fs.FileMode = 0755 + require.NoError(t, UntarBundleWithRequiredFilePermission(filepath.Join(newDir, "anything"), tarballFile, requiredFileMode)) + + // Confirm the tarball has the contents we expect + newTopLevelFile := filepath.Join(newDir, filepath.Base(topLevelFile)) + require.FileExists(t, newTopLevelFile) + newNestedFile := filepath.Join(newDir, "some", "path", "to", filepath.Base(nestedFile)) + require.FileExists(t, newNestedFile) + + // Require that both files have the required permission 0755 + topLevelFileInfo, err := os.Stat(newTopLevelFile) + require.NoError(t, err) + require.Equal(t, requiredFileMode, topLevelFileInfo.Mode()) + nestedFileInfo, err := os.Stat(newNestedFile) + require.NoError(t, err) + require.Equal(t, requiredFileMode, nestedFileInfo.Mode()) +} + // createTar is a helper to create a test tar func createTar(t *testing.T, createLocation string, sourceDir string) { tarballFile, err := os.Create(createLocation)