Add krkr xp3 archive support

This commit is contained in:
2025-10-10 00:23:00 +08:00
parent f8a487071b
commit f12a66d3d4
9 changed files with 342 additions and 2 deletions

13
Cargo.lock generated
View File

@@ -1366,6 +1366,7 @@ dependencies = [
"webp",
"windows-sys 0.61.2",
"xml5ever",
"xp3",
"zstd",
]
@@ -2534,6 +2535,18 @@ 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.0"

View File

@@ -47,6 +47,7 @@ url = { version = "2.5", optional = true }
utf16string = "0.2"
webp = { version = "0.3", default-features = false, optional = true }
xml5ever = { version = "0.35", optional = true }
xp3 = { version = "0.3", optional = true}
zstd = { version = "0.13", optional = true }
[features]
@@ -54,7 +55,7 @@ default = ["all-fmt", "image-jpg", "image-jxl", "image-webp", "audio-flac", "jie
all-fmt = ["all-script", "all-img", "all-arc", "all-audio"]
all-script = ["artemis", "artemis-panmimisoft", "bgi", "cat-system", "circus", "entis-gls", "escude", "ex-hibit", "favorite", "hexen-haus", "kirikiri", "silky", "softpal", "will-plus", "yaneurao", "yaneurao-itufuru"]
all-img = ["bgi-img", "cat-system-img", "circus-img", "emote-img", "hexen-haus-img", "kirikiri-img", "softpal-img", "will-plus-img"]
all-arc = ["artemis-arc", "bgi-arc", "cat-system-arc", "circus-arc", "escude-arc", "ex-hibit-arc", "hexen-haus-arc", "softpal-arc"]
all-arc = ["artemis-arc", "bgi-arc", "cat-system-arc", "circus-arc", "escude-arc", "ex-hibit-arc", "hexen-haus-arc", "kirikiri-arc", "softpal-arc"]
all-audio = ["bgi-audio", "circus-audio"]
artemis = ["stylua", "utils-escape"]
artemis-panmimisoft = ["artemis", "rust-ini"]
@@ -81,6 +82,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", "xp3"]
kirikiri-img = ["kirikiri", "image", "libtlg-rs"]
silky = []
softpal = ["int-enum"]

View File

@@ -187,6 +187,10 @@ msg-tool create -t <script-type> <input> <output>
| `kirikiri-tjs-ns0`/`kr-tjs-ns0` | `kirikiri` | Kirikiri TJS NS0 binary encoded script | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | |
| `kirikiri-tjs2`/`kr-tjs2` | `kirikiri` | Kirikiri compiled TJS2 script | ✔️ | ✔️ | ❌ | ❌ | ✔️ | ✔️ | ❌ | |
| Archive Type | Feature Name | Name | Unpack | Pack | Remarks |
|---|---|---|---|---|---|
| `kirikiri-xp3`/`kr-xp3`/`xp3` | `kirikiri-arc` | Kirikiri XP3 Archive File (.xp3) | ✔️ | ❌ | |
| Image Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Create | Remarks |
|---|---|---|---|---|---|---|---|---|
| `kirikiri-tlg`/`kr-tlg` | `kirikiri-img` | Kirikiri TLG Image File (.tlg) | ✔️ | ✔️ | ❌ | ❌ | ✔️ | tlg6 is not supported when importing/creating image |

View File

@@ -4,7 +4,7 @@ use crate::utils::encoding::decode_to_string;
use crate::utils::struct_pack::{StructPack, StructUnpack};
use std::ffi::CString;
use std::io::*;
use std::sync::Mutex;
use std::sync::{Arc, Mutex};
/// A trait to help to peek data from a reader.
pub trait Peek {
@@ -1999,3 +1999,82 @@ impl<R: Read + Seek, W: Write + Seek, A: Fn(u64) -> Result<u64>, O: Fn(u64) -> R
Ok(())
}
}
/// A thread-safe wrapper around a Mutex-protected writer/reader.
#[derive(Clone)]
pub struct MutexWrapper<T> {
inner: Arc<Mutex<T>>,
pos: u64,
}
impl<T> MutexWrapper<T> {
/// Creates a new `MutexWrapper` with the given inner value.
pub fn new(inner: Arc<Mutex<T>>, pos: u64) -> Self {
MutexWrapper { inner, pos }
}
}
impl<T: Read + Seek> Read for MutexWrapper<T> {
fn read(&mut self, buf: &mut [u8]) -> Result<usize> {
let mut lock = self.inner.lock().map_err(|_| {
std::io::Error::new(std::io::ErrorKind::Other, "Failed to lock the mutex")
})?;
lock.seek(SeekFrom::Start(self.pos))?;
let readed = lock.read(buf)?;
self.pos += readed as u64;
Ok(readed)
}
}
impl<T: Read + Seek> Seek for MutexWrapper<T> {
fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
let mut lock = self.inner.lock().map_err(|_| {
std::io::Error::new(std::io::ErrorKind::Other, "Failed to lock the mutex")
})?;
let new_pos = match pos {
SeekFrom::Start(offset) => offset,
SeekFrom::End(offset) => {
let len = lock.stream_length()?;
(len as i64 + offset as i64) as u64
}
SeekFrom::Current(offset) => (self.pos as i64 + offset as i64) as u64,
};
if new_pos > lock.stream_length()? {
return Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"Seek position is beyond the end of the stream",
));
}
self.pos = new_pos;
Ok(self.pos)
}
fn stream_position(&mut self) -> Result<u64> {
Ok(self.pos)
}
fn rewind(&mut self) -> Result<()> {
self.pos = 0;
Ok(())
}
}
/// A writer that does nothing and always succeeds.
pub struct EmptyWriter;
impl EmptyWriter {
/// Creates a new `EmptyWriter`.
pub fn new() -> Self {
Self {}
}
}
impl Write for EmptyWriter {
fn write(&mut self, buf: &[u8]) -> Result<usize> {
Ok(buf.len())
}
fn flush(&mut self) -> Result<()> {
Ok(())
}
}

View File

@@ -0,0 +1 @@
pub mod xp3;

View File

@@ -0,0 +1,233 @@
use crate::ext::io::*;
use crate::scripts::base::*;
use crate::types::*;
use anyhow::Result;
use flate2::read::ZlibDecoder;
use std::io::{Read, Seek, Take};
use std::sync::{Arc, Mutex};
use xp3::XP3Reader;
use xp3::index::file::{IndexSegmentFlag, XP3FileIndex};
#[derive(Debug)]
/// Builder for Kirikiri XP3 Archive
pub struct Xp3ArchiveBuilder {}
impl Xp3ArchiveBuilder {
/// Create a new Kirikiri XP3 Archive Builder
pub fn new() -> Self {
Self {}
}
}
impl ScriptBuilder for Xp3ArchiveBuilder {
fn default_encoding(&self) -> Encoding {
Encoding::Utf8
}
fn default_archive_encoding(&self) -> Option<Encoding> {
Some(Encoding::Utf8)
}
fn build_script(
&self,
buf: Vec<u8>,
_filename: &str,
_encoding: Encoding,
_archive_encoding: Encoding,
config: &ExtraConfig,
_archive: Option<&Box<dyn Script>>,
) -> Result<Box<dyn Script>> {
Ok(Box::new(Xp3Archive::new(MemReader::new(buf), config)?))
}
fn build_script_from_file(
&self,
filename: &str,
_encoding: Encoding,
_archive_encoding: Encoding,
config: &ExtraConfig,
_archive: Option<&Box<dyn Script>>,
) -> Result<Box<dyn Script>> {
let file = std::fs::File::open(filename)?;
Ok(Box::new(Xp3Archive::new(file, config)?))
}
fn build_script_from_reader(
&self,
reader: Box<dyn ReadSeek>,
_filename: &str,
_encoding: Encoding,
_archive_encoding: Encoding,
config: &ExtraConfig,
_archive: Option<&Box<dyn Script>>,
) -> Result<Box<dyn Script>> {
Ok(Box::new(Xp3Archive::new(reader, config)?))
}
fn extensions(&self) -> &'static [&'static str] {
&["xp3"]
}
fn script_type(&self) -> &'static ScriptType {
&ScriptType::KirikiriXp3
}
fn is_archive(&self) -> bool {
true
}
}
#[derive(Debug)]
/// Kirikiri XP3 Archive
pub struct Xp3Archive<T: Read + Seek + std::fmt::Debug> {
reader: Arc<Mutex<T>>,
entries: Vec<(String, XP3FileIndex)>,
}
impl<T: Read + Seek + std::fmt::Debug> Xp3Archive<T> {
/// Create a new Kirikiri XP3 Archive
pub fn new(reader: T, _config: &ExtraConfig) -> Result<Self> {
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();
Ok(Self {
reader: Arc::new(Mutex::new(xp3_reader.close().1)),
entries,
})
}
}
impl<T: Read + Seek + std::fmt::Debug + 'static> Script for Xp3Archive<T> {
fn default_output_script_type(&self) -> OutputScriptType {
OutputScriptType::Json
}
fn default_format_type(&self) -> FormatOptions {
FormatOptions::None
}
fn is_archive(&self) -> bool {
true
}
fn iter_archive_filename<'a>(
&'a self,
) -> Result<Box<dyn Iterator<Item = Result<String>> + 'a>> {
Ok(Box::new(
self.entries.iter().map(|entry| Ok(entry.0.clone())),
))
}
fn open_file<'a>(&'a self, index: usize) -> Result<Box<dyn ArchiveContent + 'a>> {
let index = self
.entries
.iter()
.nth(index)
.ok_or(anyhow::anyhow!("Index out of bounds: {}", index))?
.1
.clone();
let entry = Entry::new(self.reader.clone(), index);
Ok(Box::new(entry))
}
}
struct Entry<T: Read + Seek> {
reader: Arc<Mutex<T>>,
index: XP3FileIndex,
cache: Option<ZlibDecoder<Take<MutexWrapper<T>>>>,
pos: u64,
entries_pos: Vec<u64>,
}
impl<T: Read + Seek> Entry<T> {
fn new(reader: Arc<Mutex<T>>, index: XP3FileIndex) -> Self {
let mut pos = 0;
let entries_pos = index
.segments()
.iter()
.map(|seg| {
let p = pos;
pos += seg.original_size();
p
})
.collect();
Self {
reader,
index,
cache: None,
pos: 0,
entries_pos,
}
}
}
impl<T: Read + Seek> ArchiveContent for Entry<T> {
fn name(&self) -> &str {
&self.index.info().name()
}
}
impl<T: Read + Seek> Read for Entry<T> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
if self.pos >= self.index.info().file_size() {
self.cache.take();
return Ok(0);
}
if let Some(cache) = self.cache.as_mut() {
let readed = cache.read(buf)?;
if readed > 0 {
self.pos += readed as u64;
return Ok(readed);
}
self.cache.take();
}
let seg_index = match self.entries_pos.binary_search(&self.pos) {
Ok(i) => i,
Err(i) => {
if i == 0 {
0
} else {
i - 1
}
}
};
let seg = &self.index.segments()[seg_index];
let start_pos = seg.data_offset();
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)
}
}
}
}

View File

@@ -1,4 +1,6 @@
//! Kirikiri Scripts
#[cfg(feature = "kirikiri-arc")]
pub mod archive;
#[cfg(feature = "kirikiri-img")]
pub mod image;
pub mod ks;

View File

@@ -154,6 +154,8 @@ lazy_static::lazy_static! {
Box::new(will_plus::img::wip::WillPlusWipImageBuilder::new()),
#[cfg(feature = "artemis")]
Box::new(artemis::txt::ArtemisTxtBuilder::new()),
#[cfg(feature = "kirikiri-arc")]
Box::new(kirikiri::archive::xp3::Xp3ArchiveBuilder::new()),
];
/// A list of all script extensions.
pub static ref ALL_EXTS: Vec<String> =

View File

@@ -626,6 +626,10 @@ pub enum ScriptType {
#[value(alias = "kr", alias = "kr-ks", alias = "kirikiri-ks")]
/// Kirikiri script
Kirikiri,
#[cfg(feature = "kirikiri-arc")]
#[value(alias = "kr-xp3", alias = "xp3")]
/// Kirikiri XP3 archive
KirikiriXp3,
#[cfg(feature = "kirikiri-img")]
#[value(alias("kr-tlg"))]
/// Kirikiri TLG image