386 lines
8.9 KiB
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))
|
|
}
|
|
}
|