Skip to content

Commit 8e56f7d

Browse files
feat: add fsx module and implement specific error for snap context
1 parent 5ff0eeb commit 8e56f7d

3 files changed

Lines changed: 117 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,6 @@ name: CI
22

33
on:
44
push:
5-
tags:
6-
- "v*.*.*"
75
branches:
86
- main
97
pull_request:

fsx/fsx.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package fsx
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"os"
7+
"strings"
8+
)
9+
10+
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)")
11+
12+
var ErrPermission = errors.New("permission denied: the process does not have access to read this file")
13+
14+
func isSnapProcess() bool {
15+
return os.Getenv("SNAP") != "" || os.Getenv("SNAP_NAME") != "" || strings.HasPrefix(os.Getenv("SNAP_USER_DATA"), "/home/")
16+
}
17+
18+
func ReadFile(path string) ([]byte, error) {
19+
data, err := os.ReadFile(path)
20+
if err == nil {
21+
return data, nil
22+
}
23+
24+
if errors.Is(err, os.ErrPermission) {
25+
if isSnapProcess() {
26+
return nil, fmt.Errorf("%w: %s", ErrSnapPermission, path)
27+
}
28+
29+
return nil, fmt.Errorf("%w: %s", ErrPermission, path)
30+
}
31+
32+
return nil, err
33+
}

fsx/fsx_test.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
package fsx_test
2+
3+
import (
4+
"errors"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/cerberauth/x/fsx"
10+
)
11+
12+
func TestReadFile_Success(t *testing.T) {
13+
dir := t.TempDir()
14+
path := filepath.Join(dir, "test.txt")
15+
if err := os.WriteFile(path, []byte("hello"), 0600); err != nil {
16+
t.Fatalf("failed to create test file: %v", err)
17+
}
18+
19+
data, err := fsx.ReadFile(path)
20+
if err != nil {
21+
t.Fatalf("unexpected error: %v", err)
22+
}
23+
if string(data) != "hello" {
24+
t.Errorf("expected %q, got %q", "hello", string(data))
25+
}
26+
}
27+
28+
func TestReadFile_NotFound(t *testing.T) {
29+
_, err := fsx.ReadFile("/nonexistent/path/file.txt")
30+
if err == nil {
31+
t.Fatal("expected error, got nil")
32+
}
33+
if errors.Is(err, fsx.ErrPermission) || errors.Is(err, fsx.ErrSnapPermission) {
34+
t.Errorf("expected a not-found error, got permission error: %v", err)
35+
}
36+
}
37+
38+
func TestReadFile_PermissionDenied_Generic(t *testing.T) {
39+
if os.Getuid() == 0 {
40+
t.Skip("skipping permission test: running as root")
41+
}
42+
43+
t.Setenv("SNAP", "")
44+
t.Setenv("SNAP_NAME", "")
45+
t.Setenv("SNAP_USER_DATA", "")
46+
47+
dir := t.TempDir()
48+
path := filepath.Join(dir, "noaccess.txt")
49+
if err := os.WriteFile(path, []byte("secret"), 0000); err != nil {
50+
t.Fatalf("failed to create test file: %v", err)
51+
}
52+
53+
_, err := fsx.ReadFile(path)
54+
if err == nil {
55+
t.Fatal("expected error, got nil")
56+
}
57+
if !errors.Is(err, fsx.ErrPermission) {
58+
t.Errorf("expected ErrPermission, got: %v", err)
59+
}
60+
}
61+
62+
func TestReadFile_PermissionDenied_Snap(t *testing.T) {
63+
if os.Getuid() == 0 {
64+
t.Skip("skipping permission test: running as root")
65+
}
66+
67+
t.Setenv("SNAP", "/snap/myapp/current")
68+
t.Setenv("SNAP_NAME", "myapp")
69+
t.Setenv("SNAP_USER_DATA", "")
70+
71+
dir := t.TempDir()
72+
path := filepath.Join(dir, "noaccess.txt")
73+
if err := os.WriteFile(path, []byte("secret"), 0000); err != nil {
74+
t.Fatalf("failed to create test file: %v", err)
75+
}
76+
77+
_, err := fsx.ReadFile(path)
78+
if err == nil {
79+
t.Fatal("expected error, got nil")
80+
}
81+
if !errors.Is(err, fsx.ErrSnapPermission) {
82+
t.Errorf("expected ErrSnapPermission, got: %v", err)
83+
}
84+
}

0 commit comments

Comments
 (0)