use crate::{GitError, Result};
use std::{fmt, str::FromStr};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum HashKind {
Sha1,
Sha256,
}
impl HashKind {
#[must_use]
pub const fn bytes(self) -> usize {
match self {
Self::Sha1 => 20,
Self::Sha256 => 32,
}
}
#[must_use]
pub const fn hex_len(self) -> usize {
self.bytes() * 2
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct ObjectId {
bytes: [u8; 32],
len: u8,
}
impl ObjectId {
pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
if bytes.len() != HashKind::Sha1.bytes() && bytes.len() != HashKind::Sha256.bytes() {
return Err(GitError::InvalidFormat(format!(
"object id has {} bytes",
bytes.len()
)));
}
let mut value = [0_u8; 32];
value[..bytes.len()].copy_from_slice(bytes);
Ok(Self {
bytes: value,
len: u8::try_from(bytes.len()).expect("supported hashes fit in u8"),
})
}
pub fn from_hex_for(hex: &str, kind: HashKind) -> Result<Self> {
if hex.len() != kind.hex_len() {
return Err(GitError::InvalidFormat(format!(
"expected {} hexadecimal object-id characters",
kind.hex_len()
)));
}
let mut bytes = [0_u8; 32];
for (index, pair) in hex.as_bytes().chunks_exact(2).enumerate() {
bytes[index] = decode_hex(pair[0])? << 4 | decode_hex(pair[1])?;
}
Ok(Self {
bytes,
len: u8::try_from(kind.bytes()).expect("supported hashes fit in u8"),
})
}
#[must_use]
pub const fn kind(self) -> HashKind {
if self.len == 20 {
HashKind::Sha1
} else {
HashKind::Sha256
}
}
#[must_use]
pub fn as_bytes(&self) -> &[u8] {
&self.bytes[..usize::from(self.len)]
}
#[must_use]
pub fn to_hex(self) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut output = String::with_capacity(usize::from(self.len) * 2);
for byte in self.as_bytes() {
output.push(char::from(HEX[usize::from(byte >> 4)]));
output.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
output
}
}
impl fmt::Debug for ObjectId {
fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
output
.debug_tuple("ObjectId")
.field(&self.to_hex())
.finish()
}
}
impl fmt::Display for ObjectId {
fn fmt(&self, output: &mut fmt::Formatter<'_>) -> fmt::Result {
output.write_str(&self.to_hex())
}
}
impl FromStr for ObjectId {
type Err = GitError;
fn from_str(value: &str) -> Result<Self> {
match value.len() {
40 => Self::from_hex_for(value, HashKind::Sha1),
64 => Self::from_hex_for(value, HashKind::Sha256),
length => Err(GitError::InvalidFormat(format!(
"unsupported object-id length {length}"
))),
}
}
}
fn decode_hex(value: u8) -> Result<u8> {
match value {
b'0'..=b'9' => Ok(value - b'0'),
b'a'..=b'f' => Ok(value - b'a' + 10),
b'A'..=b'F' => Ok(value - b'A' + 10),
_ => Err(GitError::InvalidFormat(
"object id contains non-hexadecimal characters".to_owned(),
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_sha1_and_sha256_roundtrip() {
let sha256 = "ab".repeat(32);
for value in ["0123456789abcdef0123456789abcdef01234567", sha256.as_str()] {
let id: ObjectId = value.parse().unwrap();
assert_eq!(id.to_string(), value);
}
}
#[test]
fn rejects_invalid_object_ids() {
assert!("abc".parse::<ObjectId>().is_err());
assert!(ObjectId::from_hex_for(&"z".repeat(40), HashKind::Sha1).is_err());
assert!(ObjectId::from_bytes(&[0; 21]).is_err());
}
#[test]
fn exposes_hash_kind_bytes_and_debug_value() {
let sha1 = ObjectId::from_bytes(&[0xab; 20]).unwrap();
let sha256 = ObjectId::from_hex_for(&"CD".repeat(32), HashKind::Sha256).unwrap();
assert_eq!(sha1.kind(), HashKind::Sha1);
assert_eq!(sha1.as_bytes(), &[0xab; 20]);
assert!(format!("{sha1:?}").contains("abab"));
assert_eq!(sha256.kind(), HashKind::Sha256);
assert_eq!(sha256.to_hex(), "cd".repeat(32));
}
}