diff options
Diffstat (limited to 'src/bin/rbw/commands.rs')
-rw-r--r-- | src/bin/rbw/commands.rs | 903 |
1 files changed, 844 insertions, 59 deletions
diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs index a4c0a26..3329f76 100644 --- a/src/bin/rbw/commands.rs +++ b/src/bin/rbw/commands.rs @@ -1,7 +1,9 @@ use anyhow::Context as _; use serde::Serialize; +use std::fmt::{Display, Formatter, Result as FmtResult}; use std::io; use std::io::prelude::Write; +use url::Url; const MISSING_CONFIG_HELP: &str = "Before using rbw, you must configure the email address you would like to \ @@ -13,6 +15,36 @@ const MISSING_CONFIG_HELP: &str = and, if your server has a non-default identity url:\n\n \ rbw config set identity_url <url>\n"; +#[derive(Debug, Clone)] +pub enum Needle { + Name(String), + Uri(Url), + Uuid(uuid::Uuid), +} + +impl Display for Needle { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + let value = match &self { + Self::Name(name) => name.clone(), + Self::Uri(uri) => uri.to_string(), + Self::Uuid(uuid) => uuid.to_string(), + }; + write!(f, "{value}") + } +} + +#[allow(clippy::unnecessary_wraps)] +pub fn parse_needle(arg: &str) -> Result<Needle, std::convert::Infallible> { + if let Ok(uuid) = uuid::Uuid::parse_str(arg) { + return Ok(Needle::Uuid(uuid)); + } + if let Ok(url) = Url::parse(arg) { + return Ok(Needle::Uri(url)); + } + + Ok(Needle::Name(arg.to_string())) +} + #[derive(Debug, Clone, Serialize)] #[cfg_attr(test, derive(Eq, PartialEq))] struct DecryptedCipher { @@ -485,13 +517,38 @@ impl DecryptedCipher { fn exact_match( &self, - name: &str, + needle: &Needle, username: Option<&str>, folder: Option<&str>, try_match_folder: bool, ) -> bool { - if name != self.name { - return false; + match needle { + Needle::Name(name) => { + if &self.name != name { + return false; + } + } + Needle::Uri(given_uri) => { + match &self.data { + DecryptedData::Login { + uris: Some(uris), .. + } => { + if !uris.iter().any(|uri| uri.matches_url(given_uri)) + { + return false; + } + } + _ => { + // not sure what else to do here, but open to suggestions + return false; + } + } + } + Needle::Uuid(uuid) => { + if uuid::Uuid::parse_str(&self.id) != Ok(*uuid) { + return false; + } + } } if let Some(given_username) = username { @@ -650,6 +707,76 @@ struct DecryptedUri { match_type: Option<rbw::api::UriMatchType>, } +impl DecryptedUri { + fn matches_url(&self, url: &Url) -> bool { + match self.match_type.unwrap_or(rbw::api::UriMatchType::Domain) { + rbw::api::UriMatchType::Domain => { + let Some(given_domain_port) = domain_port(url) else { + return false; + }; + if let Ok(self_url) = url::Url::parse(&self.uri) { + if let Some(self_domain_port) = domain_port(&self_url) { + if self_url.scheme() == url.scheme() + && (self_domain_port == given_domain_port + || given_domain_port.ends_with(&format!( + ".{self_domain_port}" + ))) + { + return true; + } + } + } + self.uri == given_domain_port + || given_domain_port.ends_with(&format!(".{}", self.uri)) + } + rbw::api::UriMatchType::Host => { + let Some(given_host_port) = host_port(url) else { + return false; + }; + if let Ok(self_url) = url::Url::parse(&self.uri) { + if let Some(self_host_port) = host_port(&self_url) { + if self_url.scheme() == url.scheme() + && self_host_port == given_host_port + { + return true; + } + } + } + self.uri == given_host_port + } + rbw::api::UriMatchType::StartsWith => { + url.to_string().starts_with(&self.uri) + } + rbw::api::UriMatchType::Exact => url.to_string() == self.uri, + rbw::api::UriMatchType::RegularExpression => { + let Ok(rx) = regex::Regex::new(&self.uri) else { + return false; + }; + rx.is_match(url.as_ref()) + } + rbw::api::UriMatchType::Never => false, + } + } +} + +fn host_port(url: &Url) -> Option<String> { + let host = url.host_str()?; + Some( + url.port().map_or_else( + || host.to_string(), + |port| format!("{host}:{port}"), + ), + ) +} + +fn domain_port(url: &Url) -> Option<String> { + let domain = url.domain()?; + Some(url.port().map_or_else( + || domain.to_string(), + |port| format!("{domain}:{port}"), + )) +} + enum ListField { Name, Id, @@ -857,7 +984,7 @@ pub fn list(fields: &[String]) -> anyhow::Result<()> { } pub fn get( - name: &str, + needle: &Needle, user: Option<&str>, folder: Option<&str>, field: Option<&str>, @@ -872,10 +999,10 @@ pub fn get( let desc = format!( "{}{}", user.map_or_else(String::new, |s| format!("{s}@")), - name + needle ); - let (_, decrypted) = find_entry(&db, name, user, folder) + let (_, decrypted) = find_entry(&db, needle, user, folder) .with_context(|| format!("couldn't find entry for '{desc}'"))?; if raw { decrypted.display_json(&desc)?; @@ -894,6 +1021,7 @@ pub fn code( name: &str, user: Option<&str>, folder: Option<&str>, + clipboard: bool, ) -> anyhow::Result<()> { unlock()?; @@ -905,12 +1033,13 @@ pub fn code( name ); - let (_, decrypted) = find_entry(&db, name, user, folder) - .with_context(|| format!("couldn't find entry for '{desc}'"))?; + let (_, decrypted) = + find_entry(&db, &Needle::Name(name.to_string()), user, folder) + .with_context(|| format!("couldn't find entry for '{desc}'"))?; if let DecryptedData::Login { totp, .. } = decrypted.data { if let Some(totp) = totp { - println!("{}", generate_totp(&totp)?); + val_display_or_store(clipboard, &generate_totp(&totp)?); } else { return Err(anyhow::anyhow!( "entry does not contain a totp secret" @@ -1133,10 +1262,11 @@ pub fn edit( name ); - let (entry, decrypted) = find_entry(&db, name, username, folder) - .with_context(|| format!("couldn't find entry for '{desc}'"))?; + let (entry, decrypted) = + find_entry(&db, &Needle::Name(name.to_string()), username, folder) + .with_context(|| format!("couldn't find entry for '{desc}'"))?; - let (data, notes, history) = match &decrypted.data { + let (data, fields, notes, history) = match &decrypted.data { DecryptedData::Login { password, .. } => { let mut contents = format!("{}\n", password.as_deref().unwrap_or("")); @@ -1144,7 +1274,7 @@ pub fn edit( contents.push_str(&format!("\n{notes}\n")); } - let contents = rbw::edit::edit(&contents, HELP_NOTES)?; + let contents = rbw::edit::edit(&contents, HELP_PW)?; let (password, notes) = parse_editor(&contents); let password = password @@ -1190,7 +1320,7 @@ pub fn edit( uris: entry_uris.clone(), totp: entry_totp.clone(), }; - (data, notes, history) + (data, entry.fields, notes, history) } DecryptedData::SecureNote {} => { let data = rbw::db::EntryData::SecureNote {}; @@ -1210,7 +1340,7 @@ pub fn edit( }) .transpose()?; - (data, notes, entry.history) + (data, entry.fields, notes, entry.history) } _ => { return Err(anyhow::anyhow!( @@ -1226,6 +1356,7 @@ pub fn edit( entry.org_id.as_deref(), &entry.name, &data, + &fields, notes.as_deref(), entry.folder_id.as_deref(), &history, @@ -1255,8 +1386,9 @@ pub fn remove( name ); - let (entry, _) = find_entry(&db, name, username, folder) - .with_context(|| format!("couldn't find entry for '{desc}'"))?; + let (entry, _) = + find_entry(&db, &Needle::Name(name.to_string()), username, folder) + .with_context(|| format!("couldn't find entry for '{desc}'"))?; if let (Some(access_token), ()) = rbw::actions::remove(access_token, refresh_token, &entry.id)? @@ -1285,8 +1417,9 @@ pub fn history( name ); - let (_, decrypted) = find_entry(&db, name, username, folder) - .with_context(|| format!("couldn't find entry for '{desc}'"))?; + let (_, decrypted) = + find_entry(&db, &Needle::Name(name.to_string()), username, folder) + .with_context(|| format!("couldn't find entry for '{desc}'"))?; for history in decrypted.history { println!("{}: {}", history.last_used_date, history.password); } @@ -1381,13 +1514,13 @@ fn version_or_quit() -> anyhow::Result<u32> { fn find_entry( db: &rbw::db::Db, - name: &str, + needle: &Needle, username: Option<&str>, folder: Option<&str>, ) -> anyhow::Result<(rbw::db::Entry, DecryptedCipher)> { - if uuid::Uuid::parse_str(name).is_ok() { + if let Needle::Uuid(uuid) = needle { for cipher in &db.entries { - if name == cipher.id { + if uuid::Uuid::parse_str(&cipher.id) == Ok(*uuid) { return Ok((cipher.clone(), decrypt_cipher(cipher)?)); } } @@ -1401,20 +1534,20 @@ fn find_entry( decrypt_cipher(&entry).map(|decrypted| (entry, decrypted)) }) .collect::<anyhow::Result<_>>()?; - find_entry_raw(&ciphers, name, username, folder) + find_entry_raw(&ciphers, needle, username, folder) } } fn find_entry_raw( entries: &[(rbw::db::Entry, DecryptedCipher)], - name: &str, + needle: &Needle, username: Option<&str>, folder: Option<&str>, ) -> anyhow::Result<(rbw::db::Entry, DecryptedCipher)> { let mut matches: Vec<(rbw::db::Entry, DecryptedCipher)> = entries .iter() .filter(|&(_, decrypted_cipher)| { - decrypted_cipher.exact_match(name, username, folder, true) + decrypted_cipher.exact_match(needle, username, folder, true) }) .cloned() .collect(); @@ -1427,7 +1560,7 @@ fn find_entry_raw( matches = entries .iter() .filter(|&(_, decrypted_cipher)| { - decrypted_cipher.exact_match(name, username, folder, false) + decrypted_cipher.exact_match(needle, username, folder, false) }) .cloned() .collect(); @@ -1437,29 +1570,32 @@ fn find_entry_raw( } } - matches = entries - .iter() - .filter(|&(_, decrypted_cipher)| { - decrypted_cipher.partial_match(name, username, folder, true) - }) - .cloned() - .collect(); - - if matches.len() == 1 { - return Ok(matches[0].clone()); - } - - if folder.is_none() { + if let Needle::Name(name) = needle { matches = entries .iter() .filter(|&(_, decrypted_cipher)| { - decrypted_cipher.partial_match(name, username, folder, false) + decrypted_cipher.partial_match(name, username, folder, true) }) .cloned() .collect(); + if matches.len() == 1 { return Ok(matches[0].clone()); } + + if folder.is_none() { + matches = entries + .iter() + .filter(|&(_, decrypted_cipher)| { + decrypted_cipher + .partial_match(name, username, folder, false) + }) + .cloned() + .collect(); + if matches.len() == 1 { + return Ok(matches[0].clone()); + } + } } if matches.is_empty() { @@ -1863,15 +1999,15 @@ mod test { #[test] fn test_find_entry() { let entries = &[ - make_entry("github", Some("foo"), None), - make_entry("gitlab", Some("foo"), None), - make_entry("gitlab", Some("bar"), None), - make_entry("gitter", Some("baz"), None), - make_entry("git", Some("foo"), None), - make_entry("bitwarden", None, None), - make_entry("github", Some("foo"), Some("websites")), - make_entry("github", Some("foo"), Some("ssh")), - make_entry("github", Some("root"), Some("ssh")), + make_entry("github", Some("foo"), None, &[]), + make_entry("gitlab", Some("foo"), None, &[]), + make_entry("gitlab", Some("bar"), None, &[]), + make_entry("gitter", Some("baz"), None, &[]), + make_entry("git", Some("foo"), None, &[]), + make_entry("bitwarden", None, None, &[]), + make_entry("github", Some("foo"), Some("websites"), &[]), + make_entry("github", Some("foo"), Some("ssh"), &[]), + make_entry("github", Some("root"), Some("ssh"), &[]), ]; assert!( @@ -1930,26 +2066,653 @@ mod test { ); } + #[test] + fn test_find_by_uuid() { + let entries = &[ + make_entry("github", Some("foo"), None, &[]), + make_entry("gitlab", Some("foo"), None, &[]), + make_entry("gitlab", Some("bar"), None, &[]), + ]; + + assert!( + one_match(entries, &entries[0].0.id, None, None, 0), + "foo@github" + ); + assert!( + one_match(entries, &entries[1].0.id, None, None, 1), + "foo@gitlab" + ); + assert!( + one_match(entries, &entries[2].0.id, None, None, 2), + "bar@gitlab" + ); + + assert!( + one_match( + entries, + &entries[0].0.id.to_uppercase(), + None, + None, + 0 + ), + "foo@github" + ); + assert!( + one_match( + entries, + &entries[0].0.id.to_lowercase(), + None, + None, + 0 + ), + "foo@github" + ); + } + + #[test] + fn test_find_by_url_default() { + let entries = &[ + make_entry("one", None, None, &[("https://one.com/", None)]), + make_entry("two", None, None, &[("https://two.com/login", None)]), + make_entry( + "three", + None, + None, + &[("https://login.three.com/", None)], + ), + make_entry("four", None, None, &[("four.com", None)]), + make_entry( + "five", + None, + None, + &[("https://five.com:8080/", None)], + ), + make_entry("six", None, None, &[("six.com:8080", None)]), + ]; + + assert!(one_match(entries, "https://one.com/", None, None, 0), "one"); + assert!( + one_match(entries, "https://login.one.com/", None, None, 0), + "one" + ); + assert!( + one_match(entries, "https://one.com:443/", None, None, 0), + "one" + ); + assert!(no_matches(entries, "one.com", None, None), "one"); + assert!(no_matches(entries, "https", None, None), "one"); + assert!(no_matches(entries, "com", None, None), "one"); + assert!(no_matches(entries, "https://com/", None, None), "one"); + + assert!(one_match(entries, "https://two.com/", None, None, 1), "two"); + assert!( + one_match(entries, "https://two.com/other-page", None, None, 1), + "two" + ); + + assert!( + one_match(entries, "https://login.three.com/", None, None, 2), + "three" + ); + assert!( + no_matches(entries, "https://three.com/", None, None), + "three" + ); + + assert!( + one_match(entries, "https://four.com/", None, None, 3), + "four" + ); + + assert!( + one_match(entries, "https://five.com:8080/", None, None, 4), + "five" + ); + assert!(no_matches(entries, "https://five.com/", None, None), "five"); + + assert!( + one_match(entries, "https://six.com:8080/", None, None, 5), + "six" + ); + assert!(no_matches(entries, "https://six.com/", None, None), "six"); + } + + #[test] + fn test_find_by_url_domain() { + let entries = &[ + make_entry( + "one", + None, + None, + &[("https://one.com/", Some(rbw::api::UriMatchType::Domain))], + ), + make_entry( + "two", + None, + None, + &[( + "https://two.com/login", + Some(rbw::api::UriMatchType::Domain), + )], + ), + make_entry( + "three", + None, + None, + &[( + "https://login.three.com/", + Some(rbw::api::UriMatchType::Domain), + )], + ), + make_entry( + "four", + None, + None, + &[("four.com", Some(rbw::api::UriMatchType::Domain))], + ), + make_entry( + "five", + None, + None, + &[( + "https://five.com:8080/", + Some(rbw::api::UriMatchType::Domain), + )], + ), + make_entry( + "six", + None, + None, + &[("six.com:8080", Some(rbw::api::UriMatchType::Domain))], + ), + ]; + + assert!(one_match(entries, "https://one.com/", None, None, 0), "one"); + assert!( + one_match(entries, "https://login.one.com/", None, None, 0), + "one" + ); + assert!( + one_match(entries, "https://one.com:443/", None, None, 0), + "one" + ); + assert!(no_matches(entries, "one.com", None, None), "one"); + assert!(no_matches(entries, "https", None, None), "one"); + assert!(no_matches(entries, "com", None, None), "one"); + assert!(no_matches(entries, "https://com/", None, None), "one"); + + assert!(one_match(entries, "https://two.com/", None, None, 1), "two"); + assert!( + one_match(entries, "https://two.com/other-page", None, None, 1), + "two" + ); + + assert!( + one_match(entries, "https://login.three.com/", None, None, 2), + "three" + ); + assert!( + no_matches(entries, "https://three.com/", None, None), + "three" + ); + + assert!( + one_match(entries, "https://four.com/", None, None, 3), + "four" + ); + + assert!( + one_match(entries, "https://five.com:8080/", None, None, 4), + "five" + ); + assert!(no_matches(entries, "https://five.com/", None, None), "five"); + + assert!( + one_match(entries, "https://six.com:8080/", None, None, 5), + "six" + ); + assert!(no_matches(entries, "https://six.com/", None, None), "six"); + } + + #[test] + fn test_find_by_url_host() { + let entries = &[ + make_entry( + "one", + None, + None, + &[("https://one.com/", Some(rbw::api::UriMatchType::Host))], + ), + make_entry( + "two", + None, + None, + &[( + "https://two.com/login", + Some(rbw::api::UriMatchType::Host), + )], + ), + make_entry( + "three", + None, + None, + &[( + "https://login.three.com/", + Some(rbw::api::UriMatchType::Host), + )], + ), + make_entry( + "four", + None, + None, + &[("four.com", Some(rbw::api::UriMatchType::Host))], + ), + make_entry( + "five", + None, + None, + &[( + "https://five.com:8080/", + Some(rbw::api::UriMatchType::Host), + )], + ), + make_entry( + "six", + None, + None, + &[("six.com:8080", Some(rbw::api::UriMatchType::Host))], + ), + ]; + + assert!(one_match(entries, "https://one.com/", None, None, 0), "one"); + assert!( + no_matches(entries, "https://login.one.com/", None, None), + "one" + ); + assert!( + one_match(entries, "https://one.com:443/", None, None, 0), + "one" + ); + assert!(no_matches(entries, "one.com", None, None), "one"); + assert!(no_matches(entries, "https", None, None), "one"); + assert!(no_matches(entries, "com", None, None), "one"); + assert!(no_matches(entries, "https://com/", None, None), "one"); + + assert!(one_match(entries, "https://two.com/", None, None, 1), "two"); + assert!( + one_match(entries, "https://two.com/other-page", None, None, 1), + "two" + ); + + assert!( + one_match(entries, "https://login.three.com/", None, None, 2), + "three" + ); + assert!( + no_matches(entries, "https://three.com/", None, None), + "three" + ); + + assert!( + one_match(entries, "https://four.com/", None, None, 3), + "four" + ); + + assert!( + one_match(entries, "https://five.com:8080/", None, None, 4), + "five" + ); + assert!(no_matches(entries, "https://five.com/", None, None), "five"); + + assert!( + one_match(entries, "https://six.com:8080/", None, None, 5), + "six" + ); + assert!(no_matches(entries, "https://six.com/", None, None), "six"); + } + + #[test] + fn test_find_by_url_starts_with() { + let entries = &[ + make_entry( + "one", + None, + None, + &[( + "https://one.com/", + Some(rbw::api::UriMatchType::StartsWith), + )], + ), + make_entry( + "two", + None, + None, + &[( + "https://two.com/login", + Some(rbw::api::UriMatchType::StartsWith), + )], + ), + make_entry( + "three", + None, + None, + &[( + "https://login.three.com/", + Some(rbw::api::UriMatchType::StartsWith), + )], + ), + ]; + + assert!(one_match(entries, "https://one.com/", None, None, 0), "one"); + assert!( + no_matches(entries, "https://login.one.com/", None, None), + "one" + ); + assert!( + one_match(entries, "https://one.com:443/", None, None, 0), + "one" + ); + assert!(no_matches(entries, "one.com", None, None), "one"); + assert!(no_matches(entries, "https", None, None), "one"); + assert!(no_matches(entries, "com", None, None), "one"); + assert!(no_matches(entries, "https://com/", None, None), "one"); + + assert!( + one_match(entries, "https://two.com/login", None, None, 1), + "two" + ); + assert!( + one_match(entries, "https://two.com/login/sso", None, None, 1), + "two" + ); + assert!(no_matches(entries, "https://two.com/", None, None), "two"); + assert!( + no_matches(entries, "https://two.com/other-page", None, None), + "two" + ); + + assert!( + one_match(entries, "https://login.three.com/", None, None, 2), + "three" + ); + assert!( + no_matches(entries, "https://three.com/", None, None), + "three" + ); + } + + #[test] + fn test_find_by_url_exact() { + let entries = &[ + make_entry( + "one", + None, + None, + &[("https://one.com/", Some(rbw::api::UriMatchType::Exact))], + ), + make_entry( + "two", + None, + None, + &[( + "https://two.com/login", + Some(rbw::api::UriMatchType::Exact), + )], + ), + make_entry( + "three", + None, + None, + &[( + "https://login.three.com/", + Some(rbw::api::UriMatchType::Exact), + )], + ), + ]; + + assert!(one_match(entries, "https://one.com/", None, None, 0), "one"); + assert!( + no_matches(entries, "https://login.one.com/", None, None), + "one" + ); + assert!( + one_match(entries, "https://one.com:443/", None, None, 0), + "one" + ); + assert!(no_matches(entries, "one.com", None, None), "one"); + assert!(no_matches(entries, "https", None, None), "one"); + assert!(no_matches(entries, "com", None, None), "one"); + assert!(no_matches(entries, "https://com/", None, None), "one"); + + assert!( + one_match(entries, "https://two.com/login", None, None, 1), + "two" + ); + assert!( + no_matches(entries, "https://two.com/login/sso", None, None), + "two" + ); + assert!(no_matches(entries, "https://two.com/", None, None), "two"); + assert!( + no_matches(entries, "https://two.com/other-page", None, None), + "two" + ); + + assert!( + one_match(entries, "https://login.three.com/", None, None, 2), + "three" + ); + assert!( + no_matches(entries, "https://three.com/", None, None), + "three" + ); + } + + #[test] + fn test_find_by_url_regex() { + let entries = &[ + make_entry( + "one", + None, + None, + &[( + r"^https://one\.com/$", + Some(rbw::api::UriMatchType::RegularExpression), + )], + ), + make_entry( + "two", + None, + None, + &[( + r"^https://two\.com/(login|start)", + Some(rbw::api::UriMatchType::RegularExpression), + )], + ), + make_entry( + "three", + None, + None, + &[( + r"^https://(login\.)?three\.com/$", + Some(rbw::api::UriMatchType::RegularExpression), + )], + ), + ]; + + assert!(one_match(entries, "https://one.com/", None, None, 0), "one"); + assert!( + no_matches(entries, "https://login.one.com/", None, None), + "one" + ); + assert!( + one_match(entries, "https://one.com:443/", None, None, 0), + "one" + ); + assert!(no_matches(entries, "one.com", None, None), "one"); + assert!(no_matches(entries, "https", None, None), "one"); + assert!(no_matches(entries, "com", None, None), "one"); + assert!(no_matches(entries, "https://com/", None, None), "one"); + + assert!( + one_match(entries, "https://two.com/login", None, None, 1), + "two" + ); + assert!( + one_match(entries, "https://two.com/start", None, None, 1), + "two" + ); + assert!( + one_match(entries, "https://two.com/login/sso", None, None, 1), + "two" + ); + assert!(no_matches(entries, "https://two.com/", None, None), "two"); + assert!( + no_matches(entries, "https://two.com/other-page", None, None), + "two" + ); + + assert!( + one_match(entries, "https://login.three.com/", None, None, 2), + "three" + ); + assert!( + one_match(entries, "https://three.com/", None, None, 2), + "three" + ); + assert!( + no_matches(entries, "https://www.three.com/", None, None), + "three" + ); + } + + #[test] + fn test_find_by_url_never() { + let entries = &[ + make_entry( + "one", + None, + None, + &[("https://one.com/", Some(rbw::api::UriMatchType::Never))], + ), + make_entry( + "two", + None, + None, + &[( + "https://two.com/login", + Some(rbw::api::UriMatchType::Never), + )], + ), + make_entry( + "three", + None, + None, + &[( + "https://login.three.com/", + Some(rbw::api::UriMatchType::Never), + )], + ), + make_entry( + "four", + None, + None, + &[("four.com", Some(rbw::api::UriMatchType::Never))], + ), + make_entry( + "five", + None, + None, + &[( + "https://five.com:8080/", + Some(rbw::api::UriMatchType::Never), + )], + ), + make_entry( + "six", + None, + None, + &[("six.com:8080", Some(rbw::api::UriMatchType::Never))], + ), + ]; + + assert!(no_matches(entries, "https://one.com/", None, None), "one"); + assert!( + no_matches(entries, "https://login.one.com/", None, None), + "one" + ); + assert!( + no_matches(entries, "https://one.com:443/", None, None), + "one" + ); + assert!(no_matches(entries, "one.com", None, None), "one"); + assert!(no_matches(entries, "https", None, None), "one"); + assert!(no_matches(entries, "com", None, None), "one"); + assert!(no_matches(entries, "https://com/", None, None), "one"); + + assert!(no_matches(entries, "https://two.com/", None, None), "two"); + assert!( + no_matches(entries, "https://two.com/other-page", None, None), + "two" + ); + + assert!( + no_matches(entries, "https://login.three.com/", None, None), + "three" + ); + assert!( + no_matches(entries, "https://three.com/", None, None), + "three" + ); + + assert!(no_matches(entries, "https://four.com/", None, None), "four"); + + assert!( + no_matches(entries, "https://five.com:8080/", None, None), + "five" + ); + assert!(no_matches(entries, "https://five.com/", None, None), "five"); + + assert!( + no_matches(entries, "https://six.com:8080/", None, None), + "six" + ); + assert!(no_matches(entries, "https://six.com/", None, None), "six"); + } + + #[track_caller] fn one_match( entries: &[(rbw::db::Entry, DecryptedCipher)], - name: &str, + needle: &str, username: Option<&str>, folder: Option<&str>, idx: usize, ) -> bool { entries_eq( - &find_entry_raw(entries, name, username, folder).unwrap(), + &find_entry_raw( + entries, + &parse_needle(needle).unwrap(), + username, + folder, + ) + .unwrap(), &entries[idx], ) } + #[track_caller] fn no_matches( entries: &[(rbw::db::Entry, DecryptedCipher)], - name: &str, + needle: &str, username: Option<&str>, folder: Option<&str>, ) -> bool { - let res = find_entry_raw(entries, name, username, folder); + let res = find_entry_raw( + entries, + &parse_needle(needle).unwrap(), + username, + folder, + ); if let Err(e) = res { format!("{e}").contains("no entry found") } else { @@ -1957,13 +2720,19 @@ mod test { } } + #[track_caller] fn many_matches( entries: &[(rbw::db::Entry, DecryptedCipher)], - name: &str, + needle: &str, username: Option<&str>, folder: Option<&str>, ) -> bool { - let res = find_entry_raw(entries, name, username, folder); + let res = find_entry_raw( + entries, + &parse_needle(needle).unwrap(), + username, + folder, + ); if let Err(e) = res { format!("{e}").contains("multiple entries found") } else { @@ -1971,6 +2740,7 @@ mod test { } } + #[track_caller] fn entries_eq( a: &(rbw::db::Entry, DecryptedCipher), b: &(rbw::db::Entry, DecryptedCipher), @@ -1982,10 +2752,12 @@ mod test { name: &str, username: Option<&str>, folder: Option<&str>, + uris: &[(&str, Option<rbw::api::UriMatchType>)], ) -> (rbw::db::Entry, DecryptedCipher) { + let id = uuid::Uuid::new_v4(); ( rbw::db::Entry { - id: "irrelevant".to_string(), + id: id.to_string(), org_id: None, folder: folder.map(|_| "encrypted folder name".to_string()), folder_id: None, @@ -1995,7 +2767,13 @@ mod test { "this is the encrypted username".to_string() }), password: None, - uris: vec![], + uris: uris + .iter() + .map(|(_, match_type)| rbw::db::Uri { + uri: "this is the encrypted uri".to_string(), + match_type: *match_type, + }) + .collect(), totp: None, }, fields: vec![], @@ -2003,14 +2781,21 @@ mod test { history: vec![], }, DecryptedCipher { - id: "irrelevant".to_string(), + id: id.to_string(), folder: folder.map(std::string::ToString::to_string), name: name.to_string(), data: DecryptedData::Login { username: username.map(std::string::ToString::to_string), password: None, totp: None, - uris: None, + uris: Some( + uris.iter() + .map(|(uri, match_type)| DecryptedUri { + uri: (*uri).to_string(), + match_type: *match_type, + }) + .collect(), + ), }, fields: vec![], notes: None, |