diff options
Diffstat (limited to 'cli/internal/cacheitem/restore_test.go')
| -rw-r--r-- | cli/internal/cacheitem/restore_test.go | 1493 |
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)) + } +} |
