sbom_tools/matching/
aliases.rs1use serde::{Deserialize, Serialize};
4use std::collections::{HashMap, HashSet};
5
6#[derive(Debug, Clone, Default, Serialize, Deserialize)]
8pub struct AliasTable {
9 alias_to_canonical: HashMap<String, String>,
11 canonical_to_aliases: HashMap<String, HashSet<String>>,
13}
14
15impl AliasTable {
16 #[must_use]
18 pub fn new() -> Self {
19 Self::default()
20 }
21
22 #[must_use]
36 pub fn with_builtins() -> Self {
37 let mut table = Self::new();
38 table.load_builtins();
39 table
40 }
41
42 fn load_builtins(&mut self) {
44 self.add_aliases("pkg:pypi/pillow", &["PIL", "python-pillow", "pillow"]);
46 self.add_aliases("pkg:pypi/scikit-learn", &["sklearn", "scikit_learn"]);
47 self.add_aliases(
48 "pkg:pypi/beautifulsoup4",
49 &["bs4", "BeautifulSoup", "beautifulsoup"],
50 );
51 self.add_aliases("pkg:pypi/pyyaml", &["yaml", "PyYAML"]);
52 self.add_aliases("pkg:pypi/opencv-python", &["cv2", "opencv"]);
53 self.add_aliases("pkg:pypi/python-dateutil", &["dateutil"]);
54 self.add_aliases("pkg:pypi/importlib-metadata", &["importlib_metadata"]);
55 self.add_aliases("pkg:pypi/typing-extensions", &["typing_extensions"]);
56 }
57
58 pub fn add_aliases(&mut self, canonical: &str, aliases: &[&str]) {
60 let canonical_lower = canonical.to_lowercase();
61
62 self.alias_to_canonical
64 .insert(canonical_lower.clone(), canonical_lower.clone());
65
66 let alias_set = self
68 .canonical_to_aliases
69 .entry(canonical_lower.clone())
70 .or_default();
71 alias_set.insert(canonical_lower.clone());
72
73 for alias in aliases {
75 let alias_lower = alias.to_lowercase();
76 self.alias_to_canonical
77 .insert(alias_lower.clone(), canonical_lower.clone());
78 if let Some(set) = self.canonical_to_aliases.get_mut(&canonical_lower) {
79 set.insert(alias_lower);
80 }
81 }
82 }
83
84 #[must_use]
86 pub fn is_empty(&self) -> bool {
87 self.alias_to_canonical.is_empty()
88 }
89
90 #[must_use]
92 pub fn get_canonical(&self, alias: &str) -> Option<String> {
93 self.alias_to_canonical.get(&alias.to_lowercase()).cloned()
94 }
95
96 #[must_use]
98 pub fn is_alias(&self, canonical: &str, name: &str) -> bool {
99 let canonical_lower = canonical.to_lowercase();
100 let name_lower = name.to_lowercase();
101
102 self.canonical_to_aliases
103 .get(&canonical_lower)
104 .is_some_and(|aliases| aliases.contains(&name_lower))
105 }
106
107 #[must_use]
109 pub fn get_aliases(&self, canonical: &str) -> Option<&HashSet<String>> {
110 self.canonical_to_aliases.get(&canonical.to_lowercase())
111 }
112
113 pub fn load_json(&mut self, json: &str) -> Result<(), serde_json::Error> {
115 let entries: Vec<AliasEntry> = serde_json::from_str(json)?;
116 for entry in entries {
117 let aliases: Vec<&str> = entry
118 .aliases
119 .iter()
120 .map(std::string::String::as_str)
121 .collect();
122 self.add_aliases(&entry.canonical, &aliases);
123 }
124 Ok(())
125 }
126
127 pub fn to_json(&self) -> Result<String, serde_json::Error> {
129 let entries: Vec<AliasEntry> = self
130 .canonical_to_aliases
131 .iter()
132 .map(|(canonical, aliases)| AliasEntry {
133 canonical: canonical.clone(),
134 aliases: aliases.iter().cloned().collect(),
135 })
136 .collect();
137 serde_json::to_string_pretty(&entries)
138 }
139}
140
141#[derive(Debug, Serialize, Deserialize)]
143struct AliasEntry {
144 canonical: String,
145 aliases: Vec<String>,
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151
152 #[test]
153 fn test_alias_lookup() {
154 let table = AliasTable::with_builtins();
155
156 assert_eq!(
158 table.get_canonical("PIL"),
159 Some("pkg:pypi/pillow".to_lowercase())
160 );
161
162 assert_eq!(
164 table.get_canonical("sklearn"),
165 Some("pkg:pypi/scikit-learn".to_lowercase())
166 );
167 }
168
169 #[test]
170 fn test_is_alias() {
171 let table = AliasTable::with_builtins();
172
173 assert!(table.is_alias("pkg:pypi/pillow", "PIL"));
174 assert!(table.is_alias("pkg:pypi/pillow", "pillow"));
175 assert!(!table.is_alias("pkg:pypi/pillow", "numpy"));
176 }
177
178 #[test]
179 fn test_custom_aliases() {
180 let mut table = AliasTable::new();
181 table.add_aliases("my-package", &["my_package", "mypackage"]);
182
183 assert_eq!(
184 table.get_canonical("my_package"),
185 Some("my-package".to_string())
186 );
187 assert!(table.is_alias("my-package", "mypackage"));
188 }
189}