use crate::{BLTEHeader, Error, Result};
#[derive(Debug, Clone)]
pub struct BLTEFile {
pub header: BLTEHeader,
pub data: Vec<u8>,
}
#[derive(Debug)]
pub struct BLTEFileRef<'a> {
pub header: BLTEHeader,
pub data: &'a [u8],
}
impl BLTEFile {
pub fn parse(data: Vec<u8>) -> Result<Self> {
let header = BLTEHeader::parse(&data)?;
let data_offset = header.data_offset();
if data.len() < data_offset {
return Err(Error::TruncatedData {
expected: data_offset,
actual: data.len(),
});
}
let chunk_data = data[data_offset..].to_vec();
Ok(BLTEFile {
header,
data: chunk_data,
})
}
pub fn parse_ref(data: &[u8]) -> Result<BLTEFileRef<'_>> {
let header = BLTEHeader::parse(data)?;
let data_offset = header.data_offset();
if data.len() < data_offset {
return Err(Error::TruncatedData {
expected: data_offset,
actual: data.len(),
});
}
Ok(BLTEFileRef {
header,
data: &data[data_offset..],
})
}
pub fn get_chunk_data(&self, chunk_index: usize) -> Result<ChunkData> {
if self.header.is_single_chunk() {
if chunk_index != 0 {
return Err(Error::InvalidChunkCount(chunk_index as u32));
}
return Ok(ChunkData {
data: self.data.clone(),
compressed_size: self.data.len() as u32,
decompressed_size: 0, checksum: [0u8; 16], });
}
if chunk_index >= self.header.chunks.len() {
return Err(Error::InvalidChunkCount(chunk_index as u32));
}
let chunk_info = &self.header.chunks[chunk_index];
let mut offset = 0;
for i in 0..chunk_index {
offset += self.header.chunks[i].compressed_size as usize;
}
let end_offset = offset + chunk_info.compressed_size as usize;
if end_offset > self.data.len() {
return Err(Error::TruncatedData {
expected: end_offset,
actual: self.data.len(),
});
}
let chunk_data = self.data[offset..end_offset].to_vec();
Ok(ChunkData {
data: chunk_data,
compressed_size: chunk_info.compressed_size,
decompressed_size: chunk_info.decompressed_size,
checksum: chunk_info.checksum,
})
}
pub fn get_all_chunks(&self) -> Result<Vec<ChunkData>> {
let mut chunks = Vec::new();
let chunk_count = self.header.chunk_count();
for i in 0..chunk_count {
chunks.push(self.get_chunk_data(i)?);
}
Ok(chunks)
}
pub fn is_single_chunk(&self) -> bool {
self.header.is_single_chunk()
}
pub fn chunk_count(&self) -> usize {
self.header.chunk_count()
}
pub fn total_size(&self) -> usize {
self.header.data_offset() + self.data.len()
}
pub fn raw_data(&self) -> Vec<u8> {
let header_size = self.header.data_offset();
let mut raw = Vec::with_capacity(self.total_size());
raw.extend_from_slice(&crate::BLTE_MAGIC);
raw.extend_from_slice(&self.header.header_size.to_be_bytes());
if !self.header.is_single_chunk() {
if self.header.chunks.is_empty() {
raw.extend_from_slice(&[0x0F, 0x00, 0x00, 0x00]); } else {
raw.push(0x0F); let chunk_count = self.header.chunks.len() as u32;
raw.extend_from_slice(&chunk_count.to_be_bytes()[1..]);
for chunk in &self.header.chunks {
raw.extend_from_slice(&chunk.compressed_size.to_be_bytes());
raw.extend_from_slice(&chunk.decompressed_size.to_be_bytes());
raw.extend_from_slice(&chunk.checksum);
}
}
}
while raw.len() < header_size {
raw.push(0);
}
raw.extend_from_slice(&self.data);
raw
}
}
impl<'a> BLTEFileRef<'a> {
pub fn get_chunk_data(&self, chunk_index: usize) -> Result<ChunkDataRef<'a>> {
if self.header.is_single_chunk() {
if chunk_index != 0 {
return Err(Error::InvalidChunkCount(chunk_index as u32));
}
return Ok(ChunkDataRef {
data: self.data,
compressed_size: self.data.len() as u32,
decompressed_size: 0, checksum: [0u8; 16], });
}
if chunk_index >= self.header.chunks.len() {
return Err(Error::InvalidChunkCount(chunk_index as u32));
}
let chunk_info = &self.header.chunks[chunk_index];
let mut offset = 0;
for i in 0..chunk_index {
offset += self.header.chunks[i].compressed_size as usize;
}
let end_offset = offset + chunk_info.compressed_size as usize;
if end_offset > self.data.len() {
return Err(Error::TruncatedData {
expected: end_offset,
actual: self.data.len(),
});
}
Ok(ChunkDataRef {
data: &self.data[offset..end_offset],
compressed_size: chunk_info.compressed_size,
decompressed_size: chunk_info.decompressed_size,
checksum: chunk_info.checksum,
})
}
pub fn chunk_count(&self) -> usize {
self.header.chunk_count()
}
pub fn is_single_chunk(&self) -> bool {
self.header.is_single_chunk()
}
}
#[derive(Debug, Clone)]
pub struct ChunkData {
pub data: Vec<u8>,
pub compressed_size: u32,
pub decompressed_size: u32,
pub checksum: [u8; 16],
}
#[derive(Debug)]
pub struct ChunkDataRef<'a> {
pub data: &'a [u8],
pub compressed_size: u32,
pub decompressed_size: u32,
pub checksum: [u8; 16],
}
impl ChunkData {
pub fn verify_checksum(&self) -> bool {
if self.checksum == [0u8; 16] {
return true; }
let calculated = md5::compute(&self.data);
calculated.0 == self.checksum
}
pub fn compression_mode(&self) -> Result<crate::CompressionMode> {
if self.data.is_empty() {
return Err(Error::TruncatedData {
expected: 1,
actual: 0,
});
}
crate::CompressionMode::from_byte(self.data[0])
.ok_or(Error::UnknownCompressionMode(self.data[0]))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_single_chunk_blte() -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(b"BLTE");
data.extend_from_slice(&0u32.to_be_bytes()); data.extend_from_slice(b"N"); data.extend_from_slice(b"Hello, BLTE!"); data
}
fn create_multi_chunk_blte() -> Vec<u8> {
let chunk1_data = b"NHello";
let chunk2_data = b"N, BLTE!";
let mut data = Vec::new();
let header_size = 1 + 3 + 2 * 24;
data.extend_from_slice(b"BLTE");
data.extend_from_slice(&(header_size as u32).to_be_bytes());
data.push(0x0F); data.extend_from_slice(&[0x00, 0x00, 0x02]);
data.extend_from_slice(&(chunk1_data.len() as u32).to_be_bytes()); data.extend_from_slice(&5u32.to_be_bytes()); data.extend_from_slice(&[0; 16]);
data.extend_from_slice(&(chunk2_data.len() as u32).to_be_bytes()); data.extend_from_slice(&7u32.to_be_bytes()); data.extend_from_slice(&[0; 16]);
data.extend_from_slice(chunk1_data); data.extend_from_slice(chunk2_data);
data
}
#[test]
fn test_single_chunk_file() {
let data = create_single_chunk_blte();
let blte_file = BLTEFile::parse(data).unwrap();
assert!(blte_file.is_single_chunk());
assert_eq!(blte_file.chunk_count(), 1);
let chunk = blte_file.get_chunk_data(0).unwrap();
assert_eq!(chunk.data, b"NHello, BLTE!");
assert_eq!(chunk.compressed_size, 13);
assert_eq!(chunk.decompressed_size, 0); assert_eq!(
chunk.compression_mode().unwrap(),
crate::CompressionMode::None
);
}
#[test]
fn test_multi_chunk_file() {
let data = create_multi_chunk_blte();
let blte_file = BLTEFile::parse(data).unwrap();
assert!(!blte_file.is_single_chunk());
assert_eq!(blte_file.chunk_count(), 2);
let chunk1 = blte_file.get_chunk_data(0).unwrap();
assert_eq!(chunk1.data, b"NHello");
assert_eq!(chunk1.compressed_size, 6);
assert_eq!(chunk1.decompressed_size, 5);
assert_eq!(
chunk1.compression_mode().unwrap(),
crate::CompressionMode::None
);
let chunk2 = blte_file.get_chunk_data(1).unwrap();
assert_eq!(chunk2.data, b"N, BLTE!");
assert_eq!(chunk2.compressed_size, 8);
assert_eq!(chunk2.decompressed_size, 7);
assert_eq!(
chunk2.compression_mode().unwrap(),
crate::CompressionMode::None
);
}
#[test]
fn test_get_all_chunks() {
let data = create_multi_chunk_blte();
let blte_file = BLTEFile::parse(data).unwrap();
let chunks = blte_file.get_all_chunks().unwrap();
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].data, b"NHello");
assert_eq!(chunks[1].data, b"N, BLTE!");
}
#[test]
fn test_invalid_chunk_index() {
let data = create_single_chunk_blte();
let blte_file = BLTEFile::parse(data).unwrap();
let result = blte_file.get_chunk_data(1);
assert!(result.is_err());
matches!(result.unwrap_err(), Error::InvalidChunkCount(_));
}
#[test]
#[allow(deprecated)]
fn test_compression_mode_detection() {
let test_cases = [
(b'N', crate::CompressionMode::None),
(b'Z', crate::CompressionMode::ZLib),
(b'4', crate::CompressionMode::LZ4),
(b'F', crate::CompressionMode::Frame),
(b'E', crate::CompressionMode::Encrypted),
];
for (byte, expected_mode) in test_cases {
let chunk = ChunkData {
data: vec![byte],
compressed_size: 1,
decompressed_size: 1,
checksum: [0u8; 16],
};
assert_eq!(chunk.compression_mode().unwrap(), expected_mode);
}
}
#[test]
fn test_unknown_compression_mode() {
let chunk = ChunkData {
data: vec![b'X'], compressed_size: 1,
decompressed_size: 1,
checksum: [0u8; 16],
};
let result = chunk.compression_mode();
assert!(result.is_err());
matches!(result.unwrap_err(), Error::UnknownCompressionMode(b'X'));
}
}