//! OID string parser with `const` support. use crate::{encoder::Encoder, Arc, Error, ObjectIdentifier, Result}; /// Const-friendly OID string parser. /// /// Parses an OID from the dotted string representation. #[derive(Debug)] 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) -> Result { let bytes = s.as_bytes(); if bytes.is_empty() { return Err(Error::Empty); } match bytes[0] { b'0'..=b'9' => Self { current_arc: 0, encoder: Encoder::new(), } .parse_bytes(bytes), actual => Err(Error::DigitExpected { actual }), } } /// Finish parsing, returning the result pub(crate) const fn finish(self) -> Result { self.encoder.finish() } /// Parse the remaining bytes const fn parse_bytes(mut self, bytes: &[u8]) -> Result { match bytes { // TODO(tarcieri): use `?` when stable in `const fn` [] => match self.encoder.arc(self.current_arc) { Ok(encoder) => { self.encoder = encoder; Ok(self) } Err(err) => Err(err), }, // TODO(tarcieri): checked arithmetic #[allow(clippy::integer_arithmetic)] [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 @ ..] => { if remaining.is_empty() { return Err(Error::TrailingDot); } // TODO(tarcieri): use `?` when stable in `const fn` match self.encoder.arc(self.current_arc) { Ok(encoder) => { self.encoder = encoder; self.current_arc = 0; self.parse_bytes(remaining) } Err(err) => Err(err), } } [byte, ..] => Err(Error::DigitExpected { actual: *byte }), } } } #[cfg(test)] mod tests { use super::Parser; use crate::Error; #[test] fn parse() { let oid = Parser::parse("1.23.456").unwrap().finish().unwrap(); assert_eq!(oid, "1.23.456".parse().unwrap()); } #[test] fn reject_empty_string() { assert_eq!(Parser::parse("").err().unwrap(), Error::Empty); } #[test] fn reject_non_digits() { assert_eq!( Parser::parse("X").err().unwrap(), Error::DigitExpected { actual: b'X' } ); assert_eq!( Parser::parse("1.2.X").err().unwrap(), Error::DigitExpected { actual: b'X' } ); } #[test] fn reject_trailing_dot() { assert_eq!(Parser::parse("1.23.").err().unwrap(), Error::TrailingDot); } }