Skip to content

Commit 1867ecb

Browse files
committed
Applying macos fix to version 2.6.0
1 parent 33fa0b5 commit 1867ecb

File tree

9 files changed

+198
-7
lines changed

9 files changed

+198
-7
lines changed

go.mod

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
module github.com/rfjakob/gocryptfs/v2
22

3-
go 1.19
3+
go 1.23.0
4+
5+
toolchain go1.24.4
46

57
require (
68
github.com/aperturerobotics/jacobsa-crypto v1.1.0
@@ -14,3 +16,5 @@ require (
1416
golang.org/x/sys v0.30.0
1517
golang.org/x/term v0.29.0
1618
)
19+
20+
require golang.org/x/text v0.27.0 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
3232
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
3333
golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU=
3434
golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s=
35+
golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4=
36+
golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU=
3537
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
3638
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
3739
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

internal/fusefrontend/file_dir_ops.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ func (f *File) Readdirent(ctx context.Context) (entry *fuse.DirEntry, errno sysc
138138
continue
139139
}
140140
if f.rootNode.args.PlaintextNames {
141+
// Even in plaintext mode, normalize for macOS display
142+
entry.Name = normalizeFilenameForDisplay(cName)
141143
return
142144
}
143145
if !f.rootNode.args.DeterministicNames && cName == nametransform.DirIVFilename {
@@ -171,7 +173,7 @@ func (f *File) Readdirent(ctx context.Context) (entry *fuse.DirEntry, errno sysc
171173
}
172174
// Override the ciphertext name with the plaintext name but reuse the rest
173175
// of the structure
174-
entry.Name = name
176+
entry.Name = normalizeFilenameForDisplay(name)
175177
return
176178
}
177179
}

internal/fusefrontend/node.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ func (n *Node) Access(ctx context.Context, mode uint32) syscall.Errno {
140140
//
141141
// Symlink-safe through use of Unlinkat().
142142
func (n *Node) Unlink(ctx context.Context, name string) (errno syscall.Errno) {
143+
name = normalizeFilename(name) // Always store as NFC
143144
dirfd, cName, errno := n.prepareAtSyscall(name)
144145
if errno != 0 {
145146
return
@@ -274,6 +275,7 @@ func (n *Node) Statfs(ctx context.Context, out *fuse.StatfsOut) syscall.Errno {
274275
//
275276
// Symlink-safe through use of Mknodat().
276277
func (n *Node) Mknod(ctx context.Context, name string, mode, rdev uint32, out *fuse.EntryOut) (inode *fs.Inode, errno syscall.Errno) {
278+
name = normalizeFilename(name) // Always store as NFC
277279
dirfd, cName, errno := n.prepareAtSyscall(name)
278280
if errno != 0 {
279281
return
@@ -329,6 +331,7 @@ func (n *Node) Mknod(ctx context.Context, name string, mode, rdev uint32, out *f
329331
//
330332
// Symlink-safe through use of Linkat().
331333
func (n *Node) Link(ctx context.Context, target fs.InodeEmbedder, name string, out *fuse.EntryOut) (inode *fs.Inode, errno syscall.Errno) {
334+
name = normalizeFilename(name) // Always store as NFC
332335
dirfd, cName, errno := n.prepareAtSyscall(name)
333336
if errno != 0 {
334337
return
@@ -379,6 +382,7 @@ func (n *Node) Link(ctx context.Context, target fs.InodeEmbedder, name string, o
379382
//
380383
// Symlink-safe through use of Symlinkat.
381384
func (n *Node) Symlink(ctx context.Context, target, name string, out *fuse.EntryOut) (inode *fs.Inode, errno syscall.Errno) {
385+
name = normalizeFilename(name) // Always store as NFC
382386
dirfd, cName, errno := n.prepareAtSyscall(name)
383387
if errno != 0 {
384388
return
@@ -451,6 +455,9 @@ func (n *Node) Rename(ctx context.Context, name string, newParent fs.InodeEmbedd
451455
return errno
452456
}
453457

458+
name = normalizeFilename(name) // Always store as NFC
459+
newName = normalizeFilename(newName) // Always store as NFC
460+
454461
dirfd, cName, errno := n.prepareAtSyscall(name)
455462
if errno != 0 {
456463
return

internal/fusefrontend/node_dir_ops.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@ import (
66
"io"
77
"runtime"
88
"syscall"
9+
"unicode/utf8"
910

1011
"golang.org/x/sys/unix"
12+
"golang.org/x/text/unicode/norm"
1113

1214
"github.com/hanwen/go-fuse/v2/fs"
1315
"github.com/hanwen/go-fuse/v2/fuse"
@@ -20,6 +22,22 @@ import (
2022

2123
const dsStoreName = ".DS_Store"
2224

25+
// normalizeFilename converts filenames to NFC for consistent internal storage
26+
func normalizeFilename(name string) string {
27+
if runtime.GOOS == "darwin" && utf8.ValidString(name) {
28+
return norm.NFC.String(name)
29+
}
30+
return name
31+
}
32+
33+
// normalizeFilenameForDisplay converts NFC to NFD for macOS GUI compatibility
34+
func normalizeFilenameForDisplay(name string) string {
35+
if runtime.GOOS == "darwin" && utf8.ValidString(name) {
36+
return norm.NFD.String(name)
37+
}
38+
return name
39+
}
40+
2341
// haveDsstore return true if one of the entries in "names" is ".DS_Store".
2442
func haveDsstore(entries []fuse.DirEntry) bool {
2543
for _, e := range entries {
@@ -70,6 +88,7 @@ func (n *Node) mkdirWithIv(dirfd int, cName string, mode uint32, context *fuse.C
7088
//
7189
// Symlink-safe through use of Mkdirat().
7290
func (n *Node) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) {
91+
name = normalizeFilename(name) // Always store as NFC
7392
dirfd, cName, errno := n.prepareAtSyscall(name)
7493
if errno != 0 {
7594
return nil, errno

internal/fusefrontend/node_open_create.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ func (n *Node) Open(ctx context.Context, flags uint32) (fh fs.FileHandle, fuseFl
5858
//
5959
// Symlink-safe through the use of Openat().
6060
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) {
61+
name = normalizeFilename(name) // Always store as NFC
6162
dirfd, cName, errno := n.prepareAtSyscall(name)
6263
if errno != 0 {
6364
return

internal/fusefrontend/node_prepare_syscall.go

Lines changed: 117 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
package fusefrontend
22

33
import (
4+
"runtime"
45
"syscall"
6+
"unicode/utf8"
7+
8+
"golang.org/x/text/unicode/norm"
59

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

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

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

@@ -92,6 +94,116 @@ func (n *Node) prepareAtSyscall(child string) (dirfd int, cName string, errno sy
9294
return
9395
}
9496

97+
// migrateFilename migrates a filename from NFD to NFC form
98+
func (n *Node) migrateFilename(oldName, newName string) syscall.Errno {
99+
if oldName == newName {
100+
return 0 // Nothing to do
101+
}
102+
103+
rn := n.rootNode()
104+
105+
// Get directory file descriptor
106+
dirfd, _, errno := n.prepareAtSyscallMyself()
107+
if errno != 0 {
108+
return errno
109+
}
110+
defer syscall.Close(dirfd)
111+
112+
// For plaintext names: simple rename
113+
if rn.args.PlaintextNames {
114+
err := syscallcompat.Renameat(dirfd, oldName, dirfd, newName)
115+
return fs.ToErrno(err)
116+
}
117+
118+
// For encrypted names: encrypt both names and rename
119+
iv, err := rn.nameTransform.ReadDirIVAt(dirfd)
120+
if err != nil {
121+
return fs.ToErrno(err)
122+
}
123+
124+
var encryptName func(int, string, []byte) (string, error)
125+
if rn.nameTransform.HaveBadnamePatterns() {
126+
encryptName = func(dirfd int, child string, iv []byte) (string, error) {
127+
return rn.nameTransform.EncryptAndHashBadName(child, iv, dirfd)
128+
}
129+
} else {
130+
encryptName = func(dirfd int, child string, iv []byte) (string, error) {
131+
return rn.nameTransform.EncryptAndHashName(child, iv)
132+
}
133+
}
134+
135+
oldCName, err := encryptName(dirfd, oldName, iv)
136+
if err != nil {
137+
return fs.ToErrno(err)
138+
}
139+
140+
newCName, err := encryptName(dirfd, newName, iv)
141+
if err != nil {
142+
return fs.ToErrno(err)
143+
}
144+
145+
err = syscallcompat.Renameat(dirfd, oldCName, dirfd, newCName)
146+
return fs.ToErrno(err)
147+
}
148+
149+
// prepareAtSyscall returns a (dirfd, cName) pair that can be used
150+
// with the "___at" family of system calls (openat, fstatat, unlinkat...) to
151+
// access the backing encrypted child file.
152+
func (n *Node) prepareAtSyscall(child string) (dirfd int, cName string, errno syscall.Errno) {
153+
if child == "" {
154+
tlog.Warn.Printf("BUG: prepareAtSyscall: child=%q, should have called prepareAtSyscallMyself", child)
155+
return n.prepareAtSyscallMyself()
156+
}
157+
158+
// On macOS, implement Unicode normalization with fallback and migration
159+
if runtime.GOOS == "darwin" && utf8.ValidString(child) {
160+
// Step 1: Always try NFC first (canonical storage form)
161+
normalizedChild := norm.NFC.String(child)
162+
dirfd, cName, errno = n.prepareAtSyscallDirect(normalizedChild)
163+
if errno == 0 {
164+
return dirfd, cName, 0 // Found NFC version
165+
}
166+
167+
// Step 2: Try alternate form if input was different
168+
if normalizedChild != child {
169+
// Input was NFD, try original NFD form
170+
dirfdNFD, cNameNFD, errnoNFD := n.prepareAtSyscallDirect(child)
171+
if errnoNFD == 0 {
172+
// Found NFD file - migrate it to NFC
173+
if errno := n.migrateFilename(child, normalizedChild); errno == 0 {
174+
// Migration successful, use NFC
175+
syscall.Close(dirfdNFD) // Close the NFD dirfd
176+
return n.prepareAtSyscallDirect(normalizedChild)
177+
} else {
178+
// Migration failed, use NFD
179+
return dirfdNFD, cNameNFD, 0
180+
}
181+
}
182+
}
183+
184+
// Step 3: If input was NFC, also try NFD as fallback
185+
if normalizedChild == child {
186+
nfdChild := norm.NFD.String(child)
187+
if nfdChild != child {
188+
dirfdNFD, cNameNFD, errnoNFD := n.prepareAtSyscallDirect(nfdChild)
189+
if errnoNFD == 0 {
190+
// Found NFD file - migrate it to NFC
191+
if errno := n.migrateFilename(nfdChild, normalizedChild); errno == 0 {
192+
// Migration successful, use NFC
193+
syscall.Close(dirfdNFD) // Close the NFD dirfd
194+
return n.prepareAtSyscallDirect(normalizedChild)
195+
} else {
196+
// Migration failed, use NFD
197+
return dirfdNFD, cNameNFD, 0
198+
}
199+
}
200+
}
201+
}
202+
}
203+
204+
return n.prepareAtSyscallDirect(child) // Non-macOS or fallback
205+
}
206+
95207
func (n *Node) prepareAtSyscallMyself() (dirfd int, cName string, errno syscall.Errno) {
96208
dirfd = -1
97209

internal/fusefrontend_reverse/node_dir_ops.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package fusefrontend_reverse
33
import (
44
"context"
55
"fmt"
6+
"runtime"
67
"syscall"
8+
"unicode/utf8"
79

810
"golang.org/x/sys/unix"
911

@@ -116,3 +118,14 @@ func (n *Node) readdirPlaintextnames(entries []fuse.DirEntry) (stream fs.DirStre
116118
}
117119
return fs.NewListDirStream(entries), 0
118120
}
121+
122+
// normalizeFilenameForDisplay converts stored filenames to the form expected by macOS GUI.
123+
// In reverse mode, we present the plaintext files as-is, but ensure proper display normalization.
124+
func normalizeFilenameForDisplay(name string) string {
125+
if runtime.GOOS == "darwin" && utf8.ValidString(name) {
126+
// For reverse mode, we typically want to preserve the original normalization
127+
// of the plaintext files, but ensure they display correctly
128+
return name
129+
}
130+
return name
131+
}

internal/fusefrontend_reverse/rpath.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ import (
44
"encoding/base64"
55
"log"
66
"path/filepath"
7+
"runtime"
78
"strings"
89
"syscall"
10+
"unicode/utf8"
11+
12+
"golang.org/x/text/unicode/norm"
913

1014
"github.com/rfjakob/gocryptfs/v2/internal/nametransform"
1115
"github.com/rfjakob/gocryptfs/v2/internal/pathiv"
@@ -35,6 +39,33 @@ func (rfs *RootNode) rDecryptName(cName string, dirIV []byte, pDir string) (pNam
3539
}
3640
return "", err
3741
}
42+
43+
// On macOS, handle Unicode normalization fallback for reverse mode
44+
if runtime.GOOS == "darwin" && utf8.ValidString(pName) {
45+
// Check if the decrypted name actually exists on disk
46+
pPath := filepath.Join(rfs.args.Cipherdir, pDir, pName)
47+
var st syscall.Stat_t
48+
if statErr := syscall.Stat(pPath, &st); statErr != nil {
49+
// Try the alternate Unicode form
50+
var alternateName string
51+
if norm.NFC.String(pName) == pName {
52+
// pName is NFC, try NFD
53+
alternateName = norm.NFD.String(pName)
54+
} else {
55+
// pName is NFD (or mixed), try NFC
56+
alternateName = norm.NFC.String(pName)
57+
}
58+
59+
if alternateName != pName {
60+
alternatePath := filepath.Join(rfs.args.Cipherdir, pDir, alternateName)
61+
var altSt syscall.Stat_t
62+
if altStatErr := syscall.Stat(alternatePath, &altSt); altStatErr == nil {
63+
// The alternate form exists, use it
64+
return alternateName, nil
65+
}
66+
}
67+
}
68+
}
3869
} else if nameType == nametransform.LongNameContent {
3970
dirfd, err := syscallcompat.OpenDirNofollow(rfs.args.Cipherdir, filepath.Dir(pDir))
4071
if err != nil {

0 commit comments

Comments
 (0)