Rust Learning Path

This page links Rust concepts to the concrete places in the code where they appear — a common thread to learn along the way. It grows with the project.

Besides being a real product, wusel is deliberately also a way to dive into Rust — a learning project at ITBH. That is why this page exists, and why the code comments are sometimes fuller and more tutorial-flavoured than you would write for a codebase of nothing but experts: they often explain the why and the Rust concept in play, not just the what. If a comment feels like it is teaching, that is on purpose — read past it freely if you already know the ground.

Workspace & crates

The project is a Cargo workspace with several crates (wusel-core, wusel-fuse, wusel-desktop, wusel-mock, wusel) that share dependency versions via [workspace.dependencies]. A crate is the compilation unit; a workspace bundles several of them with a shared Cargo.lock and target/.

  • Read up: Cargo.toml (root) and the Cargo.toml of each crate.

Error handling: Result, enum, ?

Rust has no exceptions. Errors are values of type Result<T, E>.

  • wusel-core defines its own error enum (error.rs) with the thiserror derive — an error can have several variants (Http, Auth, LoginPending, …).

  • The ? operator returns an error early and converts it in the process automatically (thanks to #[from]).

  • wusel uses anyhow for convenient, context-rich errors in the binary — rule of thumb: thiserror in libraries, anyhow in binaries.

Ownership & borrowing (the core)

Every value has exactly one owner; a value is moved when you pass it on.

  • Real-world example: In webdav.rs, PartialEntry::finish(self) consumed the value. In a loop this led to a "use after move". Solution: std::mem::take(&mut cur) takes the value out and leaves a Default behind — the next iteration starts fresh. A typical Rust pattern.

Traits & the decorator pattern

A trait describes behavior (like an interface).

  • trait ContentSource: LiveWebDav implements it (reads live), CachingSource implements the same trait and wraps a live source (decorator). The FUSE layer knows only dyn ContentSource and notices nothing of the cache. See Architecture.

Traits as a test seam (and #[cfg(test)] modules)

The same trait mechanism is also how you get a dependency out of the way of a test — Rust has no mocking framework, and needs none.

  • trait Secrets in keyring.rs is everything credentials.rs needs from the OS keyring: four methods. The product passes the one real implementation (keyring::Os); the tests pass a HashMap that can be told to be absent, locked, or forgetful. Note what that changes: the interesting behaviour — falling back to the file — is no longer a property of the machine running the test.

  • &dyn Secrets is dynamic dispatch: one compiled function, the implementation chosen at runtime through a vtable. The alternative, a generic fn store<S: Secrets>(…), dispatches statically and compiles a copy per type. Here the call happens once per login, so the simpler signature wins.

  • A #[cfg(test)] pub(crate) mod fake lets the double live next to the real backend and still vanish from release builds. Test code can be shared across modules of the same crate this way — credentials.rs uses `keyring’s fake.

  • The risk of every double is that it drifts from the real thing. The answer is a contract test: one function taking &dyn Secrets, run against the fake and against Os. Written once, it holds both to the same promises.

Conditional compilation: #[cfg(…​)]

Code can be included or excluded per platform.

  • wusel-fuse/src/lib.rs: #[cfg(target_os = "linux")] includes the FUSE path only where a driver exists; otherwise a stub takes over. This way the build does not break on other platforms.

Cargo features

Optional functionality behind switches.

  • wusel has the fuse feature, which activates the optional dependency wusel-fuse (dep:wusel-fuse). Without the feature wusel builds as a pure CLI — handy on macOS (the mount is Linux-only). Build with: cargo build -p wusel --features fuse.

Async & the sync↔async bridge

Network I/O runs async via tokio (async fn, .await).

  • For this wusel starts a tokio::runtime::Runtime and calls block_on(…​) (see cmd_login in main.rs).

  • Tension: FUSE callbacks are synchronous/blocking. The adapter has to bridge the async world to the sync world — see the FUSE ↔ async bridge in the architecture.

Shared mutable state: Arc, Mutex, and an atomic in front of the lock

Rust has no synchronized: to share something mutable between threads you wrap it — Arc for shared ownership, Mutex for exclusive access.

  • health.rs (Reachability) is a compact example of all three moving parts. Several threads — the FUSE dispatchers, the syncer, the hydrator, the push listener — report every request outcome to one instance held as Arc<Reachability>.

  • An AtomicBool sits in front of the Mutex: the healthy path (a successful request, thousands per minute) is one relaxed atomic load and returns without ever taking the lock. The mutex is the truth; the atomic is only a hint that says "nothing is wrong, do not bother".

  • The lock is deliberately not held while notifying: the guard is dropped — by ending the block it lives in — before the D-Bus call, so a slow desktop cannot make requests queue behind it. Deciding inside the lock and acting outside it is the pattern to copy.

  • unwrap_or_else(|e| e.into_inner()) on lock() is poison tolerance: a thread that panicked while holding the lock must not take the whole mount down with it.

Tests

Unit tests live in a #[cfg(test)] mod tests right next to the code.

  • Examples: webdav.rs (PROPFIND parsing), state.rs (SQLite in-memory), config.rs. Run: mise run test.