From a85c67e806071196d0a3c1c4fb3bfead626f987a Mon Sep 17 00:00:00 2001 From: lifegpc Date: Sun, 5 Apr 2026 23:36:26 +0800 Subject: [PATCH] Remove xp3 deps --- Cargo.lock | 13 - Cargo.toml | 3 +- src/ext/io.rs | 14 ++ src/scripts/kirikiri/archive/mod.rs | 1 - src/scripts/kirikiri/archive/xp3/archive.rs | 61 +++++ .../archive/{xp3pack => xp3}/consts.rs | 4 + src/scripts/kirikiri/archive/xp3/crypt.rs | 34 +++ .../kirikiri/archive/{xp3.rs => xp3/mod.rs} | 229 +++++++++--------- src/scripts/kirikiri/archive/xp3/read.rs | 181 ++++++++++++++ .../archive/{xp3pack => xp3}/reader.rs | 0 .../archive/{xp3pack => xp3}/segmenter.rs | 0 .../archive/{xp3pack => xp3}/writer.rs | 0 .../kirikiri/archive/xp3pack/archive.rs | 24 -- src/scripts/kirikiri/archive/xp3pack/mod.rs | 9 - 14 files changed, 416 insertions(+), 157 deletions(-) create mode 100644 src/scripts/kirikiri/archive/xp3/archive.rs rename src/scripts/kirikiri/archive/{xp3pack => xp3}/consts.rs (86%) create mode 100644 src/scripts/kirikiri/archive/xp3/crypt.rs rename src/scripts/kirikiri/archive/{xp3.rs => xp3/mod.rs} (71%) create mode 100644 src/scripts/kirikiri/archive/xp3/read.rs rename src/scripts/kirikiri/archive/{xp3pack => xp3}/reader.rs (100%) rename src/scripts/kirikiri/archive/{xp3pack => xp3}/segmenter.rs (100%) rename src/scripts/kirikiri/archive/{xp3pack => xp3}/writer.rs (100%) delete mode 100644 src/scripts/kirikiri/archive/xp3pack/archive.rs delete mode 100644 src/scripts/kirikiri/archive/xp3pack/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 56caa96..3132050 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1457,7 +1457,6 @@ dependencies = [ "webp", "windows-sys 0.61.2", "xml5ever", - "xp3", "zopfli", "zstd", ] @@ -2664,18 +2663,6 @@ dependencies = [ "markup5ever", ] -[[package]] -name = "xp3" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c728da4ef7d98958a2d42fd957e82dd96723ec9c6255ccb3e743142d556ab6" -dependencies = [ - "adler32", - "byteorder", - "encoding", - "flate2", -] - [[package]] name = "yoke" version = "0.8.2" diff --git a/Cargo.toml b/Cargo.toml index 9fa5a97..82411ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,6 @@ url = { version = "2.5", optional = true } utf16string = "0.2" webp = { version = "0.3", default-features = false, optional = true } xml5ever = { version = "0.38", optional = true } -xp3 = { version = "0.3", optional = true} zopfli = { version = "0.8", optional = true } zstd = { version = "0.13", optional = true } @@ -93,7 +92,7 @@ hexen-haus = ["memchr", "utils-str"] hexen-haus-arc = ["hexen-haus"] hexen-haus-img = ["hexen-haus", "image"] kirikiri = ["emote-psb", "fancy-regex", "flate2", "json", "lz4", "utils-escape"] -kirikiri-arc = ["kirikiri", "adler", "fastcdc", "flate2", "parse-size", "sha2", "xp3", "zopfli", "zstd"] +kirikiri-arc = ["kirikiri", "adler", "fastcdc", "flate2", "parse-size", "sha2", "zopfli", "zstd"] kirikiri-img = ["kirikiri", "image", "libtlg-rs"] musica = [] musica-arc = ["musica", "crc32fast", "flate2", "include-flate", "utils-blowfish", "utils-rc4", "utils-serde-base64bytes", "utils-xored-stream"] diff --git a/src/ext/io.rs b/src/ext/io.rs index 80c3876..1b8fa63 100644 --- a/src/ext/io.rs +++ b/src/ext/io.rs @@ -947,6 +947,9 @@ pub trait ReadExt { /// Reads as much data as possible into the provided buffer. /// Returns the number of bytes read. fn read_most(&mut self, buf: &mut [u8]) -> Result; + /// Skips a specified number of bytes in the reader. + /// Will return an error if readed bytes are less than the specified length. + fn skip(&mut self, len: u64) -> Result<()>; } impl ReadExt for T { @@ -1129,6 +1132,17 @@ impl ReadExt for T { } Ok(total_read) } + + fn skip(&mut self, len: u64) -> Result<()> { + let skiped = std::io::copy(&mut self.by_ref().take(len), &mut std::io::sink())?; + if skiped != len { + return Err(std::io::Error::new( + std::io::ErrorKind::UnexpectedEof, + "Failed to skip the specified number of bytes", + )); + } + Ok(()) + } } /// A trait to help to write data to a writer. diff --git a/src/scripts/kirikiri/archive/mod.rs b/src/scripts/kirikiri/archive/mod.rs index bdec2f4..402fbea 100644 --- a/src/scripts/kirikiri/archive/mod.rs +++ b/src/scripts/kirikiri/archive/mod.rs @@ -1,2 +1 @@ pub mod xp3; -mod xp3pack; diff --git a/src/scripts/kirikiri/archive/xp3/archive.rs b/src/scripts/kirikiri/archive/xp3/archive.rs new file mode 100644 index 0000000..1545471 --- /dev/null +++ b/src/scripts/kirikiri/archive/xp3/archive.rs @@ -0,0 +1,61 @@ +use super::crypt::Crypt; +use crate::scripts::base::ReadSeek; +use std::sync::{Arc, Mutex}; + +/// Represents a single data segment for a file. +/// A file can be split into multiple segments, which can be compressed independently. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Segment { + pub is_compressed: bool, + /// The offset of the segment's data within the archive file. + pub start: u64, + /// The offset of this segment within the original, uncompressed file. + pub offset_in_file: u64, + /// The size of the segment after decompression. + pub original_size: u64, + /// The size of the segment in the archive (potentially compressed). + pub archived_size: u64, +} + +/// Represents a single file entry within the XP3 archive. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct ArchiveItem { + pub name: String, + pub file_hash: u32, + pub original_size: u64, + pub archived_size: u64, + pub segments: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Xp3Entry { + pub name: String, + pub flags: u32, + pub file_hash: u32, + pub original_size: u64, + pub archived_size: u64, + pub segments: Vec, + pub extras: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct ExtraProp { + pub tag: [u8; 4], + pub data: Vec, +} + +/// Represents the entire XP3 archive +#[derive(Debug)] +#[allow(dead_code)] +pub struct Xp3Archive { + pub inner: Arc>>, + pub crypt: Arc>, + /// The offset which the archive file start. If the archive is embedded in another file (such as exe), this is the offset of the archive data within the larger file. + pub base_offset: u64, + /// The offset which index start. Releatived to whole file not just xp3 archive. + pub index_offset: u64, + /// Minor version + pub minor_version: u32, + pub entries: Vec, + pub extras: Vec, +} diff --git a/src/scripts/kirikiri/archive/xp3pack/consts.rs b/src/scripts/kirikiri/archive/xp3/consts.rs similarity index 86% rename from src/scripts/kirikiri/archive/xp3pack/consts.rs rename to src/scripts/kirikiri/archive/xp3/consts.rs index 3d87d03..bd8c333 100644 --- a/src/scripts/kirikiri/archive/xp3pack/consts.rs +++ b/src/scripts/kirikiri/archive/xp3/consts.rs @@ -20,3 +20,7 @@ pub const TVP_XP3_FILE_PROTECTED: u32 = 1 << 31; pub const TVP_XP3_SEGM_ENCODE_METHOD_MASK: u32 = 0x07; pub const TVP_XP3_SEGM_ENCODE_RAW: u32 = 0; pub const TVP_XP3_SEGM_ENCODE_ZLIB: u32 = 1; + +pub const TVP_XP3_CURRENT_HEADER_VERSION: u64 = 0x17; + +pub const ZSTD_SIGNATURE: &[u8; 4] = b"\x28\xb5\x2f\xfd"; diff --git a/src/scripts/kirikiri/archive/xp3/crypt.rs b/src/scripts/kirikiri/archive/xp3/crypt.rs new file mode 100644 index 0000000..91bbf78 --- /dev/null +++ b/src/scripts/kirikiri/archive/xp3/crypt.rs @@ -0,0 +1,34 @@ +use super::archive::*; +use crate::ext::io::*; +use crate::types::*; +use crate::utils::encoding::*; +use anyhow::Result; +use std::io::Read; + +pub trait Crypt: std::fmt::Debug { + /// Initializes the cryptographic context for the archive. + fn init(&self, _archive: &mut Xp3Archive) -> Result<()> { + Ok(()) + } + + /// Read a entry name from archive index + fn read_name<'a>(&self, reader: &mut Box) -> Result<(String, u64)> { + let name_length = reader.read_u16()?; + let name = reader.read_exact_vec(name_length as usize * 2)?; + Ok(( + decode_to_string(Encoding::Utf16LE, &name, true)?, + name_length as u64 * 2 + 2, + )) + } +} + +#[derive(Debug)] +pub struct NoCrypt {} + +impl NoCrypt { + pub fn new() -> Self { + Self {} + } +} + +impl Crypt for NoCrypt {} diff --git a/src/scripts/kirikiri/archive/xp3.rs b/src/scripts/kirikiri/archive/xp3/mod.rs similarity index 71% rename from src/scripts/kirikiri/archive/xp3.rs rename to src/scripts/kirikiri/archive/xp3/mod.rs index 074689c..3def6e5 100644 --- a/src/scripts/kirikiri/archive/xp3.rs +++ b/src/scripts/kirikiri/archive/xp3/mod.rs @@ -1,16 +1,24 @@ -use super::xp3pack::*; +mod archive; +#[allow(dead_code)] +mod consts; +mod crypt; +mod read; +mod reader; +mod segmenter; +mod writer; + use crate::ext::io::*; use crate::scripts::base::*; use crate::types::*; use anyhow::Result; +use consts::ZSTD_SIGNATURE; use flate2::read::ZlibDecoder; use overf::wrapping; -use std::io::{Read, Seek, SeekFrom, Take}; +pub use segmenter::SegmenterConfig; +use std::io::{Read, Seek, SeekFrom}; use std::sync::{Arc, Mutex}; -use xp3::XP3Reader; -use xp3::index::file::{IndexSegmentFlag, XP3FileIndex}; - -pub use super::xp3pack::SegmenterConfig; +use writer::Xp3ArchiveWriter; +use zstd::stream::read::Decoder as ZstdDecoder; pub fn parse_segmenter_config(str: &str) -> Result { let parts: Vec<&str> = str.split(':').collect(); @@ -143,41 +151,32 @@ impl ScriptBuilder for Xp3ArchiveBuilder { #[derive(Debug)] /// Kirikiri XP3 Archive -pub struct Xp3Archive { - reader: Arc>, - entries: Vec<(String, XP3FileIndex)>, +pub struct Xp3Archive { + archive: archive::Xp3Archive, decrypt_simple_crypt: bool, decompress_mdf: bool, } -impl Xp3Archive { - /// Create a new Kirikiri XP3 Archive - pub fn new(reader: T, config: &ExtraConfig) -> Result { - let xp3_reader = XP3Reader::open_archive(reader) - .map_err(|e| anyhow::anyhow!("Failed to open XP3 archive: {:?}", e))?; - let entries = xp3_reader - .entries() - .filter_map(|(i, d)| { - // Skip garbage files - if i.find("$$$ This is a protected archive. $$$").is_some() - || (i.to_lowercase().ends_with(".nene") && d.info().file_size() == 0) - { - None - } else { - Some((i.clone(), d.clone())) - } - }) - .collect(); +impl Xp3Archive { + pub fn new( + stream: T, + config: &ExtraConfig, + ) -> Result { + let mut archive = archive::Xp3Archive::new(stream, config)?; + archive.entries.retain(|entry| { + let i = &entry.name; + !(i.find("$$$ This is a protected archive. $$$").is_some() + || (i.to_lowercase().ends_with(".nene") && entry.original_size == 0)) + }); Ok(Self { - reader: Arc::new(Mutex::new(xp3_reader.close().1)), - entries, + archive, decrypt_simple_crypt: config.xp3_simple_crypt, decompress_mdf: config.xp3_mdf_decompress, }) } } -impl Script for Xp3Archive { +impl Script for Xp3Archive { fn default_output_script_type(&self) -> OutputScriptType { OutputScriptType::Json } @@ -194,23 +193,26 @@ impl Script for Xp3Archive { &'a self, ) -> Result> + 'a>> { Ok(Box::new( - self.entries.iter().map(|entry| Ok(entry.0.clone())), + self.archive + .entries + .iter() + .map(|entry| Ok(entry.name.clone())), )) } fn open_file<'a>(&'a self, index: usize) -> Result> { let index = self + .archive .entries .iter() .nth(index) .ok_or(anyhow::anyhow!("Index out of bounds: {}", index))? - .1 .clone(); - let mut entry = Entry::new(self.reader.clone(), index); + let mut entry = Entry::new(self.archive.inner.clone(), index); let mut header = [0u8; 16]; let header_len = entry.read(&mut header)?; entry.rewind()?; - entry.script_type = detect_script_type(entry.index.info().name(), &header, header_len); + entry.script_type = detect_script_type(&entry.index.name, &header, header_len); if self.decrypt_simple_crypt && header_len >= 5 && header[0] == 0xFE @@ -231,7 +233,7 @@ impl Script for Xp3Archive { if self.decompress_mdf && header_len >= 4 && &header[0..4] == b"mdf\0" - && entry.index.info().file_size() > 8 + && entry.index.original_size > 8 { let index = entry.index.clone(); return Ok(Box::new(MdfEntry::new(entry, index)?)); @@ -267,25 +269,36 @@ fn detect_script_type(filename: &str, buf: &[u8], buf_len: usize) -> Option { - reader: Arc>, - index: XP3FileIndex, - cache: Option>>>, +struct Entry { + reader: Arc>>, + index: archive::Xp3Entry, + cache: Option>, pos: u64, entries_pos: Vec, script_type: Option, } -impl Entry { - fn new(reader: Arc>, index: XP3FileIndex) -> Self { +impl std::fmt::Debug for Entry { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Entry") + .field("name", &self.index.name) + .field("flags", &self.index.flags) + .field("file_hash", &self.index.file_hash) + .field("original_size", &self.index.original_size) + .field("archived_size", &self.index.archived_size) + .finish() + } +} + +impl Entry { + fn new(reader: Arc>>, index: archive::Xp3Entry) -> Self { let mut pos = 0; let entries_pos = index - .segments() + .segments .iter() .map(|seg| { let p = pos; - pos += seg.original_size(); + pos += seg.original_size; p }) .collect(); @@ -300,9 +313,9 @@ impl Entry { } } -impl ArchiveContent for Entry { +impl ArchiveContent for Entry { fn name(&self) -> &str { - &self.index.info().name() + &self.index.name } fn to_data<'a>(&'a mut self) -> Result> { @@ -314,9 +327,9 @@ impl ArchiveContent for Entry { } } -impl Read for Entry { +impl Read for Entry { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { - if self.pos >= self.index.info().file_size() { + if self.pos >= self.index.original_size { self.cache.take(); return Ok(0); } @@ -338,50 +351,50 @@ impl Read for Entry { } } }; - let seg = &self.index.segments()[seg_index]; - let start_pos = seg.data_offset(); + let seg = &self.index.segments[seg_index]; + let start_pos = seg.start; let seg_pos = self.entries_pos[seg_index]; let skip_pos = self.pos - seg_pos; - let read_size = seg.saved_size(); - match seg.flag() { - IndexSegmentFlag::UnCompressed => { - let mut lock = MutexWrapper::new(self.reader.clone(), start_pos + skip_pos); - let readed = (&mut lock).take(read_size - skip_pos).read(buf)?; - self.pos += readed as u64; - Ok(readed) - } - IndexSegmentFlag::Compressed => { - let mut cache = ZlibDecoder::new( - MutexWrapper::new(self.reader.clone(), start_pos).take(read_size), - ); - if skip_pos != 0 { - let mut e = EmptyWriter::new(); - std::io::copy(&mut (&mut cache).take(skip_pos), &mut e)?; // skip - } - let readed = cache.read(buf)?; - self.pos += readed as u64; - self.cache = Some(cache); - Ok(readed) + let read_size = seg.archived_size; + if seg.is_compressed { + let mut inner = MutexWrapper::new(self.reader.clone(), start_pos).take(read_size); + let mut cache = if inner.peek_and_equal(ZSTD_SIGNATURE).is_ok() { + Box::new(ZstdDecoder::new(inner)?) as Box + } else { + Box::new(ZlibDecoder::new(inner)) as Box + }; + if skip_pos != 0 { + let mut e = EmptyWriter::new(); + std::io::copy(&mut (&mut cache).take(skip_pos), &mut e)?; // skip } + let readed = cache.read(buf)?; + self.pos += readed as u64; + self.cache = Some(cache); + Ok(readed) + } else { + let mut lock = MutexWrapper::new(self.reader.clone(), start_pos + skip_pos); + let readed = (&mut lock).take(read_size - skip_pos).read(buf)?; + self.pos += readed as u64; + Ok(readed) } } } -impl Seek for Entry { +impl Seek for Entry { fn seek(&mut self, pos: SeekFrom) -> std::io::Result { let new_pos = match pos { SeekFrom::Start(p) => p, SeekFrom::End(offset) => { if offset < 0 { - if (-offset) as u64 > self.index.info().file_size() { + if (-offset) as u64 > self.index.original_size { return Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, "Seek from end exceeds file length", )); } - self.index.info().file_size() - (-offset) as u64 + self.index.original_size - (-offset) as u64 } else { - self.index.info().file_size() + offset as u64 + self.index.original_size + offset as u64 } } SeekFrom::Current(offset) => { @@ -446,42 +459,42 @@ impl Seek for Entry { } } -struct SimpleCryptZlib { - inner: PrefixStream>>>, - index: XP3FileIndex, +struct SimpleCryptZlib { + inner: PrefixStream>>, + index: archive::Xp3Entry, } -impl SimpleCryptZlib { - fn new(mut entry: Entry, index: XP3FileIndex) -> Result { +impl SimpleCryptZlib { + fn new(mut entry: Entry, index: archive::Xp3Entry) -> Result { entry.seek(SeekFrom::Start(0x15))?; - let entry = StreamRegion::new(entry, 0x15, index.info().file_size())?; + let entry = StreamRegion::new(entry, 0x15, index.original_size)?; let inner = PrefixStream::new(vec![0xFF, 0xFE], ZlibDecoder::new(entry)); Ok(Self { inner, index }) } } -impl ArchiveContent for SimpleCryptZlib { +impl ArchiveContent for SimpleCryptZlib { fn name(&self) -> &str { - &self.index.info().name() + &self.index.name } } -impl Read for SimpleCryptZlib { +impl Read for SimpleCryptZlib { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.inner.read(buf) } } #[derive(Debug)] -struct SimpleCryptInner { - inner: StreamRegion>, +struct SimpleCryptInner { + inner: StreamRegion, crypt: u8, } -impl SimpleCryptInner { - fn new(mut entry: Entry, crypt: u8) -> Result { +impl SimpleCryptInner { + fn new(mut entry: Entry, crypt: u8) -> Result { entry.seek(SeekFrom::Start(5))?; - let size = entry.index.info().file_size(); + let size = entry.index.original_size; let entry = StreamRegion::new(entry, 5, size)?; Ok(Self { inner: entry, @@ -490,7 +503,7 @@ impl SimpleCryptInner { } } -impl Read for SimpleCryptInner { +impl Read for SimpleCryptInner { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { let readed = self.inner.read(buf)?; match self.crypt { @@ -515,7 +528,7 @@ impl Read for SimpleCryptInner { } } -impl Seek for SimpleCryptInner { +impl Seek for SimpleCryptInner { fn seek(&mut self, pos: SeekFrom) -> std::io::Result { self.inner.seek(pos) } @@ -530,21 +543,21 @@ impl Seek for SimpleCryptInner { } #[derive(Debug)] -struct SimpleCrypt { - inner: PrefixStream>, - index: XP3FileIndex, +struct SimpleCrypt { + inner: PrefixStream, + index: archive::Xp3Entry, } -impl SimpleCrypt { - fn new(entry: Entry, index: XP3FileIndex, crypt: u8) -> Result { +impl SimpleCrypt { + fn new(entry: Entry, index: archive::Xp3Entry, crypt: u8) -> Result { let inner = PrefixStream::new(vec![0xFF, 0xFE], SimpleCryptInner::new(entry, crypt)?); Ok(Self { inner, index }) } } -impl ArchiveContent for SimpleCrypt { +impl ArchiveContent for SimpleCrypt { fn name(&self) -> &str { - &self.index.info().name() + &self.index.name } fn to_data<'a>(&'a mut self) -> Result> { @@ -552,13 +565,13 @@ impl ArchiveContent for SimpleCrypt { } } -impl Read for SimpleCrypt { +impl Read for SimpleCrypt { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.inner.read(buf) } } -impl Seek for SimpleCrypt { +impl Seek for SimpleCrypt { fn seek(&mut self, pos: SeekFrom) -> std::io::Result { self.inner.seek(pos) } @@ -573,27 +586,27 @@ impl Seek for SimpleCrypt { } #[derive(Debug)] -struct MdfEntry { - inner: ZlibDecoder>>, - index: XP3FileIndex, +struct MdfEntry { + inner: ZlibDecoder>, + index: archive::Xp3Entry, } -impl MdfEntry { - fn new(mut entry: Entry, index: XP3FileIndex) -> Result { +impl MdfEntry { + fn new(mut entry: Entry, index: archive::Xp3Entry) -> Result { entry.seek(SeekFrom::Start(8))?; - let entry = StreamRegion::new(entry, 8, index.info().file_size())?; + let entry = StreamRegion::new(entry, 8, index.original_size)?; let inner = ZlibDecoder::new(entry); Ok(Self { inner, index }) } } -impl ArchiveContent for MdfEntry { +impl ArchiveContent for MdfEntry { fn name(&self) -> &str { - &self.index.info().name() + &self.index.name } } -impl Read for MdfEntry { +impl Read for MdfEntry { fn read(&mut self, buf: &mut [u8]) -> std::io::Result { self.inner.read(buf) } diff --git a/src/scripts/kirikiri/archive/xp3/read.rs b/src/scripts/kirikiri/archive/xp3/read.rs new file mode 100644 index 0000000..3fcdd4e --- /dev/null +++ b/src/scripts/kirikiri/archive/xp3/read.rs @@ -0,0 +1,181 @@ +use super::archive::*; +use super::consts::*; +use super::crypt::*; +use crate::ext::io::*; +use crate::types::*; +use anyhow::Result; +use std::io::{Read, Seek, SeekFrom}; +use std::sync::{Arc, Mutex}; + +impl Xp3Archive { + pub fn new( + stream: T, + _config: &ExtraConfig, + ) -> Result { + let crypt: Box = Box::new(NoCrypt::new()); + let crypt = Arc::new(crypt); + let mut stream = Box::new(stream); + let base_offset = 0; + if base_offset != 0 { + stream.seek(SeekFrom::Start(base_offset))?; + } + stream + .read_and_equal(XP3_MAGIC) + .map_err(|e| anyhow::anyhow!("Invalid xp3 signature: {}", e))?; + let mut index_offset = stream.read_u64()?; + let mut minor_version = 0; + if index_offset == TVP_XP3_CURRENT_HEADER_VERSION { + minor_version = stream.read_u32()?; + let sig = stream.read_u8()?; + if sig != TVP_XP3_INDEX_CONTINUE { + anyhow::bail!("Unsupported XP3 index format: {} is not continue flag", sig); + } + let index_offset_offset = stream.read_i64()?; + if index_offset_offset != 0 { + stream.seek_relative(index_offset_offset)?; + } + index_offset = stream.read_u64()?; + } + index_offset += base_offset; + stream.seek(SeekFrom::Start(index_offset))?; + let mut entries = Vec::new(); + let mut extras = Vec::new(); + { + let mut index_stream = Self::get_index_stream(&mut stream)?; + let mut sig = [0u8; 4]; + loop { + let readed = index_stream.read_most(&mut sig)?; + if readed == 0 { + break; + } + if readed < 4 { + anyhow::bail!("Invalid chunk signature in index"); + } + let mut size = index_stream.read_u64()?; + if &sig == CHUNK_FILE { + let mut name = None; + let mut flags = None; + let mut file_hash = None; + let mut original_size = None; + let mut archived_size = None; + let mut segments = Vec::new(); + let mut seg_offset = 0; + let mut entry_extras = Vec::new(); + while size > 0 { + if size < 12 { + anyhow::bail!("Invalid chunk size in index"); + } + let mut chunk_sig = [0u8; 4]; + index_stream.read_exact(&mut chunk_sig)?; + let mut chunk_size = index_stream.read_u64()?; + size -= 12; + if size < chunk_size { + anyhow::bail!("Invalid chunk size in index"); + } + size -= chunk_size; + if &chunk_sig == CHUNK_INFO { + if chunk_size < 20 { + anyhow::bail!("Invalid info chunk size in index"); + } + flags = Some(index_stream.read_u32()?); + original_size = Some(index_stream.read_u64()?); + archived_size = Some(index_stream.read_u64()?); + chunk_size -= 20; + let (n, s) = crypt.read_name(&mut index_stream)?; + name = Some(n); + chunk_size -= s; + } else if &chunk_sig == CHUNK_ADLR { + if chunk_size == 4 { + file_hash = Some(index_stream.read_u32()?); + chunk_size -= 4; + } + } else if &chunk_sig == CHUNK_SEGM { + while chunk_size > 0 { + if chunk_size < 0x1C { + anyhow::bail!("Invalid segm chunk size in index"); + } + let seg_flags = index_stream.read_u32()?; + let start = index_stream.read_u64()?; + let original_size = index_stream.read_u64()?; + let archived_size = index_stream.read_u64()?; + chunk_size -= 0x1C; + segments.push(Segment { + is_compressed: seg_flags != 0, + start, + offset_in_file: seg_offset, + original_size, + archived_size, + }); + seg_offset += original_size; + } + } else { + let data = index_stream.read_exact_vec(chunk_size as usize)?; + chunk_size = 0; + entry_extras.push(ExtraProp { + tag: chunk_sig, + data, + }); + } + if chunk_size > 0 { + index_stream.skip(chunk_size)?; + } + } + entries.push(Xp3Entry { + name: name + .ok_or_else(|| anyhow::anyhow!("Missing name chunk in file entry"))?, + flags: flags + .ok_or_else(|| anyhow::anyhow!("Missing flags chunk in file entry"))?, + file_hash: file_hash.unwrap_or(0), + original_size: original_size.ok_or_else(|| { + anyhow::anyhow!("Missing original size chunk in file entry") + })?, + archived_size: archived_size.ok_or_else(|| { + anyhow::anyhow!("Missing archived size chunk in file entry") + })?, + segments, + extras: entry_extras, + }); + } else { + let data = index_stream.read_exact_vec(size as usize)?; + extras.push(ExtraProp { tag: sig, data }); + } + } + } + let mut archive = Self { + inner: Arc::new(Mutex::new(stream)), + crypt: crypt.clone(), + base_offset, + index_offset, + minor_version, + entries, + extras, + }; + crypt.init(&mut archive)?; + Ok(archive) + } + + fn get_index_stream<'a, T: Read + Seek + std::fmt::Debug + 'static>( + stream: &'a mut Box, + ) -> Result> { + let index_type = stream.read_u8()?; + Ok(match index_type { + TVP_XP3_INDEX_ENCODE_RAW => { + let index_size = stream.read_u64()?; + Box::new(StreamRegion::with_size(stream, index_size)?) + } + TVP_XP3_INDEX_ENCODE_ZLIB => { + let packed_size = stream.read_u64()?; + let _original_size = stream.read_u64()?; + let mut compressed_data = StreamRegion::with_size(stream, packed_size)?; + if compressed_data.peek_and_equal(ZSTD_SIGNATURE).is_ok() { + Box::new(zstd::stream::read::Decoder::new(compressed_data)?) + } else { + Box::new(flate2::read::ZlibDecoder::new(compressed_data)) + } + } + _ => { + anyhow::bail!("Unsupported index type: {}", index_type); + } + }) + } +} diff --git a/src/scripts/kirikiri/archive/xp3pack/reader.rs b/src/scripts/kirikiri/archive/xp3/reader.rs similarity index 100% rename from src/scripts/kirikiri/archive/xp3pack/reader.rs rename to src/scripts/kirikiri/archive/xp3/reader.rs diff --git a/src/scripts/kirikiri/archive/xp3pack/segmenter.rs b/src/scripts/kirikiri/archive/xp3/segmenter.rs similarity index 100% rename from src/scripts/kirikiri/archive/xp3pack/segmenter.rs rename to src/scripts/kirikiri/archive/xp3/segmenter.rs diff --git a/src/scripts/kirikiri/archive/xp3pack/writer.rs b/src/scripts/kirikiri/archive/xp3/writer.rs similarity index 100% rename from src/scripts/kirikiri/archive/xp3pack/writer.rs rename to src/scripts/kirikiri/archive/xp3/writer.rs diff --git a/src/scripts/kirikiri/archive/xp3pack/archive.rs b/src/scripts/kirikiri/archive/xp3pack/archive.rs deleted file mode 100644 index 754a410..0000000 --- a/src/scripts/kirikiri/archive/xp3pack/archive.rs +++ /dev/null @@ -1,24 +0,0 @@ -/// Represents a single data segment for a file. -/// A file can be split into multiple segments, which can be compressed independently. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct Segment { - pub is_compressed: bool, - /// The offset of the segment's data within the archive file. - pub start: u64, - /// The offset of this segment within the original, uncompressed file. - pub offset_in_file: u64, - /// The size of the segment after decompression. - pub original_size: u64, - /// The size of the segment in the archive (potentially compressed). - pub archived_size: u64, -} - -/// Represents a single file entry within the XP3 archive. -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct ArchiveItem { - pub name: String, - pub file_hash: u32, - pub original_size: u64, - pub archived_size: u64, - pub segments: Vec, -} diff --git a/src/scripts/kirikiri/archive/xp3pack/mod.rs b/src/scripts/kirikiri/archive/xp3pack/mod.rs deleted file mode 100644 index 9731bdd..0000000 --- a/src/scripts/kirikiri/archive/xp3pack/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -mod archive; -#[allow(dead_code)] -mod consts; -mod reader; -mod segmenter; -mod writer; - -pub use segmenter::SegmenterConfig; -pub use writer::Xp3ArchiveWriter;