diff --git a/internal/engine/engine.go b/internal/engine/engine.go index 546302cb..f0aa2a7a 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -412,6 +412,13 @@ func untar(ctx context.Context, dst string, r io.Reader) error { // if it's a file create it case tar.TypeReg: + // If the file's parent dir doesn't exist, create it. + dirname := filepath.Dir(target) + if _, err := os.Stat(dirname); err != nil { + if err := os.MkdirAll(dirname, 0o755); err != nil { + return err + } + } f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) if err != nil { return err @@ -428,16 +435,44 @@ func untar(ctx context.Context, dst string, r io.Reader) error { f.Close() // if it's a link create it - case tar.TypeSymlink: - err := os.Symlink(header.Linkname, filepath.Join(dst, header.Name)) + case tar.TypeSymlink, tar.TypeLink: + ln := os.Symlink + if header.Typeflag == tar.TypeLink { + ln = os.Link + } + + oldNoBase, newNoBase := resolveLinkPaths(header.Linkname, header.Name) + old := filepath.Join(dst, oldNoBase) + new := filepath.Join(dst, newNoBase) + // Create the new link's directory if it doesn't exist. + dirname := filepath.Dir(new) + if _, err := os.Stat(dirname); err != nil { + if err := os.MkdirAll(dirname, 0o755); err != nil { + return err + } + } + err := ln(old, new) if err != nil { - logger.V(log.DBG).Info(fmt.Sprintf("Error creating link: %s. Ignoring.", header.Name)) + logger.V(log.DBG).Info(fmt.Sprintf("Error creating link: %s. Ignoring.", header.Name), "link", new, "linkedTo", old) continue } } } } +// resolveLinkPaths resolves relative pathing in linkname relative to filename. +// Relative link names are resolved such that the linkname is no longer relative +// to filename. E.g etc/c/foo -> ../../usr/lib/foo would become etc/c/foo and usr/lib/foo, +// making both paths relative to their shared base. +func resolveLinkPaths(oldname, newname string) (string, string) { + if !strings.HasPrefix(oldname, "..") { + return oldname, newname + } + + linkDir := filepath.Dir(newname) + return filepath.Join(linkDir, oldname), newname +} + // writeCertImage takes imageRef and writes it to disk as JSON representing a pyxis.CertImage // struct. The file is written at path certification.DefaultCertImageFilename. // diff --git a/internal/engine/engine_test.go b/internal/engine/engine_test.go index 5f2ea471..5cca3a3e 100644 --- a/internal/engine/engine_test.go +++ b/internal/engine/engine_test.go @@ -379,6 +379,20 @@ var _ = Describe("Check Name Queries", func() { }) }) +var _ = Describe("Link Path Resolution", func() { + DescribeTable( + "Link targets should resolve correctly", + func(old, new, expectedOld, expectedNew string) { + resO, resN := resolveLinkPaths(old, new) + Expect(resO).To(Equal(expectedOld)) + Expect(resN).To(Equal(expectedNew)) + }, + Entry("Up one level", "../var/lib/file", "etc/file", "var/lib/file", "etc/file"), + Entry("Up two levels", "../../var/lib/file", "etc/foo/file", "var/lib/file", "etc/foo/file"), + Entry("Up no levels", "var/lib/file", "etc/file", "var/lib/file", "etc/file"), + ) +}) + // writeTarball writes a tar archive to out with filename containing contents at the base path // with extra bytes written at the end of length extraBytes. // note: this should only be used as a helper function in tests