Skip to main content

cve_id/
lib.rs

1//! # About CVEs
2//!
3//! The [Common Vulnerabilities and Exposures (CVEs) program](https://www.cve.org/About/Overview) catalogs publicly disclosed information-security vulnerabilities.
4//!
5//! This catalog contains one so-called CVE Record for each vulnerability.
6//! Each record has an identifier ([CVE ID](https://www.cve.org/ResourcesSupport/Glossary#glossaryCVEID)) in the format "CVE-YYYY-NNNN"; that is the prefix "CVE", a 4-digit year and then a 4+digit number, separated by "-" (dashes).
7//!
8//! # About this crate
9//!
10//! This crate implements a [`CveId`] type for syntactically valid CVE IDs. It does not implement other fields of a CVE Record.
11//!
12//! The crate does not implement other semantic rules, e.g.
13//! - CVEs were first assigned with start of the program in 1999
14//! - the first number each year is 1
15//!
16//! Syntactically "CVE-0000-0000" and "CVE-9999-9999999999999999999" are valid.
17//!
18//! ```rust
19//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
20//! use cve_id::{CveId, CveYear};
21//!
22//! let cve_min = CveId::new(0.try_into()?, 0);
23//! let cve_first = "CVE-1999-0001".parse::<CveId>()?;
24//! let cve_max = CveId::new(CveYear::new(9999)?, 9_999_999_999_999_999_999);
25//!
26//! assert!(CveId::from_str("CAN-1999-0067").is_err());
27//! assert!(CveId::from_str("CVE-1900-0420")?.is_example_or_test());
28//! # Ok(())
29//! # }
30//! ```
31//!
32//! # Features
33//!
34//! - `serde` — Enable serializing and deserializing [`CveId`] using `serde` v1
35//! - `schemars` — Enable JSON schema for [`CveId`] using `schemars` v1
36//! - `arbitrary` — Enable generating arbitrary [`CveId`] using `arbitrary` v1
37
38// TODO: proper CveNumber newtype and then From<(CveYear, CveNumber))> for CveId
39// TODO: test data from CVE-10K
40// TODO: nightly core::iter::Step
41// TODO: proper private error kinds
42// TODO: nightly rustc_layout_scalar_valid_range_start rustc_layout_scalar_valid_range_end rustc_nonnull_optimization_guaranteed for CveNumber
43// TODO: repr transparent for newtypes
44
45#![deny(unsafe_code)]
46#![cfg_attr(not(any(test)), no_std)]
47#![cfg_attr(docsrs, feature(doc_cfg))]
48#![cfg_attr(docsrs, doc(auto_cfg(hide(docsrs))))]
49
50#[cfg(feature = "alloc")]
51extern crate alloc;
52
53#[cfg(feature = "std")]
54extern crate std;
55
56/// Common Vulnerabilities and Exposures Identifier
57///
58/// Id of a CVE in the "CVE-YYYY-NNNN" format.
59// TODO: Ord
60#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
61pub struct CveId {
62    year: CveYear,
63    // TODO: the JSON schema specifies 4-19 digits, but u32 would provide 4_294_967_295 MAX which already seems plenty per year
64    number: CveNumber,
65}
66
67/// Sequential number NNNN part of [`CveId`]
68pub type CveNumber = u64;
69
70const fn u8_slice_eq(left: &[u8], right: &[u8]) -> bool {
71    match (left, right) {
72        (left, right) => {
73            let mut returned = left.len() == right.len();
74
75            if returned {
76                let mut i = 0;
77
78                while i != left.len() {
79                    if !(left[i] == right[i]) {
80                        returned = false;
81
82                        break;
83                    }
84
85                    i += 1;
86                }
87            }
88
89            returned
90        }
91    }
92}
93
94macro_rules! int_from_ascii {
95    ($func_name:ident, $int_ty:ty) => {
96        const fn $func_name(src: &[u8]) -> Result<$int_ty, ParseCveIdError> {
97            let mut digits = src;
98
99            let mut result = 0;
100
101            macro_rules! unwrap_or_PCE {
102                ($option:expr) => {
103                    match $option {
104                        Some(value) => value,
105                        None => return Err(ParseCveIdError()),
106                    }
107                };
108            }
109
110            #[inline(always)]
111            pub const fn can_not_overflow<T>(digits: &[u8]) -> bool {
112                digits.len() <= core::mem::size_of::<T>() * 2
113            }
114
115            if can_not_overflow::<$int_ty>(digits) {
116                // If the len of the str is short compared to the range of the type
117                // we are parsing into, then we can be certain that an overflow will not occur.
118                // This bound is when `radix.pow(digits.len()) - 1 <= T::MAX` but the condition
119                // above is a faster (conservative) approximation of this.
120                //
121                // Consider radix 16 as it has the highest information density per digit and will thus overflow the earliest:
122                // `u8::MAX` is `ff` - any str of len 2 is guaranteed to not overflow.
123                // `i8::MAX` is `7f` - only a str of len 1 is guaranteed to not overflow.
124                while let [c, rest @ ..] = digits {
125                    result *= 10 as $int_ty;
126                    let x = unwrap_or_PCE!((*c as char).to_digit(10));
127                    result += x as $int_ty;
128                    digits = rest;
129                }
130            } else {
131                while let [c, rest @ ..] = digits {
132                    // When `radix` is passed in as a literal, rather than doing a slow `imul`
133                    // the compiler can use shifts if `radix` can be expressed as a
134                    // sum of powers of 2 (x*10 can be written as x*8 + x*2).
135                    // When the compiler can't use these optimisations,
136                    // the latency of the multiplication can be hidden by issuing it
137                    // before the result is needed to improve performance on
138                    // modern out-of-order CPU as multiplication here is slower
139                    // than the other instructions, we can get the end result faster
140                    // doing multiplication first and let the CPU spends other cycles
141                    // doing other computation and get multiplication result later.
142                    let mul = result.checked_mul(10 as $int_ty);
143                    let x = unwrap_or_PCE!((*c as char).to_digit(10)) as $int_ty;
144                    result = unwrap_or_PCE!(mul);
145                    result = unwrap_or_PCE!(<$int_ty>::checked_add(result, x));
146                    digits = rest;
147                }
148            }
149            Ok(result)
150        }
151    };
152}
153
154int_from_ascii!(u16_from_ascii, u16);
155int_from_ascii!(u64_from_ascii, u64);
156
157impl CveId {
158    const CVE_PREFIX: &[u8] = b"CVE";
159    const SEPARATOR: u8 = b'-';
160
161    /// Constructs a semantically valid CVE ID
162    #[inline]
163    pub const fn new(year: CveYear, number: CveNumber) -> Self {
164        Self { year, number }
165    }
166
167    /// Parses a CVE ID in the "CVE-YYYY-NNNN" format
168    pub const fn from_str(src: &str) -> Result<Self, ParseCveIdError> {
169        let src = src.as_bytes();
170
171        // CVE-YYYY-NNNN
172        if src.len() < 13 {
173            return Err(ParseCveIdError());
174        }
175
176        if src[3] != Self::SEPARATOR || src[8] != Self::SEPARATOR {
177            return Err(ParseCveIdError());
178        }
179
180        // CVE -YYYY-NNNN
181        let (prefix, rest) = src.split_at(3);
182
183        if !u8_slice_eq(prefix, Self::CVE_PREFIX) {
184            return Err(ParseCveIdError());
185        }
186
187        // - YYYY-NNNN
188        let (_sep, rest) = rest.split_at(1);
189        // YYYY -NNNN
190        let (year, rest) = rest.split_at(4);
191        // - NNNN
192        let (_sep, number) = rest.split_at(1);
193
194        macro_rules! unwrap_or_PCE {
195            ($result:expr) => {
196                match $result {
197                    Ok(value) => value,
198                    Err(_err) => return Err(ParseCveIdError()),
199                }
200            };
201        }
202
203        let year = unwrap_or_PCE!(u16_from_ascii(year));
204        let number = unwrap_or_PCE!(u64_from_ascii(number));
205
206        let year = unwrap_or_PCE!(CveYear::new(year));
207
208        Ok(Self { year, number })
209    }
210
211    /// Returns the YYYY year part of the CVE ID
212    pub const fn year(&self) -> CveYear {
213        self.year
214    }
215
216    /// Returns the NNNN number part of the CVE ID
217    pub const fn number(&self) -> CveNumber {
218        self.number
219    }
220
221    /// Returns whether the CVE ID is "for example, documentation, and testing purposes"
222    ///
223    /// [CVE Numbering Authority (CNA) Operational Rules - 5.4 Example or Test CVE IDs](https://www.cve.org/ResourcesSupport/AllResources/CNARules#section_5-4_Example_or_Test_CVE_IDs)
224    /// specifies:
225    ///
226    /// > *5.4.1*
227    /// > For example, documentation, and testing purposes, CVE Program participants SHOULD use CVE IDs with the prefix “CVE-1900-” that otherwise conform to the current CVE ID specification.
228    /// >
229    /// > *5.4.2*
230    /// > CVE Program participants MUST treat CVE IDs and CVE Records using the “CVE-1900-” prefix as test or example information and MUST NOT treat them as correct, live, or production information.
231    /// > The CVE Services do not support CVE IDs with this prefix.
232    pub const fn is_example_or_test(&self) -> bool {
233        1900 == self.year.0
234    }
235
236    // TODO: validate if .year is within 1999-$currentYear and .number  >= 1 ?
237    // cf. https://github.com/CVEProject/cve-core/blob/main/src/core/CveId.ts
238    // but this does not appear to be specified, just part of "CVE Services" API/impl
239}
240
241impl core::str::FromStr for CveId {
242    type Err = ParseCveIdError;
243
244    fn from_str(src: &str) -> Result<Self, Self::Err> {
245        Self::from_str(src)
246    }
247}
248
249impl core::fmt::Display for CveId {
250    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
251        write!(f, "CVE-{}-{:04}", self.year, self.number)
252    }
253}
254
255#[cfg(feature = "arbitrary")]
256#[cfg_attr(docsrs, doc(cfg(feature = "arbitrary")))]
257impl<'a> arbitrary::Arbitrary<'a> for CveId {
258    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
259        let year = arbitrary::Arbitrary::arbitrary(u)?;
260        let number = arbitrary::Arbitrary::arbitrary(u)?;
261
262        Ok(Self { year, number })
263    }
264
265    #[inline]
266    fn size_hint(depth: usize) -> (usize, Option<usize>) {
267        Self::try_size_hint(depth).unwrap_or_default()
268    }
269
270    #[inline]
271    fn try_size_hint(
272        depth: usize,
273    ) -> core::result::Result<(usize, Option<usize>), arbitrary::MaxRecursionReached> {
274        arbitrary::size_hint::try_recursion_guard(depth, |depth| {
275            Ok(arbitrary::size_hint::and_all(&[
276                <CveYear as arbitrary::Arbitrary>::try_size_hint(depth)?,
277                <CveNumber as arbitrary::Arbitrary>::try_size_hint(depth)?,
278            ]))
279        })
280    }
281}
282
283/// Year YYYY part of [`CveId`]
284///
285/// Syntactically valid years are 0000 through 9999.
286// TODO: u8 would be sufficient for e.g. 1900 + 255 = 2155
287// TODO: Ord
288#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
289pub struct CveYear(u16);
290
291impl CveYear {
292    const YEAR_MIN: u16 = 0;
293    const YEAR_MAX: u16 = 9999;
294    pub const MIN: Self = CveYear(Self::YEAR_MIN);
295    pub const MAX: Self = CveYear(Self::YEAR_MAX);
296
297    /// Constructs a semantically valid CVE year
298    pub const fn new(year: u16) -> Result<Self, InvalidCveYearError> {
299        if year > Self::YEAR_MAX {
300            return Err(InvalidCveYearError());
301        }
302
303        Ok(Self(year))
304    }
305
306    // TODO: const from_str
307}
308
309impl core::convert::TryFrom<u16> for CveYear {
310    type Error = InvalidCveYearError;
311
312    fn try_from(value: u16) -> Result<Self, Self::Error> {
313        Self::new(value)
314    }
315}
316
317impl core::convert::From<CveYear> for u16 {
318    fn from(year: CveYear) -> Self {
319        year.0
320    }
321}
322
323impl PartialEq<u16> for CveYear {
324    fn eq(&self, other: &u16) -> bool {
325        self.0 == *other
326    }
327}
328
329impl core::fmt::Display for CveYear {
330    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
331        write!(f, "{:04}", self.0)
332    }
333}
334
335#[cfg(feature = "arbitrary")]
336#[cfg_attr(docsrs, doc(cfg(feature = "arbitrary")))]
337impl<'a> arbitrary::Arbitrary<'a> for CveYear {
338    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
339        let year = u.int_in_range(Self::YEAR_MIN..=Self::YEAR_MAX)?;
340        Ok(Self(year))
341    }
342
343    #[inline]
344    fn size_hint(_depth: usize) -> (usize, Option<usize>) {
345        let n = core::mem::size_of::<u16>();
346        (n, Some(n))
347    }
348}
349
350/// Invalid value error for [`CveYear`]
351#[derive(Debug, Clone, PartialEq, Eq)]
352pub struct InvalidCveYearError();
353
354impl core::fmt::Display for InvalidCveYearError {
355    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
356        write!(f, "invalid CVE year")
357    }
358}
359
360impl core::error::Error for InvalidCveYearError {}
361
362/// Parse error for [`CveId`]
363#[derive(Debug, Clone, PartialEq, Eq)]
364pub struct ParseCveIdError();
365
366impl core::fmt::Display for ParseCveIdError {
367    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
368        write!(f, "can not parse CVE ID")
369    }
370}
371
372impl core::error::Error for ParseCveIdError {}
373
374#[cfg(feature = "serde")]
375mod serde {
376    use crate::{CveId, CveYear};
377    use ::serde_core::de::{self, Deserialize, Deserializer, MapAccess, SeqAccess, Visitor};
378    use ::serde_core::ser::{Serialize, SerializeStruct, Serializer};
379    use core::fmt::Write;
380
381    #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
382    impl Serialize for CveId {
383        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
384        where
385            S: Serializer,
386        {
387            if serializer.is_human_readable() {
388                struct Wrapper<'a> {
389                    buf: &'a mut [u8],
390                    offset: usize,
391                }
392
393                impl<'a> Wrapper<'a> {
394                    fn new(buf: &'a mut [u8]) -> Self {
395                        Wrapper {
396                            buf: buf,
397                            offset: 0,
398                        }
399                    }
400                }
401
402                impl<'a> core::fmt::Write for Wrapper<'a> {
403                    fn write_str(&mut self, s: &str) -> core::fmt::Result {
404                        let bytes = s.as_bytes();
405
406                        let remainder = &mut self.buf[self.offset..];
407                        if remainder.len() < bytes.len() {
408                            return Err(core::fmt::Error);
409                        }
410                        let remainder = &mut remainder[..bytes.len()];
411                        remainder.copy_from_slice(bytes);
412
413                        self.offset += bytes.len();
414
415                        Ok(())
416                    }
417                }
418
419                // "CVE" "-" CveYear::MAX "-" CveNumber::MAX
420                let mut buf = [0 as u8; 3 + 1 + 5 + 1 + 20];
421                let mut wrapper = Wrapper::new(&mut buf);
422                write!(wrapper, "{}", self).expect("can write to fixed buffer");
423                let chars = &wrapper.buf[0..wrapper.offset];
424                let s = str::from_utf8(chars).expect("valid ASCII");
425                serializer.serialize_str(s)
426            } else {
427                let mut state = serializer.serialize_struct("CveId", 2)?;
428                state.serialize_field("year", &self.year)?;
429                state.serialize_field("number", &self.number)?;
430                state.end()
431            }
432        }
433    }
434
435    #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
436    impl<'de> Deserialize<'de> for CveId {
437        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
438        where
439            D: Deserializer<'de>,
440        {
441            if deserializer.is_human_readable() {
442                let s = <&str>::deserialize(deserializer)?;
443                CveId::from_str(s).map_err(de::Error::custom)
444            } else {
445                enum Field {
446                    Year,
447                    Number,
448                }
449
450                impl<'de> Deserialize<'de> for Field {
451                    fn deserialize<D>(deserializer: D) -> Result<Field, D::Error>
452                    where
453                        D: Deserializer<'de>,
454                    {
455                        struct FieldVisitor;
456
457                        impl<'de> Visitor<'de> for FieldVisitor {
458                            type Value = Field;
459
460                            fn expecting(
461                                &self,
462                                formatter: &mut core::fmt::Formatter,
463                            ) -> core::fmt::Result {
464                                formatter.write_str("`year` or `number`")
465                            }
466
467                            fn visit_str<E>(self, value: &str) -> Result<Field, E>
468                            where
469                                E: de::Error,
470                            {
471                                match value {
472                                    "year" => Ok(Field::Year),
473                                    "number" => Ok(Field::Number),
474                                    _ => Err(de::Error::unknown_field(value, FIELDS)),
475                                }
476                            }
477                        }
478
479                        deserializer.deserialize_identifier(FieldVisitor)
480                    }
481                }
482
483                struct CveIdVisitor;
484
485                impl<'de> Visitor<'de> for CveIdVisitor {
486                    type Value = CveId;
487
488                    fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
489                        formatter.write_str("struct CveId")
490                    }
491
492                    fn visit_seq<V>(self, mut seq: V) -> Result<CveId, V::Error>
493                    where
494                        V: SeqAccess<'de>,
495                    {
496                        let year = seq
497                            .next_element()?
498                            .ok_or_else(|| de::Error::invalid_length(0, &self))?;
499                        let number = seq
500                            .next_element()?
501                            .ok_or_else(|| de::Error::invalid_length(1, &self))?;
502                        Ok(CveId::new(year, number))
503                    }
504
505                    fn visit_map<V>(self, mut map: V) -> Result<CveId, V::Error>
506                    where
507                        V: MapAccess<'de>,
508                    {
509                        let mut year = None;
510                        let mut number = None;
511                        while let Some(key) = map.next_key()? {
512                            match key {
513                                Field::Year => {
514                                    if year.is_some() {
515                                        return Err(de::Error::duplicate_field("year"));
516                                    }
517                                    year = Some(map.next_value()?);
518                                }
519                                Field::Number => {
520                                    if number.is_some() {
521                                        return Err(de::Error::duplicate_field("number"));
522                                    }
523                                    number = Some(map.next_value()?);
524                                }
525                            }
526                        }
527                        let year = year.ok_or_else(|| de::Error::missing_field("year"))?;
528                        let number = number.ok_or_else(|| de::Error::missing_field("number"))?;
529                        Ok(CveId::new(year, number))
530                    }
531                }
532
533                const FIELDS: &[&str] = &["year", "number"];
534                deserializer.deserialize_struct("CveId", FIELDS, CveIdVisitor)
535            }
536        }
537    }
538
539    #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
540    impl Serialize for CveYear {
541        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
542        where
543            S: Serializer,
544        {
545            serializer.serialize_newtype_struct("CveYear", &self.0)
546        }
547    }
548
549    #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
550    impl<'de> Deserialize<'de> for CveYear {
551        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
552        where
553            D: Deserializer<'de>,
554        {
555            struct CveYearVisitor;
556
557            impl<'de> Visitor<'de> for CveYearVisitor {
558                type Value = CveYear;
559
560                fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
561                    formatter.write_str("struct CveYear")
562                }
563
564                fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
565                where
566                    D: Deserializer<'de>,
567                {
568                    let year = deserializer.deserialize_u16(self)?;
569                    Ok(year)
570                }
571
572                fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E>
573                where
574                    E: de::Error,
575                {
576                    CveYear::new(v).map_err(E::custom)
577                }
578            }
579
580            deserializer.deserialize_newtype_struct("CveYear", CveYearVisitor)
581        }
582    }
583
584    #[cfg(test)]
585    mod tests {
586        use crate::{CveId, CveNumber, CveYear};
587        use claims::{assert_ok, assert_ok_eq};
588        use serde_assert::{Deserializer, Serializer, Token};
589        use serde_core::{Deserialize, Serialize};
590
591        // TODO: proptest any valid CveId should roundtrip binary
592        // TODO: proptest any valid CveId should roundtrip human-readable
593
594        #[test]
595        fn test_serialize_binary() -> Result<(), Box<dyn std::error::Error>> {
596            let serializer = Serializer::builder().is_human_readable(false).build();
597
598            let cve_id = CveId::new(1999.try_into()?, 1);
599
600            assert_ok_eq!(
601                cve_id.serialize(&serializer),
602                [
603                    Token::Struct {
604                        name: "CveId",
605                        len: 2
606                    },
607                    Token::Field("year"),
608                    Token::NewtypeStruct { name: "CveYear" },
609                    Token::U16(1999),
610                    Token::Field("number"),
611                    Token::U64(1),
612                    Token::StructEnd
613                ]
614            );
615
616            Ok(())
617        }
618
619        #[test]
620        fn test_serialize_human() -> Result<(), Box<dyn std::error::Error>> {
621            let serializer = Serializer::builder().is_human_readable(true).build();
622
623            let cve_id = CveId::new(1999.try_into()?, 1);
624
625            assert_ok_eq!(
626                cve_id.serialize(&serializer),
627                [Token::Str("CVE-1999-0001".to_string())]
628            );
629
630            Ok(())
631        }
632
633        #[test]
634        fn test_deserialize_binary() -> Result<(), Box<dyn std::error::Error>> {
635            let mut deserializer = Deserializer::builder([
636                Token::Struct {
637                    name: "CveId",
638                    len: 2,
639                },
640                Token::Field("year"),
641                Token::NewtypeStruct { name: "CveYear" },
642                Token::U16(1999),
643                Token::Field("number"),
644                Token::U64(1),
645                Token::StructEnd,
646            ])
647            .is_human_readable(false)
648            .build();
649
650            let cve_id = CveId::new(1999.try_into()?, 1);
651
652            assert_ok_eq!(CveId::deserialize(&mut deserializer), cve_id);
653
654            Ok(())
655        }
656
657        #[test]
658        fn test_deserialize_human() -> Result<(), Box<dyn std::error::Error>> {
659            let mut deserializer = Deserializer::builder([Token::Str("CVE-1999-0001".to_string())])
660                .is_human_readable(true)
661                .build();
662
663            let cve_id = CveId::new(1999.try_into()?, 1);
664
665            assert_ok_eq!(CveId::deserialize(&mut deserializer), cve_id);
666
667            Ok(())
668        }
669
670        #[test]
671        fn test_roundtrip_binary() {
672            let cve_id = CveId::new(CveYear::MIN, CveNumber::MIN);
673
674            let serializer = Serializer::builder().is_human_readable(false).build();
675            let mut deserializer = Deserializer::builder(assert_ok!(cve_id.serialize(&serializer)))
676                .is_human_readable(false)
677                .build();
678
679            assert_ok_eq!(CveId::deserialize(&mut deserializer), cve_id);
680        }
681
682        #[test]
683        fn test_roundtrip_human() {
684            let cve_id = CveId::new(CveYear::MAX, CveNumber::MAX);
685
686            let serializer = Serializer::builder().is_human_readable(true).build();
687            let mut deserializer = Deserializer::builder(assert_ok!(cve_id.serialize(&serializer)))
688                .is_human_readable(true)
689                .build();
690
691            assert_ok_eq!(CveId::deserialize(&mut deserializer), cve_id);
692        }
693    }
694}
695
696#[cfg(feature = "schemars")]
697mod schemars {
698    use crate::CveId;
699    use alloc::borrow::Cow;
700    use schemars::{JsonSchema, Schema, SchemaGenerator, json_schema};
701
702    /// JSON schema matching `cveId` from [CVE JSON record format](https://cveproject.github.io/cve-schema/schema/docs/)
703    #[cfg_attr(docsrs, doc(cfg(feature = "schemars")))]
704    impl JsonSchema for CveId {
705        fn schema_name() -> Cow<'static, str> {
706            "CveId".into()
707        }
708
709        fn schema_id() -> Cow<'static, str> {
710            "cve_id::CveId".into()
711        }
712
713        fn json_schema(_: &mut SchemaGenerator) -> Schema {
714            json_schema!({
715                "type": "string",
716                "pattern": r"^CVE-[0-9]{4}-[0-9]{4,19}$"
717            })
718        }
719
720        fn inline_schema() -> bool {
721            true
722        }
723    }
724
725    #[cfg(test)]
726    mod tests {
727        use crate::CveId;
728        use schemars::JsonSchema;
729
730        #[test]
731        fn test_jsonschema() {
732            fn assert_jsonschema<T: JsonSchema>() {}
733            assert_jsonschema::<CveId>();
734        }
735    }
736}
737
738#[cfg(test)]
739mod tests {
740    use super::*;
741
742    // TODO: proptest any str should not panic when parsing CveId
743    // TODO: proptest any valid year & number should parse back equal
744
745    #[test]
746    fn test_send() {
747        fn assert_send<T: Send>() {}
748        assert_send::<CveId>();
749    }
750
751    #[test]
752    fn test_sync() {
753        fn assert_sync<T: Sync>() {}
754        assert_sync::<CveId>();
755    }
756
757    #[test]
758    fn test_debug() -> Result<(), Box<dyn std::error::Error>> {
759        assert_eq!(
760            format!("{:?}", CveId::new(1999.try_into()?, 1)),
761            "CveId { year: CveYear(1999), number: 1 }"
762        );
763
764        Ok(())
765    }
766
767    #[test]
768    fn test_display() -> Result<(), Box<dyn std::error::Error>> {
769        assert_eq!(
770            format!("{}", CveId::new(1999.try_into()?, 1)),
771            "CVE-1999-0001"
772        );
773        assert_eq!(
774            format!("{}", CveId::new(1900.try_into()?, 424242)),
775            "CVE-1900-424242"
776        );
777
778        Ok(())
779    }
780
781    #[test]
782    fn test_from_str() -> Result<(), Box<dyn std::error::Error>> {
783        assert_eq!(
784            CveId::from_str("CVE-1999-0001")?,
785            CveId::new(1999.try_into()?, 1)
786        );
787        assert_eq!(
788            CveId::from_str("CVE-1900-424242")?,
789            CveId::new(1900.try_into()?, 424242)
790        );
791
792        assert_eq!(CveId::from_str("hurz").unwrap_err(), ParseCveIdError());
793        assert_eq!(
794            CveId::from_str("cve-1999-0001").unwrap_err(),
795            ParseCveIdError()
796        );
797
798        Ok(())
799    }
800
801    #[test]
802    fn test_parsecveiderror_display() {
803        assert_eq!(
804            format!("{}", CveId::from_str("hurz").unwrap_err()),
805            "can not parse CVE ID"
806        );
807    }
808
809    #[test]
810    fn test_fromstr() -> Result<(), Box<dyn std::error::Error>> {
811        assert_eq!("CVE-1999-0001".parse(), Ok(CveId::new(1999.try_into()?, 1)));
812
813        Ok(())
814    }
815
816    #[test]
817    fn test_isexampleortest() -> Result<(), Box<dyn std::error::Error>> {
818        assert!(CveId::new(1900.try_into()?, 666).is_example_or_test());
819        assert!(!CveId::new(1999.try_into()?, 1).is_example_or_test());
820
821        Ok(())
822    }
823
824    #[test]
825    fn test_min() -> Result<(), Box<dyn std::error::Error>> {
826        assert_eq!(
827            CveId::new(CveYear::MIN, CveNumber::MIN),
828            "CVE-0000-0000".parse()?
829        );
830
831        Ok(())
832    }
833
834    #[test]
835    fn test_max() -> Result<(), Box<dyn std::error::Error>> {
836        assert_eq!(
837            CveId::new(CveYear::MAX, CveNumber::MAX),
838            "CVE-9999-18446744073709551615".parse()?
839        );
840
841        Ok(())
842    }
843}
844
845#[cfg(doctest)]
846#[doc=include_str!("../README-crate.md")]
847mod readme {}