1use std::fmt;
2use std::str::FromStr;
3
4pub const EMPTY_HEX: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
7
8#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
14pub struct Oid([u8; 32]);
15
16impl Oid {
17 pub const EMPTY: Oid = Oid([
21 0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9,
22 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52,
23 0xb8, 0x55,
24 ]);
25
26 pub const fn from_bytes(bytes: [u8; 32]) -> Self {
29 Self(bytes)
30 }
31
32 pub fn from_hex(s: &str) -> Result<Self, OidParseError> {
34 if s.len() != 64 {
35 return Err(OidParseError::InvalidLength(s.len()));
36 }
37 let mut out = [0u8; 32];
38 let bytes = s.as_bytes();
39 for (i, byte) in out.iter_mut().enumerate() {
40 let hi = hex_digit(bytes[i * 2])?;
41 let lo = hex_digit(bytes[i * 2 + 1])?;
42 *byte = (hi << 4) | lo;
43 }
44 Ok(Oid(out))
45 }
46
47 pub fn as_bytes(&self) -> &[u8; 32] {
49 &self.0
50 }
51}
52
53fn hex_digit(b: u8) -> Result<u8, OidParseError> {
54 match b {
55 b'0'..=b'9' => Ok(b - b'0'),
56 b'a'..=b'f' => Ok(b - b'a' + 10),
57 _ => Err(OidParseError::InvalidCharacter(b as char)),
59 }
60}
61
62impl fmt::Display for Oid {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 for byte in &self.0 {
65 write!(f, "{byte:02x}")?;
66 }
67 Ok(())
68 }
69}
70
71impl fmt::Debug for Oid {
72 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
73 write!(f, "Oid({self})")
74 }
75}
76
77impl FromStr for Oid {
78 type Err = OidParseError;
79 fn from_str(s: &str) -> Result<Self, Self::Err> {
80 Oid::from_hex(s)
81 }
82}
83
84#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
85pub enum OidParseError {
86 #[error("oid must be 64 hex characters, got {0}")]
87 InvalidLength(usize),
88 #[error("oid contains invalid character {0:?} (must be lowercase 0-9a-f)")]
89 InvalidCharacter(char),
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95
96 #[test]
97 fn empty_const_matches_empty_hex() {
98 assert_eq!(Oid::EMPTY, Oid::from_hex(EMPTY_HEX).unwrap());
99 assert_eq!(Oid::EMPTY.to_string(), EMPTY_HEX);
100 }
101
102 #[test]
103 fn round_trip_hex() {
104 let hex = "4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393";
105 let oid = Oid::from_hex(hex).unwrap();
106 assert_eq!(oid.to_string(), hex);
107 }
108
109 #[test]
110 fn rejects_wrong_length() {
111 assert_eq!(Oid::from_hex(""), Err(OidParseError::InvalidLength(0)));
112 assert_eq!(Oid::from_hex("abc"), Err(OidParseError::InvalidLength(3)));
113 assert_eq!(
114 Oid::from_hex(&"a".repeat(63)),
115 Err(OidParseError::InvalidLength(63))
116 );
117 assert_eq!(
118 Oid::from_hex(&"a".repeat(65)),
119 Err(OidParseError::InvalidLength(65))
120 );
121 }
122
123 #[test]
124 fn rejects_uppercase() {
125 let upper = "4D7A214614AB2935C943F9E0FF69D22EADBB8F32B1258DAAA5E2CA24D17E2393";
127 assert_eq!(
128 Oid::from_hex(upper),
129 Err(OidParseError::InvalidCharacter('D'))
130 );
131 }
132
133 #[test]
134 fn rejects_non_hex() {
135 let mut bad = "a".repeat(63);
136 bad.push('z');
137 assert_eq!(
138 Oid::from_hex(&bad),
139 Err(OidParseError::InvalidCharacter('z'))
140 );
141
142 let trailing_amp = "4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393&"; assert_eq!(
144 Oid::from_hex(trailing_amp),
145 Err(OidParseError::InvalidLength(65))
146 );
147 }
148
149 #[test]
150 fn from_str_works() {
151 let hex = "4d7a214614ab2935c943f9e0ff69d22eadbb8f32b1258daaa5e2ca24d17e2393";
152 let oid: Oid = hex.parse().unwrap();
153 assert_eq!(oid.to_string(), hex);
154 }
155}