mirror of
https://github.com/lifegpc/msg-tool.git
synced 2026-06-09 14:28:53 +08:00
Add support to unpack Fate/stay night xp3 files
This commit is contained in:
@@ -38,6 +38,12 @@ pub struct Xp3Entry {
|
||||
pub extras: Vec<ExtraProp>,
|
||||
}
|
||||
|
||||
impl Xp3Entry {
|
||||
pub fn is_encrypted(&self) -> bool {
|
||||
self.flags != 0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct ExtraProp {
|
||||
pub tag: [u8; 4],
|
||||
|
||||
5
src/scripts/kirikiri/archive/xp3/crypt.json
Normal file
5
src/scripts/kirikiri/archive/xp3/crypt.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"Fate/stay night": {
|
||||
"$type": "FateCrypt"
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
use super::archive::*;
|
||||
use crate::ext::io::*;
|
||||
use crate::scripts::base::*;
|
||||
use crate::types::*;
|
||||
use crate::utils::encoding::*;
|
||||
use anyhow::Result;
|
||||
use std::io::Read;
|
||||
use serde::Deserialize;
|
||||
use std::collections::{BTreeMap, HashMap};
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
pub trait Crypt: std::fmt::Debug {
|
||||
/// Initializes the cryptographic context for the archive.
|
||||
@@ -20,6 +23,109 @@ pub trait Crypt: std::fmt::Debug {
|
||||
name_length as u64 * 2 + 2,
|
||||
))
|
||||
}
|
||||
|
||||
/// Decrypts the given stream of data for the specified entry and segment.
|
||||
fn decrypt<'a>(
|
||||
&self,
|
||||
_entry: &Xp3Entry,
|
||||
_cur_seg: &Segment,
|
||||
_stream: Box<dyn Read + 'a>,
|
||||
) -> Result<Box<dyn ReadDebug + 'a>> {
|
||||
Err(anyhow::anyhow!("This crypt does not support decrypt"))
|
||||
}
|
||||
|
||||
/// Decrypts the given stream of data for the specified entry and segment, with seek support.
|
||||
fn decrypt_with_seek<'a>(
|
||||
&self,
|
||||
_entry: &Xp3Entry,
|
||||
_cur_seg: &Segment,
|
||||
_stream: Box<dyn ReadSeek + 'a>,
|
||||
) -> Result<Box<dyn ReadSeek + 'a>> {
|
||||
Err(anyhow::anyhow!(
|
||||
"This crypt does not support decrypt with seek"
|
||||
))
|
||||
}
|
||||
|
||||
/// Returns true if this crypt support decrypt
|
||||
fn decrypt_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Returns true if this crypt support seek when decrypting
|
||||
fn decrypt_seek_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase", tag = "$type")]
|
||||
enum CryptType {
|
||||
NoCrypt,
|
||||
FateCrypt,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
pub struct Schema {
|
||||
#[serde(flatten)]
|
||||
crypt: CryptType,
|
||||
title: Option<String>,
|
||||
}
|
||||
|
||||
impl Schema {
|
||||
pub fn create_crypt(&self) -> Box<dyn Crypt> {
|
||||
match self.crypt {
|
||||
CryptType::NoCrypt => Box::new(NoCrypt::new()),
|
||||
CryptType::FateCrypt => Box::new(FateCrypt::new()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
include_flate::flate!(static CRYPT_DATA: str from "src/scripts/kirikiri/archive/xp3/crypt.json" with zstd);
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
static ref CRYPT_SCHEMA: BTreeMap<String, Schema> = {
|
||||
serde_json::from_str(&CRYPT_DATA).expect("Failed to parse crypt.json")
|
||||
};
|
||||
static ref ALIAS_TABLE: HashMap<String, String> = {
|
||||
let mut table = HashMap::new();
|
||||
for (game, fulltitle) in get_supported_games_with_title() {
|
||||
if let Some(title) = fulltitle {
|
||||
let mut alias_count = 0usize;
|
||||
for part in title.split("|") {
|
||||
let alias = part.trim();
|
||||
table.insert(alias.to_string(), game.to_string());
|
||||
alias_count += 1;
|
||||
}
|
||||
// also insert full title if there are multiple aliases
|
||||
if alias_count > 1 {
|
||||
table.insert(title.to_string(), game.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
table
|
||||
};
|
||||
}
|
||||
|
||||
/// Get the supported game titles for encrypted xp3 archives.
|
||||
pub fn get_supported_games() -> Vec<&'static str> {
|
||||
CRYPT_SCHEMA.keys().map(|s| s.as_str()).collect()
|
||||
}
|
||||
|
||||
/// Get the supported game titles for encrypted xp3 archives with their full titles.
|
||||
pub fn get_supported_games_with_title() -> Vec<(&'static str, Option<&'static str>)> {
|
||||
CRYPT_SCHEMA
|
||||
.iter()
|
||||
.map(|(k, v)| (k.as_str(), v.title.as_deref()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn query_crypt_schema(game: &str) -> Option<&'static Schema> {
|
||||
CRYPT_SCHEMA.get(game).or_else(|| {
|
||||
ALIAS_TABLE
|
||||
.get(game)
|
||||
.and_then(|real_game| CRYPT_SCHEMA.get(real_game))
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -32,3 +138,112 @@ impl NoCrypt {
|
||||
}
|
||||
|
||||
impl Crypt for NoCrypt {}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FateCrypt {}
|
||||
|
||||
impl FateCrypt {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl Crypt for FateCrypt {
|
||||
fn decrypt_supported(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn decrypt_seek_supported(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn decrypt<'a>(
|
||||
&self,
|
||||
_entry: &Xp3Entry,
|
||||
cur_seg: &Segment,
|
||||
stream: Box<dyn Read + 'a>,
|
||||
) -> Result<Box<dyn ReadDebug + 'a>> {
|
||||
Ok(Box::new(FateCryptReader::new(stream, cur_seg)))
|
||||
}
|
||||
|
||||
fn decrypt_with_seek<'a>(
|
||||
&self,
|
||||
_entry: &Xp3Entry,
|
||||
cur_seg: &Segment,
|
||||
stream: Box<dyn ReadSeek + 'a>,
|
||||
) -> Result<Box<dyn ReadSeek + 'a>> {
|
||||
Ok(Box::new(FateCryptReader::new(stream, cur_seg)))
|
||||
}
|
||||
}
|
||||
|
||||
struct FateCryptReader<R: Read> {
|
||||
inner: R,
|
||||
/// Start offset of the current xp3 entry.
|
||||
seg_start: u64,
|
||||
seg_size: u64,
|
||||
pos: u64,
|
||||
}
|
||||
|
||||
impl<T: Read> FateCryptReader<T> {
|
||||
pub fn new(inner: T, seg: &Segment) -> Self {
|
||||
Self {
|
||||
inner,
|
||||
seg_start: seg.offset_in_file,
|
||||
seg_size: seg.original_size,
|
||||
pos: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[automatically_derived]
|
||||
impl<T: Read> std::fmt::Debug for FateCryptReader<T> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("FateCryptReader")
|
||||
.field("seg_start", &self.seg_start)
|
||||
.field("seg_size", &self.seg_size)
|
||||
.field("pos", &self.pos)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<R: Read> Read for FateCryptReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
const XOR1_OFFSET: u64 = 0x13;
|
||||
const XOR3_OFFSET: u64 = 0x2ea29;
|
||||
let readed = self.inner.read(buf)?;
|
||||
for (i, t) in (&mut buf[..readed]).iter_mut().enumerate() {
|
||||
let tpos = self.seg_start + self.pos + i as u64;
|
||||
*t ^= 0x36;
|
||||
if tpos == XOR1_OFFSET {
|
||||
*t ^= 0x1;
|
||||
} else if tpos == XOR3_OFFSET {
|
||||
*t ^= 0x3;
|
||||
}
|
||||
}
|
||||
self.pos += readed as u64;
|
||||
Ok(readed)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Read + Seek> Seek for FateCryptReader<T> {
|
||||
fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
|
||||
let new_pos: i64 = match pos {
|
||||
SeekFrom::Start(offset) => offset as i64,
|
||||
SeekFrom::End(offset) => self.seg_size as i64 + offset,
|
||||
SeekFrom::Current(offset) => self.pos as i64 + offset,
|
||||
};
|
||||
let offset = new_pos - self.pos as i64;
|
||||
if offset != 0 {
|
||||
self.inner.seek(SeekFrom::Current(offset))?;
|
||||
self.pos = new_pos as u64;
|
||||
}
|
||||
Ok(self.pos)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_crypt() {
|
||||
for (key, schema) in CRYPT_SCHEMA.iter() {
|
||||
println!("Title: {}, Schema: {:?}", key, schema);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,9 @@ use crate::scripts::base::*;
|
||||
use crate::types::*;
|
||||
use anyhow::Result;
|
||||
use consts::ZSTD_SIGNATURE;
|
||||
use crypt::Crypt;
|
||||
pub use crypt::get_supported_games;
|
||||
pub use crypt::get_supported_games_with_title;
|
||||
use flate2::read::ZlibDecoder;
|
||||
use overf::wrapping;
|
||||
pub use segmenter::SegmenterConfig;
|
||||
@@ -166,6 +169,8 @@ impl Xp3Archive {
|
||||
archive.entries.retain(|entry| {
|
||||
let i = &entry.name;
|
||||
!(i.find("$$$ This is a protected archive. $$$").is_some()
|
||||
// Fate/stay night has spelling mistake. We also filter it.
|
||||
|| i.find("$$$ This is a protectet archive. $$$").is_some()
|
||||
|| (i.to_lowercase().ends_with(".nene") && entry.original_size == 0))
|
||||
});
|
||||
Ok(Self {
|
||||
@@ -208,7 +213,18 @@ impl Script for Xp3Archive {
|
||||
.nth(index)
|
||||
.ok_or(anyhow::anyhow!("Index out of bounds: {}", index))?
|
||||
.clone();
|
||||
let mut entry = Entry::new(self.archive.inner.clone(), index);
|
||||
let crypt = self.archive.crypt.clone();
|
||||
if index.is_encrypted() && !crypt.decrypt_supported() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"The archive is encrypted with a method that is not supported by the current crypt implementation. You may need to specify a game title by using --xp3-game-title <title>."
|
||||
));
|
||||
}
|
||||
let mut entry = Entry::new(
|
||||
self.archive.inner.clone(),
|
||||
index,
|
||||
self.archive.base_offset,
|
||||
crypt,
|
||||
);
|
||||
let mut header = [0u8; 16];
|
||||
let header_len = entry.read(&mut header)?;
|
||||
entry.rewind()?;
|
||||
@@ -272,26 +288,41 @@ fn detect_script_type(filename: &str, buf: &[u8], buf_len: usize) -> Option<Scri
|
||||
struct Entry {
|
||||
reader: Arc<Mutex<Box<dyn ReadSeek>>>,
|
||||
index: archive::Xp3Entry,
|
||||
crypt: Arc<Box<dyn Crypt>>,
|
||||
/// used to cache segment reader that can't seek. Such as decompressor reader or some decrypter reader.
|
||||
cache: Option<Box<dyn Read>>,
|
||||
/// used to store decrypted stream of current segment when the cryptor support seek when decrypting.
|
||||
crypt_stream: Option<Box<dyn ReadSeek>>,
|
||||
pos: u64,
|
||||
base_offset: u64,
|
||||
entries_pos: Vec<u64>,
|
||||
script_type: Option<ScriptType>,
|
||||
}
|
||||
|
||||
#[automatically_derived]
|
||||
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)
|
||||
.field("reader", &self.reader)
|
||||
.field("index", &self.index)
|
||||
.field("crypt", &self.crypt)
|
||||
.field("cache", &self.cache.is_some())
|
||||
.field("crypt_stream", &self.crypt_stream)
|
||||
.field("pos", &self.pos)
|
||||
.field("base_offset", &self.base_offset)
|
||||
.field("entries_pos", &self.entries_pos)
|
||||
.field("script_type", &self.script_type)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Entry {
|
||||
fn new(reader: Arc<Mutex<Box<dyn ReadSeek>>>, index: archive::Xp3Entry) -> Self {
|
||||
fn new(
|
||||
reader: Arc<Mutex<Box<dyn ReadSeek>>>,
|
||||
index: archive::Xp3Entry,
|
||||
base_offset: u64,
|
||||
crypt: Arc<Box<dyn Crypt>>,
|
||||
) -> Self {
|
||||
let mut pos = 0;
|
||||
let entries_pos = index
|
||||
.segments
|
||||
@@ -309,6 +340,9 @@ impl Entry {
|
||||
pos: 0,
|
||||
entries_pos,
|
||||
script_type: None,
|
||||
base_offset,
|
||||
crypt,
|
||||
crypt_stream: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -331,6 +365,7 @@ impl Read for Entry {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
if self.pos >= self.index.original_size {
|
||||
self.cache.take();
|
||||
self.crypt_stream.take();
|
||||
return Ok(0);
|
||||
}
|
||||
if let Some(cache) = self.cache.as_mut() {
|
||||
@@ -341,6 +376,14 @@ impl Read for Entry {
|
||||
}
|
||||
self.cache.take();
|
||||
}
|
||||
if let Some(crypt_stream) = self.crypt_stream.as_mut() {
|
||||
let readed = crypt_stream.read(buf)?;
|
||||
if readed > 0 {
|
||||
self.pos += readed as u64;
|
||||
return Ok(readed);
|
||||
}
|
||||
self.crypt_stream.take();
|
||||
}
|
||||
let seg_index = match self.entries_pos.binary_search(&self.pos) {
|
||||
Ok(i) => i,
|
||||
Err(i) => {
|
||||
@@ -352,10 +395,72 @@ impl Read for Entry {
|
||||
}
|
||||
};
|
||||
let seg = &self.index.segments[seg_index];
|
||||
let start_pos = seg.start;
|
||||
let start_pos = seg.start + self.base_offset;
|
||||
let seg_pos = self.entries_pos[seg_index];
|
||||
let skip_pos = self.pos - seg_pos;
|
||||
let read_size = seg.archived_size;
|
||||
if self.index.is_encrypted() {
|
||||
if seg.is_compressed || !self.crypt.decrypt_seek_supported() {
|
||||
let mut cache: Box<dyn Read> = if seg.is_compressed {
|
||||
let mut inner =
|
||||
MutexWrapper::new(self.reader.clone(), start_pos).take(read_size);
|
||||
let decompressed = if inner.peek_and_equal(ZSTD_SIGNATURE).is_ok() {
|
||||
Box::new(ZstdDecoder::new(inner)?) as Box<dyn Read>
|
||||
} else {
|
||||
Box::new(ZlibDecoder::new(inner)) as Box<dyn Read>
|
||||
};
|
||||
let decrypted =
|
||||
self.crypt
|
||||
.decrypt(&self.index, seg, decompressed)
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Decryption failed: {}", e),
|
||||
)
|
||||
})?;
|
||||
Box::new(decrypted) as Box<dyn Read>
|
||||
} else {
|
||||
let inner = MutexWrapper::new(self.reader.clone(), start_pos).take(read_size);
|
||||
let decrypted = self
|
||||
.crypt
|
||||
.decrypt(&self.index, seg, Box::new(inner))
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Decryption failed: {}", e),
|
||||
)
|
||||
})?;
|
||||
Box::new(decrypted) as Box<dyn Read>
|
||||
};
|
||||
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);
|
||||
return Ok(readed);
|
||||
} else {
|
||||
let inner = MutexWrapper::new(self.reader.clone(), start_pos).take(read_size);
|
||||
let mut decrypted = self
|
||||
.crypt
|
||||
.decrypt_with_seek(&self.index, seg, Box::new(inner))
|
||||
.map_err(|e| {
|
||||
std::io::Error::new(
|
||||
std::io::ErrorKind::Other,
|
||||
format!("Decryption failed: {}", e),
|
||||
)
|
||||
})?;
|
||||
if skip_pos != 0 {
|
||||
let mut e = EmptyWriter::new();
|
||||
std::io::copy(&mut (&mut decrypted).take(skip_pos), &mut e)?; // skip
|
||||
}
|
||||
let readed = decrypted.read(buf)?;
|
||||
self.pos += readed as u64;
|
||||
self.crypt_stream = Some(decrypted);
|
||||
return Ok(readed);
|
||||
}
|
||||
}
|
||||
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() {
|
||||
@@ -444,6 +549,34 @@ impl Seek for Entry {
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(crypt_stream) = self.crypt_stream.as_mut() {
|
||||
let old_seg_index = match self.entries_pos.binary_search(&self.pos) {
|
||||
Ok(i) => i,
|
||||
Err(i) => {
|
||||
if i == 0 {
|
||||
0
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
};
|
||||
let new_seg_index = match self.entries_pos.binary_search(&new_pos) {
|
||||
Ok(i) => i,
|
||||
Err(i) => {
|
||||
if i == 0 {
|
||||
0
|
||||
} else {
|
||||
i - 1
|
||||
}
|
||||
}
|
||||
};
|
||||
if old_seg_index != new_seg_index {
|
||||
self.crypt_stream.take();
|
||||
} else {
|
||||
let offset = new_pos as i64 - self.pos as i64;
|
||||
crypt_stream.seek(SeekFrom::Current(offset))?;
|
||||
}
|
||||
}
|
||||
self.pos = new_pos;
|
||||
Ok(self.pos)
|
||||
}
|
||||
@@ -451,6 +584,7 @@ impl Seek for Entry {
|
||||
fn rewind(&mut self) -> std::io::Result<()> {
|
||||
self.pos = 0;
|
||||
self.cache.take();
|
||||
self.crypt_stream.take();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -12,8 +12,15 @@ impl Xp3Archive {
|
||||
stream: T,
|
||||
_config: &ExtraConfig,
|
||||
) -> Result<Self> {
|
||||
let crypt: Box<dyn Crypt> = Box::new(NoCrypt::new());
|
||||
let crypt = Arc::new(crypt);
|
||||
let crypt: Box<dyn Crypt> = if let Some(game_title) = &_config.xp3_game_title {
|
||||
query_crypt_schema(game_title)
|
||||
.ok_or_else(|| {
|
||||
anyhow::anyhow!("Unsupported game title for XP3 archive: {}", game_title)
|
||||
})?
|
||||
.create_crypt()
|
||||
} else {
|
||||
Box::new(NoCrypt::new())
|
||||
};
|
||||
let mut stream = Box::new(stream);
|
||||
let base_offset = 0;
|
||||
if base_offset != 0 {
|
||||
@@ -141,6 +148,7 @@ impl Xp3Archive {
|
||||
}
|
||||
}
|
||||
}
|
||||
let crypt = Arc::new(crypt);
|
||||
let mut archive = Self {
|
||||
inner: Arc::new(Mutex::new(stream)),
|
||||
crypt: crypt.clone(),
|
||||
|
||||
Reference in New Issue
Block a user