use crate::error::ChdError;
use crate::header::ChdHeader;
use sha1::{Digest, Sha1};
use std::io::{Read, Seek, SeekFrom, Write};
pub const CHD_MDFLAGS_CHECKSUM: u8 = 0x01;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MetadataEntry {
pub metatag: u32,
pub flags: u8,
pub offset: u64,
pub next: u64,
pub value: Vec<u8>,
}
impl MetadataEntry {
pub fn tag_string(&self) -> String {
let bytes = self.metatag.to_be_bytes();
String::from_utf8_lossy(&bytes).to_string()
}
pub fn is_checksummed(&self) -> bool {
(self.flags & CHD_MDFLAGS_CHECKSUM) != 0
}
}
pub const MAX_METADATA_SIZE: usize = 16 * 1024 * 1024;
pub const CHD_METADATA_HEADER_SIZE: usize = 16;
pub struct MetadataIterator<'a, R: Read + Seek> {
reader: &'a mut R,
current_offset: u64,
visited_offsets: std::collections::HashSet<u64>,
file_len: Option<u64>,
}
impl<'a, R: Read + Seek> MetadataIterator<'a, R> {
pub fn new(reader: &'a mut R, meta_offset: u64) -> Self {
Self {
reader,
current_offset: meta_offset,
visited_offsets: std::collections::HashSet::new(),
file_len: None,
}
}
}
impl<'a, R: Read + Seek> Iterator for MetadataIterator<'a, R> {
type Item = Result<MetadataEntry, ChdError>;
fn next(&mut self) -> Option<Self::Item> {
if self.current_offset == 0 {
return None;
}
let offset = self.current_offset;
if !self.visited_offsets.insert(offset) {
self.current_offset = 0;
return Some(Err(ChdError::InvalidData(format!(
"circular metadata linked list detected at offset {}",
offset
))));
}
let file_len = match self.file_len {
Some(len) => len,
None => match self.reader.seek(SeekFrom::End(0)) {
Ok(len) => {
self.file_len = Some(len);
len
}
Err(e) => {
self.current_offset = 0;
return Some(Err(ChdError::Io(e)));
}
},
};
if offset + 16 > file_len {
self.current_offset = 0;
return Some(Err(ChdError::InvalidData(format!(
"metadata header at offset {} exceeds file length {}",
offset, file_len
))));
}
if let Err(e) = self.reader.seek(SeekFrom::Start(offset)) {
self.current_offset = 0;
return Some(Err(ChdError::Io(e)));
}
let mut header_buf = [0u8; CHD_METADATA_HEADER_SIZE];
if let Err(e) = self.reader.read_exact(&mut header_buf) {
self.current_offset = 0;
return Some(Err(ChdError::Io(e)));
}
let metatag =
u32::from_be_bytes([header_buf[0], header_buf[1], header_buf[2], header_buf[3]]);
let flags = header_buf[4];
let length = u32::from_be_bytes([0, header_buf[5], header_buf[6], header_buf[7]]) as usize;
let next = u64::from_be_bytes([
header_buf[8],
header_buf[9],
header_buf[10],
header_buf[11],
header_buf[12],
header_buf[13],
header_buf[14],
header_buf[15],
]);
if length > MAX_METADATA_SIZE || offset + 16 + (length as u64) > file_len {
self.current_offset = 0;
return Some(Err(ChdError::InvalidData(format!(
"invalid metadata entry length {} at offset {} (file length {})",
length, offset, file_len
))));
}
let mut value = vec![0u8; length];
if let Err(e) = self.reader.read_exact(&mut value) {
self.current_offset = 0;
return Some(Err(ChdError::Io(e)));
}
self.current_offset = next;
Some(Ok(MetadataEntry {
metatag,
flags,
offset,
next,
value,
}))
}
}
pub fn read_all_metadata<R: Read + Seek>(
reader: &mut R,
meta_offset: u64,
) -> Result<Vec<MetadataEntry>, ChdError> {
MetadataIterator::new(reader, meta_offset).collect()
}
#[must_use]
pub fn compute_overall_sha1(raw_sha1: [u8; 20], metadata: &[MetadataEntry]) -> [u8; 20] {
let mut hash_entries: Vec<[u8; 24]> = Vec::new();
for entry in metadata {
if entry.is_checksummed() {
let mut hasher = Sha1::new();
hasher.update(&entry.value);
let val_sha1: [u8; 20] = hasher.finalize().into();
let mut combined = [0u8; 24];
combined[0..4].copy_from_slice(&entry.metatag.to_be_bytes());
combined[4..24].copy_from_slice(&val_sha1);
hash_entries.push(combined);
}
}
hash_entries.sort_unstable();
let mut overall_hasher = Sha1::new();
overall_hasher.update(raw_sha1);
for item in &hash_entries {
overall_hasher.update(item);
}
overall_hasher.finalize().into()
}
pub fn write_metadata_entry<F: Read + Write + Seek>(
file: &mut F,
header: &mut ChdHeader,
tag: u32,
index: u32,
value: &[u8],
flags: u8,
) -> Result<(), ChdError> {
if value.is_empty() || value.len() >= 16 * 1024 * 1024 {
return Err(ChdError::InvalidData(
"invalid metadata payload length".into(),
));
}
let metadata = read_all_metadata(file, header.meta_offset)?;
let mut found_idx = None;
let mut tag_count = 0u32;
for (idx, entry) in metadata.iter().enumerate() {
if entry.metatag == tag {
if tag_count == index {
found_idx = Some(idx);
break;
}
tag_count += 1;
}
}
let mut finished = false;
if let Some(idx) = found_idx {
let entry = &metadata[idx];
if value.len() <= entry.value.len() {
file.seek(SeekFrom::Start(entry.offset + 16))?;
file.write_all(value)?;
if value.len() != entry.value.len() {
let val_len = (value.len() as u32).to_be_bytes();
file.seek(SeekFrom::Start(entry.offset + 5))?;
file.write_all(&val_len[1..4])?;
}
file.seek(SeekFrom::Start(entry.offset + 4))?;
file.write_all(&[flags])?;
finished = true;
} else {
if idx == 0 {
header.meta_offset = entry.next;
file.seek(SeekFrom::Start(48))?;
file.write_all(&header.meta_offset.to_be_bytes())?;
} else {
let prev = &metadata[idx - 1];
file.seek(SeekFrom::Start(prev.offset + 8))?;
file.write_all(&entry.next.to_be_bytes())?;
}
}
}
if !finished {
let new_offset = file.seek(SeekFrom::End(0))?;
let mut hdr = [0u8; 16];
hdr[0..4].copy_from_slice(&tag.to_be_bytes());
hdr[4] = flags;
let val_len = (value.len() as u32).to_be_bytes();
hdr[5..8].copy_from_slice(&val_len[1..4]);
file.write_all(&hdr)?;
file.write_all(value)?;
let current_entries = read_all_metadata(file, header.meta_offset)?;
if let Some(last) = current_entries.last() {
file.seek(SeekFrom::Start(last.offset + 8))?;
file.write_all(&new_offset.to_be_bytes())?;
} else {
header.meta_offset = new_offset;
file.seek(SeekFrom::Start(48))?;
file.write_all(&new_offset.to_be_bytes())?;
}
}
let updated_metadata = read_all_metadata(file, header.meta_offset)?;
header.sha1 = compute_overall_sha1(header.raw_sha1, &updated_metadata);
file.seek(SeekFrom::Start(84))?;
file.write_all(&header.sha1)?;
file.flush()?;
Ok(())
}
pub fn delete_metadata_entry<F: Read + Write + Seek>(
file: &mut F,
header: &mut ChdHeader,
tag: u32,
index: u32,
) -> Result<(), ChdError> {
let metadata = read_all_metadata(file, header.meta_offset)?;
let mut found_idx = None;
let mut tag_count = 0u32;
for (idx, entry) in metadata.iter().enumerate() {
if entry.metatag == tag {
if tag_count == index {
found_idx = Some(idx);
break;
}
tag_count += 1;
}
}
let idx = found_idx.ok_or(ChdError::MetadataNotFound)?;
let entry = &metadata[idx];
if idx == 0 {
header.meta_offset = entry.next;
file.seek(SeekFrom::Start(48))?;
file.write_all(&header.meta_offset.to_be_bytes())?;
} else {
let prev = &metadata[idx - 1];
file.seek(SeekFrom::Start(prev.offset + 8))?;
file.write_all(&entry.next.to_be_bytes())?;
}
let updated_metadata = read_all_metadata(file, header.meta_offset)?;
header.sha1 = compute_overall_sha1(header.raw_sha1, &updated_metadata);
file.seek(SeekFrom::Start(84))?;
file.write_all(&header.sha1)?;
file.flush()?;
Ok(())
}