Renames crate directories and sweeps identifiers so Plan 1B can reference
the post-rename names throughout.
- git mv crates/idfoto-{core,cli,wasm} → crates/relicario-{core,cli,wasm}
- sed sweep: idfoto_core/idfoto-core/IdfotoError/IDFOTO_IMAGE/.idfoto/ etc.
- All 128 relicario-core tests pass post-sweep
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
185 lines
5.4 KiB
Rust
185 lines
5.4 KiB
Rust
//! Vault-level settings: trash retention, history retention, generator
|
|
//! defaults, attachment caps, autofill TOFU acks.
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
use std::collections::HashMap;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
pub struct VaultSettings {
|
|
pub trash_retention: TrashRetention,
|
|
pub field_history_retention: HistoryRetention,
|
|
pub generator_defaults: GeneratorRequest,
|
|
pub attachment_caps: AttachmentCaps,
|
|
/// hostname → unix-seconds first-acked
|
|
#[serde(default)]
|
|
pub autofill_origin_acks: HashMap<String, i64>,
|
|
}
|
|
|
|
impl Default for VaultSettings {
|
|
fn default() -> Self {
|
|
Self {
|
|
trash_retention: TrashRetention::Days(30),
|
|
field_history_retention: HistoryRetention::Forever,
|
|
generator_defaults: GeneratorRequest::default(),
|
|
attachment_caps: AttachmentCaps::default(),
|
|
autofill_origin_acks: HashMap::new(),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
|
|
pub enum TrashRetention {
|
|
Days(u32),
|
|
Forever,
|
|
}
|
|
|
|
impl TrashRetention {
|
|
pub fn should_purge(&self, trashed_at: i64, now: i64) -> bool {
|
|
match self {
|
|
TrashRetention::Forever => false,
|
|
TrashRetention::Days(d) => now - trashed_at > (*d as i64) * 86_400,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
|
|
pub enum HistoryRetention {
|
|
LastN(u32),
|
|
Days(u32),
|
|
Forever,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
|
pub enum GeneratorRequest {
|
|
Bip39 {
|
|
word_count: u32,
|
|
separator: String,
|
|
capitalization: Capitalization,
|
|
},
|
|
Random {
|
|
length: u32,
|
|
classes: CharClasses,
|
|
symbol_charset: SymbolCharset,
|
|
},
|
|
}
|
|
|
|
impl Default for GeneratorRequest {
|
|
fn default() -> Self {
|
|
GeneratorRequest::Random {
|
|
length: 20,
|
|
classes: CharClasses { lower: true, upper: true, digits: true, symbols: true },
|
|
symbol_charset: SymbolCharset::SafeOnly,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
#[serde(rename_all = "snake_case")]
|
|
pub enum Capitalization {
|
|
Lower,
|
|
Upper,
|
|
FirstOfEach,
|
|
Title,
|
|
Mixed,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct CharClasses {
|
|
pub lower: bool,
|
|
pub upper: bool,
|
|
pub digits: bool,
|
|
pub symbols: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
#[serde(tag = "kind", content = "value", rename_all = "snake_case")]
|
|
pub enum SymbolCharset {
|
|
SafeOnly,
|
|
Extended,
|
|
Custom(String),
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
|
pub struct AttachmentCaps {
|
|
pub per_attachment_max_bytes: u64,
|
|
pub per_item_max_count: u32,
|
|
pub per_vault_soft_cap_bytes: u64,
|
|
pub per_vault_hard_cap_bytes: u64,
|
|
}
|
|
|
|
impl Default for AttachmentCaps {
|
|
fn default() -> Self {
|
|
Self {
|
|
per_attachment_max_bytes: 10 * 1024 * 1024,
|
|
per_item_max_count: 20,
|
|
per_vault_soft_cap_bytes: 100 * 1024 * 1024,
|
|
per_vault_hard_cap_bytes: 500 * 1024 * 1024,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn defaults_match_spec() {
|
|
let s = VaultSettings::default();
|
|
assert!(matches!(s.trash_retention, TrashRetention::Days(30)));
|
|
assert!(matches!(s.field_history_retention, HistoryRetention::Forever));
|
|
assert_eq!(s.attachment_caps.per_attachment_max_bytes, 10 * 1024 * 1024);
|
|
assert_eq!(s.attachment_caps.per_item_max_count, 20);
|
|
}
|
|
|
|
#[test]
|
|
fn trash_retention_purges_after_days() {
|
|
let r = TrashRetention::Days(30);
|
|
let now = 1_000_000_000;
|
|
let recently_trashed = now - 29 * 86_400;
|
|
let long_trashed = now - 31 * 86_400;
|
|
assert!(!r.should_purge(recently_trashed, now));
|
|
assert!(r.should_purge(long_trashed, now));
|
|
}
|
|
|
|
#[test]
|
|
fn trash_retention_forever_never_purges() {
|
|
let r = TrashRetention::Forever;
|
|
assert!(!r.should_purge(0, 1_000_000_000));
|
|
}
|
|
|
|
#[test]
|
|
fn settings_round_trip() {
|
|
let s = VaultSettings::default();
|
|
let json = serde_json::to_string(&s).unwrap();
|
|
let parsed: VaultSettings = serde_json::from_str(&json).unwrap();
|
|
assert_eq!(parsed.attachment_caps.per_attachment_max_bytes,
|
|
s.attachment_caps.per_attachment_max_bytes);
|
|
}
|
|
|
|
#[test]
|
|
fn random_generator_default_is_20_safe() {
|
|
match VaultSettings::default().generator_defaults {
|
|
GeneratorRequest::Random { length, classes, symbol_charset } => {
|
|
assert_eq!(length, 20);
|
|
assert!(classes.lower && classes.upper && classes.digits && classes.symbols);
|
|
assert!(matches!(symbol_charset, SymbolCharset::SafeOnly));
|
|
}
|
|
_ => panic!("expected Random default"),
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn symbol_charset_custom_round_trips() {
|
|
let c = SymbolCharset::Custom("!@#".into());
|
|
let json = serde_json::to_string(&c).unwrap();
|
|
let parsed: SymbolCharset = serde_json::from_str(&json).unwrap();
|
|
match parsed {
|
|
SymbolCharset::Custom(s) => assert_eq!(s, "!@#"),
|
|
other => panic!("expected Custom, got {:?}", other),
|
|
}
|
|
}
|
|
}
|