diff --git a/fileutil/compressor_interface.go b/fileutil/compressor_interface.go index 3ec51cd3..573c02cd 100644 --- a/fileutil/compressor_interface.go +++ b/fileutil/compressor_interface.go @@ -15,6 +15,8 @@ type Compressor interface { DecompressFileToDir(path string, dir string, options CompressorOptions) (err error) + IsNonCompressedTarball(path string) (bool, error) + // CleanUp cleans up compressed file after it was used CleanUp(path string) error } diff --git a/fileutil/fakes/fake_compressor.go b/fileutil/fakes/fake_compressor.go index 7f5aafae..a0653417 100644 --- a/fileutil/fakes/fake_compressor.go +++ b/fileutil/fakes/fake_compressor.go @@ -26,6 +26,9 @@ type FakeCompressor struct { CleanUpTarballPath string CleanUpErr error + + IsNonCompressedResult bool + IsNonCompressedErr error } func NewFakeCompressor() *FakeCompressor { @@ -65,6 +68,10 @@ func (fc *FakeCompressor) DecompressFileToDir(tarballPath string, dir string, op return fc.DecompressFileToDirErr } +func (fc *FakeCompressor) IsNonCompressedTarball(path string) (bool, error) { + return fc.IsNonCompressedResult, fc.IsNonCompressedErr +} + func (fc *FakeCompressor) CleanUp(tarballPath string) error { fc.CleanUpTarballPath = tarballPath return fc.CleanUpErr diff --git a/fileutil/tarball_compressor.go b/fileutil/tarball_compressor.go index 037eb7d6..3df17d71 100644 --- a/fileutil/tarball_compressor.go +++ b/fileutil/tarball_compressor.go @@ -1,13 +1,24 @@ package fileutil import ( + "bytes" "fmt" + "os" "runtime" bosherr "github.com/cloudfoundry/bosh-utils/errors" boshsys "github.com/cloudfoundry/bosh-utils/system" ) +var ( + gzipMagic = []byte{0x1f, 0x8b} + bzip2Magic = []byte{0x42, 0x5a, 0x68} // "BZh" + xzMagic = []byte{0xfd, 0x37, 0x7a, 0x58, 0x5a, 0x00} + zstdMagic = []byte{0x28, 0xb5, 0x2f, 0xfd} + ustarMagic = []byte("ustar") + ustarOffset = 257 // Offset of the TAR magic string in the file +) + type tarballCompressor struct { cmdRunner boshsys.CmdRunner fs boshsys.FileSystem @@ -80,6 +91,38 @@ func (c tarballCompressor) DecompressFileToDir(tarballPath string, dir string, o return nil } +func (c tarballCompressor) IsNonCompressedTarball(path string) (bool, error) { + f, err := c.fs.OpenFile(path, os.O_RDONLY, 0644) + if err != nil { + return false, fmt.Errorf("could not open file: %w", err) + } + defer f.Close() + + // Read the first 512 bytes to check both compression headers and the TAR header. + // Ignore the error from reading a partial buffer, which is fine for short files. + buffer := make([]byte, 512) + _, _ = f.Read(buffer) + + // 1. Check for compression first. + if bytes.HasPrefix(buffer, gzipMagic) || + bytes.HasPrefix(buffer, bzip2Magic) || + bytes.HasPrefix(buffer, xzMagic) || + bytes.HasPrefix(buffer, zstdMagic) { + return false, nil + } + + // 2. If NOT compressed, check for the TAR magic string at its specific offset. + // Ensure the buffer is long enough to contain the TAR header magic string. + if len(buffer) > ustarOffset+len(ustarMagic) { + magicBytes := buffer[ustarOffset : ustarOffset+len(ustarMagic)] + if bytes.Equal(magicBytes, ustarMagic) { + return true, nil + } + } + + return false, nil +} + func (c tarballCompressor) CleanUp(tarballPath string) error { return c.fs.RemoveAll(tarballPath) } diff --git a/fileutil/tarball_compressor_test.go b/fileutil/tarball_compressor_test.go index c3a74bff..d281dfb9 100644 --- a/fileutil/tarball_compressor_test.go +++ b/fileutil/tarball_compressor_test.go @@ -273,6 +273,88 @@ var _ = Describe("tarballCompressor", func() { }) }) + Describe("IsNonCompressedTarball", func() { + It("returns true for non-compressed tarball created with NoCompression=true", func() { + tgzName, err := compressor.CompressFilesInDir(testAssetsFixtureDir, CompressorOptions{NoCompression: true}) + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(tgzName) + + result, err := compressor.IsNonCompressedTarball(tgzName) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(BeTrue()) + }) + + It("returns false for compressed tarball created with NoCompression=false", func() { + tgzName, err := compressor.CompressFilesInDir(testAssetsFixtureDir, CompressorOptions{NoCompression: false}) + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(tgzName) + + result, err := compressor.IsNonCompressedTarball(tgzName) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(BeFalse()) + }) + + It("returns false for compressed tarball created with default options", func() { + tgzName, err := compressor.CompressFilesInDir(testAssetsFixtureDir, CompressorOptions{}) + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(tgzName) + + result, err := compressor.IsNonCompressedTarball(tgzName) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(BeFalse()) + }) + + It("returns error for non-existent file", func() { + result, err := compressor.IsNonCompressedTarball("/nonexistent/file.tar") + Expect(err).To(HaveOccurred()) + Expect(result).To(BeFalse()) + }) + + It("returns error for non-tarball file", func() { + tempFile, err := fs.TempFile("test-non-tarball") + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(tempFile.Name()) + + err = fs.WriteFileString(tempFile.Name(), "This is not a tar file") + Expect(err).ToNot(HaveOccurred()) + + result, err := compressor.IsNonCompressedTarball(tempFile.Name()) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(BeFalse()) + }) + + It("returns error for empty file", func() { + tempFile, err := fs.TempFile("test-empty-file") + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(tempFile.Name()) + tempFile.Close() + + result, err := compressor.IsNonCompressedTarball(tempFile.Name()) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(BeFalse()) + }) + + It("correctly identifies tarballs created with CompressSpecificFilesInDir", func() { + files := []string{"app.stdout.log", "app.stderr.log"} + + tgzName, err := compressor.CompressSpecificFilesInDir(testAssetsFixtureDir, files, CompressorOptions{NoCompression: true}) + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(tgzName) + + result, err := compressor.IsNonCompressedTarball(tgzName) + Expect(err).ToNot(HaveOccurred()) + Expect(result).To(BeTrue()) + + tgzName2, err := compressor.CompressSpecificFilesInDir(testAssetsFixtureDir, files, CompressorOptions{NoCompression: false}) + Expect(err).ToNot(HaveOccurred()) + defer os.Remove(tgzName2) + + result2, err := compressor.IsNonCompressedTarball(tgzName2) + Expect(err).ToNot(HaveOccurred()) + Expect(result2).To(BeFalse()) + }) + }) + Describe("CleanUp", func() { It("removes tarball path", func() { fs := fakesys.NewFakeFileSystem()