use std::io::Cursor;
use std::io::Read as IoRead;
use pith_digest::Read as Source;
use pith_digest::{Algorithm, Digest, Error, Format, hamming};
struct FromIo<R: IoRead>(R);
impl<R: IoRead> Source for FromIo<R> {
fn read_some(&mut self, buf: &mut [u8]) -> usize {
self.0.read(buf).unwrap_or(0)
}
}
struct Dribble<'a>(&'a [u8]);
impl Source for Dribble<'_> {
fn read_some(&mut self, buf: &mut [u8]) -> usize {
let n = self.0.len().min(buf.len()).min(1);
buf[..n].copy_from_slice(&self.0[..n]);
self.0 = &self.0[n..];
n
}
}
#[test]
fn hamming_identical_digests_are_zero_apart() {
let a = Digest::<4>::from_bytes([0xa5, 0x5a, 0xff, 0x00]);
let b = a;
assert_eq!(hamming(&a, &b), 0);
}
#[test]
fn hamming_all_bits_differing_is_eight_per_byte() {
let a = Digest::<4>::from_bytes([0x00; 4]);
let b = Digest::<4>::from_bytes([0xff; 4]);
assert_eq!(hamming(&a, &b), 32); }
#[test]
fn hamming_asymmetric_hand_counted_case() {
let a = Digest::<4>::from_bytes([0x0f, 0x00, 0xff, 0x80]);
let b = Digest::<4>::from_bytes([0x00, 0x00, 0x00, 0x00]);
assert_eq!(hamming(&a, &b), 13);
assert_eq!(hamming(&b, &a), 13);
}
#[test]
fn hamming_spans_u64_chunks_and_tails() {
assert_eq!(
hamming(
&Digest::<8>::from_bytes([0xff; 8]),
&Digest::<8>::from_bytes([0x00; 8])
),
64
);
assert_eq!(
hamming(
&Digest::from_bytes([0xff; 9]),
&Digest::from_bytes([0x00; 9])
),
72
);
let a = [
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xab, 0x00, 0x00, 0xcd, ];
assert_eq!(
hamming(&Digest::from_bytes(a), &Digest::from_bytes([0x00; 12])),
74
);
assert_eq!(
hamming(
&Digest::from_bytes([0x0f, 0x80, 0x03, 0x00, 0x00]),
&Digest::from_bytes([0x00; 5])
),
7
);
assert_eq!(
hamming(
&Digest::<1>::from_bytes([0xaa]),
&Digest::<1>::from_bytes([0x55])
),
8
);
}
#[test]
fn to_hex_covers_zero_bytes_and_high_nibbles() {
let d = Digest::<4>::from_bytes([0x00, 0xff, 0x0a, 0xa5]);
let mut hex = [0u8; 8];
d.to_hex_into(&mut hex).unwrap();
assert_eq!(&hex[..], b"00ff0aa5");
}
#[test]
fn to_hex_is_exactly_two_chars_per_byte() {
let mut hex = [0u8; 6];
Digest::<3>::from_bytes([0x00, 0x10, 0xf0])
.to_hex_into(&mut hex)
.unwrap();
assert_eq!(&hex[..], b"0010f0");
assert_eq!(Digest::<3>::HEX_LEN, 6);
let mut one = [0u8; 2];
Digest::<1>::from_bytes([0x00])
.to_hex_into(&mut one)
.unwrap();
assert_eq!(&one[..], b"00");
Digest::<1>::from_bytes([0xff])
.to_hex_into(&mut one)
.unwrap();
assert_eq!(&one[..], b"ff");
}
#[test]
fn to_hex_rejects_wrong_buffer_length() {
let mut short = [0u8; 7];
assert_eq!(
Digest::<4>::from_bytes([0; 4]).to_hex_into(&mut short),
Err(Error::BadValue("hex buffer length"))
);
let mut long = [0u8; 9];
assert_eq!(
Digest::<4>::from_bytes([0; 4]).to_hex_into(&mut long),
Err(Error::BadValue("hex buffer length"))
);
}
#[test]
fn from_hex_round_trips_to_hex() {
let d = Digest::<4>::from_bytes([0xde, 0xad, 0xbe, 0xef]);
let mut hex = [0u8; 8];
d.to_hex_into(&mut hex).unwrap();
assert_eq!(&hex[..], b"deadbeef");
assert_eq!(Digest::<4>::from_hex(&hex).unwrap(), d);
}
#[test]
fn from_hex_rejects_odd_length() {
assert_eq!(
Digest::<4>::from_hex(b"deadbee"),
Err(Error::BadValue("hex length is odd"))
);
assert_eq!(
Digest::<1>::from_hex(b"0"),
Err(Error::BadValue("hex length is odd"))
);
}
#[test]
fn from_hex_rejects_non_hex_digit() {
assert_eq!(
Digest::<4>::from_hex(b"deadbefg"),
Err(Error::BadValue("hex digit"))
);
assert_eq!(
Digest::<1>::from_hex(b"0g"),
Err(Error::BadValue("hex digit"))
);
assert_eq!(
Digest::<2>::from_hex(b"zz00"),
Err(Error::BadValue("hex digit"))
);
}
#[test]
fn from_hex_rejects_wrong_length() {
assert_eq!(
Digest::<4>::from_hex(b"deadbe"),
Err(Error::truncated("hex digest", 8, 6))
);
assert_eq!(
Digest::<4>::from_hex(b"cafebabe12"),
Err(Error::BadValue("hex digest length"))
);
}
#[test]
fn from_slice_rejects_short_and_long() {
assert_eq!(
Digest::<4>::from_slice(&[1, 2, 3]),
Err(Error::truncated("digest", 4, 3))
);
assert_eq!(
Digest::<4>::from_slice(&[0, 0, 0, 0, 0]),
Err(Error::BadValue("digest length"))
);
}
#[test]
fn from_slice_accepts_exact_length() {
assert_eq!(
Digest::<4>::from_slice(&[4, 3, 2, 1]).unwrap(),
Digest::<4>::from_bytes([4, 3, 2, 1])
);
}
#[test]
fn from_stream_short_std_reader_is_truncated() {
let mut src = FromIo(Cursor::new(&[0x01, 0x02, 0x03][..]));
assert_eq!(
Digest::<4>::from_stream(&mut src),
Err(Error::truncated("digest", 4, 3))
);
let mut empty = FromIo(Cursor::new(&b""[..]));
assert_eq!(
Digest::<4>::from_stream(&mut empty),
Err(Error::truncated("digest", 4, 0))
);
}
#[test]
fn from_stream_partial_reads_still_fill() {
let mut dribble = Dribble(&[9, 8, 7, 6]);
assert_eq!(
Digest::<4>::from_stream(&mut dribble).unwrap(),
Digest::<4>::from_bytes([9, 8, 7, 6])
);
let mut short = Dribble(&[1, 2]);
assert_eq!(
Digest::<4>::from_stream(&mut short),
Err(Error::truncated("digest", 4, 2))
);
}
#[test]
fn from_stream_reads_exactly_n_and_leaves_the_rest() {
let mut src: &[u8] = &[5, 6, 7, 8, 9, 10];
assert_eq!(
Digest::<4>::from_stream(&mut src).unwrap(),
Digest::<4>::from_bytes([5, 6, 7, 8])
);
assert_eq!(src, &[9, 10]);
}
#[test]
fn digest_default_is_all_zero_even_past_array_default_limit() {
assert_eq!(Digest::<4>::default().as_bytes(), &[0u8; 4]);
assert_eq!(Digest::<64>::default().as_bytes(), &[0u8; 64]);
}
#[test]
fn digest_orders_bytewise() {
let low = Digest::<2>::from_bytes([0x00, 0xff]);
let high = Digest::<2>::from_bytes([0x01, 0x00]);
assert!(low < high); }
#[test]
fn algorithm_wire_round_trip_and_output_len() {
let table = [
(Algorithm::Sha1, "sha1", Some(20usize)),
(Algorithm::Sha256, "sha256", Some(32)),
(Algorithm::Sha512, "sha512", Some(64)),
(Algorithm::Blake3, "blake3", Some(32)),
(Algorithm::Md5, "md5", Some(16)),
(Algorithm::XxHash64, "xxhash64", Some(8)),
(Algorithm::FastCdc, "fastcdc", Some(4)),
(Algorithm::Perceptual, "perceptual", None),
(Algorithm::Unknown, "unknown", None),
];
for (member, wire, len) in table {
assert_eq!(member.as_str(), wire);
assert_eq!(Algorithm::from_str(wire), member);
assert_eq!(member.output_len(), len);
assert_eq!(format!("{member}"), wire);
}
}
#[test]
fn algorithm_unknown_strings_yield_unknown() {
assert_eq!(Algorithm::from_str("sha3-256"), Algorithm::Unknown);
assert_eq!(Algorithm::from_str(""), Algorithm::Unknown);
assert_eq!(Algorithm::from_str("SHA256"), Algorithm::Unknown);
assert_eq!(Algorithm::from_str("sha-256"), Algorithm::Unknown);
}
#[test]
fn format_wire_round_trip() {
let table = [
(Format::Png, "png"),
(Format::Jpeg, "jpeg"),
(Format::Bmp, "bmp"),
(Format::Zip, "zip"),
(Format::Mp4, "mp4"),
(Format::Mp3, "mp3"),
(Format::Wav, "wav"),
(Format::Flac, "flac"),
(Format::Pdf, "pdf"),
(Format::Gif, "gif"),
(Format::Unknown, "unknown"),
];
for (member, wire) in table {
assert_eq!(member.as_str(), wire);
assert_eq!(Format::from_str(wire), member);
assert_eq!(format!("{member}"), wire);
}
}
#[test]
fn format_unknown_strings_yield_unknown() {
assert_eq!(Format::from_str("webp"), Format::Unknown);
assert_eq!(Format::from_str("PNG"), Format::Unknown);
}
#[test]
fn digest_display_matches_to_hex() {
let d1 = Digest::<1>::from_bytes([0x00]);
let d4 = Digest::<4>::from_bytes([0xff, 0x0a, 0x00, 0xa5]);
let d8 = Digest::<8>::from_bytes([0xde, 0xad, 0xbe, 0xef, 0x00, 0x11, 0x22, 0x33]);
assert_eq!(format!("{d1}"), "00");
assert_eq!(format!("{d4}"), "ff0a00a5");
assert_eq!(format!("{d8}"), "deadbeef00112233");
let mut hex1 = [0u8; 2];
d1.to_hex_into(&mut hex1).unwrap();
assert_eq!(format!("{d1}").as_bytes(), &hex1[..]);
let mut hex4 = [0u8; 8];
d4.to_hex_into(&mut hex4).unwrap();
assert_eq!(format!("{d4}").as_bytes(), &hex4[..]);
let mut hex8 = [0u8; 16];
d8.to_hex_into(&mut hex8).unwrap();
assert_eq!(format!("{d8}").as_bytes(), &hex8[..]);
assert_eq!(format!("{d1:>4}"), " 00");
assert_eq!(format!("{d1:<3}"), "00 ");
assert_eq!(format!("{d1:^4}"), " 00 ");
assert_eq!(format!("{d1:0>4}"), "0000");
}
#[test]
fn error_display_is_literal_concatenation() {
assert_eq!(
format!("{}", Error::truncated("idat chunk", 12, 5)),
"truncated: idat chunk"
);
assert_eq!(
format!(
"{}",
Error::InvalidMagic {
what: "PNG signature"
}
),
"invalid magic: PNG signature"
);
assert_eq!(
format!("{}", Error::BadValue("zero denominator")),
"bad value: zero denominator"
);
assert_eq!(
format!("{}", Error::Unsupported("16-bit grey with tRNS")),
"unsupported: 16-bit grey with tRNS"
);
assert_eq!(
format!("{}", Error::too_large("inflate output", 1 << 20)),
"too large: inflate output"
);
}
#[test]
fn error_constructors_build_the_named_variants() {
assert_eq!(
Error::truncated("zlib stream", 10, 4),
Error::Truncated {
what: "zlib stream",
needed: 10,
found: 4
}
);
assert_eq!(
Error::too_large("chunk table", 65536),
Error::TooLarge {
what: "chunk table",
limit: 65536
}
);
let e = Error::truncated("s", 1, 0);
let copied = e;
assert_eq!(e, copied);
}