use crate::Error;
use crate::HashAlgorithm;
use crate::HashRef;
use crate::ObjectType;
use crate::Result;
use crate::NUM_HASH_BYTES;
use core::fmt;
use core::fmt::Display;
use core::fmt::Formatter;
use sha2::digest::DynDigest;
use std::hash::Hash;
use std::io::BufReader;
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use std::ops::Not as _;
use std::str::FromStr;
use url::Url;
#[repr(C)]
#[derive(Clone, Copy, PartialOrd, Eq, Ord, Debug, Hash, PartialEq)]
pub struct GitOid {
hash_algorithm: HashAlgorithm,
object_type: ObjectType,
len: usize,
value: [u8; NUM_HASH_BYTES],
}
impl Display for GitOid {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.hash_algorithm, self.hash())
}
}
impl GitOid {
pub fn new_invalid() -> Self {
GitOid {
hash_algorithm: HashAlgorithm::Sha1,
object_type: ObjectType::Blob,
len: 0,
value: [0u8; NUM_HASH_BYTES],
}
}
pub fn new_from_bytes(
hash_algorithm: HashAlgorithm,
object_type: ObjectType,
content: &[u8],
) -> Self {
let digester = hash_algorithm.create_digester();
let reader = BufReader::new(content);
let expected_length = content.len();
let (len, value) =
bytes_from_buffer(digester, reader, expected_length, object_type).unwrap();
GitOid {
hash_algorithm,
object_type,
value,
len,
}
}
pub fn new_from_str(hash_algorithm: HashAlgorithm, object_type: ObjectType, s: &str) -> Self {
let content = s.as_bytes();
GitOid::new_from_bytes(hash_algorithm, object_type, content)
}
pub fn new_from_reader<R>(
hash_algorithm: HashAlgorithm,
object_type: ObjectType,
mut reader: BufReader<R>,
) -> Result<Self>
where
R: Read + Seek,
{
let digester = hash_algorithm.create_digester();
let expected_length = stream_len(&mut reader)? as usize;
let (len, value) = bytes_from_buffer(digester, reader, expected_length, object_type)?;
Ok(GitOid {
hash_algorithm,
object_type,
len,
value,
})
}
pub fn new_from_url(url: Url) -> Result<GitOid> {
url.try_into()
}
pub fn url(&self) -> Url {
let s = format!(
"gitoid:{}:{}:{}",
self.object_type,
self.hash_algorithm,
self.hash()
);
Url::parse(&s).unwrap()
}
pub fn hash(&self) -> HashRef<'_> {
HashRef::new(&self.value[0..self.len])
}
pub fn hash_algorithm(&self) -> HashAlgorithm {
self.hash_algorithm
}
pub fn object_type(&self) -> ObjectType {
self.object_type
}
pub fn hash_len(&self) -> usize {
self.len
}
}
impl TryFrom<Url> for GitOid {
type Error = Error;
fn try_from(url: Url) -> Result<GitOid> {
use Error::*;
if url.scheme() != "gitoid" {
return Err(InvalidScheme(url));
}
let mut segments = url.path().split(':');
let object_type = {
let part = segments
.next()
.and_then(|p| p.is_empty().not().then_some(p))
.ok_or_else(|| MissingObjectType(url.clone()))?;
ObjectType::from_str(part)?
};
let hash_algorithm = {
let part = segments
.next()
.and_then(|p| p.is_empty().not().then_some(p))
.ok_or_else(|| MissingHashAlgorithm(url.clone()))?;
HashAlgorithm::from_str(part)?
};
let hex_str = segments
.next()
.and_then(|p| p.is_empty().not().then_some(p))
.ok_or_else(|| MissingHash(url.clone()))?;
let mut value = [0u8; 32];
hex::decode_to_slice(hex_str, &mut value)?;
Ok(GitOid {
hash_algorithm,
object_type,
len: value.len(),
value,
})
}
}
fn bytes_from_buffer<R>(
mut digester: Box<dyn DynDigest>,
mut reader: BufReader<R>,
expected_length: usize,
object_type: ObjectType,
) -> Result<(usize, [u8; NUM_HASH_BYTES])>
where
BufReader<R>: Read,
{
let prefix = format!("{} {}\0", object_type, expected_length);
let mut buf = [0; 4096]; let mut amount_read: usize = 0;
digester.update(prefix.as_bytes());
loop {
match reader.read(&mut buf)? {
0 => break,
size => {
digester.update(&buf[..size]);
amount_read += size;
}
}
}
if amount_read != expected_length {
return Err(Error::BadLength {
expected: expected_length,
actual: amount_read,
});
}
let hash = digester.finalize();
let mut ret = [0u8; NUM_HASH_BYTES];
let len = std::cmp::min(NUM_HASH_BYTES, hash.len());
ret[..len].copy_from_slice(&hash);
Ok((len, ret))
}
fn stream_len<R>(mut stream: R) -> Result<u64>
where
R: Seek,
{
let old_pos = stream.stream_position()?;
let len = stream.seek(SeekFrom::End(0))?;
if old_pos != len {
stream.seek(SeekFrom::Start(old_pos))?;
}
Ok(len)
}