aboutsummaryrefslogtreecommitdiff
path: root/src/shares.rs
diff options
context:
space:
mode:
authorMartin Fischer <martin@push-f.com>2021-06-23 14:42:39 +0200
committerMartin Fischer <martin@push-f.com>2021-06-23 14:42:39 +0200
commitcde4098bb72e164524d403f626dde96913d83316 (patch)
tree07ba546af32fe291ee2778941f4cb91243bdb37c /src/shares.rs
parent3ed32ef268b54965c97b13efdf24a1539c4ec1c1 (diff)
refactor: split off shares.rs from controller.rs
Diffstat (limited to 'src/shares.rs')
-rw-r--r--src/shares.rs199
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,
+ })
+}