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}