From aab8000fcea5d407411bd3ea8911ca6652d92544 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Mon, 8 Jun 2026 10:46:29 +0800 Subject: [PATCH] Add support to unpack Yu-RIS archive in exe file --- Cargo.toml | 2 +- src/scripts/yuris/arc/mod.rs | 1 + src/scripts/yuris/arc/pe.rs | 39 ++++++++++++++++++++++++ src/scripts/yuris/arc/ypf.rs | 57 +++++++++++++++++++++++++++++++----- 4 files changed, 90 insertions(+), 9 deletions(-) create mode 100644 src/scripts/yuris/arc/pe.rs diff --git a/Cargo.toml b/Cargo.toml index d7ef330..d93dec6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -124,7 +124,7 @@ will-plus-img = ["will-plus", "image"] yaneurao = [] yaneurao-itufuru = ["yaneurao", "utils-xored-stream"] yuris = ["dep:chrono", "dep:hex", "utils-serde-base64bytes", "utils-xored-stream"] -yuris-arc = ["yuris", "dep:adler", "dep:crc32fast", "flate2", "dep:int-enum", "utils-murmur2", "dep:xxhash-rust", "xxhash-rust/xxh32", "zopfli"] +yuris-arc = ["yuris", "dep:adler", "dep:crc32fast", "flate2", "dep:int-enum", "dep:pelite", "utils-murmur2", "dep:xxhash-rust", "xxhash-rust/xxh32", "zopfli"] yuris-img = ["yuris", "image", "qoi", "webp"] # basic feature image = ["dep:png"] diff --git a/src/scripts/yuris/arc/mod.rs b/src/scripts/yuris/arc/mod.rs index 66bb2c7..3267891 100644 --- a/src/scripts/yuris/arc/mod.rs +++ b/src/scripts/yuris/arc/mod.rs @@ -1 +1,2 @@ +mod pe; pub mod ypf; diff --git a/src/scripts/yuris/arc/pe.rs b/src/scripts/yuris/arc/pe.rs new file mode 100644 index 0000000..ff3d396 --- /dev/null +++ b/src/scripts/yuris/arc/pe.rs @@ -0,0 +1,39 @@ +use anyhow::Result; +use pelite::{PeFile, Wrap}; + +const YSER_MAGIC: &[u8; 4] = b"YSER"; + +/// Find the YPF archive base offset inside a PE (EXE) file. +/// +/// Searches the PE overlay for the "YSER" header signature at 0x10-aligned +/// boundaries, then reads the 32-bit header size field at offset+4 and returns +/// `YSER_offset + header_size` as the start of the YPF data. +pub fn get_base_offset + ?Sized>(data: &D) -> Result { + let file = PeFile::from_bytes(data)?; + let last_section_end = file + .section_headers() + .iter() + .map(|s| s.PointerToRawData + s.SizeOfRawData) + .max() + .unwrap_or_else(|| match file.optional_header() { + Wrap::T32(h) => h.SizeOfHeaders, + Wrap::T64(h) => h.SizeOfHeaders, + }); + let aligned_offset = ((last_section_end + 0xF) & !0xF) as usize; + let data = data.as_ref(); + if aligned_offset + 8 > data.len() { + anyhow::bail!("No overlay for pe image."); + } + for i in (aligned_offset..(data.len() - 8)).step_by(0x10) { + if &data[i..i + 4] == YSER_MAGIC { + let header_size = u32::from_le_bytes([ + data[i + 4], + data[i + 5], + data[i + 6], + data[i + 7], + ]); + return Ok(i as u64 + header_size as u64); + } + } + anyhow::bail!("Failed to find YSER header in pe file.") +} diff --git a/src/scripts/yuris/arc/ypf.rs b/src/scripts/yuris/arc/ypf.rs index abaea00..74e398c 100644 --- a/src/scripts/yuris/arc/ypf.rs +++ b/src/scripts/yuris/arc/ypf.rs @@ -1,4 +1,5 @@ //! Yu-Ris Archive (.ypf) +use super::pe; use crate::ext::io::*; use crate::ext::mutex::*; use crate::scripts::base::*; @@ -45,10 +46,15 @@ impl ScriptBuilder for YpfBuilder { config: &ExtraConfig, _archive: Option<&Box>, ) -> Result> { + let mut base_offset = 0; + if data.starts_with(b"MZ") { + base_offset = pe::get_base_offset(&data)?; + } Ok(Box::new(YPF::new( MemReader::new(data), archive_encoding, config, + base_offset, )?)) } @@ -62,42 +68,69 @@ impl ScriptBuilder for YpfBuilder { ) -> Result> { if filename == "-" { let data = crate::utils::files::read_file(filename)?; + let mut base_offset = 0; + if data.starts_with(b"MZ") { + base_offset = pe::get_base_offset(&data)?; + } Ok(Box::new(YPF::new( MemReader::new(data), archive_encoding, config, + base_offset, )?)) } else { - let f = std::fs::File::open(filename)?; - let reader = std::io::BufReader::new(f); - Ok(Box::new(YPF::new(reader, archive_encoding, config)?)) + let mut file = std::fs::File::open(filename)?; + let mut base_offset = 0; + if file.peek_and_equal(b"MZ").is_ok() { + let mp = pelite::FileMap::open(filename)?; + base_offset = pe::get_base_offset(&mp)?; + } + Ok(Box::new(YPF::new(file, archive_encoding, config, base_offset)?)) } } fn build_script_from_reader<'a>( &self, - reader: Box, + mut reader: Box, _filename: &str, _encoding: Encoding, archive_encoding: Encoding, config: &ExtraConfig, _archive: Option<&Box>, ) -> Result> { - Ok(Box::new(YPF::new(reader, archive_encoding, config)?)) + let mut base_offset = 0; + if reader.peek_and_equal(b"MZ").is_ok() { + let mut data = Vec::new(); + let pos = reader.stream_position()?; + reader.read_to_end(&mut data)?; + reader.seek(SeekFrom::Start(pos))?; + base_offset = pe::get_base_offset(&data)?; + } + Ok(Box::new(YPF::new(reader, archive_encoding, config, base_offset)?)) } fn extensions(&self) -> &'static [&'static str] { - &["ypf"] + &["ypf", "exe"] } fn script_type(&self) -> &'static ScriptType { &ScriptType::YurisYPF } - fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { + fn is_this_format(&self, filename: &str, buf: &[u8], buf_len: usize) -> Option { if buf_len >= 4 && buf.starts_with(b"YPF\0") { return Some(20); } + if buf_len >= 2 && buf.starts_with(b"MZ") { + let p = std::path::Path::new(filename); + if p.exists() { + if let Ok(file) = pelite::FileMap::open(p) { + if pe::get_base_offset(&file).is_ok() { + return Some(20); + } + } + } + } None } @@ -344,7 +377,15 @@ fn cal_name_hash(name: &[u8], typ: NameHashType) -> u32 { } impl<'b, T: Read + Seek + std::fmt::Debug + Send + Sync + 'b> YPF<'b, T> { - pub fn new(mut reader: T, archive_encoding: Encoding, config: &ExtraConfig) -> Result { + pub fn new( + mut reader: T, + archive_encoding: Encoding, + config: &ExtraConfig, + base_offset: u64, + ) -> Result { + if base_offset > 0 { + reader.seek(SeekFrom::Start(base_offset))?; + } let mut header = [0u8; 4]; reader.read_exact(&mut header)?; if &header != b"YPF\0" {