From fc9264e9aecb4f7b1dbfac3d6d0a69caf5d16770 Mon Sep 17 00:00:00 2001 From: adlee-was-taken Date: Sat, 9 May 2026 11:33:40 -0400 Subject: [PATCH] feat(wasm): add parse_month_year, base32_decode_lenient, guess_mime exports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plan B Phase 8 — three #[wasm_bindgen] exports for the parsers migrated in Phase 7, mirrored in extension/src/wasm.d.ts under "Pure parsers (no session needed)". snake_case JS naming consistent with every existing export; SessionHandle not required. - parse_month_year(s) → { month, year } via js_value_for - base32_decode_lenient(s) → Uint8Array - guess_mime(filename) → string Tests in session_tests mod cover the OK paths; error-path / JsValue serialization can't be tested natively (JsError construction panics off-wasm) and is covered in core (time::tests + base32::tests). Plan C will wire SW message handlers consuming these exports in a future round; this commit delivers only the seam. Includes simplify-feedback fixes: - relicario-core lib.rs module-list mentions base32 and mime - item_types/totp.rs neighbour comment unified to ///-style block cargo test --workspace: green cargo clippy --workspace: silent cargo build -p relicario-wasm --target wasm32-unknown-unknown: clean cd extension && npm run test: 17 pre-existing failures only (baseline) Co-Authored-By: Claude Opus 4.7 --- crates/relicario-core/src/item_types/totp.rs | 6 +-- crates/relicario-core/src/lib.rs | 2 + crates/relicario-wasm/src/lib.rs | 46 ++++++++++++++++++++ extension/src/wasm.d.ts | 5 +++ 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/crates/relicario-core/src/item_types/totp.rs b/crates/relicario-core/src/item_types/totp.rs index 00e7b80..74d4d4f 100644 --- a/crates/relicario-core/src/item_types/totp.rs +++ b/crates/relicario-core/src/item_types/totp.rs @@ -10,9 +10,9 @@ use crate::error::{RelicarioError, Result}; /// Steam Mobile Authenticator's 5-character output alphabet. /// Deliberately excludes ambiguous glyphs (0/O, 1/I/L, S/5, A/Z). -// -// Not RFC 4648 — Steam Guard's de-ambiguated alphabet; see `crate::base32` -// for the standard implementation. +/// +/// Not RFC 4648 — Steam Guard's de-ambiguated alphabet; see [`crate::base32`] +/// for the standard implementation. const STEAM_ALPHABET: &[u8] = b"23456789BCDFGHJKMNPQRTVWXY"; #[derive(Debug, Clone, Serialize, Deserialize, Default)] diff --git a/crates/relicario-core/src/lib.rs b/crates/relicario-core/src/lib.rs index dbdc33a..237a2d1 100644 --- a/crates/relicario-core/src/lib.rs +++ b/crates/relicario-core/src/lib.rs @@ -14,6 +14,8 @@ //! - [`crypto`] — Argon2id KDF (length-prefixed inputs, Zeroizing output) and //! XChaCha20-Poly1305 AEAD with VERSION_BYTE 0x02. //! - [`ids`] — `ItemId`, `FieldId`, and content-addressed `AttachmentId`. +//! - [`base32`] — RFC 4648 base32 codec used for TOTP secret encode/decode. +//! - [`mime`] — Filename-extension → MIME-type guess for attachment storage. //! - [`time`] — unix-seconds + `MonthYear` for card expiries. //! - [`item_types`] — Per-type cores (`LoginCore`, `SecureNoteCore`, etc.) and the //! `ItemCore`/`ItemType` enums. diff --git a/crates/relicario-wasm/src/lib.rs b/crates/relicario-wasm/src/lib.rs index 66825e7..f6c6b80 100644 --- a/crates/relicario-wasm/src/lib.rs +++ b/crates/relicario-wasm/src/lib.rs @@ -330,6 +330,32 @@ pub fn embed_image_secret(carrier: &[u8], secret: &[u8]) -> Result, JsEr imgsecret::embed(carrier, s).map_err(|e| JsError::new(&e.to_string())) } +// ── Pure parsers (no session needed) ──────────────────────────────────────── + +use relicario_core::{base32 as core_base32, mime as core_mime, MonthYear}; + +/// Parse a card-expiry string (`MM/YYYY` / `MM-YYYY` / `MM/YY`). +/// Returns a plain `{ month, year }` object on success. +#[wasm_bindgen] +pub fn parse_month_year(s: &str) -> Result { + let my = MonthYear::parse(s).map_err(|e| JsError::new(&e.to_string()))?; + js_value_for(&my) +} + +/// Decode an RFC 4648 base32 string (case-insensitive, optional padding, +/// whitespace-stripped). Returned as `Uint8Array` on the JS side. +#[wasm_bindgen] +pub fn base32_decode_lenient(s: &str) -> Result, JsError> { + core_base32::decode_rfc4648_lenient(s).map_err(|e| JsError::new(&e.to_string())) +} + +/// Guess a MIME type from a filename's extension. Returns +/// `application/octet-stream` for unknown or missing extensions. +#[wasm_bindgen] +pub fn guess_mime(filename: &str) -> String { + core_mime::guess_for_extension(filename).to_string() +} + use relicario_core::item_types::{TotpConfig, compute_totp_code}; #[wasm_bindgen] @@ -624,4 +650,24 @@ mod session_tests { // Should fail with a header validation error. assert!(err.is_err()); } + + #[test] + fn base32_decode_lenient_round_trips_known_vector() { + let bytes = super::base32_decode_lenient("MZXW6YTBOI").unwrap(); + assert_eq!(bytes, b"foobar"); + } + + #[test] + fn guess_mime_known_and_unknown_extensions() { + assert_eq!(super::guess_mime("doc.pdf"), "application/pdf"); + assert_eq!(super::guess_mime("photo.JPEG"), "image/jpeg"); + assert_eq!(super::guess_mime("file.xyz"), "application/octet-stream"); + } + + // Error paths and JsValue serialization can't be exercised natively — + // JsError::new and serde_wasm_bindgen::Serializer call wasm-bindgen + // imports that panic off-wasm (same constraint as + // `parse_lastpass_csv_json_propagates_header_errors` above). Those + // paths are covered in core: `time::tests::parse_rejects_malformed` + // and `base32::tests::decode_rfc4648_lenient_rejects_non_alphabet_chars`. } diff --git a/extension/src/wasm.d.ts b/extension/src/wasm.d.ts index 3643996..2553999 100644 --- a/extension/src/wasm.d.ts +++ b/extension/src/wasm.d.ts @@ -59,6 +59,11 @@ declare module 'relicario-wasm' { export function extract_image_secret(image_bytes: Uint8Array): Uint8Array; export function embed_image_secret(carrier: Uint8Array, secret: Uint8Array): Uint8Array; + // Pure parsers (no session needed) + export function parse_month_year(s: string): { month: number; year: number }; + export function base32_decode_lenient(s: string): Uint8Array; + export function guess_mime(filename: string): string; + export function totp_compute(config_json: string, now_unix_seconds: bigint): TotpCode; export function register_device(name: string): {