Skip to content

Commit baaccc8

Browse files
committed
Bugfix: Fix SMB paths with accented characters
- macOS sends paths in NFD (decomposed Unicode, for example `a` + `\u{0308}` for `ä`) but SMB servers expect NFC (composed). Paths with accents failed with `STATUS_OBJECT_PATH_NOT_FOUND`. - NFC-normalize in `SmbVolume::to_smb_path()` using the existing `unicode-normalization` crate. - Document the SMB auto-upgrade lifecycle in volume `CLAUDE.md`.
1 parent 70d8d40 commit baaccc8

2 files changed

Lines changed: 24 additions & 2 deletions

File tree

apps/desktop/src-tauri/src/file_system/volume/CLAUDE.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,22 @@ Optional methods default to `Err(VolumeError::NotSupported)` or `false`, so new
5353

5454
`MtpVolume` is called from `tokio::task::spawn_blocking`, so `Handle::block_on` is safe inside its methods. However, `write_from_stream` must collect all chunks **before** entering `block_on` to avoid nested-runtime panics (stream chunks also use `block_on` internally).
5555

56+
## SMB auto-upgrade lifecycle
57+
58+
SMB mounts are automatically upgraded to `SmbVolume` (direct smb2 connection) in two scenarios:
59+
60+
1. **Startup** (`file_system::upgrade_existing_smb_mounts`): Scans registered volumes for `smbfs` type. Waits for mDNS
61+
discovery to reach `Active` state (polls every 500ms, up to 15s) because Keychain credentials are keyed by hostname
62+
(from mDNS), not IP (from `statfs`). Uses `tauri::async_runtime::spawn` (not `tokio::spawn` — runs during `setup()`
63+
before Tokio is fully available). Emits `volumes-changed` after upgrades so the frontend refreshes indicators.
64+
65+
2. **Mount detection** (`volumes/watcher.rs::try_upgrade_smb_mount`): When FSEvents detects a new volume in `/Volumes/`
66+
and it's `smbfs`, spawns a background upgrade attempt. By this point mDNS is already active.
67+
68+
Both paths check the `network.directSmbConnection` setting (global `AtomicBool`). Both are best-effort — failures log a
69+
warning and the volume stays as `LocalPosixVolume`. The "Connect directly" UI action (`upgrade_to_smb_volume` command)
70+
provides a manual upgrade path.
71+
5672
## Integration status
5773

5874
`LocalPosixVolume` is wired into the indexing subsystem. `VolumeManager` is actively used.

apps/desktop/src-tauri/src/file_system/volume/smb.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,13 +166,18 @@ impl SmbVolume {
166166
///
167167
/// The frontend sends paths relative to the volume root (which is the mount path).
168168
/// smb2 expects paths relative to the share root with `/` separators.
169+
/// NFC-normalizes the result because macOS sends NFD (decomposed) paths
170+
/// but SMB servers expect NFC (composed). Without this, paths with accented
171+
/// characters (like "ä") fail with STATUS_OBJECT_PATH_NOT_FOUND.
169172
fn to_smb_path(&self, path: &Path) -> String {
173+
use unicode_normalization::UnicodeNormalization;
174+
170175
let path_str = path.to_string_lossy();
171176

172177
// Handle paths that start with the mount path (absolute paths from frontend)
173178
if let Some(relative) = path_str.strip_prefix(self.mount_path.to_string_lossy().as_ref()) {
174179
let trimmed = relative.trim_start_matches('/');
175-
return trimmed.to_string();
180+
return trimmed.nfc().collect();
176181
}
177182

178183
// Handle empty or root paths
@@ -181,7 +186,8 @@ impl SmbVolume {
181186
}
182187

183188
// Strip leading slash for absolute paths
184-
path_str.strip_prefix('/').unwrap_or(&path_str).to_string()
189+
let raw = path_str.strip_prefix('/').unwrap_or(&path_str);
190+
raw.nfc().collect()
185191
}
186192

187193
/// Returns the full absolute path for a relative SMB path (under mount point).

0 commit comments

Comments
 (0)