Coverage Report

Created: 2026-06-07 06:39

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/const-oid-0.9.6/src/parser.rs
Line
Count
Source
1
//! OID string parser with `const` support.
2
3
use crate::{encoder::Encoder, Arc, Error, ObjectIdentifier, Result};
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: Arc,
12
13
    /// BER/DER encoder
14
    encoder: Encoder,
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: 0,
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.encoder.arc(self.current_arc) {
46
0
                Ok(encoder) => {
47
0
                    self.encoder = encoder;
48
0
                    Ok(self)
49
                }
50
0
                Err(err) => Err(err),
51
            },
52
            // TODO(tarcieri): checked arithmetic
53
            #[allow(clippy::integer_arithmetic)]
54
0
            [byte @ b'0'..=b'9', remaining @ ..] => {
55
0
                let digit = byte.saturating_sub(b'0');
56
0
                self.current_arc = self.current_arc * 10 + digit as Arc;
57
0
                self.parse_bytes(remaining)
58
            }
59
0
            [b'.', remaining @ ..] => {
60
0
                if remaining.is_empty() {
61
0
                    return Err(Error::TrailingDot);
62
0
                }
63
64
                // TODO(tarcieri): use `?` when stable in `const fn`
65
0
                match self.encoder.arc(self.current_arc) {
66
0
                    Ok(encoder) => {
67
0
                        self.encoder = encoder;
68
0
                        self.current_arc = 0;
69
0
                        self.parse_bytes(remaining)
70
                    }
71
0
                    Err(err) => Err(err),
72
                }
73
            }
74
0
            [byte, ..] => Err(Error::DigitExpected { actual: *byte }),
75
        }
76
0
    }
77
}
78
79
#[cfg(test)]
80
mod tests {
81
    use super::Parser;
82
    use crate::Error;
83
84
    #[test]
85
    fn parse() {
86
        let oid = Parser::parse("1.23.456").unwrap().finish().unwrap();
87
        assert_eq!(oid, "1.23.456".parse().unwrap());
88
    }
89
90
    #[test]
91
    fn reject_empty_string() {
92
        assert_eq!(Parser::parse("").err().unwrap(), Error::Empty);
93
    }
94
95
    #[test]
96
    fn reject_non_digits() {
97
        assert_eq!(
98
            Parser::parse("X").err().unwrap(),
99
            Error::DigitExpected { actual: b'X' }
100
        );
101
102
        assert_eq!(
103
            Parser::parse("1.2.X").err().unwrap(),
104
            Error::DigitExpected { actual: b'X' }
105
        );
106
    }
107
108
    #[test]
109
    fn reject_trailing_dot() {
110
        assert_eq!(Parser::parse("1.23.").err().unwrap(), Error::TrailingDot);
111
    }
112
}