aboutsummaryrefslogtreecommitdiffstats
path: root/src/bin/rbw/commands.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin/rbw/commands.rs')
-rw-r--r--src/bin/rbw/commands.rs903
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,