use std::cmp::Ordering;
use std::hash::{Hash, Hasher};
use std::ops::Deref;
use crate::hashing::{Algorithm, Digest};
use crate::Error;
const KEY_BYTES_CAP: usize = 1 + 32;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Key(Digest);
impl Key {
pub fn derive(content: &[u8], len: u64) -> Key {
Digest::derive(content, len).into()
}
pub const fn digest(&self) -> &Digest {
&self.0
}
pub const fn algorithm(&self) -> Algorithm {
self.0.algorithm()
}
pub fn as_bytes(&self) -> Vec<u8> {
self.key_bytes().as_slice().to_vec()
}
pub fn key_bytes(&self) -> KeyBytes {
let digest = self.0.bytes();
debug_assert!(
digest.len() < KEY_BYTES_CAP,
"digest length exceeds the fixed key-bytes capacity"
);
let mut bytes = [0u8; KEY_BYTES_CAP];
let len = 1 + digest.len();
bytes[0] = self.0.algorithm().byte();
bytes[1..len].copy_from_slice(digest);
KeyBytes {
bytes,
len: len as u8,
}
}
pub fn from_bytes(bytes: &[u8]) -> Result<Key, Error> {
let (&tag, digest_bytes) = bytes.split_first().ok_or(Error::KeyInvalid)?;
let algorithm = Algorithm::try_from_byte(tag).ok_or(Error::KeyInvalid)?;
if digest_bytes.len() != algorithm.digest_len() {
return Err(Error::KeyInvalid);
}
Ok(Key(Digest::from_parts(algorithm, digest_bytes.to_vec())))
}
}
#[derive(Clone, Copy, Debug)]
pub struct KeyBytes {
bytes: [u8; KEY_BYTES_CAP],
len: u8,
}
impl KeyBytes {
pub fn as_slice(&self) -> &[u8] {
&self.bytes[..self.len as usize]
}
}
impl Deref for KeyBytes {
type Target = [u8];
fn deref(&self) -> &[u8] {
self.as_slice()
}
}
impl PartialEq for KeyBytes {
fn eq(&self, other: &Self) -> bool {
self.as_slice() == other.as_slice()
}
}
impl Eq for KeyBytes {}
impl Hash for KeyBytes {
fn hash<H: Hasher>(&self, state: &mut H) {
self.as_slice().hash(state)
}
}
impl PartialOrd for KeyBytes {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl Ord for KeyBytes {
fn cmp(&self, other: &Self) -> Ordering {
self.as_slice().cmp(other.as_slice())
}
}
impl From<Digest> for Key {
fn from(digest: Digest) -> Key {
Key(digest)
}
}
impl From<Key> for Digest {
fn from(key: Key) -> Digest {
key.0
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hashing::Algorithm as A;
use std::collections::BTreeSet;
#[test]
fn encoding_is_byte_exact() {
let content = b"hello";
let sha256 = Digest::derive(content, content.len() as u64);
let sha1 = A::GitBlobSha1.derive(content, content.len() as u64);
let k256: Key = sha256.clone().into();
let k1: Key = sha1.clone().into();
let b256 = k256.as_bytes();
assert_eq!(b256.len(), 33);
assert_eq!(b256[0], 0x01);
assert_eq!(&b256[1..], sha256.bytes());
let b1 = k1.as_bytes();
assert_eq!(b1.len(), 21);
assert_eq!(b1[0], 0x02);
assert_eq!(&b1[1..], sha1.bytes());
}
#[test]
fn as_bytes_from_bytes_round_trip() {
let content = b"round trip";
for algorithm in [
Algorithm::GitBlobSha256,
Algorithm::GitBlobSha1,
Algorithm::GitObjectSha1,
Algorithm::GitObjectSha256,
] {
let key: Key = algorithm.derive(content, content.len() as u64).into();
assert_eq!(Key::from_bytes(&key.as_bytes()), Ok(key));
}
}
#[test]
fn key_bytes_borrowed_overload_round_trips() {
let content = b"borrowed overload";
for algorithm in [
Algorithm::GitBlobSha256,
Algorithm::GitBlobSha1,
Algorithm::GitObjectSha1,
Algorithm::GitObjectSha256,
] {
let key: Key = algorithm.derive(content, content.len() as u64).into();
let borrowed = key.key_bytes();
assert_eq!(borrowed.as_slice(), key.as_bytes(), "byte-identical");
assert_eq!(borrowed.len(), key.as_bytes().len(), "written prefix");
assert_eq!(Key::from_bytes(&borrowed), Ok(key), "deref round-trip");
}
}
#[test]
fn key_bytes_ordering_matches_the_typed_order() {
let contents: &[&[u8]] = &[b"a", b"bb", b"ccc", b"dddd"];
let keys: Vec<Key> = contents
.iter()
.flat_map(|c| {
[
Key::from(Digest::derive(c, c.len() as u64)),
Key::from(Algorithm::GitBlobSha1.derive(c, c.len() as u64)),
Key::from(Algorithm::GitObjectSha1.derive(c, c.len() as u64)),
Key::from(Algorithm::GitObjectSha256.derive(c, c.len() as u64)),
]
})
.collect();
let typed_sorted: Vec<Key> = {
let mut sorted = keys.clone();
sorted.sort();
sorted
};
let borrowed_sorted: Vec<KeyBytes> = {
let mut borrowed: Vec<KeyBytes> = keys.iter().map(|k| k.key_bytes()).collect();
borrowed.sort();
borrowed
};
let expected: Vec<KeyBytes> = typed_sorted.iter().map(|k| k.key_bytes()).collect();
assert_eq!(borrowed_sorted, expected, "borrowed order == typed order");
}
#[test]
fn malformed_keys_are_rejected() {
let key: Key = Digest::derive(b"rejects", 7).into();
assert_eq!(Key::from_bytes(&[]), Err(Error::KeyInvalid), "empty");
assert_eq!(
Key::from_bytes(&key.as_bytes()[..key.as_bytes().len() - 1]),
Err(Error::KeyInvalid),
"truncated"
);
assert_eq!(
Key::from_bytes(&key.as_bytes()[..32]),
Err(Error::KeyInvalid),
"digest truncated"
);
assert_eq!(
Key::from_bytes(&[&key.as_bytes()[..], &[0u8][..]].concat()),
Err(Error::KeyInvalid),
"over-long"
);
assert_eq!(Key::from_bytes(&[0x00]), Err(Error::KeyInvalid), "reserved");
assert_eq!(
Key::from_bytes(&[0x00; 33]),
Err(Error::KeyInvalid),
"reserved full-width"
);
assert_eq!(
Key::from_bytes(&[0x05; 33]),
Err(Error::KeyInvalid),
"unknown (0x03/0x04 are the ADR-018 git-object family)"
);
assert_eq!(
Key::from_bytes(&[0xFF; 33]),
Err(Error::KeyInvalid),
"unknown top"
);
assert_eq!(Key::from_bytes(&[0xFF; 21]), Err(Error::KeyInvalid));
assert_eq!(
Key::from_bytes(&[0x03]),
Err(Error::KeyInvalid),
"git-object tag alone is truncated"
);
assert_eq!(
Key::from_bytes(&[0x03; 33]),
Err(Error::KeyInvalid),
"git-object sha-1 tag with sha-256-width digest"
);
assert_eq!(
Key::from_bytes(&[0x04; 21]),
Err(Error::KeyInvalid),
"git-object sha-256 tag with sha-1-width digest"
);
}
#[test]
fn git_object_keys_round_trip_in_total_order() {
let sha1_key: Key = Algorithm::GitObjectSha1.derive(b"tree 0\0", 7).into();
let sha256_key: Key = Algorithm::GitObjectSha256.derive(b"tree 0\0", 7).into();
let b1 = sha1_key.as_bytes();
assert_eq!(b1.len(), 21);
assert_eq!(b1[0], 0x03);
assert_eq!(Key::from_bytes(&b1), Ok(sha1_key.clone()));
let b256 = sha256_key.as_bytes();
assert_eq!(b256.len(), 33);
assert_eq!(b256[0], 0x04);
assert_eq!(Key::from_bytes(&b256), Ok(sha256_key.clone()));
assert!(
sha1_key < sha256_key,
"the extended family keeps the total order"
);
}
#[test]
fn mixed_algorithms_sort_in_total_order() {
let contents: &[&[u8]] = &[b"a", b"bb", b"ccc", b"dddd"];
let mut keys: Vec<Key> = contents
.iter()
.flat_map(|c| {
[
Key::from(Digest::derive(c, c.len() as u64)),
Key::from(Algorithm::GitBlobSha1.derive(c, c.len() as u64)),
Key::from(Algorithm::GitObjectSha1.derive(c, c.len() as u64)),
Key::from(Algorithm::GitObjectSha256.derive(c, c.len() as u64)),
]
})
.collect();
keys.push(Key::from(Algorithm::GitBlobSha1.derive(b"b", 1)));
keys.sort();
let encoded: Vec<Vec<u8>> = keys.iter().map(|k| k.as_bytes()).collect();
let mut sorted_encoded = encoded.clone();
sorted_encoded.sort();
assert_eq!(encoded, sorted_encoded, "derived order == byte order");
let mut set = BTreeSet::new();
for key in &keys {
set.insert(key.as_bytes());
}
assert_eq!(set.len(), keys.len(), "all keys addressable, none collide");
}
#[test]
fn both_algorithms_coexist_in_one_collection() {
let content = b"coexist";
let mut set = BTreeSet::new();
set.insert(Key::from(Digest::derive(content, content.len() as u64)));
set.insert(Key::from(
Algorithm::GitBlobSha1.derive(content, content.len() as u64),
));
assert_eq!(set.len(), 2);
}
}