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