use sha1::Sha1;
use sha2::{Digest as _, Sha256};
pub mod registry {
pub const GIT_BLOB_SHA256: u8 = 0x01;
pub const GIT_BLOB_SHA1: u8 = 0x02;
pub const GIT_OBJECT_SHA1: u8 = 0x03;
pub const GIT_OBJECT_SHA256: u8 = 0x04;
pub const RESERVED: u8 = 0x00;
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Algorithm {
GitBlobSha256,
GitBlobSha1,
GitObjectSha1,
GitObjectSha256,
}
impl Algorithm {
pub const fn byte(self) -> u8 {
match self {
Self::GitBlobSha256 => registry::GIT_BLOB_SHA256,
Self::GitBlobSha1 => registry::GIT_BLOB_SHA1,
Self::GitObjectSha1 => registry::GIT_OBJECT_SHA1,
Self::GitObjectSha256 => registry::GIT_OBJECT_SHA256,
}
}
pub const fn try_from_byte(byte: u8) -> Option<Self> {
match byte {
registry::GIT_BLOB_SHA256 => Some(Self::GitBlobSha256),
registry::GIT_BLOB_SHA1 => Some(Self::GitBlobSha1),
registry::GIT_OBJECT_SHA1 => Some(Self::GitObjectSha1),
registry::GIT_OBJECT_SHA256 => Some(Self::GitObjectSha256),
_ => None,
}
}
pub const fn digest_len(self) -> usize {
match self {
Self::GitBlobSha256 | Self::GitObjectSha256 => 32,
Self::GitBlobSha1 | Self::GitObjectSha1 => 20,
}
}
pub fn derive(self, content: &[u8], len: u64) -> Digest {
let bytes = match self {
Self::GitBlobSha256 => blob_digest::<Sha256>(&blob_preamble(len), content),
Self::GitBlobSha1 => blob_digest::<Sha1>(&blob_preamble(len), content),
Self::GitObjectSha1 => blob_digest::<Sha1>(&[], content),
Self::GitObjectSha256 => blob_digest::<Sha256>(&[], content),
};
Digest::from_parts(self, bytes)
}
}
fn blob_preamble(len: u64) -> Vec<u8> {
format!("blob {len}\0").into_bytes()
}
fn blob_digest<H>(preamble: &[u8], content: &[u8]) -> Vec<u8>
where
H: sha2::Digest,
{
let mut hasher = H::new();
hasher.update(preamble);
hasher.update(content);
hasher.finalize().to_vec()
}
pub(crate) struct BlobHasher {
algorithm: Algorithm,
inner: InnerHasher,
}
enum InnerHasher {
Sha256(Sha256),
Sha1(Sha1),
}
impl BlobHasher {
pub(crate) fn new(algorithm: Algorithm, len: u64) -> BlobHasher {
let mut hasher = BlobHasher {
algorithm,
inner: match algorithm {
Algorithm::GitBlobSha256 => InnerHasher::Sha256(Sha256::new()),
Algorithm::GitBlobSha1 => InnerHasher::Sha1(Sha1::new()),
Algorithm::GitObjectSha1 => InnerHasher::Sha1(Sha1::new()),
Algorithm::GitObjectSha256 => InnerHasher::Sha256(Sha256::new()),
},
};
if !matches!(
algorithm,
Algorithm::GitObjectSha1 | Algorithm::GitObjectSha256
) {
hasher.update(&blob_preamble(len));
}
hasher
}
pub(crate) fn update(&mut self, chunk: &[u8]) {
match &mut self.inner {
InnerHasher::Sha256(inner) => inner.update(chunk),
InnerHasher::Sha1(inner) => inner.update(chunk),
}
}
pub(crate) fn finish(self) -> Digest {
let bytes = match self.inner {
InnerHasher::Sha256(inner) => inner.finalize().to_vec(),
InnerHasher::Sha1(inner) => inner.finalize().to_vec(),
};
Digest::from_parts(self.algorithm, bytes)
}
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Digest {
algorithm: Algorithm,
bytes: Vec<u8>,
}
impl Digest {
pub const fn algorithm(&self) -> Algorithm {
self.algorithm
}
pub fn bytes(&self) -> &[u8] {
&self.bytes
}
pub(crate) const fn from_parts(algorithm: Algorithm, bytes: Vec<u8>) -> Digest {
debug_assert!(
bytes.len() == algorithm.digest_len(),
"digest length disagrees with its algorithm"
);
Digest { algorithm, bytes }
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::keys::Key;
use crate::Error;
#[test]
fn derivation_matches_git_computed_vectors() {
assert_eq!(
Algorithm::GitBlobSha256
.derive(b"hello", 5)
.bytes()
.to_vec(),
hex::decode("8aec4e4876f854f688d0ebfc8f37598f38e5fd6903cccc850ca36591175aeb60")
.expect("sha256 'hello' vector decodes"),
"sha256 'hello'",
);
assert_eq!(
Algorithm::GitBlobSha256.derive(b"", 0).bytes().to_vec(),
hex::decode("473a0f4c3be8a93681a267e3b1e9a7dcda1185436fe141f7749120a303721813")
.expect("sha256 empty vector decodes"),
"sha256 empty",
);
assert_eq!(
Algorithm::GitBlobSha1.derive(b"hello", 5).bytes().to_vec(),
hex::decode("b6fc4c620b67d95f953a5c1c1230aaab5db5a1b0").unwrap(),
"sha1 'hello'",
);
assert_eq!(
Algorithm::GitBlobSha1.derive(b"", 0).bytes().to_vec(),
hex::decode("e69de29bb2d1d6434b8b29ae775ad8c2e48c5391").unwrap(),
"sha1 empty",
);
}
#[test]
fn git_object_derivation_matches_git_oids() {
let empty_tree = b"tree 0\0";
assert_eq!(
Algorithm::GitObjectSha1
.derive(empty_tree, empty_tree.len() as u64)
.bytes()
.to_vec(),
hex::decode("4b825dc642cb6eb9a060e54bf8d69288fbee4904").unwrap(),
"sha1 empty tree via the git-object derivation",
);
assert_eq!(
Algorithm::GitObjectSha256
.derive(empty_tree, empty_tree.len() as u64)
.bytes()
.to_vec(),
hex::decode("6ef19b41225c5369f1c104d45d8d85efa9b057b53b14b4b9b939dd74decc5321")
.expect("sha256 empty tree vector decodes"),
"sha256 empty tree via the git-object derivation",
);
assert_eq!(
Algorithm::GitObjectSha1
.derive(b"blob 5\0hello", 13)
.bytes()
.to_vec(),
Algorithm::GitBlobSha1.derive(b"hello", 5).bytes().to_vec(),
"blob loose form through the git-object derivation == the blob oid",
);
}
#[test]
fn git_object_derive_len_parameter_is_inert() {
for content in [
&b"tree 0\0"[..],
b"blob 5\0hello",
&(0..1000u32).map(|i| (i % 251) as u8).collect::<Vec<u8>>()[..],
] {
for algorithm in [Algorithm::GitObjectSha1, Algorithm::GitObjectSha256] {
let baseline = algorithm.derive(content, content.len() as u64);
for len in [0u64, 1, content.len() as u64 - 1, u64::MAX] {
assert_eq!(
algorithm.derive(content, len),
baseline,
"{algorithm:?}: derive over len {len} equals the plain hash"
);
}
}
}
}
#[test]
fn git_object_registry_bytes_and_order() {
assert_eq!(Algorithm::GitObjectSha1.byte(), registry::GIT_OBJECT_SHA1);
assert_eq!(
Algorithm::try_from_byte(registry::GIT_OBJECT_SHA256),
Some(Algorithm::GitObjectSha256)
);
assert!(
Algorithm::GitBlobSha1 < Algorithm::GitObjectSha1
&& Algorithm::GitObjectSha1 < Algorithm::GitObjectSha256,
"ordering follows the registry bytes (0x02 < 0x03 < 0x04)"
);
assert_eq!(Algorithm::GitObjectSha1.digest_len(), 20);
assert_eq!(Algorithm::GitObjectSha256.digest_len(), 32);
}
#[test]
fn digest_lengths_pinned() {
assert_eq!(Algorithm::GitBlobSha256.derive(b"x", 1).bytes().len(), 32);
assert_eq!(Algorithm::GitBlobSha1.derive(b"x", 1).bytes().len(), 20);
assert_eq!(Algorithm::GitObjectSha1.derive(b"x", 1).bytes().len(), 20);
assert_eq!(Algorithm::GitObjectSha256.derive(b"x", 1).bytes().len(), 32);
}
#[test]
fn blob_oid_keys_decode_through_the_physical_encoding() {
for algorithm in [Algorithm::GitBlobSha256, Algorithm::GitBlobSha1] {
let derived = algorithm.derive(b"hello", 5);
let oid_bytes = derived.bytes().to_vec();
let tagged = {
let mut tagged = vec![algorithm.byte()];
tagged.extend_from_slice(&oid_bytes);
tagged
};
let key = Key::from_bytes(&tagged)
.expect("the tagged oid key decodes through the physical encoding");
assert_eq!(key.algorithm(), algorithm);
assert_eq!(key.digest().bytes(), derived.bytes());
assert_eq!(
Key::from_bytes(&key.as_bytes()),
Ok(key),
"round-trips through the physical encoding"
);
let truncated = tagged[..tagged.len() - 1].to_vec();
assert_eq!(Key::from_bytes(&truncated), Err(Error::KeyInvalid));
let wrong_tag = {
let other = if algorithm == Algorithm::GitBlobSha256 {
Algorithm::GitBlobSha1
} else {
Algorithm::GitBlobSha256
};
let mut wrong = vec![other.byte()];
wrong.extend_from_slice(&oid_bytes);
wrong
};
assert_eq!(Key::from_bytes(&wrong_tag), Err(Error::KeyInvalid));
}
}
#[test]
fn streaming_hasher_matches_known_length_derivation() {
for algorithm in [
Algorithm::GitBlobSha256,
Algorithm::GitBlobSha1,
Algorithm::GitObjectSha1,
Algorithm::GitObjectSha256,
] {
let bodies: [&[u8]; 5] = [b"", b"hello", b"tree 0\0", &[0u8; 100_003], &[7u8; 9]];
for body in bodies {
let expected = algorithm.derive(body, body.len() as u64);
let mut hasher = BlobHasher::new(algorithm, body.len() as u64);
hasher.update(body);
assert_eq!(hasher.finish(), expected, "whole-body, {algorithm:?}");
let mut hasher = BlobHasher::new(algorithm, body.len() as u64);
for byte in body {
hasher.update(std::slice::from_ref(byte));
}
assert_eq!(hasher.finish(), expected, "byte-at-a-time, {algorithm:?}");
let mut hasher = BlobHasher::new(algorithm, body.len() as u64);
for chunk in body.chunks(7) {
hasher.update(chunk);
}
assert_eq!(hasher.finish(), expected, "chunks of 7, {algorithm:?}");
}
let expected = algorithm.derive(b"hello", 5).bytes().to_vec();
let mut hasher = BlobHasher::new(algorithm, 5);
hasher.update(b"hel");
hasher.update(b"lo");
assert_eq!(hasher.finish().bytes().to_vec(), expected);
let hasher = BlobHasher::new(algorithm, 0);
assert_eq!(
hasher.finish().bytes().to_vec(),
algorithm.derive(b"", 0).bytes().to_vec(),
"empty content streams with no update"
);
}
}
#[test]
fn algorithm_order_matches_registry_bytes() {
assert!(Algorithm::GitBlobSha256.byte() < Algorithm::GitBlobSha1.byte());
assert!(Algorithm::GitBlobSha256 < Algorithm::GitBlobSha1);
assert!(Algorithm::GitBlobSha1 < Algorithm::GitObjectSha1);
assert!(Algorithm::GitObjectSha1 < Algorithm::GitObjectSha256);
assert!(Algorithm::GitObjectSha256.byte() < 0xFF);
}
}