use sha2::{Digest, Sha256};
use std::{
fmt,
io::{self, Read},
str::FromStr,
};
pub mod buffer;
mod chunks;
mod copy;
#[cfg(feature = "gzip")]
mod gzip;
mod hashing;
mod matching;
#[cfg(test)]
mod test_support;
#[cfg(test)]
mod tests;
mod writer;
pub use chunks::{ChunkDigestError, chunk_digests};
pub use copy::{CopyError, copy_reader};
#[cfg(feature = "gzip")]
pub use gzip::{GzipError, decode_gzip, encode_gzip, gzip_matches, hash_gzip};
pub use hashing::HashingWriter;
pub use matching::MatchingWriter;
pub use writer::{BoundedWriter, WriterError};
use buffer::reserve_bounded;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct Sha256Digest([u8; 32]);
impl Sha256Digest {
#[must_use]
pub const fn from_bytes(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
#[must_use]
pub const fn hex_bytes(self) -> [u8; 64] {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut encoded = [0; 64];
let mut index = 0;
while index < self.0.len() {
let byte = self.0[index];
encoded[index * 2] = HEX[(byte >> 4) as usize];
encoded[index * 2 + 1] = HEX[(byte & 0x0f) as usize];
index += 1;
}
encoded
}
#[must_use]
pub fn compute(bytes: &[u8]) -> Self {
Self(Sha256::digest(bytes).into())
}
}
impl fmt::Display for Sha256Digest {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(std::str::from_utf8(&self.hex_bytes()).expect("hex encoding is ASCII"))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum DigestParseError {
Length {
actual: usize,
},
Digit {
offset: usize,
},
}
impl fmt::Display for DigestParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Length { actual } => {
write!(f, "SHA-256 requires 64 hex bytes, received {actual}")
}
Self::Digit { offset } => write!(f, "invalid lowercase SHA-256 digit at byte {offset}"),
}
}
}
impl std::error::Error for DigestParseError {}
impl FromStr for Sha256Digest {
type Err = DigestParseError;
fn from_str(text: &str) -> Result<Self, Self::Err> {
if text.len() != 64 {
return Err(DigestParseError::Length { actual: text.len() });
}
let mut bytes = [0; 32];
for (offset, digit) in text.bytes().enumerate() {
let nibble = match digit {
b'0'..=b'9' => digit - b'0',
b'a'..=b'f' => digit - b'a' + 10,
_ => return Err(DigestParseError::Digit { offset }),
};
bytes[offset / 2] |= nibble << if offset % 2 == 0 { 4 } else { 0 };
}
Ok(Self(bytes))
}
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct ArtifactIdentity {
pub bytes: u64,
pub sha256: Sha256Digest,
}
impl ArtifactIdentity {
pub fn verify(self, expected: Sha256Digest) -> Result<Self, ArtifactError> {
if self.sha256 != expected {
return Err(ArtifactError::DigestMismatch {
expected,
actual: self,
});
}
Ok(self)
}
}
#[derive(Debug)]
pub enum ArtifactError {
Io(io::Error),
NotRegularFile,
LimitExceeded {
limit: u64,
},
LengthMismatch {
expected: u64,
actual: u64,
},
DigestMismatch {
expected: Sha256Digest,
actual: ArtifactIdentity,
},
Allocation(std::collections::TryReserveError),
}
impl fmt::Display for ArtifactError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Io(source) => write!(f, "artifact read failed: {source}"),
Self::NotRegularFile => f.write_str("artifact path is not a regular file"),
Self::LimitExceeded { limit } => write!(f, "artifact exceeds {limit} bytes"),
Self::LengthMismatch { expected, actual } => {
write!(f, "artifact has {actual} bytes, expected {expected}")
}
Self::DigestMismatch { expected, actual } => write!(
f,
"artifact SHA-256 is {}, expected {expected}",
actual.sha256
),
Self::Allocation(source) => write!(f, "artifact allocation failed: {source}"),
}
}
}
impl std::error::Error for ArtifactError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(source) => Some(source),
Self::Allocation(source) => Some(source),
_ => None,
}
}
}
impl From<io::Error> for ArtifactError {
fn from(source: io::Error) -> Self {
Self::Io(source)
}
}
impl From<ArtifactError> for io::Error {
fn from(error: ArtifactError) -> Self {
match error {
ArtifactError::Io(source) => source,
other => Self::other(other),
}
}
}
pub fn hash_reader(
mut reader: impl Read,
max_bytes: u64,
) -> Result<ArtifactIdentity, ArtifactError> {
let mut hasher = Sha256::new();
let bytes = visit_reader::<ArtifactError>(&mut reader, max_bytes, |chunk| {
hasher.update(chunk);
Ok(())
})?;
Ok(ArtifactIdentity {
bytes,
sha256: Sha256Digest(hasher.finalize().into()),
})
}
pub fn hash_exact_reader(
reader: impl Read,
expected_bytes: u64,
) -> Result<ArtifactIdentity, ArtifactError> {
let identity = hash_reader(reader, expected_bytes)?;
if identity.bytes != expected_bytes {
return Err(ArtifactError::LengthMismatch {
expected: expected_bytes,
actual: identity.bytes,
});
}
Ok(identity)
}
pub fn read_reader(mut reader: impl Read, max_bytes: usize) -> Result<Vec<u8>, ArtifactError> {
let mut bytes = Vec::new();
visit_reader::<ArtifactError>(&mut reader, max_bytes as u64, |chunk| {
reserve_bounded(&mut bytes, chunk.len(), max_bytes).map_err(ArtifactError::Allocation)?;
bytes.extend_from_slice(chunk);
Ok(())
})?;
Ok(bytes)
}
pub fn read_exact_reader(
reader: impl Read,
expected_bytes: usize,
) -> Result<Vec<u8>, ArtifactError> {
let bytes = read_reader(reader, expected_bytes)?;
if bytes.len() != expected_bytes {
return Err(ArtifactError::LengthMismatch {
expected: expected_bytes as u64,
actual: bytes.len() as u64,
});
}
Ok(bytes)
}
fn visit_reader<E: From<ArtifactError>>(
reader: &mut impl Read,
limit: u64,
mut visit: impl FnMut(&[u8]) -> Result<(), E>,
) -> Result<u64, E> {
let mut bytes = 0_u64;
let mut buffer = [0_u8; 16 * 1024];
loop {
let remaining = usize::try_from(limit - bytes).unwrap_or(usize::MAX);
let allowance = remaining.saturating_add(1).min(buffer.len());
let count = match reader.read(&mut buffer[..allowance]) {
Ok(count) => count,
Err(source) if source.kind() == io::ErrorKind::Interrupted => continue,
Err(source) => return Err(ArtifactError::Io(source).into()),
};
if count > allowance {
return Err(ArtifactError::Io(io::Error::new(
io::ErrorKind::InvalidData,
"reader returned more bytes than its buffer can hold",
))
.into());
}
if count == 0 {
return Ok(bytes);
}
if count as u64 > limit - bytes {
return Err(ArtifactError::LimitExceeded { limit }.into());
}
visit(&buffer[..count])?;
bytes += count as u64;
}
}