use crate::data::{decompress, scan as scan_chunks, Header};
use crate::manifest::{scan as scan_manifests, Entry};
use crate::Result;
#[derive(Debug, Clone)]
pub struct NcsFile {
pub filename: String,
pub type_name: String,
pub index: u32,
pub offset: usize,
pub header: Header,
}
impl NcsFile {
pub fn decompress(&self, pak_data: &[u8]) -> Result<Vec<u8>> {
decompress(&pak_data[self.offset..])
}
fn type_from_filename(filename: &str) -> String {
crate::pak::type_from_filename(filename)
}
}
#[derive(Debug)]
pub struct ExtractionResult {
pub files: Vec<NcsFile>,
pub missing_chunks: Vec<Entry>,
pub orphan_chunks: Vec<(usize, Header)>,
}
pub fn extract_from_pak(pak_data: &[u8]) -> ExtractionResult {
let manifests = scan_manifests(pak_data);
if manifests.is_empty() {
let orphans = scan_chunks(pak_data);
return ExtractionResult {
files: Vec::new(),
missing_chunks: Vec::new(),
orphan_chunks: orphans,
};
}
let mut all_entries: Vec<&Entry> = manifests
.iter()
.flat_map(|(_, m)| m.entries.iter())
.collect();
all_entries.sort_by_key(|e| e.index);
let mut chunks = scan_chunks(pak_data);
chunks.sort_by_key(|(offset, _)| *offset);
let mut files = Vec::new();
let mut missing_chunks = Vec::new();
for (i, entry) in all_entries.iter().enumerate() {
if i < chunks.len() {
let (offset, header) = chunks[i];
files.push(NcsFile {
filename: entry.filename.clone(),
type_name: NcsFile::type_from_filename(&entry.filename),
index: entry.index,
offset,
header,
});
} else {
missing_chunks.push((*entry).clone());
}
}
let orphan_chunks = if chunks.len() > all_entries.len() {
chunks[all_entries.len()..].to_vec()
} else {
Vec::new()
};
ExtractionResult {
files,
missing_chunks,
orphan_chunks,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{NCS_MAGIC, NCS_MANIFEST_MAGIC};
fn make_ncs_header(compressed: u32) -> Vec<u8> {
let mut data = vec![0x01]; data.extend_from_slice(&NCS_MAGIC);
data.extend_from_slice(&0u32.to_le_bytes()); data.extend_from_slice(&compressed.to_le_bytes()); data.extend_from_slice(&compressed.to_le_bytes()); data
}
fn make_manifest_entry(filename: &str, index: u32) -> Vec<u8> {
let mut data = Vec::new();
let len = (filename.len() + 1) as u32;
data.extend_from_slice(&len.to_le_bytes());
data.extend_from_slice(filename.as_bytes());
data.push(0);
data.extend_from_slice(&index.to_le_bytes());
data
}
fn make_manifest(entries: &[(&str, u32)]) -> Vec<u8> {
let mut data = Vec::new();
data.extend_from_slice(&NCS_MANIFEST_MAGIC);
data.push(0);
data.extend_from_slice(&(entries.len() as u16).to_le_bytes());
data.extend_from_slice(&[0, 0]);
for (filename, index) in entries {
data.extend_from_slice(&make_manifest_entry(filename, *index));
}
data
}
#[test]
fn test_type_from_filename() {
assert_eq!(
NcsFile::type_from_filename("Nexus-Data-itempool0.ncs"),
"itempool"
);
assert_eq!(
NcsFile::type_from_filename("Nexus-Data-achievement6.ncs"),
"achievement"
);
assert_eq!(
NcsFile::type_from_filename("Nexus-Data-aim_assist_parameters0.ncs"),
"aim_assist_parameters"
);
}
#[test]
fn test_extract_no_manifest() {
let mut data = make_ncs_header(8);
data.extend_from_slice(&[0u8; 8]);
let result = extract_from_pak(&data);
assert!(result.files.is_empty());
assert!(result.missing_chunks.is_empty());
assert_eq!(result.orphan_chunks.len(), 1); }
#[test]
fn test_extract_with_manifest() {
let mut data = Vec::new();
let chunk1_offset = data.len();
data.extend_from_slice(&make_ncs_header(4));
data.extend_from_slice(&[0u8; 4]);
let chunk2_offset = data.len();
data.extend_from_slice(&make_ncs_header(8));
data.extend_from_slice(&[0u8; 8]);
data.extend_from_slice(&make_manifest(&[
("Nexus-Data-weapons0.ncs", 100),
("Nexus-Data-shields0.ncs", 112),
]));
let result = extract_from_pak(&data);
assert_eq!(result.files.len(), 2);
assert_eq!(result.files[0].filename, "Nexus-Data-weapons0.ncs");
assert_eq!(result.files[0].type_name, "weapons");
assert_eq!(result.files[0].offset, chunk1_offset);
assert_eq!(result.files[1].filename, "Nexus-Data-shields0.ncs");
assert_eq!(result.files[1].type_name, "shields");
assert_eq!(result.files[1].offset, chunk2_offset);
assert!(result.missing_chunks.is_empty());
assert!(result.orphan_chunks.is_empty());
}
#[test]
fn test_extract_missing_chunk() {
let mut data = Vec::new();
data.extend_from_slice(&make_ncs_header(4));
data.extend_from_slice(&[0u8; 4]);
data.extend_from_slice(&make_manifest(&[
("Nexus-Data-weapons0.ncs", 100),
("Nexus-Data-shields0.ncs", 112),
]));
let result = extract_from_pak(&data);
assert_eq!(result.files.len(), 1);
assert_eq!(result.missing_chunks.len(), 1);
assert_eq!(result.missing_chunks[0].filename, "Nexus-Data-shields0.ncs");
}
}