//! `relicario edit ` — interactive per-type field editing with history capture. use std::path::PathBuf; use anyhow::{Context, Result}; use crate::parse::base32_decode_lenient; use crate::prompt::{prompt_keep, prompt_keep_opt, prompt_secret, prompt_yesno}; pub fn cmd_edit(query: String, totp_qr: Option) -> Result<()> { use relicario_core::time::now_unix; use relicario_core::ItemCore; let vault = crate::session::UnlockedVault::unlock_interactive()?; let mut manifest = vault.load_manifest()?; let entry = super::resolve_query(&manifest, &query)?; let id = entry.id.clone(); let _ = entry; let mut item = vault.load_item(&id)?; eprintln!("Editing: {} ({}) — leave a prompt blank to keep the current value.", item.title, item.id.as_str()); if let Some(v) = prompt_keep("Title", &item.title)? { item.title = v; } if let Some(v) = prompt_keep_opt("Group", item.group.as_deref())? { item.group = Some(v); } if let Some(v) = prompt_keep_opt("Tags (comma-separated)", Some(&item.tags.join(",")))? { item.tags = v.split(',').map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect(); } let history = &mut item.field_history; match &mut item.core { ItemCore::Login(l) => edit_login(l, history, totp_qr)?, ItemCore::SecureNote(n) => edit_secure_note(n, history)?, ItemCore::Identity(i) => edit_identity(i)?, ItemCore::Card(c) => edit_card(c, history)?, ItemCore::Key(k) => edit_key(k, history)?, ItemCore::Document(_) => edit_document_message(), ItemCore::Totp(t) => edit_totp(t, history)?, } item.modified = now_unix(); vault.save_item(&item)?; manifest.upsert(&item); vault.after_manifest_change(&manifest)?; super::commit_paths(&vault, &format!("edit: {} ({})", crate::helpers::sanitize_for_commit(&item.title), item.id.as_str()), &[&format!("items/{}.enc", item.id.as_str()), "manifest.enc"])?; eprintln!("Updated {}", item.id.as_str()); Ok(()) } // --- Per-type edit handlers. Each mutates its core slice in place; the ones // that touch history-tracked fields take the item's field_history map so // they can record the prior value alongside the change. type FieldHistory = std::collections::HashMap< relicario_core::FieldId, Vec, >; fn edit_login( l: &mut relicario_core::item_types::LoginCore, history: &mut FieldHistory, totp_qr: Option, ) -> Result<()> { use relicario_core::item_types::{TotpAlgorithm, TotpConfig, TotpKind}; use zeroize::Zeroizing; if let Some(v) = prompt_keep_opt("Username", l.username.as_deref())? { l.username = Some(v); } if let Some(v) = prompt_keep_opt("URL", l.url.as_ref().map(|u| u.as_str()))? { l.url = Some(url::Url::parse(&v).with_context(|| format!("invalid URL: {v}"))?); } if prompt_yesno("Change password?")? { let old = l.password.clone(); l.password = Some(Zeroizing::new(prompt_secret("New password: ")?)); if let Some(old_pw) = old { push_history(history, "login_password", Zeroizing::new(old_pw.as_str().to_string())); } } if let Some(path) = totp_qr { let secret_b32 = crate::helpers::decode_totp_qr(&path)?; let secret_bytes = base32_decode_lenient(&secret_b32)?; l.totp = Some(TotpConfig { secret: Zeroizing::new(secret_bytes), algorithm: TotpAlgorithm::Sha1, digits: 6, period_seconds: 30, kind: TotpKind::Totp, }); eprintln!("TOTP secret set from QR image."); } Ok(()) } fn edit_secure_note(n: &mut relicario_core::item_types::SecureNoteCore, history: &mut FieldHistory) -> Result<()> { use zeroize::Zeroizing; if prompt_yesno("Edit body?")? { let old = n.body.clone(); eprintln!("Enter new body; end with Ctrl-D:"); let mut s = String::new(); std::io::Read::read_to_string(&mut std::io::stdin(), &mut s)?; n.body = Zeroizing::new(s); push_history(history, "secure_note_body", Zeroizing::new(old.as_str().to_string())); } Ok(()) } fn edit_identity(i: &mut relicario_core::item_types::IdentityCore) -> Result<()> { if let Some(v) = prompt_keep_opt("Full name", i.full_name.as_deref())? { i.full_name = Some(v); } if let Some(v) = prompt_keep_opt("Email", i.email.as_deref())? { i.email = Some(v); } if let Some(v) = prompt_keep_opt("Phone", i.phone.as_deref())? { i.phone = Some(v); } Ok(()) } fn edit_card(c: &mut relicario_core::item_types::CardCore, history: &mut FieldHistory) -> Result<()> { use zeroize::Zeroizing; if let Some(v) = prompt_keep_opt("Holder", c.holder.as_deref())? { c.holder = Some(v); } if prompt_yesno("Change card number?")? { let old = c.number.clone(); c.number = Some(Zeroizing::new(prompt_secret("New number: ")?)); if let Some(o) = old { push_history(history, "card_number", Zeroizing::new(o.as_str().to_string())); } } Ok(()) } fn edit_key(k: &mut relicario_core::item_types::KeyCore, history: &mut FieldHistory) -> Result<()> { use zeroize::Zeroizing; if prompt_yesno("Replace key material?")? { eprintln!("Paste new key material; end with Ctrl-D:"); let mut s = String::new(); std::io::Read::read_to_string(&mut std::io::stdin(), &mut s)?; let old = k.key_material.clone(); k.key_material = Zeroizing::new(s); push_history(history, "key_material", Zeroizing::new(old.as_str().to_string())); } Ok(()) } fn edit_document_message() { eprintln!("Document items: use `relicario attach` / `relicario extract` instead."); } fn edit_totp(t: &mut relicario_core::item_types::TotpCore, history: &mut FieldHistory) -> Result<()> { use zeroize::Zeroizing; if let Some(v) = prompt_keep_opt("Issuer", t.issuer.as_deref())? { t.issuer = Some(v); } if let Some(v) = prompt_keep_opt("Label", t.label.as_deref())? { t.label = Some(v); } if prompt_yesno("Change TOTP secret?")? { let old_b32 = data_encoding::BASE32.encode(&t.config.secret); let new_b32 = prompt_secret("New TOTP secret (base32): ")?; let new_bytes = base32_decode_lenient(&new_b32)?; t.config.secret = Zeroizing::new(new_bytes); push_history(history, "totp_secret", Zeroizing::new(old_b32)); } Ok(()) } fn push_history( history: &mut std::collections::HashMap>, synthetic_key: &str, old_value: zeroize::Zeroizing, ) { use relicario_core::item::FieldHistoryEntry; use relicario_core::time::now_unix; // Synthetic FieldId for core-level fields — stable per-item (prefixed so // custom-field UUIDs can't collide). let fid = relicario_core::FieldId(format!("core:{synthetic_key}")); history.entry(fid).or_default().push(FieldHistoryEntry { value: old_value, replaced_at: now_unix(), }); }