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