Files
kestrove-core/crates/kv-model/src/matching.rs
T
samiandClaude Opus 5.5 b5ced32be3 Add kv-wasm: the core compiled to WebAssembly for the web vault
New crate crates/kv-wasm runs the same crypto, item model and sync engine in the browser:
- WebStore: in-memory replica of kv-store's Store, each changed entry written to IndexedDB
  (ciphertext only) before the JS call resolves.
- Device key: non-extractable WebCrypto ECDSA P-256 key in IndexedDB (raw r||s signatures).
- WebCore mirrors kv-ffi's VaultCore; wasm-bindgen surface in camelCase with TypeScript types,
  plus a Web Worker wrapper (connectVault) so Argon2id never blocks the page.
- Tests: Chrome (IndexedDB, WebCrypto, RFC 9421 verify), shared envelope fixtures both ways
  with kv-ffi, native sync with kv-ffi via the test server, Node smoke of the built package.

Small changes to shared crates so they build for wasm32:
- kv-store: SqliteStore moved to a native-only module.
- kv-sync: rustls/pinning/tokio native only (fetch on wasm); DeviceSigner::sign_async (default
  wraps sign) used by ApiClient; unix_now() instead of SystemTime/now_utc.
- kv-model: Public Suffix List behind a default `psl` feature (kv-ffi keeps it; the web build
  drops ~0.9 MB of code and matches same-host only).
- wasm-release profile (opt-level z, Argon2 at 3) and a wasm CI job.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-17 23:58:07 +09:00

83 lines
3.0 KiB
Rust

//! URL matching for autofill (docs/05 "Matching rules").
use url::Url;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum MatchLevel {
None = 0,
/// Same registrable domain (eTLD+1), different host.
Domain = 1,
/// Same host, different port.
Host = 2,
/// Same scheme, host and port.
Exact = 3,
}
/// Compares a saved item URL with the page URL. Never matches across schemes,
/// except that an `http` item may match an `https` page (an upgrade) but never the reverse.
pub fn match_url(item_url: &str, page_url: &str) -> MatchLevel {
let (Some(item), Some(page)) = (parse(item_url), parse(page_url)) else {
return MatchLevel::None;
};
let scheme_ok = item.scheme() == page.scheme() || (item.scheme() == "http" && page.scheme() == "https");
if !scheme_ok {
return MatchLevel::None;
}
let (Some(ih), Some(ph)) = (item.host_str(), page.host_str()) else {
return MatchLevel::None;
};
let (ih, ph) = (ih.to_ascii_lowercase(), ph.to_ascii_lowercase());
if ih == ph {
return if item.port_or_known_default() == page.port_or_known_default() && item.scheme() == page.scheme() {
MatchLevel::Exact
} else {
MatchLevel::Host
};
}
same_registrable_domain(&ih, &ph)
}
#[cfg(feature = "psl")]
fn same_registrable_domain(a: &str, b: &str) -> MatchLevel {
match (psl::domain_str(a), psl::domain_str(b)) {
(Some(a), Some(b)) if a == b => MatchLevel::Domain,
_ => MatchLevel::None,
}
}
/// Without the Public Suffix List there is no safe way to tell `a.example.com` / `b.example.com`
/// (same site) from `a.github.io` / `b.github.io` (different sites), so different hosts never match.
#[cfg(not(feature = "psl"))]
fn same_registrable_domain(_: &str, _: &str) -> MatchLevel {
MatchLevel::None
}
fn parse(s: &str) -> Option<Url> {
let s = s.trim();
// Saved URLs are often stored without a scheme ("example.com/login").
if s.contains("://") {
Url::parse(s).ok()
} else {
Url::parse(&format!("https://{s}")).ok()
}
}
#[cfg(test)]
mod tests {
use super::*;
use MatchLevel::*;
#[test]
fn levels() {
assert_eq!(match_url("https://example.com", "https://example.com/login"), Exact);
assert_eq!(match_url("example.com", "https://example.com/"), Exact);
assert_eq!(match_url("https://example.com:8443", "https://example.com"), Host);
assert_eq!(match_url("https://example.com", "https://login.example.com"), Domain);
assert_eq!(match_url("https://a.github.io", "https://b.github.io"), None); // public suffix
assert_eq!(match_url("https://example.com", "https://example.com.evil.net"), None);
assert_eq!(match_url("https://example.com", "http://example.com"), None); // no downgrade
assert_eq!(match_url("http://example.com", "https://example.com"), Host); // upgrade allowed
assert_eq!(match_url("https://example.co.uk", "https://other.co.uk"), None);
}
}