Skip to content
Closed
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: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@ gocryptfs.1
/_vendor-*

# Source tarball version. Should never be committed to git.
/VERSION
/VERSION
6 changes: 5 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
module github.com/rfjakob/gocryptfs/v2

go 1.19
go 1.23.0

toolchain go1.24.4

require (
github.com/aperturerobotics/jacobsa-crypto v1.1.0
Expand All @@ -14,3 +16,5 @@ require (
golang.org/x/sys v0.30.0
golang.org/x/term v0.29.0
)

require golang.org/x/text v0.27.0 // indirect
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
127 changes: 121 additions & 6 deletions internal/fusefrontend/file_dir_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package fusefrontend

import (
"context"
"os"
"runtime"
"syscall"

"github.com/hanwen/go-fuse/v2/fs"
Expand All @@ -18,6 +20,7 @@ func (n *Node) OpendirHandle(ctx context.Context, flags uint32) (fh fs.FileHandl
var file *File
var dirIV []byte
var ds fs.DirStream
var err error
rn := n.rootNode()

dirfd, cName, errno := n.prepareAtSyscallMyself()
Expand All @@ -27,23 +30,31 @@ func (n *Node) OpendirHandle(ctx context.Context, flags uint32) (fh fs.FileHandl
defer syscall.Close(dirfd)

// Open backing directory
fd, err := syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0)
fd, err = syscallcompat.Openat(dirfd, cName, syscall.O_RDONLY|syscall.O_DIRECTORY|syscall.O_NOFOLLOW, 0)
if err != nil {
errno = fs.ToErrno(err)
return
}

// NewLoopbackDirStreamFd gets its own fd to untangle Release vs Releasedir
fdDup, err = syscall.Dup(fd)

if err != nil {
errno = fs.ToErrno(err)
goto err_out
}

ds, errno = fs.NewLoopbackDirStreamFd(fdDup)
if errno != 0 {
goto err_out
// Use custom directory stream on macOS due to issues with go-fuse loopback implementation
if runtime.GOOS == "darwin" {
// On macOS, use our custom directory stream implementation
// The go-fuse NewLoopbackDirStreamFd has compatibility issues with macOS/APFS
ds = &customDirStream{fd: fdDup}
errno = 0
} else {
// On other platforms, use the standard loopback directory stream
ds, errno = fs.NewLoopbackDirStreamFd(fdDup)
if errno != 0 {
goto err_out
}
}

if !rn.args.PlaintextNames {
Expand Down Expand Up @@ -93,6 +104,106 @@ type DirHandle struct {
ds fs.FileHandle
}

// customDirStream implements our own directory reading for macOS.
// This works around compatibility issues with go-fuse's NewLoopbackDirStreamFd on macOS/APFS.
type customDirStream struct {
fd int
entries []string
pos int
}

func (ds *customDirStream) Readdirent(ctx context.Context) (entry *fuse.DirEntry, errno syscall.Errno) {
// Load entries on first call
if ds.entries == nil {
osFile := os.NewFile(uintptr(ds.fd), "custom-dir")
if osFile == nil {
return nil, syscall.EIO
}

// Don't close osFile since that would close our fd
defer func() {
// Seek back to beginning for potential future reads
osFile.Seek(0, 0)
}()

entries, err := osFile.Readdirnames(-1)
if err != nil {
return nil, fs.ToErrno(err)
}

ds.entries = entries
ds.pos = 0
}

// Return next entry
if ds.pos >= len(ds.entries) {
return nil, 0
}

name := ds.entries[ds.pos]
ds.pos++

return &fuse.DirEntry{
Name: name,
Mode: 0, // We don't provide mode info, let FUSE handle it
}, 0
}

func (ds *customDirStream) Seekdir(ctx context.Context, off uint64) syscall.Errno {
if ds.entries == nil {
// Not loaded yet, seeking to 0 is OK
if off == 0 {
return 0
}
return syscall.EINVAL
}

if off > uint64(len(ds.entries)) {
return syscall.EINVAL
}

ds.pos = int(off)
return 0
}

func (ds *customDirStream) Releasedir(ctx context.Context, flags uint32) {
if ds.fd >= 0 {
syscall.Close(ds.fd)
ds.fd = -1
}
}

func (ds *customDirStream) Fsyncdir(ctx context.Context, flags uint32) syscall.Errno {
// No-op for directory streams
return 0
}

func (ds *customDirStream) Close() {
// Close is part of the fs.DirStream interface
if ds.fd >= 0 {
syscall.Close(ds.fd)
ds.fd = -1
}
}

func (ds *customDirStream) HasNext() bool {
// HasNext is part of the fs.DirStream interface
if ds.entries == nil {
// Not loaded yet, assume we have entries to avoid early termination
return true
}
return ds.pos < len(ds.entries)
}

func (ds *customDirStream) Next() (fuse.DirEntry, syscall.Errno) {
// Next is part of the fs.DirStream interface
entry, errno := ds.Readdirent(context.Background())
if entry == nil {
return fuse.DirEntry{}, errno
}
return *entry, errno
}

var _ = (fs.FileReleasedirer)((*File)(nil))

func (f *File) Releasedir(ctx context.Context, flags uint32) {
Expand Down Expand Up @@ -124,11 +235,13 @@ func (f *File) Readdirent(ctx context.Context) (entry *fuse.DirEntry, errno sysc

for {
entry, errno = f.dirHandle.ds.(fs.FileReaddirenter).Readdirent(ctx)

if errno != 0 || entry == nil {
return
}

cName := entry.Name

if cName == "." || cName == ".." {
// We want these as-is
return
Expand All @@ -138,6 +251,8 @@ func (f *File) Readdirent(ctx context.Context) (entry *fuse.DirEntry, errno sysc
continue
}
if f.rootNode.args.PlaintextNames {
// Even in plaintext mode, normalize for macOS display
entry.Name = normalizeFilenameForDisplay(cName)
return
}
if !f.rootNode.args.DeterministicNames && cName == nametransform.DirIVFilename {
Expand Down Expand Up @@ -171,7 +286,7 @@ func (f *File) Readdirent(ctx context.Context) (entry *fuse.DirEntry, errno sysc
}
// Override the ciphertext name with the plaintext name but reuse the rest
// of the structure
entry.Name = name
entry.Name = normalizeFilenameForDisplay(name)
return
}
}
7 changes: 7 additions & 0 deletions internal/fusefrontend/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ func (n *Node) Access(ctx context.Context, mode uint32) syscall.Errno {
//
// Symlink-safe through use of Unlinkat().
func (n *Node) Unlink(ctx context.Context, name string) (errno syscall.Errno) {
name = normalizeFilename(name) // Always store as NFC
dirfd, cName, errno := n.prepareAtSyscall(name)
if errno != 0 {
return
Expand Down Expand Up @@ -274,6 +275,7 @@ func (n *Node) Statfs(ctx context.Context, out *fuse.StatfsOut) syscall.Errno {
//
// Symlink-safe through use of Mknodat().
func (n *Node) Mknod(ctx context.Context, name string, mode, rdev uint32, out *fuse.EntryOut) (inode *fs.Inode, errno syscall.Errno) {
name = normalizeFilename(name) // Always store as NFC
dirfd, cName, errno := n.prepareAtSyscall(name)
if errno != 0 {
return
Expand Down Expand Up @@ -329,6 +331,7 @@ func (n *Node) Mknod(ctx context.Context, name string, mode, rdev uint32, out *f
//
// Symlink-safe through use of Linkat().
func (n *Node) Link(ctx context.Context, target fs.InodeEmbedder, name string, out *fuse.EntryOut) (inode *fs.Inode, errno syscall.Errno) {
name = normalizeFilename(name) // Always store as NFC
dirfd, cName, errno := n.prepareAtSyscall(name)
if errno != 0 {
return
Expand Down Expand Up @@ -379,6 +382,7 @@ func (n *Node) Link(ctx context.Context, target fs.InodeEmbedder, name string, o
//
// Symlink-safe through use of Symlinkat.
func (n *Node) Symlink(ctx context.Context, target, name string, out *fuse.EntryOut) (inode *fs.Inode, errno syscall.Errno) {
name = normalizeFilename(name) // Always store as NFC
dirfd, cName, errno := n.prepareAtSyscall(name)
if errno != 0 {
return
Expand Down Expand Up @@ -451,6 +455,9 @@ func (n *Node) Rename(ctx context.Context, name string, newParent fs.InodeEmbedd
return errno
}

name = normalizeFilename(name) // Always store as NFC
newName = normalizeFilename(newName) // Always store as NFC

dirfd, cName, errno := n.prepareAtSyscall(name)
if errno != 0 {
return
Expand Down
20 changes: 20 additions & 0 deletions internal/fusefrontend/node_dir_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import (
"io"
"runtime"
"syscall"
"unicode/utf8"

"golang.org/x/sys/unix"
"golang.org/x/text/unicode/norm"

"github.com/hanwen/go-fuse/v2/fs"
"github.com/hanwen/go-fuse/v2/fuse"
Expand All @@ -20,6 +22,22 @@ import (

const dsStoreName = ".DS_Store"

// normalizeFilename converts filenames to NFC for consistent internal storage
func normalizeFilename(name string) string {
if runtime.GOOS == "darwin" && utf8.ValidString(name) {
return norm.NFC.String(name)
}
return name
}

// normalizeFilenameForDisplay converts NFC to NFD for macOS GUI compatibility
func normalizeFilenameForDisplay(name string) string {
if runtime.GOOS == "darwin" && utf8.ValidString(name) {
return norm.NFD.String(name)
}
return name
}

// haveDsstore return true if one of the entries in "names" is ".DS_Store".
func haveDsstore(entries []fuse.DirEntry) bool {
for _, e := range entries {
Expand Down Expand Up @@ -70,6 +88,7 @@ func (n *Node) mkdirWithIv(dirfd int, cName string, mode uint32, context *fuse.C
//
// Symlink-safe through use of Mkdirat().
func (n *Node) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
name = normalizeFilename(name) // Always store as NFC
dirfd, cName, errno := n.prepareAtSyscall(name)
if errno != 0 {
return nil, errno
Expand Down Expand Up @@ -162,6 +181,7 @@ func (n *Node) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.En
//
// Symlink-safe through Unlinkat() + AT_REMOVEDIR.
func (n *Node) Rmdir(ctx context.Context, name string) (code syscall.Errno) {
name = normalizeFilename(name) // Always store as NFC
rn := n.rootNode()
parentDirFd, cName, errno := n.prepareAtSyscall(name)
if errno != 0 {
Expand Down
1 change: 1 addition & 0 deletions internal/fusefrontend/node_open_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ func (n *Node) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFl
//
// Symlink-safe through the use of Openat().
func (n *Node) Create(ctx context.Context, name string, flags uint32, mode uint32, out *fuse.EntryOut) (inode *fs.Inode, fh fs.FileHandle, fuseFlags uint32, errno syscall.Errno) {
name = normalizeFilename(name) // Always store as NFC
dirfd, cName, errno := n.prepareAtSyscall(name)
if errno != 0 {
return
Expand Down
Loading