Add RhapsodyCrypt

This commit is contained in:
2026-04-11 14:02:35 +08:00
parent 4f3110cef8
commit 072fbad6f9
9 changed files with 1778 additions and 11 deletions

View File

@@ -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"]

View File

@@ -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(())
}

View File

@@ -11,6 +11,7 @@ proc-macro = true
[features]
artemis-arc = []
kirikiri-arc = []
unstable = []
[dependencies]

View File

@@ -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)
}

View File

@@ -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();
}

View File

@@ -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

View File

@@ -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.

View File

File diff suppressed because it is too large Load Diff

View File

@@ -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() {