use std::{ collections::HashMap, path::{Component, Path, PathBuf}, }; /// Maps paths to access rules. #[derive(Default, Debug)] pub struct Shares { pub exact_rules: HashMap, pub prefix_rules: HashMap, } impl Shares { pub fn rules_iter<'a>(&'a self, path: &'a str) -> RulesIter { RulesIter { shares: self, path, state: RulesIterState::Exact, } } pub fn find_mode<'a>(&'a self, path: &'a str, username: &str) -> Option<&AccessMode> { for ruleset in self.rules_iter(path) { if let Some(rule) = ruleset.0.get(username) { return Some(&rule.mode); } } None } } /// Maps usernames to access modes and .shares.txt source line. #[derive(Default, Debug)] pub struct AccessRuleset(pub HashMap); #[derive(Debug)] pub struct AccessRule { pub mode: AccessMode, pub line: usize, pub start: usize, pub end: usize, } #[derive(PartialEq, Debug)] pub enum AccessMode { ReadAndWrite, ReadOnly, } #[derive(Debug)] enum RulesIterState { Exact, Prefix(usize), Done, } #[derive(Debug)] pub struct RulesIter<'a> { shares: &'a Shares, path: &'a str, state: RulesIterState, } impl<'a> Iterator for RulesIter<'a> { type Item = &'a AccessRuleset; fn next(&mut self) -> Option { match self.state { RulesIterState::Exact => { self.state = RulesIterState::Prefix(self.path.len()); self.shares .exact_rules .get(self.path) .or_else(|| self.next()) } RulesIterState::Prefix(idx) => { let path = &self.path[..idx]; if let Some(new_idx) = path.rfind('/') { self.state = RulesIterState::Prefix(new_idx); } else { self.state = RulesIterState::Done; } self.shares.prefix_rules.get(path).or_else(|| self.next()) } RulesIterState::Done => None, } } } pub fn parse_shares_txt(text: &str) -> Result { let mut exact_rules: HashMap = HashMap::new(); let mut prefix_rules: HashMap = HashMap::new(); let mut start; let mut next = 0; for (idx, line) in text.lines().enumerate() { start = next; next = start + line.chars().count() + 1; if line.starts_with('#') || line.trim_end().is_empty() { continue; } let mut iter = line.split('#').next().unwrap().split_ascii_whitespace(); let permissions = iter.next().unwrap(); let path = iter .next() .ok_or_else(|| format!("line #{} expected three values", idx + 1))?; let username = iter .next() .ok_or_else(|| format!("line #{} expected three values", idx + 1))? .trim_end(); if iter.next().is_some() { return Err(format!( "line #{} unexpected fourth argument (paths with spaces are unsupported)", idx + 1 )); } if permissions != "r" && permissions != "w" { return Err(format!("line #{} must start with r or w", idx + 1)); } if username.is_empty() { return Err(format!("line #{} empty username", idx + 1)); } if path.is_empty() { return Err(format!("line #{} empty path", idx + 1)); } if let (Some(first_ast), Some(last_ast)) = (path.find('*'), path.rfind('*')) { if first_ast != last_ast || !path.ends_with("/*") { return Err(format!( "line #{} wildcards in paths may only occur at the very end as /*", idx + 1 )); } } let rule = AccessRule { mode: if permissions == "w" { AccessMode::ReadAndWrite } else { AccessMode::ReadOnly }, line: idx + 1, start, end: next, }; // normalize path let mut comps = Vec::new(); for comp in Path::new(&*path).components() { match comp { Component::Normal(name) => comps.push(name), Component::ParentDir => { return Err(format!("line #{}: path may not contain ../", idx + 1)) } _ => {} } } let path: PathBuf = comps.iter().collect(); let path = path.to_str().unwrap(); if let Some(stripped) = path.strip_suffix("/*") { let pr = prefix_rules.entry(stripped.to_owned()).or_default(); if let Some(prevrule) = pr.0.get(username) { if rule.mode != prevrule.mode { return Err(format!( "line #{} conflicts with line #{} ({} {})", idx + 1, line, username, path )); } } pr.0.insert(username.to_string(), rule); } else { let ac = exact_rules.entry(path.to_owned()).or_default(); if let Some(prevrule) = ac.0.get(username) { if rule.mode != prevrule.mode { return Err(format!( "line #{} conflicts with line #{} ({} {})", idx + 1, line, username, path )); } } ac.0.insert(username.to_string(), rule); } } Ok(Shares { exact_rules, prefix_rules, }) }