473 lines
12 KiB
Go
473 lines
12 KiB
Go
package local
|
|
|
|
import (
|
|
"io"
|
|
"io/fs"
|
|
"syscall"
|
|
"testing"
|
|
|
|
core "dappco.re/go/core"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestLocal_New_ResolvesRoot_Good(t *testing.T) {
|
|
root := t.TempDir()
|
|
localMedium, err := New(root)
|
|
assert.NoError(t, err)
|
|
resolved, err := resolveSymlinksPath(root)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, resolved, localMedium.filesystemRoot)
|
|
}
|
|
|
|
func TestLocal_Path_Sandboxed_Good(t *testing.T) {
|
|
localMedium := &Medium{filesystemRoot: "/home/user"}
|
|
|
|
assert.Equal(t, "/home/user/file.txt", localMedium.sandboxedPath("file.txt"))
|
|
assert.Equal(t, "/home/user/dir/file.txt", localMedium.sandboxedPath("dir/file.txt"))
|
|
|
|
assert.Equal(t, "/home/user", localMedium.sandboxedPath(""))
|
|
|
|
assert.Equal(t, "/home/user/file.txt", localMedium.sandboxedPath("../file.txt"))
|
|
assert.Equal(t, "/home/user/file.txt", localMedium.sandboxedPath("dir/../file.txt"))
|
|
|
|
assert.Equal(t, "/home/user/etc/passwd", localMedium.sandboxedPath("/etc/passwd"))
|
|
}
|
|
|
|
func TestLocal_Path_RootFilesystem_Good(t *testing.T) {
|
|
localMedium := &Medium{filesystemRoot: "/"}
|
|
|
|
assert.Equal(t, "/etc/passwd", localMedium.sandboxedPath("/etc/passwd"))
|
|
assert.Equal(t, "/home/user/file.txt", localMedium.sandboxedPath("/home/user/file.txt"))
|
|
|
|
workingDirectory := currentWorkingDir()
|
|
assert.Equal(t, core.Path(workingDirectory, "file.txt"), localMedium.sandboxedPath("file.txt"))
|
|
}
|
|
|
|
func TestLocal_ReadWrite_Basic_Good(t *testing.T) {
|
|
root := t.TempDir()
|
|
localMedium, _ := New(root)
|
|
|
|
err := localMedium.Write("test.txt", "hello")
|
|
assert.NoError(t, err)
|
|
|
|
content, err := localMedium.Read("test.txt")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "hello", content)
|
|
|
|
err = localMedium.Write("a/b/c.txt", "nested")
|
|
assert.NoError(t, err)
|
|
|
|
content, err = localMedium.Read("a/b/c.txt")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "nested", content)
|
|
|
|
_, err = localMedium.Read("nope.txt")
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestLocal_EnsureDir_Basic_Good(t *testing.T) {
|
|
root := t.TempDir()
|
|
localMedium, _ := New(root)
|
|
|
|
err := localMedium.EnsureDir("one/two/three")
|
|
assert.NoError(t, err)
|
|
|
|
info, err := localMedium.Stat("one/two/three")
|
|
assert.NoError(t, err)
|
|
assert.True(t, info.IsDir())
|
|
}
|
|
|
|
func TestLocal_IsDir_Basic_Good(t *testing.T) {
|
|
root := t.TempDir()
|
|
localMedium, _ := New(root)
|
|
|
|
_ = localMedium.EnsureDir("mydir")
|
|
_ = localMedium.Write("myfile", "x")
|
|
|
|
assert.True(t, localMedium.IsDir("mydir"))
|
|
assert.False(t, localMedium.IsDir("myfile"))
|
|
assert.False(t, localMedium.IsDir("nope"))
|
|
assert.False(t, localMedium.IsDir(""))
|
|
}
|
|
|
|
func TestLocal_IsFile_Basic_Good(t *testing.T) {
|
|
root := t.TempDir()
|
|
localMedium, _ := New(root)
|
|
|
|
_ = localMedium.EnsureDir("mydir")
|
|
_ = localMedium.Write("myfile", "x")
|
|
|
|
assert.True(t, localMedium.IsFile("myfile"))
|
|
assert.False(t, localMedium.IsFile("mydir"))
|
|
assert.False(t, localMedium.IsFile("nope"))
|
|
assert.False(t, localMedium.IsFile(""))
|
|
}
|
|
|
|
func TestLocal_Exists_Basic_Good(t *testing.T) {
|
|
root := t.TempDir()
|
|
localMedium, _ := New(root)
|
|
|
|
_ = localMedium.Write("exists", "x")
|
|
|
|
assert.True(t, localMedium.Exists("exists"))
|
|
assert.False(t, localMedium.Exists("nope"))
|
|
}
|
|
|
|
func TestLocal_List_Basic_Good(t *testing.T) {
|
|
root := t.TempDir()
|
|
localMedium, _ := New(root)
|
|
|
|
_ = localMedium.Write("a.txt", "a")
|
|
_ = localMedium.Write("b.txt", "b")
|
|
_ = localMedium.EnsureDir("subdir")
|
|
|
|
entries, err := localMedium.List("")
|
|
assert.NoError(t, err)
|
|
assert.Len(t, entries, 3)
|
|
}
|
|
|
|
func TestLocal_Stat_Basic_Good(t *testing.T) {
|
|
root := t.TempDir()
|
|
localMedium, _ := New(root)
|
|
|
|
_ = localMedium.Write("file", "content")
|
|
|
|
info, err := localMedium.Stat("file")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, int64(7), info.Size())
|
|
}
|
|
|
|
func TestLocal_Delete_Basic_Good(t *testing.T) {
|
|
root := t.TempDir()
|
|
localMedium, _ := New(root)
|
|
|
|
_ = localMedium.Write("todelete", "x")
|
|
assert.True(t, localMedium.Exists("todelete"))
|
|
|
|
err := localMedium.Delete("todelete")
|
|
assert.NoError(t, err)
|
|
assert.False(t, localMedium.Exists("todelete"))
|
|
}
|
|
|
|
func TestLocal_DeleteAll_Basic_Good(t *testing.T) {
|
|
root := t.TempDir()
|
|
localMedium, _ := New(root)
|
|
|
|
_ = localMedium.Write("dir/sub/file", "x")
|
|
|
|
err := localMedium.DeleteAll("dir")
|
|
assert.NoError(t, err)
|
|
assert.False(t, localMedium.Exists("dir"))
|
|
}
|
|
|
|
func TestLocal_Delete_ProtectedHomeViaSymlinkEnv_Bad(t *testing.T) {
|
|
realHome := t.TempDir()
|
|
linkParent := t.TempDir()
|
|
homeLink := core.Path(linkParent, "home-link")
|
|
require.NoError(t, syscall.Symlink(realHome, homeLink))
|
|
t.Setenv("HOME", homeLink)
|
|
|
|
localMedium, err := New("/")
|
|
require.NoError(t, err)
|
|
|
|
err = localMedium.Delete(realHome)
|
|
require.Error(t, err)
|
|
assert.DirExists(t, realHome)
|
|
}
|
|
|
|
func TestLocal_DeleteAll_ProtectedHomeViaEnv_Bad(t *testing.T) {
|
|
tempHome := t.TempDir()
|
|
t.Setenv("HOME", tempHome)
|
|
|
|
localMedium, err := New("/")
|
|
require.NoError(t, err)
|
|
|
|
err = localMedium.DeleteAll(tempHome)
|
|
require.Error(t, err)
|
|
assert.DirExists(t, tempHome)
|
|
}
|
|
|
|
func TestLocal_Rename_Basic_Good(t *testing.T) {
|
|
root := t.TempDir()
|
|
localMedium, _ := New(root)
|
|
|
|
_ = localMedium.Write("old", "x")
|
|
|
|
err := localMedium.Rename("old", "new")
|
|
assert.NoError(t, err)
|
|
assert.False(t, localMedium.Exists("old"))
|
|
assert.True(t, localMedium.Exists("new"))
|
|
}
|
|
|
|
func TestLocal_Delete_Good(t *testing.T) {
|
|
testRoot := t.TempDir()
|
|
|
|
localMedium, err := New(testRoot)
|
|
assert.NoError(t, err)
|
|
|
|
err = localMedium.Write("file.txt", "content")
|
|
assert.NoError(t, err)
|
|
assert.True(t, localMedium.IsFile("file.txt"))
|
|
|
|
err = localMedium.Delete("file.txt")
|
|
assert.NoError(t, err)
|
|
assert.False(t, localMedium.IsFile("file.txt"))
|
|
|
|
err = localMedium.EnsureDir("emptydir")
|
|
assert.NoError(t, err)
|
|
err = localMedium.Delete("emptydir")
|
|
assert.NoError(t, err)
|
|
assert.False(t, localMedium.IsDir("emptydir"))
|
|
}
|
|
|
|
func TestLocal_Delete_NotEmpty_Bad(t *testing.T) {
|
|
testRoot := t.TempDir()
|
|
|
|
localMedium, err := New(testRoot)
|
|
assert.NoError(t, err)
|
|
|
|
err = localMedium.Write("mydir/file.txt", "content")
|
|
assert.NoError(t, err)
|
|
|
|
err = localMedium.Delete("mydir")
|
|
assert.Error(t, err)
|
|
}
|
|
|
|
func TestLocal_DeleteAll_Good(t *testing.T) {
|
|
testRoot := t.TempDir()
|
|
|
|
localMedium, err := New(testRoot)
|
|
assert.NoError(t, err)
|
|
|
|
err = localMedium.Write("mydir/file1.txt", "content1")
|
|
assert.NoError(t, err)
|
|
err = localMedium.Write("mydir/subdir/file2.txt", "content2")
|
|
assert.NoError(t, err)
|
|
|
|
err = localMedium.DeleteAll("mydir")
|
|
assert.NoError(t, err)
|
|
assert.False(t, localMedium.Exists("mydir"))
|
|
assert.False(t, localMedium.Exists("mydir/file1.txt"))
|
|
assert.False(t, localMedium.Exists("mydir/subdir/file2.txt"))
|
|
}
|
|
|
|
func TestLocal_Rename_Good(t *testing.T) {
|
|
testRoot := t.TempDir()
|
|
|
|
localMedium, err := New(testRoot)
|
|
assert.NoError(t, err)
|
|
|
|
err = localMedium.Write("old.txt", "content")
|
|
assert.NoError(t, err)
|
|
err = localMedium.Rename("old.txt", "new.txt")
|
|
assert.NoError(t, err)
|
|
assert.False(t, localMedium.IsFile("old.txt"))
|
|
assert.True(t, localMedium.IsFile("new.txt"))
|
|
|
|
content, err := localMedium.Read("new.txt")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "content", content)
|
|
}
|
|
|
|
func TestLocal_Rename_TraversalSanitised_Good(t *testing.T) {
|
|
testRoot := t.TempDir()
|
|
|
|
localMedium, err := New(testRoot)
|
|
assert.NoError(t, err)
|
|
|
|
err = localMedium.Write("file.txt", "content")
|
|
assert.NoError(t, err)
|
|
|
|
err = localMedium.Rename("file.txt", "../escaped.txt")
|
|
assert.NoError(t, err)
|
|
assert.False(t, localMedium.Exists("file.txt"))
|
|
assert.True(t, localMedium.Exists("escaped.txt"))
|
|
}
|
|
|
|
func TestLocal_List_Good(t *testing.T) {
|
|
testRoot := t.TempDir()
|
|
|
|
localMedium, err := New(testRoot)
|
|
assert.NoError(t, err)
|
|
|
|
err = localMedium.Write("file1.txt", "content1")
|
|
assert.NoError(t, err)
|
|
err = localMedium.Write("file2.txt", "content2")
|
|
assert.NoError(t, err)
|
|
err = localMedium.EnsureDir("subdir")
|
|
assert.NoError(t, err)
|
|
|
|
entries, err := localMedium.List(".")
|
|
assert.NoError(t, err)
|
|
assert.Len(t, entries, 3)
|
|
|
|
names := make(map[string]bool)
|
|
for _, entry := range entries {
|
|
names[entry.Name()] = true
|
|
}
|
|
assert.True(t, names["file1.txt"])
|
|
assert.True(t, names["file2.txt"])
|
|
assert.True(t, names["subdir"])
|
|
}
|
|
|
|
func TestLocal_Stat_Good(t *testing.T) {
|
|
testRoot := t.TempDir()
|
|
|
|
localMedium, err := New(testRoot)
|
|
assert.NoError(t, err)
|
|
|
|
err = localMedium.Write("file.txt", "hello world")
|
|
assert.NoError(t, err)
|
|
info, err := localMedium.Stat("file.txt")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "file.txt", info.Name())
|
|
assert.Equal(t, int64(11), info.Size())
|
|
assert.False(t, info.IsDir())
|
|
|
|
err = localMedium.EnsureDir("mydir")
|
|
assert.NoError(t, err)
|
|
info, err = localMedium.Stat("mydir")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "mydir", info.Name())
|
|
assert.True(t, info.IsDir())
|
|
}
|
|
|
|
func TestLocal_Exists_Good(t *testing.T) {
|
|
testRoot := t.TempDir()
|
|
|
|
localMedium, err := New(testRoot)
|
|
assert.NoError(t, err)
|
|
|
|
assert.False(t, localMedium.Exists("nonexistent"))
|
|
|
|
err = localMedium.Write("file.txt", "content")
|
|
assert.NoError(t, err)
|
|
assert.True(t, localMedium.Exists("file.txt"))
|
|
|
|
err = localMedium.EnsureDir("mydir")
|
|
assert.NoError(t, err)
|
|
assert.True(t, localMedium.Exists("mydir"))
|
|
}
|
|
|
|
func TestLocal_IsDir_Good(t *testing.T) {
|
|
testRoot := t.TempDir()
|
|
|
|
localMedium, err := New(testRoot)
|
|
assert.NoError(t, err)
|
|
|
|
err = localMedium.Write("file.txt", "content")
|
|
assert.NoError(t, err)
|
|
assert.False(t, localMedium.IsDir("file.txt"))
|
|
|
|
err = localMedium.EnsureDir("mydir")
|
|
assert.NoError(t, err)
|
|
assert.True(t, localMedium.IsDir("mydir"))
|
|
|
|
assert.False(t, localMedium.IsDir("nonexistent"))
|
|
}
|
|
|
|
func TestLocal_ReadStream_Basic_Good(t *testing.T) {
|
|
root := t.TempDir()
|
|
localMedium, _ := New(root)
|
|
|
|
content := "streaming content"
|
|
err := localMedium.Write("stream.txt", content)
|
|
assert.NoError(t, err)
|
|
|
|
reader, err := localMedium.ReadStream("stream.txt")
|
|
assert.NoError(t, err)
|
|
defer reader.Close()
|
|
|
|
limitReader := io.LimitReader(reader, 9)
|
|
data, err := io.ReadAll(limitReader)
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "streaming", string(data))
|
|
}
|
|
|
|
func TestLocal_WriteStream_Basic_Good(t *testing.T) {
|
|
root := t.TempDir()
|
|
localMedium, _ := New(root)
|
|
|
|
writer, err := localMedium.WriteStream("output.txt")
|
|
assert.NoError(t, err)
|
|
|
|
_, err = io.Copy(writer, core.NewReader("piped data"))
|
|
assert.NoError(t, err)
|
|
err = writer.Close()
|
|
assert.NoError(t, err)
|
|
|
|
content, err := localMedium.Read("output.txt")
|
|
assert.NoError(t, err)
|
|
assert.Equal(t, "piped data", content)
|
|
}
|
|
|
|
func TestLocal_Path_TraversalSandbox_Good(t *testing.T) {
|
|
localMedium := &Medium{filesystemRoot: "/sandbox"}
|
|
|
|
assert.Equal(t, "/sandbox/file.txt", localMedium.sandboxedPath("../../../file.txt"))
|
|
assert.Equal(t, "/sandbox/target", localMedium.sandboxedPath("dir/../../target"))
|
|
|
|
assert.Equal(t, "/sandbox/.ssh/id_rsa", localMedium.sandboxedPath(".ssh/id_rsa"))
|
|
assert.Equal(t, "/sandbox/id_rsa", localMedium.sandboxedPath(".ssh/../id_rsa"))
|
|
|
|
assert.Equal(t, "/sandbox/file\x00.txt", localMedium.sandboxedPath("file\x00.txt"))
|
|
}
|
|
|
|
func TestLocal_ValidatePath_SymlinkEscape_Bad(t *testing.T) {
|
|
root := t.TempDir()
|
|
localMedium, err := New(root)
|
|
assert.NoError(t, err)
|
|
|
|
outside := t.TempDir()
|
|
outsideFile := core.Path(outside, "secret.txt")
|
|
outsideMedium, err := New("/")
|
|
require.NoError(t, err)
|
|
err = outsideMedium.Write(outsideFile, "secret")
|
|
assert.NoError(t, err)
|
|
|
|
_, err = localMedium.validatePath("../outside.txt")
|
|
assert.NoError(t, err)
|
|
|
|
linkPath := core.Path(root, "evil_link")
|
|
err = syscall.Symlink(outside, linkPath)
|
|
assert.NoError(t, err)
|
|
|
|
_, err = localMedium.validatePath("evil_link/secret.txt")
|
|
assert.Error(t, err)
|
|
assert.ErrorIs(t, err, fs.ErrPermission)
|
|
|
|
err = localMedium.EnsureDir("inner")
|
|
assert.NoError(t, err)
|
|
innerDir := core.Path(root, "inner")
|
|
nestedLink := core.Path(innerDir, "nested_evil")
|
|
err = syscall.Symlink(outside, nestedLink)
|
|
assert.NoError(t, err)
|
|
|
|
_, err = localMedium.validatePath("inner/nested_evil/secret.txt")
|
|
assert.Error(t, err)
|
|
assert.ErrorIs(t, err, fs.ErrPermission)
|
|
}
|
|
|
|
func TestLocal_EmptyPaths_Good(t *testing.T) {
|
|
root := t.TempDir()
|
|
localMedium, err := New(root)
|
|
assert.NoError(t, err)
|
|
|
|
_, err = localMedium.Read("")
|
|
assert.Error(t, err)
|
|
|
|
err = localMedium.Write("", "content")
|
|
assert.Error(t, err)
|
|
|
|
err = localMedium.EnsureDir("")
|
|
assert.NoError(t, err)
|
|
|
|
assert.False(t, localMedium.IsDir(""))
|
|
|
|
assert.True(t, localMedium.Exists(""))
|
|
|
|
entries, err := localMedium.List("")
|
|
assert.NoError(t, err)
|
|
assert.NotNil(t, entries)
|
|
}
|