allowlist.txt, parsed. The file is the data; this is only its reader.
3use std::sync::OnceLock;
What the allowlist says about one icon.
A category of the pack, as the pack's own README names it, and whether it was excluded whole.
One icon of the pack and the decision about it.
Reads an allowlist. Refuses a line it cannot place, an excluded line without a reason, an admitted line in a category that is excluded whole, and a name listed twice.
45 pub fn parse(text: &'static str) -> Result<Allowlist, ParseError> { 46 let mut categories: Vec<Category> = Vec::new(); 47 let mut entries: Vec<Entry> = Vec::new(); 48 for (i, raw) in text.lines().enumerate() { 49 let line = i + 1; 50 let l = raw.trim(); 51 if l.is_empty() || l.starts_with('#') { 52 continue; 53 } 54 if let Some(rest) = l.strip_prefix('[') { 55 let (name, tail) = rest.split_once(']').ok_or(ParseError { line, why: "a category without its closing bracket" })?; 56 let excluded_because = match tail.trim() { 57 "" => None, 58 t => Some(t.strip_prefix("EXCLUDED:").ok_or(ParseError { line, why: "text after a category that is not EXCLUDED: and a reason" })?.trim()), 59 }; 60 if excluded_because == Some("") { 61 return Err(ParseError { line, why: "an excluded category without a reason" }); 62 } 63 categories.push(Category { name, excluded_because }); 64 continue; 65 } 66 let cat = categories.last().ok_or(ParseError { line, why: "an icon before any category" })?; 67 let (mark, rest) = l.split_at(1); 68 let rest = rest.trim(); 69 let (name, reason) = match rest.split_once(char::is_whitespace) { 70 Some((n, r)) => (n, Some(r.trim())), 71 None => (rest, None), 72 }; 73 if name.is_empty() || !name.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-') { 74 return Err(ParseError { line, why: "not an icon name" }); 75 } 76 let verdict = match mark { 77 "+" => Verdict::Admitted, 78 "-" => Verdict::Excluded, 79 _ => return Err(ParseError { line, why: "a line that is neither + nor -" }), 80 }; 81 if verdict == Verdict::Admitted && (cat.excluded_because.is_some() || reason.is_some()) { 82 return Err(ParseError { line, why: "an admitted icon in an excluded category, or with a trailing note" }); 83 } 84 if verdict == Verdict::Excluded && reason.is_none() { 85 return Err(ParseError { line, why: "an excluded icon without a reason" }); 86 } 87 if entries.iter().any(|e| e.name == name) { 88 return Err(ParseError { line, why: "an icon listed twice" }); 89 } 90 entries.push(Entry { name, category: cat.name, verdict, reason }); 91 } 92 Ok(Allowlist { categories, entries }) 93 }
The allowlist shipped in this crate. A file that does not parse is a build error found by the tests, so reaching a panic here means the tests were not run.