package chunked import ( "bytes" "fmt" "io" "os" "path" "syscall" "testing" "github.com/containers/storage/pkg/archive" "github.com/containers/storage/pkg/chunked/internal/minimal" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) type nopCloser struct { *bytes.Reader } func (nopCloser) Close() error { return nil } func TestSeekableFileGetBlobAt(t *testing.T) { content := []byte("Hello, World!") br := bytes.NewReader(content) reader := nopCloser{br} sf := newSeekableFile(reader) chunks := []ImageSourceChunk{ {Offset: 0, Length: 5}, {Offset: 7, Length: 5}, } streams, errs, err := sf.GetBlobAt(chunks) assert.NoError(t, err) expectedContents := [][]byte{ []byte("Hello"), []byte("World"), } i := 0 for stream := range streams { buf := new(bytes.Buffer) _, err := buf.ReadFrom(stream) require.NoError(t, err) require.Equal(t, expectedContents[i], buf.Bytes()) i++ } err, ok := <-errs assert.NoError(t, err) assert.False(t, ok) } func TestDoHardLink(t *testing.T) { tmpDir := t.TempDir() srcFile := createTempFile(t, tmpDir, "source") defer srcFile.Close() srcFd := int(srcFile.Fd()) destDir := t.TempDir() destDirFd, err := syscall.Open(destDir, syscall.O_RDONLY|syscall.O_CLOEXEC, 0) require.NoError(t, err) defer syscall.Close(destDirFd) destBase := "dest-file" err = doHardLink(destDirFd, srcFd, destBase) require.NoError(t, err) // an existing file is unlinked first err = doHardLink(destDirFd, srcFd, destBase) assert.NoError(t, err) err = doHardLink(destDirFd, -1, destBase) assert.Error(t, err) err = doHardLink(-1, srcFd, destBase) assert.Error(t, err) } func TestAppendHole(t *testing.T) { tmpDir := t.TempDir() tmpFile := createTempFile(t, tmpDir, "file-with-holes") defer tmpFile.Close() fd := int(tmpFile.Fd()) size := int64(1024) err := appendHole(fd, tmpFile.Name(), size) assert.NoError(t, err, "Appending hole failed") fileSize, err := syscall.Seek(fd, 0, io.SeekEnd) assert.NoError(t, err) assert.Equal(t, size, fileSize, "File size is not as expected") } func TestSafeMkdir(t *testing.T) { rootDir := t.TempDir() dirName := "../dir" rootFile, err := os.Open(rootDir) require.NoError(t, err) defer rootFile.Close() rootFd := int(rootFile.Fd()) metadata := fileMetadata{ FileMetadata: minimal.FileMetadata{ Type: minimal.TypeDir, Mode: 0o755, }, } options := &archive.TarOptions{ // Allow the test to run without privileges IgnoreChownErrors: true, } err = safeMkdir(rootFd, 0o755, "/", &metadata, options) require.NoError(t, err) err = safeMkdir(rootFd, 0o755, dirName, &metadata, options) require.NoError(t, err) dir, err := openFileUnderRoot(rootFd, dirName, syscall.O_DIRECTORY|syscall.O_CLOEXEC, 0) require.NoError(t, err) err = dir.Close() assert.NoError(t, err) } func TestSafeLink(t *testing.T) { linkName := "a-hard-link" rootDir := t.TempDir() rootFile, err := os.Open(rootDir) require.NoError(t, err) defer rootFile.Close() rootFd := int(rootFile.Fd()) file := createTempFile(t, rootDir, "an-existing-file") existingFile := path.Base(file.Name()) err = file.Close() assert.NoError(t, err) metadata := fileMetadata{ FileMetadata: minimal.FileMetadata{ Name: linkName, // try to create outside the root Linkname: "../../" + existingFile, Type: minimal.TypeReg, Mode: 0o755, }, } options := &archive.TarOptions{ // Allow the test to run without privileges IgnoreChownErrors: true, } err = safeLink(rootFd, 0o755, &metadata, options) require.NoError(t, err) // validate it was created newFile, err := openFileUnderRoot(rootFd, linkName, syscall.O_RDONLY, 0) require.NoError(t, err) st := syscall.Stat_t{} err = syscall.Fstat(int(newFile.Fd()), &st) assert.NoError(t, err) assert.Equal(t, 2, int(st.Nlink)) err = newFile.Close() assert.NoError(t, err) } func TestSafeSymlink(t *testing.T) { linkName := "a-hard-link" rootDir := t.TempDir() rootFile, err := os.Open(rootDir) require.NoError(t, err) defer rootFile.Close() rootFd := int(rootFile.Fd()) file := createTempFile(t, rootDir, "an-existing-file") st := syscall.Stat_t{} err = syscall.Fstat(int(file.Fd()), &st) assert.NoError(t, err) err = file.Close() assert.NoError(t, err) existingFile := path.Base(file.Name()) metadata := fileMetadata{ FileMetadata: minimal.FileMetadata{ Name: linkName, // try to create outside the root Linkname: "../../" + existingFile, Type: minimal.TypeReg, Mode: 0o755, }, } err = safeSymlink(rootFd, &metadata) require.NoError(t, err) // validate it was created newFile, err := openFileUnderRoot(rootFd, linkName, syscall.O_RDONLY, 0) require.NoError(t, err) st2 := syscall.Stat_t{} err = syscall.Fstat(int(newFile.Fd()), &st2) require.NoError(t, err) // validate that the opened file is the same as the original file that was // created earlier. Compare the inode and device numbers. assert.Equal(t, st.Dev, st2.Dev) assert.Equal(t, st.Ino, st2.Ino) err = newFile.Close() assert.NoError(t, err) } func TestOpenOrCreateDirUnderRoot(t *testing.T) { rootDir := t.TempDir() dirName := "dir" rootFile, err := os.Open(rootDir) require.NoError(t, err) defer rootFile.Close() rootFd := int(rootFile.Fd()) // try to create a directory outside the root dir, err := openOrCreateDirUnderRoot(rootFd, "../../"+dirName, 0o755) require.NoError(t, err) err = dir.Close() assert.NoError(t, err) dir, err = openFileUnderRoot(rootFd, dirName, syscall.O_DIRECTORY|syscall.O_CLOEXEC, 0) require.NoError(t, err) err = dir.Close() require.NoError(t, err) } func TestCopyFileContent(t *testing.T) { rootDir := t.TempDir() rootFile, err := os.Open(rootDir) require.NoError(t, err) defer rootFile.Close() rootFd := int(rootFile.Fd()) file := createTempFile(t, rootDir, "an-existing-file") defer file.Close() size, err := file.Write([]byte("Hello, World!")) require.NoError(t, err) _, err = file.Seek(0, io.SeekStart) assert.NoError(t, err) st := syscall.Stat_t{} err = syscall.Fstat(int(file.Fd()), &st) require.NoError(t, err) metadata := fileMetadata{ FileMetadata: minimal.FileMetadata{ Name: "new-file", Type: minimal.TypeDir, Mode: 0o755, }, } newFile, newSize, err := copyFileContent(int(file.Fd()), &metadata, rootFd, 0o755, false) require.NoError(t, err) assert.Equal(t, size, int(newSize)) st2 := syscall.Stat_t{} err = syscall.Fstat(int(newFile.Fd()), &st2) require.NoError(t, err) err = newFile.Close() require.NoError(t, err) // the file was copied without hard links, the inodes must be different assert.Equal(t, st.Dev, st2.Dev) assert.NotEqual(t, st.Ino, st2.Ino) metadataCopyHardLinks := fileMetadata{ FileMetadata: minimal.FileMetadata{ Name: "new-file2", Type: minimal.TypeDir, Mode: 0o755, }, } newFile, newSize, err = copyFileContent(int(file.Fd()), &metadataCopyHardLinks, rootFd, 0o755, true) require.NoError(t, err) assert.Nil(t, newFile) // validate it was created as an inode newFile, err = openFileUnderRoot(rootFd, metadataCopyHardLinks.FileMetadata.Name, syscall.O_RDONLY, 0) require.NoError(t, err) assert.Equal(t, size, int(newSize)) st2 = syscall.Stat_t{} err = syscall.Fstat(int(newFile.Fd()), &st2) require.NoError(t, err) err = newFile.Close() require.NoError(t, err) // the file was copied with hard links, the inodes must be equal assert.Equal(t, st.Dev, st2.Dev) assert.Equal(t, st.Ino, st2.Ino) } func createTempFile(t *testing.T, dir, name string) *os.File { tmpFile, err := os.CreateTemp(dir, name) require.NoError(t, err) return tmpFile } func TestSplitPath(t *testing.T) { tests := []struct { path string expectedDir string expectedBase string }{ {"", "/", "."}, {".", "/", "."}, {"..", "/", "."}, {"../..", "/", "."}, {"../../..", "/", "."}, {"../../../foo", "/", "foo"}, {"../../../foo/..", "/", "."}, {"../../../foo/./../foo/bar/baz", "/foo/bar", "baz"}, {"../../../foo/bar", "/foo", "bar"}, {"/", "/", "."}, {"/.", "/", "."}, {"/..", "/", "."}, {"////foo////bar////", "/foo", "bar"}, {"/foo", "/", "foo"}, {"/foo/", "/", "foo"}, {"/foo/bar", "/foo", "bar"}, {"/foo/bar/", "/foo", "bar"}, {"/foo/////bar/", "/foo", "bar"}, {"/home/foo/file.txt", "/home/foo", "file.txt"}, {"/home/foo////file.txt", "/home/foo", "file.txt"}, {"file", "/", "file"}, {"foo/", "/", "foo"}, {"foo/.", "/", "foo"}, {"foo/..", "/", "."}, {"foo/../../bar", "/", "bar"}, {"foo/bar/", "/foo", "bar"}, {"foo/bar/..", "/", "foo"}, {"foo/bar/../baz", "/foo", "baz"}, {"foo/bar/baz/", "/foo/bar", "baz"}, {"foo/file.txt", "/foo", "file.txt"}, } for _, test := range tests { dir, base, err := splitPath(test.path) assert.NoError(t, err) assert.Equal(t, test.expectedDir, dir, fmt.Sprintf("path %q: expected dir %q, got %q", test.path, test.expectedDir, dir)) assert.Equal(t, test.expectedBase, base, fmt.Sprintf("path %q: expected base %q, got %q", test.path, test.expectedBase, base)) } }