feat(wasm): add parse_month_year, base32_decode_lenient, guess_mime exports
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 <noreply@anthropic.com>
This commit is contained in:
@@ -10,9 +10,9 @@ use crate::error::{RelicarioError, Result};
|
|||||||
|
|
||||||
/// Steam Mobile Authenticator's 5-character output alphabet.
|
/// Steam Mobile Authenticator's 5-character output alphabet.
|
||||||
/// Deliberately excludes ambiguous glyphs (0/O, 1/I/L, S/5, A/Z).
|
/// 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`
|
/// Not RFC 4648 — Steam Guard's de-ambiguated alphabet; see [`crate::base32`]
|
||||||
// for the standard implementation.
|
/// for the standard implementation.
|
||||||
const STEAM_ALPHABET: &[u8] = b"23456789BCDFGHJKMNPQRTVWXY";
|
const STEAM_ALPHABET: &[u8] = b"23456789BCDFGHJKMNPQRTVWXY";
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||||
|
|||||||
@@ -14,6 +14,8 @@
|
|||||||
//! - [`crypto`] — Argon2id KDF (length-prefixed inputs, Zeroizing output) and
|
//! - [`crypto`] — Argon2id KDF (length-prefixed inputs, Zeroizing output) and
|
||||||
//! XChaCha20-Poly1305 AEAD with VERSION_BYTE 0x02.
|
//! XChaCha20-Poly1305 AEAD with VERSION_BYTE 0x02.
|
||||||
//! - [`ids`] — `ItemId`, `FieldId`, and content-addressed `AttachmentId`.
|
//! - [`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.
|
//! - [`time`] — unix-seconds + `MonthYear` for card expiries.
|
||||||
//! - [`item_types`] — Per-type cores (`LoginCore`, `SecureNoteCore`, etc.) and the
|
//! - [`item_types`] — Per-type cores (`LoginCore`, `SecureNoteCore`, etc.) and the
|
||||||
//! `ItemCore`/`ItemType` enums.
|
//! `ItemCore`/`ItemType` enums.
|
||||||
|
|||||||
@@ -330,6 +330,32 @@ pub fn embed_image_secret(carrier: &[u8], secret: &[u8]) -> Result<Vec<u8>, JsEr
|
|||||||
imgsecret::embed(carrier, s).map_err(|e| JsError::new(&e.to_string()))
|
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<JsValue, JsError> {
|
||||||
|
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<Vec<u8>, 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};
|
use relicario_core::item_types::{TotpConfig, compute_totp_code};
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
@@ -624,4 +650,24 @@ mod session_tests {
|
|||||||
// Should fail with a header validation error.
|
// Should fail with a header validation error.
|
||||||
assert!(err.is_err());
|
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`.
|
||||||
}
|
}
|
||||||
|
|||||||
5
extension/src/wasm.d.ts
vendored
5
extension/src/wasm.d.ts
vendored
@@ -59,6 +59,11 @@ declare module 'relicario-wasm' {
|
|||||||
export function extract_image_secret(image_bytes: Uint8Array): Uint8Array;
|
export function extract_image_secret(image_bytes: Uint8Array): Uint8Array;
|
||||||
export function embed_image_secret(carrier: Uint8Array, secret: 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 totp_compute(config_json: string, now_unix_seconds: bigint): TotpCode;
|
||||||
|
|
||||||
export function register_device(name: string): {
|
export function register_device(name: string): {
|
||||||
|
|||||||
Reference in New Issue
Block a user