304 lines
8.2 KiB
Rust
304 lines
8.2 KiB
Rust
use crate::config::{ConnectionProfile, ConnectionType};
|
|
use std::fs;
|
|
|
|
pub fn matches_search(name: &str, profile: &ConnectionProfile, query: &str) -> bool {
|
|
let query = query.trim().to_lowercase();
|
|
if query.is_empty() {
|
|
return true;
|
|
}
|
|
if name.to_lowercase().contains(&query) {
|
|
return true;
|
|
}
|
|
if profile
|
|
.tags
|
|
.iter()
|
|
.chain(profile.local_tags.iter())
|
|
.any(|t| t.to_lowercase().contains(&query))
|
|
{
|
|
return true;
|
|
}
|
|
match &profile.kind {
|
|
ConnectionType::Ssh { host, user, .. } => {
|
|
host.to_lowercase().contains(&query)
|
|
|| format!("{user}@{host}").to_lowercase().contains(&query)
|
|
}
|
|
ConnectionType::Shell { command, .. } => command.to_lowercase().contains(&query),
|
|
}
|
|
}
|
|
|
|
pub fn smart_score(profile: &ConnectionProfile) -> u64 {
|
|
profile.usage_count.saturating_mul(10_000) + profile.added_order
|
|
}
|
|
|
|
impl ConnectionProfile {
|
|
pub fn auth_ref(&self) -> Option<&str> {
|
|
match &self.kind {
|
|
ConnectionType::Ssh { auth_ref, .. } => {
|
|
if auth_ref.is_empty() {
|
|
None
|
|
} else {
|
|
Some(auth_ref)
|
|
}
|
|
}
|
|
ConnectionType::Shell { auth_ref, .. } => auth_ref.as_deref(),
|
|
}
|
|
}
|
|
|
|
pub fn auth_ref_mut(&mut self) -> Option<&mut String> {
|
|
match &mut self.kind {
|
|
ConnectionType::Ssh { auth_ref, .. } => {
|
|
if auth_ref.is_empty() {
|
|
None
|
|
} else {
|
|
Some(auth_ref)
|
|
}
|
|
}
|
|
ConnectionType::Shell { auth_ref, .. } => auth_ref.as_mut(),
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn resolve_secret(secret: &str) -> String {
|
|
let path = crate::config::expand_user_path(secret);
|
|
if !looks_like_private_key(secret)
|
|
&& path.is_file()
|
|
&& let Ok(content) = fs::read_to_string(&path)
|
|
{
|
|
return content;
|
|
}
|
|
secret.to_string()
|
|
}
|
|
|
|
pub fn non_empty(value: &str, fallback: &str) -> String {
|
|
let value = value.trim();
|
|
if value.is_empty() {
|
|
fallback.to_string()
|
|
} else {
|
|
value.to_string()
|
|
}
|
|
}
|
|
|
|
pub fn shell_name_for_command(command: &str) -> String {
|
|
let command = non_empty(command, "bash");
|
|
std::path::Path::new(&command)
|
|
.file_name()
|
|
.and_then(|value| value.to_str())
|
|
.unwrap_or(command.as_str())
|
|
.to_string()
|
|
}
|
|
|
|
pub fn split_args(raw: &str) -> Vec<String> {
|
|
let mut out = Vec::new();
|
|
let mut current = String::new();
|
|
let mut quote = None;
|
|
let mut escaped = false;
|
|
|
|
for ch in raw.chars() {
|
|
if escaped {
|
|
current.push(ch);
|
|
escaped = false;
|
|
continue;
|
|
}
|
|
if ch == '\\' {
|
|
escaped = true;
|
|
continue;
|
|
}
|
|
if let Some(q) = quote {
|
|
if ch == q {
|
|
quote = None;
|
|
} else {
|
|
current.push(ch);
|
|
}
|
|
continue;
|
|
}
|
|
match ch {
|
|
'\'' | '"' => quote = Some(ch),
|
|
ch if ch.is_whitespace() => {
|
|
if !current.is_empty() {
|
|
out.push(std::mem::take(&mut current));
|
|
}
|
|
}
|
|
_ => current.push(ch),
|
|
}
|
|
}
|
|
|
|
if escaped {
|
|
current.push('\\');
|
|
}
|
|
if !current.is_empty() {
|
|
out.push(current);
|
|
}
|
|
out
|
|
}
|
|
|
|
pub fn looks_like_private_key(value: &str) -> bool {
|
|
value.contains("BEGIN ") && value.contains("PRIVATE KEY")
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn split_args_empty() {
|
|
assert_eq!(split_args(""), Vec::<String>::new());
|
|
}
|
|
|
|
#[test]
|
|
fn split_args_simple() {
|
|
assert_eq!(split_args("hello world foo"), vec!["hello", "world", "foo"]);
|
|
}
|
|
|
|
#[test]
|
|
fn split_args_single_quotes() {
|
|
assert_eq!(
|
|
split_args("hello 'world baz' foo"),
|
|
vec!["hello", "world baz", "foo"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn split_args_double_quotes() {
|
|
assert_eq!(
|
|
split_args("hello \"world baz\" foo"),
|
|
vec!["hello", "world baz", "foo"]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn split_args_escaped() {
|
|
assert_eq!(split_args(r"hello\ world foo"), vec!["hello world", "foo"]);
|
|
}
|
|
|
|
#[test]
|
|
fn split_args_trailing_backslash() {
|
|
assert_eq!(split_args(r"hello\"), vec!["hello\\"]);
|
|
}
|
|
|
|
#[test]
|
|
fn looks_like_private_key_positive() {
|
|
assert!(looks_like_private_key(
|
|
"-----BEGIN OPENSSH PRIVATE KEY-----"
|
|
));
|
|
assert!(looks_like_private_key(
|
|
"-----BEGIN RSA PRIVATE KEY-----\nsome data"
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn looks_like_private_key_negative() {
|
|
assert!(!looks_like_private_key("just a password"));
|
|
assert!(!looks_like_private_key("BEGIN something"));
|
|
}
|
|
|
|
#[test]
|
|
fn smart_score_basic() {
|
|
let make_profile = |usage: u64, order: u64| ConnectionProfile {
|
|
tags: vec![],
|
|
local_tags: vec![],
|
|
source: crate::config::ConnectionSource::Manual,
|
|
added_order: order,
|
|
usage_count: usage,
|
|
kind: crate::config::ConnectionType::Ssh {
|
|
host: "h".into(),
|
|
port: 22,
|
|
user: "u".into(),
|
|
auth_ref: "a".into(),
|
|
sync: true,
|
|
},
|
|
};
|
|
assert_eq!(smart_score(&make_profile(0, 1)), 1);
|
|
assert_eq!(smart_score(&make_profile(3, 5)), 30_005);
|
|
assert!(smart_score(&make_profile(5, 1)) > smart_score(&make_profile(1, 5)));
|
|
}
|
|
|
|
#[test]
|
|
fn matches_search_by_name() {
|
|
let profile = ConnectionProfile {
|
|
tags: vec![],
|
|
local_tags: vec![],
|
|
source: crate::config::ConnectionSource::Manual,
|
|
added_order: 1,
|
|
usage_count: 0,
|
|
kind: ConnectionType::Ssh {
|
|
host: "example.com".into(),
|
|
port: 22,
|
|
user: "admin".into(),
|
|
auth_ref: "a".into(),
|
|
sync: true,
|
|
},
|
|
};
|
|
assert!(matches_search("myserver", &profile, "myserver"));
|
|
assert!(matches_search("myserver", &profile, "My"));
|
|
assert!(!matches_search("myserver", &profile, "other"));
|
|
}
|
|
|
|
#[test]
|
|
fn matches_search_by_host() {
|
|
let profile = ConnectionProfile {
|
|
tags: vec![],
|
|
local_tags: vec![],
|
|
source: crate::config::ConnectionSource::Manual,
|
|
added_order: 1,
|
|
usage_count: 0,
|
|
kind: ConnectionType::Ssh {
|
|
host: "example.com".into(),
|
|
port: 22,
|
|
user: "admin".into(),
|
|
auth_ref: "a".into(),
|
|
sync: true,
|
|
},
|
|
};
|
|
assert!(matches_search("x", &profile, "example"));
|
|
assert!(matches_search("x", &profile, "admin@example"));
|
|
}
|
|
|
|
#[test]
|
|
fn matches_search_empty_query() {
|
|
let profile = ConnectionProfile {
|
|
tags: vec![],
|
|
local_tags: vec![],
|
|
source: crate::config::ConnectionSource::Manual,
|
|
added_order: 1,
|
|
usage_count: 0,
|
|
kind: ConnectionType::Ssh {
|
|
host: "h".into(),
|
|
port: 22,
|
|
user: "u".into(),
|
|
auth_ref: "a".into(),
|
|
sync: true,
|
|
},
|
|
};
|
|
assert!(matches_search("anything", &profile, ""));
|
|
}
|
|
|
|
#[test]
|
|
fn shell_name_for_command_path() {
|
|
assert_eq!(shell_name_for_command("/bin/bash"), "bash");
|
|
assert_eq!(shell_name_for_command("/usr/bin/zsh"), "zsh");
|
|
}
|
|
|
|
#[test]
|
|
fn shell_name_for_command_empty() {
|
|
assert_eq!(shell_name_for_command(""), "bash");
|
|
}
|
|
|
|
#[test]
|
|
fn shell_name_for_command_bare() {
|
|
assert_eq!(shell_name_for_command("bash"), "bash");
|
|
}
|
|
|
|
#[test]
|
|
fn shell_name_for_command_windows_path() {
|
|
// Use forward slashes so Path::file_name() works on all platforms
|
|
assert_eq!(
|
|
shell_name_for_command("C:/Windows/System32/cmd.exe"),
|
|
"cmd.exe"
|
|
);
|
|
assert_eq!(
|
|
shell_name_for_command("C:/Program Files/Git/bin/bash.exe"),
|
|
"bash.exe"
|
|
);
|
|
}
|
|
}
|