use crate::error::ChdError;
use std::io::{Read, Seek, SeekFrom, Write};
pub const CHD_V5_SIGNATURE: &[u8; 8] = b"MComprHD";
pub const CHD_V3_HEADER_SIZE: u32 = 120;
pub const CHD_V3_VERSION: u32 = 3;
pub const CHD_V4_HEADER_SIZE: u32 = 108;
pub const CHD_V4_VERSION: u32 = 4;
pub const CHD_V5_HEADER_SIZE: u32 = 124;
pub const CHD_V5_VERSION: u32 = 5;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ChdHeader {
pub tag: [u8; 8],
pub length: u32,
pub version: u32,
pub compressors: [u32; 4],
pub logical_bytes: u64,
pub map_offset: u64,
pub meta_offset: u64,
pub hunk_bytes: u32,
pub unit_bytes: u32,
pub raw_sha1: [u8; 20],
pub sha1: [u8; 20],
pub parent_sha1: [u8; 20],
}
impl ChdHeader {
pub fn hunk_count(&self) -> u64 {
if self.hunk_bytes == 0 {
0
} else {
self.logical_bytes.div_ceil(self.hunk_bytes as u64)
}
}
pub fn unit_count(&self) -> u64 {
if self.unit_bytes == 0 {
0
} else {
self.logical_bytes.div_ceil(self.unit_bytes as u64)
}
}
pub fn is_compressed(&self) -> bool {
self.compressors.iter().any(|&c| c != 0)
}
pub fn raw_sha1_offset(&self) -> u64 {
match self.version {
3 => 80,
4 => 88,
_ => 64,
}
}
pub fn sha1_offset(&self) -> u64 {
match self.version {
3 => 80,
4 => 48,
_ => 84,
}
}
pub fn read<R: Read + Seek>(reader: &mut R) -> Result<Self, ChdError> {
reader.seek(SeekFrom::Start(0))?;
let mut magic = [0u8; 16];
reader.read_exact(&mut magic)?;
if &magic[0..8] != CHD_V5_SIGNATURE {
return Err(ChdError::InvalidMagic);
}
let length = u32::from_be_bytes([magic[8], magic[9], magic[10], magic[11]]);
let version = u32::from_be_bytes([magic[12], magic[13], magic[14], magic[15]]);
match version {
3 => Self::read_v3(reader, length),
4 => Self::read_v4(reader, length),
5 => Self::read_v5(reader),
_ => Err(ChdError::UnsupportedVersion(version)),
}
}
pub fn read_v3<R: Read + Seek>(reader: &mut R, length: u32) -> Result<Self, ChdError> {
if length != CHD_V3_HEADER_SIZE {
return Err(ChdError::InvalidHeaderSize(length));
}
reader.seek(SeekFrom::Start(0))?;
let mut buf = [0u8; CHD_V3_HEADER_SIZE as usize];
reader.read_exact(&mut buf)?;
let flags = u32::from_be_bytes([buf[16], buf[17], buf[18], buf[19]]);
let comp_type = u32::from_be_bytes([buf[20], buf[21], buf[22], buf[23]]);
let logical_bytes = u64::from_be_bytes([
buf[28], buf[29], buf[30], buf[31], buf[32], buf[33], buf[34], buf[35],
]);
let meta_offset = u64::from_be_bytes([
buf[36], buf[37], buf[38], buf[39], buf[40], buf[41], buf[42], buf[43],
]);
let hunk_bytes = u32::from_be_bytes([buf[76], buf[77], buf[78], buf[79]]);
if hunk_bytes == 0 || hunk_bytes > crate::MAX_HUNK_BYTES {
return Err(ChdError::InvalidData(format!(
"invalid hunk_bytes: {}",
hunk_bytes
)));
}
let mut sha1 = [0u8; 20];
sha1.copy_from_slice(&buf[80..100]);
let mut parent_sha1 = [0u8; 20];
if (flags & 1) != 0 {
parent_sha1.copy_from_slice(&buf[100..120]);
}
let mut compressors = [0u32; 4];
match comp_type {
0 => {}
1 | 2 => compressors[0] = u32::from_be_bytes(*b"zlib"),
3 => compressors[0] = u32::from_be_bytes(*b"avhu"),
_ => {
return Err(ChdError::Codec(format!(
"unknown v3 compression type {}",
comp_type
)))
}
}
let mut unit_bytes = hunk_bytes;
if meta_offset != 0 {
if let Ok(metadata_list) = crate::metadata::read_all_metadata(reader, meta_offset) {
for meta in &metadata_list {
if meta.metatag == u32::from_be_bytes(*b"GDDD")
|| meta.metatag == u32::from_be_bytes(*b"CDTR")
|| meta.metatag == u32::from_be_bytes(*b"CDRM")
{
unit_bytes = 2448;
break;
} else if meta.metatag == u32::from_be_bytes(*b"GDSY") {
if let Ok(text) = std::str::from_utf8(&meta.value) {
if let Some(pos) = text.find("BPS:") {
if let Ok(bps) = text[pos + 4..]
.trim()
.split(',')
.next()
.unwrap_or("")
.parse::<u32>()
{
if bps > 0 {
unit_bytes = bps;
break;
}
}
}
}
}
}
}
}
Ok(Self {
tag: *CHD_V5_SIGNATURE,
length: CHD_V3_HEADER_SIZE,
version: CHD_V3_VERSION,
compressors,
logical_bytes,
map_offset: CHD_V3_HEADER_SIZE as u64,
meta_offset,
hunk_bytes,
unit_bytes,
raw_sha1: sha1,
sha1,
parent_sha1,
})
}
pub fn read_v4<R: Read + Seek>(reader: &mut R, length: u32) -> Result<Self, ChdError> {
if length != CHD_V4_HEADER_SIZE {
return Err(ChdError::InvalidHeaderSize(length));
}
reader.seek(SeekFrom::Start(0))?;
let mut buf = [0u8; CHD_V4_HEADER_SIZE as usize];
reader.read_exact(&mut buf)?;
let flags = u32::from_be_bytes([buf[16], buf[17], buf[18], buf[19]]);
let comp_type = u32::from_be_bytes([buf[20], buf[21], buf[22], buf[23]]);
let logical_bytes = u64::from_be_bytes([
buf[28], buf[29], buf[30], buf[31], buf[32], buf[33], buf[34], buf[35],
]);
let meta_offset = u64::from_be_bytes([
buf[36], buf[37], buf[38], buf[39], buf[40], buf[41], buf[42], buf[43],
]);
let hunk_bytes = u32::from_be_bytes([buf[44], buf[45], buf[46], buf[47]]);
if hunk_bytes == 0 || hunk_bytes > crate::MAX_HUNK_BYTES {
return Err(ChdError::InvalidData(format!(
"invalid hunk_bytes: {}",
hunk_bytes
)));
}
let mut sha1 = [0u8; 20];
sha1.copy_from_slice(&buf[48..68]);
let mut parent_sha1 = [0u8; 20];
if (flags & 1) != 0 {
parent_sha1.copy_from_slice(&buf[68..88]);
}
let mut raw_sha1 = [0u8; 20];
raw_sha1.copy_from_slice(&buf[88..108]);
let mut compressors = [0u32; 4];
match comp_type {
0 => {}
1 | 2 => compressors[0] = u32::from_be_bytes(*b"zlib"),
3 => compressors[0] = u32::from_be_bytes(*b"avhu"),
_ => {
return Err(ChdError::Codec(format!(
"unknown v4 compression type {}",
comp_type
)))
}
}
let mut unit_bytes = hunk_bytes;
if meta_offset != 0 {
if let Ok(metadata_list) = crate::metadata::read_all_metadata(reader, meta_offset) {
for meta in &metadata_list {
if meta.metatag == u32::from_be_bytes(*b"GDDD")
|| meta.metatag == u32::from_be_bytes(*b"CDTR")
|| meta.metatag == u32::from_be_bytes(*b"CDRM")
{
unit_bytes = 2448;
break;
} else if meta.metatag == u32::from_be_bytes(*b"GDSY") {
if let Ok(text) = std::str::from_utf8(&meta.value) {
if let Some(pos) = text.find("BPS:") {
if let Ok(bps) = text[pos + 4..]
.trim()
.split(',')
.next()
.unwrap_or("")
.parse::<u32>()
{
if bps > 0 {
unit_bytes = bps;
break;
}
}
}
}
}
}
}
}
Ok(Self {
tag: *CHD_V5_SIGNATURE,
length: CHD_V4_HEADER_SIZE,
version: CHD_V4_VERSION,
compressors,
logical_bytes,
map_offset: CHD_V4_HEADER_SIZE as u64,
meta_offset,
hunk_bytes,
unit_bytes,
raw_sha1,
sha1,
parent_sha1,
})
}
pub fn read_v5<R: Read + Seek>(reader: &mut R) -> Result<Self, ChdError> {
reader.seek(SeekFrom::Start(0))?;
let mut buf = [0u8; CHD_V5_HEADER_SIZE as usize];
reader.read_exact(&mut buf)?;
if &buf[0..8] != CHD_V5_SIGNATURE {
return Err(ChdError::InvalidMagic);
}
let length = u32::from_be_bytes([buf[8], buf[9], buf[10], buf[11]]);
if length != CHD_V5_HEADER_SIZE {
return Err(ChdError::InvalidHeaderSize(length));
}
let version = u32::from_be_bytes([buf[12], buf[13], buf[14], buf[15]]);
if version != CHD_V5_VERSION {
return Err(ChdError::UnsupportedVersion(version));
}
let mut compressors = [0u32; 4];
for (i, item) in compressors.iter_mut().enumerate() {
let start = 16 + i * 4;
*item =
u32::from_be_bytes([buf[start], buf[start + 1], buf[start + 2], buf[start + 3]]);
}
let logical_bytes = u64::from_be_bytes([
buf[32], buf[33], buf[34], buf[35], buf[36], buf[37], buf[38], buf[39],
]);
let map_offset = u64::from_be_bytes([
buf[40], buf[41], buf[42], buf[43], buf[44], buf[45], buf[46], buf[47],
]);
let meta_offset = u64::from_be_bytes([
buf[48], buf[49], buf[50], buf[51], buf[52], buf[53], buf[54], buf[55],
]);
let hunk_bytes = u32::from_be_bytes([buf[56], buf[57], buf[58], buf[59]]);
let unit_bytes = u32::from_be_bytes([buf[60], buf[61], buf[62], buf[63]]);
if hunk_bytes == 0 || unit_bytes == 0 {
return Err(ChdError::InvalidData(
"hunk_bytes and unit_bytes must be non-zero".into(),
));
}
if hunk_bytes > crate::MAX_HUNK_BYTES {
return Err(ChdError::InvalidData(format!(
"hunk_bytes ({}) exceeds maximum allowed ({})",
hunk_bytes,
crate::MAX_HUNK_BYTES
)));
}
if !hunk_bytes.is_multiple_of(unit_bytes) {
return Err(ChdError::InvalidData(format!(
"hunk_bytes ({}) must be a multiple of unit_bytes ({})",
hunk_bytes, unit_bytes
)));
}
let mut raw_sha1 = [0u8; 20];
raw_sha1.copy_from_slice(&buf[64..84]);
let mut sha1 = [0u8; 20];
sha1.copy_from_slice(&buf[84..104]);
let mut parent_sha1 = [0u8; 20];
parent_sha1.copy_from_slice(&buf[104..124]);
Ok(Self {
tag: *CHD_V5_SIGNATURE,
length,
version,
compressors,
logical_bytes,
map_offset,
meta_offset,
hunk_bytes,
unit_bytes,
raw_sha1,
sha1,
parent_sha1,
})
}
pub fn write_v5<W: Write + Seek>(&self, writer: &mut W) -> Result<(), ChdError> {
writer.seek(SeekFrom::Start(0))?;
let mut buf = [0u8; CHD_V5_HEADER_SIZE as usize];
buf[0..8].copy_from_slice(&self.tag);
buf[8..12].copy_from_slice(&self.length.to_be_bytes());
buf[12..16].copy_from_slice(&self.version.to_be_bytes());
for (i, &comp) in self.compressors.iter().enumerate() {
let start = 16 + i * 4;
buf[start..start + 4].copy_from_slice(&comp.to_be_bytes());
}
buf[32..40].copy_from_slice(&self.logical_bytes.to_be_bytes());
buf[40..48].copy_from_slice(&self.map_offset.to_be_bytes());
buf[48..56].copy_from_slice(&self.meta_offset.to_be_bytes());
buf[56..60].copy_from_slice(&self.hunk_bytes.to_be_bytes());
buf[60..64].copy_from_slice(&self.unit_bytes.to_be_bytes());
buf[64..84].copy_from_slice(&self.raw_sha1);
buf[84..104].copy_from_slice(&self.sha1);
buf[104..124].copy_from_slice(&self.parent_sha1);
writer.write_all(&buf)?;
Ok(())
}
}