storage/pkg/chunked/filesystem_linux_test.go

386 lines
8.9 KiB
Go

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