From b83643ee0a961cf2d39f5eccd545b019691f9f54 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 20 Jun 2026 17:27:05 -0400 Subject: [PATCH] refactor(cli): move per-type edit helpers into shared item_build module --- crates/relicario-cli/src/commands/edit.rs | 141 ++---------------- .../relicario-cli/src/commands/item_build.rs | 119 +++++++++++++++ 2 files changed, 128 insertions(+), 132 deletions(-) diff --git a/crates/relicario-cli/src/commands/edit.rs b/crates/relicario-cli/src/commands/edit.rs index 798aa52..14897cd 100644 --- a/crates/relicario-cli/src/commands/edit.rs +++ b/crates/relicario-cli/src/commands/edit.rs @@ -2,10 +2,9 @@ use std::path::PathBuf; -use anyhow::{Context, Result}; +use anyhow::Result; -use crate::parse::base32_decode_lenient; -use crate::prompt::{prompt_keep, prompt_keep_opt, prompt_secret, prompt_yesno}; +use crate::prompt::{prompt_keep, prompt_keep_opt}; pub fn cmd_edit(query: String, totp_qr: Option) -> Result<()> { use relicario_core::time::now_unix; @@ -29,13 +28,13 @@ pub fn cmd_edit(query: String, totp_qr: Option) -> Result<()> { 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)?, + ItemCore::Login(l) => crate::commands::item_build::edit_login(l, history, totp_qr)?, + ItemCore::SecureNote(n) => crate::commands::item_build::edit_secure_note(n, history)?, + ItemCore::Identity(i) => crate::commands::item_build::edit_identity(i)?, + ItemCore::Card(c) => crate::commands::item_build::edit_card(c, history)?, + ItemCore::Key(k) => crate::commands::item_build::edit_key(k, history)?, + ItemCore::Document(_) => crate::commands::item_build::edit_document_message(), + ItemCore::Totp(t) => crate::commands::item_build::edit_totp(t, history)?, } item.modified = now_unix(); @@ -47,125 +46,3 @@ pub fn cmd_edit(query: String, totp_qr: Option) -> Result<()> { 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(), - }); -} diff --git a/crates/relicario-cli/src/commands/item_build.rs b/crates/relicario-cli/src/commands/item_build.rs index 4231a28..e80a168 100644 --- a/crates/relicario-cli/src/commands/item_build.rs +++ b/crates/relicario-cli/src/commands/item_build.rs @@ -10,8 +10,12 @@ use zeroize::Zeroizing; use relicario_core::item::FieldHistoryEntry; use relicario_core::item_types::{CardKind, TotpAlgorithm}; +use relicario_core::time::now_unix; use relicario_core::{EncryptedAttachment, FieldId, Item, ItemCore}; +use crate::parse::base32_decode_lenient; +use crate::prompt::{prompt_keep_opt, prompt_secret, prompt_yesno}; + pub(crate) type FieldHistory = HashMap>; /// Resolve a single-line secret: from stdin when `from_stdin`, else an @@ -57,6 +61,121 @@ pub(crate) fn parse_totp_algorithm(s: &str) -> Result { }) } +// --- Per-type interactive edit helpers (moved from commands/edit.rs). Each +// mutates its core slice in place; history-tracked variants take the +// item's field_history map so they can record the prior value. + +pub(crate) 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(()) +} + +pub(crate) 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(()) +} + +pub(crate) 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(()) +} + +pub(crate) 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(()) +} + +pub(crate) 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(()) +} + +pub(crate) fn edit_document_message() { + eprintln!("Document items: use `relicario attach` / `relicario extract` instead."); +} + +pub(crate) 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(()) +} + +pub(crate) fn push_history( + history: &mut FieldHistory, + synthetic_key: &str, + old_value: zeroize::Zeroizing, +) { + // 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(), + }); +} + #[cfg(test)] mod tests { use super::*;