diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 197c710..bdfc9f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,8 +2,6 @@ name: CI on: push: - tags: - - "v*.*.*" branches: - main pull_request: diff --git a/fsx/fsx.go b/fsx/fsx.go new file mode 100644 index 0000000..355d2a2 --- /dev/null +++ b/fsx/fsx.go @@ -0,0 +1,33 @@ +package fsx + +import ( + "errors" + "fmt" + "os" + "strings" +) + +var ErrSnapPermission = errors.New("permission denied: this binary was installed via snap which restricts filesystem access; please install using a different method (e.g. direct binary, docker, or package manager)") + +var ErrPermission = errors.New("permission denied: the process does not have access to read this file") + +func isSnapProcess() bool { + return os.Getenv("SNAP") != "" || os.Getenv("SNAP_NAME") != "" || strings.HasPrefix(os.Getenv("SNAP_USER_DATA"), "/home/") +} + +func ReadFile(path string) ([]byte, error) { + data, err := os.ReadFile(path) + if err == nil { + return data, nil + } + + if errors.Is(err, os.ErrPermission) { + if isSnapProcess() { + return nil, fmt.Errorf("%w: %s", ErrSnapPermission, path) + } + + return nil, fmt.Errorf("%w: %s", ErrPermission, path) + } + + return nil, err +} diff --git a/fsx/fsx_test.go b/fsx/fsx_test.go new file mode 100644 index 0000000..a6fd260 --- /dev/null +++ b/fsx/fsx_test.go @@ -0,0 +1,84 @@ +package fsx_test + +import ( + "errors" + "os" + "path/filepath" + "testing" + + "github.com/cerberauth/x/fsx" +) + +func TestReadFile_Success(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "test.txt") + if err := os.WriteFile(path, []byte("hello"), 0600); err != nil { + t.Fatalf("failed to create test file: %v", err) + } + + data, err := fsx.ReadFile(path) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if string(data) != "hello" { + t.Errorf("expected %q, got %q", "hello", string(data)) + } +} + +func TestReadFile_NotFound(t *testing.T) { + _, err := fsx.ReadFile("/nonexistent/path/file.txt") + if err == nil { + t.Fatal("expected error, got nil") + } + if errors.Is(err, fsx.ErrPermission) || errors.Is(err, fsx.ErrSnapPermission) { + t.Errorf("expected a not-found error, got permission error: %v", err) + } +} + +func TestReadFile_PermissionDenied_Generic(t *testing.T) { + if os.Getuid() == 0 { + t.Skip("skipping permission test: running as root") + } + + t.Setenv("SNAP", "") + t.Setenv("SNAP_NAME", "") + t.Setenv("SNAP_USER_DATA", "") + + dir := t.TempDir() + path := filepath.Join(dir, "noaccess.txt") + if err := os.WriteFile(path, []byte("secret"), 0000); err != nil { + t.Fatalf("failed to create test file: %v", err) + } + + _, err := fsx.ReadFile(path) + if err == nil { + t.Fatal("expected error, got nil") + } + if !errors.Is(err, fsx.ErrPermission) { + t.Errorf("expected ErrPermission, got: %v", err) + } +} + +func TestReadFile_PermissionDenied_Snap(t *testing.T) { + if os.Getuid() == 0 { + t.Skip("skipping permission test: running as root") + } + + t.Setenv("SNAP", "/snap/myapp/current") + t.Setenv("SNAP_NAME", "myapp") + t.Setenv("SNAP_USER_DATA", "") + + dir := t.TempDir() + path := filepath.Join(dir, "noaccess.txt") + if err := os.WriteFile(path, []byte("secret"), 0000); err != nil { + t.Fatalf("failed to create test file: %v", err) + } + + _, err := fsx.ReadFile(path) + if err == nil { + t.Fatal("expected error, got nil") + } + if !errors.Is(err, fsx.ErrSnapPermission) { + t.Errorf("expected ErrSnapPermission, got: %v", err) + } +}