Skip to content

Commit 2d7904f

Browse files
committed
Replace smb/smb-rpc with smb2 crate
- Swap `smb` 0.11.1 + `smb-rpc` =0.11.1 for `smb2` git dependency (same target gate) - Rewrite `smb_connection.rs`: `SmbClient::connect()` + `list_shares()` replaces the 3-step smb-rs flow - Rewrite `smb_util.rs`: `ErrorKind` matching replaces NtStatus/string heuristics, `convert_shares` replaces NDR debug-format parsing hacks - Update `smb_client.rs` orchestration for new error types and connection API - Update `lib.rs`: log filter `smb` → `smb2`, remove `sspi` filter - Update `docker_smb_test.rs` example for smb2 API - Tested against Docker SMB containers (guest + auth) and real network devices
1 parent 364ddf1 commit 2d7904f

11 files changed

Lines changed: 616 additions & 1618 deletions

File tree

Cargo.lock

Lines changed: 100 additions & 1226 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src-tauri/Cargo.toml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,8 @@ nusb = "0.2.3"
115115
bytes = "1"
116116
# mDNS discovery for network host browsing (pure Rust, cross-platform)
117117
mdns-sd = { version = "0.18", features = ["logging"] }
118-
# SMB protocol client for share enumeration (pure Rust)
119-
smb = "0.11.1"
120-
smb-rpc = "=0.11.1"
118+
# SMB2/3 protocol client for share enumeration (pure Rust, pipelined I/O)
119+
smb2 = { git = "https://github.com/vdavid/smb2", branch = "main" }
121120

122121
[target.'cfg(target_os = "macos")'.dependencies]
123122
# Drive indexing: NFD normalization for APFS case-insensitive collation

apps/desktop/src-tauri/examples/docker_smb_test.rs

Lines changed: 33 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@
33
//! Run with:
44
//! cargo run --example docker_smb_test
55
//!
6-
//! NOTE: This example only works on macOS (requires the `smb` crate).
6+
//! NOTE: This example only works on macOS/Linux (requires the `smb2` crate).
77
8-
#[cfg(target_os = "macos")]
8+
#[cfg(any(target_os = "macos", target_os = "linux"))]
99
mod inner {
10-
use smb::{Client, ClientConfig};
11-
use std::net::SocketAddr;
10+
use smb2::{ClientConfig, SmbClient};
11+
use std::time::Duration;
1212

1313
const TEST_PORT: u16 = 9445; // smb-guest Docker container
1414
const TEST_IP: &str = "127.0.0.1";
@@ -17,65 +17,54 @@ mod inner {
1717
pub async fn main() {
1818
println!("Testing Docker SMB container at {}:{}", TEST_IP, TEST_PORT);
1919

20-
// Create client with unsigned guest access allowed (required for test Docker servers)
21-
let mut config = ClientConfig::default();
22-
config.connection.allow_unsigned_guest_access = true;
23-
let client = Client::new(config);
20+
let config = ClientConfig {
21+
addr: format!("{}:{}", TEST_IP, TEST_PORT),
22+
timeout: Duration::from_secs(5),
23+
username: "Guest".to_string(),
24+
password: String::new(),
25+
domain: String::new(),
26+
auto_reconnect: false,
27+
compression: false,
28+
};
2429

25-
// Step 1: Connect to address with custom port
26-
// KEY FIX: Use IP address as server name to ensure consistent connection lookup
27-
let socket_addr: SocketAddr = format!("{}:{}", TEST_IP, TEST_PORT).parse().unwrap();
28-
println!("Step 1: connect_to_address('{}', {:?})", TEST_IP, socket_addr);
29-
30-
match client.connect_to_address(TEST_IP, socket_addr).await {
31-
Ok(_conn) => println!(" ✅ connect_to_address succeeded"),
32-
Err(e) => {
33-
println!(" ❌ connect_to_address failed: {:?}", e);
34-
return;
30+
// Step 1: Connect
31+
println!("Step 1: Connecting as Guest...");
32+
let mut client = match SmbClient::connect(config).await {
33+
Ok(client) => {
34+
println!(" Connected");
35+
client
3536
}
36-
}
37-
38-
// Step 2: Try ipc_connect with the IP address as server name
39-
println!("Step 2: ipc_connect('{}', 'Guest', '')", TEST_IP);
40-
41-
match client.ipc_connect(TEST_IP, "Guest", String::new()).await {
42-
Ok(_) => println!(" ✅ ipc_connect succeeded"),
4337
Err(e) => {
44-
println!(" ❌ ipc_connect failed: {:?}", e);
45-
46-
// Try an alternative approach - let's see if the connection is really established
47-
println!("\nDiagnostic: Checking if connection exists for '{}'...", TEST_IP);
48-
match client.get_connection(TEST_IP).await {
49-
Ok(_conn) => println!(" Connection exists"),
50-
Err(e) => println!(" No connection found: {:?}", e),
51-
}
38+
println!(" Connect failed: {:?}", e);
5239
return;
5340
}
54-
}
55-
56-
// Step 3: List shares
57-
println!("Step 3: list_shares('{}')", TEST_IP);
41+
};
5842

59-
match client.list_shares(TEST_IP).await {
43+
// Step 2: List shares
44+
println!("Step 2: Listing shares...");
45+
match client.list_shares().await {
6046
Ok(shares) => {
61-
println!(" Found {} shares:", shares.len());
47+
println!(" Found {} shares:", shares.len());
6248
for share in shares {
63-
println!(" - {:?}", share.netname);
49+
println!(
50+
" - {} (type={}, comment={:?})",
51+
share.name, share.share_type, share.comment
52+
);
6453
}
6554
}
6655
Err(e) => {
67-
println!(" list_shares failed: {:?}", e);
56+
println!(" list_shares failed: {:?}", e);
6857
}
6958
}
7059
}
7160
}
7261

73-
#[cfg(target_os = "macos")]
62+
#[cfg(any(target_os = "macos", target_os = "linux"))]
7463
fn main() {
7564
inner::main();
7665
}
7766

78-
#[cfg(not(target_os = "macos"))]
67+
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
7968
fn main() {
80-
println!("This example only works on macOS (requires the `smb` crate).");
69+
println!("This example only works on macOS/Linux (requires the `smb2` crate).");
8170
}

apps/desktop/src-tauri/src/indexing/writer.rs

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -661,32 +661,33 @@ fn handle_upsert_entry_v2(
661661
// would leave counts wrong because the old type's count isn't decremented.
662662
let old_entry = IndexStore::get_entry_by_id(conn, existing_id).ok().flatten();
663663
if let Some(ref old) = old_entry
664-
&& old.is_directory != is_directory {
665-
log::debug!(
666-
"Writer: UpsertEntryV2 type change for id={existing_id} \
664+
&& old.is_directory != is_directory
665+
{
666+
log::debug!(
667+
"Writer: UpsertEntryV2 type change for id={existing_id} \
667668
(was_dir={}, now_dir={is_directory}), converting to delete+insert",
668-
old.is_directory
669-
);
670-
if old.is_directory {
671-
handle_delete_subtree_by_id(conn, existing_id);
672-
} else {
673-
handle_delete_entry_by_id(conn, existing_id);
674-
}
675-
upsert_insert_new(
676-
conn,
677-
parent_id,
678-
&name,
679-
is_directory,
680-
is_symlink,
681-
logical_size,
682-
physical_size,
683-
modified_at,
684-
inode,
685-
should_dedup,
686-
next_id,
687-
);
688-
return;
669+
old.is_directory
670+
);
671+
if old.is_directory {
672+
handle_delete_subtree_by_id(conn, existing_id);
673+
} else {
674+
handle_delete_entry_by_id(conn, existing_id);
689675
}
676+
upsert_insert_new(
677+
conn,
678+
parent_id,
679+
&name,
680+
is_directory,
681+
is_symlink,
682+
logical_size,
683+
physical_size,
684+
modified_at,
685+
inode,
686+
should_dedup,
687+
next_id,
688+
);
689+
return;
690+
}
690691

691692
upsert_update_existing(
692693
conn,

apps/desktop/src-tauri/src/lib.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,9 @@ use notify as _;
2222
// drag is used by tauri-plugin-drag for drag-and-drop support
2323
use drag as _;
2424
//noinspection ALL
25-
// smb crates are used in network/smb_client module (macOS + Linux)
25+
// smb2 crate is used in network/smb_client module (macOS + Linux)
2626
#[cfg(any(target_os = "macos", target_os = "linux"))]
27-
use smb as _;
28-
//noinspection ALL
29-
#[cfg(any(target_os = "macos", target_os = "linux"))]
30-
use smb_rpc as _;
27+
use smb2 as _;
3128

3229
//noinspection ALL
3330
// trash crate is used in write_operations/trash.rs (Linux only)
@@ -257,8 +254,7 @@ pub fn run() {
257254
.level_for("nusb", log::LevelFilter::Warn)
258255
.level_for("zbus", log::LevelFilter::Warn)
259256
.level_for("tracing::span", log::LevelFilter::Warn)
260-
.level_for("smb", log::LevelFilter::Warn)
261-
.level_for("sspi", log::LevelFilter::Warn)
257+
.level_for("smb2", log::LevelFilter::Warn)
262258
.level_for("tao", log::LevelFilter::Warn);
263259

264260
// Parse RUST_LOG env var for per-module level overrides

apps/desktop/src-tauri/src/network/CLAUDE.md

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@ Discover, browse, and mount SMB network shares. Works on macOS and Linux.
77
- **Discovery**: `mdns_discovery.rs` — Pure Rust mDNS using `mdns-sd` crate. Cross-platform.
88
- **E2E testing**: `virtual_smb_hosts.rs` — Injects synthetic `NetworkHost` entries for Docker SMB containers. Gated behind `smb-e2e` Cargo feature. Never enabled in production.
99
- **Share listing**: Split across multiple files:
10-
- `smb_client.rs` — Top-level share-listing entry point; orchestrates guest -> keychain -> prompt auth flow; tries smb-rs first, falls back to smbutil (macOS only)
11-
- `smb_connection.rs` — TCP connection establishment and IPC-level share listing calls
10+
- `smb_client.rs` — Top-level share-listing entry point; orchestrates guest -> keychain -> prompt auth flow; tries smb2 first, falls back to smbutil (macOS only)
11+
- `smb_connection.rs` — TCP connection establishment and share listing via `smb2::SmbClient`
1212
- `smb_cache.rs` — 30-second in-memory cache for share lists, keyed by server address
1313
- `smb_smbutil.rs``smbutil view -G` fallback for older Samba/NAS servers (macOS); on Linux delegates to `smb_smbclient`
1414
- `smb_smbclient.rs``smbclient -L` fallback for Linux (requires `samba-client` package)
1515
- `linux_distro.rs` — Thin wrapper calling `crate::linux_distro::LinuxDistro` for smbclient install hints; `cfg(target_os = "linux")` gated
1616
- `smb_types.rs` — Shared types (`ShareInfo`, `AuthMode`, `ShareListError`, etc.)
17-
- `smb_util.rs` — Helpers: hostname derivation, IP resolution, account-name normalization
17+
- `smb_util.rs` — Helpers: error classification (`classify_error`, `is_auth_error`) and `convert_shares` (maps `smb2::ShareInfo` to Cmdr's `ShareInfo`)
1818
- **Mounting** (platform-specific via `#[path]` in `mod.rs`):
1919
- `mount.rs` — macOS `NetFSMountURLSync` for native `/Volumes/` mounts
2020
- `mount_linux.rs` — Linux `gio mount` for GVFS-based user-space mounts
@@ -28,7 +28,7 @@ Discover, browse, and mount SMB network shares. Works on macOS and Linux.
2828
| Component | macOS | Linux |
2929
|-----------|-------|-------|
3030
| mDNS discovery | `mdns-sd` (pure Rust) | `mdns-sd` (same) |
31-
| SMB share listing | `smb` + `smb-rpc` crates | `smb` + `smb-rpc` (same) |
31+
| SMB share listing | `smb2` crate (pure Rust) | `smb2` (same) |
3232
| smbutil fallback | `smbutil view -G` | `smbclient -L` (from `samba-client` package) |
3333
| Credential storage | `security-framework` (macOS Keychain) | `keyring` (Secret Service) → `cocoon` encrypted file fallback |
3434
| Mounting | `NetFSMountURLSync``/Volumes/` | `gio mount``/run/user/<uid>/gvfs/` |
@@ -47,16 +47,19 @@ Full UX control (login form appears in-pane), smart defaults (pre-fill username
4747
guest/credentials toggle. Uses `security-framework` crate for Keychain access. Passwords never stored in our settings
4848
file — only in Keychain. Linux uses `keyring` crate (Secret Service) with encrypted file fallback.
4949

50-
### `smb-rs` for SMB share enumeration (not `pavao`/libsmbclient or `smbutil`)
50+
### `smb2` for SMB share enumeration (not `pavao`/libsmbclient, `smb-rs`, or `smbutil`)
5151

5252
MIT license (compatible with BSL, allows dual-licensing for enterprise), pure Rust (no C dependencies), async-native
53-
(built on tokio), and cross-platform (macOS, Linux, Windows). `pavao` (libsmbclient wrapper) was rejected for its GPLv3
54-
license. `smbutil` CLI was rejected for fragile text parsing and process spawning. Fallback to `smbutil`/`smbclient` is
55-
available for older Samba servers where smb-rs's RPC fails.
53+
(built on tokio), cross-platform, and typed errors (`smb2::Error` variants vs string pattern matching). David's own
54+
crate — single dependency replaces the old `smb` + `smb-rpc` pair. `smb2::list_shares()` returns pre-filtered disk
55+
shares with clean `String` fields (no NDR parsing needed). Fallback to `smbutil`/`smbclient` is available for older
56+
Samba servers where smb2's RPC fails.
5657

5758
### Always use IP when available
5859

59-
smb-rs doesn't resolve `.local` hostnames reliably (std lib DNS doesn't handle mDNS). Always pass resolved IP from mDNS discovery. If IP unavailable, use derived hostname (`service_name_to_hostname`).
60+
smb2 uses the addr host component in UNC paths (`\\server\IPC$`). When hostname has a `.local` suffix, strip it
61+
before passing as addr (some servers reject `.local` in UNC paths). Always pass resolved IP from mDNS discovery when
62+
available. If IP unavailable, use derived hostname with `.local` stripped.
6063

6164
### Guest-first auth flow
6265

@@ -67,14 +70,14 @@ smb-rs doesn't resolve `.local` hostnames reliably (std lib DNS doesn't handle m
6770

6871
### smbutil / smbclient fallback
6972

70-
`smb` crate fails on older Samba servers (for example, Raspberry Pi) with RPC incompatibility. Classify error as `ProtocolError`, then try a platform-specific CLI fallback:
73+
`smb2` crate may fail on older Samba servers with RPC incompatibility. Classify error as `ProtocolError`, then try a platform-specific CLI fallback:
7174
- **macOS:** `smbutil view -G` (built-in).
7275
- **Linux:** `smbclient -L` (from `samba-client` package). If `smbclient` is not installed, returns a `MissingDependency` error with a distro-specific install command (detected via `/etc/os-release`). The `smb_smbutil.rs` Linux stubs delegate to `smb_smbclient.rs`.
7376
- **Other platforms:** stubs return `ProtocolError`.
7477

7578
### No persistent connection pool
7679

77-
smb-rs connections are lightweight and created on-demand. Caching is at the share list level (30s TTL), not TCP connection level.
80+
smb2 connections are lightweight (one `SmbClient` per connection) and created on-demand. Caching is at the share list level (30s TTL), not TCP connection level.
7881

7982
### In-memory credential cache
8083

@@ -99,3 +102,4 @@ On Linux, `keychain_linux.rs` tries Secret Service (GNOME Keyring / KDE Wallet)
99102
- **`ShareListError` uses internally tagged serde format** (`#[serde(tag = "type")]`) with struct variants. This keeps a flat JSON shape (`{ "type": "protocol_error", "message": "..." }`). The `MissingDependency` variant adds an optional `installCommand` field. When adding new variants, use struct syntax (not tuple).
100103
- **macOS smbutil and NetFSMountURLSync fail with loopback IP + non-standard port**: `//127.0.0.1:9445` gives "Broken pipe", but `//localhost:9445` works. `build_smbutil_url` and `NetworkMountView.svelte` both fall back to hostname when IP is `127.0.0.1` or `::1`. This matters for E2E testing against Docker containers on localhost.
101104
- **Mount URL must include port when non-standard**: `NetworkMountView.svelte` appends `:PORT` to the server string when `port !== 445`. Without this, `NetFSMountURLSync` defaults to port 445 and can't reach Docker containers on custom ports.
105+
- **Strip `.local` from addr for smb2**: `smb2::Connection::connect()` extracts `server_name` from the addr string and uses it in UNC paths. Passing `"foo.local:445"` creates `\\foo.local\IPC$` which some servers reject. The `build_addr` helper in `smb_connection.rs` handles this.

0 commit comments

Comments
 (0)