diff options
Diffstat (limited to 'src/shares.rs')
-rw-r--r-- | src/shares.rs | 199 |
1 files changed, 199 insertions, 0 deletions
diff --git a/src/shares.rs b/src/shares.rs new file mode 100644 index 0000000..9cfe156 --- /dev/null +++ b/src/shares.rs @@ -0,0 +1,199 @@ +use std::{ + collections::HashMap, + path::{Component, Path, PathBuf}, +}; + +/// Maps paths to access rules. +#[derive(Default, Debug)] +pub struct Shares { + pub exact_rules: HashMap<String, AccessRuleset>, + pub prefix_rules: HashMap<String, AccessRuleset>, +} + +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<String, AccessRule>); + +#[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<Self::Item> { + 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<Shares, String> { + let mut exact_rules: HashMap<String, AccessRuleset> = HashMap::new(); + let mut prefix_rules: HashMap<String, AccessRuleset> = 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, + }) +} |