diff options
Diffstat (limited to 'src/bin/rbw')
-rw-r--r-- | src/bin/rbw/commands.rs | 818 | ||||
-rw-r--r-- | src/bin/rbw/main.rs | 68 |
2 files changed, 720 insertions, 166 deletions
diff --git a/src/bin/rbw/commands.rs b/src/bin/rbw/commands.rs index 6d36eb3..fc858e1 100644 --- a/src/bin/rbw/commands.rs +++ b/src/bin/rbw/commands.rs @@ -39,7 +39,9 @@ pub fn parse_needle(arg: &str) -> Result<Needle, std::convert::Infallible> { return Ok(Needle::Uuid(uuid)); } if let Ok(url) = Url::parse(arg) { - return Ok(Needle::Uri(url)); + if url.is_special() { + return Ok(Needle::Uri(url)); + } } Ok(Needle::Name(arg.to_string())) @@ -521,10 +523,14 @@ impl DecryptedCipher { username: Option<&str>, folder: Option<&str>, try_match_folder: bool, + ignore_case: bool, ) -> bool { match needle { Needle::Name(name) => { - if &self.name != name { + if !((ignore_case + && name.to_lowercase() == self.name.to_lowercase()) + || *name == self.name) + { return false; } } @@ -591,8 +597,12 @@ impl DecryptedCipher { username: Option<&str>, folder: Option<&str>, try_match_folder: bool, + ignore_case: bool, ) -> bool { - if !self.name.contains(name) { + if !((ignore_case + && self.name.to_lowercase().contains(&name.to_lowercase())) + || self.name.contains(name)) + { return false; } @@ -602,7 +612,12 @@ impl DecryptedCipher { username: Some(found_username), .. } => { - if !found_username.contains(given_username) { + if !((ignore_case + && found_username + .to_lowercase() + .contains(&given_username.to_lowercase())) + || found_username.contains(given_username)) + { return false; } } @@ -629,6 +644,41 @@ impl DecryptedCipher { true } + + fn search_match(&self, term: &str, folder: Option<&str>) -> bool { + if let Some(folder) = folder { + if self.folder.as_deref() != Some(folder) { + return false; + } + } + + let fields = [ + Some(self.name.as_str()), + self.notes.as_deref(), + if let DecryptedData::Login { + username: Some(username), + .. + } = &self.data + { + Some(username) + } else { + None + }, + ]; + for field in fields + .iter() + .filter_map(|field| field.map(std::string::ToString::to_string)) + .chain(self.fields.iter().filter_map(|field| { + field.value.as_ref().map(std::string::ToString::to_string) + })) + { + if field.to_lowercase().contains(&term.to_lowercase()) { + return true; + } + } + + false + } } fn val_display_or_store(clipboard: bool, password: &str) -> bool { @@ -691,6 +741,25 @@ enum DecryptedData { struct DecryptedField { name: Option<String>, value: Option<String>, + #[serde(serialize_with = "serialize_field_type", rename = "type")] + ty: rbw::api::FieldType, +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn serialize_field_type<S>( + ty: &rbw::api::FieldType, + serializer: S, +) -> Result<S::Ok, S::Error> +where + S: serde::Serializer, +{ + let s = match ty { + rbw::api::FieldType::Text => "text", + rbw::api::FieldType::Hidden => "hidden", + rbw::api::FieldType::Boolean => "boolean", + rbw::api::FieldType::Linked => "linked", + }; + serializer.serialize_str(s) } #[derive(Debug, Clone, Serialize)] @@ -823,8 +892,10 @@ pub fn config_set(key: &str, value: &str) -> anyhow::Result<()> { .unwrap_or_else(|_| rbw::config::Config::new()); match key { "email" => config.email = Some(value.to_string()), + "sso_id" => config.sso_id = Some(value.to_string()), "base_url" => config.base_url = Some(value.to_string()), "identity_url" => config.identity_url = Some(value.to_string()), + "ui_url" => config.ui_url = Some(value.to_string()), "notifications_url" => { config.notifications_url = Some(value.to_string()); } @@ -868,8 +939,10 @@ pub fn config_unset(key: &str) -> anyhow::Result<()> { .unwrap_or_else(|_| rbw::config::Config::new()); match key { "email" => config.email = None, + "sso_id" => config.sso_id = None, "base_url" => config.base_url = None, "identity_url" => config.identity_url = None, + "ui_url" => config.ui_url = None, "notifications_url" => config.notifications_url = None, "client_cert_path" => config.client_cert_path = None, "lock_timeout" => { @@ -983,6 +1056,7 @@ pub fn list(fields: &[String]) -> anyhow::Result<()> { Ok(()) } +#[allow(clippy::fn_params_excessive_bools)] pub fn get( needle: &Needle, user: Option<&str>, @@ -991,6 +1065,7 @@ pub fn get( full: bool, raw: bool, clipboard: bool, + ignore_case: bool, ) -> anyhow::Result<()> { unlock()?; @@ -1002,8 +1077,9 @@ pub fn get( needle ); - let (_, decrypted) = find_entry(&db, needle, user, folder) - .with_context(|| format!("couldn't find entry for '{desc}'"))?; + let (_, decrypted) = + find_entry(&db, needle, user, folder, ignore_case) + .with_context(|| format!("couldn't find entry for '{desc}'"))?; if raw { decrypted.display_json(&desc)?; } else if full { @@ -1017,11 +1093,52 @@ pub fn get( Ok(()) } +pub fn search(term: &str, folder: Option<&str>) -> anyhow::Result<()> { + unlock()?; + + let db = load_db()?; + + let found_entries: Vec<_> = db + .entries + .iter() + .map(decrypt_cipher) + .filter_map(|entry| { + entry + .map(|decrypted| { + if decrypted.search_match(term, folder) { + let mut display = decrypted.name; + if let DecryptedData::Login { + username: Some(username), + .. + } = decrypted.data + { + display = format!("{username}@{display}"); + } + if let Some(folder) = decrypted.folder { + display = format!("{folder}/{display}"); + } + Some(display) + } else { + None + } + }) + .transpose() + }) + .collect::<Result<_, anyhow::Error>>()?; + + for name in found_entries { + println!("{name}"); + } + + Ok(()) +} + pub fn code( needle: &Needle, user: Option<&str>, folder: Option<&str>, clipboard: bool, + ignore_case: bool, ) -> anyhow::Result<()> { unlock()?; @@ -1033,8 +1150,9 @@ pub fn code( needle ); - let (_, decrypted) = find_entry(&db, needle, user, folder) - .with_context(|| format!("couldn't find entry for '{desc}'"))?; + let (_, decrypted) = + find_entry(&db, needle, user, folder, ignore_case) + .with_context(|| format!("couldn't find entry for '{desc}'"))?; if let DecryptedData::Login { totp, .. } = decrypted.data { if let Some(totp) = totp { @@ -1095,7 +1213,7 @@ pub fn add( let (new_access_token, folders) = rbw::actions::list_folders(&access_token, refresh_token)?; if let Some(new_access_token) = new_access_token { - access_token = new_access_token.clone(); + access_token.clone_from(&new_access_token); db.access_token = Some(new_access_token); save_db(&db)?; } @@ -1118,7 +1236,7 @@ pub fn add( &crate::actions::encrypt(folder_name, None)?, )?; if let Some(new_access_token) = new_access_token { - access_token = new_access_token.clone(); + access_token.clone_from(&new_access_token); db.access_token = Some(new_access_token); save_db(&db)?; } @@ -1188,7 +1306,7 @@ pub fn generate( let (new_access_token, folders) = rbw::actions::list_folders(&access_token, refresh_token)?; if let Some(new_access_token) = new_access_token { - access_token = new_access_token.clone(); + access_token.clone_from(&new_access_token); db.access_token = Some(new_access_token); save_db(&db)?; } @@ -1213,7 +1331,7 @@ pub fn generate( &crate::actions::encrypt(folder_name, None)?, )?; if let Some(new_access_token) = new_access_token { - access_token = new_access_token.clone(); + access_token.clone_from(&new_access_token); db.access_token = Some(new_access_token); save_db(&db)?; } @@ -1248,6 +1366,7 @@ pub fn edit( name: &str, username: Option<&str>, folder: Option<&str>, + ignore_case: bool, ) -> anyhow::Result<()> { unlock()?; @@ -1261,9 +1380,14 @@ pub fn edit( name ); - let (entry, decrypted) = - find_entry(&db, &Needle::Name(name.to_string()), username, folder) - .with_context(|| format!("couldn't find entry for '{desc}'"))?; + let (entry, decrypted) = find_entry( + &db, + &Needle::Name(name.to_string()), + username, + folder, + ignore_case, + ) + .with_context(|| format!("couldn't find entry for '{desc}'"))?; let (data, fields, notes, history) = match &decrypted.data { DecryptedData::Login { password, .. } => { @@ -1372,6 +1496,7 @@ pub fn remove( name: &str, username: Option<&str>, folder: Option<&str>, + ignore_case: bool, ) -> anyhow::Result<()> { unlock()?; @@ -1385,9 +1510,14 @@ pub fn remove( name ); - let (entry, _) = - find_entry(&db, &Needle::Name(name.to_string()), username, folder) - .with_context(|| format!("couldn't find entry for '{desc}'"))?; + let (entry, _) = find_entry( + &db, + &Needle::Name(name.to_string()), + username, + folder, + ignore_case, + ) + .with_context(|| format!("couldn't find entry for '{desc}'"))?; if let (Some(access_token), ()) = rbw::actions::remove(access_token, refresh_token, &entry.id)? @@ -1405,6 +1535,7 @@ pub fn history( name: &str, username: Option<&str>, folder: Option<&str>, + ignore_case: bool, ) -> anyhow::Result<()> { unlock()?; @@ -1416,9 +1547,14 @@ pub fn history( name ); - let (_, decrypted) = - find_entry(&db, &Needle::Name(name.to_string()), username, folder) - .with_context(|| format!("couldn't find entry for '{desc}'"))?; + let (_, decrypted) = find_entry( + &db, + &Needle::Name(name.to_string()), + username, + folder, + ignore_case, + ) + .with_context(|| format!("couldn't find entry for '{desc}'"))?; for history in decrypted.history { println!("{}: {}", history.last_used_date, history.password); } @@ -1516,6 +1652,7 @@ fn find_entry( needle: &Needle, username: Option<&str>, folder: Option<&str>, + ignore_case: bool, ) -> anyhow::Result<(rbw::db::Entry, DecryptedCipher)> { if let Needle::Uuid(uuid) = needle { for cipher in &db.entries { @@ -1533,7 +1670,7 @@ fn find_entry( decrypt_cipher(&entry).map(|decrypted| (entry, decrypted)) }) .collect::<anyhow::Result<_>>()?; - find_entry_raw(&ciphers, needle, username, folder) + find_entry_raw(&ciphers, needle, username, folder, ignore_case) } } @@ -1542,11 +1679,18 @@ fn find_entry_raw( needle: &Needle, username: Option<&str>, folder: Option<&str>, + ignore_case: bool, ) -> anyhow::Result<(rbw::db::Entry, DecryptedCipher)> { let mut matches: Vec<(rbw::db::Entry, DecryptedCipher)> = entries .iter() .filter(|&(_, decrypted_cipher)| { - decrypted_cipher.exact_match(needle, username, folder, true) + decrypted_cipher.exact_match( + needle, + username, + folder, + true, + ignore_case, + ) }) .cloned() .collect(); @@ -1559,7 +1703,13 @@ fn find_entry_raw( matches = entries .iter() .filter(|&(_, decrypted_cipher)| { - decrypted_cipher.exact_match(needle, username, folder, false) + decrypted_cipher.exact_match( + needle, + username, + folder, + false, + ignore_case, + ) }) .cloned() .collect(); @@ -1573,7 +1723,13 @@ fn find_entry_raw( matches = entries .iter() .filter(|&(_, decrypted_cipher)| { - decrypted_cipher.partial_match(name, username, folder, true) + decrypted_cipher.partial_match( + name, + username, + folder, + true, + ignore_case, + ) }) .cloned() .collect(); @@ -1586,8 +1742,13 @@ fn find_entry_raw( matches = entries .iter() .filter(|&(_, decrypted_cipher)| { - decrypted_cipher - .partial_match(name, username, folder, false) + decrypted_cipher.partial_match( + name, + username, + folder, + false, + ignore_case, + ) }) .cloned() .collect(); @@ -1664,6 +1825,7 @@ fn decrypt_cipher(entry: &rbw::db::Entry) -> anyhow::Result<DecryptedCipher> { ) }) .transpose()?, + ty: field.ty, }) }) .collect::<anyhow::Result<_>>()?; @@ -1951,7 +2113,7 @@ struct TotpParams { fn decode_totp_secret(secret: &str) -> anyhow::Result<Vec<u8>> { base32::decode( - base32::Alphabet::RFC4648 { padding: false }, + base32::Alphabet::Rfc4648 { padding: false }, &secret.replace(' ', ""), ) .ok_or_else(|| anyhow::anyhow!("totp secret was not valid base32")) @@ -2066,59 +2228,155 @@ mod test { ]; assert!( - one_match(entries, "github", Some("foo"), None, 0), + one_match(entries, "github", Some("foo"), None, 0, false), "foo@github" ); - assert!(one_match(entries, "github", None, None, 0), "github"); assert!( - one_match(entries, "gitlab", Some("foo"), None, 1), + one_match(entries, "GITHUB", Some("foo"), None, 0, true), + "foo@GITHUB" + ); + assert!(one_match(entries, "github", None, None, 0, false), "github"); + assert!(one_match(entries, "GITHUB", None, None, 0, true), "GITHUB"); + assert!( + one_match(entries, "gitlab", Some("foo"), None, 1, false), "foo@gitlab" ); - assert!(one_match(entries, "git", Some("bar"), None, 2), "bar@git"); assert!( - one_match(entries, "gitter", Some("ba"), None, 3), + one_match(entries, "GITLAB", Some("foo"), None, 1, true), + "foo@GITLAB" + ); + assert!( + one_match(entries, "git", Some("bar"), None, 2, false), + "bar@git" + ); + assert!( + one_match(entries, "GIT", Some("bar"), None, 2, true), + "bar@GIT" + ); + assert!( + one_match(entries, "gitter", Some("ba"), None, 3, false), "ba@gitter" ); - assert!(one_match(entries, "git", Some("foo"), None, 4), "foo@git"); - assert!(one_match(entries, "git", None, None, 4), "git"); - assert!(one_match(entries, "bitwarden", None, None, 5), "bitwarden"); assert!( - one_match(entries, "github", Some("foo"), Some("websites"), 6), + one_match(entries, "GITTER", Some("ba"), None, 3, true), + "ba@GITTER" + ); + assert!( + one_match(entries, "git", Some("foo"), None, 4, false), + "foo@git" + ); + assert!( + one_match(entries, "GIT", Some("foo"), None, 4, true), + "foo@GIT" + ); + assert!(one_match(entries, "git", None, None, 4, false), "git"); + assert!(one_match(entries, "GIT", None, None, 4, true), "GIT"); + assert!( + one_match(entries, "bitwarden", None, None, 5, false), + "bitwarden" + ); + assert!( + one_match(entries, "BITWARDEN", None, None, 5, true), + "BITWARDEN" + ); + assert!( + one_match( + entries, + "github", + Some("foo"), + Some("websites"), + 6, + false + ), "websites/foo@github" ); assert!( - one_match(entries, "github", Some("foo"), Some("ssh"), 7), + one_match( + entries, + "GITHUB", + Some("foo"), + Some("websites"), + 6, + true + ), + "websites/foo@GITHUB" + ); + assert!( + one_match(entries, "github", Some("foo"), Some("ssh"), 7, false), "ssh/foo@github" ); assert!( - one_match(entries, "github", Some("root"), None, 8), + one_match(entries, "GITHUB", Some("foo"), Some("ssh"), 7, true), + "ssh/foo@GITHUB" + ); + assert!( + one_match(entries, "github", Some("root"), None, 8, false), "ssh/root@github" ); + assert!( + one_match(entries, "GITHUB", Some("root"), None, 8, true), + "ssh/root@GITHUB" + ); assert!( - no_matches(entries, "gitlab", Some("baz"), None), + no_matches(entries, "gitlab", Some("baz"), None, false), "baz@gitlab" ); assert!( - no_matches(entries, "bitbucket", Some("foo"), None), + no_matches(entries, "GITLAB", Some("baz"), None, true), + "baz@" + ); + assert!( + no_matches(entries, "bitbucket", Some("foo"), None, false), "foo@bitbucket" ); assert!( - no_matches(entries, "github", Some("foo"), Some("bar")), + no_matches(entries, "BITBUCKET", Some("foo"), None, true), + "foo@BITBUCKET" + ); + assert!( + no_matches(entries, "github", Some("foo"), Some("bar"), false), "bar/foo@github" ); assert!( - no_matches(entries, "gitlab", Some("foo"), Some("bar")), + no_matches(entries, "GITHUB", Some("foo"), Some("bar"), true), + "bar/foo@" + ); + assert!( + no_matches(entries, "gitlab", Some("foo"), Some("bar"), false), "bar/foo@gitlab" ); + assert!( + no_matches(entries, "GITLAB", Some("foo"), Some("bar"), true), + "bar/foo@GITLAB" + ); - assert!(many_matches(entries, "gitlab", None, None), "gitlab"); - assert!(many_matches(entries, "gi", Some("foo"), None), "foo@gi"); - assert!(many_matches(entries, "git", Some("ba"), None), "ba@git"); + assert!(many_matches(entries, "gitlab", None, None, false), "gitlab"); + assert!(many_matches(entries, "gitlab", None, None, true), "GITLAB"); + assert!( + many_matches(entries, "gi", Some("foo"), None, false), + "foo@gi" + ); + assert!( + many_matches(entries, "GI", Some("foo"), None, true), + "foo@GI" + ); + assert!( + many_matches(entries, "git", Some("ba"), None, false), + "ba@git" + ); assert!( - many_matches(entries, "github", Some("foo"), Some("s")), + many_matches(entries, "GIT", Some("ba"), None, true), + "ba@GIT" + ); + assert!( + many_matches(entries, "github", Some("foo"), Some("s"), false), "s/foo@github" ); + assert!( + many_matches(entries, "GITHUB", Some("foo"), Some("s"), true), + "s/foo@GITHUB" + ); } #[test] @@ -2130,15 +2388,15 @@ mod test { ]; assert!( - one_match(entries, &entries[0].0.id, None, None, 0), + one_match(entries, &entries[0].0.id, None, None, 0, false), "foo@github" ); assert!( - one_match(entries, &entries[1].0.id, None, None, 1), + one_match(entries, &entries[1].0.id, None, None, 1, false), "foo@gitlab" ); assert!( - one_match(entries, &entries[2].0.id, None, None, 2), + one_match(entries, &entries[2].0.id, None, None, 2, false), "bar@gitlab" ); @@ -2148,7 +2406,8 @@ mod test { &entries[0].0.id.to_uppercase(), None, None, - 0 + 0, + false ), "foo@github" ); @@ -2158,7 +2417,8 @@ mod test { &entries[0].0.id.to_lowercase(), None, None, - 0 + 0, + false ), "foo@github" ); @@ -2185,51 +2445,94 @@ mod test { 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_match(entries, "https://one.com/", None, None, 0, false), + "one" + ); + assert!( + one_match( + entries, + "https://login.one.com/", + None, + None, + 0, + false + ), + "one" + ); + assert!( + one_match(entries, "https://one.com:443/", None, None, 0, false), "one" ); + assert!(no_matches(entries, "one.com", None, None, false), "one"); + assert!(no_matches(entries, "https", None, None, false), "one"); + assert!(no_matches(entries, "com", None, None, false), "one"); assert!( - one_match(entries, "https://one.com:443/", None, None, 0), + no_matches(entries, "https://com/", None, None, false), "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), + one_match(entries, "https://two.com/", None, None, 1, false), + "two" + ); + assert!( + one_match( + entries, + "https://two.com/other-page", + None, + None, + 1, + false + ), "two" ); assert!( - one_match(entries, "https://login.three.com/", None, None, 2), + one_match( + entries, + "https://login.three.com/", + None, + None, + 2, + false + ), "three" ); assert!( - no_matches(entries, "https://three.com/", None, None), + no_matches(entries, "https://three.com/", None, None, false), "three" ); assert!( - one_match(entries, "https://four.com/", None, None, 3), + one_match(entries, "https://four.com/", None, None, 3, false), "four" ); assert!( - one_match(entries, "https://five.com:8080/", None, None, 4), + one_match( + entries, + "https://five.com:8080/", + None, + None, + 4, + false + ), + "five" + ); + assert!( + no_matches(entries, "https://five.com/", None, None, false), "five" ); - assert!(no_matches(entries, "https://five.com/", None, None), "five"); assert!( - one_match(entries, "https://six.com:8080/", None, None, 5), + one_match(entries, "https://six.com:8080/", None, None, 5, false), + "six" + ); + assert!( + no_matches(entries, "https://six.com/", None, None, false), "six" ); - assert!(no_matches(entries, "https://six.com/", None, None), "six"); } #[test] @@ -2282,51 +2585,94 @@ mod test { ), ]; - assert!(one_match(entries, "https://one.com/", None, None, 0), "one"); assert!( - one_match(entries, "https://login.one.com/", None, None, 0), + one_match(entries, "https://one.com/", None, None, 0, false), + "one" + ); + assert!( + one_match( + entries, + "https://login.one.com/", + None, + None, + 0, + false + ), + "one" + ); + assert!( + one_match(entries, "https://one.com:443/", None, None, 0, false), "one" ); + assert!(no_matches(entries, "one.com", None, None, false), "one"); + assert!(no_matches(entries, "https", None, None, false), "one"); + assert!(no_matches(entries, "com", None, None, false), "one"); assert!( - one_match(entries, "https://one.com:443/", None, None, 0), + no_matches(entries, "https://com/", None, None, false), "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), + one_match(entries, "https://two.com/", None, None, 1, false), + "two" + ); + assert!( + one_match( + entries, + "https://two.com/other-page", + None, + None, + 1, + false + ), "two" ); assert!( - one_match(entries, "https://login.three.com/", None, None, 2), + one_match( + entries, + "https://login.three.com/", + None, + None, + 2, + false + ), "three" ); assert!( - no_matches(entries, "https://three.com/", None, None), + no_matches(entries, "https://three.com/", None, None, false), "three" ); assert!( - one_match(entries, "https://four.com/", None, None, 3), + one_match(entries, "https://four.com/", None, None, 3, false), "four" ); assert!( - one_match(entries, "https://five.com:8080/", None, None, 4), + one_match( + entries, + "https://five.com:8080/", + None, + None, + 4, + false + ), + "five" + ); + assert!( + no_matches(entries, "https://five.com/", None, None, false), "five" ); - assert!(no_matches(entries, "https://five.com/", None, None), "five"); assert!( - one_match(entries, "https://six.com:8080/", None, None, 5), + one_match(entries, "https://six.com:8080/", None, None, 5, false), + "six" + ); + assert!( + no_matches(entries, "https://six.com/", None, None, false), "six" ); - assert!(no_matches(entries, "https://six.com/", None, None), "six"); } #[test] @@ -2379,51 +2725,87 @@ mod test { ), ]; - assert!(one_match(entries, "https://one.com/", None, None, 0), "one"); assert!( - no_matches(entries, "https://login.one.com/", None, None), + one_match(entries, "https://one.com/", None, None, 0, false), + "one" + ); + assert!( + no_matches(entries, "https://login.one.com/", None, None, false), "one" ); assert!( - one_match(entries, "https://one.com:443/", None, None, 0), + one_match(entries, "https://one.com:443/", None, None, 0, false), + "one" + ); + assert!(no_matches(entries, "one.com", None, None, false), "one"); + assert!(no_matches(entries, "https", None, None, false), "one"); + assert!(no_matches(entries, "com", None, None, false), "one"); + assert!( + no_matches(entries, "https://com/", None, None, false), "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), + one_match(entries, "https://two.com/", None, None, 1, false), + "two" + ); + assert!( + one_match( + entries, + "https://two.com/other-page", + None, + None, + 1, + false + ), "two" ); assert!( - one_match(entries, "https://login.three.com/", None, None, 2), + one_match( + entries, + "https://login.three.com/", + None, + None, + 2, + false + ), "three" ); assert!( - no_matches(entries, "https://three.com/", None, None), + no_matches(entries, "https://three.com/", None, None, false), "three" ); assert!( - one_match(entries, "https://four.com/", None, None, 3), + one_match(entries, "https://four.com/", None, None, 3, false), "four" ); assert!( - one_match(entries, "https://five.com:8080/", None, None, 4), + one_match( + entries, + "https://five.com:8080/", + None, + None, + 4, + false + ), + "five" + ); + assert!( + no_matches(entries, "https://five.com/", None, None, false), "five" ); - assert!(no_matches(entries, "https://five.com/", None, None), "five"); assert!( - one_match(entries, "https://six.com:8080/", None, None, 5), + one_match(entries, "https://six.com:8080/", None, None, 5, false), + "six" + ); + assert!( + no_matches(entries, "https://six.com/", None, None, false), "six" ); - assert!(no_matches(entries, "https://six.com/", None, None), "six"); } #[test] @@ -2458,40 +2840,69 @@ mod test { ), ]; - assert!(one_match(entries, "https://one.com/", None, None, 0), "one"); assert!( - no_matches(entries, "https://login.one.com/", None, None), + one_match(entries, "https://one.com/", None, None, 0, false), + "one" + ); + assert!( + no_matches(entries, "https://login.one.com/", None, None, false), + "one" + ); + assert!( + one_match(entries, "https://one.com:443/", None, None, 0, false), "one" ); + assert!(no_matches(entries, "one.com", None, None, false), "one"); + assert!(no_matches(entries, "https", None, None, false), "one"); + assert!(no_matches(entries, "com", None, None, false), "one"); assert!( - one_match(entries, "https://one.com:443/", None, None, 0), + no_matches(entries, "https://com/", None, None, false), "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), + one_match(entries, "https://two.com/login", None, None, 1, false), + "two" + ); + assert!( + one_match( + entries, + "https://two.com/login/sso", + None, + None, + 1, + false + ), "two" ); assert!( - one_match(entries, "https://two.com/login/sso", None, None, 1), + no_matches(entries, "https://two.com/", None, None, false), "two" ); - assert!(no_matches(entries, "https://two.com/", None, None), "two"); assert!( - no_matches(entries, "https://two.com/other-page", None, None), + no_matches( + entries, + "https://two.com/other-page", + None, + None, + false + ), "two" ); assert!( - one_match(entries, "https://login.three.com/", None, None, 2), + one_match( + entries, + "https://login.three.com/", + None, + None, + 2, + false + ), "three" ); assert!( - no_matches(entries, "https://three.com/", None, None), + no_matches(entries, "https://three.com/", None, None, false), "three" ); } @@ -2525,40 +2936,68 @@ mod test { ), ]; - assert!(one_match(entries, "https://one.com/", None, None, 0), "one"); assert!( - no_matches(entries, "https://login.one.com/", None, None), + one_match(entries, "https://one.com/", None, None, 0, false), + "one" + ); + assert!( + no_matches(entries, "https://login.one.com/", None, None, false), "one" ); assert!( - one_match(entries, "https://one.com:443/", None, None, 0), + one_match(entries, "https://one.com:443/", None, None, 0, false), + "one" + ); + assert!(no_matches(entries, "one.com", None, None, false), "one"); + assert!(no_matches(entries, "https", None, None, false), "one"); + assert!(no_matches(entries, "com", None, None, false), "one"); + assert!( + no_matches(entries, "https://com/", None, None, false), "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), + one_match(entries, "https://two.com/login", None, None, 1, false), "two" ); assert!( - no_matches(entries, "https://two.com/login/sso", None, None), + no_matches( + entries, + "https://two.com/login/sso", + None, + None, + false + ), + "two" + ); + assert!( + no_matches(entries, "https://two.com/", None, None, false), "two" ); - assert!(no_matches(entries, "https://two.com/", None, None), "two"); assert!( - no_matches(entries, "https://two.com/other-page", None, None), + no_matches( + entries, + "https://two.com/other-page", + None, + None, + false + ), "two" ); assert!( - one_match(entries, "https://login.three.com/", None, None, 2), + one_match( + entries, + "https://login.three.com/", + None, + None, + 2, + false + ), "three" ); assert!( - no_matches(entries, "https://three.com/", None, None), + no_matches(entries, "https://three.com/", None, None, false), "three" ); } @@ -2595,48 +3034,77 @@ mod test { ), ]; - assert!(one_match(entries, "https://one.com/", None, None, 0), "one"); assert!( - no_matches(entries, "https://login.one.com/", None, None), + one_match(entries, "https://one.com/", None, None, 0, false), + "one" + ); + assert!( + no_matches(entries, "https://login.one.com/", None, None, false), + "one" + ); + assert!( + one_match(entries, "https://one.com:443/", None, None, 0, false), "one" ); + assert!(no_matches(entries, "one.com", None, None, false), "one"); + assert!(no_matches(entries, "https", None, None, false), "one"); + assert!(no_matches(entries, "com", None, None, false), "one"); assert!( - one_match(entries, "https://one.com:443/", None, None, 0), + no_matches(entries, "https://com/", None, None, false), "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), + one_match(entries, "https://two.com/login", None, None, 1, false), "two" ); assert!( - one_match(entries, "https://two.com/start", None, None, 1), + one_match(entries, "https://two.com/start", None, None, 1, false), "two" ); assert!( - one_match(entries, "https://two.com/login/sso", None, None, 1), + one_match( + entries, + "https://two.com/login/sso", + None, + None, + 1, + false + ), "two" ); - assert!(no_matches(entries, "https://two.com/", None, None), "two"); assert!( - no_matches(entries, "https://two.com/other-page", None, None), + no_matches(entries, "https://two.com/", None, None, false), + "two" + ); + assert!( + no_matches( + entries, + "https://two.com/other-page", + None, + None, + false + ), "two" ); assert!( - one_match(entries, "https://login.three.com/", None, None, 2), + one_match( + entries, + "https://login.three.com/", + None, + None, + 2, + false + ), "three" ); assert!( - one_match(entries, "https://three.com/", None, None, 2), + one_match(entries, "https://three.com/", None, None, 2, false), "three" ); assert!( - no_matches(entries, "https://www.three.com/", None, None), + no_matches(entries, "https://www.three.com/", None, None, false), "three" ); } @@ -2691,48 +3159,78 @@ mod test { ), ]; - assert!(no_matches(entries, "https://one.com/", None, None), "one"); assert!( - no_matches(entries, "https://login.one.com/", None, None), + no_matches(entries, "https://one.com/", None, None, false), + "one" + ); + assert!( + no_matches(entries, "https://login.one.com/", None, None, false), "one" ); assert!( - no_matches(entries, "https://one.com:443/", None, None), + no_matches(entries, "https://one.com:443/", None, None, false), + "one" + ); + assert!(no_matches(entries, "one.com", None, None, false), "one"); + assert!(no_matches(entries, "https", None, None, false), "one"); + assert!(no_matches(entries, "com", None, None, false), "one"); + assert!( + no_matches(entries, "https://com/", None, None, false), "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), + no_matches(entries, "https://two.com/", None, None, false), + "two" + ); + assert!( + no_matches( + entries, + "https://two.com/other-page", + None, + None, + false + ), "two" ); assert!( - no_matches(entries, "https://login.three.com/", None, None), + no_matches( + entries, + "https://login.three.com/", + None, + None, + false + ), "three" ); assert!( - no_matches(entries, "https://three.com/", None, None), + no_matches(entries, "https://three.com/", None, None, false), "three" ); - assert!(no_matches(entries, "https://four.com/", None, None), "four"); + assert!( + no_matches(entries, "https://four.com/", None, None, false), + "four" + ); assert!( - no_matches(entries, "https://five.com:8080/", None, None), + no_matches(entries, "https://five.com:8080/", None, None, false), + "five" + ); + assert!( + no_matches(entries, "https://five.com/", None, None, false), "five" ); - assert!(no_matches(entries, "https://five.com/", None, None), "five"); assert!( - no_matches(entries, "https://six.com:8080/", None, None), + no_matches(entries, "https://six.com:8080/", None, None, false), + "six" + ); + assert!( + no_matches(entries, "https://six.com/", None, None, false), "six" ); - assert!(no_matches(entries, "https://six.com/", None, None), "six"); } #[track_caller] @@ -2742,6 +3240,7 @@ mod test { username: Option<&str>, folder: Option<&str>, idx: usize, + ignore_case: bool, ) -> bool { entries_eq( &find_entry_raw( @@ -2749,6 +3248,7 @@ mod test { &parse_needle(needle).unwrap(), username, folder, + ignore_case, ) .unwrap(), &entries[idx], @@ -2761,12 +3261,14 @@ mod test { needle: &str, username: Option<&str>, folder: Option<&str>, + ignore_case: bool, ) -> bool { let res = find_entry_raw( entries, &parse_needle(needle).unwrap(), username, folder, + ignore_case, ); if let Err(e) = res { format!("{e}").contains("no entry found") @@ -2781,12 +3283,14 @@ mod test { needle: &str, username: Option<&str>, folder: Option<&str>, + ignore_case: bool, ) -> bool { let res = find_entry_raw( entries, &parse_needle(needle).unwrap(), username, folder, + ignore_case, ); if let Err(e) = res { format!("{e}").contains("multiple entries found") diff --git a/src/bin/rbw/main.rs b/src/bin/rbw/main.rs index 2fb96bf..e30c3a9 100644 --- a/src/bin/rbw/main.rs +++ b/src/bin/rbw/main.rs @@ -85,6 +85,16 @@ enum Opt { raw: bool, #[structopt(long, help = "Copy result to clipboard")] clipboard: bool, + #[structopt(short, long, help = "Ignore case")] + ignorecase: bool, + }, + + #[command(about = "Search for entries")] + Search { + #[arg(help = "Search term to locate entries")] + term: String, + #[arg(long, help = "Folder name to search in")] + folder: Option<String>, }, #[command( @@ -100,6 +110,8 @@ enum Opt { folder: Option<String>, #[structopt(long, help = "Copy result to clipboard")] clipboard: bool, + #[arg(short, long, help = "Ignore case")] + ignorecase: bool, }, #[command( @@ -198,6 +210,8 @@ enum Opt { user: Option<String>, #[arg(long, help = "Folder name to search in")] folder: Option<String>, + #[arg(short, long, help = "Ignore case")] + ignorecase: bool, }, #[command(about = "Remove a given entry", visible_alias = "rm")] @@ -208,6 +222,8 @@ enum Opt { user: Option<String>, #[arg(long, help = "Folder name to search in")] folder: Option<String>, + #[arg(short, long, help = "Ignore case")] + ignorecase: bool, }, #[command(about = "View the password history for a given entry")] @@ -218,6 +234,8 @@ enum Opt { user: Option<String>, #[arg(long, help = "Folder name to search in")] folder: Option<String>, + #[arg(short, long, help = "Ignore case")] + ignorecase: bool, }, #[command(about = "Lock the password database")] @@ -249,6 +267,7 @@ impl Opt { Self::Sync => "sync".to_string(), Self::List { .. } => "list".to_string(), Self::Get { .. } => "get".to_string(), + Self::Search { .. } => "search".to_string(), Self::Code { .. } => "code".to_string(), Self::Add { .. } => "add".to_string(), Self::Generate { .. } => "generate".to_string(), @@ -330,6 +349,7 @@ fn main() { full, raw, clipboard, + ignorecase, } => commands::get( needle, user.as_deref(), @@ -338,17 +358,23 @@ fn main() { *full, *raw, *clipboard, + *ignorecase, ), + Opt::Search { term, folder } => { + commands::search(term, folder.as_deref()) + } Opt::Code { needle, user, folder, clipboard, + ignorecase, } => commands::code( needle, user.as_deref(), folder.as_deref(), *clipboard, + *ignorecase, ), Opt::Add { name, @@ -400,15 +426,39 @@ fn main() { ty, ) } - Opt::Edit { name, user, folder } => { - commands::edit(name, user.as_deref(), folder.as_deref()) - } - Opt::Remove { name, user, folder } => { - commands::remove(name, user.as_deref(), folder.as_deref()) - } - Opt::History { name, user, folder } => { - commands::history(name, user.as_deref(), folder.as_deref()) - } + Opt::Edit { + name, + user, + folder, + ignorecase, + } => commands::edit( + name, + user.as_deref(), + folder.as_deref(), + *ignorecase, + ), + Opt::Remove { + name, + user, + folder, + ignorecase, + } => commands::remove( + name, + user.as_deref(), + folder.as_deref(), + *ignorecase, + ), + Opt::History { + name, + user, + folder, + ignorecase, + } => commands::history( + name, + user.as_deref(), + folder.as_deref(), + *ignorecase, + ), Opt::Lock => commands::lock(), Opt::Purge => commands::purge(), Opt::StopAgent => commands::stop_agent(), |