1use std::borrow::Borrow;
2use std::fmt;
3use std::str::FromStr;
4
5pub(crate) const MAX_BUILD_ID_LEN: usize = 126;
10
11#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
24#[cfg_attr(docsrs, doc(cfg(feature = "lookup")))]
25pub struct BuildId(Box<[u8]>);
26
27impl BuildId {
28 pub fn new(bytes: impl AsRef<[u8]>) -> Result<Self, BuildIdError> {
35 let bytes = bytes.as_ref();
36 validate(bytes)?;
37 Ok(Self(bytes.into()))
38 }
39
40 fn from_boxed(bytes: Box<[u8]>) -> Result<Self, BuildIdError> {
41 validate(&bytes)?;
42 Ok(Self(bytes))
43 }
44
45 #[must_use]
47 pub fn as_bytes(&self) -> &[u8] {
48 &self.0
49 }
50}
51
52const fn validate(bytes: &[u8]) -> Result<(), BuildIdError> {
53 if bytes.is_empty() {
54 return Err(BuildIdError::Empty);
55 }
56 if bytes.len() > MAX_BUILD_ID_LEN {
57 return Err(BuildIdError::TooLong {
58 length: bytes.len(),
59 maximum: MAX_BUILD_ID_LEN,
60 });
61 }
62 Ok(())
63}
64
65impl AsRef<[u8]> for BuildId {
66 fn as_ref(&self) -> &[u8] {
67 self.as_bytes()
68 }
69}
70
71impl Borrow<[u8]> for BuildId {
72 fn borrow(&self) -> &[u8] {
73 self.as_bytes()
74 }
75}
76
77impl TryFrom<&[u8]> for BuildId {
78 type Error = BuildIdError;
79
80 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
81 Self::new(bytes)
82 }
83}
84
85impl TryFrom<Vec<u8>> for BuildId {
86 type Error = BuildIdError;
87
88 fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
89 Self::from_boxed(bytes.into_boxed_slice())
90 }
91}
92
93impl TryFrom<Box<[u8]>> for BuildId {
94 type Error = BuildIdError;
95
96 fn try_from(bytes: Box<[u8]>) -> Result<Self, Self::Error> {
97 Self::from_boxed(bytes)
98 }
99}
100
101impl fmt::Display for BuildId {
102 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
103 for byte in &self.0 {
104 write!(formatter, "{byte:02x}")?;
105 }
106 Ok(())
107 }
108}
109
110impl fmt::Debug for BuildId {
111 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
112 formatter.write_str("BuildId(")?;
113 fmt::Display::fmt(self, formatter)?;
114 formatter.write_str(")")
115 }
116}
117
118impl FromStr for BuildId {
119 type Err = BuildIdError;
120
121 fn from_str(encoded: &str) -> Result<Self, Self::Err> {
122 if !encoded.len().is_multiple_of(2) {
123 return Err(BuildIdError::OddHexLength {
124 length: encoded.len(),
125 });
126 }
127 let byte_length = encoded.len() / 2;
128 if byte_length > MAX_BUILD_ID_LEN {
129 return Err(BuildIdError::TooLong {
130 length: byte_length,
131 maximum: MAX_BUILD_ID_LEN,
132 });
133 }
134 let mut bytes = Vec::with_capacity(byte_length);
135 let (pairs, _) = encoded.as_bytes().as_chunks::<2>();
136 for (pair_index, [high, low]) in pairs.iter().enumerate() {
137 let high_index = pair_index.saturating_mul(2);
138 let high = hex_nibble(*high).ok_or(BuildIdError::InvalidHex {
139 index: high_index,
140 byte: *high,
141 })?;
142 let low_index = high_index.saturating_add(1);
143 let low = hex_nibble(*low).ok_or(BuildIdError::InvalidHex {
144 index: low_index,
145 byte: *low,
146 })?;
147 bytes.push((high << 4) | low);
148 }
149 Self::try_from(bytes)
150 }
151}
152
153pub(crate) const fn hex_nibble(byte: u8) -> Option<u8> {
154 match byte {
155 b'0'..=b'9' => Some(byte.wrapping_sub(b'0')),
156 b'a'..=b'f' => Some(byte.wrapping_sub(b'a').wrapping_add(10)),
157 b'A'..=b'F' => Some(byte.wrapping_sub(b'A').wrapping_add(10)),
158 _ => None,
159 }
160}
161
162#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
164#[non_exhaustive]
165#[cfg_attr(docsrs, doc(cfg(feature = "lookup")))]
166pub enum BuildIdError {
167 #[error("build identifier is empty")]
169 Empty,
170 #[error("build identifier is {length} bytes; maximum is {maximum}")]
172 TooLong {
173 length: usize,
175 maximum: usize,
177 },
178 #[error("hexadecimal build identifier has odd length {length}")]
180 OddHexLength {
181 length: usize,
183 },
184 #[error("invalid hexadecimal byte {byte:#04x} at index {index}")]
186 InvalidHex {
187 index: usize,
189 byte: u8,
191 },
192}