mirror of
https://github.com/lifegpc/msg-tool.git
synced 2026-06-18 00:45:08 +08:00
Add RhapsodyCrypt
This commit is contained in:
@@ -97,7 +97,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", "aes", "bytes", "cbc", "fastcdc", "flate2", "int-enum", "md5", "msg_tool_xp3data", "parse-size", "sha2", "utils-case-insensitive-string", "utils-serde-base64bytes", "utils-simple-pack", "zopfli", "zstd"]
|
||||
kirikiri-arc = ["kirikiri", "adler", "aes", "bytes", "cbc", "fastcdc", "flate2", "int-enum", "md5", "msg_tool_macro/kirikiri-arc", "msg_tool_xp3data", "parse-size", "sha2", "utils-case-insensitive-string", "utils-serde-base64bytes", "utils-simple-pack", "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"]
|
||||
|
||||
@@ -65,3 +65,43 @@ pub fn gen_cx_cb<P: AsRef<Path> + ?Sized, D: AsRef<Path> + ?Sized>(
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Pack all binary files in name_list into a single archive.
|
||||
pub fn gen_name_list<P: AsRef<Path> + ?Sized, D: AsRef<Path> + ?Sized>(
|
||||
json_path: &P,
|
||||
outdir: &D,
|
||||
level: i32,
|
||||
) -> std::io::Result<()> {
|
||||
let p = json_path.as_ref();
|
||||
let pb = p
|
||||
.parent()
|
||||
.unwrap_or_else(|| Path::new(""))
|
||||
.join("name_list");
|
||||
let json_data = std::fs::read_to_string(p)?;
|
||||
let json = json::parse(&json_data)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
|
||||
let mut pack = SimplePack::new(&outdir.as_ref().join("name_list.pck"))?;
|
||||
let mut seen_files = HashSet::new();
|
||||
for (_, obj) in json.entries() {
|
||||
if let Some(name) = obj["FileListName"].as_str() {
|
||||
if seen_files.contains(name) {
|
||||
continue;
|
||||
}
|
||||
seen_files.insert(name.to_string());
|
||||
let file_path = pb.join(name);
|
||||
if !file_path.exists() {
|
||||
return Err(std::io::Error::new(
|
||||
std::io::ErrorKind::NotFound,
|
||||
format!("File not found: {}", file_path.display()),
|
||||
));
|
||||
}
|
||||
let file = std::fs::File::open(file_path)?;
|
||||
let file = std::io::BufReader::new(file);
|
||||
pack.add_file(name, file)?;
|
||||
}
|
||||
}
|
||||
if level >= 0 && level <= 22 {
|
||||
pack.compress(level)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ proc-macro = true
|
||||
|
||||
[features]
|
||||
artemis-arc = []
|
||||
kirikiri-arc = []
|
||||
unstable = []
|
||||
|
||||
[dependencies]
|
||||
|
||||
@@ -62,7 +62,10 @@ pub fn struct_unpack_impl_for_num(item: TokenStream) -> TokenStream {
|
||||
}
|
||||
|
||||
fn has_skip_fmt_attr(field: &syn::Field) -> bool {
|
||||
field.attrs.iter().any(|attr| attr.path().is_ident("skip_fmt"))
|
||||
field
|
||||
.attrs
|
||||
.iter()
|
||||
.any(|attr| attr.path().is_ident("skip_fmt"))
|
||||
}
|
||||
|
||||
#[proc_macro_derive(MyDebug, attributes(skip_fmt))]
|
||||
@@ -87,7 +90,11 @@ pub fn debug_macro_derive(input: TokenStream) -> TokenStream {
|
||||
let mut generics = generics;
|
||||
{
|
||||
let where_clause = generics.make_where_clause();
|
||||
for field in data_struct.fields.iter().filter(|field| !has_skip_fmt_attr(field)) {
|
||||
for field in data_struct
|
||||
.fields
|
||||
.iter()
|
||||
.filter(|field| !has_skip_fmt_attr(field))
|
||||
{
|
||||
let ty = &field.ty;
|
||||
where_clause
|
||||
.predicates
|
||||
@@ -959,3 +966,17 @@ pub fn default_macro_derive(input: TokenStream) -> TokenStream {
|
||||
}
|
||||
.into()
|
||||
}
|
||||
|
||||
#[cfg(feature = "kirikiri-arc")]
|
||||
#[proc_macro]
|
||||
pub fn rhapsody_crypt_const_name_hash(input: TokenStream) -> TokenStream {
|
||||
let input = syn::parse_macro_input!(input as syn::LitStr);
|
||||
let s = input.value();
|
||||
let mut expanded = quote::quote! { 0u32 };
|
||||
for c in s.chars() {
|
||||
expanded = quote::quote! {
|
||||
RhapsodyCrypt::update_name_hash(#expanded, #c)
|
||||
};
|
||||
}
|
||||
TokenStream::from(expanded)
|
||||
}
|
||||
|
||||
@@ -9,15 +9,20 @@ fn main() {
|
||||
"cargo:rerun-if-changed={}",
|
||||
source_dir.join("cx_cb").display()
|
||||
);
|
||||
let level = level
|
||||
let arc_level = level
|
||||
.parse::<i32>()
|
||||
.expect("MSG_TOOL_KIRIKIRI_ARC_GEN_LEVEL must be a valid integer");
|
||||
println!("cargo:rerun-if-env-changed=MSG_TOOL_KIRIKIRI_ARC_GEN_LEVEL");
|
||||
msg_tool_build::kr_arc::gen_cx_cb(&crypt_json_path, &outdir, level).unwrap();
|
||||
msg_tool_build::kr_arc::gen_cx_cb(&crypt_json_path, &outdir, arc_level).unwrap();
|
||||
let level = std::env::var("MSG_TOOL_KIRIKIRI_CRYPT_COMPRESS_LEVEL").unwrap_or("22".to_string());
|
||||
let level = level
|
||||
.parse::<i32>()
|
||||
.expect("MSG_TOOL_KIRIKIRI_CRYPT_COMPRESS_LEVEL must be a valid integer");
|
||||
println!("cargo:rerun-if-env-changed=MSG_TOOL_KIRIKIRI_CRYPT_COMPRESS_LEVEL");
|
||||
msg_tool_build::kr_arc::gen_crypt(&crypt_json_path, &outdir, level).unwrap();
|
||||
println!(
|
||||
"cargo:rerun-if-changed={}",
|
||||
source_dir.join("name_list").display()
|
||||
);
|
||||
msg_tool_build::kr_arc::gen_name_list(&crypt_json_path, &outdir, arc_level).unwrap();
|
||||
}
|
||||
|
||||
@@ -477,6 +477,11 @@
|
||||
"EvenBranchOrder": "AAECAwQFBgc=",
|
||||
"ControlBlockName": "fate_hollow.bin"
|
||||
},
|
||||
"Fate/Knight Rhapsody ACT 2": {
|
||||
"$type": "RhapsodyCrypt",
|
||||
"FileListName": "rhapsody.lst",
|
||||
"Title": "招蕩的妄想劇場 ACT 2"
|
||||
},
|
||||
"Fate/stay night": {
|
||||
"$type": "FateCrypt",
|
||||
"HashAfterCrypt": true
|
||||
|
||||
@@ -3,6 +3,8 @@ use std::io::Read;
|
||||
|
||||
/// Control Block data for CxEncryption packed with SimplePack.
|
||||
pub const CX_CB_DATA: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/cx_cb.pck"));
|
||||
/// Name list data packed with SimplePack.
|
||||
pub const NAME_LIST_DATA: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/name_list.pck"));
|
||||
const CRYPT_DATA: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/crypt.json.zst"));
|
||||
|
||||
/// Get the crypt.json data as a string.
|
||||
|
||||
1548
msg_tool_xp3data/name_list/rhapsody.lst
Normal file
1548
msg_tool_xp3data/name_list/rhapsody.lst
Normal file
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,15 @@ pub fn default_init_crypt(archive: &mut Xp3Archive) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn default_read_name<'a>(reader: &mut Box<dyn Read + 'a>) -> 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,
|
||||
))
|
||||
}
|
||||
|
||||
pub trait Crypt: std::fmt::Debug {
|
||||
#[allow(dead_code)]
|
||||
/// whether Adler32 checksum should be calculated after contents have been encrypted.
|
||||
@@ -64,12 +73,7 @@ pub trait Crypt: std::fmt::Debug {
|
||||
|
||||
/// Read a entry name from archive index
|
||||
fn read_name<'a>(&self, reader: &mut Box<dyn Read + 'a>) -> 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,
|
||||
))
|
||||
default_read_name(reader)
|
||||
}
|
||||
|
||||
/// Decrypts the given stream of data for the specified entry and segment.
|
||||
@@ -223,6 +227,10 @@ enum CryptType {
|
||||
hash_table: Vec<u32>,
|
||||
key_table: Base64Bytes,
|
||||
},
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
RhapsodyCrypt {
|
||||
file_list_name: String,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
@@ -346,6 +354,9 @@ impl Schema {
|
||||
hash_table.clone(),
|
||||
key_table.bytes.clone(),
|
||||
)?),
|
||||
CryptType::RhapsodyCrypt { file_list_name } => {
|
||||
Box::new(RhapsodyCrypt::new(self.base.clone(), &file_list_name)?)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -388,6 +399,19 @@ lazy_static::lazy_static! {
|
||||
};
|
||||
}
|
||||
|
||||
pub fn query_filename_list(name: &str) -> Result<String> {
|
||||
let reader = MemReaderRef::new(NAME_LIST_DATA);
|
||||
let mut pack = read_simple_pack(reader)?;
|
||||
while let Some(mut entry) = pack.next()? {
|
||||
if entry.name == name {
|
||||
let mut str = String::new();
|
||||
entry.read_to_string(&mut str)?;
|
||||
return Ok(str);
|
||||
}
|
||||
}
|
||||
Err(anyhow::anyhow!("Name list entry not found: {}", name))
|
||||
}
|
||||
|
||||
/// 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()
|
||||
@@ -1512,6 +1536,127 @@ impl<R: Read> Read for PuCaCryptReader2<R> {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RhapsodyCrypt {
|
||||
base: BaseSchema,
|
||||
names: HashMap<u32, String>,
|
||||
}
|
||||
|
||||
impl RhapsodyCrypt {
|
||||
pub fn new(base: BaseSchema, file_list_name: &str) -> Result<Self> {
|
||||
let file_list = query_filename_list(file_list_name)?;
|
||||
let mut names = HashMap::new();
|
||||
for name in file_list.lines() {
|
||||
let name = name.trim();
|
||||
if !name.is_empty() {
|
||||
names.insert(Self::get_name_hash(name.chars()), name.to_string());
|
||||
}
|
||||
}
|
||||
Ok(Self { base, names })
|
||||
}
|
||||
fn get_name_hash<T: Iterator<Item = char>>(name: T) -> u32 {
|
||||
let mut hash = 0;
|
||||
for c in name {
|
||||
hash = Self::update_name_hash(hash, c);
|
||||
}
|
||||
hash
|
||||
}
|
||||
const fn update_name_hash(hash: u32, c: char) -> u32 {
|
||||
let c = c.to_ascii_lowercase() as u32;
|
||||
let mut hash = w!(0x1000193u32 * hash ^ (c & 0xFF));
|
||||
hash = w!(0x1000193u32 * hash ^ ((c >> 8) & 0xFF));
|
||||
hash
|
||||
}
|
||||
fn get_key(&self, hash: u32) -> [u8; 12] {
|
||||
let mut key = [0u8; 12];
|
||||
key[0..4].copy_from_slice(&hash.to_le_bytes());
|
||||
key[4..8].copy_from_slice(&(0x6E1DA9B2u32).to_le_bytes());
|
||||
key[8..12].copy_from_slice(&(0x0040C800u32).to_le_bytes());
|
||||
key
|
||||
}
|
||||
}
|
||||
|
||||
impl Crypt for RhapsodyCrypt {
|
||||
base_schema_impl!();
|
||||
fn read_name<'a>(&self, reader: &mut Box<dyn Read + 'a>) -> Result<(String, u64)> {
|
||||
use msg_tool_macro::rhapsody_crypt_const_name_hash as hash;
|
||||
const PNG_HASH: u32 = hash!(".png");
|
||||
const MAP_HASH: u32 = hash!(".map");
|
||||
const ASD_HASH: u32 = hash!(".asd");
|
||||
const TJS_HASH: u32 = hash!(".tjs");
|
||||
const TXT_HASH: u32 = hash!(".txt");
|
||||
const KS_HASH: u32 = hash!(".ks");
|
||||
const WAV_HASH: u32 = hash!(".wav");
|
||||
const JPG_HASH: u32 = hash!(".jpg");
|
||||
const OGG_HASH: u32 = hash!(".ogg");
|
||||
let key = reader.read_u32()?;
|
||||
let name_hash = reader.read_u32()? ^ key;
|
||||
if let Some(name) = self.names.get(&name_hash) {
|
||||
return Ok((name.clone(), 8));
|
||||
}
|
||||
let ext_hash = reader.read_u32()? ^ key;
|
||||
let mut name = format!("{:08X}", name_hash);
|
||||
match ext_hash {
|
||||
PNG_HASH => name += ".png",
|
||||
MAP_HASH => name += ".map",
|
||||
ASD_HASH => name += ".asd",
|
||||
TJS_HASH => name += ".tjs",
|
||||
TXT_HASH => name += ".txt",
|
||||
KS_HASH => name += ".ks",
|
||||
WAV_HASH => name += ".wav",
|
||||
JPG_HASH => name += ".jpg",
|
||||
OGG_HASH => name += ".ogg",
|
||||
_ => name += format!(".{:08X}", ext_hash).as_str(),
|
||||
};
|
||||
Ok((name, 12))
|
||||
}
|
||||
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(RhapsodyCryptReader::new(
|
||||
stream,
|
||||
cur_seg,
|
||||
self.get_key(entry.file_hash),
|
||||
)))
|
||||
}
|
||||
fn decrypt_with_seek<'a>(
|
||||
&self,
|
||||
entry: &Xp3Entry,
|
||||
cur_seg: &Segment,
|
||||
stream: Box<dyn ReadSeek + 'a>,
|
||||
) -> Result<Box<dyn ReadSeek + 'a>> {
|
||||
Ok(Box::new(RhapsodyCryptReader::new(
|
||||
stream,
|
||||
cur_seg,
|
||||
self.get_key(entry.file_hash),
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
seek_reader_key_impl!(RhapsodyCryptReader<T>, [u8; 12]);
|
||||
|
||||
impl<R: Read> Read for RhapsodyCryptReader<R> {
|
||||
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
|
||||
let readed = self.inner.read(buf)?;
|
||||
let mut offset = ((self.seg_start + self.pos) % 12) as usize;
|
||||
for t in (&mut buf[..readed]).iter_mut() {
|
||||
*t ^= self.key[offset];
|
||||
offset = (offset + 1) % 12;
|
||||
}
|
||||
self.pos += readed as u64;
|
||||
Ok(readed)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_crypt() {
|
||||
for (key, schema) in CRYPT_SCHEMA.iter() {
|
||||
|
||||
Reference in New Issue
Block a user