diff --git a/crates/relicario-cli/src/main.rs b/crates/relicario-cli/src/main.rs index 606d0c5..2bfca67 100644 --- a/crates/relicario-cli/src/main.rs +++ b/crates/relicario-cli/src/main.rs @@ -16,6 +16,7 @@ use anyhow::{bail, Context, Result}; use clap::{CommandFactory, Parser, Subcommand}; use clap_complete::{generate, Shell}; +use crate::parse::{base32_decode_lenient, guess_mime, parse_month_year}; use crate::prompt::{prompt, prompt_keep, prompt_keep_opt, prompt_optional, prompt_secret, prompt_yesno}; #[derive(Parser)] @@ -915,46 +916,6 @@ fn build_totp_item( Ok(item) } -fn parse_month_year(s: &str) -> Result { - // Accepts MM/YYYY or MM-YYYY or MM/YY. - let (m_str, y_str) = s.split_once(['/', '-']) - .ok_or_else(|| anyhow::anyhow!("expected MM/YYYY"))?; - let month: u8 = m_str.parse().context("invalid month")?; - let year: u16 = if y_str.len() == 2 { - 2000 + y_str.parse::().context("invalid 2-digit year")? - } else { - y_str.parse().context("invalid year")? - }; - Ok(relicario_core::MonthYear { month, year }) -} - -fn guess_mime(filename: &str) -> String { - let lower = filename.to_ascii_lowercase(); - match lower.rsplit_once('.').map(|(_, ext)| ext).unwrap_or("") { - "pdf" => "application/pdf", - "png" => "image/png", - "jpg" | "jpeg" => "image/jpeg", - "txt" => "text/plain", - "json" => "application/json", - _ => "application/octet-stream", - }.to_string() -} - -fn base32_decode_lenient(s: &str) -> Result> { - let cleaned: String = s.chars() - .filter(|c| !c.is_whitespace()) - .collect::() - .to_ascii_uppercase() - .trim_end_matches('=') - .to_string(); - let padded = { - let rem = cleaned.len() % 8; - if rem == 0 { cleaned } else { format!("{}{}", cleaned, "=".repeat(8 - rem)) } - }; - data_encoding::BASE32.decode(padded.as_bytes()) - .map_err(|e| anyhow::anyhow!("invalid base32: {e}")) -} - fn commit_paths(vault: &crate::session::UnlockedVault, message: &str, paths: &[&str]) -> Result<()> { let mut args: Vec<&str> = vec!["add"]; args.extend_from_slice(paths); diff --git a/crates/relicario-cli/src/parse.rs b/crates/relicario-cli/src/parse.rs index fb9dfce..cbf8084 100644 --- a/crates/relicario-cli/src/parse.rs +++ b/crates/relicario-cli/src/parse.rs @@ -3,3 +3,45 @@ //! Phase 7 of the CLI restructure migrates these to `relicario-core` and //! turns this file into a thin re-export shim. They live here for now so //! the Phase 1 relocation stays mechanical. + +use anyhow::{Context, Result}; + +pub(crate) fn parse_month_year(s: &str) -> Result { + // Accepts MM/YYYY or MM-YYYY or MM/YY. + let (m_str, y_str) = s.split_once(['/', '-']) + .ok_or_else(|| anyhow::anyhow!("expected MM/YYYY"))?; + let month: u8 = m_str.parse().context("invalid month")?; + let year: u16 = if y_str.len() == 2 { + 2000 + y_str.parse::().context("invalid 2-digit year")? + } else { + y_str.parse().context("invalid year")? + }; + Ok(relicario_core::MonthYear { month, year }) +} + +pub(crate) fn guess_mime(filename: &str) -> String { + let lower = filename.to_ascii_lowercase(); + match lower.rsplit_once('.').map(|(_, ext)| ext).unwrap_or("") { + "pdf" => "application/pdf", + "png" => "image/png", + "jpg" | "jpeg" => "image/jpeg", + "txt" => "text/plain", + "json" => "application/json", + _ => "application/octet-stream", + }.to_string() +} + +pub(crate) fn base32_decode_lenient(s: &str) -> Result> { + let cleaned: String = s.chars() + .filter(|c| !c.is_whitespace()) + .collect::() + .to_ascii_uppercase() + .trim_end_matches('=') + .to_string(); + let padded = { + let rem = cleaned.len() % 8; + if rem == 0 { cleaned } else { format!("{}{}", cleaned, "=".repeat(8 - rem)) } + }; + data_encoding::BASE32.decode(padded.as_bytes()) + .map_err(|e| anyhow::anyhow!("invalid base32: {e}")) +}