use alloc::string::String;
use crate::error::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum HashAlgo {
Sha1,
Sha256,
}
impl HashAlgo {
pub const fn raw_len(self) -> usize {
match self {
HashAlgo::Sha1 => 20,
HashAlgo::Sha256 => 32,
}
}
pub const fn hex_len(self) -> usize {
self.raw_len() * 2
}
pub const fn name(self) -> &'static str {
match self {
HashAlgo::Sha1 => "sha1",
HashAlgo::Sha256 => "sha256",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct ObjectId {
algo: HashAlgo,
bytes: [u8; 32],
}
impl ObjectId {
pub fn from_bytes(algo: HashAlgo, raw: &[u8]) -> Result<Self, Error> {
if raw.len() != algo.raw_len() {
return Err(Error::InvalidOid(invalid_len_msg(algo, raw.len())));
}
let mut bytes = [0u8; 32];
bytes[..raw.len()].copy_from_slice(raw);
Ok(ObjectId { algo, bytes })
}
pub fn from_hex(algo: HashAlgo, hex: &str) -> Result<Self, Error> {
if hex.len() != algo.hex_len() {
return Err(Error::InvalidOid(invalid_len_msg(algo, hex.len() / 2)));
}
let mut bytes = [0u8; 32];
let raw = &mut bytes[..algo.raw_len()];
decode_hex_into(hex, raw)?;
Ok(ObjectId { algo, bytes })
}
pub fn algo(&self) -> HashAlgo {
self.algo
}
pub fn as_bytes(&self) -> &[u8] {
&self.bytes[..self.algo.raw_len()]
}
pub fn zero(algo: HashAlgo) -> Self {
ObjectId {
algo,
bytes: [0u8; 32],
}
}
pub fn is_zero(&self) -> bool {
self.as_bytes().iter().all(|&b| b == 0)
}
pub fn to_hex(&self) -> String {
let raw = self.as_bytes();
let mut s = String::with_capacity(raw.len() * 2);
const HEX: &[u8; 16] = b"0123456789abcdef";
for &b in raw {
s.push(HEX[(b >> 4) as usize] as char);
s.push(HEX[(b & 0xf) as usize] as char);
}
s
}
pub fn to_hex_short(&self, n: usize) -> String {
let full = self.to_hex();
let n = n.min(full.len());
full[..n].into()
}
}
impl core::fmt::Display for ObjectId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
const HEX: &[u8; 16] = b"0123456789abcdef";
for &b in self.as_bytes() {
f.write_str(core::str::from_utf8(&[HEX[(b >> 4) as usize]]).unwrap())?;
f.write_str(core::str::from_utf8(&[HEX[(b & 0xf) as usize]]).unwrap())?;
}
Ok(())
}
}
impl core::fmt::Debug for ObjectId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "ObjectId({}:{})", self.algo.name(), self.to_hex())
}
}
impl PartialOrd for ObjectId {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for ObjectId {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
(self.algo.raw_len(), self.as_bytes()).cmp(&(other.algo.raw_len(), other.as_bytes()))
}
}
fn invalid_len_msg(algo: HashAlgo, got: usize) -> String {
use alloc::format;
format!(
"{} id must be {} bytes, got {}",
algo.name(),
algo.raw_len(),
got
)
}
fn decode_hex_into(hex: &str, out: &mut [u8]) -> Result<(), Error> {
let bytes = hex.as_bytes();
debug_assert_eq!(bytes.len(), out.len() * 2);
for (i, slot) in out.iter_mut().enumerate() {
let hi = hex_val(bytes[2 * i])?;
let lo = hex_val(bytes[2 * i + 1])?;
*slot = (hi << 4) | lo;
}
Ok(())
}
fn hex_val(c: u8) -> Result<u8, Error> {
match c {
b'0'..=b'9' => Ok(c - b'0'),
b'a'..=b'f' => Ok(c - b'a' + 10),
b'A'..=b'F' => Ok(c - b'A' + 10),
_ => Err(Error::InvalidOid(bad_hex_char(c))),
}
}
fn bad_hex_char(c: u8) -> String {
use alloc::format;
format!("non-hex character {:?} in object id", c as char)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sha1_hex_roundtrip() {
let hex = "e69de29bb2d1d6434b8b29ae775ad8c2e48c5391"; let id = ObjectId::from_hex(HashAlgo::Sha1, hex).unwrap();
assert_eq!(id.algo(), HashAlgo::Sha1);
assert_eq!(id.to_hex(), hex);
assert_eq!(id.as_bytes().len(), 20);
assert!(!id.is_zero());
}
#[test]
fn rejects_wrong_length() {
assert!(ObjectId::from_hex(HashAlgo::Sha1, "abcd").is_err());
assert!(ObjectId::from_bytes(HashAlgo::Sha256, &[0u8; 20]).is_err());
}
#[test]
fn rejects_bad_hex() {
let bad = "z69de29bb2d1d6434b8b29ae775ad8c2e48c5391";
assert!(ObjectId::from_hex(HashAlgo::Sha1, bad).is_err());
}
#[test]
fn zero_id() {
let z = ObjectId::zero(HashAlgo::Sha1);
assert!(z.is_zero());
assert_eq!(z.to_hex(), "0".repeat(40));
}
}