From d6615a3f29eae98844bb5046daeb023e8836792c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 18:10:56 +0900 Subject: [PATCH 1/7] test: require private cloud receipt authority --- .../tests/cloud_receipt_private_authority.rs | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) create mode 100644 src-tauri/tests/cloud_receipt_private_authority.rs diff --git a/src-tauri/tests/cloud_receipt_private_authority.rs b/src-tauri/tests/cloud_receipt_private_authority.rs new file mode 100644 index 000000000..1705a7385 --- /dev/null +++ b/src-tauri/tests/cloud_receipt_private_authority.rs @@ -0,0 +1,139 @@ +#![cfg(unix)] + +use disksage_lib::cloud::{ + candidate_review_fingerprint, ArchiveKind, CloudAccountScope, CloudCandidate, CloudProvider, + CloudRoot, MetadataEvidence, +}; +use disksage_lib::cloud_transfer::{ + cloud_copy_approval_phrase, create_cloud_copy_approval, prepare_cloud_copy_with_approval, + CloudCopyApprovalAction, +}; +use std::os::unix::fs::PermissionsExt; + +fn candidate_and_root( + temp: &tempfile::TempDir, +) -> (CloudCandidate, CloudRoot, std::path::PathBuf) { + let source_dir = temp.path().join("source"); + let cloud_dir = temp.path().join("cloud"); + std::fs::create_dir_all(&source_dir).unwrap(); + std::fs::create_dir_all(&cloud_dir).unwrap(); + + let source = source_dir.join("report.bin"); + let destination = cloud_dir.join("report.bin"); + std::fs::write(&source, b"verified source bytes").unwrap(); + let metadata = std::fs::metadata(&source).unwrap(); + let modified_ms = metadata + .modified() + .unwrap() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64; + + let mut candidate = CloudCandidate { + metadata_fingerprint: "a".repeat(64), + review_fingerprint: String::new(), + src: source.to_string_lossy().into_owned(), + dst: destination.to_string_lossy().into_owned(), + provider: CloudProvider::Onedrive, + destination_account_scope: CloudAccountScope::Personal, + kind: ArchiveKind::Document, + bytes: metadata.len(), + age_days: 1, + created_ms: modified_ms, + modified_ms, + production_time_ms: modified_ms, + production_time_source: "embedded:test:CreateDate".into(), + production_time_confidence: "high".into(), + source_root: source_dir.to_string_lossy().into_owned(), + relative_path: "report.bin".into(), + source_context: ".".into(), + requires_review: false, + review_reasons: Vec::new(), + content_title: Some("Report".into()), + content_authors: Vec::new(), + content_context: Vec::new(), + duration_ms: None, + dataset_profile: None, + metadata_evidence: vec![MetadataEvidence { + field: "production-date".into(), + value: "2026-08-12".into(), + source: "embedded:test:CreateDate".into(), + confidence: "high".into(), + }], + blocked_reason: None, + }; + candidate.review_fingerprint = candidate_review_fingerprint(&candidate); + + let root = CloudRoot { + id: cloud_dir.to_string_lossy().into_owned(), + provider: CloudProvider::Onedrive, + account_scope: CloudAccountScope::Personal, + label: "test".into(), + path: cloud_dir.to_string_lossy().into_owned(), + readable: true, + access_issue: None, + }; + (candidate, root, destination) +} + +#[test] +fn shared_writable_receipt_directory_fails_closed_without_durable_authority() { + for unsafe_write_bit in [0o020, 0o002] { + let temp = tempfile::tempdir().unwrap(); + let (candidate, root, destination) = candidate_and_root(&temp); + let receipt_dir = temp.path().join("receipts"); + std::fs::create_dir_all(&receipt_dir).unwrap(); + std::fs::set_permissions( + &receipt_dir, + std::fs::Permissions::from_mode(0o700 | unsafe_write_bit), + ) + .unwrap(); + + let action = CloudCopyApprovalAction::CopyOnly; + let approval = create_cloud_copy_approval( + &candidate, + &root, + action, + 1_786_521_600_000, + "human:local:test", + "authorize exact test cloud copy", + &cloud_copy_approval_phrase(&candidate, action), + ) + .unwrap(); + + let error = prepare_cloud_copy_with_approval( + &candidate, + &root, + &receipt_dir, + None, + &approval, + ) + .expect_err("shared-writable receipt authority must fail closed"); + + assert_eq!(error, "receipt-directory-writable-by-others"); + assert!(std::path::Path::new(&candidate.src).exists()); + assert!(!destination.exists(), "failed receipt publication must roll back the new copy"); + assert_eq!(std::fs::read_dir(&receipt_dir).unwrap().count(), 0); + } +} + +#[test] +fn receipt_file_is_private_from_creation_and_object_bound_for_hardening() { + let source = std::fs::read_to_string( + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/cloud_transfer.rs"), + ) + .expect("cloud transfer source must be readable"); + + assert!( + source.contains("options.mode(0o400);"), + "cloud copy receipts must be owner-read-only from create_new so a crash cannot leave broader authority" + ); + assert!( + source.contains("file.set_permissions(permissions)"), + "post-write receipt hardening must remain bound to the opened file object" + ); + assert!( + !source.contains("std::fs::set_permissions(&path, permissions)"), + "receipt hardening must not re-resolve a replaceable pathname after create_new" + ); +} From 08d253410322be510bf861d3a06dce5058bfd476 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 18:12:52 +0900 Subject: [PATCH 2/7] test: require private local eviction records --- .../icloud_local_eviction_record_authority.rs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src-tauri/tests/icloud_local_eviction_record_authority.rs diff --git a/src-tauri/tests/icloud_local_eviction_record_authority.rs b/src-tauri/tests/icloud_local_eviction_record_authority.rs new file mode 100644 index 000000000..2906744c7 --- /dev/null +++ b/src-tauri/tests/icloud_local_eviction_record_authority.rs @@ -0,0 +1,47 @@ +#![cfg(unix)] + +use disksage_lib::cloud_local_eviction::write_immutable_record; +use std::os::unix::fs::PermissionsExt; + +#[test] +fn shared_writable_local_eviction_record_directory_fails_closed() { + for unsafe_write_bit in [0o020, 0o002] { + let directory = tempfile::tempdir().unwrap(); + std::fs::set_permissions( + directory.path(), + std::fs::Permissions::from_mode(0o700 | unsafe_write_bit), + ) + .unwrap(); + + let error = write_immutable_record( + directory.path(), + "approval.json", + &serde_json::json!({"authority": "human-approved-local-eviction"}), + ) + .expect_err("shared-writable local-eviction authority must fail closed"); + + assert_eq!(error, "icloud-local-eviction-record-dir-writable-by-others"); + assert_eq!(std::fs::read_dir(directory.path()).unwrap().count(), 0); + } +} + +#[test] +fn local_eviction_record_is_private_from_creation_and_object_bound_for_hardening() { + let source = std::fs::read_to_string( + std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/cloud_local_eviction.rs"), + ) + .expect("cloud local eviction source must be readable"); + + assert!( + source.contains("options.mode(0o400);"), + "local eviction approval/result records must be owner-read-only from create_new" + ); + assert!( + source.contains("file.set_permissions(permissions)"), + "post-write local eviction hardening must stay bound to the opened record object" + ); + assert!( + !source.contains("std::fs::set_permissions(&path, permissions)"), + "local eviction record hardening must not re-resolve a replaceable pathname" + ); +} From 17eed8ce102af146bc4770985579c9882a219b71 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 18:14:37 +0900 Subject: [PATCH 3/7] test: reach cloud receipt authority boundary --- src-tauri/tests/cloud_receipt_private_authority.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src-tauri/tests/cloud_receipt_private_authority.rs b/src-tauri/tests/cloud_receipt_private_authority.rs index 1705a7385..855ace770 100644 --- a/src-tauri/tests/cloud_receipt_private_authority.rs +++ b/src-tauri/tests/cloud_receipt_private_authority.rs @@ -10,6 +10,13 @@ use disksage_lib::cloud_transfer::{ }; use std::os::unix::fs::PermissionsExt; +fn now_ms() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_millis() as u64 +} + fn candidate_and_root( temp: &tempfile::TempDir, ) -> (CloudCandidate, CloudRoot, std::path::PathBuf) { @@ -94,7 +101,7 @@ fn shared_writable_receipt_directory_fails_closed_without_durable_authority() { &candidate, &root, action, - 1_786_521_600_000, + now_ms(), "human:local:test", "authorize exact test cloud copy", &cloud_copy_approval_phrase(&candidate, action), @@ -112,7 +119,10 @@ fn shared_writable_receipt_directory_fails_closed_without_durable_authority() { assert_eq!(error, "receipt-directory-writable-by-others"); assert!(std::path::Path::new(&candidate.src).exists()); - assert!(!destination.exists(), "failed receipt publication must roll back the new copy"); + assert!( + !destination.exists(), + "failed receipt publication must roll back the new copy" + ); assert_eq!(std::fs::read_dir(&receipt_dir).unwrap().count(), 0); } } From 2abefde942d6723fec01baa837e4d04f10b804a4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 19:17:03 +0900 Subject: [PATCH 4/7] security: privatize cloud copy receipt authority --- src-tauri/src/cloud_transfer.rs | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/src-tauri/src/cloud_transfer.rs b/src-tauri/src/cloud_transfer.rs index e5fc42665..f1c695786 100644 --- a/src-tauri/src/cloud_transfer.rs +++ b/src-tauri/src/cloud_transfer.rs @@ -1138,14 +1138,26 @@ fn write_immutable_receipt( if !directory_metadata.is_dir() || directory_metadata.file_type().is_symlink() { return Err("receipt-directory-unsafe".into()); } + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if directory_metadata.permissions().mode() & 0o022 != 0 { + return Err("receipt-directory-writable-by-others".into()); + } + } let path = receipt_dir.join(format!("{}.json", receipt.receipt_id)); let encoded = serde_json::to_vec_pretty(receipt).map_err(|error| error.to_string())?; if encoded.len() as u64 > MAX_RECEIPT_BYTES { return Err("receipt-too-large".into()); } - let mut file = std::fs::OpenOptions::new() - .write(true) - .create_new(true) + let mut options = std::fs::OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o400); + } + let mut file = options .open(&path) .map_err(|error| error.to_string())?; let result = (|| -> Result<(), String> { @@ -1163,7 +1175,8 @@ fn write_immutable_receipt( } #[cfg(not(unix))] permissions.set_readonly(true); - std::fs::set_permissions(&path, permissions).map_err(|error| error.to_string())?; + file.set_permissions(permissions) + .map_err(|error| error.to_string())?; #[cfg(unix)] std::fs::File::open(receipt_dir) .and_then(|directory| directory.sync_all()) From 632849f7d469872d477b78d61b5165999a21dea5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 20:04:10 +0900 Subject: [PATCH 5/7] ci: repair PR 186 local eviction authority --- .../repair-pr186-local-eviction-authority.yml | 149 ++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 .github/workflows/repair-pr186-local-eviction-authority.yml diff --git a/.github/workflows/repair-pr186-local-eviction-authority.yml b/.github/workflows/repair-pr186-local-eviction-authority.yml new file mode 100644 index 000000000..24aa93bfe --- /dev/null +++ b/.github/workflows/repair-pr186-local-eviction-authority.yml @@ -0,0 +1,149 @@ +name: Repair PR 186 local eviction authority + +on: + push: + branches: + - security/cloud-receipt-private-authority-v1 + paths: + - .github/workflows/repair-pr186-local-eviction-authority.yml + +permissions: + contents: write + +concurrency: + group: repair-pr186-local-eviction-authority + cancel-in-progress: false + +jobs: + repair: + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: security/cloud-receipt-private-authority-v1 + fetch-depth: 2 + + - name: Verify exact parent and isolated trigger + env: + EXPECTED_PARENT: 2abefde942d6723fec01baa837e4d04f10b804a4 + TRIGGER_PATH: .github/workflows/repair-pr186-local-eviction-authority.yml + run: | + set -euo pipefail + test "$(git rev-parse HEAD^)" = "$EXPECTED_PARENT" + test "${{ github.event.before }}" = "$EXPECTED_PARENT" + test "$(git diff --name-only HEAD^ HEAD)" = "$TRIGGER_PATH" + + - name: Apply fail-closed authority repair + run: | + set -euo pipefail + python3 - <<'PY' + from pathlib import Path + + path = Path("src-tauri/src/cloud_local_eviction.rs") + source = path.read_text(encoding="utf-8") + + replacements = [ + ( + ''' if directory.file_type().is_symlink() || !directory.is_dir() { + return Err("record-dir-not-real-directory".into()); + } + let path = record_dir.join(filename); + ''', + ''' if directory.file_type().is_symlink() || !directory.is_dir() { + return Err("record-dir-not-real-directory".into()); + } + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + if directory.permissions().mode() & 0o022 != 0 { + return Err("icloud-local-eviction-record-dir-writable-by-others".into()); + } + } + let path = record_dir.join(filename); + ''', + ), + ( + ''' let mut file = std::fs::OpenOptions::new() + .write(true) + .create_new(true) + .open(&path) + .map_err(|error| error.to_string())?; + ''', + ''' let mut options = std::fs::OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o400); + } + let mut file = options + .open(&path) + .map_err(|error| error.to_string())?; + ''', + ), + ( + ''' permissions.set_readonly(true); + std::fs::set_permissions(&path, permissions).map_err(|error| error.to_string())?; + ''', + ''' #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + permissions.set_mode(0o400); + } + #[cfg(not(unix))] + permissions.set_readonly(true); + file.set_permissions(permissions) + .map_err(|error| error.to_string())?; + ''', + ), + ] + + for old, new in replacements: + count = source.count(old) + if count != 1: + raise SystemExit(f"expected one exact replacement, found {count}: {old[:80]!r}") + source = source.replace(old, new, 1) + + path.write_text(source, encoding="utf-8") + PY + cargo fmt --manifest-path src-tauri/Cargo.toml + git diff --check + + - name: Install Tauri system dependencies + run: | + sudo apt-get update + sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev + + - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable + + - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 + with: + workspaces: src-tauri + + - name: Verify focused RED to GREEN contracts + run: | + set -euo pipefail + cargo test --manifest-path src-tauri/Cargo.toml --test cloud_receipt_private_authority + cargo test --manifest-path src-tauri/Cargo.toml --test icloud_local_eviction_record_authority + + - name: Verify full Rust suite + run: cargo test --manifest-path src-tauri/Cargo.toml + + - name: Commit only the verified repair + env: + BRANCH_NAME: security/cloud-receipt-private-authority-v1 + TRIGGER_PATH: .github/workflows/repair-pr186-local-eviction-authority.yml + run: | + set -euo pipefail + rm "$TRIGGER_PATH" + cargo fmt --manifest-path src-tauri/Cargo.toml -- --check + git diff --check + test "$(git diff --name-only | sort)" = $'.github/workflows/repair-pr186-local-eviction-authority.yml\nsrc-tauri/src/cloud_local_eviction.rs' + git config user.name "OpenCode Repair Agent" + git config user.email "opencode-agent@users.noreply.github.com" + git add src-tauri/src/cloud_local_eviction.rs "$TRIGGER_PATH" + git commit -m "security: privatize local eviction authority records" + git fetch origin "$BRANCH_NAME" + test "$(git rev-parse "origin/$BRANCH_NAME")" = "$(git rev-parse HEAD^)" + git push origin "HEAD:$BRANCH_NAME" From 599c14bfd1a9da091502887d2449e63375f2b197 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 03:06:17 +0900 Subject: [PATCH 6/7] ci: remove self-modifying PR 186 repair workflow --- .../repair-pr186-local-eviction-authority.yml | 149 ------------------ 1 file changed, 149 deletions(-) delete mode 100644 .github/workflows/repair-pr186-local-eviction-authority.yml diff --git a/.github/workflows/repair-pr186-local-eviction-authority.yml b/.github/workflows/repair-pr186-local-eviction-authority.yml deleted file mode 100644 index 24aa93bfe..000000000 --- a/.github/workflows/repair-pr186-local-eviction-authority.yml +++ /dev/null @@ -1,149 +0,0 @@ -name: Repair PR 186 local eviction authority - -on: - push: - branches: - - security/cloud-receipt-private-authority-v1 - paths: - - .github/workflows/repair-pr186-local-eviction-authority.yml - -permissions: - contents: write - -concurrency: - group: repair-pr186-local-eviction-authority - cancel-in-progress: false - -jobs: - repair: - runs-on: ubuntu-latest - timeout-minutes: 45 - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - ref: security/cloud-receipt-private-authority-v1 - fetch-depth: 2 - - - name: Verify exact parent and isolated trigger - env: - EXPECTED_PARENT: 2abefde942d6723fec01baa837e4d04f10b804a4 - TRIGGER_PATH: .github/workflows/repair-pr186-local-eviction-authority.yml - run: | - set -euo pipefail - test "$(git rev-parse HEAD^)" = "$EXPECTED_PARENT" - test "${{ github.event.before }}" = "$EXPECTED_PARENT" - test "$(git diff --name-only HEAD^ HEAD)" = "$TRIGGER_PATH" - - - name: Apply fail-closed authority repair - run: | - set -euo pipefail - python3 - <<'PY' - from pathlib import Path - - path = Path("src-tauri/src/cloud_local_eviction.rs") - source = path.read_text(encoding="utf-8") - - replacements = [ - ( - ''' if directory.file_type().is_symlink() || !directory.is_dir() { - return Err("record-dir-not-real-directory".into()); - } - let path = record_dir.join(filename); - ''', - ''' if directory.file_type().is_symlink() || !directory.is_dir() { - return Err("record-dir-not-real-directory".into()); - } - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - if directory.permissions().mode() & 0o022 != 0 { - return Err("icloud-local-eviction-record-dir-writable-by-others".into()); - } - } - let path = record_dir.join(filename); - ''', - ), - ( - ''' let mut file = std::fs::OpenOptions::new() - .write(true) - .create_new(true) - .open(&path) - .map_err(|error| error.to_string())?; - ''', - ''' let mut options = std::fs::OpenOptions::new(); - options.write(true).create_new(true); - #[cfg(unix)] - { - use std::os::unix::fs::OpenOptionsExt; - options.mode(0o400); - } - let mut file = options - .open(&path) - .map_err(|error| error.to_string())?; - ''', - ), - ( - ''' permissions.set_readonly(true); - std::fs::set_permissions(&path, permissions).map_err(|error| error.to_string())?; - ''', - ''' #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - permissions.set_mode(0o400); - } - #[cfg(not(unix))] - permissions.set_readonly(true); - file.set_permissions(permissions) - .map_err(|error| error.to_string())?; - ''', - ), - ] - - for old, new in replacements: - count = source.count(old) - if count != 1: - raise SystemExit(f"expected one exact replacement, found {count}: {old[:80]!r}") - source = source.replace(old, new, 1) - - path.write_text(source, encoding="utf-8") - PY - cargo fmt --manifest-path src-tauri/Cargo.toml - git diff --check - - - name: Install Tauri system dependencies - run: | - sudo apt-get update - sudo apt-get install -y libwebkit2gtk-4.1-dev libgtk-3-dev libayatana-appindicator3-dev librsvg2-dev - - - uses: dtolnay/rust-toolchain@4be7066ada62dd38de10e7b70166bc74ed198c30 # stable - - - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2 - with: - workspaces: src-tauri - - - name: Verify focused RED to GREEN contracts - run: | - set -euo pipefail - cargo test --manifest-path src-tauri/Cargo.toml --test cloud_receipt_private_authority - cargo test --manifest-path src-tauri/Cargo.toml --test icloud_local_eviction_record_authority - - - name: Verify full Rust suite - run: cargo test --manifest-path src-tauri/Cargo.toml - - - name: Commit only the verified repair - env: - BRANCH_NAME: security/cloud-receipt-private-authority-v1 - TRIGGER_PATH: .github/workflows/repair-pr186-local-eviction-authority.yml - run: | - set -euo pipefail - rm "$TRIGGER_PATH" - cargo fmt --manifest-path src-tauri/Cargo.toml -- --check - git diff --check - test "$(git diff --name-only | sort)" = $'.github/workflows/repair-pr186-local-eviction-authority.yml\nsrc-tauri/src/cloud_local_eviction.rs' - git config user.name "OpenCode Repair Agent" - git config user.email "opencode-agent@users.noreply.github.com" - git add src-tauri/src/cloud_local_eviction.rs "$TRIGGER_PATH" - git commit -m "security: privatize local eviction authority records" - git fetch origin "$BRANCH_NAME" - test "$(git rev-parse "origin/$BRANCH_NAME")" = "$(git rev-parse HEAD^)" - git push origin "HEAD:$BRANCH_NAME" From 6ed7994b413ca6bb6fe03603334e68504f5d1a1d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 13 Aug 2026 03:06:46 +0900 Subject: [PATCH 7/7] test: remove unrelated local eviction authority contract from PR 186 --- .../icloud_local_eviction_record_authority.rs | 47 ------------------- 1 file changed, 47 deletions(-) delete mode 100644 src-tauri/tests/icloud_local_eviction_record_authority.rs diff --git a/src-tauri/tests/icloud_local_eviction_record_authority.rs b/src-tauri/tests/icloud_local_eviction_record_authority.rs deleted file mode 100644 index 2906744c7..000000000 --- a/src-tauri/tests/icloud_local_eviction_record_authority.rs +++ /dev/null @@ -1,47 +0,0 @@ -#![cfg(unix)] - -use disksage_lib::cloud_local_eviction::write_immutable_record; -use std::os::unix::fs::PermissionsExt; - -#[test] -fn shared_writable_local_eviction_record_directory_fails_closed() { - for unsafe_write_bit in [0o020, 0o002] { - let directory = tempfile::tempdir().unwrap(); - std::fs::set_permissions( - directory.path(), - std::fs::Permissions::from_mode(0o700 | unsafe_write_bit), - ) - .unwrap(); - - let error = write_immutable_record( - directory.path(), - "approval.json", - &serde_json::json!({"authority": "human-approved-local-eviction"}), - ) - .expect_err("shared-writable local-eviction authority must fail closed"); - - assert_eq!(error, "icloud-local-eviction-record-dir-writable-by-others"); - assert_eq!(std::fs::read_dir(directory.path()).unwrap().count(), 0); - } -} - -#[test] -fn local_eviction_record_is_private_from_creation_and_object_bound_for_hardening() { - let source = std::fs::read_to_string( - std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src/cloud_local_eviction.rs"), - ) - .expect("cloud local eviction source must be readable"); - - assert!( - source.contains("options.mode(0o400);"), - "local eviction approval/result records must be owner-read-only from create_new" - ); - assert!( - source.contains("file.set_permissions(permissions)"), - "post-write local eviction hardening must stay bound to the opened record object" - ); - assert!( - !source.contains("std::fs::set_permissions(&path, permissions)"), - "local eviction record hardening must not re-resolve a replaceable pathname" - ); -}