const_oid/
parser.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
//! OID string parser with `const` support.

use crate::{encoder::Encoder, Arc, ObjectIdentifier};

/// Const-friendly OID string parser.
///
/// Parses an OID from the dotted string representation.
pub(crate) struct Parser {
    /// Current arc in progress
    current_arc: Arc,

    /// BER/DER encoder
    encoder: Encoder,
}

impl Parser {
    /// Parse an OID from a dot-delimited string e.g. `1.2.840.113549.1.1.1`
    pub(crate) const fn parse(s: &str) -> Self {
        let bytes = s.as_bytes();
        const_assert!(!bytes.is_empty(), "OID string is empty");
        const_assert!(
            matches!(bytes[0], b'0'..=b'9'),
            "OID must start with a digit"
        );

        let current_arc = 0;
        let encoder = Encoder::new();
        Self {
            current_arc,
            encoder,
        }
        .parse_bytes(bytes)
    }

    /// Finish parsing, returning the result
    pub(crate) const fn finish(self) -> ObjectIdentifier {
        self.encoder.finish()
    }

    /// Parse the remaining bytes
    const fn parse_bytes(mut self, bytes: &[u8]) -> Self {
        match bytes {
            [] => {
                self.encoder = self.encoder.encode(self.current_arc);
                self
            }
            [byte @ b'0'..=b'9', remaining @ ..] => {
                let digit = byte.saturating_sub(b'0');
                self.current_arc = self.current_arc * 10 + digit as Arc;
                self.parse_bytes(remaining)
            }
            [b'.', remaining @ ..] => {
                const_assert!(!remaining.is_empty(), "invalid trailing '.' in OID");
                self.encoder = self.encoder.encode(self.current_arc);
                self.current_arc = 0;
                self.parse_bytes(remaining)
            }
            [byte, ..] => {
                const_assert!(
                    matches!(byte, b'0'..=b'9' | b'.'),
                    "invalid character in OID"
                );

                // Unreachable (checked by above `const_assert!`)
                // Needed for match exhaustiveness and matching types
                self
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::Parser;

    #[test]
    fn parse() {
        let oid = Parser::parse("1.23.456").finish();
        assert_eq!(oid, "1.23.456".parse().unwrap());
    }

    #[test]
    #[should_panic]
    fn reject_empty_string() {
        Parser::parse("");
    }

    #[test]
    #[should_panic]
    fn reject_non_digits() {
        Parser::parse("X");
    }

    #[test]
    #[should_panic]
    fn reject_trailing_dot() {
        Parser::parse("1.23.");
    }
}