Add support to import escude archive

This commit is contained in:
2025-06-03 22:06:07 +08:00
parent 09d850256f
commit dead6a0b18
10 changed files with 586 additions and 38 deletions

View File

@@ -299,8 +299,10 @@ pub fn export_script(
Some(output) => {
let mut pb = std::path::PathBuf::from(output);
let filename = std::path::PathBuf::from(filename);
if let Some(fname) = filename.file_name() {
pb.push(fname);
if is_dir {
if let Some(fname) = filename.file_name() {
pb.push(fname);
}
}
pb.to_string_lossy().into_owned()
}
@@ -313,7 +315,7 @@ pub fn export_script(
if !std::fs::exists(&odir)? {
std::fs::create_dir_all(&odir)?;
}
for f in script.iter_archive()? {
for f in script.iter_archive_mut()? {
let f = f?;
if f.is_script() {
let (script_file, _) = parse_script_from_archive(&f, arg, config)?;
@@ -547,7 +549,201 @@ pub fn import_script(
repl: Option<&types::ReplacementTable>,
) -> anyhow::Result<types::ScriptResult> {
eprintln!("Importing {}", filename);
let (script, builder) = parse_script(filename, arg, config)?;
let (mut script, builder) = parse_script(filename, arg, config)?;
if script.is_archive() {
let odir = {
let mut pb = std::path::PathBuf::from(&imp_cfg.output);
let filename = std::path::PathBuf::from(filename);
if is_dir {
if let Some(fname) = filename.file_name() {
pb.push(fname);
}
}
pb.to_string_lossy().into_owned()
};
let files: Vec<_> = script.iter_archive()?.collect();
let files = files.into_iter().filter_map(|f| f.ok()).collect::<Vec<_>>();
let patched_f = if is_dir {
let f = std::path::PathBuf::from(filename);
let mut pb = std::path::PathBuf::from(&imp_cfg.patched);
if let Some(fname) = f.file_name() {
pb.push(fname);
}
pb.set_extension(builder.extensions().first().unwrap_or(&""));
pb.to_string_lossy().into_owned()
} else {
imp_cfg.patched.clone()
};
let files: Vec<_> = files.iter().map(|s| s.as_str()).collect();
let encoding = get_encoding(arg, builder);
let enc = get_archived_encoding(arg, builder, encoding);
let mut arch = builder.create_archive(&patched_f, &files, enc)?;
for f in script.iter_archive_mut()? {
let f = f?;
let mut writer = arch.new_file(f.name())?;
if f.is_script() {
let (script_file, _) = parse_script_from_archive(&f, arg, config)?;
let mut of = match &arg.output_type {
Some(t) => t.clone(),
None => script_file.default_output_script_type(),
};
if !script_file.is_output_supported(of) {
of = script_file.default_output_script_type();
}
let mut out_path = std::path::PathBuf::from(&odir).join(f.name());
let ext = if of.is_custom() {
script_file.custom_output_extension()
} else {
of.as_ref()
};
out_path.set_extension(ext);
let mut mes = match of {
types::OutputScriptType::Json => {
let enc = get_output_encoding(arg);
let b = match utils::files::read_file(&out_path) {
Ok(b) => b,
Err(e) => {
eprintln!("Error reading file {}: {}", out_path.display(), e);
COUNTER.inc_error();
continue;
}
};
let s = match utils::encoding::decode_to_string(enc, &b) {
Ok(s) => s,
Err(e) => {
eprintln!("Error decoding string: {}", e);
COUNTER.inc_error();
continue;
}
};
match serde_json::from_str::<Vec<types::Message>>(&s) {
Ok(mes) => mes,
Err(e) => {
eprintln!("Error parsing JSON: {}", e);
COUNTER.inc_error();
continue;
}
}
}
types::OutputScriptType::M3t => {
let enc = get_output_encoding(arg);
let b = match utils::files::read_file(&out_path) {
Ok(b) => b,
Err(e) => {
eprintln!("Error reading file {}: {}", out_path.display(), e);
COUNTER.inc_error();
continue;
}
};
let s = match utils::encoding::decode_to_string(enc, &b) {
Ok(s) => s,
Err(e) => {
eprintln!("Error decoding string: {}", e);
COUNTER.inc_error();
continue;
}
};
let mut parser = output_scripts::m3t::M3tParser::new(&s);
match parser.parse() {
Ok(mes) => mes,
Err(e) => {
eprintln!("Error parsing M3T: {}", e);
COUNTER.inc_error();
continue;
}
}
}
types::OutputScriptType::Custom => {
Vec::new() // Custom scripts handle their own messages
}
};
if !of.is_custom() && mes.is_empty() {
eprintln!("No messages found in {}", f.name());
COUNTER.inc(types::ScriptResult::Ignored);
continue;
}
let encoding = get_patched_encoding(imp_cfg, builder);
if of.is_custom() {
let enc = get_output_encoding(arg);
match script_file.custom_import(
&out_path.to_string_lossy(),
writer,
encoding,
enc,
) {
Ok(_) => {}
Err(e) => {
eprintln!("Error importing custom script: {}", e);
COUNTER.inc_error();
continue;
}
}
COUNTER.inc(types::ScriptResult::Ok);
continue;
}
let fmt = match imp_cfg.patched_format {
Some(fmt) => match fmt {
types::FormatType::Fixed => types::FormatOptions::Fixed {
length: imp_cfg.patched_fixed_length.unwrap_or(32),
keep_original: imp_cfg.patched_keep_original,
},
types::FormatType::None => types::FormatOptions::None,
},
None => script_file.default_format_type(),
};
match name_csv {
Some(name_table) => {
utils::name_replacement::replace_message(&mut mes, name_table);
}
None => {}
}
format::fmt_message(&mut mes, fmt, *builder.script_type());
if let Err(e) = script_file.import_messages(mes, writer, encoding, repl) {
eprintln!("Error importing messages: {}", e);
COUNTER.inc_error();
continue;
}
} else {
let out_path = std::path::PathBuf::from(&odir).join(f.name());
if out_path.is_file() {
let f = match std::fs::File::open(&out_path) {
Ok(f) => f,
Err(e) => {
eprintln!("Error opening file {}: {}", out_path.display(), e);
COUNTER.inc_error();
continue;
}
};
let mut f = std::io::BufReader::new(f);
match std::io::copy(&mut f, &mut writer) {
Ok(_) => {}
Err(e) => {
eprintln!("Error writing to file {}: {}", out_path.display(), e);
COUNTER.inc_error();
continue;
}
}
} else {
eprintln!(
"Warning: File {} does not exist, use file from original archive.",
out_path.display()
);
COUNTER.inc_warning();
match writer.write_all(f.data()) {
Ok(_) => {}
Err(e) => {
eprintln!("Error writing to file {}: {}", out_path.display(), e);
COUNTER.inc_error();
continue;
}
}
}
}
COUNTER.inc(types::ScriptResult::Ok);
}
arch.write_header()?;
return Ok(types::ScriptResult::Ok);
}
let mut of = match &arg.output_type {
Some(t) => t.clone(),
None => script.default_output_script_type(),

View File

@@ -67,6 +67,17 @@ pub trait ScriptBuilder: std::fmt::Debug {
fn is_archive(&self) -> bool {
false
}
fn create_archive(
&self,
_filename: &str,
_files: &[&str],
_encoding: Encoding,
) -> Result<Box<dyn Archive>> {
Err(anyhow::anyhow!(
"This script type does not support creating an archive."
))
}
}
pub trait ArchiveContent {
@@ -97,12 +108,12 @@ pub trait Script: std::fmt::Debug {
Ok(vec![])
}
fn import_messages(
&self,
fn import_messages<'a>(
&'a self,
_messages: Vec<Message>,
_file: Box<dyn WriteSeek>,
_file: Box<dyn WriteSeek + 'a>,
_encoding: Encoding,
_replacement: Option<&ReplacementTable>,
_replacement: Option<&'a ReplacementTable>,
) -> Result<()> {
if !self.is_archive() {
return Err(anyhow::anyhow!(
@@ -130,10 +141,10 @@ pub trait Script: std::fmt::Debug {
))
}
fn custom_import(
&self,
_custom_filename: &str,
_file: Box<dyn WriteSeek>,
fn custom_import<'a>(
&'a self,
_custom_filename: &'a str,
_file: Box<dyn WriteSeek + 'a>,
_encoding: Encoding,
_output_encoding: Encoding,
) -> Result<()> {
@@ -158,9 +169,20 @@ pub trait Script: std::fmt::Debug {
false
}
fn iter_archive<'a>(
fn iter_archive<'a>(&'a mut self) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
Err(anyhow::anyhow!(
"This script type does not support iterating over archive contents."
))
}
fn iter_archive_mut<'a>(
&'a mut self,
) -> Result<Box<dyn Iterator<Item = Result<Box<dyn ArchiveContent>>> + 'a>> {
Ok(Box::new(std::iter::empty()))
}
}
pub trait Archive {
fn new_file<'a>(&'a mut self, name: &str) -> Result<Box<dyn WriteSeek + 'a>>;
fn write_header(&mut self) -> Result<()>;
}

View File

@@ -141,12 +141,12 @@ impl Script for BGIScript {
Ok(messages)
}
fn import_messages(
&self,
fn import_messages<'a>(
&'a self,
_messages: Vec<Message>,
_filename: Box<dyn WriteSeek>,
_filename: Box<dyn WriteSeek + 'a>,
_encoding: Encoding,
_replacement: Option<&ReplacementTable>,
_replacement: Option<&'a ReplacementTable>,
) -> Result<()> {
Ok(())
}

View File

@@ -221,12 +221,12 @@ impl Script for CircusMesScript {
Ok(mes)
}
fn import_messages(
&self,
fn import_messages<'a>(
&'a self,
messages: Vec<Message>,
mut writer: Box<dyn WriteSeek>,
mut writer: Box<dyn WriteSeek + 'a>,
encoding: Encoding,
replacement: Option<&ReplacementTable>,
replacement: Option<&'a ReplacementTable>,
) -> Result<()> {
let mut repls = Vec::new();
if !encoding.is_jis() {
@@ -244,7 +244,7 @@ impl Script for CircusMesScript {
let _ = insert_repl(&mut repls, "", encoding);
let _ = insert_repl(&mut repls, "", encoding);
if repls.len() < 3 {
println!(
eprintln!(
"Warning: Some replacements cannot used in current encoding. Ruby text may be broken."
);
crate::COUNTER.inc_warning();

View File

@@ -2,9 +2,11 @@ use super::crypto::*;
use crate::ext::io::*;
use crate::scripts::base::*;
use crate::types::*;
use crate::utils::encoding::decode_to_string;
use crate::utils::encoding::{decode_to_string, encode_string};
use anyhow::Result;
use std::io::{Read, Seek, SeekFrom};
use std::collections::HashMap;
use std::ffi::CString;
use std::io::{Read, Seek, SeekFrom, Write};
#[derive(Debug)]
pub struct EscudeBinArchiveBuilder {}
@@ -97,6 +99,18 @@ impl ScriptBuilder for EscudeBinArchiveBuilder {
fn is_archive(&self) -> bool {
true
}
fn create_archive(
&self,
filename: &str,
files: &[&str],
encoding: Encoding,
) -> Result<Box<dyn Archive>> {
let f = std::fs::File::create(filename)?;
let writer = std::io::BufWriter::new(f);
let archive = EscudeBinArchiveWriter::new(writer, files, encoding)?;
Ok(Box::new(archive))
}
}
#[derive(Debug)]
@@ -129,7 +143,6 @@ impl ArchiveContent for Entry {
pub struct EscudeBinArchive<T: Read + Seek + std::fmt::Debug> {
reader: T,
file_count: u32,
name_tbl_len: u32,
entries: Vec<BinEntry>,
archive_encoding: Encoding,
}
@@ -144,7 +157,7 @@ impl<T: Read + Seek + std::fmt::Debug> EscudeBinArchive<T> {
reader.seek(SeekFrom::Start(0xC))?;
let mut crypto_reader = CryptoReader::new(&mut reader)?;
let file_count = crypto_reader.read_u32()?;
let name_tbl_len = crypto_reader.read_u32()?;
let _name_tbl_len = crypto_reader.read_u32()?;
let mut entries = Vec::with_capacity(file_count as usize);
for _ in 0..file_count {
let name_offset = crypto_reader.read_u32()?;
@@ -159,7 +172,6 @@ impl<T: Read + Seek + std::fmt::Debug> EscudeBinArchive<T> {
Ok(EscudeBinArchive {
reader,
file_count,
name_tbl_len,
entries,
archive_encoding,
})
@@ -179,7 +191,16 @@ impl<T: Read + Seek + std::fmt::Debug> Script for EscudeBinArchive<T> {
true
}
fn iter_archive<'a>(
fn iter_archive<'a>(&'a mut self) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
Ok(Box::new(EscudeBinArchiveIter {
entries: self.entries.iter(),
reader: &mut self.reader,
file_count: self.file_count,
archive_encoding: self.archive_encoding,
}))
}
fn iter_archive_mut<'a>(
&'a mut self,
) -> Result<Box<dyn Iterator<Item = Result<Box<dyn ArchiveContent>>> + 'a>> {
Ok(Box::new(EscudeBinArchiveIterator {
@@ -191,6 +212,36 @@ impl<T: Read + Seek + std::fmt::Debug> Script for EscudeBinArchive<T> {
}
}
struct EscudeBinArchiveIter<'a, T: Iterator<Item = &'a BinEntry>, R: Read + Seek> {
entries: T,
reader: &'a mut R,
file_count: u32,
archive_encoding: Encoding,
}
impl<'a, T: Iterator<Item = &'a BinEntry>, R: Read + Seek> Iterator
for EscudeBinArchiveIter<'a, T, R>
{
type Item = Result<String>;
fn next(&mut self) -> Option<Self::Item> {
let entry = match self.entries.next() {
Some(entry) => entry,
None => return None,
};
let name_offset = entry.name_offset as usize + self.file_count as usize * 12 + 0x14;
let name = match self.reader.peek_cstring_at(name_offset) {
Ok(name) => name,
Err(e) => return Some(Err(e.into())),
};
let name = match decode_to_string(self.archive_encoding, name.as_bytes()) {
Ok(name) => name,
Err(e) => return Some(Err(e.into())),
};
Some(Ok(name))
}
}
struct EscudeBinArchiveIterator<'a, T: Iterator<Item = &'a BinEntry>, R: Read + Seek> {
entries: T,
reader: &'a mut R,
@@ -239,3 +290,131 @@ impl<'a, T: Iterator<Item = &'a BinEntry>, R: Read + Seek> Iterator
Some(Ok(Box::new(Entry { name, data })))
}
}
pub struct EscudeBinArchiveWriter<T: Write + Seek> {
writer: T,
headers: HashMap<String, BinEntry>,
name_tbl_len: u32,
}
impl<T: Write + Seek> EscudeBinArchiveWriter<T> {
pub fn new(mut writer: T, files: &[&str], encoding: Encoding) -> Result<Self> {
writer.write_all(b"ESC-ARC2")?;
let header_len = 0xC + 0xC * files.len();
let header = vec![0u8; header_len];
writer.write_all(&header)?;
let mut headers = HashMap::new();
for file in files {
let f = file.to_string();
let encoded = encode_string(encoding, file, true)?;
let encoded = CString::new(encoded)?;
let name_offset = writer.stream_position()? as u32;
writer.write_all(encoded.as_bytes_with_nul())?;
headers.insert(
f,
BinEntry {
name_offset,
data_offset: 0,
length: 0,
},
);
}
let name_tbl_len = writer.stream_position()? as u32 - header_len as u32 - 0x8;
Ok(EscudeBinArchiveWriter {
writer,
headers,
name_tbl_len,
})
}
}
impl<T: Write + Seek> Archive for EscudeBinArchiveWriter<T> {
fn new_file<'a>(&'a mut self, name: &str) -> Result<Box<dyn WriteSeek + 'a>> {
let entry = self
.headers
.get_mut(name)
.ok_or_else(|| anyhow::anyhow!("File '{}' not found in archive", name))?;
if entry.data_offset != 0 {
return Err(anyhow::anyhow!("File '{}' already exists in archive", name));
}
entry.data_offset = self.writer.stream_position()? as u32;
Ok(Box::new(EscudeBinArchiveFile {
header: entry,
writer: &mut self.writer,
pos: 0,
}))
}
fn write_header(&mut self) -> Result<()> {
self.writer.seek(SeekFrom::Start(0x8))?;
let mut crypto = CryptoWriter::new(&mut self.writer)?;
let file_count = self.headers.len() as u32;
crypto.write_u32(file_count)?;
crypto.write_u32(self.name_tbl_len)?;
for entry in self.headers.values() {
let name_offset = entry.name_offset - file_count * 12 - 0x14;
crypto.write_u32(name_offset)?;
crypto.write_u32(entry.data_offset)?;
crypto.write_u32(entry.length)?;
}
Ok(())
}
}
pub struct EscudeBinArchiveFile<'a, T: Write + Seek> {
header: &'a mut BinEntry,
writer: &'a mut T,
pos: usize,
}
impl<'a, T: Write + Seek> Write for EscudeBinArchiveFile<'a, T> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.writer.seek(SeekFrom::Start(
self.header.data_offset as u64 + self.pos as u64,
))?;
let written = self.writer.write(buf)?;
self.pos += written;
self.header.length = self.header.length.max(self.pos as u32);
Ok(written)
}
fn flush(&mut self) -> std::io::Result<()> {
self.writer.flush()
}
}
impl<'a, T: Write + Seek> Seek for EscudeBinArchiveFile<'a, T> {
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
let new_pos = match pos {
SeekFrom::Start(offset) => offset as usize,
SeekFrom::End(offset) => {
if offset < 0 {
if (-offset) as usize > self.header.length as usize {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Seek from end exceeds file length",
));
}
self.header.length as usize - (-offset) as usize
} else {
self.header.length as usize + offset as usize
}
}
SeekFrom::Current(offset) => {
if offset < 0 {
if (-offset) as usize > self.pos {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Seek from current exceeds current position",
));
}
self.pos.saturating_sub((-offset) as usize)
} else {
self.pos + offset as usize
}
}
};
self.pos = new_pos;
Ok(self.pos as u64)
}
}

View File

@@ -1,6 +1,7 @@
use crate::ext::io::*;
use anyhow::Result;
use std::io::{Read, Seek};
use rand::Rng;
use std::io::{Read, Seek, Write};
pub struct CryptoReader<T: Read + Seek> {
reader: T,
@@ -52,3 +53,45 @@ impl<T: Read + Seek> Read for CryptoReader<T> {
Ok(readed)
}
}
pub struct CryptoWriter<T: Write + Seek> {
writer: T,
key: u32,
in_buffer: Vec<u8>,
}
impl<T: Write + Seek> CryptoWriter<T> {
pub fn new(mut writer: T) -> Result<Self> {
let mut rng = rand::rng();
let key = rng.random();
writer.write_u32(key)?;
Ok(Self {
writer,
key,
in_buffer: Vec::new(),
})
}
fn key(&mut self) -> u32 {
self.key ^= 0x65AC9365;
self.key ^= (((self.key >> 1) ^ self.key) >> 3) ^ (((self.key << 1) ^ self.key) << 3);
return self.key;
}
}
impl<T: Write + Seek> Write for CryptoWriter<T> {
fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
self.in_buffer.extend_from_slice(buf);
while self.in_buffer.len() >= 4 {
let mut val = self.in_buffer.as_slice().read_u32()?;
val ^= self.key();
self.writer.write_u32(val)?;
self.in_buffer.drain(0..4);
}
Ok(buf.len())
}
fn flush(&mut self) -> std::io::Result<()> {
self.writer.flush()
}
}

View File

@@ -284,10 +284,10 @@ impl Script for EscudeBinList {
Ok(())
}
fn custom_import(
&self,
custom_filename: &str,
mut writer: Box<dyn WriteSeek>,
fn custom_import<'a>(
&'a self,
custom_filename: &'a str,
mut writer: Box<dyn WriteSeek + 'a>,
encoding: Encoding,
output_encoding: Encoding,
) -> Result<()> {

View File

@@ -116,12 +116,12 @@ impl Script for EscudeBinScript {
.collect())
}
fn import_messages(
&self,
fn import_messages<'a>(
&'a self,
messages: Vec<Message>,
mut writer: Box<dyn WriteSeek>,
mut writer: Box<dyn WriteSeek + 'a>,
encoding: Encoding,
replacement: Option<&ReplacementTable>,
replacement: Option<&'a ReplacementTable>,
) -> Result<()> {
writer.write_all(b"ESCR1_00")?;
let mut offsets = Vec::with_capacity(messages.len());