use std::cmp::min;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::collections::HashSet;
use glob::Pattern;
use sha1::Sha1;
use sha1::Digest;
use url::Url;
pub use shvproto::util::parse_log_verbosity;
pub fn sha1_hash(data: &[u8]) -> String {
let mut hasher = Sha1::new();
hasher.update(data);
let result = hasher.finalize();
hex::encode(&result[..])
}
pub fn sha1_password_hash(password: &[u8], nonce: &[u8]) -> String {
let mut nonce_pass= nonce.to_vec();
nonce_pass.append(&mut sha1_hash(password).into_bytes());
sha1_hash(&nonce_pass)
}
pub fn join_path(p1: impl AsRef<str>, p2: impl AsRef<str>) -> String {
let (p1, p2) = (p1.as_ref(), p2.as_ref());
if p1.is_empty() && p2.is_empty() {
"".to_string()
} else if p1.is_empty() {
p2.trim_matches('/').to_string()
} else if p2.is_empty() {
p1.trim_matches('/').to_string()
} else {
p1.trim_matches('/').to_string() + "/" + p2.trim_matches('/')
}
}
#[macro_export]
macro_rules! join_path {
( $first:expr $(, $rest:expr )+ ) => {{
let mut path = $first.to_string();
$(
path = $crate::util::join_path(&path, $rest);
)+
path
}};
}
pub fn starts_with_path(shv_path: impl AsRef<str>, with_path: impl AsRef<str>) -> bool {
let (shv_path, with_path) = (shv_path.as_ref(), with_path.as_ref());
let with_path_without_trailing_slash = with_path.strip_suffix('/').unwrap_or(with_path);
if with_path_without_trailing_slash.is_empty() {
return true
}
#[expect(clippy::string_slice, reason = "We expect UTF-8 strings")]
let res = shv_path.starts_with(with_path_without_trailing_slash)
&& (shv_path.len() == with_path_without_trailing_slash.len() || shv_path[with_path_without_trailing_slash.len() ..].starts_with('/'));
res
}
pub fn strip_prefix_path<'a>(path: &'a str, prefix: &str) -> Option<&'a str> {
let strip = path.strip_prefix(prefix)?;
if strip.is_empty() {
Some(strip)
} else {
strip.strip_prefix('/').or(if prefix.is_empty() {
Some(strip)
} else {
None
})
}
}
pub struct LoginQueryParams {
pub user: String,
pub password: String,
pub token: String,
pub session: bool,
}
pub fn parse_query_params(url: &Url) -> LoginQueryParams {
let mut user = "".to_string();
let mut password = "".to_string();
let mut token = "".to_string();
let mut session = false;
for (key,val) in url.query_pairs() {
match key.as_ref() {
"user" => user = val.to_string(),
"password" => password = val.to_string(),
"token" => token = val.to_string(),
"session" => session = true,
unknown_param => log::warn!("Unsupported URL query param: {unknown_param}={val}"),
}
}
if user.is_empty() {
user = url.username().to_string();
}
if password.is_empty() {
password = url.password().unwrap_or_default().to_string();
}
LoginQueryParams {
user,
password,
token,
session,
}
}
pub fn glob_len(glob: &str) -> usize {
glob.split('/').count()
}
pub fn left_glob(glob: &str, glob_len: usize) -> Option<&str> {
let mut ix: usize = 0;
let mut n: usize = 0;
for p in glob.splitn(glob_len + 1, '/') {
ix += p.len();
n += 1;
if n == glob_len {
break
}
}
if n == glob_len {
ix += n - 1; #[expect(clippy::string_slice, reason = "We expect UTF-8 strings")]
Some(&glob[0..ix])
} else {
None
}
}
pub fn split_glob_on_match<'a>(glob_pattern: &'a str, shv_path: &str) -> Result<Option<(&'a str, &'a str)>, String> {
if glob_pattern.is_empty() {
return Ok(None);
}
let globstar_pos = glob_pattern.find("**");
#[expect(clippy::string_slice, reason = "We expect UTF-8 strings")]
let pattern1 = globstar_pos.map_or(glob_pattern, |ix| if ix == 0 { "" } else { &glob_pattern[0 .. (ix - 1)] });
if globstar_pos.is_some() && pattern1.is_empty() {
return Ok(Some(("**", glob_pattern)))
}
if pattern1.is_empty() {
return Err("Valid glob pattern cannot be empty".into())
}
if shv_path.is_empty() {
return Err("Valid mount point cannot be empty".into())
}
let shv_path_glen = glob_len(shv_path);
let pattern1_glen = glob_len(pattern1);
let match_len = min(shv_path_glen, pattern1_glen);
let trimmed_pattern1 = left_glob(pattern1, match_len).expect("We check that the segment count matches");
let trimmed_path = left_glob(shv_path, match_len).expect("We check that the segment count matches");
let pattern = Pattern::new(trimmed_pattern1).map_err(|err| err.to_string())?;
if pattern.matches(trimmed_path) {
globstar_pos.map_or_else(|| match shv_path_glen.cmp(&pattern1_glen) {
std::cmp::Ordering::Greater => Ok(None),
std::cmp::Ordering::Equal => Ok(Some((trimmed_pattern1, ""))),
#[expect(clippy::string_slice, reason = "We expect UTF-8 strings")]
std::cmp::Ordering::Less => Ok(Some((trimmed_pattern1, &glob_pattern[(trimmed_pattern1.len()+1) .. ]))),
}, |ix| if shv_path_glen > pattern1_glen {
#[expect(clippy::string_slice, reason = "We expect UTF-8 strings")]
Ok(Some((&glob_pattern[0 .. (ix+2)], &glob_pattern[ix ..])))
} else {
#[expect(clippy::string_slice, reason = "We expect UTF-8 strings")]
Ok(Some((trimmed_pattern1, &glob_pattern[trimmed_pattern1.len()+1 ..])))
})
} else {
Ok(None)
}
}
pub fn hex_string(data: &[u8], delim: Option<&str>) -> String {
let mut ret = "".to_string();
for b in data {
if let Some(delim) = delim
&& ret.len() > 1 {
ret += delim;
}
ret += &format!("{b:02x}");
}
ret
}
pub fn children_on_path<V>(mounts: &BTreeMap<String, V>, path: &str) -> Option<Vec<String>> {
let mut dirs: Vec<String> = Vec::new();
let mut unique_dirs: HashSet<String> = HashSet::new();
let mut dir_exists = mounts.contains_key(path);
for (key, _) in mounts.range(path.to_owned()..) {
if key.starts_with(path) {
if path.is_empty() || (key.len() > path.len() && *key.as_bytes().get(path.len()).expect("We check the len") == (b'/')) {
dir_exists = true;
let dir_rest_start = if path.is_empty() { 0 } else { path.len() + 1 };
let mut updirs = key.get(dir_rest_start..).expect("We check the bounds").split('/');
if let Some(dir) = updirs.next()
&& !dir.is_empty() && !unique_dirs.contains(dir) {
dirs.push(dir.to_string());
unique_dirs.insert(dir.to_string());
}
}
} else {
break;
}
}
if dir_exists {
Some(dirs)
} else {
None
}
}
pub trait StringMapView<V> {
fn contains_key_(&self, key: &str) -> bool;
}
impl<V> StringMapView<V> for BTreeMap<String, V> {
fn contains_key_(&self, key: &str) -> bool {
self.contains_key(key)
}
}
impl<V, S: std::hash::BuildHasher> StringMapView<V> for HashMap<String, V, S> {
fn contains_key_(&self, key: &str) -> bool {
self.contains_key(key)
}
}
pub fn find_longest_path_prefix<'a, V>(
map: &impl StringMapView<V>,
shv_path: &'a str,
) -> Option<(&'a str, &'a str)> {
let mut path = shv_path;
let mut rest = "";
loop {
if map.contains_key_(path) {
return Some((path, rest));
}
if path.is_empty() {
break;
}
#[expect(clippy::string_slice, reason = "We expect UTF-8 strings")]
if let Some(slash_ix) = path.rfind('/') {
path = &shv_path[..slash_ix];
rest = &shv_path[(slash_ix + 1)..];
} else {
path = "";
rest = shv_path;
};
}
None
}
#[cfg(test)]
mod tests {
use std::collections::BTreeMap;
use log::error;
use crate::util::{glob_len, left_glob, split_glob_on_match, starts_with_path, strip_prefix_path};
fn init_log() {
env_logger::builder()
.is_test(true)
.try_init()
.inspect_err(|err| error!("Logger didn't work: {err}"))
.ok();
}
#[test]
fn test_glob_len() {
let data = vec![
("", 1usize),
("/", 2usize),
("a", 1usize),
("a/b/c", 3usize),
("a/b/", 3usize),
];
for (g, n) in data {
assert_eq!(glob_len(g), n);
}
}
#[test]
fn test_left_glob() {
let data = vec![
("", 1usize, Some("")),
("a", 1usize, Some("a")),
("a", 2usize, None),
("a/b", 1usize, Some("a")),
("a/b", 2usize, Some("a/b")),
("a/b", 3usize, None),
];
for (glob, len, trimmed) in data {
assert_eq!(left_glob(glob, len), trimmed);
}
}
#[test]
fn test_split_glob_on_match() {
let data = vec![
("", "a/b/c", None),
("a", "a/b/c", None),
("a/b", "a/b/c", None),
("a/b/c", "a/b/c", Some(("a/b/c", ""))),
("a/b/c/d", "a/b/c", Some(("a/b/c", "d"))),
("a/b/c", "a", Some(("a", "b/c"))),
("a/b/c", "a/b", Some(("a/b", "c"))),
("a/b/c", "a/b/c/d", None),
("a/b/c", "a/b/d", None),
("**", "a/b/c", Some(("**", "**"))),
("a/**", "a/b/c", Some(("a/**", "**"))),
("a/**/c", "a/b/c", Some(("a/**", "**/c"))),
("a/b/c/**", "a/b/c", Some(("a/b/c", "**"))),
("a/b*/c/**", "a/b/c", Some(("a/b*/c", "**"))),
("?/b*/c/**", "a/b/c", Some(("?/b*/c", "**"))),
("a/b/c/**/d/e/**", "a/b/c", Some(("a/b/c", "**/d/e/**"))),
("**/a/b", "a/b/c", Some(("**", "**/a/b"))),
];
for (glob, path, result) in data {
assert_eq!(split_glob_on_match(glob, path), Ok(result));
}
}
#[test]
fn test_start_with_path() {
let data = vec![
("", "", true),
("a", "", true),
("", "a", false),
("a/b/c", "a/b/c", true),
("a/b/c", "a/b/", true),
("a/b/c", "a/b", true),
("a/b/c", "b/b", false),
];
for (path, with_path, res) in data {
assert_eq!(starts_with_path(path, with_path), res);
}
}
#[test]
fn test_strip_path() {
init_log();
let data = vec![
("", "", Some("")),
("", "/", Some("")),
("", "/a", Some("a")),
("", "a", Some("a")),
("/", "", None),
("a", "", None),
("a/", "a", None),
("a/b/c", "a/b/c", Some("")),
("a/b/", "a/b/c", None),
("a/b", "a/b/c", Some("c")),
("b/b", "a/b/c", None),
("a", "abc", None),
("a/b", "a/bc", None),
];
for (prefix, path, res) in data {
assert_eq!(strip_prefix_path(path, prefix), res);
}
}
#[test]
fn join_multiple_path_segments() {
let foo = "foo".to_string();
let bar = "bar".to_string();
let baz = "baz".to_string();
assert_eq!(join_path!("foo", "bar", "baz"), "foo/bar/baz".to_string());
assert_eq!(join_path!(foo, "bar", "baz"), "foo/bar/baz".to_string());
assert_eq!(join_path!("foo", bar.clone(), "baz"), "foo/bar/baz".to_string());
assert_eq!(join_path!(foo, bar, baz), "foo/bar/baz".to_string());
}
#[test]
fn ls_mounts() {
#[expect(clippy::zero_sized_map_values, reason = "Fine for tests, and it can't be a Set")]
let mut mounts = BTreeMap::new();
mounts.insert(".broker".into(), ());
mounts.insert(".broker/client/1".into(), ());
mounts.insert(".broker/client/2".into(), ());
mounts.insert(".broker/currentClient".into(), ());
mounts.insert("test/device".into(), ());
mounts.insert("test/demo-device/x".into(), ());
mounts.insert("test/demo/y".into(), ());
assert_eq!(super::find_longest_path_prefix(&mounts, ".broker/client"), Some((".broker", "client")));
assert_eq!(super::find_longest_path_prefix(&mounts, "test"), None);
assert_eq!(super::find_longest_path_prefix(&mounts, "test/devic"), None);
assert_eq!(super::find_longest_path_prefix(&mounts, "test/device"), Some(("test/device", "")));
mounts.insert("".into(), ());
assert_eq!(super::find_longest_path_prefix(&mounts, "test"), Some(("", "test")));
assert_eq!(super::find_longest_path_prefix(&mounts, "test/devic"), Some(("", "test/devic")));
assert_eq!(super::children_on_path(&mounts, ""), Some(vec![".broker".to_string(), "test".to_string()]));
assert_eq!(super::children_on_path(&mounts, ".broker"), Some(vec!["client".to_string(), "currentClient".to_string()]));
assert_eq!(super::children_on_path(&mounts, ".broker/client"), Some(vec!["1".to_string(), "2".to_string()]));
assert_eq!(super::children_on_path(&mounts, "test"), Some(vec!["demo-device".to_string(), "demo".to_string(), "device".to_string()]));
assert_eq!(super::children_on_path(&mounts, ".broker/currentClient"), Some(vec![]));
assert_eq!(super::children_on_path(&mounts, "test/device/1"), None);
assert_eq!(super::children_on_path(&mounts, "test1"), None);
assert_eq!(super::children_on_path(&mounts, "test/devic"), None);
assert_eq!(super::children_on_path(&mounts, "test/demo-device"), Some(vec!["x".to_string()]));
assert_eq!(super::children_on_path(&mounts, "test/demo"), Some(vec!["y".to_string()]));
}
}