Files
overlayfs/mergefs_test.go
Matheus Sampaio Queiroga ecd4fae833
All checks were successful
Golang test / go-test (push) Successful in 39s
Minor changes
Signed-off-by: Matheus Sampaio Queiroga <srherobrine20@gmail.com>
2025-08-01 19:10:23 -03:00

322 lines
9.7 KiB
Go

package overlayfs
import (
"io/fs"
"os"
"path/filepath"
"strings"
"testing"
)
func setupTestDirs(t *testing.T) (string, string, []string, func()) {
t.Helper()
baseDir, err := os.MkdirTemp("", "overlayfs_test")
if err != nil {
t.Fatalf("Failed to create temp dir: %v", err)
}
upperDir := filepath.Join(baseDir, "upper")
lowerDir1 := filepath.Join(baseDir, "lower1")
lowerDir2 := filepath.Join(baseDir, "lower2")
workDir := filepath.Join(baseDir, "work")
for _, dir := range []string{upperDir, lowerDir1, lowerDir2, workDir} {
if err := os.Mkdir(dir, 0755); err != nil {
t.Fatalf("Failed to create dir %s: %v", dir, err)
}
}
// Base files
os.WriteFile(filepath.Join(lowerDir1, "file_in_lower1.txt"), []byte("lower1"), 0644)
os.WriteFile(filepath.Join(lowerDir2, "file_in_lower2.txt"), []byte("lower2"), 0644)
os.WriteFile(filepath.Join(lowerDir2, "file_in_both.txt"), []byte("from lower2"), 0644)
os.WriteFile(filepath.Join(lowerDir1, "file_in_both.txt"), []byte("from lower1"), 0644)
os.Mkdir(filepath.Join(lowerDir1, "dir_in_lower1"), 0755)
os.WriteFile(filepath.Join(lowerDir1, "dir_in_lower1", "test.txt"), []byte("test"), 0644)
os.WriteFile(filepath.Join(lowerDir1, "file_to_delete.txt"), []byte("delete me"), 0644)
// Set deleted file
os.WriteFile(filepath.Join(upperDir, ".wh.file_to_delete.txt"), nil, 0644)
cleanup := func() { os.RemoveAll(baseDir) }
return upperDir, workDir, []string{lowerDir1, lowerDir2}, cleanup
}
func TestReadOnlyOperations(t *testing.T) {
upperDir, workDir, lowerDirs, cleanup := setupTestDirs(t)
defer cleanup()
ofs := NewOverlayFS("", upperDir, workDir, lowerDirs...)
t.Run("ReadFile from lower1", func(t *testing.T) {
content, err := ofs.ReadFile("file_in_lower1.txt")
if err != nil {
t.Fatalf("Expected to read file from lower1, but got error: %v", err)
}
if string(content) != "lower1" {
t.Errorf("Expected content 'lower1', got '%s'", string(content))
}
})
t.Run("ReadFile from lower2", func(t *testing.T) {
content, err := ofs.ReadFile("file_in_lower2.txt")
if err != nil {
t.Fatalf("Expected to read file from lower2, but got error: %v", err)
}
if string(content) != "lower2" {
t.Errorf("Expected content 'lower2', got '%s'", string(content))
}
})
t.Run("ReadFile from uppermost lower layer", func(t *testing.T) {
content, err := ofs.ReadFile("file_in_both.txt")
if err != nil {
t.Fatalf("Expected to read file, but got error: %v", err)
}
if string(content) != "from lower2" {
t.Errorf("Expected content 'from lower2', got '%s'", string(content))
}
})
t.Run("ReadFile that is deleted", func(t *testing.T) {
_, err := ofs.ReadFile("file_to_delete.txt")
if err != nil && !os.IsNotExist(err) {
t.Errorf("Expected fs.ErrNotExist for deleted file, but got: %v", err)
}
})
// New test case to check for files within subdirectories
t.Run("ReadFile from a subdirectory", func(t *testing.T) {
nestedFilePath := filepath.Join("dir_in_lower1", "test.txt")
content, err := ofs.ReadFile(nestedFilePath)
if err != nil {
t.Fatalf("Expected to read file from subdirectory, but got error: %v", err)
}
if string(content) != "test" {
t.Errorf("Expected content 'test', got '%s'", string(content))
}
})
t.Run("ReadDir", func(t *testing.T) {
entries, err := ofs.ReadDir(".")
if err != nil {
t.Fatalf("ReadDir failed: %v", err)
}
expectedFiles := map[string]bool{
"file_in_lower1.txt": true,
"file_in_lower2.txt": true,
"file_in_both.txt": true,
"dir_in_lower1": true,
}
if len(entries) != len(expectedFiles) {
t.Errorf("Expected %d directory entries, but got %d", len(expectedFiles), len(entries))
}
for _, entry := range entries {
if _, ok := expectedFiles[entry.Name()]; !ok {
t.Errorf("Unexpected file in ReadDir: %s", entry.Name())
}
}
})
t.Run("ReadDir File", func(t *testing.T) {
_, err := ofs.ReadDir("./file_in_lower1.txt")
if err == nil {
t.Fatalf("ReadDir failed: require return not a directory")
}
})
}
func TestReadWriteOperations(t *testing.T) {
upperDir, workDir, lowerDirs, cleanup := setupTestDirs(t)
defer cleanup()
ofs := NewOverlayFS("target", upperDir, workDir, lowerDirs...)
t.Run("Create a new file", func(t *testing.T) {
err := ofs.WriteFile("new_file.txt", []byte("new content"), 0644)
if err != nil {
t.Fatalf("Failed to create new file: %v", err)
}
content, err := os.ReadFile(filepath.Join(upperDir, "new_file.txt"))
if err != nil {
t.Fatalf("Could not read back new file from upper dir: %v", err)
}
if string(content) != "new content" {
t.Errorf("Expected content 'new content', got '%s'", string(content))
}
})
t.Run("Modify a file from a lower layer (copy on write)", func(t *testing.T) {
err := ofs.WriteFile("file_in_lower1.txt", []byte("modified"), 0644)
if err != nil {
t.Fatalf("Failed to write to existing file: %v", err)
}
// Check upper layer
content, err := os.ReadFile(filepath.Join(upperDir, "file_in_lower1.txt"))
if err != nil {
t.Fatalf("File was not copied to upper layer: %v", err)
}
if string(content) != "modified" {
t.Errorf("Expected content 'modified', got '%s'", string(content))
}
// Check lower layer remains unchanged
content, err = os.ReadFile(filepath.Join(lowerDirs[0], "file_in_lower1.txt"))
if err != nil {
t.Fatalf("Could not read from lower layer: %v", err)
}
if string(content) != "lower1" {
t.Errorf("Lower layer file should not have been modified")
}
})
t.Run("Remove a file from a lower layer", func(t *testing.T) {
err := ofs.Remove("file_in_lower2.txt")
if err != nil {
t.Fatalf("Remove failed: %v", err)
}
_, err = os.Stat(filepath.Join(upperDir, ".wh.file_in_lower2.txt"))
if os.IsNotExist(err) {
t.Error("Whiteout file was not created for removed file")
}
_, err = ofs.Stat("file_in_lower2.txt")
if err != nil && !os.IsNotExist(err) {
t.Errorf("Expected file to be removed, but stat returned: %v", err)
}
})
t.Run("Create a directory", func(t *testing.T) {
err := ofs.Mkdir("new_dir", 0755)
if err != nil {
t.Fatalf("Mkdir failed: %v", err)
}
info, err := os.Stat(filepath.Join(upperDir, "new_dir"))
if err != nil {
t.Fatalf("Directory not created in upper layer: %v", err)
}
if !info.IsDir() {
t.Error("Expected a directory but got a file")
}
})
t.Run("Rename a file", func(t *testing.T) {
err := ofs.Rename("file_in_lower1.txt", "renamed_file.txt")
if err != nil {
t.Fatalf("Rename failed: %v", err)
}
// Check if new file exists in upper
_, err = os.Stat(filepath.Join(upperDir, "renamed_file.txt"))
if err != nil {
t.Errorf("Renamed file does not exist in upper layer: %v", err)
}
// Check if old file is marked as deleted
_, err = ofs.Stat("file_in_lower1.txt")
if err != nil && !os.IsNotExist(err) {
t.Errorf("Expected old file to be inaccessible, but got err: %v", err)
}
})
}
func TestSecurity_OutsideAccessBlocked(t *testing.T) {
upperDir, workDir, lowerDirs, cleanup := setupTestDirs(t)
defer cleanup()
ofs := NewOverlayFS("target", upperDir, workDir, lowerDirs...)
t.Run("Attempt to access ../ outside layer", func(t *testing.T) {
_, err := ofs.OpenFile("../outside.txt", os.O_CREATE|os.O_RDWR, 0644)
if err == nil || !strings.Contains(err.Error(), "invalid") && !os.IsNotExist(err) {
t.Errorf("Expected failure when accessing outside path, got: %v", err)
}
})
t.Run("Attempt to access absolute path outside", func(t *testing.T) {
_, err := ofs.OpenFile("/etc/passwd", os.O_RDONLY, 0644)
if err == nil || !strings.Contains(err.Error(), "invalid") && !os.IsNotExist(err) {
t.Errorf("Expected failure on absolute path access, got: %v", err)
}
})
t.Run("Attempt to remove outside file", func(t *testing.T) {
err := ofs.Remove("../malicious.txt")
if err == nil || !strings.Contains(err.Error(), "invalid") && !os.IsNotExist(err) {
t.Errorf("Expected failure when removing outside file, got: %v", err)
}
})
}
func TestMergefsIOFS(t *testing.T) {
upperDir, workDir, lowerDirs, cleanup := setupTestDirs(t)
defer cleanup()
iofs := NewOverlayFS("target", upperDir, workDir, lowerDirs...).Mergefs()
t.Run("fs.ReadFile", func(t *testing.T) {
data, err := fs.ReadFile(iofs, "file_in_lower1.txt")
if err != nil {
t.Fatalf("fs.ReadFile failed: %v", err)
}
if string(data) != "lower1" {
t.Errorf("Expected 'lower1', got %q", data)
}
})
t.Run("fs.ReadDir", func(t *testing.T) {
entries, err := fs.ReadDir(iofs, ".")
if err != nil {
t.Fatalf("fs.ReadDir failed: %v", err)
}
found := map[string]bool{}
for _, entry := range entries {
found[entry.Name()] = true
}
expected := []string{"file_in_lower1.txt", "file_in_lower2.txt", "file_in_both.txt", "dir_in_lower1"}
for _, name := range expected {
if !found[name] {
t.Errorf("Expected to find %q in fs.ReadDir result", name)
}
}
})
t.Run("fs.Stat", func(t *testing.T) {
info, err := fs.Stat(iofs, "file_in_both.txt")
if err != nil {
t.Fatalf("fs.Stat failed: %v", err)
}
if info.Name() != "file_in_both.txt" {
t.Errorf("Expected file_in_both.txt, got %s", info.Name())
}
})
t.Run("fs.Sub", func(t *testing.T) {
subfs, err := fs.Sub(iofs, "dir_in_lower1")
if err != nil {
t.Fatalf("fs.Sub failed: %v", err)
}
data, err := fs.ReadFile(subfs, "test.txt")
if err != nil {
t.Fatalf("fs.ReadFile on subfs failed: %v", err)
}
if string(data) != "test" {
t.Errorf("Expected 'test', got %q", data)
}
})
t.Run("fs.Glob", func(t *testing.T) {
matches, err := fs.Glob(iofs, "file_in_*.txt")
if err != nil {
t.Fatalf("fs.Glob failed: %v", err)
}
found := strings.Join(matches, ", ")
if !strings.Contains(found, "file_in_lower1.txt") || !strings.Contains(found, "file_in_lower2.txt") {
t.Errorf("Expected matches for file_in_lower1.txt and file_in_lower2.txt, got %s", found)
}
})
}