use crate::{BLTEFile, Result};
use std::time::SystemTime;
pub mod builder;
pub mod parser;
pub mod reader;
pub mod recreation;
pub mod validation;
#[derive(Debug)]
pub struct BLTEArchive {
files: Vec<ArchiveEntry>,
data: Option<Vec<u8>>,
metadata: ArchiveMetadata,
}
#[derive(Debug, Clone)]
pub struct ArchiveEntry {
pub offset: usize,
pub size: usize,
blte: Option<BLTEFile>,
pub metadata: EntryMetadata,
}
#[derive(Debug, Clone, Default)]
pub struct ArchiveMetadata {
pub file_count: usize,
pub compressed_size: u64,
pub decompressed_size: Option<u64>,
pub created: Option<SystemTime>,
}
#[derive(Debug, Clone)]
pub struct EntryMetadata {
pub compressed_size: usize,
pub decompressed_size: Option<usize>,
pub chunk_count: usize,
pub validated: bool,
}
#[derive(Debug, Clone)]
pub struct ArchiveStats {
pub file_count: usize,
pub total_size: usize,
pub compressed_size: u64,
pub decompressed_size: Option<u64>,
pub compression_ratio: Option<f64>,
pub compression_modes: CompressionModeStats,
pub size_distribution: SizeDistribution,
}
#[derive(Debug, Clone, Default)]
pub struct CompressionModeStats {
pub none_count: usize,
pub zlib_count: usize,
pub lz4_count: usize,
pub encrypted_count: usize,
pub unknown_count: usize,
}
#[derive(Debug, Clone)]
pub struct SizeDistribution {
pub min_size: usize,
pub max_size: usize,
pub avg_size: f64,
pub median_size: usize,
pub std_dev: f64,
}
impl BLTEArchive {
pub fn parse(data: Vec<u8>) -> Result<Self> {
parser::parse_archive(data)
}
pub fn file_count(&self) -> usize {
self.files.len()
}
pub fn stats(&self) -> ArchiveStats {
self.calculate_stats()
}
pub fn file_info(&self, index: usize) -> Result<&ArchiveEntry> {
self.files
.get(index)
.ok_or(crate::Error::InvalidChunkCount(index as u32))
}
pub fn get_file(&mut self, index: usize) -> Result<&BLTEFile> {
if index >= self.files.len() {
return Err(crate::Error::InvalidChunkCount(index as u32));
}
if self.files[index].blte.is_none() {
let entry = &self.files[index];
if let Some(ref data) = self.data {
let blte_data = data[entry.offset..entry.offset + entry.size].to_vec();
let blte = BLTEFile::parse(blte_data)?;
self.files[index].blte = Some(blte);
} else {
return Err(crate::Error::TruncatedData {
expected: entry.size,
actual: 0,
});
}
}
Ok(self.files[index].blte.as_ref().unwrap())
}
pub fn extract_file(&mut self, index: usize) -> Result<Vec<u8>> {
let blte = self.get_file(index)?;
crate::decompress_blte(blte.raw_data(), None)
}
pub fn extract_file_with_metadata(
&mut self,
index: usize,
) -> Result<recreation::ExtractedFile> {
let entry_offset = self.file_info(index)?.offset;
let entry_size = self.file_info(index)?.size;
let blte = self.get_file(index)?;
recreation::ExtractedFile::from_blte(index, blte, entry_offset, entry_size)
}
pub fn extract_all_with_metadata(&mut self) -> Result<Vec<recreation::ExtractedFile>> {
let mut extracted = Vec::with_capacity(self.file_count());
println!(
"Extracting {} files with metadata preservation...",
self.file_count()
);
for i in 0..self.file_count() {
if i % 1000 == 0 {
println!(" Progress: {}/{} files", i, self.file_count());
}
extracted.push(self.extract_file_with_metadata(i)?);
}
println!(
" Completed: {}/{} files extracted",
extracted.len(),
self.file_count()
);
Ok(extracted)
}
fn calculate_stats(&self) -> ArchiveStats {
let file_count = self.files.len();
let total_size = self.data.as_ref().map(|d| d.len()).unwrap_or(0);
let compressed_size = self.metadata.compressed_size;
let decompressed_size = self.metadata.decompressed_size;
let compression_ratio = if let Some(decomp) = decompressed_size {
if decomp > 0 {
Some((compressed_size as f64 / decomp as f64) * 100.0)
} else {
None
}
} else {
None
};
let sizes: Vec<usize> = self.files.iter().map(|f| f.size).collect();
let min_size = sizes.iter().min().copied().unwrap_or(0);
let max_size = sizes.iter().max().copied().unwrap_or(0);
let avg_size = if !sizes.is_empty() {
sizes.iter().sum::<usize>() as f64 / sizes.len() as f64
} else {
0.0
};
let median_size = if !sizes.is_empty() {
let mut sorted_sizes = sizes.clone();
sorted_sizes.sort_unstable();
sorted_sizes[sorted_sizes.len() / 2]
} else {
0
};
let std_dev = if sizes.len() > 1 {
let variance = sizes
.iter()
.map(|&size| (size as f64 - avg_size).powi(2))
.sum::<f64>()
/ sizes.len() as f64;
variance.sqrt()
} else {
0.0
};
let compression_modes = CompressionModeStats::default();
ArchiveStats {
file_count,
total_size,
compressed_size,
decompressed_size,
compression_ratio,
compression_modes,
size_distribution: SizeDistribution {
min_size,
max_size,
avg_size,
median_size,
std_dev,
},
}
}
}
impl ArchiveEntry {
pub fn new(offset: usize, size: usize) -> Self {
Self {
offset,
size,
blte: None,
metadata: EntryMetadata {
compressed_size: size,
decompressed_size: None,
chunk_count: 0,
validated: false,
},
}
}
pub fn is_loaded(&self) -> bool {
self.blte.is_some()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_archive_entry_creation() {
let entry = ArchiveEntry::new(100, 500);
assert_eq!(entry.offset, 100);
assert_eq!(entry.size, 500);
assert!(!entry.is_loaded());
}
#[test]
fn test_archive_metadata_default() {
let metadata = ArchiveMetadata::default();
assert_eq!(metadata.file_count, 0);
assert_eq!(metadata.compressed_size, 0);
assert!(metadata.decompressed_size.is_none());
}
}