Skip to main content

roowho2_lib/server/
ignore_list.rs

1use std::{collections::HashSet, path::Path};
2
3use anyhow::Context;
4use nix::unistd::{Uid, User};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
8pub enum IgnoreEntry {
9    Uid(u32),
10    User(String),
11}
12
13#[derive(Debug, Clone, Default, PartialEq, Eq)]
14pub struct IgnoreList {
15    entries: HashSet<IgnoreEntry>,
16}
17
18impl IgnoreList {
19    pub fn load_optional(path: Option<&Path>) -> anyhow::Result<Option<Self>> {
20        match path {
21            Some(path) => Self::load(path).map(Some),
22            None => Ok(None),
23        }
24    }
25
26    pub fn load(path: &Path) -> anyhow::Result<Self> {
27        let content = std::fs::read_to_string(path)
28            .with_context(|| format!("Failed to read ignore list {}", path.display()))?;
29        Self::parse(&content)
30    }
31
32    pub fn parse(content: &str) -> anyhow::Result<Self> {
33        let mut entries = HashSet::new();
34
35        for (idx, raw_line) in content.lines().enumerate() {
36            let line = raw_line.split('#').next().unwrap_or("").trim();
37            if line.is_empty() {
38                continue;
39            }
40
41            let entry = if let Some(uid) = line.strip_prefix("uid:") {
42                let uid = uid.trim().parse::<u32>().with_context(|| {
43                    format!("Invalid uid on ignore list line {}: {}", idx + 1, raw_line)
44                })?;
45                IgnoreEntry::Uid(uid)
46            } else if let Some(user) = line.strip_prefix("user:") {
47                let user = user.trim();
48                if user.is_empty() {
49                    anyhow::bail!("Invalid user on ignore list line {}: {}", idx + 1, raw_line);
50                }
51                IgnoreEntry::User(user.to_string())
52            } else {
53                anyhow::bail!(
54                    "Invalid ignore list entry on line {}: {}",
55                    idx + 1,
56                    raw_line
57                );
58            };
59
60            entries.insert(entry);
61        }
62
63        Ok(Self { entries })
64    }
65
66    pub fn ignores_username(&self, username: &str) -> bool {
67        if self
68            .entries
69            .contains(&IgnoreEntry::User(username.to_string()))
70        {
71            return true;
72        }
73
74        match User::from_name(username) {
75            Ok(Some(user)) => self.entries.contains(&IgnoreEntry::Uid(user.uid.as_raw())),
76            Ok(None) => false,
77            Err(err) => {
78                tracing::warn!(
79                    "Failed to resolve username '{}' for ignore list lookup: {}",
80                    username,
81                    err
82                );
83                false
84            }
85        }
86    }
87
88    pub fn ignores_uid(&self, uid: u32) -> bool {
89        if self.entries.contains(&IgnoreEntry::Uid(uid)) {
90            return true;
91        }
92
93        match User::from_uid(Uid::from_raw(uid)) {
94            Ok(Some(user)) => self.entries.contains(&IgnoreEntry::User(user.name)),
95            Ok(None) => false,
96            Err(err) => {
97                tracing::warn!(
98                    "Failed to resolve uid {} for ignore list lookup: {}",
99                    uid,
100                    err
101                );
102                false
103            }
104        }
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_parse_ignore_list() {
114        let list = IgnoreList::parse(
115            &["uid:1000", "user:alice", "  user:bob  "].join("\n"), // "\n# comment\nuid:1000\nuser:alice # trailing comment\n\nuser:bob\n",
116        )
117        .unwrap();
118
119        assert!(list.ignores_uid(1000));
120        assert!(list.ignores_username("alice"));
121        assert!(list.ignores_username("bob"));
122    }
123
124    #[test]
125    fn test_parse_ignore_list_with_comments() {
126        let list = IgnoreList::parse(
127            &[
128                "# This is a comment",
129                "uid:1000",
130                "user:alice # trailing comment",
131                "",
132                "user:bob",
133            ]
134            .join("\n"),
135        )
136        .unwrap();
137
138        assert!(list.ignores_uid(1000));
139        assert!(list.ignores_username("alice"));
140        assert!(list.ignores_username("bob"));
141    }
142}