From 0c2fa841ca74453e9ed6da06ff0cae925fd2e8a8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 09:16:17 +0900 Subject: [PATCH 1/8] fix: audit cache symlinks without traversal --- CHANGELOG.md | 2 + docs/product-technical-gap-baseline.md | 11 +++++ src-tauri/src/generated_cache_reclaim.rs | 58 ++++++++++++++++++++++-- 3 files changed, 66 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 622ceff93..8a2762d05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and options without echoing attacker-controlled option payloads. - Add an exact-allowlist generated-cache auditor that defaults to dry-run, blocks live processes, + fingerprints cache-internal symbolic links without following them, and continues to reject an + allowlisted root that is itself a symbolic link. tool locks, registered or dirty temporary Git workspaces, and provider/Photos/VM boundaries, and requires a fingerprint-bound approval plus a crash-recoverable private JSON Lines receipt before removal. Add a plan-first CLI; temporary Git workspaces remain audit-only and route to the diff --git a/docs/product-technical-gap-baseline.md b/docs/product-technical-gap-baseline.md index 118537578..71fd65f1e 100644 --- a/docs/product-technical-gap-baseline.md +++ b/docs/product-technical-gap-baseline.md @@ -1183,6 +1183,17 @@ runner's private workspace temp root instead of weakening the shared production the approved cloud items remain intact and locally materialized until that native path passes post-allocation verification. +## 2026-08-30 generated-cache symbolic-link boundary + +- Live execution-readiness audits found that the allowlisted uv and Playwright caches contain + symbolic-link entries. The prior all-or-nothing rejection prevented DiskSage from producing an + approval fingerprint, allocated-byte estimate, or activity blockers for these ordinary caches. +- The manifest now fingerprints a link's own target text without resolving, opening, or traversing + its destination. An allowlisted root that is itself a symbolic link remains denied, special files + remain denied, and fresh evidence plus exact human approval are still mandatory before removal. +- Focused regression tests prove that changing content outside the cache through a child link does + not alter the cache manifest and that a root link cannot enter the reclaim contract. + ## 2026-08-29 temporary-workspace generated-cache recovery - Project-local Python 3.14 `.venv314` environments now share the same manifest, active-use, journal, and permanent-reclaim checks as `.venv`. diff --git a/src-tauri/src/generated_cache_reclaim.rs b/src-tauri/src/generated_cache_reclaim.rs index e5b6d4878..f9cea7ccb 100644 --- a/src-tauri/src/generated_cache_reclaim.rs +++ b/src-tauri/src/generated_cache_reclaim.rs @@ -171,10 +171,14 @@ fn observe_tree(path: &Path) -> Result<(u64, u64, String, Vec), String> } let metadata = std::fs::symlink_metadata(¤t) .map_err(|_| "generated-cache-metadata-unavailable".to_string())?; - if metadata.file_type().is_symlink() { - return Err("generated-cache-symlink-rejected".into()); + let is_symlink = metadata.file_type().is_symlink(); + // The allowlisted cache root itself must remain a real directory. Symlinks below that + // root are ordinary cache entries: fingerprint the link text without resolving or + // traversing it, so a link can never extend the deletion boundary. + if current == path && is_symlink { + return Err("generated-cache-root-symlink-rejected".into()); } - if !metadata.is_dir() && !metadata.is_file() { + if !metadata.is_dir() && !metadata.is_file() && !is_symlink { return Err("generated-cache-special-file-rejected".into()); } entries += 1; @@ -184,7 +188,9 @@ fn observe_tree(path: &Path) -> Result<(u64, u64, String, Vec), String> .map_err(|_| "generated-cache-relative-path-failed")?; hasher.update(&(relative.as_os_str().as_bytes().len() as u64).to_le_bytes()); hasher.update(relative.as_os_str().as_bytes()); - hasher.update(if metadata.is_dir() { + hasher.update(if is_symlink { + b"symlink" + } else if metadata.is_dir() { b"directory" } else { b"file" @@ -200,7 +206,13 @@ fn observe_tree(path: &Path) -> Result<(u64, u64, String, Vec), String> if current.file_name().is_some_and(|name| name == ".lock") { locks.push(current.to_string_lossy().into_owned()); } - if metadata.is_file() { + if is_symlink { + let target = std::fs::read_link(¤t) + .map_err(|_| "generated-cache-symlink-unreadable".to_string())?; + let target_bytes = target.as_os_str().as_bytes(); + hasher.update(&(target_bytes.len() as u64).to_le_bytes()); + hasher.update(target_bytes); + } else if metadata.is_file() { estimated_content_bytes = estimated_content_bytes .checked_add(metadata.len()) .ok_or("generated-cache-content-bound-exceeded")?; @@ -601,6 +613,7 @@ where #[cfg(test)] mod tests { use super::*; + use std::os::unix::fs::symlink; use std::path::PathBuf; fn inactive() -> GeneratedCacheActivityEvidence { @@ -660,6 +673,41 @@ mod tests { std::fs::remove_dir_all(root).unwrap(); } + #[test] + fn cache_child_symlink_is_fingerprinted_without_following_target() { + let temp = tempfile::tempdir().unwrap(); + let home = temp.path(); + let root = home.join(".cache/uv"); + let outside = temp.path().join("customer-data"); + std::fs::create_dir_all(&root).unwrap(); + std::fs::write(&outside, b"must not be read or traversed").unwrap(); + symlink(&outside, root.join("cached-link")).unwrap(); + + let before = plan_with_evidence(&root, home, inactive(), 1).unwrap(); + std::fs::write(&outside, b"changed outside content").unwrap(); + let after = plan_with_evidence(&root, home, inactive(), 2).unwrap(); + + assert_eq!(before.content_fingerprint, after.content_fingerprint); + assert_eq!(before.allocated_bytes, after.allocated_bytes); + assert_eq!(before.entry_count, 2); + } + + #[test] + fn allowlisted_root_symlink_remains_denied() { + let temp = tempfile::tempdir().unwrap(); + let home = temp.path(); + let outside = temp.path().join("outside-cache"); + std::fs::create_dir_all(&outside).unwrap(); + std::fs::create_dir_all(home.join(".cache")).unwrap(); + let root = home.join(".cache/uv"); + symlink(&outside, &root).unwrap(); + + assert_eq!( + plan_with_evidence(&root, home, inactive(), 1).unwrap_err(), + "generated-cache-root-symlink-rejected" + ); + } + #[test] fn provider_and_virtual_machine_boundaries_are_never_admitted() { let home = Path::new("/Users/test"); From 121546ea64af253c1c39719dfb37b92ff033e7b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:05:45 +0900 Subject: [PATCH 2/8] test: align generated cache authority fixtures --- src-tauri/src/cache_manifest_symlink_tests.rs | 19 ++++++++++++------- src-tauri/src/generated_cache_reclaim.rs | 9 +++++---- 2 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src-tauri/src/cache_manifest_symlink_tests.rs b/src-tauri/src/cache_manifest_symlink_tests.rs index b683b8883..f97fbc601 100644 --- a/src-tauri/src/cache_manifest_symlink_tests.rs +++ b/src-tauri/src/cache_manifest_symlink_tests.rs @@ -46,9 +46,8 @@ fn manifest_variable_fields_are_length_framed() { let target_path = cache_root.join("ab"); fs::write(&target_path, b"generated-payload").expect("write generated cache fixture"); - let targets = crate::rules::cache_targets(&cache_root).expect("enumerate cache targets"); - assert_eq!(targets.len(), 1); - let target = &targets[0]; + let target = crate::rules_catalog::cache_target(&target_path) + .expect("snapshot the relocation-stable catalog manifest"); let mut expected = blake3::Hasher::new(); update_framed( @@ -60,7 +59,10 @@ fn manifest_variable_fields_are_length_framed() { ); expected.update(&[0]); let metadata = fs::symlink_metadata(&target_path).expect("read generated cache metadata"); - update_framed(&mut expected, crate::rules::cache_metadata_fingerprint(&metadata).as_bytes()); + update_framed( + &mut expected, + crate::rules_catalog::cache_metadata_fingerprint(&metadata).as_bytes(), + ); update_framed(&mut expected, target.object_id.as_bytes()); assert_eq!( @@ -80,7 +82,8 @@ fn reviewed_directory_snapshot_binds_root_ctime_before_staging() { fs::write(target.join("payload.bin"), b"generated") .expect("write generated cache payload"); - let reviewed = crate::rules::cache_target(&target).expect("snapshot reviewed cache target"); + let reviewed = crate::rules::cache_authority_target(&target) + .expect("snapshot reviewed cache authority"); let before = fs::symlink_metadata(&target).expect("read reviewed root metadata"); let original_mode = before.permissions().mode() & 0o7777; let temporary_mode = if original_mode & 0o100 != 0 { @@ -103,7 +106,8 @@ fn reviewed_directory_snapshot_binds_root_ctime_before_staging() { (after.ctime(), after.ctime_nsec()), "fixture must produce a ctime-only root metadata transition" ); - let live = crate::rules::cache_target(&target).expect("snapshot live cache target"); + let live = crate::rules::cache_authority_target(&target) + .expect("snapshot live cache authority"); assert_eq!(reviewed.object_id, live.object_id); assert_eq!(reviewed.modified_ms, live.modified_ms); assert_ne!( @@ -121,7 +125,8 @@ fn permanent_delete_rejects_ctime_only_root_drift() { fs::create_dir(&target_path).expect("create generated cache target"); fs::write(target_path.join("payload.bin"), b"generated") .expect("write generated cache payload"); - let reviewed = crate::rules::cache_target(&target_path).expect("snapshot reviewed cache target"); + let reviewed = crate::rules::cache_authority_target(&target_path) + .expect("snapshot reviewed cache authority"); let before = fs::symlink_metadata(&target_path).expect("read reviewed root metadata"); let original_mode = before.permissions().mode() & 0o7777; let temporary_mode = if original_mode & 0o100 != 0 { diff --git a/src-tauri/src/generated_cache_reclaim.rs b/src-tauri/src/generated_cache_reclaim.rs index f9cea7ccb..7e2cd3a91 100644 --- a/src-tauri/src/generated_cache_reclaim.rs +++ b/src-tauri/src/generated_cache_reclaim.rs @@ -614,7 +614,6 @@ where mod tests { use super::*; use std::os::unix::fs::symlink; - use std::path::PathBuf; fn inactive() -> GeneratedCacheActivityEvidence { GeneratedCacheActivityEvidence { @@ -660,8 +659,11 @@ mod tests { .blockers .contains(&"process-active".into())); } - let root = PathBuf::from("/private/tmp/disksage-generated-cache-test-dirty"); - std::fs::create_dir_all(&root).unwrap(); + let root_dir = tempfile::Builder::new() + .prefix("disksage-generated-cache-test-dirty-") + .tempdir_in(crate::rules::shared_temp_root()) + .unwrap(); + let root = root_dir.path().to_path_buf(); let mut evidence = inactive(); evidence.git_dirty = true; evidence.git_worktree_registered = true; @@ -670,7 +672,6 @@ mod tests { assert!(plan .blockers .contains(&"temporary-workspace-specialized-executor-required".into())); - std::fs::remove_dir_all(root).unwrap(); } #[test] From 96ad57e6775d7a4ce0704825b6686789e24186a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:08:44 +0900 Subject: [PATCH 3/8] fix: keep generated cache audit portable --- src-tauri/src/duplicate_audit.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src-tauri/src/duplicate_audit.rs b/src-tauri/src/duplicate_audit.rs index 3f7c96aa0..5b67a2655 100644 --- a/src-tauri/src/duplicate_audit.rs +++ b/src-tauri/src/duplicate_audit.rs @@ -643,6 +643,17 @@ fn remove_if_storage_identity( Ok(()) } +#[cfg(not(unix))] +fn remove_if_storage_identity( + _path: &Path, + _expected_storage_identity: &str, + _expected_logical_bytes: u64, + _expected_content_digests: &ContentDigests, + _staging_token: u64, +) -> Result<(), String> { + Err("duplicate-reclaim-unsupported-platform".into()) +} + fn removal_failure_code(error: &str) -> String { error.split(':').next().unwrap_or(error).to_string() } From 6579b297df35e0de820ff3d5cf3d2c0c9b68cf47 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:13:04 +0900 Subject: [PATCH 4/8] fix: preserve portable symlink cache audits --- src-tauri/src/generated_cache_reclaim.rs | 105 ++++++++++++++++------- 1 file changed, 73 insertions(+), 32 deletions(-) diff --git a/src-tauri/src/generated_cache_reclaim.rs b/src-tauri/src/generated_cache_reclaim.rs index 7e2cd3a91..0648a8e4b 100644 --- a/src-tauri/src/generated_cache_reclaim.rs +++ b/src-tauri/src/generated_cache_reclaim.rs @@ -2,7 +2,6 @@ use serde::{Deserialize, Serialize}; use std::io::Read; -use std::os::unix::ffi::OsStrExt; use std::path::Path; use std::process::{Command, Stdio}; use std::time::{Duration, Instant}; @@ -156,8 +155,51 @@ pub fn regeneration_contract(path: &Path, home: &Path) -> Option Result<(u64, u64, String, Vec), String> { +#[cfg(unix)] +fn path_bytes(path: &std::ffi::OsStr) -> Vec { + use std::os::unix::ffi::OsStrExt; + path.as_bytes().to_vec() +} + +#[cfg(windows)] +fn path_bytes(path: &std::ffi::OsStr) -> Vec { + use std::os::windows::ffi::OsStrExt; + path.encode_wide().flat_map(u16::to_le_bytes).collect() +} + +#[cfg(unix)] +fn update_platform_metadata(hasher: &mut blake3::Hasher, metadata: &std::fs::Metadata) { + use std::os::unix::fs::MetadataExt; + hasher.update(&metadata.dev().to_le_bytes()); + hasher.update(&metadata.ino().to_le_bytes()); + hasher.update(&metadata.mode().to_le_bytes()); + hasher.update(&metadata.uid().to_le_bytes()); + hasher.update(&metadata.gid().to_le_bytes()); + hasher.update(&metadata.mtime().to_le_bytes()); + hasher.update(&metadata.mtime_nsec().to_le_bytes()); +} + +#[cfg(windows)] +fn update_platform_metadata(hasher: &mut blake3::Hasher, metadata: &std::fs::Metadata) { + use std::os::windows::fs::MetadataExt; + hasher.update(&metadata.creation_time().to_le_bytes()); + hasher.update(&metadata.last_write_time().to_le_bytes()); + hasher.update(&metadata.file_attributes().to_le_bytes()); + hasher.update(&metadata.file_size().to_le_bytes()); +} + +#[cfg(unix)] +fn allocated_bytes(metadata: &std::fs::Metadata) -> u64 { use std::os::unix::fs::MetadataExt; + metadata.blocks().saturating_mul(512) +} + +#[cfg(windows)] +fn allocated_bytes(metadata: &std::fs::Metadata) -> u64 { + metadata.len() +} + +fn observe_tree(path: &Path) -> Result<(u64, u64, String, Vec), String> { let mut stack = vec![path.to_path_buf()]; let mut bytes = 0_u64; let mut entries = 0_u64; @@ -182,12 +224,13 @@ fn observe_tree(path: &Path) -> Result<(u64, u64, String, Vec), String> return Err("generated-cache-special-file-rejected".into()); } entries += 1; - bytes = bytes.saturating_add(metadata.blocks().saturating_mul(512)); + bytes = bytes.saturating_add(allocated_bytes(&metadata)); let relative = current .strip_prefix(path) .map_err(|_| "generated-cache-relative-path-failed")?; - hasher.update(&(relative.as_os_str().as_bytes().len() as u64).to_le_bytes()); - hasher.update(relative.as_os_str().as_bytes()); + let relative_bytes = path_bytes(relative.as_os_str()); + hasher.update(&(relative_bytes.len() as u64).to_le_bytes()); + hasher.update(&relative_bytes); hasher.update(if is_symlink { b"symlink" } else if metadata.is_dir() { @@ -195,23 +238,17 @@ fn observe_tree(path: &Path) -> Result<(u64, u64, String, Vec), String> } else { b"file" }); - hasher.update(&metadata.dev().to_le_bytes()); - hasher.update(&metadata.ino().to_le_bytes()); - hasher.update(&metadata.mode().to_le_bytes()); - hasher.update(&metadata.uid().to_le_bytes()); - hasher.update(&metadata.gid().to_le_bytes()); hasher.update(&metadata.len().to_le_bytes()); - hasher.update(&metadata.mtime().to_le_bytes()); - hasher.update(&metadata.mtime_nsec().to_le_bytes()); + update_platform_metadata(&mut hasher, &metadata); if current.file_name().is_some_and(|name| name == ".lock") { locks.push(current.to_string_lossy().into_owned()); } if is_symlink { let target = std::fs::read_link(¤t) .map_err(|_| "generated-cache-symlink-unreadable".to_string())?; - let target_bytes = target.as_os_str().as_bytes(); + let target_bytes = path_bytes(target.as_os_str()); hasher.update(&(target_bytes.len() as u64).to_le_bytes()); - hasher.update(target_bytes); + hasher.update(&target_bytes); } else if metadata.is_file() { estimated_content_bytes = estimated_content_bytes .checked_add(metadata.len()) @@ -477,7 +514,6 @@ pub fn stage_and_remove_regenerable_root( home: &Path, now_ms: u64, ) -> Result<(), String> { - use std::os::unix::fs::DirBuilderExt; if plan.root != path.to_string_lossy() || plan.contract != regeneration_contract(path, home).ok_or("generated-cache-removal-boundary-denied")? @@ -494,9 +530,13 @@ pub fn stage_and_remove_regenerable_root( ".disksage-generated-cache-staging-{}", &plan.plan_fingerprint[..16] )); - std::fs::DirBuilder::new() - .mode(0o700) - .create(&staging) + let mut staging_builder = std::fs::DirBuilder::new(); + #[cfg(unix)] + { + use std::os::unix::fs::DirBuilderExt; + staging_builder.mode(0o700); + } + staging_builder.create(&staging) .map_err(|_| "generated-cache-staging-create-failed")?; let staged = staging.join(name); if let Err(error) = std::fs::rename(path, &staged) { @@ -539,15 +579,22 @@ pub fn stage_and_remove_regenerable_root( Ok(()) } +fn create_new_private_file(path: &Path) -> Result { + let mut options = std::fs::OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + options + .open(path) + .map_err(|_| "generated-cache-receipt-create-failed".to_string()) +} + pub fn write_immutable_receipt(path: &Path, receipt: &GeneratedCacheReceipt) -> Result<(), String> { use std::io::Write; - use std::os::unix::fs::OpenOptionsExt; - let mut file = std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .mode(0o600) - .open(path) - .map_err(|_| "generated-cache-receipt-create-failed".to_string())?; + let mut file = create_new_private_file(path)?; let bytes = serde_json::to_vec_pretty(receipt) .map_err(|_| "generated-cache-receipt-encode-failed".to_string())?; file.write_all(&bytes) @@ -571,14 +618,8 @@ where F: FnOnce(&Path) -> Result<(), String>, { use std::io::Write; - use std::os::unix::fs::OpenOptionsExt; validate_execution(plan, approval, fresh, attempted_at_ms)?; - let mut file = std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .mode(0o600) - .open(receipt_path) - .map_err(|_| "generated-cache-receipt-create-failed".to_string())?; + let mut file = create_new_private_file(receipt_path)?; let pending = GeneratedCachePendingReceipt { schema_version: GENERATED_CACHE_SCHEMA_VERSION, plan_fingerprint: plan.plan_fingerprint.clone(), From e892c7d0cf37e5997339de3b51f20159ac095f33 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 30 Aug 2026 10:15:28 +0900 Subject: [PATCH 5/8] fix: recognize platform shared temporary roots --- src-tauri/src/generated_cache_reclaim.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src-tauri/src/generated_cache_reclaim.rs b/src-tauri/src/generated_cache_reclaim.rs index 0648a8e4b..d818f5f06 100644 --- a/src-tauri/src/generated_cache_reclaim.rs +++ b/src-tauri/src/generated_cache_reclaim.rs @@ -150,7 +150,7 @@ pub fn regeneration_contract(path: &Path, home: &Path) -> Option Result Date: Sun, 30 Aug 2026 10:19:41 +0900 Subject: [PATCH 6/8] fix: fail closed on unsupported cache allocation --- src-tauri/src/generated_cache_reclaim.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src-tauri/src/generated_cache_reclaim.rs b/src-tauri/src/generated_cache_reclaim.rs index d818f5f06..fb8480bcb 100644 --- a/src-tauri/src/generated_cache_reclaim.rs +++ b/src-tauri/src/generated_cache_reclaim.rs @@ -150,8 +150,15 @@ pub fn regeneration_contract(path: &Path, home: &Path) -> Option u64 { +fn allocated_bytes(metadata: &std::fs::Metadata) -> Result { use std::os::unix::fs::MetadataExt; - metadata.blocks().saturating_mul(512) + Ok(metadata.blocks().saturating_mul(512)) } #[cfg(windows)] -fn allocated_bytes(metadata: &std::fs::Metadata) -> u64 { - metadata.len() +fn allocated_bytes(_metadata: &std::fs::Metadata) -> Result { + Err("generated-cache-allocation-evidence-unsupported-platform".into()) } fn observe_tree(path: &Path) -> Result<(u64, u64, String, Vec), String> { @@ -224,7 +231,7 @@ fn observe_tree(path: &Path) -> Result<(u64, u64, String, Vec), String> return Err("generated-cache-special-file-rejected".into()); } entries += 1; - bytes = bytes.saturating_add(allocated_bytes(&metadata)); + bytes = bytes.saturating_add(allocated_bytes(&metadata)?); let relative = current .strip_prefix(path) .map_err(|_| "generated-cache-relative-path-failed")?; @@ -381,7 +388,7 @@ pub fn audit(path: &Path, home: &Path, observed_at_ms: u64) -> Result Date: Sat, 29 Aug 2026 19:07:13 -0700 Subject: [PATCH 7/8] fix: carry generated cache audit contract --- src-tauri/src/generated_cache_reclaim.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src-tauri/src/generated_cache_reclaim.rs b/src-tauri/src/generated_cache_reclaim.rs index c6df984bb..b82b1f0a8 100644 --- a/src-tauri/src/generated_cache_reclaim.rs +++ b/src-tauri/src/generated_cache_reclaim.rs @@ -426,6 +426,8 @@ fn bounded_git(path: &Path, args: &[&str]) -> Result { /// Collect path-free process evidence and bounded Git ownership evidence before planning. pub fn audit(path: &Path, home: &Path, observed_at_ms: u64) -> Result { + let contract = regeneration_contract(path, home) + .ok_or_else(|| "generated-cache-regeneration-contract-missing".to_string())?; let active = crate::git_worktree::active_use_evidence(path, 5_000, 128, true); let mut evidence = GeneratedCacheActivityEvidence { evidence_complete: active.evidence_complete, From 705c17c09e58ca28b1ee763447aac6f38eaab7a3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 7 Sep 2026 00:55:09 +0900 Subject: [PATCH 8/8] test: keep symlink fixtures within Unix platform contract --- src-tauri/src/generated_cache_reclaim.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src-tauri/src/generated_cache_reclaim.rs b/src-tauri/src/generated_cache_reclaim.rs index e42ab229b..fc35c26a7 100644 --- a/src-tauri/src/generated_cache_reclaim.rs +++ b/src-tauri/src/generated_cache_reclaim.rs @@ -814,6 +814,7 @@ where #[cfg(test)] mod tests { use super::*; + #[cfg(unix)] use std::os::unix::fs::symlink; fn inactive() -> GeneratedCacheActivityEvidence { @@ -897,6 +898,7 @@ mod tests { ); } + #[cfg(unix)] #[test] fn cache_child_symlink_is_fingerprinted_without_following_target() { let temp = tempfile::tempdir().unwrap(); @@ -916,6 +918,7 @@ mod tests { assert_eq!(before.entry_count, 2); } + #[cfg(unix)] #[test] fn allowlisted_root_symlink_remains_denied() { let temp = tempfile::tempdir().unwrap();