use byteorder::{BigEndian, ReadBytesExt};
use std::io::Cursor;
use tracing::{debug, trace, warn};
use super::{ArchiveEntry, ArchiveMetadata, BLTEArchive};
use crate::{BLTE_MAGIC, Error, Result};
pub fn parse_archive(data: Vec<u8>) -> Result<BLTEArchive> {
debug!("Parsing BLTE archive: {} bytes", data.len());
let entries = find_blte_files(&data)?;
debug!("Found {} BLTE files in archive", entries.len());
let total_compressed = entries.iter().map(|e| e.size as u64).sum();
let metadata = ArchiveMetadata {
file_count: entries.len(),
compressed_size: total_compressed,
decompressed_size: None, created: None,
};
Ok(BLTEArchive {
files: entries,
data: Some(data),
metadata,
})
}
fn find_blte_files(data: &[u8]) -> Result<Vec<ArchiveEntry>> {
let mut files = Vec::new();
let mut offset = 0;
while offset + 8 <= data.len() {
#[allow(clippy::op_ref)]
if &data[offset..offset + 4] == BLTE_MAGIC {
trace!("Found BLTE magic at offset {}", offset);
let mut cursor = Cursor::new(&data[offset + 4..offset + 8]);
let header_size = cursor.read_u32::<BigEndian>().map_err(Error::Io)?;
match calculate_blte_size(&data[offset..], header_size) {
Ok(total_size) => {
if offset + total_size <= data.len() {
let mut entry = ArchiveEntry::new(offset, total_size);
if let Ok(metadata) =
extract_entry_metadata(&data[offset..offset + total_size])
{
entry.metadata = metadata;
}
trace!(
"Added BLTE file: offset={}, size={}",
entry.offset, entry.size
);
files.push(entry);
offset += total_size;
} else {
warn!(
"BLTE file extends beyond archive bounds: offset={}, size={}, archive_size={}",
offset,
total_size,
data.len()
);
break;
}
}
Err(e) => {
debug!("Failed to calculate BLTE size at offset {}: {}", offset, e);
offset += 1; }
}
} else {
offset += 1;
}
}
if files.is_empty() {
return Err(Error::InvalidMagic([0, 0, 0, 0])); }
Ok(files)
}
fn calculate_blte_size(data: &[u8], header_size: u32) -> Result<usize> {
if data.len() < 8 {
return Err(Error::TruncatedData {
expected: 8,
actual: data.len(),
});
}
if header_size == 0 {
find_single_chunk_end(data)
} else {
calculate_multichunk_size(data, header_size)
}
}
fn find_single_chunk_end(data: &[u8]) -> Result<usize> {
let mut pos = 8;
while pos + 4 <= data.len() {
#[allow(clippy::op_ref)]
if &data[pos..pos + 4] == BLTE_MAGIC {
return Ok(pos);
}
pos += 1;
}
Ok(data.len())
}
fn calculate_multichunk_size(data: &[u8], header_size: u32) -> Result<usize> {
if data.len() < 12 {
return Err(Error::TruncatedData {
expected: 12,
actual: data.len(),
});
}
let chunk_count_bytes = [data[9], data[10], data[11]];
let chunk_count = u32::from_be_bytes([
0,
chunk_count_bytes[0],
chunk_count_bytes[1],
chunk_count_bytes[2],
]);
if chunk_count == 0 || chunk_count > 65536 {
return Err(Error::InvalidChunkCount(chunk_count));
}
let expected_chunk_table_size = 4 + (chunk_count * 24) as usize; let data_offset = if header_size as usize == expected_chunk_table_size {
8 + header_size as usize
} else if header_size as usize == 8 + expected_chunk_table_size {
header_size as usize
} else {
8 + header_size as usize
};
let mut total_chunk_size = 0u32;
let mut chunk_offset = 12;
let flags = data[8];
let chunk_entry_size = match flags {
0x0F => 24, 0x10 => 40, _ => return Err(Error::InvalidHeaderSize(flags as u32)),
};
for _ in 0..chunk_count {
if chunk_offset + 4 > data.len() {
return Err(Error::TruncatedData {
expected: chunk_offset + 4,
actual: data.len(),
});
}
let mut cursor = Cursor::new(&data[chunk_offset..chunk_offset + 4]);
let chunk_compressed_size = cursor.read_u32::<BigEndian>().map_err(Error::Io)?;
total_chunk_size += chunk_compressed_size;
chunk_offset += chunk_entry_size;
}
Ok(data_offset + total_chunk_size as usize)
}
fn extract_entry_metadata(data: &[u8]) -> Result<super::EntryMetadata> {
use super::EntryMetadata;
if data.len() < 8 {
return Err(Error::TruncatedData {
expected: 8,
actual: data.len(),
});
}
let mut cursor = Cursor::new(&data[4..8]);
let header_size = cursor.read_u32::<BigEndian>().map_err(Error::Io)?;
if header_size == 0 {
Ok(EntryMetadata {
compressed_size: data.len(),
decompressed_size: None,
chunk_count: 1,
validated: false,
})
} else {
if data.len() < 12 {
return Err(Error::TruncatedData {
expected: 12,
actual: data.len(),
});
}
let chunk_count_bytes = [data[9], data[10], data[11]];
let chunk_count = u32::from_be_bytes([
0,
chunk_count_bytes[0],
chunk_count_bytes[1],
chunk_count_bytes[2],
]);
let mut total_decompressed = 0u32;
let mut chunk_offset = 12;
for _ in 0..chunk_count.min(10) {
if chunk_offset + 8 > data.len() {
break;
}
let mut cursor = Cursor::new(&data[chunk_offset + 4..chunk_offset + 8]);
if let Ok(decompressed_size) = cursor.read_u32::<BigEndian>() {
total_decompressed += decompressed_size;
}
let flags = data[8];
let chunk_entry_size = match flags {
0x0F => 24,
0x10 => 40,
_ => 24, };
chunk_offset += chunk_entry_size;
}
Ok(EntryMetadata {
compressed_size: data.len(),
decompressed_size: if total_decompressed > 0 {
Some(total_decompressed as usize)
} else {
None
},
chunk_count: chunk_count as usize,
validated: false,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_single_chunk_end() {
let mut data = vec![0u8; 30];
data[0..4].copy_from_slice(b"BLTE"); data[20..24].copy_from_slice(b"BLTE");
let end = find_single_chunk_end(&data).unwrap();
assert_eq!(end, 20);
}
#[test]
fn test_calculate_blte_size_single_chunk() {
let mut data = vec![0u8; 30];
data[0..4].copy_from_slice(b"BLTE");
data[4..8].copy_from_slice(&0u32.to_be_bytes());
let size = calculate_blte_size(&data, 0).unwrap();
assert_eq!(size, 30); }
}