Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
79c6e4e
test: expose generic cleanup recycle race
seonghobae Aug 11, 2026
99baa11
fix: fail generic cleanup closed without object-bound recycle
seonghobae Aug 11, 2026
7497329
fix: route generic cleanup through fail-closed authority
seonghobae Aug 11, 2026
04bdb50
Merge protected main into generic cleanup security line
seonghobae Aug 11, 2026
033c433
fix: avoid duplicate Tauri clean_paths command symbol
seonghobae Aug 11, 2026
50b1076
merge: converge generic cleanup authority with protected main
seonghobae Aug 11, 2026
d29f3fc
merge: converge generic cleanup authority with current main
seonghobae Aug 11, 2026
5919c44
merge: converge generic cleanup authority with protected main
seonghobae Aug 11, 2026
b32eb8d
test: preserve object-bound dev artifact cleanup route
seonghobae Aug 12, 2026
9bccaae
fix: preserve object-bound dev artifact cleanup route
seonghobae Aug 12, 2026
a20c4f3
merge: converge generic cleanup authority with current main
seonghobae Aug 12, 2026
e2ffab7
merge: converge generic cleanup authority with current main
seonghobae Aug 12, 2026
8250b3d
merge: converge generic cleanup with current main
seonghobae Aug 12, 2026
034e29c
merge: converge generic cleanup authority with current main
seonghobae Aug 12, 2026
844520c
merge: converge generic cleanup authority with current main
seonghobae Aug 12, 2026
8d88051
merge: converge generic cleanup authority with current main
seonghobae Aug 12, 2026
dc5ce7f
merge: converge generic cleanup authority onto current main
seonghobae Aug 23, 2026
a54159e
security: converge generic cleanup fail-closed authority onto current…
seonghobae Aug 24, 2026
a875e97
test: keep generic cleanup handler coverage-visible
seonghobae Aug 24, 2026
6421098
fix: keep generic cleanup fail-closed handler coverage-visible
seonghobae Aug 24, 2026
3864a8d
merge: converge fail-closed generic cleanup onto current main
seonghobae Aug 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 80 additions & 0 deletions src-tauri/src/generic_cleanup.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use crate::commands::CleanResult;

const IDENTITY_BOUND_RECYCLE_UNAVAILABLE: &str =
"generic-cleanup-identity-bound-recycle-unavailable";

fn clean_paths_inner(paths: &[String]) -> Vec<CleanResult> {
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.
///
/// 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. 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<String>) -> Result<Vec<CleanResult>, String> {
Comment thread
seonghobae marked this conversation as resolved.
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 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();
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());
}
}
4 changes: 3 additions & 1 deletion src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 node_navigation;
#[cfg_attr(coverage, allow(dead_code))]
pub mod cache_cleanup;
Expand Down Expand Up @@ -113,7 +115,7 @@ pub fn run() {
commands::clean_regenerable_caches,
cache_cleanup::list_cache_targets,
commands::list_dev_artifacts,
commands::clean_paths,
generic_cleanup::fail_closed_clean_paths,
Comment thread
seonghobae marked this conversation as resolved.
cache_cleanup::clean_cache_contents,
commands::clean_dev_artifacts,
commands::recent_operations,
Expand Down
50 changes: 50 additions & 0 deletions src-tauri/tests/generic_cleanup_mutation_authority.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
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");
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::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"
);
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]
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"
);
}
Loading