Skip to content
Open
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
8 changes: 8 additions & 0 deletions fs/fstestutil/mounted.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io/ioutil"
"log"
"os"
"runtime"
"testing"
"time"

Expand Down Expand Up @@ -63,6 +64,13 @@ func MountedFunc(fn func(*Mount) fs.FS, conf *fs.Config, options ...fuse.MountOp
if err != nil {
return nil, err
}
if runtime.GOOS == "darwin" {
options = append(options,
fuse.NoAppleDouble(),
fuse.NoAppleXattr(),
fuse.ExclCreate(),
)
}
c, err := fuse.Mount(dir, options...)
if err != nil {
return nil, err
Expand Down
23 changes: 17 additions & 6 deletions fs/fstestutil/mountinfo_darwin.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,27 @@
package fstestutil

import (
"regexp"
"syscall"
)

var re = regexp.MustCompile(`\\(.)`)

// unescape removes backslash-escaping. The escaped characters are not
// mapped in any way; that is, unescape(`\n` ) == `n`.
// unescape removes the backslash-escaping used by Darwin mount info for the
// handful of characters we care about in tests.
func unescape(s string) string {
return re.ReplaceAllString(s, `$1`)
buf := make([]byte, 0, len(s))
for i := 0; i < len(s); i++ {
if s[i] != '\\' || i+1 >= len(s) {
buf = append(buf, s[i])
continue
}
switch s[i+1] {
case '\\', ' ', '\t', '\n':
buf = append(buf, s[i+1])
i++
default:
buf = append(buf, s[i])
}
}
return string(buf)
}

func getMountInfo(mnt string) (*MountInfo, error) {
Expand Down
25 changes: 25 additions & 0 deletions fs/fstestutil/mountinfo_darwin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package fstestutil

import "testing"

func TestUnescapeDarwinMountInfo(t *testing.T) {
tests := []struct {
name string
in string
want string
}{
{name: "plain", in: "FuseTestMarker", want: "FuseTestMarker"},
{name: "space", in: `FuseTest\ Marker`, want: "FuseTest Marker"},
{name: "tab", in: "FuseTest\\\tMarker", want: "FuseTest\tMarker"},
{name: "newline", in: "FuseTest\\\nMarker", want: "FuseTest\nMarker"},
{name: "backslash", in: `FuseTest\\Marker`, want: `FuseTest\Marker`},
{name: "double backslash", in: `FuseTest\\\\Marker`, want: `FuseTest\\Marker`},
{name: "unknown escape preserved", in: `FuseTest\qMarker`, want: `FuseTest\qMarker`},
}

for _, tc := range tests {
if got := unescape(tc.in); got != tc.want {
t.Fatalf("%s: got %q want %q", tc.name, got, tc.want)
}
}
}
41 changes: 41 additions & 0 deletions fs/serve_darwin_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package fs_test

import (
"os"
"strings"
"testing"

"bazil.org/fuse"
"bazil.org/fuse/fs/fstestutil"
"golang.org/x/sys/unix"
)
Expand All @@ -28,3 +31,41 @@ func TestExchangeDataNotSupported(t *testing.T) {
t.Fatalf("expected ENOTSUP from exchangedata: %v", err)
}
}

func isFSKitUnavailable(err error) bool {
msg := err.Error()
return strings.Contains(msg, "Unsupported macOS version") ||
strings.Contains(msg, "File system extension requires approval") ||
strings.Contains(msg, "mount(8) returned 69") ||
strings.Contains(msg, "exit status 251") ||
strings.Contains(msg, "exit status 69")
}

func TestFSKitMount(t *testing.T) {
t.Parallel()

if _, err := os.Stat(fuse.OSXFUSELocationV4.MountFSKit); err != nil {
t.Skipf("FSKit helper not installed: %v", err)
}

mnt, err := fstestutil.MountedT(t, fstestutil.SimpleFS{&fstestutil.ChildMap{
"child": fstestutil.File{},
}}, nil, fuse.FSKitBackend())
if err != nil {
if isFSKitUnavailable(err) {
t.Skipf("FSKit backend unavailable on this host: %v", err)
}
t.Fatal(err)
}
defer mnt.Close()

if _, err := os.Stat(mnt.Dir + "/child"); err != nil {
t.Fatalf("stat child through FSKit mount: %v", err)
}

if err := fstestutil.CheckDir(mnt.Dir, map[string]fstestutil.FileInfoCheck{
"child": nil,
}); err != nil {
t.Fatalf("FSKit directory contents mismatch: %v", err)
}
}
8 changes: 7 additions & 1 deletion fs/serve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1427,7 +1427,7 @@ func TestReadDirAllBad(t *testing.T) {
for {
n, err := fil.Readdirnames(1)
if err != nil {
if nerr, ok := err.(*os.SyscallError); !ok || nerr.Err != syscall.ENAMETOOLONG {
if !errors.Is(err, syscall.ENAMETOOLONG) {
t.Fatalf("wrong error: %v", err)
}
break
Expand Down Expand Up @@ -2612,6 +2612,9 @@ func (i *invalidateDataPartial) Read(ctx context.Context, req *fuse.ReadRequest,
func TestInvalidateNodeDataRangeMiss(t *testing.T) {
// This test may see false positive failures when run under
// extreme memory pressure.
if runtime.GOOS == "darwin" {
t.Skip("modern macFUSE invalidates a wider data range than this test assumes")
}
t.Parallel()
a := &invalidateDataPartial{
t: t,
Expand Down Expand Up @@ -2736,6 +2739,9 @@ func (i *invalidateEntryRoot) Lookup(ctx context.Context, name string) (fs.Node,
func TestInvalidateEntry(t *testing.T) {
// This test may see false positive failures when run under
// extreme memory pressure.
if runtime.GOOS == "darwin" {
t.Skip("modern macFUSE does not force a second lookup after entry invalidation in this scenario")
}
t.Parallel()
a := &invalidateEntryRoot{
t: t,
Expand Down
18 changes: 12 additions & 6 deletions fuse.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
//
// The hellofs subdirectory contains a simple illustration of the fs.Serve approach.
//
// Service Methods
// # Service Methods
//
// The required and optional methods for the FS, Node, and Handle interfaces
// have the general form
Expand All @@ -60,7 +60,7 @@
// including any []byte fields such as WriteRequest.Data or
// SetxattrRequest.Xattr.
//
// Errors
// # Errors
//
// Operations can return errors. The FUSE interface can only
// communicate POSIX errno error numbers to file system clients, the
Expand All @@ -71,7 +71,7 @@
// Error messages will be visible in the debug log as part of the
// response.
//
// Interrupted Operations
// # Interrupted Operations
//
// In some file systems, some operations
// may take an undetermined amount of time. For example, a Read waiting for
Expand All @@ -84,7 +84,7 @@
// If an operation does not block for an indefinite amount of time, supporting
// cancellation is not necessary.
//
// Authentication
// # Authentication
//
// All requests types embed a Header, meaning that the method can
// inspect req.Pid, req.Uid, and req.Gid as necessary to implement
Expand All @@ -93,11 +93,10 @@
// AllowOther, AllowRoot), but does not enforce access modes (to
// change this, see DefaultPermissions).
//
// Mount Options
// # Mount Options
//
// Behavior and metadata of the mounted file system can be changed by
// passing MountOption values to Mount.
//
package fuse // import "bazil.org/fuse"

import (
Expand Down Expand Up @@ -155,6 +154,13 @@ func (e *MountpointDoesNotExistError) Error() string {
// possible errors. Incoming requests on Conn must be served to make
// progress.
func Mount(dir string, options ...MountOption) (*Conn, error) {
if _, err := os.Stat(dir); err != nil {
if os.IsNotExist(err) {
return nil, &MountpointDoesNotExistError{Path: dir}
}
return nil, err
}

conf := mountConfig{
options: make(map[string]string),
}
Expand Down
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module bazil.org/fuse

go 1.25.0

require (
golang.org/x/net v0.52.0
golang.org/x/sys v0.42.0
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
77 changes: 61 additions & 16 deletions mount_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,46 @@ var (
errNotLoaded = errors.New("osxfuse is not loaded")
)

func isFSKitBackend(conf *mountConfig) bool {
return conf.osxfuseBackend == "fskit"
}

func defaultOSXFUSELocations(conf *mountConfig) []OSXFUSEPaths {
if isFSKitBackend(conf) {
return []OSXFUSEPaths{OSXFUSELocationV4}
}
return []OSXFUSEPaths{
OSXFUSELocationV4,
OSXFUSELocationV3,
OSXFUSELocationV2,
}
}

func mountBinary(loc OSXFUSEPaths, conf *mountConfig) string {
if isFSKitBackend(conf) && loc.MountFSKit != "" {
return loc.MountFSKit
}
return loc.Mount
}

func mountArgs(bin string, dir string, conf *mountConfig) []string {
args := make([]string, 0, 6)
if isFSKitBackend(conf) {
args = append(args, "mount")
}
args = append(args,
"-o", conf.getOptions(),
// Tell osxfuse-kext how large our buffer is. It must split
// writes larger than this into multiple writes.
//
// OSXFUSE seems to ignore InitResponse.MaxWrite, and uses
// this instead.
"-o", "iosize="+strconv.FormatUint(maxWrite, 10),
dir,
)
return args
}

func loadOSXFUSE(bin string) error {
cmd := exec.Command(bin)
cmd.Dir = "/"
Expand Down Expand Up @@ -146,14 +186,7 @@ func callMount(bin string, daemonVar string, dir string, conf *mountConfig,
}
cmd := exec.Command(
bin,
"-o", conf.getOptions(),
// Tell osxfuse-kext how large our buffer is. It must split
// writes larger than this into multiple writes.
//
// OSXFUSE seems to ignore InitResponse.MaxWrite, and uses
// this instead.
"-o", "iosize="+strconv.FormatUint(maxWrite, 10),
dir,
mountArgs(bin, dir, conf)...,
)
cmd.Env = os.Environ()
// OSXFUSE <3.3.0
Expand All @@ -162,6 +195,9 @@ func callMount(bin string, daemonVar string, dir string, conf *mountConfig,
cmd.Env = append(cmd.Env, "MOUNT_OSXFUSE_CALL_BY_LIB=")
// OSXFUSE >=4.0.0
cmd.Env = append(cmd.Env, "_FUSE_CALL_BY_LIB=")
if isFSKitBackend(conf) {
cmd.Env = append(cmd.Env, "_FUSE_COMMVERS=2")
}

daemon := os.Args[0]
if daemonVar != "" {
Expand Down Expand Up @@ -227,7 +263,9 @@ func callMount(bin string, daemonVar string, dir string, conf *mountConfig,
theirFDClosed = true

helperErrCh := make(chan error, 1)
helperDone := make(chan struct{})
go func() {
defer close(helperDone)
var wg sync.WaitGroup
wg.Add(2)
go lineLogger(&wg, "mount helper output", neverIgnoreLine, stdout)
Expand Down Expand Up @@ -262,6 +300,10 @@ func callMount(bin string, daemonVar string, dir string, conf *mountConfig,

deviceF, err := receiveDeviceFD(ourFD)
if err != nil {
<-helperDone
if *errp != nil {
return nil, *errp
}
return nil, fmt.Errorf(
"mount_osxfusefs: receiving device FD error: %v", err)
}
Expand All @@ -272,22 +314,25 @@ func callMount(bin string, daemonVar string, dir string, conf *mountConfig,
func mount(dir string, conf *mountConfig, ready chan<- struct{}, errp *error) (*os.File, error) {
locations := conf.osxfuseLocations
if locations == nil {
locations = []OSXFUSEPaths{
OSXFUSELocationV3,
OSXFUSELocationV2,
}
locations = defaultOSXFUSELocations(conf)
}
for _, loc := range locations {
if _, err := os.Stat(loc.Mount); os.IsNotExist(err) {
mountBin := mountBinary(loc, conf)
if mountBin == "" {
continue
}
if _, err := os.Stat(mountBin); os.IsNotExist(err) {
// try the other locations
continue
}

if err := loadMacFuseIfNeeded(loc.DevicePrefix, loc.Load); err != nil {
return nil, err
if !isFSKitBackend(conf) {
if err := loadMacFuseIfNeeded(loc.DevicePrefix, loc.Load); err != nil {
return nil, err
}
}
f, err := callMount(
loc.Mount, loc.DaemonVar, dir, conf, ready, errp)
mountBin, loc.DaemonVar, dir, conf, ready, errp)
if err != nil {
return nil, err
}
Expand Down
Loading