use std::collections::HashMap;
use std::io::{Read, Seek, SeekFrom};
use binrw::BinRead;
use crate::chunk_header::ChunkHeader;
use crate::chunk_id::ChunkId;
use crate::error::{AdtError, Result};
#[derive(Debug, Clone)]
pub struct ChunkDiscovery {
pub chunks: HashMap<ChunkId, Vec<ChunkLocation>>,
pub file_size: u64,
pub total_chunks: usize,
}
#[derive(Debug, Clone, Copy)]
pub struct ChunkLocation {
pub offset: u64,
pub size: u32,
}
impl ChunkDiscovery {
pub fn new(file_size: u64) -> Self {
Self {
chunks: HashMap::new(),
file_size,
total_chunks: 0,
}
}
pub fn get_chunks(&self, id: ChunkId) -> Option<&Vec<ChunkLocation>> {
self.chunks.get(&id)
}
pub fn has_chunk(&self, id: ChunkId) -> bool {
self.chunks.contains_key(&id)
}
pub fn chunk_count(&self, id: ChunkId) -> usize {
self.chunks.get(&id).map_or(0, |v| v.len())
}
pub fn chunk_types(&self) -> Vec<ChunkId> {
let mut types: Vec<_> = self.chunks.keys().copied().collect();
types.sort_by_key(|id| id.0);
types
}
#[must_use]
pub fn detect_file_type(&self) -> crate::file_type::AdtFileType {
crate::file_type::AdtFileType::from_discovery(self)
}
fn add_chunk(&mut self, id: ChunkId, location: ChunkLocation) {
self.chunks.entry(id).or_default().push(location);
self.total_chunks += 1;
}
}
pub fn discover_chunks<R: Read + Seek>(reader: &mut R) -> Result<ChunkDiscovery> {
let file_size = reader.seek(SeekFrom::End(0))?;
reader.seek(SeekFrom::Start(0))?;
const MIN_FILE_SIZE: u64 = 12; if file_size < MIN_FILE_SIZE {
return Err(AdtError::Io(std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("File too small: {} bytes", file_size),
)));
}
let mut discovery = ChunkDiscovery::new(file_size);
while reader.stream_position()? < file_size {
let chunk_offset = reader.stream_position()?;
let header = match ChunkHeader::read_le(reader) {
Ok(h) => h,
Err(_) => {
break;
}
};
let data_end = chunk_offset + 8 + u64::from(header.size);
if data_end > file_size {
log::warn!(
"Chunk {} at offset {} exceeds file size (chunk ends at {}, file size {})",
header.id,
chunk_offset,
data_end,
file_size
);
break;
}
let location = ChunkLocation {
offset: chunk_offset,
size: header.size,
};
discovery.add_chunk(header.id, location);
reader.seek(SeekFrom::Start(data_end))?;
}
log::debug!(
"Discovery complete: {} chunks, {} unique types",
discovery.total_chunks,
discovery.chunks.len()
);
Ok(discovery)
}
#[cfg(test)]
mod tests {
use super::*;
use std::io::Cursor;
#[test]
fn test_chunk_discovery_basic() {
let mut data = Vec::new();
data.extend_from_slice(&ChunkId::MVER.0); data.extend_from_slice(&4u32.to_le_bytes()); data.extend_from_slice(&18u32.to_le_bytes());
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
assert_eq!(discovery.total_chunks, 1);
assert!(discovery.has_chunk(ChunkId::MVER));
assert_eq!(discovery.chunk_count(ChunkId::MVER), 1);
}
#[test]
fn test_multiple_chunks() {
let mut data = Vec::new();
data.extend_from_slice(&ChunkId::MVER.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&18u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MHDR.0);
data.extend_from_slice(&64u32.to_le_bytes());
data.extend_from_slice(&[0u8; 64]);
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
assert_eq!(discovery.total_chunks, 2);
assert!(discovery.has_chunk(ChunkId::MVER));
assert!(discovery.has_chunk(ChunkId::MHDR));
}
#[test]
fn test_chunk_location_tracking() {
let mut data = Vec::new();
data.extend_from_slice(&ChunkId::MVER.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&18u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MHDR.0);
data.extend_from_slice(&8u32.to_le_bytes());
data.extend_from_slice(&[0u8; 8]);
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
let mver_chunks = discovery.get_chunks(ChunkId::MVER).unwrap();
assert_eq!(mver_chunks[0].offset, 0);
assert_eq!(mver_chunks[0].size, 4);
let mhdr_chunks = discovery.get_chunks(ChunkId::MHDR).unwrap();
assert_eq!(mhdr_chunks[0].offset, 12);
assert_eq!(mhdr_chunks[0].size, 8);
}
#[test]
fn test_duplicate_chunk_types() {
let mut data = Vec::new();
data.extend_from_slice(&ChunkId::MCNK.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&[0u8; 4]);
data.extend_from_slice(&ChunkId::MCNK.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&[0u8; 4]);
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
assert_eq!(discovery.total_chunks, 2);
assert_eq!(discovery.chunk_count(ChunkId::MCNK), 2);
let mcnk_chunks = discovery.get_chunks(ChunkId::MCNK).unwrap();
assert_eq!(mcnk_chunks.len(), 2);
assert_eq!(mcnk_chunks[0].offset, 0);
assert_eq!(mcnk_chunks[1].offset, 12); }
#[test]
fn test_chunk_types_sorted() {
let mut data = Vec::new();
data.extend_from_slice(&ChunkId::MHDR.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&[0u8; 4]);
data.extend_from_slice(&ChunkId::MVER.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&[0u8; 4]);
data.extend_from_slice(&ChunkId::MCIN.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&[0u8; 4]);
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
let types = discovery.chunk_types();
assert_eq!(types.len(), 3);
}
#[test]
fn test_file_too_small() {
let data = vec![0u8; 10];
let mut cursor = Cursor::new(data);
let result = discover_chunks(&mut cursor);
assert!(result.is_err());
}
#[test]
fn test_truncated_chunk() {
let mut data = Vec::new();
data.extend_from_slice(&ChunkId::MVER.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&18u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MHDR.0);
data.extend_from_slice(&64u32.to_le_bytes());
data.extend_from_slice(&[0u8; 10]);
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
assert_eq!(discovery.total_chunks, 1);
assert!(discovery.has_chunk(ChunkId::MVER));
assert!(!discovery.has_chunk(ChunkId::MHDR));
}
#[test]
fn test_empty_discovery() {
let discovery = ChunkDiscovery::new(100);
assert_eq!(discovery.total_chunks, 0);
assert_eq!(discovery.file_size, 100);
assert!(discovery.chunk_types().is_empty());
assert!(!discovery.has_chunk(ChunkId::MVER));
}
#[test]
fn test_detect_file_type_root() {
let mut data = Vec::new();
data.extend_from_slice(&ChunkId::MVER.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&18u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MCNK.0);
data.extend_from_slice(&8u32.to_le_bytes());
data.extend_from_slice(&[0u8; 8]);
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
assert_eq!(
discovery.detect_file_type(),
crate::file_type::AdtFileType::Root
);
}
#[test]
fn test_detect_file_type_texture() {
let mut data = Vec::new();
data.extend_from_slice(&ChunkId::MVER.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&18u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MTEX.0);
data.extend_from_slice(&8u32.to_le_bytes());
data.extend_from_slice(&[0u8; 8]);
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
assert_eq!(
discovery.detect_file_type(),
crate::file_type::AdtFileType::Tex0
);
}
#[test]
fn test_detect_file_type_object() {
let mut data = Vec::new();
data.extend_from_slice(&ChunkId::MVER.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&18u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MDDF.0);
data.extend_from_slice(&8u32.to_le_bytes());
data.extend_from_slice(&[0u8; 8]);
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
assert_eq!(
discovery.detect_file_type(),
crate::file_type::AdtFileType::Obj0
);
}
#[test]
fn test_detect_file_type_lod() {
let mut data = Vec::new();
data.extend_from_slice(&ChunkId::MVER.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&18u32.to_le_bytes());
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
assert_eq!(
discovery.detect_file_type(),
crate::file_type::AdtFileType::Lod
);
}
#[test]
fn test_detect_file_type_mcnk_takes_precedence() {
let mut data = Vec::new();
data.extend_from_slice(&ChunkId::MVER.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&18u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MHDR.0);
data.extend_from_slice(&64u32.to_le_bytes());
data.extend_from_slice(&[0u8; 64]);
data.extend_from_slice(&ChunkId::MTEX.0);
data.extend_from_slice(&8u32.to_le_bytes());
data.extend_from_slice(&[0u8; 8]);
data.extend_from_slice(&ChunkId::MCNK.0);
data.extend_from_slice(&8u32.to_le_bytes());
data.extend_from_slice(&[0u8; 8]);
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
assert_eq!(
discovery.detect_file_type(),
crate::file_type::AdtFileType::Root
);
}
#[test]
fn test_detect_file_type_tex0_split_file() {
let mut data = Vec::new();
data.extend_from_slice(&ChunkId::MVER.0);
data.extend_from_slice(&4u32.to_le_bytes());
data.extend_from_slice(&18u32.to_le_bytes());
data.extend_from_slice(&ChunkId::MTEX.0);
data.extend_from_slice(&8u32.to_le_bytes());
data.extend_from_slice(&[0u8; 8]);
data.extend_from_slice(&ChunkId::MCNK.0);
data.extend_from_slice(&8u32.to_le_bytes());
data.extend_from_slice(&[0u8; 8]);
let mut cursor = Cursor::new(data);
let discovery = discover_chunks(&mut cursor).unwrap();
assert_eq!(
discovery.detect_file_type(),
crate::file_type::AdtFileType::Tex0
);
}
}