Passkeys for the browser extension, tags in item lists
The browser bridge answers passkey_query/get/create/result/cancel: the page's RP ID is checked against the tab origin (no public suffixes), every request waits for the user to approve it in the app, and the extension polls for the result. Tags are normalised on save and included in item summaries. confirm_master_password verifies the user where there are no biometrics. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -80,6 +80,30 @@ impl Item {
|
||||
}
|
||||
}
|
||||
|
||||
/// Longest tag kept, in characters.
|
||||
pub const MAX_TAG_LEN: usize = 64;
|
||||
/// Most tags kept on one item.
|
||||
pub const MAX_TAGS: usize = 32;
|
||||
|
||||
/// The tags as every app stores them: trimmed, without a leading `#`, inner whitespace collapsed,
|
||||
/// at most [`MAX_TAG_LEN`] characters, case-insensitive duplicates dropped (first spelling wins),
|
||||
/// in the given order, at most [`MAX_TAGS`].
|
||||
pub fn normalize_tags<S: AsRef<str>>(tags: &[S]) -> Vec<String> {
|
||||
let mut out: Vec<String> = Vec::new();
|
||||
for t in tags {
|
||||
let t = t.as_ref().trim().trim_start_matches('#');
|
||||
let t: String = t.split_whitespace().collect::<Vec<_>>().join(" ").chars().take(MAX_TAG_LEN).collect();
|
||||
if t.is_empty() || out.iter().any(|o| o.to_lowercase() == t.to_lowercase()) {
|
||||
continue;
|
||||
}
|
||||
out.push(t);
|
||||
if out.len() == MAX_TAGS {
|
||||
break;
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
impl Drop for Item {
|
||||
fn drop(&mut self) {
|
||||
self.password.zeroize();
|
||||
@@ -88,3 +112,20 @@ impl Drop for Item {
|
||||
self.passkeys.zeroize();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tag_tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn tags_are_normalised() {
|
||||
assert_eq!(
|
||||
normalize_tags(&[" #Work ", "work", "", " family trip ", "#", "Bank"]),
|
||||
["Work", "family trip", "Bank"]
|
||||
);
|
||||
let long = "x".repeat(100);
|
||||
assert_eq!(normalize_tags(&[long.as_str()])[0].chars().count(), MAX_TAG_LEN);
|
||||
let many: Vec<String> = (0..50).map(|i| format!("t{i}")).collect();
|
||||
assert_eq!(normalize_tags(&many).len(), MAX_TAGS);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,7 @@ pub mod matching;
|
||||
pub mod passkey;
|
||||
pub mod totp;
|
||||
|
||||
pub use item::{Item, ItemKind, Passkey};
|
||||
pub use item::{normalize_tags, Item, ItemKind, Passkey, MAX_TAGS, MAX_TAG_LEN};
|
||||
#[cfg(feature = "psl")]
|
||||
pub use matching::registrable_domain;
|
||||
pub use matching::{match_url, MatchLevel};
|
||||
|
||||
@@ -37,6 +37,12 @@ pub fn match_url(item_url: &str, page_url: &str) -> MatchLevel {
|
||||
same_registrable_domain(&ih, &ph)
|
||||
}
|
||||
|
||||
/// The registrable domain (eTLD+1) of `host`, or `None` for a public suffix such as `com` or `github.io`.
|
||||
#[cfg(feature = "psl")]
|
||||
pub fn registrable_domain(host: &str) -> Option<&str> {
|
||||
psl::domain_str(host)
|
||||
}
|
||||
|
||||
#[cfg(feature = "psl")]
|
||||
fn same_registrable_domain(a: &str, b: &str) -> MatchLevel {
|
||||
match (psl::domain_str(a), psl::domain_str(b)) {
|
||||
|
||||
Reference in New Issue
Block a user