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
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=
4 changes: 3 additions & 1 deletion internal/fusefrontend/file_dir_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,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 +173,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
19 changes: 19 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
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
54 changes: 49 additions & 5 deletions internal/fusefrontend/node_prepare_syscall.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
package fusefrontend

import (
"runtime"
"syscall"
"unicode/utf8"

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

"github.com/rfjakob/gocryptfs/v2/internal/tlog"

Expand All @@ -10,12 +14,10 @@ import (
"github.com/rfjakob/gocryptfs/v2/internal/syscallcompat"
)

// prepareAtSyscall returns a (dirfd, cName) pair that can be used
// with the "___at" family of system calls (openat, fstatat, unlinkat...) to
// access the backing encrypted child file.
func (n *Node) prepareAtSyscall(child string) (dirfd int, cName string, errno syscall.Errno) {
// prepareAtSyscallDirect is the direct version without Unicode normalization fallback
func (n *Node) prepareAtSyscallDirect(child string) (dirfd int, cName string, errno syscall.Errno) {
if child == "" {
tlog.Warn.Printf("BUG: prepareAtSyscall: child=%q, should have called prepareAtSyscallMyself", child)
tlog.Warn.Printf("BUG: prepareAtSyscallDirect: child=%q, should have called prepareAtSyscallMyself", child)
return n.prepareAtSyscallMyself()
}

Expand Down Expand Up @@ -92,6 +94,48 @@ func (n *Node) prepareAtSyscall(child string) (dirfd int, cName string, errno sy
return
}

// prepareAtSyscall returns a (dirfd, cName) pair that can be used
// with the "___at" family of system calls (openat, fstatat, unlinkat...) to
// access the backing encrypted child file.
func (n *Node) prepareAtSyscall(child string) (dirfd int, cName string, errno syscall.Errno) {
if child == "" {
tlog.Warn.Printf("BUG: prepareAtSyscall: child=%q, should have called prepareAtSyscallMyself", child)
return n.prepareAtSyscallMyself()
}

// On macOS, implement Unicode normalization with fallback
if runtime.GOOS == "darwin" && utf8.ValidString(child) {
// Step 1: Always try NFC first (canonical storage form)
normalizedChild := norm.NFC.String(child)
dirfd, cName, errno = n.prepareAtSyscallDirect(normalizedChild)
if errno == 0 {
return dirfd, cName, 0 // Found NFC version
}

// Step 2: Try alternate form if input was different
if normalizedChild != child {
// Input was NFD, try original NFD form
dirfdNFD, cNameNFD, errnoNFD := n.prepareAtSyscallDirect(child)
if errnoNFD == 0 {
return dirfdNFD, cNameNFD, 0
}
}

// Step 3: If input was NFC, also try NFD as fallback
if normalizedChild == child {
Copy link

Copilot AI Aug 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This condition checks if input was NFC after already trying NFC and NFD. This creates redundant NFD normalization when the input was already NFC. Consider restructuring to avoid duplicate normalization calls.

Suggested change
if normalizedChild == child {
if normalizedChild != child {
// Input was NFD, try original NFD form
dirfdNFD, cNameNFD, errnoNFD := n.prepareAtSyscallDirect(child)
if errnoNFD == 0 {
return dirfdNFD, cNameNFD, 0
}
} else {
// Input was NFC, try NFD as fallback if different

Copilot uses AI. Check for mistakes.

nfdChild := norm.NFD.String(child)
if nfdChild != child {
dirfdNFD, cNameNFD, errnoNFD := n.prepareAtSyscallDirect(nfdChild)
if errnoNFD == 0 {
return dirfdNFD, cNameNFD, 0
}
}
}
}

return n.prepareAtSyscallDirect(child) // Non-macOS or fallback
Copy link

Copilot AI Aug 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The logic for trying different normalization forms is complex and could be simplified. Consider extracting the normalization attempts into a separate helper function to improve readability and maintainability.

Suggested change
return n.prepareAtSyscallDirect(child) // Non-macOS or fallback
return n.tryNormalizationForms(child)
}
return n.prepareAtSyscallDirect(child) // Non-macOS or fallback
}
// tryNormalizationForms tries different Unicode normalization forms for macOS
func (n *Node) tryNormalizationForms(child string) (dirfd int, cName string, errno syscall.Errno) {
// Step 1: Always try NFC first (canonical storage form)
normalizedChild := norm.NFC.String(child)
dirfd, cName, errno = n.prepareAtSyscallDirect(normalizedChild)
if errno == 0 {
return dirfd, cName, 0 // Found NFC version
}
// Step 2: Try alternate form if input was different
if normalizedChild != child {
// Input was NFD, try original NFD form
dirfdNFD, cNameNFD, errnoNFD := n.prepareAtSyscallDirect(child)
if errnoNFD == 0 {
return dirfdNFD, cNameNFD, 0
}
}
// Step 3: If input was NFC, also try NFD as fallback
if normalizedChild == child {
nfdChild := norm.NFD.String(child)
if nfdChild != child {
dirfdNFD, cNameNFD, errnoNFD := n.prepareAtSyscallDirect(nfdChild)
if errnoNFD == 0 {
return dirfdNFD, cNameNFD, 0
}
}
}
return n.prepareAtSyscallDirect(child)

Copilot uses AI. Check for mistakes.

}

func (n *Node) prepareAtSyscallMyself() (dirfd int, cName string, errno syscall.Errno) {
dirfd = -1

Expand Down
Loading
Loading