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. Used as the OID of the empty
5/// pointer (which represents an empty file — see `docs/spec.md`).
6pub const EMPTY_HEX: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
7
8/// A SHA-256 object identifier.
9///
10/// Stored as the raw 32 bytes; rendered as 64 lowercase hex characters by
11/// [`fmt::Display`]. Construction via [`Oid::from_hex`] enforces the spec's
12/// strict-lowercase, exactly-64-hex-character format.
13#[derive(Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
14pub struct Oid([u8; 32]);
15
16impl Oid {
17    /// SHA-256 of the empty input. The OID of the [empty pointer].
18    ///
19    /// [empty pointer]: crate::Pointer::empty
20    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    /// Construct an OID from raw 32 hash bytes (e.g. the output of a
27    /// streaming SHA-256 hasher).
28    pub const fn from_bytes(bytes: [u8; 32]) -> Self {
29        Self(bytes)
30    }
31
32    /// Parse an OID from its 64-character lowercase hex form.
33    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    /// Borrow the raw 32-byte hash.
48    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        // Uppercase A-F is rejected on purpose: the spec mandates lowercase.
58        _ => 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        // Spec mandates lowercase; uppercase A-F is not accepted.
126        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&"; // 65 chars
143        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}