" >> $(REPORT_DIR)/posix_compliance_report.html
+ @echo "
Test Summary
" >> $(REPORT_DIR)/posix_compliance_report.html
+ @if [ -f "$(REPORT_DIR)/posix_basic_results.log" ]; then \
+ TOTAL=$$(awk '/RUN|PASS|FAIL/{c++} END{print c+0}' $(REPORT_DIR)/posix_basic_results.log 2>/dev/null || echo 0); \
+ PASSED=$$(awk '/PASS:/{c++} END{print c+0}' $(REPORT_DIR)/posix_basic_results.log 2>/dev/null || echo 0); \
+ FAILED=$$(awk '/FAIL:/{c++} END{print c+0}' $(REPORT_DIR)/posix_basic_results.log 2>/dev/null || echo 0); \
+ echo "
Basic Tests: $$PASSED passed, $$FAILED failed
" >> $(REPORT_DIR)/posix_compliance_report.html; \
+ fi
+ @if [ -f "$(REPORT_DIR)/posix_critical_results.log" ]; then \
+ TOTAL=$$(awk '/RUN|PASS|FAIL/{c++} END{print c+0}' $(REPORT_DIR)/posix_critical_results.log 2>/dev/null || echo 0); \
+ PASSED=$$(awk '/PASS:/{c++} END{print c+0}' $(REPORT_DIR)/posix_critical_results.log 2>/dev/null || echo 0); \
+ FAILED=$$(awk '/FAIL:/{c++} END{print c+0}' $(REPORT_DIR)/posix_critical_results.log 2>/dev/null || echo 0); \
+ echo "
Critical Tests: $$PASSED passed, $$FAILED failed
" >> $(REPORT_DIR)/posix_compliance_report.html; \
+ fi
+ @echo "
" >> $(REPORT_DIR)/posix_compliance_report.html
+ @echo "" >> $(REPORT_DIR)/posix_compliance_report.html; \
+ echo "
$$basename
" >> $(REPORT_DIR)/posix_compliance_report.html; \
+ echo "
" >> $(REPORT_DIR)/posix_compliance_report.html; \
+ tail -50 "$$logfile" | sed 's/&/\&/g; s/\</g; s/>/\>/g' >> $(REPORT_DIR)/posix_compliance_report.html; \
+ echo "
" >> $(REPORT_DIR)/posix_compliance_report.html; \
+ fi; \
+ done
+ @echo "" >> $(REPORT_DIR)/posix_compliance_report.html
+ @echo "$(GREEN)[OK] HTML report generated: $(REPORT_DIR)/posix_compliance_report.html$(RESET)"
+
+generate-text-report:
+ @echo "$(BLUE)[REPORT] Generating text report...$(RESET)"
+ @echo "SeaweedFS POSIX Compliance Report" > $(REPORT_DIR)/posix_compliance_summary.txt
+ @echo "=================================" >> $(REPORT_DIR)/posix_compliance_summary.txt
+ @echo "" >> $(REPORT_DIR)/posix_compliance_summary.txt
+ @echo "Generated: $$(date)" >> $(REPORT_DIR)/posix_compliance_summary.txt
+ @echo "SeaweedFS Version: $$($(WEED_BINARY) version 2>/dev/null || echo 'Unknown')" >> $(REPORT_DIR)/posix_compliance_summary.txt
+ @echo "" >> $(REPORT_DIR)/posix_compliance_summary.txt
+ @echo "Test Summary: See individual log files for detailed results" >> $(REPORT_DIR)/posix_compliance_summary.txt
+ @echo "$(GREEN)[OK] Text report generated: $(REPORT_DIR)/posix_compliance_summary.txt$(RESET)"
+
+generate-json-report:
+ @echo "$(BLUE)[REPORT] Generating JSON report...$(RESET)"
+ @echo "{" > $(REPORT_DIR)/posix_compliance_report.json
+ @echo " \"report_type\": \"posix_compliance\"," >> $(REPORT_DIR)/posix_compliance_report.json
+ @echo " \"timestamp\": \"$$(date -u +"%Y-%m-%dT%H:%M:%SZ")\"," >> $(REPORT_DIR)/posix_compliance_report.json
+ @echo " \"seaweedfs_version\": \"$$($(WEED_BINARY) version 2>/dev/null | head -n1 | sed 's/\\/\\\\/g; s/\"/\\\"/g' || echo 'Unknown')\"," >> $(REPORT_DIR)/posix_compliance_report.json
+ @echo " \"test_environment\": { \"os\": \"$$(uname -s)\", \"arch\": \"$$(uname -m)\" }," >> $(REPORT_DIR)/posix_compliance_report.json
+ @echo " \"test_suites\": {" >> $(REPORT_DIR)/posix_compliance_report.json
+ @FIRST=true; \
+ for logfile in $(REPORT_DIR)/*.log; do \
+ if [ -f "$$logfile" ]; then \
+ basename=$$(basename "$$logfile" .log); \
+ if [ "$$FIRST" = "true" ]; then \
+ FIRST=false; \
+ else \
+ echo " ," >> $(REPORT_DIR)/posix_compliance_report.json; \
+ fi; \
+ PASSED=$$(awk '/PASS:/{c++} END{print c+0}' "$$logfile" 2>/dev/null || echo 0); \
+ FAILED=$$(awk '/FAIL:/{c++} END{print c+0}' "$$logfile" 2>/dev/null || echo 0); \
+ TOTAL=$$(expr "$$PASSED" + "$$FAILED" 2>/dev/null || echo 0); \
+ echo " \"$$basename\": {" >> $(REPORT_DIR)/posix_compliance_report.json; \
+ echo " \"total_tests\": $$TOTAL," >> $(REPORT_DIR)/posix_compliance_report.json; \
+ echo " \"passed\": $$PASSED," >> $(REPORT_DIR)/posix_compliance_report.json; \
+ echo " \"failed\": $$FAILED," >> $(REPORT_DIR)/posix_compliance_report.json; \
+ if [ "$$TOTAL" -gt 0 ] 2>/dev/null; then \
+ SUCCESS_RATE=$$(awk "BEGIN {printf \"%.2f\", ($$PASSED/$$TOTAL)*100}" 2>/dev/null || echo "0.00"); \
+ else \
+ SUCCESS_RATE="0.00"; \
+ fi; \
+ echo " \"success_rate\": \"$$SUCCESS_RATE%\"" >> $(REPORT_DIR)/posix_compliance_report.json; \
+ echo " }" >> $(REPORT_DIR)/posix_compliance_report.json; \
+ fi; \
+ done
+ @echo " }," >> $(REPORT_DIR)/posix_compliance_report.json
+ @TOTAL_PASSED=0; TOTAL_FAILED=0; \
+ for logfile in $(REPORT_DIR)/*.log; do \
+ if [ -f "$$logfile" ]; then \
+ PASSED=$$(awk '/PASS:/{c++} END{print c+0}' "$$logfile" 2>/dev/null || echo 0); \
+ FAILED=$$(awk '/FAIL:/{c++} END{print c+0}' "$$logfile" 2>/dev/null || echo 0); \
+ TOTAL_PASSED=$$(expr "$$TOTAL_PASSED" + "$$PASSED" 2>/dev/null || echo "$$TOTAL_PASSED"); \
+ TOTAL_FAILED=$$(expr "$$TOTAL_FAILED" + "$$FAILED" 2>/dev/null || echo "$$TOTAL_FAILED"); \
+ fi; \
+ done; \
+ GRAND_TOTAL=$$(expr "$$TOTAL_PASSED" + "$$TOTAL_FAILED" 2>/dev/null || echo 0); \
+ if [ "$$GRAND_TOTAL" -gt 0 ] 2>/dev/null; then \
+ OVERALL_SUCCESS=$$(awk "BEGIN {printf \"%.2f\", ($$TOTAL_PASSED/$$GRAND_TOTAL)*100}" 2>/dev/null || echo "0.00"); \
+ else \
+ OVERALL_SUCCESS="0.00"; \
+ fi; \
+ echo " \"summary\": {" >> $(REPORT_DIR)/posix_compliance_report.json; \
+ echo " \"total_tests\": $$GRAND_TOTAL," >> $(REPORT_DIR)/posix_compliance_report.json; \
+ echo " \"total_passed\": $$TOTAL_PASSED," >> $(REPORT_DIR)/posix_compliance_report.json; \
+ echo " \"total_failed\": $$TOTAL_FAILED," >> $(REPORT_DIR)/posix_compliance_report.json; \
+ echo " \"overall_success_rate\": \"$$OVERALL_SUCCESS%\"" >> $(REPORT_DIR)/posix_compliance_report.json; \
+ echo " }" >> $(REPORT_DIR)/posix_compliance_report.json
+ @echo "}" >> $(REPORT_DIR)/posix_compliance_report.json
+ @echo "$(GREEN)[OK] JSON report generated: $(REPORT_DIR)/posix_compliance_report.json$(RESET)"
+
+# Cleanup and maintenance
+clean:
+ @echo "$(YELLOW)🧹 Cleaning up test artifacts...$(RESET)"
+ @rm -rf $(REPORT_DIR)
+ @rm -rf /tmp/seaweedfs_*_test_*
+ @go clean -testcache
+ @echo "$(GREEN)[OK] Cleanup complete$(RESET)"
+
+clean-external-tools:
+ @echo "$(YELLOW)🧹 Cleaning up external tools...$(RESET)"
+ @rm -rf $(EXTERNAL_TOOLS_DIR)
+
+clean-all: clean clean-external-tools
+
+# Development and debugging
+validate:
+ @echo "$(BLUE)[OK] Validating test files...$(RESET)"
+ @go build -o /dev/null ./...
+ @echo "$(GREEN)[OK] All test files compile successfully$(RESET)"
+
+fmt:
+ @echo "$(BLUE)🎨 Formatting Go code...$(RESET)"
+ @go fmt ./...
+
+lint:
+ @echo "$(BLUE)🔍 Running linter...$(RESET)"
+ @if command -v golangci-lint >/dev/null 2>&1; then \
+ golangci-lint run; \
+ else \
+ echo "$(YELLOW)[WARNING] golangci-lint not found, running go vet instead$(RESET)"; \
+ go vet ./...; \
+ fi
+
+# CI/CD integration
+ci-posix-tests: check-prereqs setup-external-tools
+ @echo "$(CYAN)🚀 Running POSIX tests for CI/CD...$(RESET)"
+ @$(MAKE_SELF) test-posix-critical
+ @$(MAKE_SELF) generate-report
+
+
+
+# Documentation and help
+list-tests:
+ @echo "$(BLUE)📋 Available POSIX test functions:$(RESET)"
+ @grep -r "^func Test" *.go 2>/dev/null | sed 's/.*func \(Test[^(]*\).*/ \1/' | sort || echo "No test files found"
+
+test-info:
+ @echo "$(BLUE)[INFO] POSIX Test Information:$(RESET)"
+ @echo "Test files:"
+ @echo " - $(POSIX_BASIC_TESTS): Core POSIX compliance tests"
+ @echo " - $(POSIX_EXTENDED_TESTS): Extended POSIX feature tests"
+ @echo " - $(POSIX_EXTERNAL_TESTS): External test suite integration"
+ @echo ""
+ @echo "External tools:"
+ @echo " - pjdfstest: Comprehensive POSIX filesystem test suite"
+ @echo " - nfstest: Network filesystem POSIX API verification"
+ @echo " - FIO: Flexible I/O performance testing"
+
+help:
+ @echo "$(CYAN)SeaweedFS POSIX Compliance Testing$(RESET)"
+ @echo "=================================="
+ @echo ""
+ @echo "$(WHITE)Prerequisites:$(RESET)"
+ @echo " $(GREEN)make check-prereqs$(RESET) - Check all prerequisites"
+ @echo " $(GREEN)make setup-external-tools$(RESET) - Setup external POSIX test tools"
+ @echo ""
+ @echo "$(WHITE)Core POSIX Tests:$(RESET)"
+ @echo " $(GREEN)make test-posix-basic$(RESET) - Run basic POSIX compliance tests"
+ @echo " $(GREEN)make test-posix-extended$(RESET) - Run extended POSIX feature tests"
+ @echo " $(GREEN)make test-posix-external$(RESET) - Run external test suite integration"
+ @echo " $(GREEN)make test-posix-full$(RESET) - Run complete POSIX test suite"
+ @echo ""
+ @echo "$(WHITE)Focused Tests:$(RESET)"
+ @echo " $(GREEN)make test-posix-critical$(RESET) - Run critical POSIX compliance tests"
+ @echo " $(GREEN)make test-posix-stress$(RESET) - Run POSIX stress tests"
+ @echo ""
+ @echo "$(WHITE)Performance & Analysis:$(RESET)"
+ @echo " $(GREEN)make benchmark-posix$(RESET) - Run POSIX performance benchmarks"
+ @echo " $(GREEN)make profile-posix$(RESET) - Run tests with performance profiling"
+ @echo " $(GREEN)make coverage-posix$(RESET) - Run tests with coverage analysis"
+ @echo " $(GREEN)make test-fio-posix$(RESET) - Run FIO-based I/O performance tests"
+ @echo ""
+ @echo "$(WHITE)External Test Suites:$(RESET)"
+ @echo " $(GREEN)make test-pjdfstest$(RESET) - Run pjdfstest suite"
+ @echo " $(GREEN)make test-nfstest-posix$(RESET) - Run nfstest_posix"
+ @echo ""
+ @echo "$(WHITE)Reporting:$(RESET)"
+ @echo " $(GREEN)make generate-report$(RESET) - Generate comprehensive compliance report"
+ @echo ""
+ @echo "$(WHITE)Development:$(RESET)"
+ @echo " $(GREEN)make validate$(RESET) - Validate test file compilation"
+ @echo " $(GREEN)make fmt$(RESET) - Format Go code"
+ @echo " $(GREEN)make lint$(RESET) - Run linter"
+ @echo " $(GREEN)make list-tests$(RESET) - List available test functions"
+ @echo ""
+ @echo "$(WHITE)Maintenance:$(RESET)"
+ @echo " $(GREEN)make clean$(RESET) - Clean up test artifacts"
+ @echo " $(GREEN)make clean-all$(RESET) - Clean everything including external tools"
+ @echo ""
+ @echo "$(WHITE)CI/CD Integration:$(RESET)"
+ @echo " $(GREEN)make ci-posix-tests$(RESET) - Run POSIX tests optimized for CI/CD"
+
+
+.PHONY: help check-prereqs check-binary check-fuse check-go setup-reports \
+ setup-external-tools setup-pjdfstest setup-nfstest setup-fio \
+ test-posix-basic test-posix-extended test-posix-external test-posix-full \
+ test-posix-critical test-posix-stress benchmark-posix profile-posix coverage-posix \
+ test-pjdfstest test-nfstest-posix test-fio-posix create-fio-configs \
+ generate-report generate-html-report generate-text-report generate-json-report \
+ clean clean-external-tools clean-all validate fmt lint ci-posix-tests \
+ list-tests test-info
diff --git a/test/fuse_integration/posix_compliance_test.go b/test/fuse_integration/posix_compliance_test.go
new file mode 100644
index 0000000000..6ce17a3a40
--- /dev/null
+++ b/test/fuse_integration/posix_compliance_test.go
@@ -0,0 +1,713 @@
+//go:build !windows
+
+package fuse
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "syscall"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/require"
+)
+
+// POSIXComplianceTestSuite provides comprehensive POSIX compliance testing for FUSE mounts
+type POSIXComplianceTestSuite struct {
+ framework *FuseTestFramework
+ t *testing.T
+}
+
+// NewPOSIXComplianceTestSuite creates a new POSIX compliance test suite
+func NewPOSIXComplianceTestSuite(t *testing.T, framework *FuseTestFramework) *POSIXComplianceTestSuite {
+ return &POSIXComplianceTestSuite{
+ framework: framework,
+ t: t,
+ }
+}
+
+// TestPOSIXCompliance runs all POSIX compliance tests
+func TestPOSIXCompliance(t *testing.T) {
+ config := DefaultTestConfig()
+ config.EnableDebug = true
+ config.MountOptions = []string{"-allowOthers", "-nonempty"}
+
+ framework := NewFuseTestFramework(t, config)
+ defer framework.Cleanup()
+ require.NoError(t, framework.Setup(config))
+
+ suite := NewPOSIXComplianceTestSuite(t, framework)
+
+ // Run all POSIX compliance test categories
+ t.Run("FileOperations", suite.TestFileOperations)
+ t.Run("DirectoryOperations", suite.TestDirectoryOperations)
+ t.Run("SymlinkOperations", suite.TestSymlinkOperations)
+ t.Run("PermissionTests", suite.TestPermissions)
+ t.Run("TimestampTests", suite.TestTimestamps)
+ t.Run("IOOperations", suite.TestIOOperations)
+ t.Run("FileDescriptorTests", suite.TestFileDescriptors)
+ t.Run("AtomicOperations", suite.TestAtomicOperations)
+ t.Run("ConcurrentAccess", suite.TestConcurrentAccess)
+ t.Run("ErrorHandling", suite.TestErrorHandling)
+}
+
+// TestFileOperations tests POSIX file operation compliance
+func (s *POSIXComplianceTestSuite) TestFileOperations(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("CreateFile", func(t *testing.T) {
+ filepath := filepath.Join(mountPoint, "test_create.txt")
+
+ // Test file creation with O_CREAT
+ fd, err := syscall.Open(filepath, syscall.O_CREAT|syscall.O_WRONLY, 0644)
+ require.NoError(t, err)
+ require.Greater(t, fd, 0)
+
+ err = syscall.Close(fd)
+ require.NoError(t, err)
+
+ // Verify file exists
+ _, err = os.Stat(filepath)
+ require.NoError(t, err)
+ })
+
+ t.Run("CreateExclusiveFile", func(t *testing.T) {
+ filepath := filepath.Join(mountPoint, "test_excl.txt")
+
+ // First creation should succeed
+ fd, err := syscall.Open(filepath, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, 0644)
+ require.NoError(t, err)
+ syscall.Close(fd)
+
+ // Second creation should fail with EEXIST
+ _, err = syscall.Open(filepath, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, 0644)
+ require.Error(t, err)
+ require.Equal(t, syscall.EEXIST, err)
+ })
+
+ t.Run("TruncateFile", func(t *testing.T) {
+ filepath := filepath.Join(mountPoint, "test_truncate.txt")
+ content := []byte("Hello, World! This is a test file for truncation.")
+
+ // Create file with content
+ err := os.WriteFile(filepath, content, 0644)
+ require.NoError(t, err)
+
+ // Truncate to 5 bytes
+ err = syscall.Truncate(filepath, 5)
+ require.NoError(t, err)
+
+ // Verify truncation
+ readContent, err := os.ReadFile(filepath)
+ require.NoError(t, err)
+ require.Equal(t, []byte("Hello"), readContent)
+ })
+
+ t.Run("UnlinkFile", func(t *testing.T) {
+ filepath := filepath.Join(mountPoint, "test_unlink.txt")
+
+ // Create file
+ err := os.WriteFile(filepath, []byte("test"), 0644)
+ require.NoError(t, err)
+
+ // Unlink file
+ err = syscall.Unlink(filepath)
+ require.NoError(t, err)
+
+ // Verify file no longer exists
+ _, err = os.Stat(filepath)
+ require.True(t, os.IsNotExist(err))
+ })
+}
+
+// TestDirectoryOperations tests POSIX directory operation compliance
+func (s *POSIXComplianceTestSuite) TestDirectoryOperations(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("CreateDirectory", func(t *testing.T) {
+ dirPath := filepath.Join(mountPoint, "test_mkdir")
+
+ err := syscall.Mkdir(dirPath, 0755)
+ require.NoError(t, err)
+
+ // Verify directory exists and has correct type
+ stat, err := os.Stat(dirPath)
+ require.NoError(t, err)
+ require.True(t, stat.IsDir())
+ })
+
+ t.Run("RemoveDirectory", func(t *testing.T) {
+ dirPath := filepath.Join(mountPoint, "test_rmdir")
+
+ // Create directory
+ err := os.Mkdir(dirPath, 0755)
+ require.NoError(t, err)
+
+ // Remove directory
+ err = syscall.Rmdir(dirPath)
+ require.NoError(t, err)
+
+ // Verify directory no longer exists
+ _, err = os.Stat(dirPath)
+ require.True(t, os.IsNotExist(err))
+ })
+
+ t.Run("RemoveNonEmptyDirectory", func(t *testing.T) {
+ dirPath := filepath.Join(mountPoint, "test_rmdir_nonempty")
+ filePath := filepath.Join(dirPath, "file.txt")
+
+ // Create directory and file
+ err := os.Mkdir(dirPath, 0755)
+ require.NoError(t, err)
+ err = os.WriteFile(filePath, []byte("test"), 0644)
+ require.NoError(t, err)
+
+ // Attempt to remove non-empty directory should fail
+ err = syscall.Rmdir(dirPath)
+ require.Error(t, err)
+ require.Equal(t, syscall.ENOTEMPTY, err)
+ })
+
+ t.Run("RenameDirectory", func(t *testing.T) {
+ oldPath := filepath.Join(mountPoint, "old_dir")
+ newPath := filepath.Join(mountPoint, "new_dir")
+
+ // Create directory
+ err := os.Mkdir(oldPath, 0755)
+ require.NoError(t, err)
+
+ // Rename directory
+ err = os.Rename(oldPath, newPath)
+ require.NoError(t, err)
+
+ // Verify old path doesn't exist and new path does
+ _, err = os.Stat(oldPath)
+ require.True(t, os.IsNotExist(err))
+ stat, err := os.Stat(newPath)
+ require.NoError(t, err)
+ require.True(t, stat.IsDir())
+ })
+}
+
+// TestSymlinkOperations tests POSIX symlink operation compliance
+func (s *POSIXComplianceTestSuite) TestSymlinkOperations(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("CreateSymlink", func(t *testing.T) {
+ targetFile := filepath.Join(mountPoint, "target.txt")
+ linkFile := filepath.Join(mountPoint, "link.txt")
+
+ // Create target file
+ err := os.WriteFile(targetFile, []byte("target content"), 0644)
+ require.NoError(t, err)
+
+ // Create symlink
+ err = os.Symlink(targetFile, linkFile)
+ require.NoError(t, err)
+
+ // Verify symlink properties
+ linkStat, err := os.Lstat(linkFile)
+ require.NoError(t, err)
+ require.Equal(t, os.ModeSymlink, linkStat.Mode()&os.ModeType)
+
+ // Verify symlink content
+ linkTarget, err := os.Readlink(linkFile)
+ require.NoError(t, err)
+ require.Equal(t, targetFile, linkTarget)
+
+ // Verify following symlink works
+ content, err := os.ReadFile(linkFile)
+ require.NoError(t, err)
+ require.Equal(t, []byte("target content"), content)
+ })
+
+ t.Run("BrokenSymlink", func(t *testing.T) {
+ nonexistentTarget := filepath.Join(mountPoint, "nonexistent.txt")
+ linkFile := filepath.Join(mountPoint, "broken_link.txt")
+
+ // Create symlink to nonexistent file
+ err := os.Symlink(nonexistentTarget, linkFile)
+ require.NoError(t, err)
+
+ // Lstat should work (doesn't follow symlink)
+ _, err = os.Lstat(linkFile)
+ require.NoError(t, err)
+
+ // Stat should fail (follows symlink to nonexistent target)
+ _, err = os.Stat(linkFile)
+ require.Error(t, err)
+ require.True(t, os.IsNotExist(err))
+ })
+}
+
+// TestPermissions tests POSIX permission compliance
+func (s *POSIXComplianceTestSuite) TestPermissions(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("FilePermissions", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "perm_test.txt")
+
+ // Create file with specific permissions
+ fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_WRONLY, 0642)
+ require.NoError(t, err)
+ syscall.Close(fd)
+
+ // Verify permissions
+ stat, err := os.Stat(testFile)
+ require.NoError(t, err)
+
+ // Note: The final file permissions are affected by the system's umask.
+ // We check against the requested mode and the mode as affected by a common umask (e.g. 0022).
+ actualMode := stat.Mode() & os.ModePerm
+ expectedMode := os.FileMode(0642)
+ expectedModeWithUmask := os.FileMode(0640) // e.g., 0642 with umask 0002 or 0022
+
+ // Accept either the exact permissions or permissions as modified by umask
+ if actualMode != expectedMode && actualMode != expectedModeWithUmask {
+ t.Errorf("Expected file permissions %o or %o, but got %o",
+ expectedMode, expectedModeWithUmask, actualMode)
+ }
+ })
+
+ t.Run("ChangeFilePermissions", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "chmod_test.txt")
+
+ // Create file
+ err := os.WriteFile(testFile, []byte("test"), 0644)
+ require.NoError(t, err)
+
+ // Change permissions
+ err = os.Chmod(testFile, 0755)
+ require.NoError(t, err)
+
+ // Verify new permissions
+ stat, err := os.Stat(testFile)
+ require.NoError(t, err)
+ require.Equal(t, os.FileMode(0755), stat.Mode()&os.ModePerm)
+ })
+
+ t.Run("DirectoryPermissions", func(t *testing.T) {
+ testDir := filepath.Join(mountPoint, "perm_dir")
+
+ // Create directory with specific permissions
+ err := syscall.Mkdir(testDir, 0750)
+ require.NoError(t, err)
+
+ // Verify permissions
+ stat, err := os.Stat(testDir)
+ require.NoError(t, err)
+ require.Equal(t, os.FileMode(0750)|os.ModeDir, stat.Mode())
+ })
+}
+
+// TestTimestamps tests POSIX timestamp compliance
+func (s *POSIXComplianceTestSuite) TestTimestamps(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("AccessTime", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "atime_test.txt")
+
+ // Create file
+ err := os.WriteFile(testFile, []byte("test content"), 0644)
+ require.NoError(t, err)
+
+ // Get initial timestamps
+ stat1, err := os.Stat(testFile)
+ require.NoError(t, err)
+
+ // Wait a bit to ensure time difference
+ time.Sleep(100 * time.Millisecond)
+
+ // Read file (should update access time)
+ _, err = os.ReadFile(testFile)
+ require.NoError(t, err)
+
+ // Get new timestamps
+ stat2, err := os.Stat(testFile)
+ require.NoError(t, err)
+
+ // Access time should have been updated, and modification time should be unchanged.
+ stat1Sys := stat1.Sys().(*syscall.Stat_t)
+ stat2Sys := stat2.Sys().(*syscall.Stat_t)
+ require.True(t, getAtimeNano(stat2Sys) >= getAtimeNano(stat1Sys), "access time should be updated or stay the same")
+ require.Equal(t, stat1.ModTime(), stat2.ModTime(), "modification time should not change on read")
+ })
+
+ t.Run("ModificationTime", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "mtime_test.txt")
+
+ // Create file
+ err := os.WriteFile(testFile, []byte("initial content"), 0644)
+ require.NoError(t, err)
+
+ // Get initial timestamp
+ stat1, err := os.Stat(testFile)
+ require.NoError(t, err)
+
+ // Wait to ensure time difference
+ time.Sleep(100 * time.Millisecond)
+
+ // Modify file
+ err = os.WriteFile(testFile, []byte("modified content"), 0644)
+ require.NoError(t, err)
+
+ // Get new timestamp
+ stat2, err := os.Stat(testFile)
+ require.NoError(t, err)
+
+ // Modification time should have changed
+ require.True(t, stat2.ModTime().After(stat1.ModTime()))
+ })
+
+ t.Run("SetTimestamps", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "utime_test.txt")
+
+ // Create file
+ err := os.WriteFile(testFile, []byte("test"), 0644)
+ require.NoError(t, err)
+
+ // Set specific timestamps
+ atime := time.Now().Add(-24 * time.Hour)
+ mtime := time.Now().Add(-12 * time.Hour)
+
+ err = os.Chtimes(testFile, atime, mtime)
+ require.NoError(t, err)
+
+ // Verify timestamps were set
+ stat, err := os.Stat(testFile)
+ require.NoError(t, err)
+ require.True(t, stat.ModTime().Equal(mtime))
+ })
+}
+
+// TestIOOperations tests POSIX I/O operation compliance
+func (s *POSIXComplianceTestSuite) TestIOOperations(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("ReadWrite", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "rw_test.txt")
+ testData := []byte("Hello, POSIX World!")
+
+ // Write data
+ fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_WRONLY, 0644)
+ require.NoError(t, err)
+
+ n, err := syscall.Write(fd, testData)
+ require.NoError(t, err)
+ require.Equal(t, len(testData), n)
+
+ err = syscall.Close(fd)
+ require.NoError(t, err)
+
+ // Read data back
+ fd, err = syscall.Open(testFile, syscall.O_RDONLY, 0)
+ require.NoError(t, err)
+
+ readBuffer := make([]byte, len(testData))
+ n, err = syscall.Read(fd, readBuffer)
+ require.NoError(t, err)
+ require.Equal(t, len(testData), n)
+ require.Equal(t, testData, readBuffer)
+
+ err = syscall.Close(fd)
+ require.NoError(t, err)
+ })
+
+ t.Run("Seek", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "seek_test.txt")
+ testData := []byte("0123456789ABCDEF")
+
+ // Create file with test data
+ err := os.WriteFile(testFile, testData, 0644)
+ require.NoError(t, err)
+
+ // Open for reading
+ fd, err := syscall.Open(testFile, syscall.O_RDONLY, 0)
+ require.NoError(t, err)
+ defer syscall.Close(fd)
+
+ // Seek to position 5
+ pos, err := syscall.Seek(fd, 5, 0) // SEEK_SET
+ require.NoError(t, err)
+ require.Equal(t, int64(5), pos)
+
+ // Read 3 bytes
+ buffer := make([]byte, 3)
+ n, err := syscall.Read(fd, buffer)
+ require.NoError(t, err)
+ require.Equal(t, 3, n)
+ require.Equal(t, []byte("567"), buffer)
+
+ // Seek from current position
+ pos, err = syscall.Seek(fd, 2, 1) // SEEK_CUR
+ require.NoError(t, err)
+ require.Equal(t, int64(10), pos)
+
+ // Read 1 byte
+ buffer = make([]byte, 1)
+ n, err = syscall.Read(fd, buffer)
+ require.NoError(t, err)
+ require.Equal(t, 1, n)
+ require.Equal(t, []byte("A"), buffer)
+
+ // Test positioned I/O operations (pread/pwrite)
+ syscall.Close(fd)
+
+ // Open for read/write to test pwrite
+ fd, err = syscall.Open(testFile, syscall.O_RDWR, 0)
+ require.NoError(t, err)
+ defer syscall.Close(fd)
+
+ // Positioned write test
+ writeData := []byte("XYZ")
+ n, err = syscall.Pwrite(fd, writeData, 5) // pwrite at offset 5
+ require.NoError(t, err)
+ require.Equal(t, len(writeData), n)
+
+ // Verify file position is unchanged by pwrite
+ currentPos, err := syscall.Seek(fd, 0, 1) // SEEK_CUR
+ require.NoError(t, err)
+ require.Equal(t, int64(0), currentPos, "file offset should not be changed by pwrite")
+
+ // Read back with pread
+ readBuffer := make([]byte, len(writeData))
+ n, err = syscall.Pread(fd, readBuffer, 5) // pread at offset 5
+ require.NoError(t, err)
+ require.Equal(t, len(writeData), n)
+ require.Equal(t, writeData, readBuffer)
+
+ // Verify file position is still unchanged by pread
+ currentPos, err = syscall.Seek(fd, 0, 1) // SEEK_CUR
+ require.NoError(t, err)
+ require.Equal(t, int64(0), currentPos, "file offset should not be changed by pread")
+ })
+
+ t.Run("AppendMode", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "append_test.txt")
+
+ // Create file with initial content
+ err := os.WriteFile(testFile, []byte("initial"), 0644)
+ require.NoError(t, err)
+
+ // Open in append mode
+ fd, err := syscall.Open(testFile, syscall.O_WRONLY|syscall.O_APPEND, 0)
+ require.NoError(t, err)
+
+ // Write additional content
+ appendData := []byte(" appended")
+ n, err := syscall.Write(fd, appendData)
+ require.NoError(t, err)
+ require.Equal(t, len(appendData), n)
+
+ err = syscall.Close(fd)
+ require.NoError(t, err)
+
+ // Verify content
+ content, err := os.ReadFile(testFile)
+ require.NoError(t, err)
+ require.Equal(t, []byte("initial appended"), content)
+ })
+}
+
+// TestFileDescriptors tests POSIX file descriptor behavior
+func (s *POSIXComplianceTestSuite) TestFileDescriptors(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("DuplicateFileDescriptors", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "dup_test.txt")
+ testData := []byte("duplicate test")
+
+ // Create and open file
+ fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_RDWR, 0644)
+ require.NoError(t, err)
+ defer syscall.Close(fd)
+
+ // Write initial data
+ n, err := syscall.Write(fd, testData)
+ require.NoError(t, err)
+ require.Equal(t, len(testData), n)
+
+ // Duplicate file descriptor
+ dupFd, err := syscall.Dup(fd)
+ require.NoError(t, err)
+ defer syscall.Close(dupFd)
+
+ // Both descriptors should refer to the same file
+ // Seek on one should affect the other
+ pos, err := syscall.Seek(fd, 0, 0) // SEEK_SET
+ require.NoError(t, err)
+ require.Equal(t, int64(0), pos)
+
+ // Read from duplicate descriptor should start from position 0
+ buffer := make([]byte, 9)
+ n, err = syscall.Read(dupFd, buffer)
+ require.NoError(t, err)
+ require.Equal(t, 9, n)
+ require.Equal(t, []byte("duplicate"), buffer)
+ })
+
+ t.Run("FileDescriptorFlags", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "flags_test.txt")
+
+ // Create file
+ err := os.WriteFile(testFile, []byte("test"), 0644)
+ require.NoError(t, err)
+
+ // Open with close-on-exec flag
+ fd, err := syscall.Open(testFile, syscall.O_RDONLY|syscall.O_CLOEXEC, 0)
+ require.NoError(t, err)
+ defer syscall.Close(fd)
+
+ // Verify close-on-exec flag is set
+ // Note: FcntlInt is not available on all platforms, this test may need platform-specific implementation
+ t.Skip("FcntlInt not available on this platform")
+ })
+}
+
+// TestAtomicOperations tests POSIX atomic operation compliance
+func (s *POSIXComplianceTestSuite) TestAtomicOperations(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("AtomicRename", func(t *testing.T) {
+ oldFile := filepath.Join(mountPoint, "atomic_old.txt")
+ newFile := filepath.Join(mountPoint, "atomic_new.txt")
+ testData := []byte("atomic test data")
+
+ // Create source file
+ err := os.WriteFile(oldFile, testData, 0644)
+ require.NoError(t, err)
+
+ // Create existing target file
+ err = os.WriteFile(newFile, []byte("old content"), 0644)
+ require.NoError(t, err)
+
+ // Atomic rename should replace target
+ err = os.Rename(oldFile, newFile)
+ require.NoError(t, err)
+
+ // Verify source no longer exists
+ _, err = os.Stat(oldFile)
+ require.True(t, os.IsNotExist(err))
+
+ // Verify target has new content
+ content, err := os.ReadFile(newFile)
+ require.NoError(t, err)
+ require.Equal(t, testData, content)
+ })
+}
+
+// TestConcurrentAccess tests concurrent access patterns
+func (s *POSIXComplianceTestSuite) TestConcurrentAccess(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("ConcurrentReads", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "concurrent_read.txt")
+ testData := []byte("concurrent read test data")
+
+ // Create test file
+ err := os.WriteFile(testFile, testData, 0644)
+ require.NoError(t, err)
+
+ // Launch multiple concurrent readers
+ const numReaders = 10
+ results := make(chan error, numReaders)
+
+ for i := 0; i < numReaders; i++ {
+ go func() {
+ content, err := os.ReadFile(testFile)
+ if err != nil {
+ results <- err
+ return
+ }
+ if string(content) != string(testData) {
+ results <- fmt.Errorf("content mismatch")
+ return
+ }
+ results <- nil
+ }()
+ }
+
+ // Check all readers succeeded
+ for i := 0; i < numReaders; i++ {
+ err := <-results
+ require.NoError(t, err)
+ }
+ })
+
+ t.Run("ConcurrentFileCreations", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "concurrent_write.txt")
+
+ // Launch multiple concurrent writers
+ const numWriters = 5
+ results := make(chan error, numWriters)
+
+ for i := 0; i < numWriters; i++ {
+ go func(id int) {
+ content := fmt.Sprintf("writer %d data", id)
+ err := os.WriteFile(fmt.Sprintf("%s_%d", testFile, id), []byte(content), 0644)
+ results <- err
+ }(i)
+ }
+
+ // Check all writers succeeded
+ for i := 0; i < numWriters; i++ {
+ err := <-results
+ require.NoError(t, err)
+ }
+
+ // Verify all files were created
+ for i := 0; i < numWriters; i++ {
+ fileName := fmt.Sprintf("%s_%d", testFile, i)
+ _, err := os.Stat(fileName)
+ require.NoError(t, err)
+ }
+ })
+}
+
+// TestErrorHandling tests POSIX error handling compliance
+func (s *POSIXComplianceTestSuite) TestErrorHandling(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("AccessNonexistentFile", func(t *testing.T) {
+ nonexistentFile := filepath.Join(mountPoint, "does_not_exist.txt")
+
+ // Reading nonexistent file should return ENOENT
+ _, err := os.ReadFile(nonexistentFile)
+ require.Error(t, err)
+ require.True(t, os.IsNotExist(err))
+ })
+
+ t.Run("CreateFileInNonexistentDirectory", func(t *testing.T) {
+ fileInNonexistentDir := filepath.Join(mountPoint, "nonexistent_dir", "file.txt")
+
+ // Creating file in nonexistent directory should fail
+ err := os.WriteFile(fileInNonexistentDir, []byte("test"), 0644)
+ require.Error(t, err)
+ require.True(t, os.IsNotExist(err))
+ })
+
+ t.Run("InvalidFileDescriptor", func(t *testing.T) {
+ // Using an invalid file descriptor should return appropriate error
+ buffer := make([]byte, 10)
+ _, err := syscall.Read(999, buffer) // 999 is likely an invalid fd
+ require.Error(t, err)
+ require.Equal(t, syscall.EBADF, err)
+ })
+
+ t.Run("PermissionDenied", func(t *testing.T) {
+ if os.Geteuid() == 0 {
+ t.Skip("running as root: mode bits are not enforced")
+ }
+ testFile := filepath.Join(mountPoint, "readonly.txt")
+
+ // Create read-only file
+ err := os.WriteFile(testFile, []byte("readonly"), 0444)
+ require.NoError(t, err)
+
+ // Attempting to write to read-only file should fail
+ err = os.WriteFile(testFile, []byte("modified"), 0444)
+ require.Error(t, err)
+ require.True(t, os.IsPermission(err))
+ })
+}
diff --git a/test/fuse_integration/posix_extended_test.go b/test/fuse_integration/posix_extended_test.go
new file mode 100644
index 0000000000..7e8cbd7724
--- /dev/null
+++ b/test/fuse_integration/posix_extended_test.go
@@ -0,0 +1,758 @@
+//go:build !windows
+
+package fuse
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "syscall"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// POSIXExtendedTestSuite provides additional POSIX compliance tests
+// covering extended attributes, file locking, and advanced features.
+//
+// NOTE: Some tests in this suite may be skipped or may have different behavior
+// depending on the FUSE implementation and platform support. This is expected
+// behavior for comprehensive POSIX compliance testing.
+//
+// See POSIX_IMPLEMENTATION_ROADMAP.md for a comprehensive plan to implement
+// these missing features.
+type POSIXExtendedTestSuite struct {
+ framework *FuseTestFramework
+ t *testing.T
+}
+
+// ErrorCollector helps collect multiple test errors without stopping execution
+type ErrorCollector struct {
+ errors []string
+ t *testing.T
+}
+
+// NewErrorCollector creates a new error collector
+func NewErrorCollector(t *testing.T) *ErrorCollector {
+ return &ErrorCollector{
+ errors: make([]string, 0),
+ t: t,
+ }
+}
+
+// Add adds an error to the collection
+func (ec *ErrorCollector) Add(format string, args ...interface{}) {
+ ec.errors = append(ec.errors, fmt.Sprintf(format, args...))
+}
+
+// Check checks a condition and adds an error if it fails
+func (ec *ErrorCollector) Check(condition bool, format string, args ...interface{}) {
+ if !condition {
+ ec.Add(format, args...)
+ }
+}
+
+// CheckError checks if an error is nil and adds it if not
+func (ec *ErrorCollector) CheckError(err error, format string, args ...interface{}) {
+ if err != nil {
+ ec.Add(format+": %v", append(args, err)...)
+ }
+}
+
+// HasErrors returns true if any errors were collected
+func (ec *ErrorCollector) HasErrors() bool {
+ return len(ec.errors) > 0
+}
+
+// Report reports all collected errors to the test
+func (ec *ErrorCollector) Report() {
+ if len(ec.errors) > 0 {
+ ec.t.Errorf("Test completed with %d errors:\n%s", len(ec.errors), strings.Join(ec.errors, "\n"))
+ }
+}
+
+// NewPOSIXExtendedTestSuite creates a new extended POSIX compliance test suite
+func NewPOSIXExtendedTestSuite(t *testing.T, framework *FuseTestFramework) *POSIXExtendedTestSuite {
+ return &POSIXExtendedTestSuite{
+ framework: framework,
+ t: t,
+ }
+}
+
+// TestPOSIXExtended runs extended POSIX compliance tests
+func TestPOSIXExtended(t *testing.T) {
+ config := DefaultTestConfig()
+ config.EnableDebug = true
+ config.MountOptions = []string{"-allowOthers", "-nonempty"}
+
+ framework := NewFuseTestFramework(t, config)
+ defer framework.Cleanup()
+
+ // Setup framework with better error handling
+ err := framework.Setup(config)
+ if err != nil {
+ t.Logf("Framework setup failed: %v", err)
+ t.Skip("Skipping extended tests due to framework setup failure")
+ return
+ }
+
+ // Verify framework is working
+ mountPoint := framework.GetMountPoint()
+ if mountPoint == "" {
+ t.Log("Framework mount point is empty")
+ t.Skip("Skipping extended tests due to invalid mount point")
+ return
+ }
+
+ suite := NewPOSIXExtendedTestSuite(t, framework)
+
+ // Run extended POSIX compliance test categories
+ // Use a resilient approach that continues even if individual tests fail
+ testCategories := []struct {
+ name string
+ fn func(*testing.T)
+ }{
+ {"ExtendedAttributes", suite.TestExtendedAttributes},
+ {"FileLocking", suite.TestFileLocking},
+ {"AdvancedIO", suite.TestAdvancedIO},
+ {"SparseFiles", suite.TestSparseFiles},
+ {"LargeFiles", suite.TestLargeFiles},
+ {"MMap", suite.TestMemoryMapping},
+ {"DirectIO", suite.TestDirectIO},
+ {"FileSealing", suite.TestFileSealing},
+ {"Fallocate", suite.TestFallocate},
+ {"Sendfile", suite.TestSendfile},
+ }
+
+ // Run all test categories and collect results
+ var failedCategories []string
+ for _, category := range testCategories {
+ t.Run(category.name, func(subT *testing.T) {
+ // Capture panics and convert them to test failures
+ defer func() {
+ if r := recover(); r != nil {
+ subT.Errorf("Test category %s panicked: %v", category.name, r)
+ failedCategories = append(failedCategories, category.name)
+ }
+ }()
+
+ category.fn(subT)
+ if subT.Failed() {
+ failedCategories = append(failedCategories, category.name)
+ }
+ })
+ }
+
+ // Report overall results
+ if len(failedCategories) > 0 {
+ t.Logf("Extended POSIX tests completed. Failed categories: %v", failedCategories)
+ } else {
+ t.Logf("All extended POSIX test categories passed!")
+ }
+}
+
+// TestExtendedAttributes tests POSIX extended attribute support
+func (s *POSIXExtendedTestSuite) TestExtendedAttributes(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("SetAndGetXattr", func(t *testing.T) {
+ if !isXattrSupported() {
+ t.Skip("Extended attributes not supported on this platform")
+ return
+ }
+
+ testFile := filepath.Join(mountPoint, "xattr_test.txt")
+
+ // Create test file
+ err := os.WriteFile(testFile, []byte("xattr test"), 0644)
+ if !assert.NoError(t, err, "Failed to create test file") {
+ return
+ }
+
+ // Set extended attribute
+ attrName := "user.test_attr"
+ attrValue := []byte("test_value")
+ err = setXattr(testFile, attrName, attrValue, 0)
+ assert.NoError(t, err, "Failed to set extended attribute")
+
+ // Verify attribute was set
+ readValue := make([]byte, 256)
+ size, err := getXattr(testFile, attrName, readValue)
+ assert.NoError(t, err, "Failed to get extended attribute")
+ assert.Equal(t, len(attrValue), size, "Extended attribute size mismatch")
+ assert.Equal(t, attrValue, readValue[:size], "Extended attribute value mismatch")
+ })
+
+ t.Run("ListXattrs", func(t *testing.T) {
+ if !isXattrSupported() {
+ t.Skip("Extended attributes not supported on this platform")
+ return
+ }
+
+ testFile := filepath.Join(mountPoint, "xattr_list_test.txt")
+
+ // Create test file
+ err := os.WriteFile(testFile, []byte("list xattr test"), 0644)
+ if !assert.NoError(t, err, "Failed to create test file") {
+ return
+ }
+
+ // Set multiple extended attributes
+ attrs := map[string][]byte{
+ "user.attr1": []byte("value1"),
+ "user.attr2": []byte("value2"),
+ "user.attr3": []byte("value3"),
+ }
+
+ for name, value := range attrs {
+ err = setXattr(testFile, name, value, 0)
+ assert.NoError(t, err, "Failed to set extended attribute %s", name)
+ }
+
+ // List all attributes
+ listBuf := make([]byte, 1024)
+ size, err := listXattr(testFile, listBuf)
+ assert.NoError(t, err, "Failed to list extended attributes")
+ assert.Greater(t, size, 0, "No extended attributes found")
+
+ // Parse the null-separated list and verify attributes
+ if size > 0 {
+ attrList := parseXattrList(listBuf[:size])
+ expectedAttrs := []string{"user.attr1", "user.attr2", "user.attr3"}
+ for _, expectedAttr := range expectedAttrs {
+ assert.Contains(t, attrList, expectedAttr, "Expected attribute %s should be in the list", expectedAttr)
+ }
+ }
+ })
+
+ t.Run("RemoveXattr", func(t *testing.T) {
+ if !isXattrSupported() {
+ t.Skip("Extended attributes not supported on this platform")
+ return
+ }
+
+ testFile := filepath.Join(mountPoint, "xattr_remove_test.txt")
+
+ // Create test file
+ err := os.WriteFile(testFile, []byte("remove xattr test"), 0644)
+ require.NoError(t, err)
+
+ // Set extended attribute
+ attrName := "user.remove_test"
+ attrValue := []byte("to_be_removed")
+ err = setXattr(testFile, attrName, attrValue, 0)
+ require.NoError(t, err)
+
+ // Verify attribute exists
+ readValue := make([]byte, 256)
+ size, err := getXattr(testFile, attrName, readValue)
+ require.NoError(t, err)
+ require.Equal(t, len(attrValue), size)
+
+ // Remove the attribute
+ err = removeXattr(testFile, attrName)
+ require.NoError(t, err)
+
+ // Verify attribute is gone
+ _, err = getXattr(testFile, attrName, readValue)
+ require.Error(t, err) // Should fail with ENODATA or similar
+ })
+}
+
+// TestFileLocking tests POSIX file locking mechanisms
+// Note: File locking behavior may vary between FUSE implementations.
+// Some implementations may not enforce locks between file descriptors
+// from the same process, which is a known limitation.
+func (s *POSIXExtendedTestSuite) TestFileLocking(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("AdvisoryLocking", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "lock_test.txt")
+
+ // Create test file
+ err := os.WriteFile(testFile, []byte("locking test"), 0644)
+ if !assert.NoError(t, err, "Failed to create test file") {
+ return
+ }
+
+ // Open file
+ file, err := os.OpenFile(testFile, os.O_RDWR, 0644)
+ if !assert.NoError(t, err, "Failed to open test file") {
+ return
+ }
+ defer file.Close()
+
+ // Apply exclusive lock
+ flock := syscall.Flock_t{
+ Type: syscall.F_WRLCK,
+ Whence: 0,
+ Start: 0,
+ Len: 0, // Lock entire file
+ }
+
+ err = syscall.FcntlFlock(file.Fd(), syscall.F_SETLK, &flock)
+ assert.NoError(t, err, "Failed to acquire exclusive lock")
+
+ // Try to lock from another file descriptor (should fail)
+ file2, err := os.OpenFile(testFile, os.O_RDWR, 0644)
+ if !assert.NoError(t, err, "Failed to open second file descriptor") {
+ return
+ }
+ defer file2.Close()
+
+ flock2 := syscall.Flock_t{
+ Type: syscall.F_WRLCK,
+ Whence: 0,
+ Start: 0,
+ Len: 0,
+ }
+
+ // Note: Some FUSE implementations may not enforce file locking between file descriptors
+ // from the same process. This is a known limitation in some FUSE implementations.
+ err = syscall.FcntlFlock(file2.Fd(), syscall.F_SETLK, &flock2)
+ if err == syscall.EAGAIN {
+ // Lock was properly enforced
+ t.Log("File locking properly enforced between file descriptors")
+ } else if err == nil {
+ // Lock was not enforced (common in some FUSE implementations)
+ t.Log("File locking not enforced between file descriptors from same process (FUSE limitation)")
+ } else {
+ // Some other error occurred
+ t.Logf("File locking attempt resulted in error: %v", err)
+ }
+
+ // Log the actual error for debugging
+ t.Logf("File locking test completed with result: %v", err)
+
+ // Release lock
+ flock.Type = syscall.F_UNLCK
+ err = syscall.FcntlFlock(file.Fd(), syscall.F_SETLK, &flock)
+ assert.NoError(t, err, "Failed to release lock")
+
+ // Now second lock should succeed
+ err = syscall.FcntlFlock(file2.Fd(), syscall.F_SETLK, &flock2)
+ assert.NoError(t, err, "Failed to acquire lock after release")
+ })
+
+ t.Run("SharedLocking", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "shared_lock_test.txt")
+
+ // Create test file
+ err := os.WriteFile(testFile, []byte("shared locking test"), 0644)
+ if !assert.NoError(t, err, "Failed to create test file") {
+ return
+ }
+
+ // Open file for reading
+ file1, err := os.Open(testFile)
+ if !assert.NoError(t, err, "Failed to open first file descriptor") {
+ return
+ }
+ defer file1.Close()
+
+ file2, err := os.Open(testFile)
+ if !assert.NoError(t, err, "Failed to open second file descriptor") {
+ return
+ }
+ defer file2.Close()
+
+ // Apply shared locks (should both succeed)
+ flock1 := syscall.Flock_t{
+ Type: syscall.F_RDLCK,
+ Whence: 0,
+ Start: 0,
+ Len: 0,
+ }
+
+ flock2 := syscall.Flock_t{
+ Type: syscall.F_RDLCK,
+ Whence: 0,
+ Start: 0,
+ Len: 0,
+ }
+
+ err = syscall.FcntlFlock(file1.Fd(), syscall.F_SETLK, &flock1)
+ assert.NoError(t, err, "Failed to acquire first shared lock")
+
+ err = syscall.FcntlFlock(file2.Fd(), syscall.F_SETLK, &flock2)
+ assert.NoError(t, err, "Failed to acquire second shared lock")
+ })
+}
+
+// TestAdvancedIO tests advanced I/O operations
+// Note: Vectored I/O support may vary between FUSE implementations.
+// Some implementations may not properly support readv/writev operations.
+func (s *POSIXExtendedTestSuite) TestAdvancedIO(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("ReadWriteV", func(t *testing.T) {
+ if !isVectoredIOSupported() {
+ t.Skip("Vectored I/O not supported on this platform")
+ return
+ }
+
+ testFile := filepath.Join(mountPoint, "readwritev_test.txt")
+
+ // Create file
+ fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_RDWR, 0644)
+ if !assert.NoError(t, err, "Failed to create test file") {
+ return
+ }
+ defer syscall.Close(fd)
+
+ // Prepare test data in multiple buffers
+ writeBuffers := [][]byte{
+ []byte("Hello "),
+ []byte("vectored "),
+ []byte("I/O "),
+ []byte("world!"),
+ }
+
+ // Write using writev
+ writeIOVs := makeIOVecs(writeBuffers)
+ t.Logf("Attempting vectored I/O write with %d buffers", len(writeIOVs))
+ totalWritten, err := writevFile(fd, writeIOVs)
+ if err != nil {
+ // Some FUSE implementations may not support vectored I/O properly
+ t.Logf("Vectored I/O write failed: %v - this may be a FUSE limitation", err)
+ t.Skip("Vectored I/O not properly supported by this FUSE implementation")
+ return
+ }
+
+ expectedTotal := 0
+ for _, buf := range writeBuffers {
+ expectedTotal += len(buf)
+ }
+ assert.Equal(t, expectedTotal, totalWritten, "Vectored write size mismatch")
+
+ // Seek back to beginning
+ _, err = syscall.Seek(fd, 0, 0)
+ if !assert.NoError(t, err, "Failed to seek to beginning") {
+ return
+ }
+
+ // Read using readv into multiple buffers
+ readBuffers := [][]byte{
+ make([]byte, 6), // "Hello "
+ make([]byte, 9), // "vectored "
+ make([]byte, 4), // "I/O "
+ make([]byte, 6), // "world!"
+ }
+
+ readIOVs := makeIOVecs(readBuffers)
+ t.Logf("Attempting vectored I/O read with %d buffers", len(readIOVs))
+ totalRead, err := readvFile(fd, readIOVs)
+ if err != nil {
+ t.Logf("Vectored I/O read failed: %v - this may be a FUSE limitation", err)
+ t.Skip("Vectored I/O not properly supported by this FUSE implementation")
+ return
+ }
+ assert.Equal(t, expectedTotal, totalRead, "Vectored read size mismatch")
+
+ // Verify data matches
+ for i, expected := range writeBuffers {
+ assert.Equal(t, expected, readBuffers[i], "Vectored I/O data mismatch at buffer %d", i)
+ }
+ })
+
+}
+
+// TestSparseFiles tests sparse file handling
+func (s *POSIXExtendedTestSuite) TestSparseFiles(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("CreateSparseFile", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "sparse_test.txt")
+
+ // Open file
+ fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_RDWR, 0644)
+ require.NoError(t, err)
+ defer syscall.Close(fd)
+
+ // Create a sparse file by seeking beyond end and writing
+ const sparseSize = 1024 * 1024 // 1MB
+ _, err = syscall.Seek(fd, sparseSize, 0)
+ require.NoError(t, err)
+
+ // Write at the end
+ endData := []byte("end")
+ n, err := syscall.Write(fd, endData)
+ require.NoError(t, err)
+ require.Equal(t, len(endData), n)
+
+ // Verify file size
+ stat, err := os.Stat(testFile)
+ require.NoError(t, err)
+ require.Equal(t, int64(sparseSize+len(endData)), stat.Size())
+
+ // Read from the beginning (should be zeros)
+ _, err = syscall.Seek(fd, 0, 0)
+ require.NoError(t, err)
+
+ buffer := make([]byte, 100)
+ n, err = syscall.Read(fd, buffer)
+ require.NoError(t, err)
+ require.Equal(t, 100, n)
+
+ // Should be all zeros
+ for _, b := range buffer {
+ require.Equal(t, byte(0), b)
+ }
+ })
+}
+
+// TestLargeFiles tests large file handling (>2GB)
+func (s *POSIXExtendedTestSuite) TestLargeFiles(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("LargeFileOperations", func(t *testing.T) {
+ testFile := filepath.Join(mountPoint, "large_file_test.txt")
+
+ // Open file
+ fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_RDWR, 0644)
+ require.NoError(t, err)
+ defer syscall.Close(fd)
+
+ // Seek to position > 2GB
+ const largeOffset = 3 * 1024 * 1024 * 1024 // 3GB
+ pos, err := syscall.Seek(fd, largeOffset, 0)
+ require.NoError(t, err)
+ require.Equal(t, int64(largeOffset), pos)
+
+ // Write at large offset
+ testData := []byte("large file data")
+ n, err := syscall.Write(fd, testData)
+ require.NoError(t, err)
+ require.Equal(t, len(testData), n)
+
+ // Read back from large offset
+ _, err = syscall.Seek(fd, largeOffset, 0)
+ require.NoError(t, err)
+
+ readBuffer := make([]byte, len(testData))
+ n, err = syscall.Read(fd, readBuffer)
+ require.NoError(t, err)
+ require.Equal(t, len(testData), n)
+ require.Equal(t, testData, readBuffer)
+ })
+}
+
+// TestMemoryMapping tests memory mapping functionality
+func (s *POSIXExtendedTestSuite) TestMemoryMapping(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("MmapFile", func(t *testing.T) {
+ if !isMmapSupported() {
+ t.Skip("Memory mapping not supported on this platform")
+ return
+ }
+
+ testFile := filepath.Join(mountPoint, "mmap_test.txt")
+ testData := make([]byte, 4096)
+ for i := range testData {
+ testData[i] = byte(i % 256)
+ }
+
+ // Create test file
+ err := os.WriteFile(testFile, testData, 0644)
+ require.NoError(t, err)
+
+ // Open file for reading
+ fd, err := syscall.Open(testFile, syscall.O_RDONLY, 0)
+ require.NoError(t, err)
+ defer syscall.Close(fd)
+
+ // Memory map the file
+ mappedData, err := mmapFile(fd, 0, len(testData), PROT_READ, MAP_SHARED)
+ require.NoError(t, err)
+ defer munmapFile(mappedData)
+
+ // Verify mapped content matches original
+ require.Equal(t, testData, mappedData)
+ })
+
+ t.Run("MmapWrite", func(t *testing.T) {
+ if !isMmapSupported() {
+ t.Skip("Memory mapping not supported on this platform")
+ return
+ }
+
+ testFile := filepath.Join(mountPoint, "mmap_write_test.txt")
+ size := 4096
+
+ // Create empty file of specific size
+ fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_RDWR, 0644)
+ require.NoError(t, err)
+ defer syscall.Close(fd)
+
+ err = syscall.Ftruncate(fd, int64(size))
+ require.NoError(t, err)
+
+ // Memory map the file for writing
+ mappedData, err := mmapFile(fd, 0, size, PROT_READ|PROT_WRITE, MAP_SHARED)
+ require.NoError(t, err)
+ defer munmapFile(mappedData)
+
+ // Write test pattern to mapped memory
+ testPattern := []byte("Hello, mmap world!")
+ copy(mappedData, testPattern)
+
+ // Sync changes to disk
+ err = msyncFile(mappedData, MS_SYNC)
+ require.NoError(t, err)
+
+ // Verify changes were written by reading file directly
+ readData, err := os.ReadFile(testFile)
+ require.NoError(t, err)
+ require.True(t, len(readData) >= len(testPattern))
+ require.Equal(t, testPattern, readData[:len(testPattern)])
+ })
+}
+
+// TestDirectIO tests direct I/O operations
+func (s *POSIXExtendedTestSuite) TestDirectIO(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("DirectIO", func(t *testing.T) {
+ if !isDirectIOSupported() {
+ t.Skip("Direct I/O not supported on this platform")
+ return
+ }
+
+ testFile := filepath.Join(mountPoint, "directio_test.txt")
+
+ // Open file with direct I/O
+ fd, err := openDirectIO(testFile, syscall.O_CREAT|syscall.O_RDWR, 0644)
+ require.NoError(t, err)
+ defer syscall.Close(fd)
+
+ // For direct I/O, data must be aligned to sector boundaries
+ // Use 4KB aligned buffer (common sector size)
+ blockSize := 4096
+ testData := make([]byte, blockSize)
+ for i := range testData {
+ testData[i] = byte(i % 256)
+ }
+
+ // Write data using direct I/O
+ n, err := syscall.Write(fd, testData)
+ require.NoError(t, err)
+ require.Equal(t, len(testData), n)
+
+ // Seek back to beginning
+ _, err = syscall.Seek(fd, 0, 0)
+ require.NoError(t, err)
+
+ // Read data using direct I/O
+ readData := make([]byte, blockSize)
+ n, err = syscall.Read(fd, readData)
+ require.NoError(t, err)
+ require.Equal(t, len(testData), n)
+ require.Equal(t, testData, readData)
+ })
+}
+
+// TestFileSealing tests file sealing mechanisms (Linux-specific)
+func (s *POSIXExtendedTestSuite) TestFileSealing(t *testing.T) {
+ // This test is Linux-specific and may not be applicable to all systems
+ t.Skip("File sealing tests require Linux-specific features")
+}
+
+// TestFallocate tests file preallocation
+func (s *POSIXExtendedTestSuite) TestFallocate(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("FallocateSpace", func(t *testing.T) {
+ if !isFallocateSupported() {
+ t.Skip("fallocate not supported on this platform")
+ return
+ }
+
+ testFile := filepath.Join(mountPoint, "fallocate_test.txt")
+
+ // Open file
+ fd, err := syscall.Open(testFile, syscall.O_CREAT|syscall.O_RDWR, 0644)
+ require.NoError(t, err)
+ defer syscall.Close(fd)
+
+ // Preallocate 1MB of space
+ allocSize := int64(1024 * 1024)
+ err = fallocateFile(fd, 0, 0, allocSize)
+ require.NoError(t, err)
+
+ // Verify file size was extended
+ var stat syscall.Stat_t
+ err = syscall.Fstat(fd, &stat)
+ require.NoError(t, err)
+ require.GreaterOrEqual(t, stat.Size, allocSize)
+
+ // Write some data and verify it works
+ testData := []byte("fallocate test data")
+ n, err := syscall.Write(fd, testData)
+ require.NoError(t, err)
+ require.Equal(t, len(testData), n)
+ })
+}
+
+// TestSendfile tests zero-copy file transfer
+func (s *POSIXExtendedTestSuite) TestSendfile(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ t.Run("SendfileCopy", func(t *testing.T) {
+ sourceFile := filepath.Join(mountPoint, "sendfile_source.txt")
+ targetFile := filepath.Join(mountPoint, "sendfile_target.txt")
+
+ testData := make([]byte, 8192)
+ for i := range testData {
+ testData[i] = byte(i % 256)
+ }
+
+ // Create source file
+ err := os.WriteFile(sourceFile, testData, 0644)
+ require.NoError(t, err)
+
+ // Open source for reading
+ srcFd, err := syscall.Open(sourceFile, syscall.O_RDONLY, 0)
+ require.NoError(t, err)
+ defer syscall.Close(srcFd)
+
+ // Create target file
+ dstFd, err := syscall.Open(targetFile, syscall.O_CREAT|syscall.O_WRONLY, 0644)
+ require.NoError(t, err)
+ defer syscall.Close(dstFd)
+
+ // Sendfile test
+ if !isSendfileSupported() {
+ t.Skip("sendfile not supported on this platform")
+ return
+ }
+
+ // Use sendfile to copy data
+ var offset int64 = 0
+ transferred, err := sendfileTransfer(dstFd, srcFd, &offset, len(testData))
+ require.NoError(t, err)
+ require.Equal(t, len(testData), transferred)
+
+ // Verify copy
+ copiedData, err := os.ReadFile(targetFile)
+ require.NoError(t, err)
+ require.Equal(t, testData, copiedData)
+ })
+}
+
+// Helper function to parse null-separated xattr list
+func parseXattrList(data []byte) []string {
+ var attrs []string
+ start := 0
+ for i, b := range data {
+ if b == 0 {
+ if i > start {
+ attrs = append(attrs, string(data[start:i]))
+ }
+ start = i + 1
+ }
+ }
+ return attrs
+}
diff --git a/test/fuse_integration/posix_external_test.go b/test/fuse_integration/posix_external_test.go
new file mode 100644
index 0000000000..2c63fcb929
--- /dev/null
+++ b/test/fuse_integration/posix_external_test.go
@@ -0,0 +1,576 @@
+package fuse
+
+import (
+ "fmt"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "strings"
+ "syscall"
+ "testing"
+ "time"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// ExternalPOSIXTestSuite manages integration with external POSIX test suites
+type ExternalPOSIXTestSuite struct {
+ framework *FuseTestFramework
+ t *testing.T
+ workDir string
+}
+
+// NewExternalPOSIXTestSuite creates a new external POSIX test suite runner
+func NewExternalPOSIXTestSuite(t *testing.T, framework *FuseTestFramework) *ExternalPOSIXTestSuite {
+ workDir, err := os.MkdirTemp("", "posix_external_tests_")
+ require.NoError(t, err)
+
+ return &ExternalPOSIXTestSuite{
+ framework: framework,
+ t: t,
+ workDir: workDir,
+ }
+}
+
+// Cleanup removes temporary test directories
+func (s *ExternalPOSIXTestSuite) Cleanup() {
+ os.RemoveAll(s.workDir)
+}
+
+// TestExternalPOSIXSuites runs integration tests with external POSIX test suites
+func TestExternalPOSIXSuites(t *testing.T) {
+ config := DefaultTestConfig()
+ config.EnableDebug = true
+ config.MountOptions = []string{"-allowOthers", "-nonempty"}
+
+ framework := NewFuseTestFramework(t, config)
+ defer framework.Cleanup()
+ require.NoError(t, framework.Setup(config))
+
+ suite := NewExternalPOSIXTestSuite(t, framework)
+ defer suite.Cleanup()
+
+ // Run various external POSIX test suites
+ t.Run("PjdFsTest", suite.TestPjdFsTest)
+ t.Run("NFSTestPOSIX", suite.TestNFSTestPOSIX)
+ t.Run("LitmusTests", suite.TestLitmusTests)
+ t.Run("CustomPOSIXTests", suite.TestCustomPOSIXTests)
+}
+
+// TestPjdFsTest runs the comprehensive pjdfstest POSIX filesystem test suite
+func (s *ExternalPOSIXTestSuite) TestPjdFsTest(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ // Create test directory within mount point
+ testDir := filepath.Join(mountPoint, "pjdfstest")
+ err := os.MkdirAll(testDir, 0755)
+ require.NoError(t, err)
+
+ // List of critical POSIX operations to test
+ pjdTests := []struct {
+ name string
+ testPath string
+ critical bool
+ }{
+ {"chflags", "tests/chflags", false},
+ {"chmod", "tests/chmod", true},
+ {"chown", "tests/chown", true},
+ {"create", "tests/create", true},
+ {"link", "tests/link", true},
+ {"mkdir", "tests/mkdir", true},
+ {"mkfifo", "tests/mkfifo", false},
+ {"mknod", "tests/mknod", false},
+ {"open", "tests/open", true},
+ {"rename", "tests/rename", true},
+ {"rmdir", "tests/rmdir", true},
+ {"symlink", "tests/symlink", true},
+ {"truncate", "tests/truncate", true},
+ {"unlink", "tests/unlink", true},
+ }
+
+ // Prefer the suite that `make setup-pjdfstest` builds under
+ // external_tools/ before cloning a second copy into the work dir
+ pjdDir := s.findPjdDir()
+ if pjdDir == "" {
+ pjdDir = filepath.Join(s.workDir, "pjdfstest")
+ if err := s.setupPjdFsTest(pjdDir); err != nil {
+ if os.Getenv("POSIX_REQUIRE_EXTERNAL_TOOLS") == "true" {
+ t.Fatalf("pjdfstest is required for comprehensive POSIX compliance testing: %v", err)
+ }
+ t.Skipf("pjdfstest not found and setup failed: %v", err)
+ }
+ }
+
+ // Run each test category
+ for _, test := range pjdTests {
+ t.Run(test.name, func(t *testing.T) {
+ s.runPjdTest(t, pjdDir, test.testPath, testDir, test.critical)
+ })
+ }
+}
+
+// TestNFSTestPOSIX runs nfstest_posix for POSIX API verification
+func (s *ExternalPOSIXTestSuite) TestNFSTestPOSIX(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ // Check if nfstest_posix is available
+ _, err := exec.LookPath("nfstest_posix")
+ if err != nil {
+ // For comprehensive POSIX compliance testing, external tools should be mandatory
+ if os.Getenv("POSIX_REQUIRE_EXTERNAL_TOOLS") == "true" {
+ t.Fatalf("nfstest_posix is required for comprehensive POSIX compliance testing but not found. Install via: pip install nfstest")
+ }
+ t.Skip("nfstest_posix not found. Install via: pip install nfstest")
+ }
+
+ testDir := filepath.Join(mountPoint, "nfstest")
+ err = os.MkdirAll(testDir, 0755)
+ require.NoError(t, err)
+
+ // Run nfstest_posix with comprehensive API testing
+ cmd := exec.Command("nfstest_posix",
+ "--path", testDir,
+ "--verbose",
+ "--createlog",
+ "--runid", fmt.Sprintf("seaweedfs_%d", time.Now().Unix()),
+ )
+
+ output, err := cmd.CombinedOutput()
+ t.Logf("nfstest_posix output:\n%s", string(output))
+
+ if err != nil {
+ t.Logf("nfstest_posix failed: %v", err)
+ // Don't fail the test completely, just log the failure
+ }
+}
+
+// TestLitmusTests runs focused POSIX compliance litmus tests
+func (s *ExternalPOSIXTestSuite) TestLitmusTests(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ // Create litmus test scripts for critical POSIX behaviors
+ litmusTests := []struct {
+ name string
+ script string
+ }{
+ {
+ name: "AtomicRename",
+ script: `#!/bin/bash
+set -e
+cd "$1"
+echo "test data" > temp_file
+echo "original" > target_file
+mv temp_file target_file
+[ "$(cat target_file)" = "test data" ]
+echo "PASS: Atomic rename works"
+`,
+ },
+ {
+ name: "LinkCount",
+ script: `#!/bin/bash
+set -e
+cd "$1"
+echo "test" > original
+ln original hardlink
+if [[ "$(uname)" == "Darwin" ]]; then
+ [ $(stat -f %l original) -eq 2 ]
+else
+ [ $(stat -c %h original) -eq 2 ]
+fi
+rm hardlink
+if [[ "$(uname)" == "Darwin" ]]; then
+ [ $(stat -f %l original) -eq 1 ]
+else
+ [ $(stat -c %h original) -eq 1 ]
+fi
+echo "PASS: Hard link counting works"
+`,
+ },
+ {
+ name: "SymlinkCycles",
+ script: `#!/bin/bash
+set -e
+cd "$1"
+ln -s link1 link2
+ln -s link2 link1
+# Try to access the file, which should fail due to too many symbolic links
+if cat link1 2>/dev/null; then
+ echo "FAIL: Symlink cycle not detected"
+ exit 1
+fi
+echo "PASS: Symlink cycle handling works"
+`,
+ },
+ {
+ name: "ConcurrentCreate",
+ script: `#!/bin/bash
+set -e
+cd "$1"
+for i in {1..10}; do
+ (echo "process $i" > "file_$i") &
+done
+wait
+[ $(ls file_* | wc -l) -eq 10 ]
+echo "PASS: Concurrent file creation works"
+`,
+ },
+ {
+ name: "DirectoryConsistency",
+ script: `#!/bin/bash
+set -e
+cd "$1"
+mkdir testdir
+cd testdir
+touch file1 file2 file3
+cd ..
+entries=$(ls testdir | wc -l)
+[ $entries -eq 3 ]
+rmdir testdir 2>/dev/null && echo "FAIL: Non-empty directory removed" && exit 1
+rm testdir/*
+rmdir testdir
+echo "PASS: Directory consistency works"
+`,
+ },
+ }
+
+ testDir := filepath.Join(mountPoint, "litmus")
+ err := os.MkdirAll(testDir, 0755)
+ require.NoError(t, err)
+
+ // Run each litmus test
+ for _, test := range litmusTests {
+ t.Run(test.name, func(t *testing.T) {
+ s.runLitmusTest(t, test.name, test.script, testDir)
+ })
+ }
+}
+
+// TestCustomPOSIXTests runs custom POSIX compliance tests
+func (s *ExternalPOSIXTestSuite) TestCustomPOSIXTests(t *testing.T) {
+ mountPoint := s.framework.GetMountPoint()
+
+ // Custom stress tests for POSIX compliance
+ t.Run("StressRename", func(t *testing.T) {
+ s.stressTestRename(t, mountPoint)
+ })
+
+ t.Run("StressCreate", func(t *testing.T) {
+ s.stressTestCreate(t, mountPoint)
+ })
+
+ t.Run("StressDirectory", func(t *testing.T) {
+ s.stressTestDirectory(t, mountPoint)
+ })
+
+ t.Run("EdgeCases", func(t *testing.T) {
+ s.testEdgeCases(t, mountPoint)
+ })
+}
+
+// findPjdDir returns a directory holding a built pjdfstest suite, or ""
+// when none is installed. The path is absolute because tests run with
+// cmd.Dir inside the mount.
+func (s *ExternalPOSIXTestSuite) findPjdDir() string {
+ dir := filepath.Join("external_tools", "pjdfstest")
+ if info, err := os.Stat(filepath.Join(dir, "pjdfstest")); err == nil && !info.IsDir() && info.Mode()&0111 != 0 {
+ if abs, err := filepath.Abs(dir); err == nil {
+ return abs
+ }
+ }
+ return ""
+}
+
+// setupPjdFsTest downloads and sets up the pjdfstest suite
+func (s *ExternalPOSIXTestSuite) setupPjdFsTest(targetDir string) error {
+ // Clone pjdfstest repository
+ cmd := exec.Command("git", "clone", "https://github.com/pjd/pjdfstest.git", targetDir)
+ if err := cmd.Run(); err != nil {
+ return fmt.Errorf("failed to clone pjdfstest: %w", err)
+ }
+
+ // pjdfstest is autotools-based: the fresh clone has no Makefile until
+ // configure runs
+ for _, step := range [][]string{
+ {"autoreconf", "-ifs"},
+ {"./configure"},
+ {"make"},
+ } {
+ cmd = exec.Command(step[0], step[1:]...)
+ cmd.Dir = targetDir
+ if out, err := cmd.CombinedOutput(); err != nil {
+ return fmt.Errorf("pjdfstest %s: %w: %s", step[0], err, out)
+ }
+ }
+
+ return nil
+}
+
+// runPjdTest executes a specific pjdfstest
+func (s *ExternalPOSIXTestSuite) runPjdTest(t *testing.T, pjdDir, testPath, mountDir string, critical bool) {
+ fullTestPath := filepath.Join(pjdDir, testPath)
+
+ // Check if test path exists
+ if _, err := os.Stat(fullTestPath); os.IsNotExist(err) {
+ t.Skipf("Test path does not exist: %s", fullTestPath)
+ }
+
+ cmd := exec.Command("prove", "-r", fullTestPath)
+ cmd.Dir = mountDir
+ cmd.Env = append(os.Environ(), "FSTEST="+filepath.Join(pjdDir, "pjdfstest"))
+
+ output, err := cmd.CombinedOutput()
+ t.Logf("pjdfstest %s output:\n%s", testPath, string(output))
+
+ if err != nil {
+ if critical {
+ t.Errorf("Critical pjdfstest failed: %s - %v", testPath, err)
+ } else {
+ t.Logf("Non-critical pjdfstest failed: %s - %v", testPath, err)
+ }
+ }
+}
+
+// runLitmusTest executes a litmus test script
+func (s *ExternalPOSIXTestSuite) runLitmusTest(t *testing.T, name, script, testDir string) {
+ scriptPath := filepath.Join(s.workDir, name+".sh")
+
+ // Write script to file
+ err := os.WriteFile(scriptPath, []byte(script), 0755)
+ require.NoError(t, err)
+
+ // Create isolated test directory
+ isolatedDir := filepath.Join(testDir, name)
+ err = os.MkdirAll(isolatedDir, 0755)
+ require.NoError(t, err)
+ defer os.RemoveAll(isolatedDir)
+
+ // Execute script
+ cmd := exec.Command("bash", scriptPath, isolatedDir)
+ output, err := cmd.CombinedOutput()
+
+ t.Logf("Litmus test %s output:\n%s", name, string(output))
+
+ if err != nil {
+ t.Errorf("Litmus test %s failed: %v", name, err)
+ }
+}
+
+// stressTestRename tests rename operations under stress
+func (s *ExternalPOSIXTestSuite) stressTestRename(t *testing.T, mountPoint string) {
+ testDir := filepath.Join(mountPoint, "stress_rename")
+ err := os.MkdirAll(testDir, 0755)
+ require.NoError(t, err)
+ defer os.RemoveAll(testDir)
+
+ // Create files and rename them concurrently
+ const numFiles = 100
+ const numWorkers = 10
+
+ // Create initial files
+ for i := 0; i < numFiles; i++ {
+ fileName := filepath.Join(testDir, fmt.Sprintf("file_%d.txt", i))
+ err := os.WriteFile(fileName, []byte(fmt.Sprintf("content_%d", i)), 0644)
+ if !assert.NoError(t, err, "Failed to create initial file %d", i) {
+ return // Skip test if setup fails
+ }
+ }
+
+ // Concurrent rename operations
+ results := make(chan error, numWorkers)
+ for w := 0; w < numWorkers; w++ {
+ go func(worker int) {
+ for i := worker; i < numFiles; i += numWorkers {
+ oldName := filepath.Join(testDir, fmt.Sprintf("file_%d.txt", i))
+ newName := filepath.Join(testDir, fmt.Sprintf("renamed_%d.txt", i))
+ err := os.Rename(oldName, newName)
+ if err != nil {
+ results <- err
+ return
+ }
+ }
+ results <- nil
+ }(w)
+ }
+
+ // Wait for all workers and collect errors
+ var errorCount int
+ for w := 0; w < numWorkers; w++ {
+ err := <-results
+ if err != nil {
+ assert.NoError(t, err, "Worker %d failed", w)
+ errorCount++
+ }
+ }
+
+ // Verify all files were renamed (if no errors occurred)
+ if errorCount == 0 {
+ files, err := filepath.Glob(filepath.Join(testDir, "renamed_*.txt"))
+ assert.NoError(t, err, "Failed to list renamed files")
+ assert.Equal(t, numFiles, len(files), "Not all files were renamed successfully")
+ }
+}
+
+// stressTestCreate tests file creation under stress
+func (s *ExternalPOSIXTestSuite) stressTestCreate(t *testing.T, mountPoint string) {
+ testDir := filepath.Join(mountPoint, "stress_create")
+ err := os.MkdirAll(testDir, 0755)
+ require.NoError(t, err)
+ defer os.RemoveAll(testDir)
+
+ const numFiles = 1000
+ const numWorkers = 20
+
+ results := make(chan error, numWorkers)
+
+ for w := 0; w < numWorkers; w++ {
+ go func(worker int) {
+ for i := worker; i < numFiles; i += numWorkers {
+ fileName := filepath.Join(testDir, fmt.Sprintf("stress_%d_%d.txt", worker, i))
+ err := os.WriteFile(fileName, []byte(fmt.Sprintf("data_%d_%d", worker, i)), 0644)
+ if err != nil {
+ results <- err
+ return
+ }
+ }
+ results <- nil
+ }(w)
+ }
+
+ // Wait for all workers
+ for w := 0; w < numWorkers; w++ {
+ err := <-results
+ require.NoError(t, err)
+ }
+
+ // Verify all files were created
+ files, err := filepath.Glob(filepath.Join(testDir, "stress_*.txt"))
+ require.NoError(t, err)
+ require.Equal(t, numFiles, len(files))
+}
+
+// stressTestDirectory tests directory operations under stress
+func (s *ExternalPOSIXTestSuite) stressTestDirectory(t *testing.T, mountPoint string) {
+ testDir := filepath.Join(mountPoint, "stress_directory")
+ err := os.MkdirAll(testDir, 0755)
+ require.NoError(t, err)
+ defer os.RemoveAll(testDir)
+
+ const numDirs = 500
+ const numWorkers = 10
+
+ results := make(chan error, numWorkers)
+
+ // Create directories concurrently
+ for w := 0; w < numWorkers; w++ {
+ go func(worker int) {
+ for i := worker; i < numDirs; i += numWorkers {
+ dirName := filepath.Join(testDir, fmt.Sprintf("dir_%d_%d", worker, i))
+ err := os.MkdirAll(dirName, 0755)
+ if err != nil {
+ results <- err
+ return
+ }
+
+ // Create a file in each directory
+ fileName := filepath.Join(dirName, "test.txt")
+ err = os.WriteFile(fileName, []byte("test"), 0644)
+ if err != nil {
+ results <- err
+ return
+ }
+ }
+ results <- nil
+ }(w)
+ }
+
+ // Wait for all workers
+ for w := 0; w < numWorkers; w++ {
+ err := <-results
+ require.NoError(t, err)
+ }
+
+ // Verify directory structure
+ dirs, err := filepath.Glob(filepath.Join(testDir, "dir_*"))
+ require.NoError(t, err)
+ require.Equal(t, numDirs, len(dirs))
+}
+
+// testEdgeCases tests various POSIX edge cases
+func (s *ExternalPOSIXTestSuite) testEdgeCases(t *testing.T, mountPoint string) {
+ testDir := filepath.Join(mountPoint, "edge_cases")
+ err := os.MkdirAll(testDir, 0755)
+ require.NoError(t, err)
+ defer os.RemoveAll(testDir)
+
+ t.Run("WriteToDirectoryAsFile", func(t *testing.T) {
+ // Test writing to a directory as if it were a file (should fail)
+ // Note: filepath.Join(testDir, "") returns testDir itself
+ err := os.WriteFile(testDir, []byte("test"), 0644)
+ require.Error(t, err)
+ // Verify the error is specifically about the target being a directory
+ var pathErr *os.PathError
+ require.ErrorAs(t, err, &pathErr)
+ require.Equal(t, syscall.EISDIR, pathErr.Err)
+ })
+
+ t.Run("VeryLongFileName", func(t *testing.T) {
+ // Test very long file names
+ longName := strings.Repeat("a", 255) // NAME_MAX is typically 255
+ longFile := filepath.Join(testDir, longName)
+
+ err := os.WriteFile(longFile, []byte("long name test"), 0644)
+ // This might succeed or fail depending on filesystem limits
+ if err == nil {
+ defer os.Remove(longFile)
+ t.Logf("Very long filename accepted")
+ } else {
+ t.Logf("Very long filename rejected: %v", err)
+ }
+ })
+
+ t.Run("SpecialCharacters", func(t *testing.T) {
+ // Test files with special characters
+ specialFiles := []string{
+ "file with spaces.txt",
+ "file-with-dashes.txt",
+ "file_with_underscores.txt",
+ "file.with.dots.txt",
+ }
+
+ for _, fileName := range specialFiles {
+ filePath := filepath.Join(testDir, fileName)
+ err := os.WriteFile(filePath, []byte("special char test"), 0644)
+ require.NoError(t, err, "Failed to create file with special chars: %s", fileName)
+
+ // Verify we can read it back
+ _, err = os.ReadFile(filePath)
+ require.NoError(t, err, "Failed to read file with special chars: %s", fileName)
+ }
+ })
+
+ t.Run("DeepDirectoryNesting", func(t *testing.T) {
+ // Test deep directory nesting
+ deepPath := testDir
+ for i := 0; i < 100; i++ {
+ deepPath = filepath.Join(deepPath, fmt.Sprintf("level_%d", i))
+ }
+
+ err := os.MkdirAll(deepPath, 0755)
+ // This might fail due to PATH_MAX limits
+ if err != nil {
+ t.Logf("Deep directory nesting failed at some level: %v", err)
+ } else {
+ t.Logf("Deep directory nesting succeeded")
+
+ // Test creating a file in the deep path
+ testFile := filepath.Join(deepPath, "deep_file.txt")
+ err = os.WriteFile(testFile, []byte("deep test"), 0644)
+ if err == nil {
+ t.Logf("File creation in deep path succeeded")
+ } else {
+ t.Logf("File creation in deep path failed: %v", err)
+ }
+ }
+ })
+}
diff --git a/test/fuse_integration/sendfile_darwin.go b/test/fuse_integration/sendfile_darwin.go
new file mode 100644
index 0000000000..1042ba7a08
--- /dev/null
+++ b/test/fuse_integration/sendfile_darwin.go
@@ -0,0 +1,45 @@
+//go:build darwin
+
+package fuse
+
+import (
+ "syscall"
+ "unsafe"
+)
+
+// Sendfile support for macOS
+
+func sendfileTransfer(outFd int, inFd int, offset *int64, count int) (int, error) {
+ // macOS sendfile has different signature: sendfile(in_fd, out_fd, offset, len, hdtr, flags)
+ var off int64
+ if offset != nil {
+ off = *offset
+ }
+
+ length := int64(count)
+
+ _, _, errno := syscall.Syscall6(syscall.SYS_SENDFILE,
+ uintptr(inFd), // input fd
+ uintptr(outFd), // output fd
+ uintptr(off), // offset
+ uintptr(unsafe.Pointer(&length)), // length (in/out parameter)
+ 0, // hdtr (headers/trailers)
+ 0) // flags
+
+ if errno != 0 {
+ return 0, errno
+ }
+
+ // Update offset if provided
+ if offset != nil {
+ *offset += length
+ }
+
+ return int(length), nil
+}
+
+func isSendfileSupported() bool {
+ // macOS sendfile(2) requires a socket destination; file-to-file copies
+ // fail with ENOTSOCK.
+ return false
+}
diff --git a/test/fuse_integration/sendfile_linux.go b/test/fuse_integration/sendfile_linux.go
new file mode 100644
index 0000000000..b5a0c0d610
--- /dev/null
+++ b/test/fuse_integration/sendfile_linux.go
@@ -0,0 +1,33 @@
+//go:build linux
+
+package fuse
+
+import (
+ "syscall"
+ "unsafe"
+)
+
+// Sendfile support for Linux
+
+func sendfileTransfer(outFd int, inFd int, offset *int64, count int) (int, error) {
+ var offsetPtr uintptr
+ if offset != nil {
+ offsetPtr = uintptr(unsafe.Pointer(offset))
+ }
+
+ n, _, errno := syscall.Syscall6(syscall.SYS_SENDFILE,
+ uintptr(outFd),
+ uintptr(inFd),
+ offsetPtr,
+ uintptr(count),
+ 0, 0)
+
+ if errno != 0 {
+ return 0, errno
+ }
+ return int(n), nil
+}
+
+func isSendfileSupported() bool {
+ return true
+}
diff --git a/test/fuse_integration/sendfile_unsupported.go b/test/fuse_integration/sendfile_unsupported.go
new file mode 100644
index 0000000000..64a7818dd2
--- /dev/null
+++ b/test/fuse_integration/sendfile_unsupported.go
@@ -0,0 +1,19 @@
+//go:build !linux && !darwin
+
+package fuse
+
+import (
+ "errors"
+)
+
+// Sendfile support for unsupported platforms
+
+var ErrSendfileNotSupported = errors.New("sendfile not supported on this platform")
+
+func sendfileTransfer(outFd int, inFd int, offset *int64, count int) (int, error) {
+ return 0, ErrSendfileNotSupported
+}
+
+func isSendfileSupported() bool {
+ return false
+}
diff --git a/test/fuse_integration/vectored_io_unix.go b/test/fuse_integration/vectored_io_unix.go
new file mode 100644
index 0000000000..59b2b2cc47
--- /dev/null
+++ b/test/fuse_integration/vectored_io_unix.go
@@ -0,0 +1,126 @@
+//go:build unix
+
+package fuse
+
+import (
+ "syscall"
+ "unsafe"
+)
+
+// Vectored I/O support for Unix-like systems
+
+// IOVec represents an I/O vector for readv/writev operations
+type IOVec struct {
+ Base *byte
+ Len uint64
+}
+
+func readvFile(fd int, iovs []IOVec) (int, error) {
+ if len(iovs) == 0 {
+ return 0, nil
+ }
+
+ n, _, errno := syscall.Syscall(syscall.SYS_READV,
+ uintptr(fd),
+ uintptr(unsafe.Pointer(&iovs[0])),
+ uintptr(len(iovs)))
+
+ if errno != 0 {
+ return 0, errno
+ }
+ return int(n), nil
+}
+
+func writevFile(fd int, iovs []IOVec) (int, error) {
+ if len(iovs) == 0 {
+ return 0, nil
+ }
+
+ n, _, errno := syscall.Syscall(syscall.SYS_WRITEV,
+ uintptr(fd),
+ uintptr(unsafe.Pointer(&iovs[0])),
+ uintptr(len(iovs)))
+
+ if errno != 0 {
+ return 0, errno
+ }
+ return int(n), nil
+}
+
+func preadvFile(fd int, iovs []IOVec, offset int64) (int, error) {
+ if len(iovs) == 0 {
+ return 0, nil
+ }
+
+ // preadv/pwritev may not be available on all Unix systems
+ // Fall back to individual pread calls
+ totalRead := 0
+ currentOffset := offset
+
+ for _, iov := range iovs {
+ if iov.Len == 0 {
+ continue
+ }
+
+ buf := (*[1 << 30]byte)(unsafe.Pointer(iov.Base))[:iov.Len:iov.Len]
+ n, err := syscall.Pread(fd, buf, currentOffset)
+ if err != nil {
+ return totalRead, err
+ }
+ totalRead += n
+ currentOffset += int64(n)
+
+ if n < int(iov.Len) {
+ break // EOF or partial read
+ }
+ }
+
+ return totalRead, nil
+}
+
+func pwritevFile(fd int, iovs []IOVec, offset int64) (int, error) {
+ if len(iovs) == 0 {
+ return 0, nil
+ }
+
+ // preadv/pwritev may not be available on all Unix systems
+ // Fall back to individual pwrite calls
+ totalWritten := 0
+ currentOffset := offset
+
+ for _, iov := range iovs {
+ if iov.Len == 0 {
+ continue
+ }
+
+ buf := (*[1 << 30]byte)(unsafe.Pointer(iov.Base))[:iov.Len:iov.Len]
+ n, err := syscall.Pwrite(fd, buf, currentOffset)
+ if err != nil {
+ return totalWritten, err
+ }
+ totalWritten += n
+ currentOffset += int64(n)
+
+ if n < int(iov.Len) {
+ break // Partial write
+ }
+ }
+
+ return totalWritten, nil
+}
+
+// Helper function to create IOVec from byte slices
+func makeIOVecs(buffers [][]byte) []IOVec {
+ iovs := make([]IOVec, len(buffers))
+ for i, buf := range buffers {
+ if len(buf) > 0 {
+ iovs[i].Base = &buf[0]
+ iovs[i].Len = uint64(len(buf))
+ }
+ }
+ return iovs
+}
+
+func isVectoredIOSupported() bool {
+ return true
+}
diff --git a/test/fuse_integration/vectored_io_unsupported.go b/test/fuse_integration/vectored_io_unsupported.go
new file mode 100644
index 0000000000..963c6e2d4a
--- /dev/null
+++ b/test/fuse_integration/vectored_io_unsupported.go
@@ -0,0 +1,41 @@
+//go:build !unix
+
+package fuse
+
+import (
+ "errors"
+)
+
+// Vectored I/O support for unsupported platforms
+
+var ErrVectoredIONotSupported = errors.New("vectored I/O not supported on this platform")
+
+// IOVec represents an I/O vector for readv/writev operations
+type IOVec struct {
+ Base *byte
+ Len uint64
+}
+
+func readvFile(fd int, iovs []IOVec) (int, error) {
+ return 0, ErrVectoredIONotSupported
+}
+
+func writevFile(fd int, iovs []IOVec) (int, error) {
+ return 0, ErrVectoredIONotSupported
+}
+
+func preadvFile(fd int, iovs []IOVec, offset int64) (int, error) {
+ return 0, ErrVectoredIONotSupported
+}
+
+func pwritevFile(fd int, iovs []IOVec, offset int64) (int, error) {
+ return 0, ErrVectoredIONotSupported
+}
+
+func makeIOVecs(buffers [][]byte) []IOVec {
+ return nil
+}
+
+func isVectoredIOSupported() bool {
+ return false
+}
diff --git a/test/fuse_integration/xattr_darwin.go b/test/fuse_integration/xattr_darwin.go
new file mode 100644
index 0000000000..6bd6f2a908
--- /dev/null
+++ b/test/fuse_integration/xattr_darwin.go
@@ -0,0 +1,125 @@
+//go:build darwin
+
+package fuse
+
+import (
+ "syscall"
+ "unsafe"
+)
+
+// Extended attributes support for macOS
+
+const (
+ // macOS-specific flags
+ XATTR_NOFOLLOW = 0x0001
+ XATTR_CREATE = 0x0002
+ XATTR_REPLACE = 0x0004
+)
+
+func setXattr(path, name string, value []byte, flags int) error {
+ pathBytes, err := syscall.BytePtrFromString(path)
+ if err != nil {
+ return err
+ }
+ nameBytes, err := syscall.BytePtrFromString(name)
+ if err != nil {
+ return err
+ }
+
+ var valuePtr unsafe.Pointer
+ if len(value) > 0 {
+ valuePtr = unsafe.Pointer(&value[0])
+ }
+
+ _, _, errno := syscall.Syscall6(syscall.SYS_SETXATTR,
+ uintptr(unsafe.Pointer(pathBytes)),
+ uintptr(unsafe.Pointer(nameBytes)),
+ uintptr(valuePtr),
+ uintptr(len(value)),
+ uintptr(0), // position (not used for regular files)
+ uintptr(flags))
+
+ if errno != 0 {
+ return errno
+ }
+ return nil
+}
+
+func getXattr(path, name string, value []byte) (int, error) {
+ pathBytes, err := syscall.BytePtrFromString(path)
+ if err != nil {
+ return 0, err
+ }
+ nameBytes, err := syscall.BytePtrFromString(name)
+ if err != nil {
+ return 0, err
+ }
+
+ var valuePtr unsafe.Pointer
+ if len(value) > 0 {
+ valuePtr = unsafe.Pointer(&value[0])
+ }
+
+ size, _, errno := syscall.Syscall6(syscall.SYS_GETXATTR,
+ uintptr(unsafe.Pointer(pathBytes)),
+ uintptr(unsafe.Pointer(nameBytes)),
+ uintptr(valuePtr),
+ uintptr(len(value)),
+ uintptr(0), // position
+ uintptr(0)) // options
+
+ if errno != 0 {
+ return 0, errno
+ }
+ return int(size), nil
+}
+
+func listXattr(path string, list []byte) (int, error) {
+ pathBytes, err := syscall.BytePtrFromString(path)
+ if err != nil {
+ return 0, err
+ }
+
+ var listPtr unsafe.Pointer
+ if len(list) > 0 {
+ listPtr = unsafe.Pointer(&list[0])
+ }
+
+ size, _, errno := syscall.Syscall6(syscall.SYS_LISTXATTR,
+ uintptr(unsafe.Pointer(pathBytes)),
+ uintptr(listPtr),
+ uintptr(len(list)),
+ uintptr(0), // options
+ 0, 0)
+
+ if errno != 0 {
+ return 0, errno
+ }
+ return int(size), nil
+}
+
+func removeXattr(path, name string) error {
+ pathBytes, err := syscall.BytePtrFromString(path)
+ if err != nil {
+ return err
+ }
+ nameBytes, err := syscall.BytePtrFromString(name)
+ if err != nil {
+ return err
+ }
+
+ _, _, errno := syscall.Syscall6(syscall.SYS_REMOVEXATTR,
+ uintptr(unsafe.Pointer(pathBytes)),
+ uintptr(unsafe.Pointer(nameBytes)),
+ uintptr(0), // options
+ 0, 0, 0)
+
+ if errno != 0 {
+ return errno
+ }
+ return nil
+}
+
+func isXattrSupported() bool {
+ return true
+}
diff --git a/test/fuse_integration/xattr_linux.go b/test/fuse_integration/xattr_linux.go
new file mode 100644
index 0000000000..82f40d1c7f
--- /dev/null
+++ b/test/fuse_integration/xattr_linux.go
@@ -0,0 +1,115 @@
+//go:build linux
+
+package fuse
+
+import (
+ "syscall"
+ "unsafe"
+)
+
+// Extended attributes support for Linux
+
+func setXattr(path, name string, value []byte, flags int) error {
+ pathBytes, err := syscall.BytePtrFromString(path)
+ if err != nil {
+ return err
+ }
+ nameBytes, err := syscall.BytePtrFromString(name)
+ if err != nil {
+ return err
+ }
+
+ var valuePtr unsafe.Pointer
+ if len(value) > 0 {
+ valuePtr = unsafe.Pointer(&value[0])
+ }
+
+ _, _, errno := syscall.Syscall6(syscall.SYS_SETXATTR,
+ uintptr(unsafe.Pointer(pathBytes)),
+ uintptr(unsafe.Pointer(nameBytes)),
+ uintptr(valuePtr),
+ uintptr(len(value)),
+ uintptr(flags),
+ 0)
+
+ if errno != 0 {
+ return errno
+ }
+ return nil
+}
+
+func getXattr(path, name string, value []byte) (int, error) {
+ pathBytes, err := syscall.BytePtrFromString(path)
+ if err != nil {
+ return 0, err
+ }
+ nameBytes, err := syscall.BytePtrFromString(name)
+ if err != nil {
+ return 0, err
+ }
+
+ var valuePtr unsafe.Pointer
+ if len(value) > 0 {
+ valuePtr = unsafe.Pointer(&value[0])
+ }
+
+ size, _, errno := syscall.Syscall6(syscall.SYS_GETXATTR,
+ uintptr(unsafe.Pointer(pathBytes)),
+ uintptr(unsafe.Pointer(nameBytes)),
+ uintptr(valuePtr),
+ uintptr(len(value)),
+ 0,
+ 0)
+
+ if errno != 0 {
+ return 0, errno
+ }
+ return int(size), nil
+}
+
+func listXattr(path string, list []byte) (int, error) {
+ pathBytes, err := syscall.BytePtrFromString(path)
+ if err != nil {
+ return 0, err
+ }
+
+ var listPtr unsafe.Pointer
+ if len(list) > 0 {
+ listPtr = unsafe.Pointer(&list[0])
+ }
+
+ size, _, errno := syscall.Syscall(syscall.SYS_LISTXATTR,
+ uintptr(unsafe.Pointer(pathBytes)),
+ uintptr(listPtr),
+ uintptr(len(list)))
+
+ if errno != 0 {
+ return 0, errno
+ }
+ return int(size), nil
+}
+
+func removeXattr(path, name string) error {
+ pathBytes, err := syscall.BytePtrFromString(path)
+ if err != nil {
+ return err
+ }
+ nameBytes, err := syscall.BytePtrFromString(name)
+ if err != nil {
+ return err
+ }
+
+ _, _, errno := syscall.Syscall(syscall.SYS_REMOVEXATTR,
+ uintptr(unsafe.Pointer(pathBytes)),
+ uintptr(unsafe.Pointer(nameBytes)),
+ 0)
+
+ if errno != 0 {
+ return errno
+ }
+ return nil
+}
+
+func isXattrSupported() bool {
+ return true
+}
diff --git a/test/fuse_integration/xattr_unsupported.go b/test/fuse_integration/xattr_unsupported.go
new file mode 100644
index 0000000000..945fc2ff69
--- /dev/null
+++ b/test/fuse_integration/xattr_unsupported.go
@@ -0,0 +1,31 @@
+//go:build !linux && !darwin
+
+package fuse
+
+import (
+ "errors"
+)
+
+// Extended attributes support for unsupported platforms
+
+var ErrXattrNotSupported = errors.New("extended attributes not supported on this platform")
+
+func setXattr(path, name string, value []byte, flags int) error {
+ return ErrXattrNotSupported
+}
+
+func getXattr(path, name string, value []byte) (int, error) {
+ return 0, ErrXattrNotSupported
+}
+
+func listXattr(path string, list []byte) (int, error) {
+ return 0, ErrXattrNotSupported
+}
+
+func removeXattr(path, name string) error {
+ return ErrXattrNotSupported
+}
+
+func isXattrSupported() bool {
+ return false
+}
diff --git a/test/kafka/simple-publisher/go.mod b/test/kafka/simple-publisher/go.mod
index 09309f0f23..de391da489 100644
--- a/test/kafka/simple-publisher/go.mod
+++ b/test/kafka/simple-publisher/go.mod
@@ -1,10 +1,10 @@
module simple-publisher
-go 1.21
+go 1.25
require github.com/segmentio/kafka-go v0.4.47
require (
- github.com/klauspost/compress v1.17.0 // indirect
+ github.com/klauspost/compress v1.20.3 // indirect
github.com/pierrec/lz4/v4 v4.1.15 // indirect
)
diff --git a/test/kafka/simple-publisher/go.sum b/test/kafka/simple-publisher/go.sum
index c9f731f2bb..82af443f27 100644
--- a/test/kafka/simple-publisher/go.sum
+++ b/test/kafka/simple-publisher/go.sum
@@ -2,8 +2,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU=
-github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM=
-github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
+github.com/klauspost/compress v1.20.3 h1:zWNoFqq/lSuYQRim3dUXlXeA0IqwuZGicdG5ZTfY5j4=
+github.com/klauspost/compress v1.20.3/go.mod h1:LUdAzn7YLVvxLpc7y3V1m40wESHTgc1422pwwBSKYuI=
github.com/pierrec/lz4/v4 v4.1.15 h1:MO0/ucJhngq7299dKLwIMtgTfbkoSPF6AoMYDd8Q4q0=
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
diff --git a/test/s3/versioning/s3_listing_regression_test.go b/test/s3/versioning/s3_listing_regression_test.go
new file mode 100644
index 0000000000..169c8511bb
--- /dev/null
+++ b/test/s3/versioning/s3_listing_regression_test.go
@@ -0,0 +1,112 @@
+package s3api
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "sort"
+ "testing"
+
+ "github.com/aws/aws-sdk-go-v2/aws"
+ "github.com/aws/aws-sdk-go-v2/service/s3"
+ "github.com/aws/aws-sdk-go-v2/service/s3/types"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+// Check both wire APIs with small pages. The directory walker alone does
+// not exercise prefix normalization, delimiter-marker adjustment, or tokens.
+func TestListingPrefixAndDelimiterPagination(t *testing.T) {
+ client := getS3Client(t)
+ ctx := context.Background()
+ for _, versioning := range []string{"", "Enabled", "Suspended"} {
+ t.Run("versioning="+versioning, func(t *testing.T) {
+ bucket := getNewBucketName()
+ createBucket(t, client, bucket)
+ defer deleteBucket(t, client, bucket)
+ if versioning != "" {
+ enableVersioning(t, client, bucket)
+ if versioning == "Suspended" {
+ _, err := client.PutBucketVersioning(ctx, &s3.PutBucketVersioningInput{
+ Bucket: aws.String(bucket), VersioningConfiguration: &types.VersioningConfiguration{Status: types.BucketVersioningStatusSuspended},
+ })
+ require.NoError(t, err)
+ }
+ }
+ for _, key := range []string{"a", "a-c", "a.txt", "a/b", "foo_bar", "foo_bar_baz", "foo_baz"} {
+ _, err := client.PutObject(ctx, &s3.PutObjectInput{Bucket: aws.String(bucket), Key: aws.String(key), Body: bytes.NewReader([]byte("x"))})
+ require.NoError(t, err)
+ }
+ for _, tc := range []struct {
+ name, prefix, delimiter, startAfter string
+ keys, prefixes []string
+ }{
+ {"trailing slash", "a/", "", "", []string{"a/b"}, nil},
+ {"trailing slash custom delimiter", "a/", "_", "", []string{"a/b"}, nil},
+ {"custom delimiter", "foo", "_", "", nil, []string{"foo_"}},
+ {"custom delimiter nested group", "foo_", "_", "", []string{"foo_bar", "foo_baz"}, []string{"foo_bar_"}},
+ {"marker names directory", "a", "", "a", []string{"a-c", "a.txt", "a/b"}, nil},
+ {"marker between name and slash", "a", "", "a.txt", []string{"a/b"}, nil},
+ } {
+ for _, v2 := range []bool{false, true} {
+ for _, pageSize := range []int32{1, 2, 1000} {
+ t.Run(fmt.Sprintf("%s/v2=%t/max=%d", tc.name, v2, pageSize), func(t *testing.T) {
+ var keys, prefixes []string
+ marker, token := tc.startAfter, ""
+ for page := 0; ; page++ {
+ require.Less(t, page, 10, "listing must terminate without repeating a prefix")
+ var objects []types.Object
+ var commonPrefixes []types.CommonPrefix
+ var truncated bool
+ if v2 {
+ in := &s3.ListObjectsV2Input{Bucket: aws.String(bucket), Prefix: aws.String(tc.prefix), Delimiter: aws.String(tc.delimiter), MaxKeys: aws.Int32(pageSize)}
+ if token != "" {
+ in.ContinuationToken = aws.String(token)
+ } else if marker != "" {
+ in.StartAfter = aws.String(marker)
+ }
+ out, err := client.ListObjectsV2(ctx, in)
+ require.NoError(t, err)
+ objects, commonPrefixes, truncated = out.Contents, out.CommonPrefixes, aws.ToBool(out.IsTruncated)
+ next := aws.ToString(out.NextContinuationToken)
+ if truncated {
+ require.NotEmpty(t, next)
+ require.NotEqual(t, token, next, "token must advance")
+ }
+ token = next
+ } else {
+ out, err := client.ListObjects(ctx, &s3.ListObjectsInput{
+ Bucket: aws.String(bucket), Prefix: aws.String(tc.prefix), Delimiter: aws.String(tc.delimiter), MaxKeys: aws.Int32(pageSize), Marker: aws.String(marker),
+ })
+ require.NoError(t, err)
+ objects, commonPrefixes, truncated = out.Contents, out.CommonPrefixes, aws.ToBool(out.IsTruncated)
+ next := aws.ToString(out.NextMarker)
+ if truncated {
+ if next == "" && len(objects) > 0 {
+ next = aws.ToString(objects[len(objects)-1].Key)
+ }
+ require.Greater(t, next, marker, "marker must advance")
+ }
+ marker = next
+ }
+ for _, object := range objects {
+ keys = append(keys, aws.ToString(object.Key))
+ }
+ for _, prefix := range commonPrefixes {
+ prefixes = append(prefixes, aws.ToString(prefix.Prefix))
+ }
+ if !truncated {
+ break
+ }
+ }
+ sort.Strings(keys)
+ sort.Strings(prefixes)
+ assert.Equal(t, tc.keys, keys)
+ assert.Equal(t, tc.prefixes, prefixes)
+ })
+ }
+ }
+ }
+ })
+ }
+}
diff --git a/weed/admin/handlers/file_browser_grpc.go b/weed/admin/handlers/file_browser_grpc.go
index 0b2f9eb98d..9239688ed0 100644
--- a/weed/admin/handlers/file_browser_grpc.go
+++ b/weed/admin/handlers/file_browser_grpc.go
@@ -153,6 +153,9 @@ func (h *FileBrowserHandlers) uploadFileGrpc(ctx context.Context, filePath strin
ChunkSize: adminUploadChunkSize,
MimeType: mimeType,
AssignFunc: assignFunc,
+ LookupJwtFunc: func(lookupCtx context.Context, fileId string) security.EncodedJwt {
+ return operation.LookupJwt(lookupCtx, h.adminServer.GetMasterClient().GetMaster(lookupCtx), h.adminServer.GetGrpcDialOption(), fileId)
+ },
})
if err != nil {
// Partial chunks come back even on error so we can clean them up rather
diff --git a/weed/admin/plugin/plugin_scheduler.go b/weed/admin/plugin/plugin_scheduler.go
index aa97e92737..58ad878d6a 100644
--- a/weed/admin/plugin/plugin_scheduler.go
+++ b/weed/admin/plugin/plugin_scheduler.go
@@ -12,6 +12,7 @@ import (
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
+ "google.golang.org/protobuf/proto"
"google.golang.org/protobuf/types/known/timestamppb"
)
@@ -614,7 +615,7 @@ func deriveSchedulerAdminRuntime(
descriptor *plugin_pb.JobTypeDescriptor,
) *plugin_pb.AdminRuntimeConfig {
if cfg != nil && cfg.AdminRuntime != nil {
- adminConfig := *cfg.AdminRuntime
+ adminConfig := proto.Clone(cfg.AdminRuntime).(*plugin_pb.AdminRuntimeConfig)
// Overlay descriptor defaults for any zero numeric fields. Persisted
// configs from older versions have no execution_timeout_seconds, and
// without this overlay the scheduler would fall back to the 90s
@@ -646,7 +647,7 @@ func deriveSchedulerAdminRuntime(
adminConfig.ExecutionTimeoutSeconds = defaults.ExecutionTimeoutSeconds
}
}
- return &adminConfig
+ return adminConfig
}
if descriptor == nil || descriptor.AdminRuntimeDefaults == nil {
diff --git a/weed/admin/plugin/registry.go b/weed/admin/plugin/registry.go
index 2201e6a9c5..6e27d30411 100644
--- a/weed/admin/plugin/registry.go
+++ b/weed/admin/plugin/registry.go
@@ -8,6 +8,7 @@ import (
"time"
"github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
+ "google.golang.org/protobuf/proto"
)
const defaultWorkerStaleTimeout = 2 * time.Minute
@@ -495,36 +496,35 @@ func cloneJobTypeCapability(in *plugin_pb.JobTypeCapability) *plugin_pb.JobTypeC
if in == nil {
return nil
}
- out := *in
- return &out
+ return proto.Clone(in).(*plugin_pb.JobTypeCapability)
}
func cloneWorkerHeartbeat(in *plugin_pb.WorkerHeartbeat) *plugin_pb.WorkerHeartbeat {
if in == nil {
return nil
}
- out := *in
+ out := proto.Clone(in).(*plugin_pb.WorkerHeartbeat)
if in.RunningWork != nil {
- out.RunningWork = make([]*plugin_pb.RunningWork, 0, len(in.RunningWork))
- for _, rw := range in.RunningWork {
- if rw == nil {
- continue
+ running := out.RunningWork[:0]
+ // Clone can turn nil messages into empty messages, so filter using
+ // their original positions while reusing the cloned entries.
+ for i, work := range in.RunningWork {
+ if work != nil {
+ running = append(running, out.RunningWork[i])
}
- clone := *rw
- out.RunningWork = append(out.RunningWork, &clone)
}
- }
- if in.QueuedJobsByType != nil {
- out.QueuedJobsByType = make(map[string]int32, len(in.QueuedJobsByType))
- for k, v := range in.QueuedJobsByType {
- out.QueuedJobsByType[k] = v
+ clear(out.RunningWork[len(running):])
+ if running == nil {
+ running = []*plugin_pb.RunningWork{}
}
+ out.RunningWork = running
}
- if in.Metadata != nil {
- out.Metadata = make(map[string]string, len(in.Metadata))
- for k, v := range in.Metadata {
- out.Metadata[k] = v
- }
+ // Preserve writable empty maps without rebuilding populated clones.
+ if in.QueuedJobsByType != nil && out.QueuedJobsByType == nil {
+ out.QueuedJobsByType = make(map[string]int32)
}
- return &out
+ if in.Metadata != nil && out.Metadata == nil {
+ out.Metadata = make(map[string]string)
+ }
+ return out
}
diff --git a/weed/admin/plugin/registry_snapshot_test.go b/weed/admin/plugin/registry_snapshot_test.go
new file mode 100644
index 0000000000..1546096048
--- /dev/null
+++ b/weed/admin/plugin/registry_snapshot_test.go
@@ -0,0 +1,65 @@
+package plugin
+
+import (
+ "testing"
+
+ "github.com/seaweedfs/seaweedfs/weed/pb/plugin_pb"
+)
+
+func TestCloneWorkerHeartbeatFiltersNilAndIsolatesState(t *testing.T) {
+ t.Parallel()
+ source := &plugin_pb.WorkerHeartbeat{
+ RunningWork: []*plugin_pb.RunningWork{nil, {WorkId: "first"}, nil, {WorkId: "second"}, nil},
+ QueuedJobsByType: map[string]int32{"vacuum": 2},
+ Metadata: map[string]string{"rack": "rack-a"},
+ }
+ got := cloneWorkerHeartbeat(source)
+ if len(got.RunningWork) != 2 || got.RunningWork[0].GetWorkId() != "first" || got.RunningWork[1].GetWorkId() != "second" {
+ t.Fatalf("unexpected filtered work: %v", got.RunningWork)
+ }
+ got.RunningWork[0].WorkId = "changed"
+ got.QueuedJobsByType["vacuum"] = 99
+ got.Metadata["rack"] = "changed"
+ if len(source.RunningWork) != 5 || source.RunningWork[0] != nil || source.RunningWork[1].WorkId != "first" {
+ t.Fatalf("cloning mutated source work: %v", source.RunningWork)
+ }
+ if source.QueuedJobsByType["vacuum"] != 2 || source.Metadata["rack"] != "rack-a" {
+ t.Fatal("cloned maps alias the source")
+ }
+}
+
+func TestCloneWorkerHeartbeatPreservesEmptyCollections(t *testing.T) {
+ t.Parallel()
+ if cloneWorkerHeartbeat(nil) != nil {
+ t.Fatal("nil heartbeat must stay nil")
+ }
+ for _, tc := range []struct {
+ name string
+ source *plugin_pb.WorkerHeartbeat
+ wantNilWork bool
+ }{
+ {"nil", &plugin_pb.WorkerHeartbeat{}, true},
+ {"empty", &plugin_pb.WorkerHeartbeat{
+ RunningWork: []*plugin_pb.RunningWork{},
+ QueuedJobsByType: map[string]int32{},
+ Metadata: map[string]string{},
+ }, false},
+ {"all nil work", &plugin_pb.WorkerHeartbeat{RunningWork: []*plugin_pb.RunningWork{nil, nil}}, false},
+ } {
+ t.Run(tc.name, func(t *testing.T) {
+ got := cloneWorkerHeartbeat(tc.source)
+ if len(got.RunningWork) != 0 || (got.RunningWork == nil) != tc.wantNilWork {
+ t.Fatalf("unexpected empty work slice: %#v", got.RunningWork)
+ }
+ if (got.QueuedJobsByType == nil) != (tc.source.QueuedJobsByType == nil) || (got.Metadata == nil) != (tc.source.Metadata == nil) {
+ t.Fatal("clone changed whether empty maps are writable")
+ }
+ if got.Metadata != nil {
+ got.Metadata["added"] = "value"
+ if len(tc.source.Metadata) != 0 {
+ t.Fatal("empty metadata map aliases source")
+ }
+ }
+ })
+ }
+}
diff --git a/weed/cluster/lock_ring_manager.go b/weed/cluster/lock_ring_manager.go
index 036db3c4f9..04577ac18b 100644
--- a/weed/cluster/lock_ring_manager.go
+++ b/weed/cluster/lock_ring_manager.go
@@ -7,6 +7,7 @@ import (
"github.com/seaweedfs/seaweedfs/weed/glog"
"github.com/seaweedfs/seaweedfs/weed/pb"
"github.com/seaweedfs/seaweedfs/weed/pb/master_pb"
+ "google.golang.org/protobuf/proto"
)
const LockRingStabilizationInterval = 1 * time.Second
@@ -102,9 +103,7 @@ func (lrm *LockRingManager) GetLastUpdate(filerGroup FilerGroupName) *master_pb.
if !ok || update == nil {
return nil
}
- cp := *update
- cp.Servers = append([]string(nil), update.Servers...)
- return &cp
+ return proto.Clone(update).(*master_pb.LockRingUpdate)
}
// scheduleBroadcast resets the stabilization timer. If another change arrives
diff --git a/weed/command/benchmark.go b/weed/command/benchmark.go
index ad0dbadc4c..0ee3f03136 100644
--- a/weed/command/benchmark.go
+++ b/weed/command/benchmark.go
@@ -238,7 +238,7 @@ func writeFiles(idChan chan int, fileIdLineChan chan string, s *stat) {
}
var jwtAuthorization security.EncodedJwt
if isSecure {
- jwtAuthorization = operation.LookupJwt(b.masterClient.GetMaster(context.Background()), b.grpcDialOption, df.fp.Fid)
+ jwtAuthorization = operation.LookupJwt(context.Background(), b.masterClient.GetMaster(context.Background()), b.grpcDialOption, df.fp.Fid)
}
if e := util_http.Delete(fmt.Sprintf("http://%s/%s", df.fp.Server, df.fp.Fid), string(jwtAuthorization)); e == nil {
s.completed++
diff --git a/weed/command/filer.go b/weed/command/filer.go
index 90eb04bee9..245182117a 100644
--- a/weed/command/filer.go
+++ b/weed/command/filer.go
@@ -51,6 +51,15 @@ var (
// S3 gateway, whose remote-mount read paths dial the mounted endpoint directly.
const allowUntrustedRemoteEndpointsUsage = "if true, a read of a remote-only entry accepts arbitrary remote S3 endpoints including loopback / link-local hosts. Default rejects internal / metadata endpoints."
+// iamSelfServiceRequiresGrantUsage documents the flag shared by every command
+// that runs the embedded IAM API.
+const iamSelfServiceRequiresGrantUsage = "if true, an identity needs an explicit iam: grant to manage its own access keys (CreateAccessKey, DeleteAccessKey, UpdateAccessKey, ListAccessKeys, GetUser), as on AWS. Default allows it to every authenticated identity."
+
+// allowBackslashInKeyUsage documents the S3 flag keeping '\' a literal
+// object-key character instead of normalizing it to '/'. Repeated '//' in a
+// key still collapses: a filer entry name cannot contain an empty segment.
+const allowBackslashInKeyUsage = "treat backslash as a regular object-key character instead of converting it to '/' (aws-compatible; '//' still collapses)"
+
type FilerOptions struct {
masters *pb.ServerDiscovery
mastersString *string
@@ -173,6 +182,8 @@ func init() {
filerS3Options.cacheSizeMB = cmdFiler.Flag.Int64("s3.cacheCapacityMB", 0, "in-memory chunk cache capacity in MB for S3 GETs shared across requests (0 disables)")
filerS3Options.readerCacheSizeMB = cmdFiler.Flag.Int64("s3.readerCacheSizeMB", 0, "memory budget in MiB for downloaded and in-flight reader buffers across all S3 GETs (0 means unlimited)")
filerS3Options.allowUntrustedRemoteEndpoints = cmdFiler.Flag.Bool("s3.allowUntrustedRemoteEndpoints", false, allowUntrustedRemoteEndpointsUsage)
+ filerS3Options.allowBackslashInKey = cmdFiler.Flag.Bool("s3.allowBackslashInKey", false, allowBackslashInKeyUsage)
+ filerS3Options.iamSelfServiceRequiresGrant = cmdFiler.Flag.Bool("s3.iam.selfServiceRequiresGrant", false, iamSelfServiceRequiresGrantUsage)
// start webdav on filer
filerStartWebDav = cmdFiler.Flag.Bool("webdav", false, "whether to start webdav gateway")
@@ -191,6 +202,7 @@ func init() {
filerStartIam = cmdFiler.Flag.Bool("iam", false, "whether to start IAM service")
filerIamOptions.ip = cmdFiler.Flag.String("iam.ip", *f.ip, "iam server http listen ip address")
filerIamOptions.port = cmdFiler.Flag.Int("iam.port", 8111, "iam server http listen port")
+ filerIamOptions.selfServiceRequiresGrant = filerS3Options.iamSelfServiceRequiresGrant
filerStartSftp = cmdFiler.Flag.Bool("sftp", false, "whether to start the SFTP server")
filerSftpOptions.port = cmdFiler.Flag.Int("sftp.port", 2022, "SFTP server listen port")
diff --git a/weed/command/filer_remote_gateway_buckets.go b/weed/command/filer_remote_gateway_buckets.go
index 7e22549316..569ef62430 100644
--- a/weed/command/filer_remote_gateway_buckets.go
+++ b/weed/command/filer_remote_gateway_buckets.go
@@ -214,11 +214,17 @@ func (option *RemoteGatewayOptions) makeBucketedEventProcessor(filerSource *sour
}
if filer_pb.IsCreate(resp) {
if message.NewParentPath == option.bucketsDir {
+ if isSystemPath(message.NewParentPath, message.NewEntry.Name) {
+ return nil
+ }
return handleCreateBucket(message.NewEntry)
}
if isMultipartUploadFile(message.NewParentPath, message.NewEntry.Name) {
return nil
}
+ if isSystemPath(message.NewParentPath, message.NewEntry.Name) {
+ return nil
+ }
// Propagate delete markers as deletions on the remote.
// Delete markers are zero-content version entries, so they
// would be filtered out by the HasData check below.
@@ -292,6 +298,9 @@ func (option *RemoteGatewayOptions) makeBucketedEventProcessor(filerSource *sour
return nil
}
if resp.Directory == option.bucketsDir {
+ if isSystemPath(resp.Directory, message.OldEntry.Name) {
+ return nil
+ }
return handleDeleteBucket(message.OldEntry)
}
// Skip deletion of internal version files; individual version
@@ -300,6 +309,9 @@ func (option *RemoteGatewayOptions) makeBucketedEventProcessor(filerSource *sour
glog.V(2).Infof("skipping delete of internal version path: %s/%s", resp.Directory, message.OldEntry.Name)
return nil
}
+ if isSystemPath(resp.Directory, message.OldEntry.Name) {
+ return nil
+ }
bucket, remoteStorageMountLocation, remoteStorage, ok := option.detectBucketInfo(resp.Directory)
if !ok {
return nil
@@ -323,6 +335,9 @@ func (option *RemoteGatewayOptions) makeBucketedEventProcessor(filerSource *sour
if message.OldEntry.Name == message.NewEntry.Name {
return nil
}
+ if isSystemPath(resp.Directory, message.OldEntry.Name) || isSystemPath(message.NewParentPath, message.NewEntry.Name) {
+ return nil
+ }
if err := handleCreateBucket(message.NewEntry); err != nil {
return err
}
@@ -339,6 +354,9 @@ func (option *RemoteGatewayOptions) makeBucketedEventProcessor(filerSource *sour
glog.V(2).Infof("skipping update of internal version path: %s/%s", message.NewParentPath, message.NewEntry.Name)
return nil
}
+ if isSystemPath(message.NewParentPath, message.NewEntry.Name) {
+ return nil
+ }
oldBucket, oldRemoteStorageMountLocation, oldRemoteStorage, oldOk := option.detectBucketInfo(resp.Directory)
newBucket, newRemoteStorageMountLocation, newRemoteStorage, newOk := option.detectBucketInfo(message.NewParentPath)
if oldOk && newOk {
diff --git a/weed/command/filer_remote_sync_dir.go b/weed/command/filer_remote_sync_dir.go
index da6650f462..d36147191f 100644
--- a/weed/command/filer_remote_sync_dir.go
+++ b/weed/command/filer_remote_sync_dir.go
@@ -144,6 +144,10 @@ func (option *RemoteSyncOptions) makeEventProcessor(remoteStorage *remote_pb.Rem
if isMultipartUploadFile(message.NewParentPath, message.NewEntry.Name) {
return nil
}
+ if isSystemPath(message.NewParentPath, message.NewEntry.Name) {
+ glog.V(2).Infof("skipping create of system path: %s/%s", message.NewParentPath, message.NewEntry.Name)
+ return nil
+ }
// Propagate delete markers as deletions on the remote.
// Delete markers are zero-content version entries, so they
// would be filtered out by the HasData check below.
@@ -229,6 +233,10 @@ func processDeleteEvent(
glog.V(2).Infof("skipping delete of internal version path: %s/%s", resp.Directory, message.OldEntry.Name)
return nil
}
+ if isSystemPath(resp.Directory, message.OldEntry.Name) {
+ glog.V(2).Infof("skipping delete of system path: %s/%s", resp.Directory, message.OldEntry.Name)
+ return nil
+ }
if filer.IsMetadataOnlyDelete(message.OldEntry) {
glog.V(2).Infof("skipping remote delete of metadata-only delete: %s/%s", resp.Directory, message.OldEntry.Name)
return nil
@@ -339,6 +347,10 @@ func processUpdateEvent(
glog.V(2).Infof("skipping update of internal version path: %s/%s", message.NewParentPath, message.NewEntry.Name)
return nil
}
+ if isSystemPath(message.NewParentPath, message.NewEntry.Name) {
+ glog.V(2).Infof("skipping update of system path: %s/%s", message.NewParentPath, message.NewEntry.Name)
+ return nil
+ }
oldDest := toRemoteStorageLocation(util.FullPath(mountedDir), util.NewFullPath(resp.Directory, message.OldEntry.Name), remoteStorageMountLocation)
dest := toRemoteStorageLocation(util.FullPath(mountedDir), util.NewFullPath(message.NewParentPath, message.NewEntry.Name), remoteStorageMountLocation)
eventSource := renameSource{path: util.NewFullPath(resp.Directory, message.OldEntry.Name), loc: oldDest, stamp: message.OldEntry.GetRemoteEntry()}
@@ -428,7 +440,7 @@ func processUpdateEvent(
}
glog.V(0).Infof("delete %s", remote_storage.FormatLocation(oldDest))
if err := client.DeleteFile(oldDest); err != nil {
- if isMultipartUploadFile(resp.Directory, message.OldEntry.Name) {
+ if isMultipartUploadFile(resp.Directory, message.OldEntry.Name) || isSystemPath(resp.Directory, message.OldEntry.Name) {
return nil
}
if !errors.Is(err, remote_storage.ErrRemoteObjectNotFound) {
@@ -881,6 +893,23 @@ func isMultipartUploadDir(dir string) bool {
strings.Contains(dir, "/"+s3_constants.MultipartUploadsFolder+"/")
}
+// isSystemPath returns true if the dir and name refer to the internal .system folder
+// under /buckets (such as the bucket-owner index at /buckets/.system/owners/...)
+// or any entry inside it. These paths are internal to SeaweedFS and must not
+// be synced to remote storage.
+func isSystemPath(dir string, name string) bool {
+ fullPath := string(util.NewFullPath(dir, name))
+ trimmed := strings.Trim(fullPath, "/")
+ parts := strings.Split(trimmed, "/")
+ if len(parts) >= 1 && parts[0] == s3_constants.SystemFolder {
+ return true
+ }
+ if len(parts) >= 2 && parts[1] == s3_constants.SystemFolder {
+ return true
+ }
+ return false
+}
+
// isDeleteMarker returns true if the entry is an S3 delete marker
// (a zero-content version entry with ExtDeleteMarkerKey set to "true").
func isDeleteMarker(entry *filer_pb.Entry) bool {
diff --git a/weed/command/filer_remote_sync_dir_test.go b/weed/command/filer_remote_sync_dir_test.go
index d7fb99ea3f..93c8340b47 100644
--- a/weed/command/filer_remote_sync_dir_test.go
+++ b/weed/command/filer_remote_sync_dir_test.go
@@ -2016,3 +2016,74 @@ func TestMetadataUpdateKeepsTheStoredStampOnNilResult(t *testing.T) {
t.Fatalf("stamped RemoteEntry = %+v, want the entry's own stamp kept (mtime 100)", filerClient.updated.Entry.RemoteEntry)
}
}
+
+func TestIsSystemPath(t *testing.T) {
+ tests := []struct {
+ dir string
+ name string
+ expected bool
+ }{
+ {dir: "/buckets", name: ".system", expected: true},
+ {dir: "/buckets/.system", name: "owners", expected: true},
+ {dir: "/buckets/.system/owners", name: "alice", expected: true},
+ {dir: "/buckets/.system/owners/alice", name: "mybucket", expected: true},
+ {dir: "/", name: ".system", expected: true},
+ {dir: "/.system", name: "config", expected: true},
+ {dir: "/custom-mount", name: ".system", expected: true},
+ {dir: "/custom-mount/.system/owners", name: "alice", expected: true},
+ // Bucket contents should not match
+ {dir: "/buckets/mybucket", name: "file.txt", expected: false},
+ {dir: "/buckets/mybucket", name: ".system", expected: false},
+ {dir: "/buckets/mybucket/.system", name: "data", expected: false},
+ {dir: "/buckets", name: ".system-other", expected: false},
+ {dir: "/buckets/.system-other", name: "sub", expected: false},
+ }
+
+ for _, tt := range tests {
+ t.Run(fmt.Sprintf("%s/%s", tt.dir, tt.name), func(t *testing.T) {
+ got := isSystemPath(tt.dir, tt.name)
+ if got != tt.expected {
+ t.Errorf("isSystemPath(%q, %q) = %v, want %v", tt.dir, tt.name, got, tt.expected)
+ }
+ })
+ }
+}
+
+func TestRemoteSyncSkipsSystemFolder(t *testing.T) {
+ const mountedDir = "/buckets"
+ mountLoc := &remote_pb.RemoteStorageLocation{Name: "azure", Bucket: "project-storage", Path: "/"}
+
+ // Test update event on /buckets/.system/owners/... is skipped
+ updateResp := &filer_pb.SubscribeMetadataResponse{
+ Directory: "/buckets/.system/owners/structify-api",
+ EventNotification: &filer_pb.EventNotification{
+ OldEntry: &filer_pb.Entry{Name: "structify-copy-canary"},
+ NewParentPath: "/buckets/.system/owners/structify-api",
+ NewEntry: &filer_pb.Entry{Name: "structify-copy-canary"},
+ },
+ }
+ remote := &recordingRemote{}
+ filerClient := &stubFilerClient{}
+ if err := processUpdateEvent(filerClient, filerClient, "", remote, mountedDir, mountLoc, updateResp); err != nil {
+ t.Fatalf("processUpdateEvent failed: %v", err)
+ }
+ if len(remote.writes) != 0 || len(remote.metadataUpdates) != 0 {
+ t.Fatalf("expected 0 remote writes or metadata updates for system path, got %d writes and %d metadata updates",
+ len(remote.writes), len(remote.metadataUpdates))
+ }
+
+ // Test delete event on /buckets/.system/owners/... is skipped
+ deleteResp := &filer_pb.SubscribeMetadataResponse{
+ Directory: "/buckets/.system/owners/structify-api",
+ EventNotification: &filer_pb.EventNotification{
+ OldEntry: &filer_pb.Entry{Name: "structify-copy-canary"},
+ },
+ }
+ if err := processDeleteEvent(filerClient, remote, mountedDir, mountLoc, deleteResp); err != nil {
+ t.Fatalf("processDeleteEvent failed: %v", err)
+ }
+ if len(remote.deletes) != 0 {
+ t.Fatalf("expected 0 remote deletes for system path, got %d deletes", len(remote.deletes))
+ }
+}
+
diff --git a/weed/command/iam.go b/weed/command/iam.go
index 77f3a9014a..ce11f56ab2 100644
--- a/weed/command/iam.go
+++ b/weed/command/iam.go
@@ -28,10 +28,11 @@ var (
)
type IamOptions struct {
- filer *string
- masters *string
- ip *string
- port *int
+ filer *string
+ masters *string
+ ip *string
+ port *int
+ selfServiceRequiresGrant *bool
}
func init() {
@@ -40,6 +41,7 @@ func init() {
iamStandaloneOptions.masters = cmdIam.Flag.String("master", "localhost:9333", "comma-separated master servers")
iamStandaloneOptions.ip = cmdIam.Flag.String("ip", util.DetectedHostAddress(), "iam server http listen ip address")
iamStandaloneOptions.port = cmdIam.Flag.Int("port", 8111, "iam server http listen port")
+ iamStandaloneOptions.selfServiceRequiresGrant = cmdIam.Flag.Bool("selfServiceRequiresGrant", false, iamSelfServiceRequiresGrantUsage)
}
var cmdIam = &Command{
@@ -101,10 +103,11 @@ func (iamopt *IamOptions) startIamServer() bool {
masters := pb.ServerAddresses(*iamopt.masters).ToAddressMap()
router := mux.NewRouter().SkipClean(true)
iamApiServer, iamApiServer_err := iamapi.NewIamApiServer(router, &iamapi.IamServerOption{
- Masters: masters,
- Filers: filerAddresses,
- Port: *iamopt.port,
- GrpcDialOption: grpcDialOption,
+ Masters: masters,
+ Filers: filerAddresses,
+ Port: *iamopt.port,
+ GrpcDialOption: grpcDialOption,
+ IamSelfServiceRequiresGrant: *iamopt.selfServiceRequiresGrant,
})
glog.V(0).Info("NewIamApiServer created")
if iamApiServer_err != nil {
diff --git a/weed/command/mini.go b/weed/command/mini.go
index 1ded2f936c..9fd8d0963f 100644
--- a/weed/command/mini.go
+++ b/weed/command/mini.go
@@ -556,6 +556,8 @@ func initMiniS3Flags() {
miniS3Options.cacheSizeMB = cmdMini.Flag.Int64("s3.cacheCapacityMB", 0, "in-memory chunk cache capacity in MB for S3 GETs shared across requests (0 disables)")
miniS3Options.readerCacheSizeMB = cmdMini.Flag.Int64("s3.readerCacheSizeMB", 0, "memory budget in MiB for downloaded and in-flight reader buffers across all S3 GETs (0 means unlimited)")
miniS3Options.allowUntrustedRemoteEndpoints = cmdMini.Flag.Bool("s3.allowUntrustedRemoteEndpoints", false, allowUntrustedRemoteEndpointsUsage)
+ miniS3Options.allowBackslashInKey = cmdMini.Flag.Bool("s3.allowBackslashInKey", false, allowBackslashInKeyUsage)
+ miniS3Options.iamSelfServiceRequiresGrant = cmdMini.Flag.Bool("s3.iam.selfServiceRequiresGrant", false, iamSelfServiceRequiresGrantUsage)
// In mini mode, S3 uses the shared debug server started at line 681, not its own separate debug server
miniS3Options.debug = new(bool) // explicitly false
miniS3Options.debugPort = cmdMini.Flag.Int("s3.debug.port", 6060, "http port for debugging (unused in mini mode)")
diff --git a/weed/command/s3.go b/weed/command/s3.go
index 036e3813eb..958afb8eab 100644
--- a/weed/command/s3.go
+++ b/weed/command/s3.go
@@ -84,6 +84,8 @@ type S3Options struct {
readerCacheSizeMB *int64
allowUntrustedRemoteEndpoints *bool
+ allowBackslashInKey *bool
+ iamSelfServiceRequiresGrant *bool
// shutdownCtx, when non-nil, tells startS3Server to gracefully shut down
// its HTTP/gRPC servers once the ctx is cancelled, in addition to on
// interrupt. Used by weed mini to orchestrate an ordered shutdown.
@@ -140,6 +142,8 @@ func init() {
s3StandaloneOptions.cacheSizeMB = cmdS3.Flag.Int64("cacheCapacityMB", 0, "in-memory chunk cache capacity in MB for S3 GETs shared across requests (0 disables)")
s3StandaloneOptions.readerCacheSizeMB = cmdS3.Flag.Int64("readerCacheSizeMB", 0, "memory budget in MiB for downloaded and in-flight reader buffers across all S3 GETs (0 means unlimited)")
s3StandaloneOptions.allowUntrustedRemoteEndpoints = cmdS3.Flag.Bool("allowUntrustedRemoteEndpoints", false, allowUntrustedRemoteEndpointsUsage)
+ s3StandaloneOptions.allowBackslashInKey = cmdS3.Flag.Bool("allowBackslashInKey", false, allowBackslashInKeyUsage)
+ s3StandaloneOptions.iamSelfServiceRequiresGrant = cmdS3.Flag.Bool("iam.selfServiceRequiresGrant", false, iamSelfServiceRequiresGrantUsage)
}
var cmdS3 = &Command{
@@ -337,7 +341,8 @@ func (s3opt *S3Options) startS3Server() bool {
router := mux.NewRouter().SkipClean(true)
var localFilerSocket string
- if s3opt.localFilerSocket != nil {
+ // The filer only listens on its local socket outside Windows.
+ if s3opt.localFilerSocket != nil && runtime.GOOS != "windows" {
localFilerSocket = *s3opt.localFilerSocket
}
var s3ApiServer *s3api.S3ApiServer
@@ -399,6 +404,8 @@ func (s3opt *S3Options) startS3Server() bool {
MaxMB: filerMaxMB,
AllowUntrustedRemoteEndpoints: *s3opt.allowUntrustedRemoteEndpoints,
+ AllowBackslashInKey: *s3opt.allowBackslashInKey,
+ IamSelfServiceRequiresGrant: *s3opt.iamSelfServiceRequiresGrant,
})
if s3ApiServer_err != nil {
glog.Fatalf("S3 API Server startup error: %v", s3ApiServer_err)
diff --git a/weed/command/scaffold/filer.toml b/weed/command/scaffold/filer.toml
index 67bfea93aa..8b9552e052 100644
--- a/weed/command/scaffold/filer.toml
+++ b/weed/command/scaffold/filer.toml
@@ -33,12 +33,22 @@ recursive_delete = false
# faster than previous leveldb, recommended.
enabled = true
dir = "./filerldb2" # directory to store level db files
+# optional tuning, applied to each of the 8 partitioned stores
+# block_cache_size_mb = 32 # sorted-table read cache
+# write_buffer_size_mb = 16 # memtable size before flushing to disk
+# block_size_kb = 4 # uncompressed block ('page') size inside table files
+# compaction_table_size_mb = 2 # target size of generated table files
[leveldb3]
# similar to leveldb2.
# each bucket has its own meta store.
enabled = false
dir = "./filerldb3" # directory to store level db files
+# optional tuning, applied to the default store and each bucket store
+# block_cache_size_mb = 32 # sorted-table read cache
+# write_buffer_size_mb = 16 # memtable size before flushing to disk
+# block_size_kb = 4 # uncompressed block ('page') size inside table files
+# compaction_table_size_mb = 2 # target size of generated table files
[rocksdb]
# local on disk, similar to leveldb
diff --git a/weed/command/server.go b/weed/command/server.go
index 0e4ac169a3..a9b27bc064 100644
--- a/weed/command/server.go
+++ b/weed/command/server.go
@@ -195,10 +195,12 @@ func init() {
s3Options.iamReadOnly = cmdServer.Flag.Bool("s3.iam.readOnly", true, "disable IAM write operations on this server")
s3Options.cipher = cmdServer.Flag.Bool("s3.encryptVolumeData", false, "encrypt data on volume servers for S3 uploads")
s3Options.externalUrl = cmdServer.Flag.String("s3.externalUrl", "", "the external URL clients use to connect (e.g. https://api.example.com:9000). Advertised to Iceberg and Lance clients, and tried first when verifying S3 signatures behind a reverse proxy. Falls back to S3_EXTERNAL_URL env var.")
+ s3Options.allowBackslashInKey = cmdServer.Flag.Bool("s3.allowBackslashInKey", false, allowBackslashInKeyUsage)
s3Options.defaultFileMode = cmdServer.Flag.String("s3.defaultFileMode", "", "default file mode for S3 uploaded objects, e.g. 0660, 0644, 0666")
s3Options.cacheSizeMB = cmdServer.Flag.Int64("s3.cacheCapacityMB", 0, "in-memory chunk cache capacity in MB for S3 GETs shared across requests (0 disables)")
s3Options.readerCacheSizeMB = cmdServer.Flag.Int64("s3.readerCacheSizeMB", 0, "memory budget in MiB for downloaded and in-flight reader buffers across all S3 GETs (0 means unlimited)")
s3Options.allowUntrustedRemoteEndpoints = cmdServer.Flag.Bool("s3.allowUntrustedRemoteEndpoints", false, allowUntrustedRemoteEndpointsUsage)
+ s3Options.iamSelfServiceRequiresGrant = cmdServer.Flag.Bool("s3.iam.selfServiceRequiresGrant", false, iamSelfServiceRequiresGrantUsage)
sftpOptions.port = cmdServer.Flag.Int("sftp.port", 2022, "SFTP server listen port")
sftpOptions.sshPrivateKey = cmdServer.Flag.String("sftp.sshPrivateKey", "", "path to the SSH private key file for host authentication")
@@ -214,6 +216,7 @@ func init() {
sftpOptions.dataCenter = cmdServer.Flag.String("sftp.dataCenter", "", "prefer to read and write to volumes in this data center")
sftpOptions.localSocket = cmdServer.Flag.String("sftp.localSocket", "", "default to /tmp/seaweedfs-sftp-