use crate::Error;
use crate::HashAlgorithm;
use crate::ObjectType;
use crate::Result;
use core::fmt::Formatter;
use core::hash::Hash;
use core::marker::PhantomData;
use core::ops::Not as _;
use digest::OutputSizeUser;
use generic_array::sequence::GenericSequence;
use generic_array::ArrayLength;
use generic_array::GenericArray;
use std::cmp::Ordering;
use std::fmt::Debug;
use std::fmt::Display;
use std::fmt::Result as FmtResult;
use std::io::Read;
use std::io::Seek;
use std::io::SeekFrom;
use std::str::Split;
use std::{hash::Hasher, io::BufReader};
use url::Url;
#[repr(C)]
pub struct GitOid<H, O>
where
H: HashAlgorithm,
O: ObjectType,
<H as OutputSizeUser>::OutputSize: ArrayLength<u8>,
GenericArray<u8, H::OutputSize>: Copy,
{
#[doc(hidden)]
_phantom: PhantomData<O>,
#[doc(hidden)]
value: GenericArray<u8, H::OutputSize>,
}
impl<H, O> GitOid<H, O>
where
H: HashAlgorithm,
O: ObjectType,
<H as OutputSizeUser>::OutputSize: ArrayLength<u8>,
GenericArray<u8, H::OutputSize>: Copy,
{
fn new_from_hash(value: GenericArray<u8, H::OutputSize>) -> GitOid<H, O> {
GitOid {
_phantom: PhantomData,
value,
}
}
pub fn new_from_bytes<B: AsRef<[u8]>>(content: B) -> GitOid<H, O> {
fn inner<H, O>(content: &[u8]) -> GitOid<H, O>
where
H: HashAlgorithm,
O: ObjectType,
<H as OutputSizeUser>::OutputSize: ArrayLength<u8>,
GenericArray<u8, H::OutputSize>: Copy,
{
let digester = H::new();
let reader = BufReader::new(content);
let expected_length = content.len();
gitoid_from_buffer(digester, reader, expected_length).unwrap()
}
inner(content.as_ref())
}
pub fn new_from_str<S: AsRef<str>>(s: S) -> GitOid<H, O> {
fn inner<H, O>(s: &str) -> GitOid<H, O>
where
H: HashAlgorithm,
O: ObjectType,
<H as OutputSizeUser>::OutputSize: ArrayLength<u8>,
GenericArray<u8, H::OutputSize>: Copy,
{
GitOid::new_from_bytes(s.as_bytes())
}
inner(s.as_ref())
}
pub fn new_from_reader<R>(mut reader: R) -> Result<GitOid<H, O>>
where
R: Read + Seek,
{
let digester = H::new();
let expected_length = stream_len(&mut reader)? as usize;
gitoid_from_buffer(digester, reader, expected_length)
}
pub fn new_from_url(url: Url) -> Result<GitOid<H, O>> {
url.try_into()
}
pub fn url(&self) -> Url {
let s = format!("gitoid:{}:{}:{}", O::NAME, H::NAME, self.as_hex());
Url::parse(&s).unwrap()
}
pub fn as_bytes(&self) -> &[u8] {
&self.value[..]
}
pub fn as_hex(&self) -> String {
hex::encode(self.as_bytes())
}
pub const fn hash_algorithm(&self) -> &'static str {
H::NAME
}
pub const fn object_type(&self) -> &'static str {
O::NAME
}
pub fn hash_len(&self) -> usize {
<H as OutputSizeUser>::output_size()
}
}
impl<H, O> Clone for GitOid<H, O>
where
H: HashAlgorithm,
O: ObjectType,
<H as OutputSizeUser>::OutputSize: ArrayLength<u8>,
GenericArray<u8, H::OutputSize>: Copy,
{
fn clone(&self) -> Self {
*self
}
}
impl<H, O> Copy for GitOid<H, O>
where
H: HashAlgorithm,
O: ObjectType,
<H as OutputSizeUser>::OutputSize: ArrayLength<u8>,
GenericArray<u8, H::OutputSize>: Copy,
{
}
impl<H, O> PartialEq<GitOid<H, O>> for GitOid<H, O>
where
H: HashAlgorithm,
O: ObjectType,
<H as OutputSizeUser>::OutputSize: ArrayLength<u8>,
GenericArray<u8, H::OutputSize>: Copy,
{
fn eq(&self, other: &GitOid<H, O>) -> bool {
self.value == other.value
}
}
impl<H, O> Eq for GitOid<H, O>
where
H: HashAlgorithm,
O: ObjectType,
<H as OutputSizeUser>::OutputSize: ArrayLength<u8>,
GenericArray<u8, H::OutputSize>: Copy,
{
}
impl<H, O> PartialOrd<GitOid<H, O>> for GitOid<H, O>
where
H: HashAlgorithm,
O: ObjectType,
<H as OutputSizeUser>::OutputSize: ArrayLength<u8>,
GenericArray<u8, H::OutputSize>: Copy,
{
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<H, O> Ord for GitOid<H, O>
where
H: HashAlgorithm,
O: ObjectType,
<H as OutputSizeUser>::OutputSize: ArrayLength<u8>,
GenericArray<u8, H::OutputSize>: Copy,
{
fn cmp(&self, other: &Self) -> Ordering {
self.value.cmp(&other.value)
}
}
impl<H, O> Hash for GitOid<H, O>
where
H: HashAlgorithm,
O: ObjectType,
<H as OutputSizeUser>::OutputSize: ArrayLength<u8>,
GenericArray<u8, H::OutputSize>: Copy,
{
fn hash<H2>(&self, state: &mut H2)
where
H2: Hasher,
{
self.value.hash(state);
}
}
impl<H, O> Debug for GitOid<H, O>
where
H: HashAlgorithm,
O: ObjectType,
<H as OutputSizeUser>::OutputSize: ArrayLength<u8>,
GenericArray<u8, H::OutputSize>: Copy,
{
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
f.debug_struct("GitOid")
.field("value", &self.value)
.finish()
}
}
impl<H, O> Display for GitOid<H, O>
where
H: HashAlgorithm,
O: ObjectType,
<H as OutputSizeUser>::OutputSize: ArrayLength<u8>,
GenericArray<u8, H::OutputSize>: Copy,
{
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
write!(f, "{}:{}", H::NAME, self.as_hex())
}
}
struct GitOidUrlParser<'u, H, O>
where
H: HashAlgorithm,
O: ObjectType,
<H as OutputSizeUser>::OutputSize: ArrayLength<u8>,
GenericArray<u8, H::OutputSize>: Copy,
{
url: &'u Url,
segments: Split<'u, char>,
#[doc(hidden)]
_hash_algorithm: PhantomData<H>,
#[doc(hidden)]
_object_type: PhantomData<O>,
}
fn some_if_not_empty(s: &str) -> Option<&str> {
s.is_empty().not().then_some(s)
}
impl<'u, H, O> GitOidUrlParser<'u, H, O>
where
H: HashAlgorithm,
O: ObjectType,
<H as OutputSizeUser>::OutputSize: ArrayLength<u8>,
GenericArray<u8, H::OutputSize>: Copy,
{
fn new(url: &'u Url) -> GitOidUrlParser<'u, H, O> {
GitOidUrlParser {
url,
segments: url.path().split(':'),
_hash_algorithm: PhantomData,
_object_type: PhantomData,
}
}
fn parse(&mut self) -> Result<GitOid<H, O>> {
self.validate_url_scheme()
.and_then(|_| self.validate_object_type())
.and_then(|_| self.validate_hash_algorithm())
.and_then(|_| self.parse_hash())
.map(GitOid::new_from_hash)
}
fn validate_url_scheme(&self) -> Result<()> {
if self.url.scheme() != "gitoid" {
return Err(Error::InvalidScheme(self.url.clone()));
}
Ok(())
}
fn validate_object_type(&mut self) -> Result<()> {
let object_type = self
.segments
.next()
.and_then(some_if_not_empty)
.ok_or_else(|| Error::MissingObjectType(self.url.clone()))?;
if object_type != O::NAME {
return Err(Error::MismatchedObjectType {
expected: O::NAME.to_string(),
observed: object_type.to_string(),
});
}
Ok(())
}
fn validate_hash_algorithm(&mut self) -> Result<()> {
let hash_algorithm = self
.segments
.next()
.and_then(some_if_not_empty)
.ok_or_else(|| Error::MissingHashAlgorithm(self.url.clone()))?;
if hash_algorithm != H::NAME {
return Err(Error::MismatchedHashAlgorithm {
expected: H::NAME.to_string(),
observed: hash_algorithm.to_string(),
});
}
Ok(())
}
fn parse_hash(&mut self) -> Result<GenericArray<u8, H::OutputSize>> {
let hex_str = self
.segments
.next()
.and_then(some_if_not_empty)
.ok_or_else(|| Error::MissingHash(self.url.clone()))?;
let mut value = GenericArray::generate(|_| 0);
hex::decode_to_slice(hex_str, &mut value)?;
let expected_size = <H as OutputSizeUser>::output_size();
if value.len() != expected_size {
return Err(Error::UnexpectedHashLength {
expected: expected_size,
observed: value.len(),
});
}
Ok(value)
}
}
impl<H, O> TryFrom<Url> for GitOid<H, O>
where
H: HashAlgorithm,
O: ObjectType,
<H as OutputSizeUser>::OutputSize: ArrayLength<u8>,
GenericArray<u8, H::OutputSize>: Copy,
{
type Error = Error;
fn try_from(url: Url) -> Result<GitOid<H, O>> {
GitOidUrlParser::new(&url).parse()
}
}
fn gitoid_from_buffer<H, O, R>(
mut digester: H,
mut reader: R,
expected_length: usize,
) -> Result<GitOid<H, O>>
where
H: HashAlgorithm,
O: ObjectType,
<H as OutputSizeUser>::OutputSize: ArrayLength<u8>,
GenericArray<u8, H::OutputSize>: Copy,
R: Read,
{
let prefix = format!("{} {}\0", O::NAME, 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 expected_size = <H as OutputSizeUser>::output_size();
if hash.len() != expected_size {
return Err(Error::UnexpectedHashLength {
expected: expected_size,
observed: hash.len(),
});
}
Ok(GitOid::new_from_hash(hash))
}
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)
}