Skip to main content

git_lfs_pointer/
oid.rs

1use std::fmt;
2use std::str::FromStr;
3
4/// Hex form of the SHA-256 of the empty input.
5///
6/// Used as the OID of the [empty pointer], which represents an
7/// empty file (see the spec).
8///
9/// [empty pointer]: crate::Pointer::empty
10pub const EMPTY_HEX: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
11
12/// A SHA-256 object identifier.
13///
14/// Stored as the raw 32 bytes; rendered as 64 lowercase hex characters by
15/// [`fmt::Display`]. Construction via [`Oid::from_hex`] enforces the spec's
16/// strict-lowercase, exactly-64-hex-character format.
17#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
18pub struct Oid([u8; 32]);
19
20impl Oid {
21    /// SHA-256 of the empty input. The OID of the [empty pointer].
22    ///
23    /// [empty pointer]: crate::Pointer::empty
24    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    /// Construct an OID from raw 32 hash bytes (e.g. the output of a
31    /// streaming SHA-256 hasher).
32    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
33        Self(bytes)
34    }
35
36    /// Parse an OID from its 64-character lowercase hex form.
37    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    /// Borrow the raw 32-byte hash.
52    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        // Uppercase A-F is rejected on purpose: the spec mandates lowercase.
62        _ => 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/// Why [`Oid::from_hex`] (or `Oid::from_str`) failed.
89#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
90pub enum OidParseError {
91    /// Input wasn't exactly 64 characters long.
92    #[error("oid must be 64 hex characters, got {0}")]
93    InvalidLength(usize),
94    /// Input contained a character outside `0-9a-f` (uppercase
95    /// rejected; the spec mandates lowercase).
96    #[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        // Spec mandates lowercase; uppercase A-F is not accepted.
134        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&"; // 65 chars
151        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}