1#![doc(html_root_url = "https://emo-crab.github.io/nvd-rs/cpe")]
18
19use serde::{Deserialize, Serialize};
23use std::collections::HashSet;
24use std::{fmt, str::FromStr};
25
26pub mod component;
27pub mod dictionary;
28pub mod error;
29pub mod part;
30
31use crate::component::Language;
32use crate::error::{CPEError, Result};
33use component::Component;
34use part::Part;
35
36#[derive(Deserialize, Serialize, Debug, Clone, PartialEq, Eq)]
42#[serde(deny_unknown_fields)]
43pub struct CPEName {
44 pub part: Part,
46 pub vendor: Component,
48 pub product: Component,
50 pub version: Component,
52 pub update: Component,
54 pub edition: Component,
56 pub language: Language,
58 pub sw_edition: Component,
60 pub target_sw: Component,
62 pub target_hw: Component,
64 pub other: Component,
66}
67
68#[derive(Debug, Clone, PartialEq, Eq, Serialize, Default, Hash)]
69pub struct Product {
70 pub part: String,
71 pub vendor: String,
72 pub product: String,
73}
74
75impl From<&CPEName> for Product {
76 fn from(val: &CPEName) -> Self {
77 Product {
78 part: val.part.to_string(),
79 vendor: val.vendor.to_string(),
80 product: val.product.to_string(),
81 }
82 }
83}
84
85impl CPEName {
86 pub fn from_uri(uri: &str) -> Result<Self> {
88 let uri = match uri.strip_prefix("cpe:2.3:") {
89 Some(u) => u,
90 None => {
91 return Err(CPEError::InvalidPrefix {
92 value: uri.to_string(),
93 });
94 }
95 };
96
97 let mut components = uri.split(':');
98 let error = CPEError::InvalidPart {
99 value: uri.to_string(),
100 };
101 let part = Part::from_str(components.next().ok_or(&error)?)?;
102 let vendor = Component::from_str(components.next().ok_or(&error)?)?;
103 let product = Component::from_str(components.next().ok_or(&error)?)?;
104 let version = Component::from_str(components.next().ok_or(&error)?)?;
105 let update = Component::from_str(components.next().ok_or(&error)?)?;
106 let edition = Component::from_str(components.next().ok_or(&error)?)?;
107 let language = Language::from_str(components.next().ok_or(&error)?)?;
108 let sw_edition = Component::from_str(components.next().ok_or(&error)?)?;
109 let target_sw = Component::from_str(components.next().ok_or(&error)?)?;
110 let target_hw = Component::from_str(components.next().ok_or(&error)?)?;
111 let other = Component::from_str(components.next().ok_or(&error)?)?;
112
113 Ok(Self {
114 part,
115 vendor,
116 product,
117 version,
118 update,
119 edition,
120 language,
121 sw_edition,
122 target_sw,
123 target_hw,
124 other,
125 })
126 }
127 pub fn from_wfn(name: &str) -> Result<Self> {
129 let prefix = match name.strip_prefix("wfn:[") {
130 Some(u) => u,
131 None => {
132 return Err(CPEError::InvalidPrefix {
133 value: name.to_string(),
134 });
135 }
136 };
137 let components = match prefix.strip_suffix(']') {
138 Some(u) => u,
139 None => {
140 return Err(CPEError::InvalidPrefix {
141 value: name.to_string(),
142 });
143 }
144 };
145 let mut att = CPEName {
146 part: Part::default(),
147 vendor: Default::default(),
148 product: Default::default(),
149 version: Default::default(),
150 update: Default::default(),
151 edition: Default::default(),
152 language: Default::default(),
153 sw_edition: Default::default(),
154 target_sw: Default::default(),
155 target_hw: Default::default(),
156 other: Default::default(),
157 };
158 let mut verify_set = HashSet::from([
159 "part",
160 "vendor",
161 "product",
162 "version",
163 "update",
164 "edition",
165 "language",
166 "sw_edition",
167 "target_sw",
168 "target_hw",
169 "other",
170 ]);
171 for component in components.split(',') {
172 match component.split_once('=') {
173 None => {
174 return Err(CPEError::InvalidPart {
175 value: component.to_string(),
176 });
177 }
178 Some((k, v)) => {
179 match k {
180 "part" => att.part = Part::from_str(v)?,
181 "vendor" => att.vendor = Component::from_str(v)?,
182 "product" => att.product = Component::from_str(v)?,
183 "version" => att.version = Component::from_str(v)?,
184 "update" => att.update = Component::from_str(v)?,
185 "edition" => att.edition = Component::from_str(v)?,
186 "language" => att.language = Language::from_str(v)?,
187 "sw_edition" => att.sw_edition = Component::from_str(v)?,
188 "target_sw" => att.target_sw = Component::from_str(v)?,
189 "target_hw" => att.target_hw = Component::from_str(v)?,
190 "other" => att.other = Component::from_str(v)?,
191 _ => {
192 return Err(CPEError::InvalidPart {
193 value: k.to_string(),
194 });
195 }
196 }
197 if !verify_set.remove(k) {
199 return Err(CPEError::InvalidPart {
200 value: k.to_string(),
201 });
202 }
203 }
204 }
205 }
206 if !verify_set.is_empty() {
207 return Err(CPEError::InvalidWfn {
208 value: name.to_string(),
209 });
210 }
211 Ok(att)
212 }
213}
214
215impl FromStr for CPEName {
216 type Err = CPEError;
217 fn from_str(uri: &str) -> Result<Self> {
218 CPEName::from_uri(uri)
219 }
220}
221
222impl fmt::Display for CPEName {
223 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
224 let Self {
225 part,
226 vendor,
227 product,
228 version,
229 update,
230 edition,
231 language,
232 sw_edition,
233 target_sw,
234 target_hw,
235 other,
236 } = self;
237
238 write!(
239 f,
240 "cpe:2.3:{part:#}:{vendor}:{product}:{version}:{update}:{edition}:{language}:{sw_edition}:{target_sw}:{target_hw}:{other}",
241 )
242 }
243}
244
245fn strip_slashes(s: &str) -> String {
246 let mut out = String::new();
247 let mut chats = s.chars();
248 while let Some(c) = chats.next() {
249 if c == '\\' {
250 if let Some(cc) = chats.next() {
251 match cc {
252 '\\' => {
253 continue;
254 }
255 _ => {
256 out.push(cc);
257 }
258 }
259 }
260 } else {
261 out.push(c);
262 }
263 }
264 out
265}
266
267fn parse_uri_attribute(value: &str) -> Result<String> {
268 let value = if value.contains("%01") || value.contains("%02") {
269 let value = value.replace("%01", "?").replace("%02", "*");
270 percent_encoding::percent_decode_str(&value)
271 .decode_utf8()
272 .map_err(|source| CPEError::Utf8Error {
273 source,
274 value: value.to_owned(),
275 })?
276 .to_string()
277 } else {
278 percent_encoding::percent_decode_str(value)
279 .decode_utf8()
280 .map_err(|source| CPEError::Utf8Error {
281 source,
282 value: value.to_owned(),
283 })?
284 .to_string()
285 };
286 let value = strip_slashes(value.as_str());
287 Ok(value)
288}
289
290pub fn version_cmp(a: &str, b: &str, operator: &str) -> bool {
291 if let Ok(op) = version_compare::Cmp::from_sign(operator) {
292 if let Ok(res) = version_compare::compare_to(a, b, op) {
293 return res;
294 }
295 }
296 false
297}
298
299impl CPEName {
300 pub fn match_version(&self, version: &str) -> bool {
302 if self.version.is_any() {
303 return true;
304 } else if self.version.is_na() {
305 return false;
306 }
307 let my_version = if self.update.is_value() {
308 format!("{} {}", self.version, self.update)
309 } else {
310 self.version.to_string()
311 };
312 version_cmp(version, &my_version, "==")
313 }
314 pub fn match_product(&self, product: &str) -> bool {
316 if self.product.is_any() {
317 return true;
318 } else if self.product.is_na() {
319 return false;
320 }
321 product == self.normalize_target_software()
322 }
323 fn normalize_target_software(&self) -> String {
325 if let Component::Value(software) = &self.target_sw {
326 format!("{}-{}", software, self.product)
327 } else {
328 self.product.to_string()
329 }
330 }
331}