1use super::date::{YEAR_MAX, YEAR_MIN};
4use crate::error::{Error, ErrorKind};
5use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as DeError};
6use std::{
7 fmt::{self, Display},
8 str::FromStr,
9};
10
11#[derive(Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
13pub struct Id {
14 kind: IdKind,
16
17 year: Option<u32>,
19
20 string: String,
22}
23
24impl Id {
25 pub const PLACEHOLDER: &'static str = "RUSTSEC-0000-0000";
27
28 pub fn as_str(&self) -> &str {
30 self.string.as_ref()
31 }
32
33 pub fn kind(&self) -> IdKind {
35 self.kind
36 }
37
38 pub fn is_placeholder(&self) -> bool {
40 self.string == Self::PLACEHOLDER
41 }
42
43 pub fn is_rustsec(&self) -> bool {
45 self.kind == IdKind::RustSec
46 }
47
48 pub fn is_cve(&self) -> bool {
50 self.kind == IdKind::Cve
51 }
52
53 pub fn is_ghsa(&self) -> bool {
55 self.kind == IdKind::Ghsa
56 }
57 pub fn is_talos(&self) -> bool {
59 self.kind == IdKind::Talos
60 }
61
62 pub fn is_other(&self) -> bool {
64 self.kind == IdKind::Other
65 }
66
67 pub fn year(&self) -> Option<u32> {
69 self.year
70 }
71
72 pub fn numerical_part(&self) -> Option<u32> {
76 if self.is_placeholder() {
77 return None;
78 }
79
80 self.string
81 .split('-')
82 .next_back()
83 .and_then(|s| str::parse(s).ok())
84 }
85
86 pub fn url(&self) -> Option<String> {
90 match self.kind {
91 IdKind::RustSec => {
92 if self.is_placeholder() {
93 None
94 } else {
95 Some(format!("https://rustsec.org/advisories/{}", &self.string))
96 }
97 }
98 IdKind::Cve => Some(format!(
99 "https://cve.mitre.org/cgi-bin/cvename.cgi?name={}",
100 &self.string
101 )),
102 IdKind::Ghsa => Some(format!("https://github.com/advisories/{}", &self.string)),
103 IdKind::Talos => Some(format!(
104 "https://www.talosintelligence.com/reports/{}",
105 &self.string
106 )),
107 _ => None,
108 }
109 }
110}
111
112impl AsRef<str> for Id {
113 fn as_ref(&self) -> &str {
114 self.as_str()
115 }
116}
117
118impl Default for Id {
119 fn default() -> Id {
120 Id {
121 kind: IdKind::RustSec,
122 year: None,
123 string: Id::PLACEHOLDER.into(),
124 }
125 }
126}
127
128impl Display for Id {
129 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130 f.write_str(self.as_str())
131 }
132}
133
134impl FromStr for Id {
135 type Err = Error;
136
137 fn from_str(advisory_id: &str) -> Result<Self, Error> {
139 if advisory_id == Id::PLACEHOLDER {
140 return Ok(Id::default());
141 }
142
143 let kind = IdKind::detect(advisory_id);
144
145 let year = match kind {
147 IdKind::RustSec | IdKind::Cve | IdKind::Talos => Some(parse_year(advisory_id)?),
148 _ => None,
149 };
150
151 Ok(Self {
152 kind,
153 year,
154 string: advisory_id.into(),
155 })
156 }
157}
158
159impl Serialize for Id {
160 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
161 serializer.serialize_str(&self.string)
162 }
163}
164
165impl<'de> Deserialize<'de> for Id {
166 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
167 Self::from_str(&String::deserialize(deserializer)?).map_err(D::Error::custom)
168 }
169}
170
171#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq, PartialOrd, Ord)]
173#[non_exhaustive]
174pub enum IdKind {
175 RustSec,
177
178 Cve,
180
181 Ghsa,
183
184 Talos,
186
187 Other,
189}
190
191impl IdKind {
192 pub fn detect(string: &str) -> Self {
194 if string.starts_with("RUSTSEC-") {
195 IdKind::RustSec
196 } else if string.starts_with("CVE-") {
197 IdKind::Cve
198 } else if string.starts_with("TALOS-") {
199 IdKind::Talos
200 } else if string.starts_with("GHSA-") {
201 IdKind::Ghsa
202 } else {
203 IdKind::Other
204 }
205 }
206}
207
208fn parse_year(advisory_id: &str) -> Result<u32, Error> {
210 let mut parts = advisory_id.split('-');
211 parts.next().unwrap();
212
213 let year = match parts.next().unwrap().parse::<u32>() {
214 Ok(n) => match n {
215 YEAR_MIN..=YEAR_MAX => n,
216 _ => fail!(
217 ErrorKind::Parse,
218 "out-of-range year in advisory ID: {}",
219 advisory_id
220 ),
221 },
222 _ => fail!(
223 ErrorKind::Parse,
224 "malformed year in advisory ID: {}",
225 advisory_id
226 ),
227 };
228
229 if let Some(num) = parts.next() {
230 if num.parse::<u32>().is_err() {
231 fail!(ErrorKind::Parse, "malformed advisory ID: {}", advisory_id);
232 }
233 } else {
234 fail!(ErrorKind::Parse, "incomplete advisory ID: {}", advisory_id);
235 }
236
237 if parts.next().is_some() {
238 fail!(ErrorKind::Parse, "malformed advisory ID: {}", advisory_id);
239 }
240
241 Ok(year)
242}
243
244#[cfg(test)]
245mod tests {
246 use super::{Id, IdKind};
247
248 const EXAMPLE_RUSTSEC_ID: &str = "RUSTSEC-2018-0001";
249 const EXAMPLE_CVE_ID: &str = "CVE-2017-1000168";
250 const EXAMPLE_GHSA_ID: &str = "GHSA-4mmc-49vf-jmcp";
251 const EXAMPLE_TALOS_ID: &str = "TALOS-2017-0468";
252 const EXAMPLE_UNKNOWN_ID: &str = "Anonymous-42";
253
254 #[test]
255 fn rustsec_id_test() {
256 let rustsec_id = EXAMPLE_RUSTSEC_ID.parse::<Id>().unwrap();
257 assert!(rustsec_id.is_rustsec());
258 assert_eq!(rustsec_id.year().unwrap(), 2018);
259 assert_eq!(
260 rustsec_id.url().unwrap(),
261 "https://rustsec.org/advisories/RUSTSEC-2018-0001"
262 );
263 assert_eq!(rustsec_id.numerical_part().unwrap(), 1);
264 }
265
266 #[test]
268 fn rustsec_0000_0000_test() {
269 let rustsec_id = Id::PLACEHOLDER.parse::<Id>().unwrap();
270 assert!(rustsec_id.is_rustsec());
271 assert!(rustsec_id.year().is_none());
272 assert!(rustsec_id.url().is_none());
273 assert!(rustsec_id.numerical_part().is_none());
274 }
275
276 #[test]
277 fn cve_id_test() {
278 let cve_id = EXAMPLE_CVE_ID.parse::<Id>().unwrap();
279 assert!(cve_id.is_cve());
280 assert_eq!(cve_id.year().unwrap(), 2017);
281 assert_eq!(
282 cve_id.url().unwrap(),
283 "https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2017-1000168"
284 );
285 assert_eq!(cve_id.numerical_part().unwrap(), 1000168);
286 }
287
288 #[test]
289 fn ghsa_id_test() {
290 let ghsa_id = EXAMPLE_GHSA_ID.parse::<Id>().unwrap();
291 assert!(ghsa_id.is_ghsa());
292 assert!(ghsa_id.year().is_none());
293 assert_eq!(
294 ghsa_id.url().unwrap(),
295 "https://github.com/advisories/GHSA-4mmc-49vf-jmcp"
296 );
297 assert!(ghsa_id.numerical_part().is_none());
298 }
299
300 #[test]
301 fn talos_id_test() {
302 let talos_id = EXAMPLE_TALOS_ID.parse::<Id>().unwrap();
303 assert_eq!(talos_id.kind(), IdKind::Talos);
304 assert_eq!(talos_id.year().unwrap(), 2017);
305 assert_eq!(
306 talos_id.url().unwrap(),
307 "https://www.talosintelligence.com/reports/TALOS-2017-0468"
308 );
309 assert_eq!(talos_id.numerical_part().unwrap(), 468);
310 }
311
312 #[test]
313 fn other_id_test() {
314 let other_id = EXAMPLE_UNKNOWN_ID.parse::<Id>().unwrap();
315 assert!(other_id.is_other());
316 assert!(other_id.year().is_none());
317 assert!(other_id.url().is_none());
318 assert_eq!(other_id.numerical_part().unwrap(), 42);
319 }
320}