use std::fmt;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Oid([u8; 32]);
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum OidError {
#[error("an lfs oid is 64 hex characters, got {0}")]
Length(usize),
#[error("an lfs oid is lowercase hex; byte {at} is {byte:?}")]
NotHex {
at: usize,
byte: char,
},
}
impl Oid {
pub fn parse(text: &str) -> Result<Oid, OidError> {
let bytes = text.as_bytes();
if bytes.len() != 64 {
return Err(OidError::Length(text.chars().count()));
}
let mut out = [0u8; 32];
for (i, pair) in bytes.chunks_exact(2).enumerate() {
let hi = nibble(pair[0]).ok_or(OidError::NotHex {
at: i * 2,
byte: pair[0] as char,
})?;
let lo = nibble(pair[1]).ok_or(OidError::NotHex {
at: i * 2 + 1,
byte: pair[1] as char,
})?;
out[i] = (hi << 4) | lo;
}
Ok(Oid(out))
}
pub fn of(bytes: &[u8]) -> Oid {
let mut hasher = Sha256Stream::new();
hasher.update(bytes);
hasher.finish()
}
pub fn from_digest(digest: [u8; 32]) -> Oid {
Oid(digest)
}
pub fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
pub fn to_hex(self) -> String {
let mut out = String::with_capacity(64);
for byte in self.0 {
out.push(HEX[(byte >> 4) as usize] as char);
out.push(HEX[(byte & 0x0f) as usize] as char);
}
out
}
#[cfg(feature = "fs")]
pub(crate) fn hex_prefix(self, n: usize) -> String {
self.to_hex().chars().take(n).collect()
}
}
const HEX: &[u8; 16] = b"0123456789abcdef";
fn nibble(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
_ => None,
}
}
impl fmt::Display for Oid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.to_hex())
}
}
impl fmt::Debug for Oid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Oid({})", self.to_hex())
}
}
pub(crate) struct Sha256Stream(Box<gix_hash::Hasher>);
impl Sha256Stream {
pub(crate) fn new() -> Self {
Sha256Stream(Box::new(gix_hash::hasher(gix_hash::Kind::Sha256)))
}
pub(crate) fn update(&mut self, bytes: &[u8]) {
self.0.update(bytes);
}
pub(crate) fn finish(self) -> Oid {
let id = (*self.0)
.try_finalize()
.expect("sha256 finalisation has no failure mode");
match id {
gix_hash::ObjectId::Sha256(bytes) => Oid(bytes),
other => unreachable!("asked for sha256, got {:?}", other.kind()),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const REAL: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
#[test]
fn a_real_oid_round_trips() {
assert_eq!(Oid::parse(REAL).unwrap().to_hex(), REAL);
}
#[test]
fn the_hash_of_the_empty_input_is_the_known_sha256_of_nothing() {
let empty = Sha256Stream::new().finish();
assert_eq!(empty.to_hex(), REAL);
let mut one = Sha256Stream::new();
one.update(b"abc");
assert_eq!(
one.finish().to_hex(),
"ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad"
);
}
#[test]
fn everything_that_is_not_an_oid_is_refused() {
for bad in [
"",
"../../../etc/passwd",
"..",
"/",
&REAL[..63],
&format!("{REAL}0"),
&REAL.to_uppercase(),
&format!("{}/{}", &REAL[..32], &REAL[33..]),
&REAL.replace('e', "g"),
] {
assert!(
Oid::parse(bad).is_err(),
"{bad:?} must not parse as an oid; it becomes a path component"
);
}
assert!(Oid::parse(REAL).is_ok());
}
}