From 79c6e4e04198e76cf7e69c78163d4b46b9282d67 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 02:32:34 +0900 Subject: [PATCH 1/8] test: expose generic cleanup recycle race --- .../generic_cleanup_mutation_authority.rs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src-tauri/tests/generic_cleanup_mutation_authority.rs diff --git a/src-tauri/tests/generic_cleanup_mutation_authority.rs b/src-tauri/tests/generic_cleanup_mutation_authority.rs new file mode 100644 index 000000000..a31e2ba0a --- /dev/null +++ b/src-tauri/tests/generic_cleanup_mutation_authority.rs @@ -0,0 +1,25 @@ +use std::fs; +use std::path::PathBuf; + +fn source(path: &str) -> String { + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + fs::read_to_string(root.join(path)).expect("repository source must be readable") +} + +#[test] +fn generic_cleanup_route_must_fail_closed_before_path_consuming_recycle() { + let lib = source("src/lib.rs"); + + assert!( + lib.contains("mod generic_cleanup;"), + "the cleanup authority must live in its own fail-closed module" + ); + assert!( + lib.contains("generic_cleanup::clean_paths,"), + "the Tauri clean_paths route must use the fail-closed authority" + ); + assert!( + !lib.contains("commands::clean_paths,"), + "the path-consuming legacy cleanup route must not be registered" + ); +} From 99baa11bd47969e31dc3549016918157f7c1f966 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 02:42:28 +0900 Subject: [PATCH 2/8] fix: fail generic cleanup closed without object-bound recycle --- src-tauri/src/generic_cleanup.rs | 65 ++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 src-tauri/src/generic_cleanup.rs diff --git a/src-tauri/src/generic_cleanup.rs b/src-tauri/src/generic_cleanup.rs new file mode 100644 index 000000000..d9b4a68bc --- /dev/null +++ b/src-tauri/src/generic_cleanup.rs @@ -0,0 +1,65 @@ +use crate::commands::CleanResult; + +const IDENTITY_BOUND_RECYCLE_UNAVAILABLE: &str = + "generic-cleanup-identity-bound-recycle-unavailable"; + +fn clean_paths_inner(paths: &[String]) -> Vec { + paths + .iter() + .map(|path| CleanResult { + path: path.clone(), + ok: false, + error: IDENTITY_BOUND_RECYCLE_UNAVAILABLE.into(), + }) + .collect() +} + +/// Fail generic cleanup closed until the final reversible recycle primitive can remain bound to +/// the exact filesystem object that was authorized. Path revalidation cannot close a same-user +/// rename/symlink replacement window when the operating-system trash API consumes a pathname. +#[cfg(not(coverage))] +#[tauri::command] +pub fn clean_paths(paths: Vec) -> Result, String> { + Ok(clean_paths_inner(&paths)) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::fs; + + #[test] + fn refusal_is_stable_for_every_requested_path() { + let paths = vec!["first".to_string(), "second".to_string()]; + + let results = clean_paths_inner(&paths); + + assert_eq!(results.len(), 2); + assert_eq!(results[0].path, "first"); + assert!(!results[0].ok); + assert_eq!(results[0].error, IDENTITY_BOUND_RECYCLE_UNAVAILABLE); + assert_eq!(results[1].path, "second"); + assert!(!results[1].ok); + assert_eq!(results[1].error, IDENTITY_BOUND_RECYCLE_UNAVAILABLE); + } + + #[test] + fn refusal_does_not_mutate_the_named_filesystem_object() { + let tmp = tempfile::tempdir().unwrap(); + let victim = tmp.path().join("keep.bin"); + fs::write(&victim, b"keep").unwrap(); + let paths = vec![victim.to_string_lossy().into_owned()]; + + let results = clean_paths_inner(&paths); + + assert_eq!(results.len(), 1); + assert!(!results[0].ok); + assert_eq!(results[0].error, IDENTITY_BOUND_RECYCLE_UNAVAILABLE); + assert_eq!(fs::read(&victim).unwrap(), b"keep"); + } + + #[test] + fn empty_request_is_a_noop() { + assert!(clean_paths_inner(&[]).is_empty()); + } +} From 74973297224cda33c3620e986ecec437746abad0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 02:43:06 +0900 Subject: [PATCH 3/8] fix: route generic cleanup through fail-closed authority --- src-tauri/src/lib.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6dd64cb87..3354efc3c 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -7,6 +7,8 @@ mod dupes; #[cfg_attr(coverage, allow(dead_code))] mod commands; #[cfg_attr(coverage, allow(dead_code))] +mod generic_cleanup; +#[cfg_attr(coverage, allow(dead_code))] mod cache_cleanup; #[cfg_attr(coverage, allow(dead_code))] mod scanner; @@ -92,7 +94,7 @@ pub fn run() { commands::top_files, commands::list_cache_candidates, commands::list_dev_artifacts, - commands::clean_paths, + generic_cleanup::clean_paths, cache_cleanup::clean_cache_contents, commands::recent_operations, commands::expand_clean_targets, From 033c43381a5024efc014de0ad1c3dde9963ea84f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 03:04:30 +0900 Subject: [PATCH 4/8] fix: avoid duplicate Tauri clean_paths command symbol --- src-tauri/src/generic_cleanup.rs | 8 ++++++-- src-tauri/src/lib.rs | 2 +- src-tauri/tests/generic_cleanup_mutation_authority.rs | 11 ++++++++++- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/generic_cleanup.rs b/src-tauri/src/generic_cleanup.rs index d9b4a68bc..d1bf974ee 100644 --- a/src-tauri/src/generic_cleanup.rs +++ b/src-tauri/src/generic_cleanup.rs @@ -17,9 +17,13 @@ fn clean_paths_inner(paths: &[String]) -> Vec { /// Fail generic cleanup closed until the final reversible recycle primitive can remain bound to /// the exact filesystem object that was authorized. Path revalidation cannot close a same-user /// rename/symlink replacement window when the operating-system trash API consumes a pathname. +/// +/// The Rust function name is intentionally distinct from the legacy command wrapper. Tauri 2.11+ +/// maps this handler back to the stable external `clean_paths` IPC name without generating the +/// duplicate command macro symbol that the former same-named Rust function produced. #[cfg(not(coverage))] -#[tauri::command] -pub fn clean_paths(paths: Vec) -> Result, String> { +#[tauri::command(rename = "clean_paths")] +pub fn fail_closed_clean_paths(paths: Vec) -> Result, String> { Ok(clean_paths_inner(&paths)) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 3354efc3c..6e8efde86 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -94,7 +94,7 @@ pub fn run() { commands::top_files, commands::list_cache_candidates, commands::list_dev_artifacts, - generic_cleanup::clean_paths, + generic_cleanup::fail_closed_clean_paths, cache_cleanup::clean_cache_contents, commands::recent_operations, commands::expand_clean_targets, diff --git a/src-tauri/tests/generic_cleanup_mutation_authority.rs b/src-tauri/tests/generic_cleanup_mutation_authority.rs index a31e2ba0a..57a113376 100644 --- a/src-tauri/tests/generic_cleanup_mutation_authority.rs +++ b/src-tauri/tests/generic_cleanup_mutation_authority.rs @@ -9,17 +9,26 @@ fn source(path: &str) -> String { #[test] fn generic_cleanup_route_must_fail_closed_before_path_consuming_recycle() { let lib = source("src/lib.rs"); + let cleanup = source("src/generic_cleanup.rs"); assert!( lib.contains("mod generic_cleanup;"), "the cleanup authority must live in its own fail-closed module" ); assert!( - lib.contains("generic_cleanup::clean_paths,"), + lib.contains("generic_cleanup::fail_closed_clean_paths,"), "the Tauri clean_paths route must use the fail-closed authority" ); assert!( !lib.contains("commands::clean_paths,"), "the path-consuming legacy cleanup route must not be registered" ); + assert!( + cleanup.contains("#[tauri::command(rename = \"clean_paths\")]"), + "the fail-closed Rust handler must preserve the stable clean_paths IPC command name" + ); + assert!( + cleanup.contains("pub fn fail_closed_clean_paths("), + "the fail-closed Rust handler name must remain distinct from the legacy command macro" + ); } From b32eb8d59467314c7991735c019bf28a853d4ba8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 09:10:43 +0900 Subject: [PATCH 5/8] test: preserve object-bound dev artifact cleanup route --- src-tauri/tests/generic_cleanup_mutation_authority.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src-tauri/tests/generic_cleanup_mutation_authority.rs b/src-tauri/tests/generic_cleanup_mutation_authority.rs index 57a113376..a35b7dfc2 100644 --- a/src-tauri/tests/generic_cleanup_mutation_authority.rs +++ b/src-tauri/tests/generic_cleanup_mutation_authority.rs @@ -32,3 +32,13 @@ fn generic_cleanup_route_must_fail_closed_before_path_consuming_recycle() { "the fail-closed Rust handler name must remain distinct from the legacy command macro" ); } + +#[test] +fn generic_cleanup_repair_must_not_unregister_identity_bound_dev_artifact_cleanup() { + let lib = source("src/lib.rs"); + + assert!( + lib.contains("commands::clean_dev_artifacts,"), + "generic clean_paths hardening must preserve the separately object-bound clean_dev_artifacts IPC route" + ); +} From 9bccaaef24961e5f810e3368448720cd0b8a81f2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 09:11:34 +0900 Subject: [PATCH 6/8] fix: preserve object-bound dev artifact cleanup route --- src-tauri/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 6e8efde86..6a751afe3 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -96,6 +96,7 @@ pub fn run() { commands::list_dev_artifacts, generic_cleanup::fail_closed_clean_paths, cache_cleanup::clean_cache_contents, + commands::clean_dev_artifacts, commands::recent_operations, commands::expand_clean_targets, commands::find_duplicate_files, From a875e973c6ebed305492f56801628ca5f92bf103 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 03:07:05 -0700 Subject: [PATCH 7/8] test: keep generic cleanup handler coverage-visible --- src-tauri/tests/generic_cleanup_mutation_authority.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src-tauri/tests/generic_cleanup_mutation_authority.rs b/src-tauri/tests/generic_cleanup_mutation_authority.rs index a35b7dfc2..36cbfb758 100644 --- a/src-tauri/tests/generic_cleanup_mutation_authority.rs +++ b/src-tauri/tests/generic_cleanup_mutation_authority.rs @@ -31,6 +31,12 @@ fn generic_cleanup_route_must_fail_closed_before_path_consuming_recycle() { cleanup.contains("pub fn fail_closed_clean_paths("), "the fail-closed Rust handler name must remain distinct from the legacy command macro" ); + assert!( + !cleanup.contains( + "#[cfg(not(coverage))]\n#[tauri::command(rename = \"clean_paths\")]" + ), + "coverage builds must retain the shipped fail-closed clean_paths handler instead of compiling a different command surface" + ); } #[test] From 64210982bdc333477ef7e32ff34be3e1fbbbe2c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 03:07:32 -0700 Subject: [PATCH 8/8] fix: keep generic cleanup fail-closed handler coverage-visible --- src-tauri/src/generic_cleanup.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/generic_cleanup.rs b/src-tauri/src/generic_cleanup.rs index d1bf974ee..eff66e377 100644 --- a/src-tauri/src/generic_cleanup.rs +++ b/src-tauri/src/generic_cleanup.rs @@ -20,8 +20,9 @@ fn clean_paths_inner(paths: &[String]) -> Vec { /// /// The Rust function name is intentionally distinct from the legacy command wrapper. Tauri 2.11+ /// maps this handler back to the stable external `clean_paths` IPC name without generating the -/// duplicate command macro symbol that the former same-named Rust function produced. -#[cfg(not(coverage))] +/// duplicate command macro symbol that the former same-named Rust function produced. The handler +/// remains compiled in coverage builds so instrumentation measures the same fail-closed command +/// surface that ships to customers. #[tauri::command(rename = "clean_paths")] pub fn fail_closed_clean_paths(paths: Vec) -> Result, String> { Ok(clean_paths_inner(&paths)) @@ -47,6 +48,16 @@ mod tests { assert_eq!(results[1].error, IDENTITY_BOUND_RECYCLE_UNAVAILABLE); } + #[test] + fn public_handler_returns_the_same_fail_closed_results() { + let results = fail_closed_clean_paths(vec!["customer-file".to_string()]).unwrap(); + + assert_eq!(results.len(), 1); + assert_eq!(results[0].path, "customer-file"); + assert!(!results[0].ok); + assert_eq!(results[0].error, IDENTITY_BOUND_RECYCLE_UNAVAILABLE); + } + #[test] fn refusal_does_not_mutate_the_named_filesystem_object() { let tmp = tempfile::tempdir().unwrap();