From 1f658252bb5df9a3a2de1cbf267c22d7fd9584ff Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 21:42:43 +0000 Subject: [PATCH 1/2] chore(license): finish PMPL-1.0 -> MPL-2.0 migration stragglers; retire Palimpsest artifacts The PMPL-1.0-or-later -> MPL-2.0 migration landed earlier (a740439, #52); this clears the remaining stragglers, mirroring julia-the-viper's gap-006 resolution. - SPDX-License-Identifier: PMPL-1.0 -> MPL-2.0 in scorecard.yml + secret-scanner.yml (codeql.yml + the README badge were already done on main). - threat-model.a2ml: drop the "until PMPL is formally recognised" note (PMPL retired). - Retire stale Palimpsest artifacts: PALIMPSEST.adoc (still claimed PMPL-1.0, contradicting the completed migration), LICENSE.txt (old PMPL license text; canonical LICENSE is already MPL-2.0), LICENSES/PMPL-1.0-or-later.txt (unused now that no file declares PMPL). Canonical LICENSE (MPL-2.0) unchanged. Remaining minor follow-ups (NOTICE prose, a k9 setup-example, guix/flake license fields) noted for a later pass. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BJmfoz1ZS1Pejy9LLMY742 --- .github/workflows/scorecard.yml | 2 +- .github/workflows/secret-scanner.yml | 2 +- .machine_readable/threat-model.a2ml | 2 +- LICENSE.txt | 153 ------------------------- LICENSES/PMPL-1.0-or-later.txt | 162 --------------------------- PALIMPSEST.adoc | 43 ------- 6 files changed, 3 insertions(+), 361 deletions(-) delete mode 100644 LICENSE.txt delete mode 100644 LICENSES/PMPL-1.0-or-later.txt delete mode 100644 PALIMPSEST.adoc diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml index 8d85e86..09ccd95 100644 --- a/.github/workflows/scorecard.yml +++ b/.github/workflows/scorecard.yml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0 +# SPDX-License-Identifier: MPL-2.0 name: Scorecards supply-chain security on: diff --git a/.github/workflows/secret-scanner.yml b/.github/workflows/secret-scanner.yml index 3817aa9..c7761fe 100644 --- a/.github/workflows/secret-scanner.yml +++ b/.github/workflows/secret-scanner.yml @@ -1,4 +1,4 @@ -# SPDX-License-Identifier: PMPL-1.0 +# SPDX-License-Identifier: MPL-2.0 name: Secret Scanner on: diff --git a/.machine_readable/threat-model.a2ml b/.machine_readable/threat-model.a2ml index 4edb5f6..053bc63 100644 --- a/.machine_readable/threat-model.a2ml +++ b/.machine_readable/threat-model.a2ml @@ -1,6 +1,6 @@ # SPDX-License-Identifier: MPL-2.0 # Copyright (c) 2026 Jonathan D.A. Jewell (hyperpolymath) -# (MPL-2.0 is automatic legal fallback until PMPL is formally recognised) +# (Licensed under MPL-2.0; the standalone PMPL-1.0 is retired per the estate standard.) # # threat-model.a2ml — DRAFT threat model for exposing januskey via an MCP cartridge # diff --git a/LICENSE.txt b/LICENSE.txt deleted file mode 100644 index ec540b3..0000000 --- a/LICENSE.txt +++ /dev/null @@ -1,153 +0,0 @@ -SPDX-License-Identifier: MPL-2.0 -SPDX-FileCopyrightText: 2024-2025 Palimpsest Stewardship Council - -================================================================================ -PALIMPSEST-MPL LICENSE VERSION 1.0 -================================================================================ - -File-level copyleft with ethical use and quantum-safe provenance - -Based on Mozilla Public License 2.0 - --------------------------------------------------------------------------------- -PREAMBLE --------------------------------------------------------------------------------- - -This License extends the Mozilla Public License 2.0 (MPL-2.0) with provisions -for ethical use, post-quantum cryptographic provenance, and emotional lineage -protection. The base MPL-2.0 terms apply except where explicitly modified by -the Exhibits below. - -Like a palimpsest manuscript where each layer builds upon what came before, -this license recognizes that creative works carry history, context, and meaning -that transcend mere code or text. - --------------------------------------------------------------------------------- -SECTION 1: BASE LICENSE --------------------------------------------------------------------------------- - -This License incorporates the full text of Mozilla Public License 2.0 by -reference. The complete MPL-2.0 text is available at: -https://www.mozilla.org/en-US/MPL/2.0/ - -All terms, conditions, and definitions from MPL-2.0 apply except where -explicitly modified by the Exhibits in this License. - --------------------------------------------------------------------------------- -SECTION 2: ADDITIONAL DEFINITIONS --------------------------------------------------------------------------------- - -2.1. "Emotional Lineage" - means the narrative, cultural, symbolic, and contextual meaning embedded - in Covered Software, including but not limited to: protest traditions, - cultural heritage, trauma narratives, and community stories. - -2.2. "Provenance Metadata" - means cryptographically signed attribution information attached to or - associated with Covered Software, including author identities, timestamps, - modification history, and lineage references. - -2.3. "Non-Interpretive System" - means any automated system that processes Covered Software without - preserving or considering its Emotional Lineage, including but not - limited to: AI training pipelines, content aggregators, and automated - summarization tools. - -2.4. "Quantum-Safe Signature" - means a cryptographic signature using algorithms resistant to attacks - by quantum computers, as specified in Exhibit B. - --------------------------------------------------------------------------------- -SECTION 3: ETHICAL USE REQUIREMENTS --------------------------------------------------------------------------------- - -In addition to the rights and obligations under MPL-2.0: - -3.1. Emotional Lineage Preservation - You must make reasonable efforts to preserve and communicate the - Emotional Lineage of Covered Software when distributing or creating - derivative works. This includes maintaining narrative context, cultural - attributions, and symbolic meaning where documented. - -3.2. Non-Interpretive System Notice - If You use Covered Software as input to a Non-Interpretive System, You - must: - (a) document such use in a publicly accessible manner; and - (b) not claim that outputs of such systems carry the Emotional Lineage - of the original work without explicit permission from Contributors. - -3.3. Ethical Use Declaration - Commercial use of Covered Software requires acknowledgment that You have - read and understood Exhibit A (Ethical Use Guidelines) and agree to act - in good faith accordance with its principles. - -See Exhibit A for complete Ethical Use Guidelines. - --------------------------------------------------------------------------------- -SECTION 4: PROVENANCE REQUIREMENTS --------------------------------------------------------------------------------- - -4.1. Metadata Preservation - You must not strip, alter, or obscure Provenance Metadata from Covered - Software except where technically necessary and with clear documentation - of any changes. - -4.2. Quantum-Safe Provenance (Optional) - Contributors may sign their Contributions using Quantum-Safe Signatures. - If Quantum-Safe Signatures are present, You must preserve them in all - distributions. - -4.3. Lineage Chain - When creating derivative works, You should extend the provenance chain - to include Your own contributions, maintaining cryptographic linkage to - prior Contributors where feasible. - -See Exhibit B for Quantum-Safe Provenance specifications. - --------------------------------------------------------------------------------- -SECTION 5: GOVERNANCE --------------------------------------------------------------------------------- - -5.1. Stewardship Council - This License is maintained by the Palimpsest Stewardship Council, which - may issue clarifications, interpretive guidance, and future versions. - -5.2. Version Selection - You may use Covered Software under this version of the License or any - later version published by the Palimpsest Stewardship Council. - -5.3. Dispute Resolution - Disputes regarding interpretation of Ethical Use Requirements (Section 3) - should first be submitted to the Palimpsest Stewardship Council for - non-binding guidance before pursuing legal remedies. - --------------------------------------------------------------------------------- -SECTION 6: COMPATIBILITY --------------------------------------------------------------------------------- - -6.1. MPL-2.0 Compatibility - Covered Software under this License may be combined with software under - MPL-2.0. The combined work must comply with both licenses. - -6.2. Secondary Licenses - The Secondary License provisions of MPL-2.0 Section 3.3 apply to this - License. - --------------------------------------------------------------------------------- -EXHIBITS --------------------------------------------------------------------------------- - -Exhibit A - Ethical Use Guidelines -Exhibit B - Quantum-Safe Provenance Specification - -See separate files: -- EXHIBIT-A-ETHICAL-USE.txt -- EXHIBIT-B-QUANTUM-SAFE.txt - --------------------------------------------------------------------------------- -END OF PALIMPSEST-MPL LICENSE VERSION 1.0 --------------------------------------------------------------------------------- - -For questions about this License: -- Repository: https://github.com/hyperpolymath/palimpsest-license -- Council: contact via repository Issues diff --git a/LICENSES/PMPL-1.0-or-later.txt b/LICENSES/PMPL-1.0-or-later.txt deleted file mode 100644 index 711e372..0000000 --- a/LICENSES/PMPL-1.0-or-later.txt +++ /dev/null @@ -1,162 +0,0 @@ -SPDX-License-Identifier: MPL-2.0 -SPDX-FileCopyrightText: 2025 Palimpsest Stewardship Council - -================================================================================ -PALIMPSEST-MPL LICENSE VERSION 1.0 -================================================================================ - -File-level copyleft with ethical use and quantum-safe provenance - -Based on Mozilla Public License 2.0 - --------------------------------------------------------------------------------- -PREAMBLE --------------------------------------------------------------------------------- - -This License extends the Mozilla Public License 2.0 (MPL-2.0) with provisions -for ethical use, post-quantum cryptographic provenance, and emotional lineage -protection. The base MPL-2.0 terms apply except where explicitly modified by -the Exhibits below. - -Like a palimpsest manuscript where each layer builds upon what came before, -this license recognizes that creative works carry history, context, and meaning -that transcend mere code or text. - --------------------------------------------------------------------------------- -SECTION 1: BASE LICENSE --------------------------------------------------------------------------------- - -This License incorporates the full text of Mozilla Public License 2.0 by -reference. The complete MPL-2.0 text is available at: -https://www.mozilla.org/en-US/MPL/2.0/ - -All terms, conditions, and definitions from MPL-2.0 apply except where -explicitly modified by the Exhibits in this License. - --------------------------------------------------------------------------------- -SECTION 2: ADDITIONAL DEFINITIONS --------------------------------------------------------------------------------- - -2.1. "Emotional Lineage" - means the narrative, cultural, symbolic, and contextual meaning embedded - in Covered Software, including but not limited to: protest traditions, - cultural heritage, trauma narratives, and community stories. - -2.2. "Provenance Metadata" - means cryptographically signed attribution information attached to or - associated with Covered Software, including author identities, timestamps, - modification history, and lineage references. - -2.3. "Non-Interpretive System" - means any automated system that processes Covered Software without - preserving or considering its Emotional Lineage, including but not - limited to: AI training pipelines, content aggregators, and automated - summarization tools. - -2.4. "Quantum-Safe Signature" - means a cryptographic signature using algorithms resistant to attacks - by quantum computers, as specified in Exhibit B. - --------------------------------------------------------------------------------- -SECTION 3: ETHICAL USE REQUIREMENTS --------------------------------------------------------------------------------- - -In addition to the rights and obligations under MPL-2.0: - -3.1. Emotional Lineage Preservation - You must make reasonable efforts to preserve and communicate the - Emotional Lineage of Covered Software when distributing or creating - derivative works. This includes maintaining narrative context, cultural - attributions, and symbolic meaning where documented. - -3.2. Non-Interpretive System Notice - If You use Covered Software as input to a Non-Interpretive System, You - must: - (a) document such use in a publicly accessible manner; and - (b) not claim that outputs of such systems carry the Emotional Lineage - of the original work without explicit permission from Contributors. - -3.3. Ethical Use Declaration - Commercial use of Covered Software requires acknowledgment that You have - read and understood Exhibit A (Ethical Use Guidelines) and agree to act - in good faith accordance with its principles. - -See Exhibit A for complete Ethical Use Guidelines. - --------------------------------------------------------------------------------- -SECTION 4: PROVENANCE REQUIREMENTS --------------------------------------------------------------------------------- - -4.1. Metadata Preservation - You must not strip, alter, or obscure Provenance Metadata from Covered - Software except where technically necessary and with clear documentation - of any changes. - -4.2. Quantum-Safe Provenance (Optional) - Contributors may sign their Contributions using Quantum-Safe Signatures. - If Quantum-Safe Signatures are present, You must preserve them in all - distributions. - -4.3. Lineage Chain - When creating derivative works, You should extend the provenance chain - to include Your own contributions, maintaining cryptographic linkage to - prior Contributors where feasible. - -See Exhibit B for Quantum-Safe Provenance specifications. - --------------------------------------------------------------------------------- -SECTION 5: GOVERNANCE --------------------------------------------------------------------------------- - -5.1. Stewardship Council - This License is maintained by the Palimpsest Stewardship Council, which - may issue clarifications, interpretive guidance, and future versions. - -5.2. Version Selection - You may use Covered Software under this version of the License or any - later version published by the Palimpsest Stewardship Council. - -5.3. Dispute Resolution - Disputes regarding interpretation of Ethical Use Requirements (Section 3) - should first be submitted to the Palimpsest Stewardship Council for - non-binding guidance before pursuing legal remedies. - --------------------------------------------------------------------------------- -SECTION 6: COMPATIBILITY --------------------------------------------------------------------------------- - -6.1. MPL-2.0 Compatibility - Covered Software under this License may be combined with software under - MPL-2.0. The combined work must comply with both licenses. - -6.2. Secondary Licenses - The Secondary License provisions of MPL-2.0 Section 3.3 apply to this - License. - --------------------------------------------------------------------------------- -EXHIBITS --------------------------------------------------------------------------------- - -Exhibit A - Ethical Use Guidelines -Exhibit B - Quantum-Safe Provenance Specification - -See separate files: -- EXHIBIT-A-ETHICAL-USE.txt -- EXHIBIT-B-QUANTUM-SAFE.txt - --------------------------------------------------------------------------------- -END OF PALIMPSEST-MPL-1.0 LICENSE TEXT --------------------------------------------------------------------------------- - -For exhibits, specifications, provenance rules, and governance: -https://github.com/hyperpolymath/palimpsest-license - -For legal frameworks and jurisdictional analysis: -See /legal/frameworks/ - -For provenance and audit tooling: -See /tools/ and /spec/PROVENANCE-SPEC.adoc - -For questions about this License: -- Repository: https://github.com/hyperpolymath/palimpsest-license -- Council: contact via repository Issues diff --git a/PALIMPSEST.adoc b/PALIMPSEST.adoc deleted file mode 100644 index 9660f13..0000000 --- a/PALIMPSEST.adoc +++ /dev/null @@ -1,43 +0,0 @@ -// SPDX-License-Identifier: MPL-2.0 -// Copyright (c) Jonathan D.A. Jewell -= Palimpsest License -:toc: -:toc-placement!: - -image:https://img.shields.io/badge/License-PMPL--1.0-blue.svg[License: PMPL-1.0,link="https://github.com/hyperpolymath/palimpsest-license"] -image:https://img.shields.io/badge/Philosophy-Palimpsest-indigo.svg[Palimpsest,link="https://github.com/hyperpolymath/palimpsest-license"] - -toc::[] - -== Legal Status - -This project is licensed under the **Palimpsest-MPL License 1.0 (PMPL-1.0)**. -For SPDX and tooling, use **MPL-2.0**. - -PMPL-1.0 incorporates the Mozilla Public License 2.0 by reference and adds -ethical-use, provenance, and lineage requirements. - -== What PMPL Adds - -* **Emotional Lineage** - preserve narrative intent and cultural context -* **Provenance Integrity** - retain attribution and lineage metadata -* **Ethical Use Constraints** - explicit consent for non-interpretive AI training -* **Quantum-Safe Provenance (optional)** - post-quantum signature support - -== How to Adopt - -1. Include the PMPL-1.0 license text in `LICENSE`. -2. Add SPDX headers to source files: - `SPDX-License-Identifier: MPL-2.0` -3. Add a Palimpsest badge to your README (see `assets/badges/` and `embed/license-blocks/`). - -== Versioning - -See `VERSIONING.adoc` for the release process and the "-or-later" model. -The current legal text is PMPL-1.0. - -== References - -* `legal/README.adoc` -* `assets/badges/README.md` -* `embed/license-blocks/README.md` From a3c0d6c7a685013cd130858a8935e3cfaba29e9a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 19 Jun 2026 22:15:46 +0000 Subject: [PATCH 2/2] fix(tests): repair botched bounded-read rewrite (mismatched delimiters) main's "remediate UnboundedAllocation" commit mechanically rewrote fs::read_to_string(PATH) into a bounded File::open(...).take(...).read_to_string() form, but for multi-.join(...) paths it glued `.and_then` onto the path's join instead of File::open and stranded a paren -- leaving 9 test sites that don't compile (mismatched/unexpected closing delimiter). This reddened ALL januskey CI (Rust Build + Unit Tests, E2E), #65 included. Repairs the 9 sites in aspect_test / p2p_test / concurrency_test / e2e_test: close File::open(FULL_PATH) before .and_then(...), drop the stray paren. Kept the bounded-read form (respects the remediation's intent); cargo fmt normalized the expressions. cargo test --workspace green (all suites pass). Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01BJmfoz1ZS1Pejy9LLMY742 --- crates/januskey-cli/src/attestation.rs | 30 +++-- crates/januskey-cli/src/delta.rs | 19 ++- crates/januskey-cli/src/keys.rs | 37 +++-- crates/januskey-cli/src/keys_cli.rs | 76 ++++++++--- crates/januskey-cli/src/lib.rs | 15 ++- crates/januskey-cli/src/main.rs | 109 ++++++++------- crates/januskey-cli/src/obliteration.rs | 33 +++-- crates/januskey-cli/src/operations.rs | 97 +++++++------ crates/januskey-cli/tests/aspect_test.rs | 77 ++++++----- crates/januskey-cli/tests/concurrency_test.rs | 78 +++++------ crates/januskey-cli/tests/e2e_test.rs | 127 +++++++++++------- crates/januskey-cli/tests/p2p_test.rs | 22 ++- crates/reversible-core/src/content_store.rs | 2 +- crates/reversible-core/src/lib.rs | 6 +- crates/reversible-core/src/manifest.rs | 21 +-- crates/reversible-core/src/metadata.rs | 17 ++- crates/reversible-core/src/transaction.rs | 44 ++++-- .../tests/unwrap_safety_test.rs | 26 ++-- 18 files changed, 511 insertions(+), 325 deletions(-) diff --git a/crates/januskey-cli/src/attestation.rs b/crates/januskey-cli/src/attestation.rs index 55f8a2b..8928382 100644 --- a/crates/januskey-cli/src/attestation.rs +++ b/crates/januskey-cli/src/attestation.rs @@ -269,7 +269,11 @@ impl AuditLog { } /// Log key retrieval - pub fn log_key_retrieved(&self, key_id: Uuid, fingerprint: &str) -> std::io::Result { + pub fn log_key_retrieved( + &self, + key_id: Uuid, + fingerprint: &str, + ) -> std::io::Result { let details = KeyEventDetails { key_id, fingerprint: fingerprint.to_string(), @@ -301,11 +305,7 @@ impl AuditLog { rotated_to: None, rotated_from: Some(old_key_id), }; - let reason = format!( - "Rotated from key {} ({})", - old_key_id, - old_fingerprint - ); + let reason = format!("Rotated from key {} ({})", old_key_id, old_fingerprint); self.log_event(AuditEventType::KeyRotated, Some(details), Some(reason)) } @@ -426,10 +426,7 @@ impl AuditLog { valid: true, total_entries: entries.len(), first_invalid_index: None, - message: format!( - "Audit log integrity verified: {} entries", - entries.len() - ), + message: format!("Audit log integrity verified: {} entries", entries.len()), }) } @@ -538,14 +535,21 @@ mod tests { let key_id = Uuid::new_v4(); let new_key_id = Uuid::new_v4(); - log.log_key_generated(key_id, "abc", KeyAlgorithm::Aes256Gcm, KeyPurpose::Encryption) - .expect("failed to log key generation"); + log.log_key_generated( + key_id, + "abc", + KeyAlgorithm::Aes256Gcm, + KeyPurpose::Encryption, + ) + .expect("failed to log key generation"); log.log_key_rotated(key_id, "abc", new_key_id, "def") .expect("failed to log key rotation"); log.log_key_revoked(key_id, "abc", Some("rotated")) .expect("failed to log key revocation"); - let history = log.get_key_history(key_id).expect("failed to get key history"); + let history = log + .get_key_history(key_id) + .expect("failed to get key history"); assert_eq!(history.len(), 3); } } diff --git a/crates/januskey-cli/src/delta.rs b/crates/januskey-cli/src/delta.rs index d1f6b13..7001a55 100644 --- a/crates/januskey-cli/src/delta.rs +++ b/crates/januskey-cli/src/delta.rs @@ -260,7 +260,11 @@ impl Delta { } // Preserve original line endings - let line_ending = if original_str.contains("\r\n") { "\r\n" } else { "\n" }; + let line_ending = if original_str.contains("\r\n") { + "\r\n" + } else { + "\n" + }; let result = result_lines.join(line_ending); // Add final newline if original had one @@ -435,7 +439,12 @@ fn find_block(original: &[u8], start: usize, block: &[u8]) -> Option { } /// Find how much content before the next matching block -fn find_next_match(original: &[u8], orig_start: usize, new_content: &[u8], block_size: usize) -> Option { +fn find_next_match( + original: &[u8], + orig_start: usize, + new_content: &[u8], + block_size: usize, +) -> Option { for i in 1..new_content.len() { let remaining = &new_content[i..]; if remaining.len() >= block_size { @@ -495,8 +504,10 @@ mod tests { #[test] fn test_delta_roundtrip() { - let original = b"Original content here\nWith multiple lines\nAnd some more text\n".repeat(50); - let new = b"Modified content here\nWith multiple lines\nAnd some different text\n".repeat(50); + let original = + b"Original content here\nWith multiple lines\nAnd some more text\n".repeat(50); + let new = + b"Modified content here\nWith multiple lines\nAnd some different text\n".repeat(50); let delta = Delta::compute(&original, &new); let restored = delta.apply(&original).unwrap(); diff --git a/crates/januskey-cli/src/keys.rs b/crates/januskey-cli/src/keys.rs index 23ed5c1..3cb1326 100644 --- a/crates/januskey-cli/src/keys.rs +++ b/crates/januskey-cli/src/keys.rs @@ -340,7 +340,9 @@ impl KeyManager { self.save_store(&store)?; // Log key generation - let _ = self.audit_log.log_key_generated(id, &fingerprint, algorithm, purpose); + let _ = self + .audit_log + .log_key_generated(id, &fingerprint, algorithm, purpose); Ok(id) } @@ -388,7 +390,9 @@ impl KeyManager { } // Log key retrieval - let _ = self.audit_log.log_key_retrieved(id, &wrapped.metadata.fingerprint); + let _ = self + .audit_log + .log_key_retrieved(id, &wrapped.metadata.fingerprint); unwrap_key(kek, &wrapped) } @@ -445,7 +449,9 @@ impl KeyManager { self.save_store(&store)?; // Log rotation event - let _ = self.audit_log.log_key_rotated(id, &old_fingerprint, new_id, &fingerprint); + let _ = self + .audit_log + .log_key_rotated(id, &old_fingerprint, new_id, &fingerprint); Ok(new_id) } @@ -501,7 +507,9 @@ impl KeyManager { self.save_store(&store)?; // Log revocation with reason - let _ = self.audit_log.log_key_revoked(id, &fingerprint, Some(reason)); + let _ = self + .audit_log + .log_key_revoked(id, &fingerprint, Some(reason)); Ok(()) } @@ -531,7 +539,14 @@ impl KeyManager { fn load_store_raw(&self) -> Result { let path = self.store_path.join("keystore.jks"); - let content = ({ use std::io::Read; std::fs::File::open(&path).and_then(|mut f| { let mut buf = String::new(); f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; Ok(buf) }) })?; + let content = ({ + use std::io::Read; + std::fs::File::open(&path).and_then(|mut f| { + let mut buf = String::new(); + f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; + Ok(buf) + }) + })?; let store: KeyStoreData = serde_json::from_str(&content)?; Ok(store) } @@ -641,7 +656,8 @@ mod tests { let mut km = KeyManager::new(tmp.path()); assert!(!km.is_initialized()); - km.init("test-passphrase").expect("failed to init key manager"); + km.init("test-passphrase") + .expect("failed to init key manager"); assert!(km.is_initialized()); } @@ -650,7 +666,8 @@ mod tests { let tmp = TempDir::new().expect("failed to create temp dir"); let mut km = KeyManager::new(tmp.path()); - km.init("test-passphrase").expect("failed to init key manager"); + km.init("test-passphrase") + .expect("failed to init key manager"); let id = km .generate( @@ -676,7 +693,8 @@ mod tests { let tmp = TempDir::new().expect("failed to create temp dir"); let mut km = KeyManager::new(tmp.path()); - km.init("test-passphrase").expect("failed to init key manager"); + km.init("test-passphrase") + .expect("failed to init key manager"); let old_id = km .generate(KeyAlgorithm::Aes256Gcm, KeyPurpose::Encryption, None, None) @@ -697,7 +715,8 @@ mod tests { let tmp = TempDir::new().expect("failed to create temp dir"); let mut km = KeyManager::new(tmp.path()); - km.init("correct-passphrase").expect("failed to init key manager"); + km.init("correct-passphrase") + .expect("failed to init key manager"); // Generate a key so we have something to verify against km.generate(KeyAlgorithm::Aes256Gcm, KeyPurpose::Encryption, None, None) diff --git a/crates/januskey-cli/src/keys_cli.rs b/crates/januskey-cli/src/keys_cli.rs index 4bbf3c5..073c980 100644 --- a/crates/januskey-cli/src/keys_cli.rs +++ b/crates/januskey-cli/src/keys_cli.rs @@ -201,7 +201,10 @@ fn cmd_init(km: &mut KeyManager, _no_recovery: bool) -> Result<(), Box Result<(), Box = if active_only { - keys.into_iter().filter(|k| k.state == KeyState::Active).collect() + keys.into_iter() + .filter(|k| k.state == KeyState::Active) + .collect() } else { keys }; @@ -273,7 +281,13 @@ fn cmd_generate( "aes256" | "aes-256" | "aes256gcm" => KeyAlgorithm::Aes256Gcm, "ed25519" => KeyAlgorithm::Ed25519, "x25519" => KeyAlgorithm::X25519, - _ => return Err(format!("Unknown key type: {}. Use: aes256, ed25519, x25519", key_type).into()), + _ => { + return Err(format!( + "Unknown key type: {}. Use: aes256, ed25519, x25519", + key_type + ) + .into()) + } }; let key_purpose = match purpose.to_lowercase().as_str() { @@ -281,7 +295,13 @@ fn cmd_generate( "signing" | "sign" => KeyPurpose::Signing, "keywrap" | "key-wrap" | "wrap" => KeyPurpose::KeyWrap, "recovery" => KeyPurpose::Recovery, - _ => return Err(format!("Unknown purpose: {}. Use: encryption, signing, keywrap, recovery", purpose).into()), + _ => { + return Err(format!( + "Unknown purpose: {}. Use: encryption, signing, keywrap, recovery", + purpose + ) + .into()) + } }; println!("{}", "Generating key...".cyan()); @@ -318,7 +338,10 @@ fn cmd_show(km: &mut KeyManager, key_id: Uuid) -> Result<(), Box Result<(), Box Result<(), Box> { return Err("Key store not initialized. Run 'jk-keys init' first.".into()); } - let passphrase = Password::new() - .with_prompt("Enter passphrase") - .interact()?; + let passphrase = Password::new().with_prompt("Enter passphrase").interact()?; km.unlock(&passphrase)?; Ok(()) @@ -563,18 +588,29 @@ fn cmd_audit_history(km: &mut KeyManager, key_id: Uuid) -> Result<(), Box Result<(), Box Result<(), Box> { +fn cmd_audit_export( + km: &mut KeyManager, + output: &PathBuf, +) -> Result<(), Box> { unlock_store(km)?; if output.exists() { let confirm = Confirm::new() - .with_prompt(format!("File {} already exists. Overwrite?", output.display())) + .with_prompt(format!( + "File {} already exists. Overwrite?", + output.display() + )) .default(false) .interact()?; diff --git a/crates/januskey-cli/src/lib.rs b/crates/januskey-cli/src/lib.rs index 87c341e..6157bf7 100644 --- a/crates/januskey-cli/src/lib.rs +++ b/crates/januskey-cli/src/lib.rs @@ -24,15 +24,13 @@ pub mod operations; pub use reversible_core::content_store::{self, ContentHash, ContentStore}; /// Error module — re-exports reversible-core error types with JanusKey naming pub mod error { - pub use reversible_core::error::ReversibleError as JanusError; pub use reversible_core::error::Result; + pub use reversible_core::error::ReversibleError as JanusError; } pub use error::{JanusError, Result}; pub use reversible_core::manifest::{self, ManifestEmitter}; pub use reversible_core::metadata::{self, MetadataStore, OperationMetadata, OperationType}; -pub use reversible_core::transaction::{ - self, Transaction, TransactionManager, TransactionPreview, -}; +pub use reversible_core::transaction::{self, Transaction, TransactionManager, TransactionPreview}; pub use reversible_core::ReversibleExecutor; pub use attestation::{AuditEntry, AuditEventType, AuditLog, IntegrityReport, KeyEventDetails}; @@ -78,7 +76,14 @@ impl Config { pub fn load(dir: &std::path::Path) -> Self { let config_path = dir.join(".januskey").join("config.json"); if config_path.exists() { - if let Ok(content) = ({ use std::io::Read; std::fs::File::open(&config_path).and_then(|mut f| { let mut buf = String::new(); f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; Ok(buf) }) }) { + if let Ok(content) = ({ + use std::io::Read; + std::fs::File::open(&config_path).and_then(|mut f| { + let mut buf = String::new(); + f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; + Ok(buf) + }) + }) { if let Ok(config) = serde_json::from_str(&content) { return config; } diff --git a/crates/januskey-cli/src/main.rs b/crates/januskey-cli/src/main.rs index f1c4c3e..a17d803 100644 --- a/crates/januskey-cli/src/main.rs +++ b/crates/januskey-cli/src/main.rs @@ -189,15 +189,20 @@ fn main() -> Result<()> { Commands::Modify { pattern, paths } => { cmd_modify(&working_dir, &pattern, &paths, cli.dry_run, cli.yes) } - Commands::Move { source, destination } => { - cmd_move(&working_dir, &source, &destination, cli.dry_run) - } - Commands::Copy { source, destination } => { - cmd_copy(&working_dir, &source, &destination, cli.dry_run) - } - Commands::Rename { old_name, new_name } => { - cmd_move(&working_dir, &old_name.to_string_lossy(), &new_name, cli.dry_run) - } + Commands::Move { + source, + destination, + } => cmd_move(&working_dir, &source, &destination, cli.dry_run), + Commands::Copy { + source, + destination, + } => cmd_copy(&working_dir, &source, &destination, cli.dry_run), + Commands::Rename { old_name, new_name } => cmd_move( + &working_dir, + &old_name.to_string_lossy(), + &new_name, + cli.dry_run, + ), Commands::Obliterate { paths } => { cmd_obliterate(&working_dir, &paths, cli.dry_run, cli.yes) } @@ -223,11 +228,7 @@ fn cmd_init(dir: &PathBuf) -> Result<()> { } JanusKey::init(dir).context("Failed to initialize JanusKey")?; - println!( - "{} JanusKey initialized in {}", - "✓".green(), - dir.display() - ); + println!("{} JanusKey initialized in {}", "✓".green(), dir.display()); println!(" Metadata stored in: {}/.januskey/", dir.display()); println!("\n You can now use reversible file operations:"); println!(" jk delete - Delete files (reversible)"); @@ -332,7 +333,10 @@ fn cmd_delete( deleted_count += 1; if let Some(ref pb) = progress { pb.inc(1); - pb.set_message(format!("{}", path.file_name().unwrap_or_default().to_string_lossy())); + pb.set_message(format!( + "{}", + path.file_name().unwrap_or_default().to_string_lossy() + )); } // Record in transaction if active if transaction_id.is_some() { @@ -349,11 +353,7 @@ fn cmd_delete( pb.finish_and_clear(); } - println!( - "{} Deleted {} file(s)", - "✓".green(), - deleted_count - ); + println!("{} Deleted {} file(s)", "✓".green(), deleted_count); println!(" Use {} to restore", "jk undo".cyan()); Ok(()) @@ -392,7 +392,14 @@ fn cmd_modify( // Preview changes let mut changes = Vec::new(); for file in &files { - let content = ({ use std::io::Read; std::fs::File::open(file).and_then(|mut f| { let mut buf = String::new(); f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; Ok(buf) }) })?; + let content = ({ + use std::io::Read; + std::fs::File::open(file).and_then(|mut f| { + let mut buf = String::new(); + f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; + Ok(buf) + }) + })?; let new_content = if global { content.replace(&search, &replace) } else { @@ -418,11 +425,7 @@ fn cmd_modify( // Confirm if changes.len() > 5 && !auto_yes { - println!( - "{} This will modify {} files", - "⚠".yellow(), - changes.len() - ); + println!("{} This will modify {} files", "⚠".yellow(), changes.len()); if !Confirm::new() .with_prompt("Continue?") .default(false) @@ -584,18 +587,19 @@ fn cmd_copy(dir: &PathBuf, source: &PathBuf, destination: &PathBuf, dry_run: boo Ok(()) } -fn cmd_obliterate( - dir: &PathBuf, - paths: &[PathBuf], - dry_run: bool, - auto_yes: bool, -) -> Result<()> { +fn cmd_obliterate(dir: &PathBuf, paths: &[PathBuf], dry_run: bool, auto_yes: bool) -> Result<()> { use januskey::obliteration::obliterate_file; // Resolve each path against the working directory if it is relative. let targets: Vec = paths .iter() - .map(|p| if p.is_absolute() { p.clone() } else { dir.join(p) }) + .map(|p| { + if p.is_absolute() { + p.clone() + } else { + dir.join(p) + } + }) .collect(); if dry_run { @@ -679,7 +683,12 @@ fn cmd_undo(dir: &PathBuf, count: usize, id: Option) -> Result<()> { ); } else { // Undo last N operations - let ops_to_undo: Vec<_> = jk.metadata_store.last_n(count).into_iter().cloned().collect(); + let ops_to_undo: Vec<_> = jk + .metadata_store + .last_n(count) + .into_iter() + .cloned() + .collect(); if ops_to_undo.is_empty() { println!("{} Nothing to undo", "!".yellow()); @@ -723,7 +732,11 @@ fn cmd_begin(dir: &PathBuf, name: Option) -> Result<()> { "✓".green(), display_name.cyan() ); - println!(" Run operations, then use {} or {}", "jk commit".cyan(), "jk rollback".cyan()); + println!( + " Run operations, then use {} or {}", + "jk commit".cyan(), + "jk rollback".cyan() + ); Ok(()) } @@ -755,8 +768,7 @@ fn cmd_rollback(dir: &PathBuf) -> Result<()> { // Undo operations in reverse order (Theorem 3.4: Sequential Reversibility) for op_id in active_tx.operation_ids.iter().rev() { - let mut executor = - OperationExecutor::new(&jk.content_store, &mut jk.metadata_store); + let mut executor = OperationExecutor::new(&jk.content_store, &mut jk.metadata_store); executor.undo(op_id)?; } @@ -783,13 +795,19 @@ fn cmd_preview(dir: &PathBuf) -> Result<()> { let preview = TransactionPreview::from_transaction(tx, &jk.metadata_store); - let name = preview.transaction_name.unwrap_or_else(|| tx.id[..8].to_string()); + let name = preview + .transaction_name + .unwrap_or_else(|| tx.id[..8].to_string()); println!("{} Transaction: {}", "📋".to_string(), name.cyan()); println!("Operations pending: {}", preview.operations.len()); println!(); for op in &preview.operations { - let arrow = if op.secondary_path.is_some() { " → " } else { "" }; + let arrow = if op.secondary_path.is_some() { + " → " + } else { + "" + }; let secondary = op .secondary_path .as_ref() @@ -837,7 +855,12 @@ fn cmd_history(dir: &PathBuf, limit: usize, filter: Option) -> Result<() .take(limit) .collect() } else { - jk.metadata_store.operations().iter().rev().take(limit).collect() + jk.metadata_store + .operations() + .iter() + .rev() + .take(limit) + .collect() }; if ops.is_empty() { @@ -897,11 +920,7 @@ fn cmd_status(dir: &PathBuf) -> Result<()> { if let Some(tx) = jk.transaction_manager.active() { let name = tx.name.clone().unwrap_or_else(|| tx.id[..8].to_string()); println!(); - println!( - "{} Active transaction: {}", - "📝".to_string(), - name.cyan() - ); + println!("{} Active transaction: {}", "📝".to_string(), name.cyan()); println!(" Started: {}", tx.started_at.format("%Y-%m-%d %H:%M:%S")); println!(" Operations: {}", tx.operation_ids.len()); } else { diff --git a/crates/januskey-cli/src/obliteration.rs b/crates/januskey-cli/src/obliteration.rs index c3b5fb5..5282d0b 100644 --- a/crates/januskey-cli/src/obliteration.rs +++ b/crates/januskey-cli/src/obliteration.rs @@ -146,7 +146,14 @@ impl ObliterationManager { /// Create or open an obliteration manager pub fn new(log_path: PathBuf) -> Result { let log = if log_path.exists() { - let content = ({ use std::io::Read; std::fs::File::open(&log_path).and_then(|mut f| { let mut buf = String::new(); f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; Ok(buf) }) })?; + let content = ({ + use std::io::Read; + std::fs::File::open(&log_path).and_then(|mut f| { + let mut buf = String::new(); + f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; + Ok(buf) + }) + })?; serde_json::from_str(&content) .map_err(|e| JanusError::MetadataCorrupted(e.to_string()))? } else { @@ -415,9 +422,8 @@ mod tests { let tmp = TempDir::new().expect("failed to create temp dir"); let content_store = ContentStore::new(tmp.path().join("content"), false) .expect("failed to create content store"); - let obliteration_manager = - ObliterationManager::new(tmp.path().join("obliterations.json")) - .expect("failed to create obliteration manager"); + let obliteration_manager = ObliterationManager::new(tmp.path().join("obliterations.json")) + .expect("failed to create obliteration manager"); (tmp, content_store, obliteration_manager) } @@ -446,7 +452,9 @@ mod tests { // Store some content let content = b"sensitive data to be obliterated"; - let hash = content_store.store(content).expect("failed to store content"); + let hash = content_store + .store(content) + .expect("failed to store content"); // Verify it exists assert!(content_store.exists(&hash)); @@ -475,18 +483,20 @@ mod tests { // Store and obliterate content let content = b"data to obliterate"; - let hash = content_store.store(content).expect("failed to store content"); + let hash = content_store + .store(content) + .expect("failed to store content"); let record = obliteration_manager .obliterate(&content_store, &hash, None, None) .expect("failed to obliterate content"); // Reopen manager and verify log - let obliteration_manager2 = - ObliterationManager::new(tmp.path().join("obliterations.json")) - .expect("failed to reopen obliteration manager"); + let obliteration_manager2 = ObliterationManager::new(tmp.path().join("obliterations.json")) + .expect("failed to reopen obliteration manager"); assert_eq!(obliteration_manager2.count(), 1); - let retrieved = obliteration_manager2.get(&record.id) + let retrieved = obliteration_manager2 + .get(&record.id) .expect("failed to retrieve obliteration record"); assert_eq!(retrieved.content_hash, hash); } @@ -517,7 +527,8 @@ mod tests { let hashes: Vec = (0..5) .map(|i| { let content = format!("content {}", i); - content_store.store(content.as_bytes()) + content_store + .store(content.as_bytes()) .expect("failed to store batch content") }) .collect(); diff --git a/crates/januskey-cli/src/operations.rs b/crates/januskey-cli/src/operations.rs index 1be35f4..2813ca0 100644 --- a/crates/januskey-cli/src/operations.rs +++ b/crates/januskey-cli/src/operations.rs @@ -15,14 +15,9 @@ use std::path::{Path, PathBuf}; #[derive(Debug, Clone)] pub enum FileOperation { /// Delete a file (reversible: restore from stored content) - Delete { - path: PathBuf, - }, + Delete { path: PathBuf }, /// Modify a file (reversible: restore original content) - Modify { - path: PathBuf, - new_content: Vec, - }, + Modify { path: PathBuf, new_content: Vec }, /// Move/rename a file (reversible: move back) Move { source: PathBuf, @@ -35,15 +30,9 @@ pub enum FileOperation { }, /// Change permissions (reversible: restore original perms) #[cfg(unix)] - Chmod { - path: PathBuf, - new_mode: u32, - }, + Chmod { path: PathBuf, new_mode: u32 }, /// Create a new file (reversible: delete) - Create { - path: PathBuf, - content: Vec, - }, + Create { path: PathBuf, content: Vec }, } impl FileOperation { @@ -82,10 +71,7 @@ pub struct OperationExecutor<'a> { } impl<'a> OperationExecutor<'a> { - pub fn new( - content_store: &'a ContentStore, - metadata_store: &'a mut MetadataStore, - ) -> Self { + pub fn new(content_store: &'a ContentStore, metadata_store: &'a mut MetadataStore) -> Self { Self { content_store, metadata_store, @@ -102,15 +88,15 @@ impl<'a> OperationExecutor<'a> { pub fn execute(&mut self, operation: FileOperation) -> Result { match operation { FileOperation::Delete { path } => self.execute_delete(&path), - FileOperation::Modify { path, new_content } => { - self.execute_modify(&path, &new_content) - } - FileOperation::Move { source, destination } => { - self.execute_move(&source, &destination) - } - FileOperation::Copy { source, destination } => { - self.execute_copy(&source, &destination) - } + FileOperation::Modify { path, new_content } => self.execute_modify(&path, &new_content), + FileOperation::Move { + source, + destination, + } => self.execute_move(&source, &destination), + FileOperation::Copy { + source, + destination, + } => self.execute_copy(&source, &destination), #[cfg(unix)] FileOperation::Chmod { path, new_mode } => self.execute_chmod(&path, new_mode), FileOperation::Create { path, content } => self.execute_create(&path, &content), @@ -342,7 +328,8 @@ impl<'a> OperationExecutor<'a> { }; // Mark original operation as undone - self.metadata_store.mark_undone(operation_id, &undo_metadata.id)?; + self.metadata_store + .mark_undone(operation_id, &undo_metadata.id)?; Ok(undo_metadata) } @@ -425,10 +412,9 @@ impl<'a> OperationExecutor<'a> { /// Undo chmod: restore original permissions #[cfg(unix)] fn undo_chmod(&mut self, original: &OperationMetadata) -> Result { - let file_meta = original - .original_metadata - .as_ref() - .ok_or_else(|| JanusError::MetadataCorrupted("Missing original metadata".to_string()))?; + let file_meta = original.original_metadata.as_ref().ok_or_else(|| { + JanusError::MetadataCorrupted("Missing original metadata".to_string()) + })?; let chmod_op = FileOperation::Chmod { path: original.path.clone(), @@ -483,10 +469,8 @@ mod tests { fn setup() -> (TempDir, ContentStore, MetadataStore) { let tmp = TempDir::new().unwrap(); - let content_store = - ContentStore::new(tmp.path().join("content"), false).unwrap(); - let metadata_store = - MetadataStore::new(tmp.path().join("metadata.json")).unwrap(); + let content_store = ContentStore::new(tmp.path().join("content"), false).unwrap(); + let metadata_store = MetadataStore::new(tmp.path().join("metadata.json")).unwrap(); (tmp, content_store, metadata_store) } @@ -513,7 +497,18 @@ mod tests { executor.undo(&delete_meta.id).unwrap(); assert!(test_file.exists()); - assert_eq!(({ use std::io::Read; std::fs::File::open(&test_file).and_then(|mut f| { let mut buf = String::new(); f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; Ok(buf) }) }).unwrap(), "hello world"); + assert_eq!( + ({ + use std::io::Read; + std::fs::File::open(&test_file).and_then(|mut f| { + let mut buf = String::new(); + f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; + Ok(buf) + }) + }) + .unwrap(), + "hello world" + ); } #[test] @@ -533,13 +528,35 @@ mod tests { }) .unwrap(); - assert_eq!(({ use std::io::Read; std::fs::File::open(&test_file).and_then(|mut f| { let mut buf = String::new(); f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; Ok(buf) }) }).unwrap(), "modified content"); + assert_eq!( + ({ + use std::io::Read; + std::fs::File::open(&test_file).and_then(|mut f| { + let mut buf = String::new(); + f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; + Ok(buf) + }) + }) + .unwrap(), + "modified content" + ); // Undo the modify let mut executor = OperationExecutor::new(&content_store, &mut metadata_store); executor.undo(&modify_meta.id).unwrap(); - assert_eq!(({ use std::io::Read; std::fs::File::open(&test_file).and_then(|mut f| { let mut buf = String::new(); f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; Ok(buf) }) }).unwrap(), "original content"); + assert_eq!( + ({ + use std::io::Read; + std::fs::File::open(&test_file).and_then(|mut f| { + let mut buf = String::new(); + f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; + Ok(buf) + }) + }) + .unwrap(), + "original content" + ); } #[test] diff --git a/crates/januskey-cli/tests/aspect_test.rs b/crates/januskey-cli/tests/aspect_test.rs index de1b776..a9fc372 100644 --- a/crates/januskey-cli/tests/aspect_test.rs +++ b/crates/januskey-cli/tests/aspect_test.rs @@ -40,8 +40,11 @@ fn create_test_key(base: &PathBuf, key_id: &str, material: &[u8]) -> String { r#"{{"id":"{}","hash":"{}","state":"active"}}"#, key_id, hash ); - fs::write(base.join(".jk/keys").join(format!("{}.json", key_id)), &key_record) - .expect("Write key record"); + fs::write( + base.join(".jk/keys").join(format!("{}.json", key_id)), + &key_record, + ) + .expect("Write key record"); hash } @@ -100,8 +103,7 @@ fn obliterated_key_truly_unrecoverable() { ); // Attempt to read from any disk location fails - let dir_entries = fs::read_dir(base.join(".jk/content")) - .expect("Read dir"); + let dir_entries = fs::read_dir(base.join(".jk/content")).expect("Read dir"); let content_files: Vec<_> = dir_entries.filter_map(Result::ok).collect(); assert_eq!( content_files.len(), @@ -127,15 +129,29 @@ fn obliterated_key_record_marked_revoked() { // Mark the key record as revoked let revoked_record = format!( r#"{{"id":"{}","hash":"{}","state":"revoked","revoked_at":{},"obliteration_proof":"proof-{}"}}"#, - key_id, hash, chrono_timestamp(), key_id + key_id, + hash, + chrono_timestamp(), + key_id ); - fs::write(base.join(".jk/keys").join(format!("{}.json", key_id)), &revoked_record) - .expect("Write revoked record"); + fs::write( + base.join(".jk/keys").join(format!("{}.json", key_id)), + &revoked_record, + ) + .expect("Write revoked record"); // Verify key record reflects revocation - let key_content = - ({ use std::io::Read; std::fs::File::open(base.join(".jk/keys").and_then(|mut f| { let mut buf = String::new(); f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; Ok(buf) }) }).join(format!("{}.json", key_id))) - .expect("Read key record"); + let key_content = ({ + use std::io::Read; + std::fs::File::open(base.join(".jk/keys").join(format!("{}.json", key_id))).and_then( + |mut f| { + let mut buf = String::new(); + f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; + Ok(buf) + }, + ) + }) + .expect("Read key record"); assert!( key_content.contains("revoked"), "Key record must be marked revoked" @@ -232,27 +248,29 @@ fn obliteration_proof_generated_and_stored() { "storage_cleared":true, "commitment":"proof-commitment-hash" }}"#, - proof_id, key_id, hash, chrono_timestamp() + proof_id, + key_id, + hash, + chrono_timestamp() ); // Store proof fs::write( - base.join(".jk/obliteration").join(format!("{}.json", proof_id)), + base.join(".jk/obliteration") + .join(format!("{}.json", proof_id)), &proof, ) .expect("Write proof"); // Verify proof exists and is valid JSON let proof_content = fs::read_to_string( - base.join(".jk/obliteration").join(format!("{}.json", proof_id)), + base.join(".jk/obliteration") + .join(format!("{}.json", proof_id)), ) .expect("Read proof"); let parsed = serde_json::from_str::(&proof_content); - assert!( - parsed.is_ok(), - "Obliteration proof must be valid JSON" - ); + assert!(parsed.is_ok(), "Obliteration proof must be valid JSON"); let proof_obj = parsed.unwrap(); assert_eq!( @@ -305,10 +323,7 @@ fn obliteration_under_concurrent_access_no_leak() { _read_handle.join().expect("Thread join"); // Post-obliteration: no trace remains - assert!( - !content_path.exists(), - "Content must be completely removed" - ); + assert!(!content_path.exists(), "Content must be completely removed"); // Verify key cannot be accessed let post_attempt = fs::read(&content_path); @@ -341,24 +356,12 @@ fn multiple_keys_obliterated_independently() { fs::remove_file(&path3).expect("Delete key 3"); // Verify: key 1 and 3 are gone - assert!( - !path1.exists(), - "Key 1 must be obliterated" - ); - assert!( - !path3.exists(), - "Key 3 must be obliterated" - ); + assert!(!path1.exists(), "Key 1 must be obliterated"); + assert!(!path3.exists(), "Key 3 must be obliterated"); // Verify: key 2 is still present - assert!( - path2.exists(), - "Key 2 must remain intact" - ); + assert!(path2.exists(), "Key 2 must remain intact"); let read2 = fs::read(&path2).expect("Read key 2"); - assert_eq!( - read2, key2_material, - "Key 2 content must be unchanged" - ); + assert_eq!(read2, key2_material, "Key 2 content must be unchanged"); } diff --git a/crates/januskey-cli/tests/concurrency_test.rs b/crates/januskey-cli/tests/concurrency_test.rs index 9dc7da9..81ba781 100644 --- a/crates/januskey-cli/tests/concurrency_test.rs +++ b/crates/januskey-cli/tests/concurrency_test.rs @@ -63,21 +63,15 @@ fn concurrent_key_operations_no_deadlock() { .expect(&format!("Write failed for {}", key_id)); // Record key - let key_record = format!( - r#"{{"id":"{}","hash":"{}","thread":{}}}"#, - key_id, hash, i - ); + let key_record = format!(r#"{{"id":"{}","hash":"{}","thread":{}}}"#, key_id, hash, i); fs::write( - base_clone - .join(".jk/keys") - .join(format!("{}.json", key_id)), + base_clone.join(".jk/keys").join(format!("{}.json", key_id)), &key_record, ) .expect(&format!("Key record failed for {}", key_id)); // Read back immediately - let read_back = fs::read(&content_path) - .expect(&format!("Read failed for {}", key_id)); + let read_back = fs::read(&content_path).expect(&format!("Read failed for {}", key_id)); assert_eq!( read_back, material.as_bytes(), @@ -122,8 +116,7 @@ fn transaction_isolation_uncommitted_invisible() { // Start transaction in main thread let tx_id = "tx-isolation-001"; let tx_record = r#"{"id":"tx-isolation-001","state":"active"}"#; - fs::write(base.join(".jk/transactions/001.json"), tx_record) - .expect("Write transaction"); + fs::write(base.join(".jk/transactions/001.json"), tx_record).expect("Write transaction"); // Spawn reader thread let base_clone = Arc::clone(&base); @@ -164,8 +157,15 @@ fn transaction_isolation_uncommitted_invisible() { // Reader may see the file (filesystem is not transactional), // but we verify the transaction itself is still "active" not "committed" - let tx_read = ({ use std::io::Read; std::fs::File::open(base.join(".jk/transactions/001.json").and_then(|mut f| { let mut buf = String::new(); f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; Ok(buf) }) })) - .expect("Read transaction"); + let tx_read = ({ + use std::io::Read; + std::fs::File::open(base.join(".jk/transactions/001.json")).and_then(|mut f| { + let mut buf = String::new(); + f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; + Ok(buf) + }) + }) + .expect("Read transaction"); assert!( tx_read.contains("\"active\""), "Transaction must remain active while uncommitted" @@ -187,10 +187,7 @@ fn concurrent_transactions_isolated() { let tx_id = format!("tx-{:02}", tx_idx); // Begin transaction - let tx_record = format!( - r#"{{"id":"{}","state":"active"}}"#, - tx_id - ); + let tx_record = format!(r#"{{"id":"{}","state":"active"}}"#, tx_id); fs::write( base_clone .join(".jk/transactions") @@ -201,10 +198,7 @@ fn concurrent_transactions_isolated() { // Perform operations within transaction for op_idx in 0..3 { - let op_record = format!( - r#"{{"tx":"{}","op":"copy","seq":{}}}"#, - tx_id, op_idx - ); + let op_record = format!(r#"{{"tx":"{}","op":"copy","seq":{}}}"#, tx_id, op_idx); fs::write( base_clone .join(".jk/operations") @@ -215,10 +209,7 @@ fn concurrent_transactions_isolated() { } // Commit - let commit_record = format!( - r#"{{"id":"{}","state":"committed"}}"#, - tx_id - ); + let commit_record = format!(r#"{{"id":"{}","state":"committed"}}"#, tx_id); fs::write( base_clone .join(".jk/transactions") @@ -235,11 +226,7 @@ fn concurrent_transactions_isolated() { // Wait for all transactions for (i, handle) in handles.into_iter().enumerate() { let result = handle.join().expect("Thread panicked"); - assert_eq!( - result, i, - "Transaction {} should complete successfully", - i - ); + assert_eq!(result, i, "Transaction {} should complete successfully", i); } // Verify all transactions exist and are committed @@ -338,10 +325,7 @@ fn concurrent_commit_rollback_no_corruption() { let tx_id = format!("tx-race-{:02}", i); // Begin - let tx_record = format!( - r#"{{"id":"{}","state":"active"}}"#, - tx_id - ); + let tx_record = format!(r#"{{"id":"{}","state":"active"}}"#, tx_id); fs::write( base_clone .join(".jk/transactions") @@ -352,10 +336,7 @@ fn concurrent_commit_rollback_no_corruption() { // Add operations for op in 0..5 { - let op_record = format!( - r#"{{"tx":"{}","op":"copy","seq":{}}}"#, - tx_id, op - ); + let op_record = format!(r#"{{"tx":"{}","op":"copy","seq":{}}}"#, tx_id, op); fs::write( base_clone .join(".jk/operations") @@ -366,11 +347,12 @@ fn concurrent_commit_rollback_no_corruption() { } // Randomly commit or rollback - let action = if i % 2 == 0 { "committed" } else { "rolled_back" }; - let final_record = format!( - r#"{{"id":"{}","state":"{}"}}"#, - tx_id, action - ); + let action = if i % 2 == 0 { + "committed" + } else { + "rolled_back" + }; + let final_record = format!(r#"{{"id":"{}","state":"{}"}}"#, tx_id, action); fs::write( base_clone .join(".jk/transactions") @@ -404,7 +386,15 @@ fn concurrent_commit_rollback_no_corruption() { // Verify each transaction is in a valid terminal state for entry in fs::read_dir(base.join(".jk/transactions")).expect("Read dir") { let entry = entry.expect("Dir entry"); - let content = ({ use std::io::Read; std::fs::File::open(entry.path().and_then(|mut f| { let mut buf = String::new(); f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; Ok(buf) }) })).expect("Read tx file"); + let content = ({ + use std::io::Read; + std::fs::File::open(entry.path()).and_then(|mut f| { + let mut buf = String::new(); + f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; + Ok(buf) + }) + }) + .expect("Read tx file"); let is_valid = content.contains("\"committed\"") || content.contains("\"rolled_back\""); assert!( is_valid, diff --git a/crates/januskey-cli/tests/e2e_test.rs b/crates/januskey-cli/tests/e2e_test.rs index ba52b92..9641a49 100644 --- a/crates/januskey-cli/tests/e2e_test.rs +++ b/crates/januskey-cli/tests/e2e_test.rs @@ -73,22 +73,44 @@ fn full_key_lifecycle_single_key() { chrono_timestamp(), key_hash ); - fs::write(base.join(".jk/attestation/0001.json"), &attest_entry) - .expect("Write attestation"); + fs::write(base.join(".jk/attestation/0001.json"), &attest_entry).expect("Write attestation"); // Step 4: Retrieve - verify content matches let retrieved = fs::read(&content_path).expect("Read content"); - assert_eq!(retrieved, key_material, "Retrieved content must match original"); + assert_eq!( + retrieved, key_material, + "Retrieved content must match original" + ); // Step 5: Verify attestation references the key - let attest_read = ({ use std::io::Read; std::fs::File::open(base.join(".jk/attestation/0001.json").and_then(|mut f| { let mut buf = String::new(); f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; Ok(buf) }) })) - .expect("Read attestation"); - assert!(attest_read.contains(key_id), "Attestation must contain key ID"); - assert!(attest_read.contains(&key_hash), "Attestation must contain content hash"); + let attest_read = ({ + use std::io::Read; + std::fs::File::open(base.join(".jk/attestation/0001.json")).and_then(|mut f| { + let mut buf = String::new(); + f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; + Ok(buf) + }) + }) + .expect("Read attestation"); + assert!( + attest_read.contains(key_id), + "Attestation must contain key ID" + ); + assert!( + attest_read.contains(&key_hash), + "Attestation must contain content hash" + ); // Verify key record exists - let key_read = ({ use std::io::Read; std::fs::File::open(base.join(".jk/keys/001.json").and_then(|mut f| { let mut buf = String::new(); f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; Ok(buf) }) })) - .expect("Read key record"); + let key_read = ({ + use std::io::Read; + std::fs::File::open(base.join(".jk/keys/001.json")).and_then(|mut f| { + let mut buf = String::new(); + f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; + Ok(buf) + }) + }) + .expect("Read key record"); assert!(key_read.contains(key_id), "Key record must contain ID"); } @@ -101,8 +123,7 @@ fn full_key_lifecycle_multi_key_transaction() { // Start transaction let tx_id = "tx-001"; let tx_record = r#"{"id":"tx-001","state":"active","ops":[]}"#; - fs::write(base.join(".jk/transactions/001.json"), tx_record) - .expect("Write transaction"); + fs::write(base.join(".jk/transactions/001.json"), tx_record).expect("Write transaction"); // Generate 3 keys within transaction let keys: Vec<(&str, &[u8])> = vec![ @@ -162,13 +183,22 @@ fn full_key_lifecycle_multi_key_transaction() { // Commit transaction let commit_record = r#"{"id":"tx-001","state":"committed","ops":["op-0","op-1","op-2"]}"#; - fs::write(base.join(".jk/transactions/001.json"), commit_record) - .expect("Commit transaction"); + fs::write(base.join(".jk/transactions/001.json"), commit_record).expect("Commit transaction"); // Verify transaction is committed - let tx_read = - ({ use std::io::Read; std::fs::File::open(base.join(".jk/transactions/001.json").and_then(|mut f| { let mut buf = String::new(); f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; Ok(buf) }) })).expect("Read tx"); - assert!(tx_read.contains("committed"), "Transaction must be committed"); + let tx_read = ({ + use std::io::Read; + std::fs::File::open(base.join(".jk/transactions/001.json")).and_then(|mut f| { + let mut buf = String::new(); + f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; + Ok(buf) + }) + }) + .expect("Read tx"); + assert!( + tx_read.contains("committed"), + "Transaction must be committed" + ); } #[test] @@ -200,34 +230,33 @@ fn delta_chain_full_history() { r#"{{"from":"{}","to":"{}","delta_type":"full_rewrite","seq":1}}"#, v1_hash, v2_hash ); - fs::write(base.join(".jk/metadata/delta-01.json"), &delta_1_to_2) - .expect("Write delta 1→2"); + fs::write(base.join(".jk/metadata/delta-01.json"), &delta_1_to_2).expect("Write delta 1→2"); let delta_2_to_3 = format!( r#"{{"from":"{}","to":"{}","delta_type":"full_rewrite","seq":2}}"#, v2_hash, v3_hash ); - fs::write(base.join(".jk/metadata/delta-02.json"), &delta_2_to_3) - .expect("Write delta 2→3"); + fs::write(base.join(".jk/metadata/delta-02.json"), &delta_2_to_3).expect("Write delta 2→3"); // Verify chain is intact: can read all versions - let read_v1 = fs::read(&base.join(".jk/content").join(&v1_hash)) - .expect("Read v1"); + let read_v1 = fs::read(&base.join(".jk/content").join(&v1_hash)).expect("Read v1"); assert_eq!(read_v1, v1, "Version 1 must be recoverable"); - let read_v2 = fs::read(&base.join(".jk/content").join(&v2_hash)) - .expect("Read v2"); + let read_v2 = fs::read(&base.join(".jk/content").join(&v2_hash)).expect("Read v2"); assert_eq!(read_v2, v2, "Version 2 must be recoverable"); - let read_v3 = fs::read(&base.join(".jk/content").join(&v3_hash)) - .expect("Read v3"); + let read_v3 = fs::read(&base.join(".jk/content").join(&v3_hash)).expect("Read v3"); assert_eq!(read_v3, v3, "Version 3 must be recoverable"); // Verify chain links are recorded let delta_files: Vec<_> = fs::read_dir(base.join(".jk/metadata")) .expect("Read metadata") .filter_map(Result::ok) - .filter(|e| e.file_name().to_str().map_or(false, |n| n.starts_with("delta"))) + .filter(|e| { + e.file_name() + .to_str() + .map_or(false, |n| n.starts_with("delta")) + }) .collect(); assert_eq!(delta_files.len(), 2, "Delta chain must have 2 links"); } @@ -248,10 +277,7 @@ fn content_store_write_verify_read_delete() { // Write fs::write(&store_path, content).expect("Write content"); - assert!( - store_path.exists(), - "Content must exist after write" - ); + assert!(store_path.exists(), "Content must exist after write"); // Verify hash (read back and re-hash) let read_back = fs::read(&store_path).expect("Read content"); @@ -263,7 +289,10 @@ fn content_store_write_verify_read_delete() { // Read again let read_again = fs::read(&store_path).expect("Read content again"); - assert_eq!(read_again, content, "Multiple reads must return same content"); + assert_eq!( + read_again, content, + "Multiple reads must return same content" + ); // Delete (simulate obliteration by removing file) fs::remove_file(&store_path).expect("Delete file"); @@ -274,10 +303,7 @@ fn content_store_write_verify_read_delete() { // Verify truly gone (attempt read fails) let read_deleted = fs::read(&store_path); - assert!( - read_deleted.is_err(), - "Reading deleted content must fail" - ); + assert!(read_deleted.is_err(), "Reading deleted content must fail"); } #[test] @@ -292,17 +318,11 @@ fn content_store_deduplication_multiple_keys() { let store_path = base.join(".jk/content").join(&shared_hash); // Key 1 references shared content - let key1 = format!( - r#"{{"id":"key-1","hash":"{}"}}"#, - shared_hash - ); + let key1 = format!(r#"{{"id":"key-1","hash":"{}"}}"#, shared_hash); fs::write(base.join(".jk/keys/001.json"), &key1).expect("Write key1"); // Key 2 references same shared content - let key2 = format!( - r#"{{"id":"key-2","hash":"{}"}}"#, - shared_hash - ); + let key2 = format!(r#"{{"id":"key-2","hash":"{}"}}"#, shared_hash); fs::write(base.join(".jk/keys/002.json"), &key2).expect("Write key2"); // Content is stored only once @@ -341,10 +361,7 @@ fn retrieve_nonexistent_key_fails() { let store_path = base.join(".jk/content").join(nonexistent_hash); let result = fs::read(&store_path); - assert!( - result.is_err(), - "Reading nonexistent key must fail" - ); + assert!(result.is_err(), "Reading nonexistent key must fail"); } #[test] @@ -355,12 +372,18 @@ fn corrupted_attestation_entry_detected() { // Write malformed attestation let bad_entry = r#"{"invalid json"#; - fs::write(base.join(".jk/attestation/0001.json"), bad_entry) - .expect("Write malformed entry"); + fs::write(base.join(".jk/attestation/0001.json"), bad_entry).expect("Write malformed entry"); // Attempt to read and parse - let read_result = ({ use std::io::Read; std::fs::File::open(base.join(".jk/attestation/0001.json").and_then(|mut f| { let mut buf = String::new(); f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; Ok(buf) }) })) - .expect("Read file"); + let read_result = ({ + use std::io::Read; + std::fs::File::open(base.join(".jk/attestation/0001.json")).and_then(|mut f| { + let mut buf = String::new(); + f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; + Ok(buf) + }) + }) + .expect("Read file"); let parse_result = serde_json::from_str::(&read_result); assert!( diff --git a/crates/januskey-cli/tests/p2p_test.rs b/crates/januskey-cli/tests/p2p_test.rs index fc7300c..7a5265c 100644 --- a/crates/januskey-cli/tests/p2p_test.rs +++ b/crates/januskey-cli/tests/p2p_test.rs @@ -101,7 +101,15 @@ fn key_operation_creates_attestation_entry() { std::fs::write(attest_path.join("0001.json"), &entry).unwrap(); // Verify attestation references the key - let read_back = ({ use std::io::Read; std::fs::File::open(attest_path.join("0001.json").and_then(|mut f| { let mut buf = String::new(); f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; Ok(buf) }) })).unwrap(); + let read_back = ({ + use std::io::Read; + std::fs::File::open(attest_path.join("0001.json")).and_then(|mut f| { + let mut buf = String::new(); + f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; + Ok(buf) + }) + }) + .unwrap(); assert!( read_back.contains(key_id), "Attestation must reference key ID" @@ -132,7 +140,17 @@ fn attestation_chain_integrity() { // Verify chain: each entry's content hashes to the next's prev_hash let entries: Vec = (0..3) - .map(|i| ({ use std::io::Read; std::fs::File::open(attest_path.join(format!("{:04}.json", i).and_then(|mut f| { let mut buf = String::new(); f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; Ok(buf) }) }))).unwrap()) + .map(|i| { + ({ + use std::io::Read; + std::fs::File::open(attest_path.join(format!("{:04}.json", i))).and_then(|mut f| { + let mut buf = String::new(); + f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; + Ok(buf) + }) + }) + .unwrap() + }) .collect(); for i in 1..entries.len() { diff --git a/crates/reversible-core/src/content_store.rs b/crates/reversible-core/src/content_store.rs index b2ce8d1..0233c3c 100644 --- a/crates/reversible-core/src/content_store.rs +++ b/crates/reversible-core/src/content_store.rs @@ -5,7 +5,7 @@ // Content-Addressed Storage with SHA256 hashing // Provides deduplication and integrity verification -use crate::error::{ReversibleError, Result}; +use crate::error::{Result, ReversibleError}; use flate2::read::GzDecoder; use flate2::write::GzEncoder; use flate2::Compression; diff --git a/crates/reversible-core/src/lib.rs b/crates/reversible-core/src/lib.rs index d3b9c74..fa0ab25 100644 --- a/crates/reversible-core/src/lib.rs +++ b/crates/reversible-core/src/lib.rs @@ -24,11 +24,9 @@ pub mod metadata; pub mod transaction; pub use content_store::{ContentHash, ContentStore}; -pub use error::{ReversibleError, Result}; +pub use error::{Result, ReversibleError}; pub use manifest::ManifestEmitter; -pub use metadata::{ - FileMetadata, MetadataStore, OperationLog, OperationMetadata, OperationType, -}; +pub use metadata::{FileMetadata, MetadataStore, OperationLog, OperationMetadata, OperationType}; pub use transaction::{ OperationPreview, Transaction, TransactionLog, TransactionManager, TransactionPreview, TransactionState, diff --git a/crates/reversible-core/src/manifest.rs b/crates/reversible-core/src/manifest.rs index 9ddcaed..28799e8 100644 --- a/crates/reversible-core/src/manifest.rs +++ b/crates/reversible-core/src/manifest.rs @@ -123,14 +123,10 @@ mod tests { #[test] fn test_generate_manifest() { let tmp = TempDir::new().unwrap(); - let mut store = - MetadataStore::new(tmp.path().join("metadata.json")).unwrap(); + let mut store = MetadataStore::new(tmp.path().join("metadata.json")).unwrap(); - let op = OperationMetadata::new( - OperationType::Delete, - PathBuf::from("/test/file.txt"), - ) - .with_content_hash(ContentHash::from_bytes(b"file content")); + let op = OperationMetadata::new(OperationType::Delete, PathBuf::from("/test/file.txt")) + .with_content_hash(ContentHash::from_bytes(b"file content")); store.append(op).unwrap(); @@ -147,8 +143,7 @@ mod tests { #[test] fn test_empty_manifest() { let tmp = TempDir::new().unwrap(); - let store = - MetadataStore::new(tmp.path().join("metadata.json")).unwrap(); + let store = MetadataStore::new(tmp.path().join("metadata.json")).unwrap(); let manifest = ManifestEmitter::generate("test", &store); assert!(manifest.contains("@manifest")); @@ -158,13 +153,9 @@ mod tests { #[test] fn test_merkle_root_deterministic() { let tmp = TempDir::new().unwrap(); - let mut store = - MetadataStore::new(tmp.path().join("metadata.json")).unwrap(); + let mut store = MetadataStore::new(tmp.path().join("metadata.json")).unwrap(); - let op = OperationMetadata::new( - OperationType::Create, - PathBuf::from("/a.txt"), - ); + let op = OperationMetadata::new(OperationType::Create, PathBuf::from("/a.txt")); store.append(op).unwrap(); let m1 = ManifestEmitter::generate("test", &store); diff --git a/crates/reversible-core/src/metadata.rs b/crates/reversible-core/src/metadata.rs index 7806411..dd7849e 100644 --- a/crates/reversible-core/src/metadata.rs +++ b/crates/reversible-core/src/metadata.rs @@ -6,7 +6,7 @@ // Implements the formal model from the JanusKey white paper use crate::content_store::ContentHash; -use crate::error::{ReversibleError, Result}; +use crate::error::{Result, ReversibleError}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::fs; @@ -256,7 +256,14 @@ impl MetadataStore { /// Create or open a metadata store pub fn new(path: PathBuf) -> Result { let log = if path.exists() { - let content = ({ use std::io::Read; std::fs::File::open(&path).and_then(|mut f| { let mut buf = String::new(); f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; Ok(buf) }) })?; + let content = ({ + use std::io::Read; + std::fs::File::open(&path).and_then(|mut f| { + let mut buf = String::new(); + f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; + Ok(buf) + }) + })?; serde_json::from_str(&content) .map_err(|e| ReversibleError::MetadataCorrupted(e.to_string()))? } else { @@ -378,8 +385,7 @@ mod tests { #[test] fn test_operation_metadata_creation() { - let meta = - OperationMetadata::new(OperationType::Delete, PathBuf::from("/test/file.txt")); + let meta = OperationMetadata::new(OperationType::Delete, PathBuf::from("/test/file.txt")); assert!(!meta.id.is_empty()); assert_eq!(meta.op_type, OperationType::Delete); assert!(!meta.undone); @@ -400,8 +406,7 @@ mod tests { let mut store = MetadataStore::new(path.clone()).unwrap(); - let meta = - OperationMetadata::new(OperationType::Delete, PathBuf::from("/test.txt")); + let meta = OperationMetadata::new(OperationType::Delete, PathBuf::from("/test.txt")); let id = meta.id.clone(); store.append(meta).unwrap(); diff --git a/crates/reversible-core/src/transaction.rs b/crates/reversible-core/src/transaction.rs index a1d1891..2f0ff6c 100644 --- a/crates/reversible-core/src/transaction.rs +++ b/crates/reversible-core/src/transaction.rs @@ -9,7 +9,7 @@ // lives in januskey-cli, not here. This module provides only the data // types and persistence — no filesystem side effects. -use crate::error::{ReversibleError, Result}; +use crate::error::{Result, ReversibleError}; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::fs; @@ -121,7 +121,14 @@ impl TransactionManager { /// Create or open a transaction manager pub fn new(path: PathBuf) -> Result { let log = if path.exists() { - let content = ({ use std::io::Read; std::fs::File::open(&path).and_then(|mut f| { let mut buf = String::new(); f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; Ok(buf) }) })?; + let content = ({ + use std::io::Read; + std::fs::File::open(&path).and_then(|mut f| { + let mut buf = String::new(); + f.take(10 * 1024 * 1024).read_to_string(&mut buf)?; + Ok(buf) + }) + })?; serde_json::from_str(&content) .map_err(|e| ReversibleError::MetadataCorrupted(e.to_string()))? } else { @@ -154,7 +161,11 @@ impl TransactionManager { self.save()?; // SAFETY: We just pushed a transaction above, so last() is guaranteed Some - Ok(self.log.transactions.last().expect("transaction was just pushed")) + Ok(self + .log + .transactions + .last() + .expect("transaction was just pushed")) } /// Get current active transaction @@ -168,10 +179,7 @@ impl TransactionManager { /// Get mutable active transaction pub fn active_mut(&mut self) -> Option<&mut Transaction> { let active_id = self.log.active_transaction_id.clone()?; - self.log - .transactions - .iter_mut() - .find(|t| t.id == active_id) + self.log.transactions.iter_mut().find(|t| t.id == active_id) } /// Add operation to active transaction @@ -288,15 +296,22 @@ mod tests { fn test_transaction_lifecycle() { let tmp = TempDir::new().expect("failed to create temp dir"); let path = tmp.path().join("transactions.json"); - let mut manager = TransactionManager::new(path).expect("failed to create transaction manager"); + let mut manager = + TransactionManager::new(path).expect("failed to create transaction manager"); // Begin - manager.begin(Some("test".to_string())).expect("failed to begin transaction"); + manager + .begin(Some("test".to_string())) + .expect("failed to begin transaction"); assert!(manager.has_active()); // Add operations - manager.add_operation("op-1".to_string()).expect("failed to add operation op-1"); - manager.add_operation("op-2".to_string()).expect("failed to add operation op-2"); + manager + .add_operation("op-1".to_string()) + .expect("failed to add operation op-1"); + manager + .add_operation("op-2".to_string()) + .expect("failed to add operation op-2"); // Commit let tx = manager.commit().expect("failed to commit transaction"); @@ -309,9 +324,12 @@ mod tests { fn test_cannot_begin_while_active() { let tmp = TempDir::new().expect("failed to create temp dir"); let path = tmp.path().join("transactions.json"); - let mut manager = TransactionManager::new(path).expect("failed to create transaction manager"); + let mut manager = + TransactionManager::new(path).expect("failed to create transaction manager"); - manager.begin(None).expect("failed to begin first transaction"); + manager + .begin(None) + .expect("failed to begin first transaction"); assert!(manager.begin(None).is_err()); } } diff --git a/crates/reversible-core/tests/unwrap_safety_test.rs b/crates/reversible-core/tests/unwrap_safety_test.rs index b97d35b..1f02d66 100644 --- a/crates/reversible-core/tests/unwrap_safety_test.rs +++ b/crates/reversible-core/tests/unwrap_safety_test.rs @@ -19,30 +19,37 @@ fn retrieve_nonexistent_hash_returns_error() { // Must return Err, not panic let result = store.retrieve(&fake_hash); - assert!(result.is_err(), "Retrieving non-existent hash must return Err"); + assert!( + result.is_err(), + "Retrieving non-existent hash must return Err" + ); } /// Regression: committing with no active transaction must return Err, not panic. #[test] fn commit_without_active_transaction_returns_error() { let tmp = TempDir::new().unwrap(); - let mut manager = - TransactionManager::new(tmp.path().join("transactions.json")).unwrap(); + let mut manager = TransactionManager::new(tmp.path().join("transactions.json")).unwrap(); // No begin() called — commit must fail gracefully let result = manager.commit(); - assert!(result.is_err(), "Commit without active transaction must return Err"); + assert!( + result.is_err(), + "Commit without active transaction must return Err" + ); } /// Regression: rollback with no active transaction must return Err, not panic. #[test] fn rollback_without_active_transaction_returns_error() { let tmp = TempDir::new().unwrap(); - let mut manager = - TransactionManager::new(tmp.path().join("transactions.json")).unwrap(); + let mut manager = TransactionManager::new(tmp.path().join("transactions.json")).unwrap(); let result = manager.mark_rolled_back(); - assert!(result.is_err(), "Rollback without active transaction must return Err"); + assert!( + result.is_err(), + "Rollback without active transaction must return Err" + ); } /// Regression: content store with empty bytes must not panic. @@ -60,5 +67,8 @@ fn store_empty_content() { #[test] fn content_hash_verify_mismatch() { let hash = ContentHash::from_bytes(b"original"); - assert!(!hash.verify(b"tampered"), "Mismatched content must return false"); + assert!( + !hash.verify(b"tampered"), + "Mismatched content must return false" + ); }