aboutsummaryrefslogtreecommitdiffstatshomepage
path: root/cli/internal/cacheitem/restore_test.go
diff options
context:
space:
mode:
Diffstat (limited to 'cli/internal/cacheitem/restore_test.go')
-rw-r--r--cli/internal/cacheitem/restore_test.go1493
1 files changed, 1493 insertions, 0 deletions
diff --git a/cli/internal/cacheitem/restore_test.go b/cli/internal/cacheitem/restore_test.go
new file mode 100644
index 0000000..a0a33d6
--- /dev/null
+++ b/cli/internal/cacheitem/restore_test.go
@@ -0,0 +1,1493 @@
+package cacheitem
+
+import (
+ "archive/tar"
+ "errors"
+ "fmt"
+ "io"
+ "io/fs"
+ "os"
+ "path/filepath"
+ "reflect"
+ "runtime"
+ "syscall"
+ "testing"
+
+ "github.com/DataDog/zstd"
+ "github.com/vercel/turbo/cli/internal/turbopath"
+ "gotest.tools/v3/assert"
+)
+
+type tarFile struct {
+ Body string
+ *tar.Header
+}
+
+type restoreFile struct {
+ Name turbopath.AnchoredUnixPath
+ Linkname string
+ fs.FileMode
+}
+
+// generateTar is used specifically to generate tar files that Turborepo would
+// rarely or never encounter without malicious or pathological inputs. We use it
+// to make sure that we respond well in these scenarios during restore attempts.
+func generateTar(t *testing.T, files []tarFile) turbopath.AbsoluteSystemPath {
+ t.Helper()
+ testDir := turbopath.AbsoluteSystemPath(t.TempDir())
+ testArchivePath := testDir.UntypedJoin("out.tar")
+
+ handle, handleCreateErr := testArchivePath.Create()
+ assert.NilError(t, handleCreateErr, "os.Create")
+
+ tw := tar.NewWriter(handle)
+
+ for _, file := range files {
+ if file.Header.Typeflag == tar.TypeReg {
+ file.Header.Size = int64(len(file.Body))
+ }
+
+ writeHeaderErr := tw.WriteHeader(file.Header)
+ assert.NilError(t, writeHeaderErr, "tw.WriteHeader")
+
+ _, writeErr := tw.Write([]byte(file.Body))
+ assert.NilError(t, writeErr, "tw.Write")
+ }
+
+ twCloseErr := tw.Close()
+ assert.NilError(t, twCloseErr, "tw.Close")
+
+ handleCloseErr := handle.Close()
+ assert.NilError(t, handleCloseErr, "handle.Close")
+
+ return testArchivePath
+}
+
+// compressTar splits the compression of a tar file so that we don't
+// accidentally diverge in tar creation while still being able to test
+// restoration from tar and from .tar.zst.
+func compressTar(t *testing.T, archivePath turbopath.AbsoluteSystemPath) turbopath.AbsoluteSystemPath {
+ t.Helper()
+
+ inputHandle, inputHandleOpenErr := archivePath.Open()
+ assert.NilError(t, inputHandleOpenErr, "os.Open")
+
+ outputPath := archivePath + ".zst"
+ outputHandle, outputHandleCreateErr := outputPath.Create()
+ assert.NilError(t, outputHandleCreateErr, "os.Create")
+
+ zw := zstd.NewWriter(outputHandle)
+ _, copyError := io.Copy(zw, inputHandle)
+ assert.NilError(t, copyError, "io.Copy")
+
+ zwCloseErr := zw.Close()
+ assert.NilError(t, zwCloseErr, "zw.Close")
+
+ inputHandleCloseErr := inputHandle.Close()
+ assert.NilError(t, inputHandleCloseErr, "inputHandle.Close")
+
+ outputHandleCloseErr := outputHandle.Close()
+ assert.NilError(t, outputHandleCloseErr, "outputHandle.Close")
+
+ return outputPath
+}
+
+func generateAnchor(t *testing.T) turbopath.AbsoluteSystemPath {
+ t.Helper()
+ testDir := turbopath.AbsoluteSystemPath(t.TempDir())
+ anchorPoint := testDir.UntypedJoin("anchor")
+
+ mkdirErr := anchorPoint.Mkdir(0777)
+ assert.NilError(t, mkdirErr, "Mkdir")
+
+ return anchorPoint
+}
+
+func assertFileExists(t *testing.T, anchor turbopath.AbsoluteSystemPath, diskFile restoreFile) {
+ t.Helper()
+ // If we have gotten here we can assume this to be true.
+ processedName := diskFile.Name.ToSystemPath()
+ fullName := processedName.RestoreAnchor(anchor)
+ fileInfo, err := fullName.Lstat()
+ assert.NilError(t, err, "Lstat")
+
+ assert.Equal(t, fileInfo.Mode()&fs.ModePerm, diskFile.FileMode&fs.ModePerm, "File has the expected permissions: "+processedName)
+ assert.Equal(t, fileInfo.Mode()|fs.ModePerm, diskFile.FileMode|fs.ModePerm, "File has the expected mode.")
+
+ if diskFile.FileMode&os.ModeSymlink != 0 {
+ linkname, err := fullName.Readlink()
+ assert.NilError(t, err, "Readlink")
+
+ // We restore Linkname verbatim.
+ assert.Equal(t, linkname, diskFile.Linkname, "Link target matches.")
+ }
+}
+
+func TestOpen(t *testing.T) {
+ type wantErr struct {
+ unix error
+ windows error
+ }
+ type wantOutput struct {
+ unix []turbopath.AnchoredSystemPath
+ windows []turbopath.AnchoredSystemPath
+ }
+ type wantFiles struct {
+ unix []restoreFile
+ windows []restoreFile
+ }
+ tests := []struct {
+ name string
+ tarFiles []tarFile
+ wantOutput wantOutput
+ wantFiles wantFiles
+ wantErr wantErr
+ }{
+ {
+ name: "cache optimized",
+ tarFiles: []tarFile{
+ {
+ Header: &tar.Header{
+ Name: "one/",
+ Typeflag: tar.TypeDir,
+ Mode: 0755,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one/two/",
+ Typeflag: tar.TypeDir,
+ Mode: 0755,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one/two/three/",
+ Typeflag: tar.TypeDir,
+ Mode: 0755,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one/two/three/file-one",
+ Typeflag: tar.TypeReg,
+ Mode: 0644,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one/two/three/file-two",
+ Typeflag: tar.TypeReg,
+ Mode: 0644,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one/two/a/",
+ Typeflag: tar.TypeDir,
+ Mode: 0755,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one/two/a/file",
+ Typeflag: tar.TypeReg,
+ Mode: 0644,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one/two/b/",
+ Typeflag: tar.TypeDir,
+ Mode: 0755,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one/two/b/file",
+ Typeflag: tar.TypeReg,
+ Mode: 0644,
+ },
+ },
+ },
+ wantFiles: wantFiles{
+ unix: []restoreFile{
+ {
+ Name: "one",
+ FileMode: 0 | os.ModeDir | 0755,
+ },
+ {
+ Name: "one/two",
+ FileMode: 0 | os.ModeDir | 0755,
+ },
+ {
+ Name: "one/two/three",
+ FileMode: 0 | os.ModeDir | 0755,
+ },
+ {
+ Name: "one/two/three/file-one",
+ FileMode: 0644,
+ },
+ {
+ Name: "one/two/three/file-two",
+ FileMode: 0644,
+ },
+ {
+ Name: "one/two/a",
+ FileMode: 0 | os.ModeDir | 0755,
+ },
+ {
+ Name: "one/two/a/file",
+ FileMode: 0644,
+ },
+ {
+ Name: "one/two/b",
+ FileMode: 0 | os.ModeDir | 0755,
+ },
+ {
+ Name: "one/two/b/file",
+ FileMode: 0644,
+ },
+ },
+ windows: []restoreFile{
+ {
+ Name: "one",
+ FileMode: 0 | os.ModeDir | 0777,
+ },
+ {
+ Name: "one/two",
+ FileMode: 0 | os.ModeDir | 0777,
+ },
+ {
+ Name: "one/two/three",
+ FileMode: 0 | os.ModeDir | 0777,
+ },
+ {
+ Name: "one/two/three/file-one",
+ FileMode: 0666,
+ },
+ {
+ Name: "one/two/three/file-two",
+ FileMode: 0666,
+ },
+ {
+ Name: "one/two/a",
+ FileMode: 0 | os.ModeDir | 0777,
+ },
+ {
+ Name: "one/two/a/file",
+ FileMode: 0666,
+ },
+ {
+ Name: "one/two/b",
+ FileMode: 0 | os.ModeDir | 0777,
+ },
+ {
+ Name: "one/two/b/file",
+ FileMode: 0666,
+ },
+ },
+ },
+ wantOutput: wantOutput{
+ unix: turbopath.AnchoredUnixPathArray{
+ "one",
+ "one/two",
+ "one/two/three",
+ "one/two/three/file-one",
+ "one/two/three/file-two",
+ "one/two/a",
+ "one/two/a/file",
+ "one/two/b",
+ "one/two/b/file",
+ }.ToSystemPathArray(),
+ },
+ },
+ {
+ name: "pathological cache works",
+ tarFiles: []tarFile{
+ {
+ Header: &tar.Header{
+ Name: "one/",
+ Typeflag: tar.TypeDir,
+ Mode: 0755,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one/two/",
+ Typeflag: tar.TypeDir,
+ Mode: 0755,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one/two/a/",
+ Typeflag: tar.TypeDir,
+ Mode: 0755,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one/two/b/",
+ Typeflag: tar.TypeDir,
+ Mode: 0755,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one/two/three/",
+ Typeflag: tar.TypeDir,
+ Mode: 0755,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one/two/a/file",
+ Typeflag: tar.TypeReg,
+ Mode: 0644,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one/two/b/file",
+ Typeflag: tar.TypeReg,
+ Mode: 0644,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one/two/three/file-one",
+ Typeflag: tar.TypeReg,
+ Mode: 0644,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one/two/three/file-two",
+ Typeflag: tar.TypeReg,
+ Mode: 0644,
+ },
+ },
+ },
+ wantFiles: wantFiles{
+ unix: []restoreFile{
+ {
+ Name: "one",
+ FileMode: 0 | os.ModeDir | 0755,
+ },
+ {
+ Name: "one/two",
+ FileMode: 0 | os.ModeDir | 0755,
+ },
+ {
+ Name: "one/two/three",
+ FileMode: 0 | os.ModeDir | 0755,
+ },
+ {
+ Name: "one/two/three/file-one",
+ FileMode: 0644,
+ },
+ {
+ Name: "one/two/three/file-two",
+ FileMode: 0644,
+ },
+ {
+ Name: "one/two/a",
+ FileMode: 0 | os.ModeDir | 0755,
+ },
+ {
+ Name: "one/two/a/file",
+ FileMode: 0644,
+ },
+ {
+ Name: "one/two/b",
+ FileMode: 0 | os.ModeDir | 0755,
+ },
+ {
+ Name: "one/two/b/file",
+ FileMode: 0644,
+ },
+ },
+ windows: []restoreFile{
+ {
+ Name: "one",
+ FileMode: 0 | os.ModeDir | 0777,
+ },
+ {
+ Name: "one/two",
+ FileMode: 0 | os.ModeDir | 0777,
+ },
+ {
+ Name: "one/two/three",
+ FileMode: 0 | os.ModeDir | 0777,
+ },
+ {
+ Name: "one/two/three/file-one",
+ FileMode: 0666,
+ },
+ {
+ Name: "one/two/three/file-two",
+ FileMode: 0666,
+ },
+ {
+ Name: "one/two/a",
+ FileMode: 0 | os.ModeDir | 0777,
+ },
+ {
+ Name: "one/two/a/file",
+ FileMode: 0666,
+ },
+ {
+ Name: "one/two/b",
+ FileMode: 0 | os.ModeDir | 0777,
+ },
+ {
+ Name: "one/two/b/file",
+ FileMode: 0666,
+ },
+ },
+ },
+ wantOutput: wantOutput{
+ unix: turbopath.AnchoredUnixPathArray{
+ "one",
+ "one/two",
+ "one/two/a",
+ "one/two/b",
+ "one/two/three",
+ "one/two/a/file",
+ "one/two/b/file",
+ "one/two/three/file-one",
+ "one/two/three/file-two",
+ }.ToSystemPathArray(),
+ },
+ },
+ {
+ name: "hello world",
+ tarFiles: []tarFile{
+ {
+ Header: &tar.Header{
+ Name: "target",
+ Typeflag: tar.TypeReg,
+ Mode: 0644,
+ },
+ Body: "target",
+ },
+ {
+ Header: &tar.Header{
+ Name: "source",
+ Linkname: "target",
+ Typeflag: tar.TypeSymlink,
+ Mode: 0777,
+ },
+ },
+ },
+ wantFiles: wantFiles{
+ unix: []restoreFile{
+ {
+ Name: "source",
+ Linkname: "target",
+ FileMode: 0 | os.ModeSymlink | 0777,
+ },
+ {
+ Name: "target",
+ FileMode: 0644,
+ },
+ },
+ windows: []restoreFile{
+ {
+ Name: "source",
+ Linkname: "target",
+ FileMode: 0 | os.ModeSymlink | 0666,
+ },
+ {
+ Name: "target",
+ FileMode: 0666,
+ },
+ },
+ },
+ wantOutput: wantOutput{
+ unix: turbopath.AnchoredUnixPathArray{"target", "source"}.ToSystemPathArray(),
+ },
+ },
+ {
+ name: "nested file",
+ tarFiles: []tarFile{
+ {
+ Header: &tar.Header{
+ Name: "folder/",
+ Typeflag: tar.TypeDir,
+ Mode: 0755,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "folder/file",
+ Typeflag: tar.TypeReg,
+ Mode: 0644,
+ },
+ Body: "file",
+ },
+ },
+ wantFiles: wantFiles{
+ unix: []restoreFile{
+ {
+ Name: "folder",
+ FileMode: 0 | os.ModeDir | 0755,
+ },
+ {
+ Name: "folder/file",
+ FileMode: 0644,
+ },
+ },
+ windows: []restoreFile{
+ {
+ Name: "folder",
+ FileMode: 0 | os.ModeDir | 0777,
+ },
+ {
+ Name: "folder/file",
+ FileMode: 0666,
+ },
+ },
+ },
+ wantOutput: wantOutput{
+ unix: turbopath.AnchoredUnixPathArray{"folder", "folder/file"}.ToSystemPathArray(),
+ },
+ },
+ {
+ name: "nested symlink",
+ tarFiles: []tarFile{
+ {
+ Header: &tar.Header{
+ Name: "folder/",
+ Typeflag: tar.TypeDir,
+ Mode: 0755,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "folder/symlink",
+ Linkname: "../",
+ Typeflag: tar.TypeSymlink,
+ Mode: 0777,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "folder/symlink/folder-sibling",
+ Typeflag: tar.TypeReg,
+ Mode: 0644,
+ },
+ Body: "folder-sibling",
+ },
+ },
+ wantFiles: wantFiles{
+ unix: []restoreFile{
+ {
+ Name: "folder",
+ FileMode: 0 | os.ModeDir | 0755,
+ },
+ {
+ Name: "folder/symlink",
+ FileMode: 0 | os.ModeSymlink | 0777,
+ Linkname: "../",
+ },
+ {
+ Name: "folder/symlink/folder-sibling",
+ FileMode: 0644,
+ },
+ {
+ Name: "folder-sibling",
+ FileMode: 0644,
+ },
+ },
+ windows: []restoreFile{
+ {
+ Name: "folder",
+ FileMode: 0 | os.ModeDir | 0777,
+ },
+ {
+ Name: "folder/symlink",
+ FileMode: 0 | os.ModeSymlink | 0666,
+ Linkname: "..\\",
+ },
+ {
+ Name: "folder/symlink/folder-sibling",
+ FileMode: 0666,
+ },
+ {
+ Name: "folder-sibling",
+ FileMode: 0666,
+ },
+ },
+ },
+ wantOutput: wantOutput{
+ unix: turbopath.AnchoredUnixPathArray{"folder", "folder/symlink", "folder/symlink/folder-sibling"}.ToSystemPathArray(),
+ },
+ },
+ {
+ name: "pathological symlinks",
+ tarFiles: []tarFile{
+ {
+ Header: &tar.Header{
+ Name: "one",
+ Linkname: "two",
+ Typeflag: tar.TypeSymlink,
+ Mode: 0777,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "two",
+ Linkname: "three",
+ Typeflag: tar.TypeSymlink,
+ Mode: 0777,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "three",
+ Linkname: "real",
+ Typeflag: tar.TypeSymlink,
+ Mode: 0777,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "real",
+ Typeflag: tar.TypeReg,
+ Mode: 0755,
+ },
+ Body: "real",
+ },
+ },
+ wantFiles: wantFiles{
+ unix: []restoreFile{
+ {
+ Name: "one",
+ Linkname: "two",
+ FileMode: 0 | os.ModeSymlink | 0777,
+ },
+ {
+ Name: "two",
+ Linkname: "three",
+ FileMode: 0 | os.ModeSymlink | 0777,
+ },
+ {
+ Name: "three",
+ Linkname: "real",
+ FileMode: 0 | os.ModeSymlink | 0777,
+ },
+ {
+ Name: "real",
+ FileMode: 0 | 0755,
+ },
+ },
+ windows: []restoreFile{
+ {
+ Name: "one",
+ Linkname: "two",
+ FileMode: 0 | os.ModeSymlink | 0666,
+ },
+ {
+ Name: "two",
+ Linkname: "three",
+ FileMode: 0 | os.ModeSymlink | 0666,
+ },
+ {
+ Name: "three",
+ Linkname: "real",
+ FileMode: 0 | os.ModeSymlink | 0666,
+ },
+ {
+ Name: "real",
+ FileMode: 0 | 0666,
+ },
+ },
+ },
+ wantOutput: wantOutput{
+ unix: turbopath.AnchoredUnixPathArray{"real", "three", "two", "one"}.ToSystemPathArray(),
+ },
+ },
+ {
+ name: "place file at dir location",
+ tarFiles: []tarFile{
+ {
+ Header: &tar.Header{
+ Name: "folder-not-file/",
+ Typeflag: tar.TypeDir,
+ Mode: 0755,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "folder-not-file/subfile",
+ Typeflag: tar.TypeReg,
+ Mode: 0755,
+ },
+ Body: "subfile",
+ },
+ {
+ Header: &tar.Header{
+ Name: "folder-not-file",
+ Typeflag: tar.TypeReg,
+ Mode: 0755,
+ },
+ Body: "this shouldn't work",
+ },
+ },
+ wantFiles: wantFiles{
+ unix: []restoreFile{
+ {
+ Name: "folder-not-file",
+ FileMode: 0 | os.ModeDir | 0755,
+ },
+ {
+ Name: "folder-not-file/subfile",
+ FileMode: 0755,
+ },
+ },
+ windows: []restoreFile{
+ {
+ Name: "folder-not-file",
+ FileMode: 0 | os.ModeDir | 0777,
+ },
+ {
+ Name: "folder-not-file/subfile",
+ FileMode: 0666,
+ },
+ },
+ },
+ wantOutput: wantOutput{
+ unix: turbopath.AnchoredUnixPathArray{"folder-not-file", "folder-not-file/subfile"}.ToSystemPathArray(),
+ },
+ wantErr: wantErr{
+ unix: syscall.EISDIR,
+ windows: syscall.EISDIR,
+ },
+ },
+ // {
+ // name: "missing symlink with file at subdir",
+ // tarFiles: []tarFile{
+ // {
+ // Header: &tar.Header{
+ // Name: "one",
+ // Linkname: "two",
+ // Typeflag: tar.TypeSymlink,
+ // Mode: 0777,
+ // },
+ // },
+ // {
+ // Header: &tar.Header{
+ // Name: "one/file",
+ // Typeflag: tar.TypeReg,
+ // Mode: 0755,
+ // },
+ // Body: "file",
+ // },
+ // },
+ // wantFiles: wantFiles{
+ // unix: []restoreFile{
+ // {
+ // Name: "one",
+ // Linkname: "two",
+ // FileMode: 0 | os.ModeSymlink | 0777,
+ // },
+ // },
+ // },
+ // wantOutput: wantOutput{
+ // unix: turbopath.AnchoredUnixPathArray{"one"}.ToSystemPathArray(),
+ // windows: nil,
+ // },
+ // wantErr: wantErr{
+ // unix: os.ErrExist,
+ // windows: os.ErrExist,
+ // },
+ // },
+ {
+ name: "symlink cycle",
+ tarFiles: []tarFile{
+ {
+ Header: &tar.Header{
+ Name: "one",
+ Linkname: "two",
+ Typeflag: tar.TypeSymlink,
+ Mode: 0777,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "two",
+ Linkname: "three",
+ Typeflag: tar.TypeSymlink,
+ Mode: 0777,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "three",
+ Linkname: "one",
+ Typeflag: tar.TypeSymlink,
+ Mode: 0777,
+ },
+ },
+ },
+ wantFiles: wantFiles{
+ unix: []restoreFile{},
+ },
+ wantOutput: wantOutput{
+ unix: []turbopath.AnchoredSystemPath{},
+ },
+ wantErr: wantErr{
+ unix: errCycleDetected,
+ windows: errCycleDetected,
+ },
+ },
+ {
+ name: "symlink clobber",
+ tarFiles: []tarFile{
+ {
+ Header: &tar.Header{
+ Name: "one",
+ Linkname: "two",
+ Typeflag: tar.TypeSymlink,
+ Mode: 0777,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one",
+ Linkname: "three",
+ Typeflag: tar.TypeSymlink,
+ Mode: 0777,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one",
+ Linkname: "real",
+ Typeflag: tar.TypeSymlink,
+ Mode: 0777,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "real",
+ Typeflag: tar.TypeReg,
+ Mode: 0755,
+ },
+ Body: "real",
+ },
+ },
+ wantFiles: wantFiles{
+ unix: []restoreFile{
+ {
+ Name: "one",
+ Linkname: "real",
+ FileMode: 0 | os.ModeSymlink | 0777,
+ },
+ {
+ Name: "real",
+ FileMode: 0755,
+ },
+ },
+ windows: []restoreFile{
+ {
+ Name: "one",
+ Linkname: "real",
+ FileMode: 0 | os.ModeSymlink | 0666,
+ },
+ {
+ Name: "real",
+ FileMode: 0666,
+ },
+ },
+ },
+ wantOutput: wantOutput{
+ unix: turbopath.AnchoredUnixPathArray{"real", "one"}.ToSystemPathArray(),
+ },
+ },
+ {
+ name: "symlink traversal",
+ tarFiles: []tarFile{
+ {
+ Header: &tar.Header{
+ Name: "escape",
+ Linkname: "../",
+ Typeflag: tar.TypeSymlink,
+ Mode: 0777,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "escape/file",
+ Typeflag: tar.TypeReg,
+ Mode: 0644,
+ },
+ Body: "file",
+ },
+ },
+ wantFiles: wantFiles{
+ unix: []restoreFile{
+ {
+ Name: "escape",
+ Linkname: "../",
+ FileMode: 0 | os.ModeSymlink | 0777,
+ },
+ },
+ windows: []restoreFile{
+ {
+ Name: "escape",
+ Linkname: "..\\",
+ FileMode: 0 | os.ModeSymlink | 0666,
+ },
+ },
+ },
+ wantOutput: wantOutput{
+ unix: turbopath.AnchoredUnixPathArray{"escape"}.ToSystemPathArray(),
+ },
+ wantErr: wantErr{
+ unix: errTraversal,
+ windows: errTraversal,
+ },
+ },
+ {
+ name: "Double indirection: file",
+ tarFiles: []tarFile{
+ {
+ Header: &tar.Header{
+ Name: "up",
+ Linkname: "../",
+ Typeflag: tar.TypeSymlink,
+ Mode: 0777,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "link",
+ Linkname: "up",
+ Typeflag: tar.TypeSymlink,
+ Mode: 0777,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "link/outside-file",
+ Typeflag: tar.TypeReg,
+ Mode: 0755,
+ },
+ },
+ },
+ wantErr: wantErr{unix: errTraversal, windows: errTraversal},
+ wantOutput: wantOutput{
+ unix: turbopath.AnchoredUnixPathArray{
+ "up",
+ "link",
+ }.ToSystemPathArray(),
+ },
+ },
+ {
+ name: "Double indirection: folder",
+ tarFiles: []tarFile{
+ {
+ Header: &tar.Header{
+ Name: "up",
+ Linkname: "../",
+ Typeflag: tar.TypeSymlink,
+ Mode: 0777,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "link",
+ Linkname: "up",
+ Typeflag: tar.TypeSymlink,
+ Mode: 0777,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "link/level-one/level-two/",
+ Typeflag: tar.TypeDir,
+ Mode: 0755,
+ },
+ },
+ },
+ wantErr: wantErr{unix: errTraversal, windows: errTraversal},
+ wantOutput: wantOutput{
+ unix: turbopath.AnchoredUnixPathArray{
+ "up",
+ "link",
+ }.ToSystemPathArray(),
+ },
+ },
+ {
+ name: "name traversal",
+ tarFiles: []tarFile{
+ {
+ Header: &tar.Header{
+ Name: "../escape",
+ Typeflag: tar.TypeReg,
+ Mode: 0644,
+ },
+ Body: "file",
+ },
+ },
+ wantFiles: wantFiles{
+ unix: []restoreFile{},
+ },
+ wantOutput: wantOutput{
+ unix: []turbopath.AnchoredSystemPath{},
+ },
+ wantErr: wantErr{
+ unix: errNameMalformed,
+ windows: errNameMalformed,
+ },
+ },
+ {
+ name: "windows unsafe",
+ tarFiles: []tarFile{
+ {
+ Header: &tar.Header{
+ Name: "back\\slash\\file",
+ Typeflag: tar.TypeReg,
+ Mode: 0644,
+ },
+ Body: "file",
+ },
+ },
+ wantFiles: wantFiles{
+ unix: []restoreFile{
+ {
+ Name: "back\\slash\\file",
+ FileMode: 0644,
+ },
+ },
+ windows: []restoreFile{},
+ },
+ wantOutput: wantOutput{
+ unix: turbopath.AnchoredUnixPathArray{"back\\slash\\file"}.ToSystemPathArray(),
+ windows: turbopath.AnchoredUnixPathArray{}.ToSystemPathArray(),
+ },
+ wantErr: wantErr{
+ unix: nil,
+ windows: errNameWindowsUnsafe,
+ },
+ },
+ {
+ name: "fifo (and others) unsupported",
+ tarFiles: []tarFile{
+ {
+ Header: &tar.Header{
+ Name: "fifo",
+ Typeflag: tar.TypeFifo,
+ },
+ },
+ },
+ wantFiles: wantFiles{
+ unix: []restoreFile{},
+ },
+ wantOutput: wantOutput{
+ unix: []turbopath.AnchoredSystemPath{},
+ },
+ wantErr: wantErr{
+ unix: errUnsupportedFileType,
+ windows: errUnsupportedFileType,
+ },
+ },
+ }
+ for _, tt := range tests {
+ getTestFunc := func(compressed bool) func(t *testing.T) {
+ return func(t *testing.T) {
+ var archivePath turbopath.AbsoluteSystemPath
+ if compressed {
+ archivePath = compressTar(t, generateTar(t, tt.tarFiles))
+ } else {
+ archivePath = generateTar(t, tt.tarFiles)
+ }
+ anchor := generateAnchor(t)
+
+ cacheItem, err := Open(archivePath)
+ assert.NilError(t, err, "Open")
+
+ restoreOutput, restoreErr := cacheItem.Restore(anchor)
+ var desiredErr error
+ if runtime.GOOS == "windows" {
+ desiredErr = tt.wantErr.windows
+ } else {
+ desiredErr = tt.wantErr.unix
+ }
+ if desiredErr != nil {
+ if !errors.Is(restoreErr, desiredErr) {
+ t.Errorf("wanted err: %v, got err: %v", tt.wantErr, restoreErr)
+ }
+ } else {
+ assert.NilError(t, restoreErr, "Restore")
+ }
+
+ outputComparison := tt.wantOutput.unix
+ if runtime.GOOS == "windows" && tt.wantOutput.windows != nil {
+ outputComparison = tt.wantOutput.windows
+ }
+
+ if !reflect.DeepEqual(restoreOutput, outputComparison) {
+ t.Errorf("Restore() = %v, want %v", restoreOutput, outputComparison)
+ }
+
+ // Check files on disk.
+ filesComparison := tt.wantFiles.unix
+ if runtime.GOOS == "windows" && tt.wantFiles.windows != nil {
+ filesComparison = tt.wantFiles.windows
+ }
+ for _, diskFile := range filesComparison {
+ assertFileExists(t, anchor, diskFile)
+ }
+
+ assert.NilError(t, cacheItem.Close(), "Close")
+ }
+ }
+ t.Run(tt.name+"zst", getTestFunc(true))
+ t.Run(tt.name, getTestFunc(false))
+ }
+}
+
+func Test_checkName(t *testing.T) {
+ tests := []struct {
+ path string
+ wellFormed bool
+ windowsSafe bool
+ }{
+ // Empty
+ {
+ path: "",
+ wellFormed: false,
+ windowsSafe: false,
+ },
+ // Bad prefix
+ {
+ path: ".",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ {
+ path: "..",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ {
+ path: "/",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ {
+ path: "./",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ {
+ path: "../",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ // Bad prefix, suffixed
+ {
+ path: "/a",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ {
+ path: "./a",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ {
+ path: "../a",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ // Bad Suffix
+ {
+ path: "/.",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ {
+ path: "/..",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ // Bad Suffix, with prefix
+ {
+ path: "a/.",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ {
+ path: "a/..",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ // Bad middle
+ {
+ path: "//",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ {
+ path: "/./",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ {
+ path: "/../",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ // Bad middle, prefixed
+ {
+ path: "a//",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ {
+ path: "a/./",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ {
+ path: "a/../",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ // Bad middle, suffixed
+ {
+ path: "//a",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ {
+ path: "/./a",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ {
+ path: "/../a",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ // Bad middle, wrapped
+ {
+ path: "a//a",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ {
+ path: "a/./a",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ {
+ path: "a/../a",
+ wellFormed: false,
+ windowsSafe: true,
+ },
+ // False positive tests
+ {
+ path: "...",
+ wellFormed: true,
+ windowsSafe: true,
+ },
+ {
+ path: ".../a",
+ wellFormed: true,
+ windowsSafe: true,
+ },
+ {
+ path: "a/...",
+ wellFormed: true,
+ windowsSafe: true,
+ },
+ {
+ path: "a/.../a",
+ wellFormed: true,
+ windowsSafe: true,
+ },
+ {
+ path: ".../...",
+ wellFormed: true,
+ windowsSafe: true,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(fmt.Sprintf("Path: \"%v\"", tt.path), func(t *testing.T) {
+ wellFormed, windowsSafe := checkName(tt.path)
+ if wellFormed != tt.wellFormed || windowsSafe != tt.windowsSafe {
+ t.Errorf("\nwantOutput: checkName(\"%v\") wellFormed = %v, windowsSafe %v\ngot: checkName(\"%v\") wellFormed = %v, windowsSafe %v", tt.path, tt.wellFormed, tt.windowsSafe, tt.path, wellFormed, windowsSafe)
+ }
+ })
+ }
+}
+
+func Test_canonicalizeLinkname(t *testing.T) {
+ // We're lying that this thing is absolute, but that's not relevant for tests.
+ anchor := turbopath.AbsoluteSystemPath(filepath.Join("path", "to", "anchor"))
+
+ tests := []struct {
+ name string
+ processedName turbopath.AnchoredSystemPath
+ linkname string
+ canonicalUnix string
+ canonicalWindows string
+ }{
+ {
+ name: "hello world",
+ processedName: turbopath.AnchoredSystemPath("source"),
+ linkname: "target",
+ canonicalUnix: "path/to/anchor/target",
+ canonicalWindows: "path\\to\\anchor\\target",
+ },
+ {
+ name: "Unix path subdirectory traversal",
+ processedName: turbopath.AnchoredUnixPath("child/source").ToSystemPath(),
+ linkname: "../sibling/target",
+ canonicalUnix: "path/to/anchor/sibling/target",
+ canonicalWindows: "path\\to\\anchor\\sibling\\target",
+ },
+ {
+ name: "Windows path subdirectory traversal",
+ processedName: turbopath.AnchoredUnixPath("child/source").ToSystemPath(),
+ linkname: "..\\sibling\\target",
+ canonicalUnix: "path/to/anchor/child/..\\sibling\\target",
+ canonicalWindows: "path\\to\\anchor\\sibling\\target",
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ canonical := tt.canonicalUnix
+ if runtime.GOOS == "windows" {
+ canonical = tt.canonicalWindows
+ }
+ if got := canonicalizeLinkname(anchor, tt.processedName, tt.linkname); got != canonical {
+ t.Errorf("canonicalizeLinkname() = %v, want %v", got, canonical)
+ }
+ })
+ }
+}
+
+func Test_canonicalizeName(t *testing.T) {
+ tests := []struct {
+ name string
+ fileName string
+ want turbopath.AnchoredSystemPath
+ wantErr error
+ }{
+ {
+ name: "hello world",
+ fileName: "test.txt",
+ want: "test.txt",
+ },
+ {
+ name: "directory",
+ fileName: "something/",
+ want: "something",
+ },
+ {
+ name: "malformed name",
+ fileName: "//",
+ want: "",
+ wantErr: errNameMalformed,
+ },
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ got, err := canonicalizeName(tt.fileName)
+ if tt.wantErr != nil && !errors.Is(err, tt.wantErr) {
+ t.Errorf("canonicalizeName() error = %v, wantErr %v", err, tt.wantErr)
+ return
+ }
+ if !reflect.DeepEqual(got, tt.want) {
+ t.Errorf("canonicalizeName() = %v, want %v", got, tt.want)
+ }
+ })
+ }
+}
+
+func TestCacheItem_Restore(t *testing.T) {
+ tests := []struct {
+ name string
+ tarFiles []tarFile
+ want []turbopath.AnchoredSystemPath
+ }{
+ {
+ name: "duplicate restores",
+ tarFiles: []tarFile{
+ {
+ Header: &tar.Header{
+ Name: "target",
+ Typeflag: tar.TypeReg,
+ Mode: 0644,
+ },
+ Body: "target",
+ },
+ {
+ Header: &tar.Header{
+ Name: "source",
+ Linkname: "target",
+ Typeflag: tar.TypeSymlink,
+ Mode: 0777,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one/",
+ Typeflag: tar.TypeDir,
+ Mode: 0755,
+ },
+ },
+ {
+ Header: &tar.Header{
+ Name: "one/two/",
+ Typeflag: tar.TypeDir,
+ Mode: 0755,
+ },
+ },
+ },
+ want: turbopath.AnchoredUnixPathArray{"target", "source", "one", "one/two"}.ToSystemPathArray(),
+ },
+ }
+ for _, tt := range tests {
+ getTestFunc := func(compressed bool) func(t *testing.T) {
+ return func(t *testing.T) {
+ var archivePath turbopath.AbsoluteSystemPath
+ if compressed {
+ archivePath = compressTar(t, generateTar(t, tt.tarFiles))
+ } else {
+ archivePath = generateTar(t, tt.tarFiles)
+ }
+ anchor := generateAnchor(t)
+
+ cacheItem, err := Open(archivePath)
+ assert.NilError(t, err, "Open")
+
+ restoreOutput, restoreErr := cacheItem.Restore(anchor)
+ if !reflect.DeepEqual(restoreOutput, tt.want) {
+ t.Errorf("#1 CacheItem.Restore() = %v, want %v", restoreOutput, tt.want)
+ }
+ assert.NilError(t, restoreErr, "Restore #1")
+ assert.NilError(t, cacheItem.Close(), "Close")
+
+ cacheItem2, err2 := Open(archivePath)
+ assert.NilError(t, err2, "Open")
+
+ restoreOutput2, restoreErr2 := cacheItem2.Restore(anchor)
+ if !reflect.DeepEqual(restoreOutput2, tt.want) {
+ t.Errorf("#2 CacheItem.Restore() = %v, want %v", restoreOutput2, tt.want)
+ }
+ assert.NilError(t, restoreErr2, "Restore #2")
+ assert.NilError(t, cacheItem2.Close(), "Close")
+ }
+ }
+ t.Run(tt.name+"zst", getTestFunc(true))
+ t.Run(tt.name, getTestFunc(false))
+ }
+}