use std::collections::HashMap;
use anyhow::{bail, Result};
use crate::archive::{RpfEncryption, RPF7_MAGIC, RSC7_MAGIC};
use crate::crypto::{encrypt_aes, GtaKeys};
struct BuildDir {
name: String,
subdirs: Vec<BuildDir>,
files: Vec<BuildFile>,
}
struct BuildFile {
name: String,
data: Vec<u8>,
is_resource: bool,
system_flags: u32,
graphics_flags: u32,
}
impl BuildDir {
fn new(name: impl Into<String>) -> Self {
Self { name: name.into(), subdirs: vec![], files: vec![] }
}
fn get_or_create_subdir(&mut self, name: &str) -> &mut BuildDir {
let pos = self.subdirs.iter().position(|d| d.name == name);
if pos.is_none() {
self.subdirs.push(BuildDir::new(name));
}
let idx = self.subdirs.iter().position(|d| d.name == name).unwrap();
&mut self.subdirs[idx]
}
}
#[derive(Debug)]
enum FlatKind {
Directory { entries_index: u32, entries_count: u32 },
Binary { file_offset: u32, file_size: u32, uncompressed_size: u32 },
Resource { file_offset: u32, file_size: u32, system_flags: u32, graphics_flags: u32 },
}
#[derive(Debug)]
struct FlatEntry {
name: String,
name_offset: u32,
kind: FlatKind,
}
pub struct RpfBuilder {
encryption: RpfEncryption,
root: BuildDir,
}
impl RpfBuilder {
pub fn new(encryption: RpfEncryption) -> Self {
Self {
encryption,
root: BuildDir::new(""),
}
}
pub fn add_file(&mut self, path: &str, data: Vec<u8>) {
let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
if parts.is_empty() {
return;
}
let filename = parts[parts.len() - 1];
let dir_parts = &parts[..parts.len() - 1];
let mut dir = &mut self.root;
for part in dir_parts {
dir = dir.get_or_create_subdir(part);
}
let is_resource = data.len() >= 4
&& u32::from_le_bytes(data[..4].try_into().unwrap()) == RSC7_MAGIC;
let (system_flags, graphics_flags) = if is_resource && data.len() >= 16 {
let sys = u32::from_le_bytes(data[8..12].try_into().unwrap());
let gfx = u32::from_le_bytes(data[12..16].try_into().unwrap());
(sys, gfx)
} else {
(0, 0)
};
dir.files.push(BuildFile {
name: filename.to_string(),
data,
is_resource,
system_flags,
graphics_flags,
});
}
pub fn build(self, keys: Option<&GtaKeys>) -> Result<Vec<u8>> {
if self.encryption == RpfEncryption::Ng {
bail!("NG encryption write is not yet implemented");
}
if self.encryption == RpfEncryption::Aes && keys.is_none() {
bail!("AES encryption requires crypto keys");
}
let mut flat: Vec<FlatEntry> = Vec::new();
flat.push(FlatEntry {
name: String::new(),
name_offset: 0,
kind: FlatKind::Directory { entries_index: 0, entries_count: 0 },
});
let mut file_data: Vec<Vec<u8>> = Vec::new();
Self::bfs_flatten(&self.root, 0, &mut flat, &mut file_data);
let mut names_buf: Vec<u8> = Vec::new();
let mut name_map: HashMap<String, u32> = HashMap::new();
for entry in flat.iter_mut() {
let name = entry.name.as_str();
if let Some(&off) = name_map.get(name) {
entry.name_offset = off;
} else {
let off = names_buf.len() as u32;
name_map.insert(name.to_string(), off);
entry.name_offset = off;
names_buf.extend_from_slice(name.as_bytes());
names_buf.push(0); }
}
let rem = names_buf.len() % 16;
if rem != 0 {
names_buf.resize(names_buf.len() + (16 - rem), 0);
}
let names_length = names_buf.len() as u32;
let entry_count = flat.len() as u32;
let header_bytes: u64 = 16 + entry_count as u64 * 16 + names_length as u64;
let header_blocks = (header_bytes + 511) / 512;
let mut current_block = header_blocks as u32;
let mut file_idx = 0usize;
for entry in flat.iter_mut() {
match &mut entry.kind {
FlatKind::Binary { file_offset, file_size, .. }
| FlatKind::Resource { file_offset, file_size, .. } => {
let data = &file_data[file_idx];
let data_blocks = (data.len() as u32 + 511) / 512;
*file_offset = current_block;
*file_size = data.len() as u32;
current_block += data_blocks;
file_idx += 1;
}
FlatKind::Directory { .. } => {}
}
}
let mut entries_buf: Vec<u8> = Vec::with_capacity(flat.len() * 16);
for entry in &flat {
match &entry.kind {
FlatKind::Directory { entries_index, entries_count } => {
entries_buf.extend_from_slice(&entry.name_offset.to_le_bytes());
entries_buf.extend_from_slice(&0x7FFFFF00u32.to_le_bytes());
entries_buf.extend_from_slice(&entries_index.to_le_bytes());
entries_buf.extend_from_slice(&entries_count.to_le_bytes());
}
FlatKind::Binary { file_offset, file_size, uncompressed_size } => {
let name_off = entry.name_offset as u16;
entries_buf.extend_from_slice(&name_off.to_le_bytes());
entries_buf.push((file_size & 0xFF) as u8);
entries_buf.push(((file_size >> 8) & 0xFF) as u8);
entries_buf.push(((file_size >> 16) & 0xFF) as u8);
entries_buf.push((file_offset & 0xFF) as u8);
entries_buf.push(((file_offset >> 8) & 0xFF) as u8);
entries_buf.push(((file_offset >> 16) & 0xFF) as u8);
entries_buf.extend_from_slice(&uncompressed_size.to_le_bytes());
entries_buf.extend_from_slice(&0u32.to_le_bytes()); }
FlatKind::Resource { file_offset, file_size, system_flags, graphics_flags } => {
let name_off = entry.name_offset as u16;
let fs = (*file_size).min(0xFFFFFF);
entries_buf.extend_from_slice(&name_off.to_le_bytes());
entries_buf.push((fs & 0xFF) as u8);
entries_buf.push(((fs >> 8) & 0xFF) as u8);
entries_buf.push(((fs >> 16) & 0xFF) as u8);
entries_buf.push((file_offset & 0xFF) as u8);
entries_buf.push(((file_offset >> 8) & 0xFF) as u8);
entries_buf.push((((file_offset >> 16) & 0xFF) | 0x80) as u8);
entries_buf.extend_from_slice(&system_flags.to_le_bytes());
entries_buf.extend_from_slice(&graphics_flags.to_le_bytes());
}
}
}
let (entries_buf, names_buf) = if self.encryption == RpfEncryption::Aes {
let aes_key = &keys.unwrap().aes_key;
(
encrypt_aes(&entries_buf, aes_key),
encrypt_aes(&names_buf, aes_key),
)
} else {
(entries_buf, names_buf)
};
let total_header = header_blocks as usize * 512;
let total_data: usize = file_data.iter()
.map(|d| ((d.len() + 511) / 512) * 512)
.sum();
let mut out = Vec::with_capacity(total_header + total_data);
out.extend_from_slice(&RPF7_MAGIC.to_le_bytes());
out.extend_from_slice(&entry_count.to_le_bytes());
out.extend_from_slice(&names_length.to_le_bytes());
out.extend_from_slice(&self.encryption.as_u32().to_le_bytes());
out.extend_from_slice(&entries_buf);
out.extend_from_slice(&names_buf);
let pad = total_header - out.len();
out.resize(out.len() + pad, 0);
for data in &file_data {
out.extend_from_slice(data);
let block_pad = ((data.len() + 511) / 512) * 512 - data.len();
out.resize(out.len() + block_pad, 0);
}
Ok(out)
}
fn bfs_flatten(
dir: &BuildDir,
self_flat_idx: usize,
flat: &mut Vec<FlatEntry>,
file_data: &mut Vec<Vec<u8>>,
) {
let mut children_dirs: Vec<&BuildDir> = dir.subdirs.iter().collect();
let mut children_files: Vec<&BuildFile> = dir.files.iter().collect();
children_dirs.sort_by(|a, b| a.name.cmp(&b.name));
children_files.sort_by(|a, b| a.name.cmp(&b.name));
let mut all_names: Vec<(bool, usize)> = Vec::new(); for i in 0..children_dirs.len() {
all_names.push((true, i));
}
for i in 0..children_files.len() {
all_names.push((false, i));
}
all_names.sort_by_key(|&(is_dir, idx)| {
if is_dir { children_dirs[idx].name.clone() }
else { children_files[idx].name.clone() }
});
let entries_index = flat.len() as u32;
let entries_count = all_names.len() as u32;
if let FlatKind::Directory { entries_index: ei, entries_count: ec } =
&mut flat[self_flat_idx].kind
{
*ei = entries_index;
*ec = entries_count;
}
let child_flat_start = flat.len();
for &(is_dir, idx) in &all_names {
if is_dir {
flat.push(FlatEntry {
name: children_dirs[idx].name.clone(),
name_offset: 0,
kind: FlatKind::Directory { entries_index: 0, entries_count: 0 },
});
} else {
let f = children_files[idx];
if f.is_resource {
flat.push(FlatEntry {
name: f.name.clone(),
name_offset: 0,
kind: FlatKind::Resource {
file_offset: 0,
file_size: 0,
system_flags: f.system_flags,
graphics_flags: f.graphics_flags,
},
});
} else {
flat.push(FlatEntry {
name: f.name.clone(),
name_offset: 0,
kind: FlatKind::Binary {
file_offset: 0,
file_size: 0,
uncompressed_size: f.data.len() as u32,
},
});
}
file_data.push(f.data.clone());
}
}
let mut child_flat_idx = child_flat_start;
for &(is_dir, idx) in &all_names {
if is_dir {
let dir_flat_idx = child_flat_idx;
Self::bfs_flatten(children_dirs[idx], dir_flat_idx, flat, file_data);
}
child_flat_idx += 1;
}
}
}