1use std::collections::HashMap;
8
9const BUNDLED: &str = include_str!("../data/advisories.txt");
10
11pub struct Advisory {
13 pub source: String,
14 pub note: String,
15}
16
17pub struct AdvisoryDb {
19 entries: HashMap<String, Advisory>,
20}
21
22impl AdvisoryDb {
23 pub fn bundled() -> Self {
25 Self::parse(BUNDLED)
26 }
27
28 pub fn parse(content: &str) -> Self {
29 let mut entries = HashMap::new();
30 for line in content.lines() {
31 let line = line.trim();
32 if line.is_empty() || line.starts_with('#') {
33 continue;
34 }
35 let mut parts = line.splitn(3, char::is_whitespace);
36 let (Some(reference), Some(source)) = (parts.next(), parts.next()) else {
37 continue;
38 };
39 entries.insert(
40 reference.to_lowercase(),
41 Advisory {
42 source: source.to_string(),
43 note: parts.next().unwrap_or("").trim().to_string(),
44 },
45 );
46 }
47 Self { entries }
48 }
49
50 pub fn lookup(&self, repo: &str, git_ref: &str) -> Option<&Advisory> {
52 self.entries
53 .get(&format!("{repo}@{git_ref}").to_lowercase())
54 }
55
56 pub fn len(&self) -> usize {
57 self.entries.len()
58 }
59
60 pub fn is_empty(&self) -> bool {
61 self.entries.is_empty()
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::AdvisoryDb;
68
69 #[test]
70 fn bundled_snapshot_parses_and_contains_real_world_entry() {
71 let db = AdvisoryDb::bundled();
72 assert!(!db.is_empty());
73 let advisory = db
74 .lookup(
75 "tj-actions/changed-files",
76 "0e58ed8671d6b60d0890c21b07f8835ace038e67",
77 )
78 .expect("실세계 권고(CVE-2025-30066)가 동봉돼야 한다");
79 assert_eq!(advisory.source, "CVE-2025-30066");
80 }
81
82 #[test]
83 fn lookup_is_case_insensitive_and_misses_are_none() {
84 let db = AdvisoryDb::parse("Evil/Action@V1.0.0 GHSA-test 설명 텍스트\n# 주석\n");
85 assert!(db.lookup("evil/action", "v1.0.0").is_some());
86 assert!(db.lookup("evil/action", "v1.0.1").is_none());
87 assert!(db.lookup("good/action", "v1.0.0").is_none());
88 }
89}