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)) } }