Skip to main content

gsym_cache/
build_id.rs

1use std::borrow::Borrow;
2use std::fmt;
3use std::str::FromStr;
4
5/// Longest build identifier accepted by the conventional `.build-id` layout.
6///
7/// At 126 bytes, the second path component remains within the common 255-byte
8/// filesystem name limit after hexadecimal encoding and adding `.gsym`.
9pub(crate) const MAX_BUILD_ID_LEN: usize = 126;
10
11/// A validated, owned binary build identifier.
12///
13/// Identifiers can be constructed from raw bytes or parsed from hexadecimal:
14///
15/// ```
16/// use gsym_cache::BuildId;
17///
18/// let build_id: BuildId = "0123abcdef".parse()?;
19/// assert_eq!(build_id.as_bytes(), &[0x01, 0x23, 0xab, 0xcd, 0xef]);
20/// assert_eq!(build_id.to_string(), "0123abcdef");
21/// # Ok::<(), gsym_cache::BuildIdError>(())
22/// ```
23#[derive(Clone, Eq, Hash, Ord, PartialEq, PartialOrd)]
24#[cfg_attr(docsrs, doc(cfg(feature = "lookup")))]
25pub struct BuildId(Box<[u8]>);
26
27impl BuildId {
28    /// Validates and copies a binary build identifier.
29    ///
30    /// # Errors
31    ///
32    /// Returns an error for an empty identifier or one too long for the cache
33    /// layout.
34    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    /// Returns the raw build-identifier bytes.
46    #[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/// Build identifier validation error.
163#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
164#[non_exhaustive]
165#[cfg_attr(docsrs, doc(cfg(feature = "lookup")))]
166pub enum BuildIdError {
167    /// Build identifiers used as cache keys may not be empty.
168    #[error("build identifier is empty")]
169    Empty,
170    /// The encoded identifier would exceed the filesystem component limit.
171    #[error("build identifier is {length} bytes; maximum is {maximum}")]
172    TooLong {
173        /// Supplied byte length.
174        length: usize,
175        /// Maximum accepted byte length.
176        maximum: usize,
177    },
178    /// A hexadecimal representation contains an incomplete byte.
179    #[error("hexadecimal build identifier has odd length {length}")]
180    OddHexLength {
181        /// Supplied string length in bytes.
182        length: usize,
183    },
184    /// A hexadecimal representation contains a non-hexadecimal byte.
185    #[error("invalid hexadecimal byte {byte:#04x} at index {index}")]
186    InvalidHex {
187        /// Byte index in the supplied string.
188        index: usize,
189        /// Rejected byte.
190        byte: u8,
191    },
192}