use crate::ecdsa::sha1;
use crate::error::AacsError;
pub const LOGICAL_SECTOR_SIZE: usize = 2048;
pub const LOGICAL_SECTORS_PER_HASH_UNIT: usize = 96;
pub const HASH_UNIT_SIZE: usize = LOGICAL_SECTORS_PER_HASH_UNIT * LOGICAL_SECTOR_SIZE;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ClipDescriptor {
pub starting_hu_num: u32,
pub clip_num: u32,
pub hu_offset_in_clip: u32,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContentHashTable {
pub clips: Vec<ClipDescriptor>,
pub hash_values: Vec<[u8; 8]>,
}
const CLIP_DESCRIPTOR_SIZE: usize = 12;
pub const HASH_VALUE_SIZE: usize = 8;
impl ContentHashTable {
pub fn parse(
bytes: &[u8],
number_of_digests: u32,
number_of_hash_units: u32,
) -> Result<Self, AacsError> {
let n_digests = number_of_digests as usize;
let n_hash_units = number_of_hash_units as usize;
let header_len = n_digests
.checked_mul(CLIP_DESCRIPTOR_SIZE)
.ok_or(AacsError::Truncated("Content Hash Table header"))?;
let body_len = n_hash_units
.checked_mul(HASH_VALUE_SIZE)
.ok_or(AacsError::Truncated("Content Hash Table body"))?;
let total = header_len
.checked_add(body_len)
.ok_or(AacsError::Truncated("Content Hash Table"))?;
if bytes.len() < total {
return Err(AacsError::OversizedRecord {
what: "Content Hash Table",
declared: total,
available: bytes.len(),
});
}
let mut clips = Vec::with_capacity(n_digests);
let mut cursor = 0usize;
for _ in 0..n_digests {
let starting_hu_num = u32::from_be_bytes([
bytes[cursor],
bytes[cursor + 1],
bytes[cursor + 2],
bytes[cursor + 3],
]);
let clip_num = u32::from_be_bytes([
bytes[cursor + 4],
bytes[cursor + 5],
bytes[cursor + 6],
bytes[cursor + 7],
]);
let hu_offset_in_clip = u32::from_be_bytes([
bytes[cursor + 8],
bytes[cursor + 9],
bytes[cursor + 10],
bytes[cursor + 11],
]);
clips.push(ClipDescriptor {
starting_hu_num,
clip_num,
hu_offset_in_clip,
});
cursor += CLIP_DESCRIPTOR_SIZE;
}
let mut hash_values = Vec::with_capacity(n_hash_units);
for _ in 0..n_hash_units {
let mut hv = [0u8; HASH_VALUE_SIZE];
hv.copy_from_slice(&bytes[cursor..cursor + HASH_VALUE_SIZE]);
hash_values.push(hv);
cursor += HASH_VALUE_SIZE;
}
Ok(Self { clips, hash_values })
}
pub fn len(&self) -> usize {
self.hash_values.len()
}
pub fn is_empty(&self) -> bool {
self.hash_values.is_empty()
}
pub fn verify_hash_unit(
&self,
hash_unit_index: usize,
hash_unit_bytes: &[u8],
) -> Result<(), AacsError> {
let stored = self
.hash_values
.get(hash_unit_index)
.ok_or(AacsError::InvalidValue {
what: "Content Hash Table hash-unit index",
value: hash_unit_index as u64,
})?;
if hash_unit_bytes.len() != HASH_UNIT_SIZE {
return Err(AacsError::BadHashUnitLength(hash_unit_bytes.len()));
}
let computed = hash_value_of_unit(hash_unit_bytes);
if &computed == stored {
Ok(())
} else {
Err(AacsError::ContentHashMismatch {
index: hash_unit_index,
})
}
}
}
pub fn hash_value_of_unit(hash_unit: &[u8]) -> [u8; HASH_VALUE_SIZE] {
let digest = sha1(hash_unit);
let mut out = [0u8; HASH_VALUE_SIZE];
out.copy_from_slice(&digest[20 - HASH_VALUE_SIZE..]);
out
}
#[cfg(test)]
mod tests {
use super::*;
fn synth_hash_unit(seed: u8) -> Vec<u8> {
(0..HASH_UNIT_SIZE)
.map(|i| (i as u8).wrapping_add(seed).wrapping_mul(31))
.collect()
}
#[test]
fn hash_unit_size_is_1344kb_over_seven() {
assert_eq!(HASH_UNIT_SIZE, 96 * 2048);
assert_eq!(HASH_UNIT_SIZE, 196_608);
assert_eq!(7 * HASH_UNIT_SIZE, 1344 * 1024);
}
#[test]
fn hash_value_is_lsb_64_of_sha1() {
let unit = synth_hash_unit(0x10);
let digest = sha1(&unit);
let hv = hash_value_of_unit(&unit);
assert_eq!(&hv[..], &digest[12..20]);
}
#[test]
fn parse_roundtrips_header_and_body() {
let clips = [
ClipDescriptor {
starting_hu_num: 0,
clip_num: 0,
hu_offset_in_clip: 0,
},
ClipDescriptor {
starting_hu_num: 2,
clip_num: 17,
hu_offset_in_clip: 0,
},
];
let units: Vec<Vec<u8>> = (0..3u8).map(synth_hash_unit).collect();
let mut buf = Vec::new();
for c in &clips {
buf.extend_from_slice(&c.starting_hu_num.to_be_bytes());
buf.extend_from_slice(&c.clip_num.to_be_bytes());
buf.extend_from_slice(&c.hu_offset_in_clip.to_be_bytes());
}
for u in &units {
buf.extend_from_slice(&hash_value_of_unit(u));
}
let cht = ContentHashTable::parse(&buf, clips.len() as u32, units.len() as u32).unwrap();
assert_eq!(cht.clips, clips);
assert_eq!(cht.len(), 3);
assert!(!cht.is_empty());
for (i, u) in units.iter().enumerate() {
cht.verify_hash_unit(i, u).unwrap();
}
}
#[test]
fn verify_rejects_tampered_unit() {
let unit = synth_hash_unit(5);
let mut buf = Vec::new();
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&hash_value_of_unit(&unit));
let cht = ContentHashTable::parse(&buf, 1, 1).unwrap();
let mut tampered = unit.clone();
tampered[123] ^= 0x01;
assert_eq!(
cht.verify_hash_unit(0, &tampered),
Err(AacsError::ContentHashMismatch { index: 0 })
);
}
#[test]
fn verify_rejects_wrong_unit_length() {
let unit = synth_hash_unit(1);
let mut buf = Vec::new();
buf.extend_from_slice(&[0u8; CLIP_DESCRIPTOR_SIZE]);
buf.extend_from_slice(&hash_value_of_unit(&unit));
let cht = ContentHashTable::parse(&buf, 1, 1).unwrap();
let short = vec![0u8; HASH_UNIT_SIZE - 1];
assert_eq!(
cht.verify_hash_unit(0, &short),
Err(AacsError::BadHashUnitLength(HASH_UNIT_SIZE - 1))
);
}
#[test]
fn verify_rejects_out_of_range_index() {
let unit = synth_hash_unit(2);
let mut buf = Vec::new();
buf.extend_from_slice(&[0u8; CLIP_DESCRIPTOR_SIZE]);
buf.extend_from_slice(&hash_value_of_unit(&unit));
let cht = ContentHashTable::parse(&buf, 1, 1).unwrap();
assert!(matches!(
cht.verify_hash_unit(9, &unit),
Err(AacsError::InvalidValue { .. })
));
}
#[test]
fn parse_tolerates_trailing_padding() {
let unit = synth_hash_unit(3);
let mut buf = Vec::new();
buf.extend_from_slice(&[0u8; CLIP_DESCRIPTOR_SIZE]);
buf.extend_from_slice(&hash_value_of_unit(&unit));
buf.extend_from_slice(&[0u8; 2048]); let cht = ContentHashTable::parse(&buf, 1, 1).unwrap();
assert_eq!(cht.len(), 1);
cht.verify_hash_unit(0, &unit).unwrap();
}
#[test]
fn parse_rejects_truncated_body() {
let mut buf = Vec::new();
buf.extend_from_slice(&[0u8; CLIP_DESCRIPTOR_SIZE]);
buf.extend_from_slice(&[0u8; HASH_VALUE_SIZE]);
assert!(matches!(
ContentHashTable::parse(&buf, 1, 4),
Err(AacsError::OversizedRecord { .. })
));
}
#[test]
fn zero_byte_table_is_empty() {
let cht = ContentHashTable::parse(&[], 0, 0).unwrap();
assert!(cht.is_empty());
assert_eq!(cht.len(), 0);
}
}