Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ name: CI

on:
push:
tags:
- "v*.*.*"
branches:
- main
pull_request:
Expand Down
33 changes: 33 additions & 0 deletions fsx/fsx.go
Original file line number Diff line number Diff line change
@@ -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
}
84 changes: 84 additions & 0 deletions fsx/fsx_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
Loading