Skip to main content

weavatrix_git/
oid.rs

1use crate::{GitError, Result};
2use std::{fmt, str::FromStr};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
5pub enum HashKind {
6    Sha1,
7    Sha256,
8}
9
10impl HashKind {
11    #[must_use]
12    pub const fn bytes(self) -> usize {
13        match self {
14            Self::Sha1 => 20,
15            Self::Sha256 => 32,
16        }
17    }
18
19    #[must_use]
20    pub const fn hex_len(self) -> usize {
21        self.bytes() * 2
22    }
23}
24
25#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
26pub struct ObjectId {
27    bytes: [u8; 32],
28    len: u8,
29}
30
31impl ObjectId {
32    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
33        if bytes.len() != HashKind::Sha1.bytes() && bytes.len() != HashKind::Sha256.bytes() {
34            return Err(GitError::InvalidFormat(format!(
35                "object id has {} bytes",
36                bytes.len()
37            )));
38        }
39        let mut value = [0_u8; 32];
40        value[..bytes.len()].copy_from_slice(bytes);
41        Ok(Self {
42            bytes: value,
43            len: u8::try_from(bytes.len()).expect("supported hashes fit in u8"),
44        })
45    }
46
47    pub fn from_hex_for(hex: &str, kind: HashKind) -> Result<Self> {
48        if hex.len() != kind.hex_len() {
49            return Err(GitError::InvalidFormat(format!(
50                "expected {} hexadecimal object-id characters",
51                kind.hex_len()
52            )));
53        }
54        let mut bytes = [0_u8; 32];
55        for (index, pair) in hex.as_bytes().chunks_exact(2).enumerate() {
56            bytes[index] = decode_hex(pair[0])? << 4 | decode_hex(pair[1])?;
57        }
58        Ok(Self {
59            bytes,
60            len: u8::try_from(kind.bytes()).expect("supported hashes fit in u8"),
61        })
62    }
63
64    #[must_use]
65    pub const fn kind(self) -> HashKind {
66        if self.len == 20 {
67            HashKind::Sha1
68        } else {
69            HashKind::Sha256
70        }
71    }
72
73    #[must_use]
74    pub fn as_bytes(&self) -> &[u8] {
75        &self.bytes[..usize::from(self.len)]
76    }
77
78    #[must_use]
79    pub fn to_hex(self) -> String {
80        const HEX: &[u8; 16] = b"0123456789abcdef";
81        let mut output = String::with_capacity(usize::from(self.len) * 2);
82        for byte in self.as_bytes() {
83            output.push(char::from(HEX[usize::from(byte >> 4)]));
84            output.push(char::from(HEX[usize::from(byte & 0x0f)]));
85        }
86        output
87    }
88}
89
90impl fmt::Debug for ObjectId {
91    fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
92        output
93            .debug_tuple("ObjectId")
94            .field(&self.to_hex())
95            .finish()
96    }
97}
98
99impl fmt::Display for ObjectId {
100    fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
101        output.write_str(&self.to_hex())
102    }
103}
104
105impl FromStr for ObjectId {
106    type Err = GitError;
107
108    fn from_str(value: &str) -> Result<Self> {
109        match value.len() {
110            40 => Self::from_hex_for(value, HashKind::Sha1),
111            64 => Self::from_hex_for(value, HashKind::Sha256),
112            length => Err(GitError::InvalidFormat(format!(
113                "unsupported object-id length {length}"
114            ))),
115        }
116    }
117}
118
119fn decode_hex(value: u8) -> Result<u8> {
120    match value {
121        b'0'..=b'9' => Ok(value - b'0'),
122        b'a'..=b'f' => Ok(value - b'a' + 10),
123        b'A'..=b'F' => Ok(value - b'A' + 10),
124        _ => Err(GitError::InvalidFormat(
125            "object id contains non-hexadecimal characters".to_owned(),
126        )),
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn parses_sha1_and_sha256_roundtrip() {
136        let sha256 = "ab".repeat(32);
137        for value in ["0123456789abcdef0123456789abcdef01234567", sha256.as_str()] {
138            let id: ObjectId = value.parse().unwrap();
139            assert_eq!(id.to_string(), value);
140        }
141    }
142
143    #[test]
144    fn rejects_invalid_object_ids() {
145        assert!("abc".parse::<ObjectId>().is_err());
146        assert!(ObjectId::from_hex_for(&"z".repeat(40), HashKind::Sha1).is_err());
147        assert!(ObjectId::from_bytes(&[0; 21]).is_err());
148    }
149
150    #[test]
151    fn exposes_hash_kind_bytes_and_debug_value() {
152        let sha1 = ObjectId::from_bytes(&[0xab; 20]).unwrap();
153        let sha256 = ObjectId::from_hex_for(&"CD".repeat(32), HashKind::Sha256).unwrap();
154        assert_eq!(sha1.kind(), HashKind::Sha1);
155        assert_eq!(sha1.as_bytes(), &[0xab; 20]);
156        assert!(format!("{sha1:?}").contains("abab"));
157        assert_eq!(sha256.kind(), HashKind::Sha256);
158        assert_eq!(sha256.to_hex(), "cd".repeat(32));
159    }
160}