roowho2_lib/server/
ignore_list.rs1use 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"), )
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}