clean-rust-filen-rs
SkillDev toolsLoad before writing, editing or reviewing Rust anywhere in this repository (filen-sdk-rs, filen-types, filen-cli, microthumb, tests included), together with the clean-rust skill. The house rules the maintainer enforces in review - what already exists and must be reused, module layout, the job and API shape to copy from fs/copy, error, FFI and wasm, drive-lock, untrusted-input, test and docs rules, and a pre-handoff checklist - plus the pinned toolchain, enabled unstable features, in-repo exemplars and known debt not to copy.
Instructions available. Your AI can read the instructions. Execution depends on the setup they require.
Account requirements not reviewed. Check the skill instructions before use; ahel provides instructions and does not run this skill.
Add ahel to your AI once: Claude, ChatGPT, Cursor, Claude Code or Codex. Then ask it to use this.
Then ask your AI: use the clean-rust-filen-rs skill
What this skill tells your AI
The instructions your AI receives, as published by filenclouddienste/filen-rs in .claude/skills/clean-rust-filen-rs/SKILL.md and read by ahel’s review.
Load the clean-rust skill first (.claude/skills/clean-rust/SKILL.md). It carries the generic
rules; this file carries the filen-rs facts and house rules.
Toolchain
rust-toolchain.toml pins channel = "nightly-2026-02-20" (rustc 1.95.0-nightly),
components = ["rust-src"], every crate on edition 2024. Its comment cites
higher-ranked-assumptions (rust-lang/rust#143545) for Send inference in handle_request.
higher-ranked-assumptions is a -Z compiler flag, not a #![feature] — probing
#![feature(higher_ranked_assumptions)] on this pin gives error[E0635]: unknown feature. It is
enabled as -Zhigher-ranked-assumptions in .cargo/config.toml ([build] rustflags and
every [target.*] rustflags table) and in the workflow env overrides
(git grep -n Zhigher-ranked-assumptions -- '*.toml' '*.yml' '*.yaml' lists every site);
keep it in every new target table and override. It is why the crate is on nightly at
all. Because it lives in build.rustflags, cargo +1.95.0 … run from inside the repo fails with
the option `Z` is only accepted on the nightly compiler — run stable comparisons from a cwd
outside the repo, or with RUSTFLAGS="".
Already enabled — no ask needed
| Crate root | Gate | One real use site |
|---|---|---|
filen-sdk-rs/src/lib.rs | min_specialization | default fn in the Serialize tower layer, auth/http/serialize.rs |
try_with_capacity | fallible alloc from an untrusted Content-Length, auth/http/download_body/full.rs | |
ascii_char | char::as_ascii() table lookup in filename validation, fs/name.rs | |
iter_intersperse | joining remote path segments with /, io/fs_tree/tree/mod.rs | |
cfg_select | wasm-vs-native SpawnTaskHandle::poll body, runtime.rs | |
filen-types/src/lib.rs | min_specialization | default fn into_data overridden for (), api/response.rs |
lib.rs says cfg_select is "stable in 1.95"; on this exact pin it still gates. Believe the
compiler, not the comment.
Compile-verified on this pin — worth a stop-and-ask when they make the code cleaner
| Gate | Stable shape you would otherwise write |
|---|---|
try_blocks | IIFE closure (|| { ...; Ok(y) })() to scope ? mid-fn |
return_type_notation | name + box an associated future just to write a Send bound |
arbitrary_self_types | a method on a custom smart pointer as fn f(this: &MyPtr<Self>) — self: &Arc<Self>, self: Rc<Self>, self: Pin<&mut Self> are stable already, no ask |
gen_blocks / coroutines | hand-rolled resumable iterator struct (FSTreeDFSIteratorWithPath) |
negative_impls | PhantomData<*const ()> to lose an auto trait implicitly |
type_alias_impl_trait, impl_trait_in_assoc_type | Pin<Box<dyn Future + Send>> associated type |
trait_alias | marker trait + blanket impl |
associated_type_defaults | blanket impl or macro-generated per-type impls |
box_patterns | match *b { .. } moves out of the Box on stable — ask only for nested box patterns |
iterator_try_collect, try_find, iter_map_windows, iter_array_chunks | collect::<Result<_,_>>(), manual loop, collect + windows |
map_try_insert | match m.entry(k) { Entry::Vacant(v) => v.insert(..), Entry::Occupied(o) => .. } (single lookup) |
btree_cursors | range per step |
default_field_values | ..Default::default() |
never_type | std::convert::Infallible |
Also compile on this pin, rarely worth an ask: closure_lifetime_binder, const_trait_impl,
adt_const_params, exhaustive_patterns, yeet_expr, try_trait_v2, async_fn_traits,
async_iterator. Internal-only, discouraged even though they compile: pattern_types,
unsized_fn_params, rustc_attrs — prefer a validated newtype (fs/name.rs) over pattern_types.
Just use it — stable on 1.95, no gate, no ask
let_chains (1.88), slice_as_chunks (1.88), result_flattening (1.89),
precise_capturing_in_traits (1.87), array_windows (1.94), if_let_guard (1.95). Also
Option::zip (1.46 — only zip_with is still gated) and moving out of a Box with match *b.
Do not use on this pin — pick the stable shape
Removed: associated_const_equality (error[E0557], removed in 1.94.0, merged into
min_generic_const_args). Compile but are incomplete_features — may crash the compiler, not
eligible for an ask: specialization (use min_specialization), generic_const_exprs,
inherent_associated_types, lazy_type_alias, non_lifetime_binders, deref_patterns,
never_patterns, explicit_tail_calls.
Exemplars — copy these shapes
| Pattern | Exemplar | What to look at |
|---|---|---|
| Validated newtype, one parser | filen-sdk-rs/src/fs/name.rs ValidatedName | private field; parse_name is the only entrance — Deserialize, TryFrom<&str>, uniffi try_lift all route through it. EntryNameError is the structured-error shape |
| Mint-only newtype | filen-types/src/stable_uuid.rs StableUuid | the server mints it, so no constructor: serde, FromSql, uniffi lift only, plus the test-seams hatch |
| Invariant = "the OS resolved this" | filen-sdk-rs/src/io/canonical_path.rs CanonicalPath | create_descendant_path guards each component before PathBuf::push |
Secret Debug | filen-sdk-rs/src/crypto/rsa.rs HMACKey | hand-written Debug prints a digest, never bytes |
| Marker generic | filen-sdk-rs/src/fs/categories/mod.rs Category | DirType<'a, Cat: Category + ?Sized> — the marker threaded through the whole item tree |
Required + blanket-Ext split | filen-types/src/traits.rs CowHelpersExt | required CowHelpers vs blanket ext. Same split at scale: fs/categories/fs.rs CategoryFS vs CategoryFSExt |
| Consuming transition | filen-sdk-rs/src/fs/file/write.rs into_waiting_for_drive_lock_state | per-phase structs; the drive-lock Arc exists only in the phases that must hold it |
| Unconstructible-until-finished | filen-sdk-rs/src/io/fs_tree/entry/mod.rs UnfinalizedDirEntry | DirEntry::from_unfinalized — child ranges unreadable until the subtree finishes |
| One type, borrowed + owned | filen-types/src/traits.rs CowHelpers (derive at filen-macros/src/lib.rs) | derive it instead of writing FooBorrowed/FooOwned; payoff at cache/state/event.rs CacheEventType<'a>, persisted as Arc<CacheEvent<'static>> (cache/state/mod.rs, Arc reason in the doc comment) |
cfg'd Send/Arc split | filen-sdk-rs/src/util.rs | MaybeSendBoxFuture/MaybeSendSync/MaybeSend/MaybeSendCallback/MaybeArc/MaybeArcWeak — the only place target_family = "wasm" may split Send/Arc |
| Borrowed parse | filen-mobile-native-cache/src/ffi/mod.rs UuidFfiId<'a> | &'a str slices, no String per segment |
| Borrow-first params | filen-sdk-rs/src/io/canonical_path.rs create_descendant_path | impl Iterator<Item = &'a str> + Clone, cloned once to precompute capacity. auth/mod.rs get_avatar_url — Arc<str> cloned to escape a read guard |
| Poll state machine | filen-sdk-rs/src/fs/file/write.rs FileWriterState | one variant per phase + terminal Error(&'a str); take_with_err = mem::replace; poll_close reinstalls the exact state on Pending; DummyFuture + FileWriterDefault is the default-callback trick |
| pin_project enum future | filen-sdk-rs/src/auth/http/download_body/full.rs DownloadBodyFuture | self.set(..) inside a loop to advance stages in one poll |
| Cancellation without a flag | filen-sdk-rs/src/socket/thread_handling.rs ListenerRegisterGuard | PinnedDrop + Option::take as the completion signal |
| Pin without projection | filen-sdk-rs/src/http_provider/mod.rs IdleTimeout | S: Unpin + get_mut()/Pin::new. auth/http/logging.rs LoggedFuture — thin delegating wrapper, no state machine |
| Errors / NonZero | filen-sdk-rs/src/error.rs InvalidTypeError | tiny thiserror struct per failure + impl_from! onto an opaque ErrorKind (FilenSdkError), with downcast / downcast_ref to get the fields back. consts.rs FILE_CHUNK_SIZE: NonZeroU32 — no downstream assert!(n > 0) |
Category is a marker-generic exemplar, not a sealing one: #[allow(private_bounds)] type Client: SharedClient (SharedClient is the pub(crate) trait at
auth/shared_client.rs) only restricts which client type an impl may pick — auth::Client is
pub and implements it, so a downstream impl Category for X { type Client = Client, .. } compiles.
Seal with the clean-rust skill's private-module Sealed supertrait if closure ever matters.
Repo conventions
- Hard tabs (
rustfmt.toml: hard_tabs = true). The editor/agent save-formatter does not match this repo's nightlycargo fmt; runcargo fmt -p <crate>before staging or pre-commit fails. - The root
Cargo.toml's[workspace.lints](every member opts in with[lints] workspace = true) deniesunsafe_code,clippy::dbg_macro,clippy::todo,clippy::self_named_module_files,clippy::option_option,clippy::struct_excessive_bools,clippy::string_sliceandclippy::cast_possible_truncation. Every hit that predates a lint carries an#[allow]whose comment gives the reason; a new hit gets a rewrite, or an allow with its own reason. The exception iscast_possible_truncation, which has no allows at all: a cast that could truncate becomesT::try_from(with?, a saturatingunwrap_or, or anexpectnaming the invariant), and a float-to-int onenum_traits::ToPrimitive; the diff policy blocks an allow of it or ofclippy::pedantic. There is noclippy.toml, and nothing enforces the import or allow-comment conventions mechanically; review does. pin_project(proc-macro,project = Name) is used only underfilen-sdk-rs/src/auth/http/; everything else (runtime.rs,socket/thread_handling.rs,js/managed_futures/*) usespin_project_lite. Follow the neighbourhood; do not mix them in one file.- Baseline
unsafe: every existing site sits under an#[allow(unsafe_code)]whose comment says what the unsafe is for —git grep -n "allow(unsafe_code)" -- '*.rs'is the inventory. Two are wide: the module-level one onfilen-types/src/serde/str(fixed-size string newtypes) and the crate-level one inheif-decoder/src/lib.rs(the C FFI surface); the rest cover one small module, item, statement or match arm (filen-sdk-rs/src/runtime.rs,fs/file/chunk.rs,crypto/{v1,v2,v3}.rs,js/**wasm ABI lifts,obs/mod.rs,filen-mobile-native-cache/src/{auth.rs,env.rs}, a few tests). New unsafe outside those scopes fails the lint; adding an allow or widening one needs the ask from theclean-rustskill and a// SAFETY:comment, andscripts/check-diff-policy.shblocks added or changed unsafe lines either way. filen-mobile-native-cacheis UniFFI: build/test with-p filen-mobile-native-cache.- Touching API request/response types or serde? Pair with the
types-serde-conventionsskill.
Known debt — do not copy
Fixing any of these is its own commit, offered before it is made.
fs/dir/meta.rspub name: Cow<'a, str>—fs/dir/mod.rsvalidates viaValidatedNamethen.into()s the proof away, and thepubfield lets anyone skip it.DirectoryMetaChanges(meta.rs) keepsOption<ValidatedName>— that is the shape to follow.filen-mobile-native-cache/src/ffi/mod.rsFfiId(pub String)— documented grammar, zero enforcement, infallibleFrom<&str>; every reader re-parses. Parse once into an enum.filen-cli/src/util.rsRemotePath(pub(crate) String)— crate-visible field bypassesnew()'s leading-slash normalization that every method assumes.fs/file/write.rsupload_key: Arc<String>— read only as&str;Arc<str>is one allocation.fs/file/write.rshand-rollsArc::try_unwrap(..).unwrap_or_else(|a| (*a).clone());Arc::unwrap_or_clone(x)is stable std.filen-mobile-native-cache/src/io/mod.rsio_upload_updated_file'sname: Stringparam is only borrowed inside, forcingmeta.name.clone()atfilen-mobile-native-cache/src/remote.rs. Same fn'smime: Stringis genuinely consumed — the contrast is the lesson.fs/file/write.rswrite-after-close answered withio::Error::other("..."); its cascade needs deadunreachable!()arms;#[allow(clippy::large_enum_variant)]with no stated tradeoff. Do not grow this file's cascade — new phases go behind consuming transitions.- House answer for poll-after-
Readyis the typed terminal variant (FileWriterState::Error(&'a str),write.rs) returned asReady(Err(..)).fs/file/exif.rspanic!("poll_finalize called after completion")is the outlier — convert it when touched; add no new panics. filen-mobile-native-cache/src/error.rs— 15 stringly variants overErrorContext(Cow);.context()re-format!s and the source type is unrecoverable.filen-sdk-rs/src/error.rs(boxed source +downcast) already has the pattern to copy.fs/file/meta.rsbareserde_json::to_string(..).unwrap(); the sibling atfs/dir/meta.rsshows the house style (justifying comment +.expect).- No trait here uses the private-module
Sealedform yet; the three#[allow(private_bounds)]sites (filen-types/src/traits.rs,fs/categories/mod.rs,fs/categories/fs.rs) suppress the lint instead, none of them with a comment naming the tradeoff (fs.rsexplains only the neighbouringasync_fn_in_traitallow), andtraits.rs's blanketimpl<T: CowHelpers> Sealed for Tseals nothing. New sealed traits use theclean-rustskill's form. #[must_use]appears nowhere in the repo. Do not add it in feature work; if builders or fallible constructors need it, propose that as a separate change.
House rules the maintainer enforces in review
Every rule below was a review correction on a real branch. Apply them while writing, then
walk the checklist at the end before handing work over. The house-review skill runs the
same rules over a finished branch.
Reuse before writing
Search before adding a helper, constant, type or dependency: git grep -n NAME -- '*.rs'.
What already exists on main:
| Need | Use |
|---|---|
| Long-running drive job plumbing (control, cancellation, accounting) | crate::job (job.rs; test helpers in job::test_support) |
| Spawning tasks on native and wasm | crate::runtime (spawn_local, spawn_async, spawn_task_maybe_send) |
| Chunk size, open-file limit and other shared numbers | crate::consts (CHUNK_SIZE_U64, MAX_OPEN_FILES); tests derive sizes from these |
Send/Sync bounds and Arc that differ on wasm | the MaybeSend* / MaybeArc aliases in util.rs |
| Name validation | ValidatedName (fs/name.rs) |
| Holding the drive lock | Client::lock_drive (sync/mod.rs) |
| Client-wide settings a job reads | a Client accessor such as thumbnails() (auth/mod.rs), not a copy in each job's config |
| Listing and creating items from a job | the fs::dir / fs::file client_impl helpers |
| Catching a panic in async work | AssertUnwindSafe(fut).catch_unwind() as in catch_parse_panic (fs/file/exif.rs); native and uniffi only — wasm32 traps before unwinding |
Debug for a type holding a secret | the hashed Debug on FileKey (crypto/v1.rs) |
| Ordered callback delivery on uniffi | crate::js::spawn_ordered_dispatch (js/uniffi.rs, uniffi-only) |
| Live-test accounts, directories and cleanup | test-utils |
Check std and the crates already in Cargo.toml before writing an algorithm by hand (for
example cbc for CBC chaining, str::floor_char_boundary). Adding a crate is the
maintainer's decision.
When a second job needs something the first job has, lift it into a shared module
(normally crate::job), migrate every caller and delete the original in the same series.
Never copy it and adapt. Code in a shared module is named job-neutrally, and core modules
(fs, auth, io) never import from a feature module such as fs::copy.
One type per concept: sibling jobs share their item and error types, and event payloads embed the entry structs instead of re-declaring their fields. Extract a repeated block on its second occurrence.
Module layout and size
- A module with children is
foo/mod.rs; neverfoo.rsnext tofoo/. - Split work into named phases (see
fs/copy/plan.rsandfs/copy/engine) instead of one long function, and group more than a handful of parameters into an options struct. Do not add a newtoo_many_argumentsallow. - Visibility starts private, then
pub(crate);pubonly for items re-exported on the crate's public surface. A public module is private withpub useof its items, and every item has exactly one export path. - Imports go in one
useblock at the top of the file: no function-localuse, no aliases that shadow a crate module. - A new module that parses untrusted input or runs a job may copy the header of
cache/mod.rs, the one module that turns extra lints on:#![warn(unreachable_pub, unused_qualifications)]. Indexing and arithmetic follow the rules under Validation below.
Public API and job shape: copy fs/copy
- A job's public entry point returns
Result<XReport, XFailed>, whereXFailedcarries the partial report (seecopy_itemsinfs/copy/client_impl.rs). No outcome enums. - Entry point:
self: Arc<Self>taking(.., config, callback, control)likecopy_items, holdingclient.lock_drive()for the destructive phase (seefs/copy/backend.rs). - Vocabulary:
XConfig,XReport,XFailed,XCallback. Names must not clash with existingClientmethods. - Callback trait methods are
on_<event>; batch variants say so in their name; producer-side types never useon_*(seefs/copy/report.rsandio/dir_upload.rs). - Public types: no single-variant enums, no
Optionthat is alwaysSome, no positional tuples, no request type reused as a response. Do not leak internal generic parameters; expose a concrete alias. - State is an enum, not a set of booleans.
- Every success, failure and cancel path returns a complete report of what was done.
Validation, arithmetic and untrusted input
- Validate at the boundary and carry the proof in a type (
ValidatedName). Invalid input is rejected, never silently sanitised. FFI entry points validate before the job starts. - No sentinel values (use
Option), noascasts that can truncate (usetry_from), no unchecked indexing (use.get()).debug_assert!is not a guard. - Size limits are exceeded only when
needed > limit. Use one helper for the comparison and test bothmaxandmax + 1. - Counters and sums over external data use
checked_*arithmetic, and a loop that generates candidate names must provably make progress. Test theu64::MAXboundary (seefs/copy/naming.rsandcache/state/mod.rs). - Bound anything an archive or a server response can amplify: entry count, expanded size,
path depth. Collision handling stays linear. If a gap is accepted, say so in a plain
comment at the site (see
io/canonical_path.rsandio/dir_download.rs). - Hash maps keyed by untrusted data keep the default randomly seeded hasher.
Errors
- Wrapping an error keeps
server_code(),inner_message(),kind()and downcasting working through the wrapper; add a test (seeerror.rsand its tests). - Never rebuild an error from its string (
Error::custom(kind, e.to_string())); keep the source. - Module errors are typed enums with
thiserrorandFromimpls, reached through theerror.rsdowncast helpers. Noanyhowinfilen-sdk-rs. - The same condition maps to the same
ErrorKindeverywhere; invariant violations areErrorKind::Internal. - Never swallow an error: return it, or log it with
warn!including the cause. No.ok()?on a listing; when unsure, fail closed. - No
unwrap()outside tests.expect("... (should be impossible)")is for true invariants only. Untrusted input returnsErrand never panics. Nodebug_assert!(false).
FFI and wasm
- Binding types use
#[js_type]; never hand-writeSerialize,Tsifyor uniffi derives for them. If the macro needs extending, changefilen-macrosin its own commit. - Errors cross FFI as
Arc<Error>on uniffi andJsValue::fromon wasm; no error DTOs. - A job's bindings live in one
js_impl.rswithuniffi_implandwasm_implsubmodules (seefs/copy/js_impl.rs). - Optional JS callbacks are checked with
is_undefined() || is_null()(seeconnect/js_impls.rs), never replaced withFunction::default(). On uniffi, deliver callbacks in order withspawn_ordered_dispatch; on wasm, followwasm_implinfs/copy/js_impl.rs. - Gate wasm code on
feature = "wasm-full"and use theMaybeSendaliases. A re-export carries the samecfgas its users. - Every native config knob is mirrored in
JsClientConfig(auth/http/mod.rs) in the same commit, with the same meaning on every platform. - Follow the sibling's resolve/reject convention (copy resolves with the report on
failure). Document any deviation and cover it in
web/main.test.ts.
Compatibility and sibling semantics
- Anything shipped (public Rust items, FFI exports, error messages, callback names and payloads) is not renamed or reworded without the maintainer's sign-off. If it is approved, keep an alias and make the change a standalone commit.
- A new job matches its sibling's semantics (limits, conflict handling, cancellation, error states). Document every intended difference at the site.
Drive lock and concurrency
- Verify-then-act on the drive happens under one
let _lock = client.lock_drive().await?scope, and items are identified by uuid and parent, not by name (seefs/dir/client_impl.rsandfs/copy/backend.rs). - Never wait without a bound while holding the lock; its lease is about 60 seconds.
- Concurrency limits use
clamp(1, Semaphore::MAX_PERMITS)(seeauth/http/mod.rs),consts::MAX_OPEN_FILES, and a boundedFuturesUnorderedwindow. - Spawn through
crate::runtime; do not calltokio::spawndirectly in code that also builds for wasm.
Security and dependencies
- No
zeroizeand no wiping of keys or buffers (maintainer decision). Secret-holding types implement a redactingDebug(seecrypto/v1.rs). - No new
unsafe(includingunsafe impland custom allocators) without the maintainer's permission. Existingunsafecarries a// SAFETY:comment. - No
vendor/and no[patch]. A new git dependency is a fork under a maintainer-controlled account, pinned byrev(precedent:async_zipfromEnduriel/rs-async-zipinfilen-sdk-rs/Cargo.toml). Never modify the vendored libheif or libde265. - A new dependency (after approval) is optional behind the feature that needs it (
dep:), is never a second major version of an existing crate, carries a comment on any pin, and passestaplo fmt.
Tests
Shortened here. Read the whole file on GitHub.
Signals
- GitHub stars
- 95
- Forks
- 9
- Last commit
- Oct 2026
Advanced
- Item type
- skill
- Key
clean-rust-filen-rs- Source
- github.com/filenclouddienste/filen-rs
github.com/filenclouddienste/filen-rs
Related picks
Skill · thedaviddias
The pick for JavaScriptmodern-javascript-patterns
Skill · wshobson
The pick for JavaScriptomh-rust
Skill · rlaope
The pick for Rustrust-sdk-specialist
Skill · a5c-ai
The pick for Rustsetup-ts-deep-modules
Skill · mattpocock
The pick for TypeScripttypescript-pro
Skill · jeffallan
The pick for TypeScript