diff --git a/vacation/init_test.go b/vacation/init_test.go index 03e4fb37..c0e33e4c 100644 --- a/vacation/init_test.go +++ b/vacation/init_test.go @@ -10,6 +10,7 @@ import ( func TestVacation(t *testing.T) { suite := spec.New("vacation", spec.Report(report.Terminal{})) suite("VacationArchive", testVacationArchive) + suite("VacationSymlinkSorting", testVacationSymlinkSorting) suite("VacationTar", testVacationTar) suite("VacationTarGzip", testVacationTarGzip) suite("VacationTarXZ", testVacationTarXZ) diff --git a/vacation/vacation.go b/vacation/vacation.go index 3b53010d..17cdf2b9 100644 --- a/vacation/vacation.go +++ b/vacation/vacation.go @@ -14,6 +14,7 @@ import ( "io" "os" "path/filepath" + "sort" "strings" "github.com/gabriel-vasile/mimetype" @@ -165,11 +166,15 @@ func (ta TarArchive) Decompress(destination string) error { } } + sort.Slice(symlinkHeaders, func(i, j int) bool { + return filepath.Clean(symlinkHeaders[i].name) < filepath.Clean(filepath.Join(filepath.Dir(symlinkHeaders[j].name), symlinkHeaders[j].linkname)) + }) + for _, h := range symlinkHeaders { // Check to see if the file that will be linked to is valid for symlinking _, err := filepath.EvalSymlinks(filepath.Join(filepath.Dir(h.path), h.linkname)) if err != nil { - return err + return fmt.Errorf("failed to evaluate symlink %s: %w", h.path, err) } // Check that the file being symlinked to is inside the destination @@ -395,11 +400,15 @@ func (z ZipArchive) Decompress(destination string) error { } } + sort.Slice(symlinkHeaders, func(i, j int) bool { + return filepath.Clean(symlinkHeaders[i].name) < filepath.Clean(filepath.Join(filepath.Dir(symlinkHeaders[j].name), symlinkHeaders[j].linkname)) + }) + for _, h := range symlinkHeaders { // Check to see if the file that will be linked to is valid for symlinking _, err := filepath.EvalSymlinks(filepath.Join(filepath.Dir(h.path), h.linkname)) if err != nil { - return err + return fmt.Errorf("failed to evaluate symlink %s: %w", h.path, err) } // Check that the file being symlinked to is inside the destination diff --git a/vacation/vacation_symlink_sorting_test.go b/vacation/vacation_symlink_sorting_test.go new file mode 100644 index 00000000..53e446d0 --- /dev/null +++ b/vacation/vacation_symlink_sorting_test.go @@ -0,0 +1,167 @@ +package vacation_test + +import ( + "archive/tar" + "archive/zip" + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/paketo-buildpacks/packit/vacation" + "github.com/sclevine/spec" + + . "github.com/onsi/gomega" +) + +func testVacationSymlinkSorting(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + ) + + context("TarArchive test that symlinks are sorted so that symlink to other symlinks are created after the initial symlink", func() { + var ( + tempDir string + tarArchive vacation.TarArchive + ) + + it.Before(func() { + var err error + tempDir, err = os.MkdirTemp("", "vacation") + Expect(err).NotTo(HaveOccurred()) + + buffer := bytes.NewBuffer(nil) + tw := tar.NewWriter(buffer) + + Expect(tw.WriteHeader(&tar.Header{Name: "b-symlink", Mode: 0755, Size: int64(0), Typeflag: tar.TypeSymlink, Linkname: filepath.Join("a-symlink", "x")})).To(Succeed()) + _, err = tw.Write([]byte{}) + Expect(err).NotTo(HaveOccurred()) + + Expect(tw.WriteHeader(&tar.Header{Name: "a-symlink", Mode: 0755, Size: int64(0), Typeflag: tar.TypeSymlink, Linkname: "z"})).To(Succeed()) + _, err = tw.Write([]byte{}) + Expect(err).NotTo(HaveOccurred()) + + Expect(tw.WriteHeader(&tar.Header{Name: "z", Mode: 0755, Typeflag: tar.TypeDir})).To(Succeed()) + _, err = tw.Write(nil) + Expect(err).NotTo(HaveOccurred()) + + xFile := filepath.Join("z", "x") + Expect(tw.WriteHeader(&tar.Header{Name: xFile, Mode: 0755, Size: int64(len(xFile))})).To(Succeed()) + _, err = tw.Write([]byte(xFile)) + Expect(err).NotTo(HaveOccurred()) + + Expect(tw.Close()).To(Succeed()) + + tarArchive = vacation.NewTarArchive(bytes.NewReader(buffer.Bytes())) + }) + + it.After(func() { + Expect(os.RemoveAll(tempDir)).To(Succeed()) + }) + + it("unpackages the archive into the path", func() { + var err error + err = tarArchive.Decompress(tempDir) + Expect(err).ToNot(HaveOccurred()) + + files, err := filepath.Glob(fmt.Sprintf("%s/*", tempDir)) + Expect(err).NotTo(HaveOccurred()) + Expect(files).To(ConsistOf([]string{ + filepath.Join(tempDir, "a-symlink"), + filepath.Join(tempDir, "b-symlink"), + filepath.Join(tempDir, "z"), + })) + + Expect(filepath.Join(tempDir, "z")).To(BeADirectory()) + Expect(filepath.Join(tempDir, "z", "x")).To(BeARegularFile()) + + link, err := os.Readlink(filepath.Join(tempDir, "a-symlink")) + Expect(err).NotTo(HaveOccurred()) + Expect(link).To(Equal("z")) + + data, err := os.ReadFile(filepath.Join(tempDir, "b-symlink")) + Expect(err).NotTo(HaveOccurred()) + Expect(data).To(Equal([]byte(filepath.Join("z", "x")))) + }) + }) + + context("ZipArchive test that symlinks are sorted so that symlink to other symlinks are created after the initial symlink", func() { + var ( + tempDir string + zipArchive vacation.ZipArchive + ) + + it.Before(func() { + var err error + tempDir, err = os.MkdirTemp("", "vacation") + Expect(err).NotTo(HaveOccurred()) + + buffer := bytes.NewBuffer(nil) + zw := zip.NewWriter(buffer) + + fileHeader := &zip.FileHeader{Name: "b-symlink"} + fileHeader.SetMode(0755 | os.ModeSymlink) + + bSymlink, err := zw.CreateHeader(fileHeader) + Expect(err).NotTo(HaveOccurred()) + + _, err = bSymlink.Write([]byte(filepath.Join("a-symlink", "x"))) + Expect(err).NotTo(HaveOccurred()) + + fileHeader = &zip.FileHeader{Name: "a-symlink"} + fileHeader.SetMode(0755 | os.ModeSymlink) + + aSymlink, err := zw.CreateHeader(fileHeader) + Expect(err).NotTo(HaveOccurred()) + + _, err = aSymlink.Write([]byte(`z`)) + Expect(err).NotTo(HaveOccurred()) + + _, err = zw.Create("z" + string(filepath.Separator)) + Expect(err).NotTo(HaveOccurred()) + + fileHeader = &zip.FileHeader{Name: filepath.Join("z", "x")} + fileHeader.SetMode(0644) + + xFile, err := zw.CreateHeader(fileHeader) + Expect(err).NotTo(HaveOccurred()) + + _, err = xFile.Write([]byte("x file")) + Expect(err).NotTo(HaveOccurred()) + + Expect(zw.Close()).To(Succeed()) + + zipArchive = vacation.NewZipArchive(bytes.NewReader(buffer.Bytes())) + }) + + it.After(func() { + Expect(os.RemoveAll(tempDir)).To(Succeed()) + }) + + it("unpackages the archive into the path", func() { + var err error + err = zipArchive.Decompress(tempDir) + Expect(err).ToNot(HaveOccurred()) + + files, err := filepath.Glob(fmt.Sprintf("%s/*", tempDir)) + Expect(err).NotTo(HaveOccurred()) + Expect(files).To(ConsistOf([]string{ + filepath.Join(tempDir, "a-symlink"), + filepath.Join(tempDir, "b-symlink"), + filepath.Join(tempDir, "z"), + })) + + Expect(filepath.Join(tempDir, "z")).To(BeADirectory()) + Expect(filepath.Join(tempDir, "z", "x")).To(BeARegularFile()) + + link, err := os.Readlink(filepath.Join(tempDir, "a-symlink")) + Expect(err).NotTo(HaveOccurred()) + Expect(link).To(Equal("z")) + + data, err := os.ReadFile(filepath.Join(tempDir, "b-symlink")) + Expect(err).NotTo(HaveOccurred()) + Expect(data).To(Equal([]byte(`x file`))) + }) + }) +} diff --git a/vacation/vacation_tar_test.go b/vacation/vacation_tar_test.go index 5bd9b8e5..3685ff3d 100644 --- a/vacation/vacation_tar_test.go +++ b/vacation/vacation_tar_test.go @@ -255,6 +255,7 @@ func testVacationTar(t *testing.T, context spec.G, it spec.S) { it("returns an error", func() { err := zipSlipSymlinkTar.Decompress(tempDir) + Expect(err).To(MatchError(ContainSubstring("failed to evaluate symlink"))) Expect(err).To(MatchError(ContainSubstring("no such file or directory"))) }) }) diff --git a/vacation/vacation_zip_test.go b/vacation/vacation_zip_test.go index c5c9278f..182a68e1 100644 --- a/vacation/vacation_zip_test.go +++ b/vacation/vacation_zip_test.go @@ -82,7 +82,7 @@ func testVacationZip(t *testing.T, context spec.G, it spec.S) { Expect(os.RemoveAll(tempDir)).To(Succeed()) }) - it("downloads the dependency and unpackages it into the path", func() { + it("unpackages the archive into the path", func() { var err error err = zipArchive.Decompress(tempDir) Expect(err).ToNot(HaveOccurred()) @@ -223,6 +223,7 @@ func testVacationZip(t *testing.T, context spec.G, it spec.S) { readyArchive := vacation.NewZipArchive(buffer) err := readyArchive.Decompress(tempDir) + Expect(err).To(MatchError(ContainSubstring("failed to evaluate symlink"))) Expect(err).To(MatchError(ContainSubstring("no such file or directory"))) }) })