Coverage Report

Created: 2026-05-16 07:06

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/const-oid-0.10.2/src/parser.rs
Line
Count
Source
1
//! OID string parser with `const` support.
2
3
use crate::{Arc, Error, ObjectIdentifier, Result, encoder::Encoder};
4
5
/// Const-friendly OID string parser.
6
///
7
/// Parses an OID from the dotted string representation.
8
#[derive(Debug)]
9
pub(crate) struct Parser {
10
    /// Current arc in progress
11
    current_arc: Option<Arc>,
12
13
    /// BER/DER encoder
14
    encoder: Encoder<{ ObjectIdentifier::MAX_SIZE }>,
15
}
16
17
impl Parser {
18
    /// Parse an OID from a dot-delimited string e.g. `1.2.840.113549.1.1.1`
19
0
    pub(crate) const fn parse(s: &str) -> Result<Self> {
20
0
        let bytes = s.as_bytes();
21
22
0
        if bytes.is_empty() {
23
0
            return Err(Error::Empty);
24
0
        }
25
26
0
        match bytes[0] {
27
0
            b'0'..=b'9' => Self {
28
0
                current_arc: None,
29
0
                encoder: Encoder::new(),
30
0
            }
31
0
            .parse_bytes(bytes),
32
0
            actual => Err(Error::DigitExpected { actual }),
33
        }
34
0
    }
35
36
    /// Finish parsing, returning the result
37
0
    pub(crate) const fn finish(self) -> Result<ObjectIdentifier> {
38
0
        self.encoder.finish()
39
0
    }
40
41
    /// Parse the remaining bytes
42
0
    const fn parse_bytes(mut self, bytes: &[u8]) -> Result<Self> {
43
0
        match bytes {
44
            // TODO(tarcieri): use `?` when stable in `const fn`
45
0
            [] => match self.current_arc {
46
0
                Some(arc) => match self.encoder.arc(arc) {
47
0
                    Ok(encoder) => {
48
0
                        self.encoder = encoder;
49
0
                        Ok(self)
50
                    }
51
0
                    Err(err) => Err(err),
52
                },
53
0
                None => Err(Error::TrailingDot),
54
            },
55
0
            [byte @ b'0'..=b'9', remaining @ ..] => {
56
0
                let digit = byte.saturating_sub(b'0');
57
0
                let arc = match self.current_arc {
58
0
                    Some(arc) => arc,
59
0
                    None => 0,
60
                };
61
62
                // TODO(tarcieri): use `and_then` when const traits are stable
63
0
                self.current_arc = match arc.checked_mul(10) {
64
0
                    Some(arc) => match arc.checked_add(digit as Arc) {
65
0
                        None => return Err(Error::ArcTooBig),
66
0
                        Some(arc) => Some(arc),
67
                    },
68
0
                    None => return Err(Error::ArcTooBig),
69
                };
70
0
                self.parse_bytes(remaining)
71
            }
72
0
            [b'.', remaining @ ..] => {
73
0
                match self.current_arc {
74
0
                    Some(arc) => {
75
0
                        if remaining.is_empty() {
76
0
                            return Err(Error::TrailingDot);
77
0
                        }
78
79
                        // TODO(tarcieri): use `?` when stable in `const fn`
80
0
                        match self.encoder.arc(arc) {
81
0
                            Ok(encoder) => {
82
0
                                self.encoder = encoder;
83
0
                                self.current_arc = None;
84
0
                                self.parse_bytes(remaining)
85
                            }
86
0
                            Err(err) => Err(err),
87
                        }
88
                    }
89
0
                    None => Err(Error::RepeatedDot),
90
                }
91
            }
92
0
            [byte, ..] => Err(Error::DigitExpected { actual: *byte }),
93
        }
94
0
    }
95
}
96
97
#[cfg(test)]
98
#[allow(clippy::unwrap_used)]
99
mod tests {
100
    use super::Parser;
101
    use crate::Error;
102
103
    #[test]
104
    fn parse() {
105
        let oid = Parser::parse("1.23.456").unwrap().finish().unwrap();
106
        assert_eq!(oid, "1.23.456".parse().unwrap());
107
    }
108
109
    #[test]
110
    fn reject_empty_string() {
111
        assert_eq!(Parser::parse("").err().unwrap(), Error::Empty);
112
    }
113
114
    #[test]
115
    fn reject_non_digits() {
116
        assert_eq!(
117
            Parser::parse("X").err().unwrap(),
118
            Error::DigitExpected { actual: b'X' }
119
        );
120
121
        assert_eq!(
122
            Parser::parse("1.2.X").err().unwrap(),
123
            Error::DigitExpected { actual: b'X' }
124
        );
125
    }
126
127
    #[test]
128
    fn reject_trailing_dot() {
129
        assert_eq!(Parser::parse("1.23.").err().unwrap(), Error::TrailingDot);
130
    }
131
}