diff --git a/Cargo.toml b/Cargo.toml index 1013a10..466b7b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -98,7 +98,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_macro/kirikiri-arc", "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-lzss", "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"] @@ -130,6 +130,7 @@ utils-blowfish = ["byteorder"] utils-case-insensitive-string = [] utils-crc32 = [] utils-escape = ["fancy-regex"] +utils-lzss = [] utils-mmx = [] utils-pcm = [] utils-psd = ["image", "flate2", "utils-bit-stream"] diff --git a/msg_tool_xp3data/crypt.json b/msg_tool_xp3data/crypt.json index 3c05aef..663219d 100644 --- a/msg_tool_xp3data/crypt.json +++ b/msg_tool_xp3data/crypt.json @@ -1280,6 +1280,10 @@ "ControlBlockName": "loca_love.bin", "Title": "ろけらぶ - 同棲×後輩 | 恋爱定位Location Love同居×后辈" }, + "L.O.V.E ~Kokuhaku~": { + "$type": "ChainReactionCrypt", + "Title": "L.O.V.E. ~告白~" + }, "Love Love Boin series": { "$type": "FlyingShineCrypt", "Title": "らぶらぶボイン" @@ -1466,6 +1470,10 @@ "$type": "HashCrypt", "Title": "ナマイキJKに復讐の性活指導 ~先生、お願いだからもう許して…~" }, + "Nakadashi Hara Maid series": { + "$type": "ChainReactionCrypt", + "Title": "なかだし孕メイド | なかだし孕メイド2" + }, "Nantai Lesson": { "$type": "HashCrypt", "Title": "軟体レッスン ~いいなり彼女とひみつの放課後~" @@ -1572,6 +1580,10 @@ "$type": "HashCrypt", "Title": "堕ちてゆく聖職淑女~いけない、夫に無理矢理やらされていただけなのに感じちゃう~" }, + "Oishii Mahou no Tonaekata": { + "$type": "ChainReactionCrypt", + "Title": "おいしい魔法のとなえかた。" + }, "Ojou-sama wa Gakuen no Seieki Benjo": { "$type": "HashCrypt", "Title": "お嬢様は学園の精液便所 ~寝取らせ・ぶっかけ・乱交生活日誌~" diff --git a/src/ext/io.rs b/src/ext/io.rs index 698e4c7..34da75f 100644 --- a/src/ext/io.rs +++ b/src/ext/io.rs @@ -1898,6 +1898,11 @@ impl<'a> MemWriterRef<'a> { pub fn new(data: &'a mut [u8]) -> Self { MemWriterRef { data, pos: 0 } } + + /// Returns true if is eof. + pub fn is_eof(&self) -> bool { + self.pos >= self.data.len() + } } impl<'a> Read for MemWriterRef<'a> { diff --git a/src/scripts/kirikiri/archive/xp3/crypt/chain_reaction.rs b/src/scripts/kirikiri/archive/xp3/crypt/chain_reaction.rs new file mode 100644 index 0000000..9aec3ce --- /dev/null +++ b/src/scripts/kirikiri/archive/xp3/crypt/chain_reaction.rs @@ -0,0 +1,281 @@ +use super::*; +use crate::ext::mutex::*; +use crate::utils::lzss::*; +use std::sync::Mutex; + +macro_rules! base_schema_impl { + () => { + fn hash_after_crypt(&self) -> bool { + AsRef::::as_ref(self).hash_after_crypt + } + fn startup_tjs_not_encrypted(&self) -> bool { + AsRef::::as_ref(self).startup_tjs_not_encrypted + } + fn obfuscated_index(&self) -> bool { + AsRef::::as_ref(self).obfuscated_index + } + }; +} + +fn convert_u32_from_string(input: &str) -> Result { + let s = input.trim(); + if s.is_empty() { + anyhow::bail!("String is empty"); + } + Ok(if s.starts_with("0x") || s.starts_with("0X") { + u32::from_str_radix(&s[2..], 16)? + } else if s.starts_with('#') { + u32::from_str_radix(&s[1..], 16)? + } else if s.to_lowercase().starts_with("&h") { + u32::from_str_radix(&s[2..], 16)? + } else { + s.parse::()? + }) +} + +trait IChainReactionCrypt: std::fmt::Debug { + fn get_encryption_limit(&self, entry: &Xp3Entry) -> u32; + fn init(&self, archive: &mut Xp3Archive) -> Result<()>; +} + +#[derive(Debug)] +struct ChainReactionCryptBase { + encryption_threshold_map: Mutex>, + list_bin: String, +} + +impl ChainReactionCryptBase { + fn new(list_bin: String) -> Self { + Self { + encryption_threshold_map: Mutex::new(HashMap::new()), + list_bin, + } + } + + fn init2(&self, mut bin: Vec) -> Result<()> { + if !bin.starts_with(b"\"\r\n") { + for _ in 0..3 { + bin = Self::decode_list_bin(bin)?; + } + // std::fs::write("test.bin", &bin)?; + } + self.encryption_threshold_map.lock_blocking().clear(); + self.parse_list_bin(bin) + } + + fn read_list_bin(archive: &mut Xp3Archive, list_name: &str) -> Result>> { + let bin = match archive.entries.iter().find(|x| x.name == list_name) { + Some(index) => index.clone(), + None => return Ok(None), + }; + let mut entry = Entry::new2( + archive.inner.clone(), + bin, + archive.base_offset, + archive.crypt.clone(), + ); + let mut data = Vec::new(); + entry.read_to_end(&mut data)?; + Ok(Some(data)) + } + + fn parse_list_bin(&self, data: Vec) -> Result<()> { + let mut map = self.encryption_threshold_map.lock_blocking(); + let decoded = decode_to_string(Encoding::Utf8, &data, true)?; + for line in decoded.lines() { + let line = line.trim(); + if line.is_empty() || !line.starts_with("0") { + continue; + } + let pair: Vec<_> = line.split(',').collect(); + if pair.len() > 1 { + let hash = convert_u32_from_string(pair[0])?; + let threshold = convert_u32_from_string(pair[1])?; + map.insert(hash, threshold); + } + } + Ok(()) + } + + fn decode_list_bin(data: Vec) -> Result> { + let mut header = [0; 0x30]; + Self::decode_dpd(&data[..0x30], &mut header)?; + let hread = MemReaderRef::new(&header); + let packed_size = hread.cpeek_u32_at(0x0c)? as usize; + let unpacked_size = hread.cpeek_u32_at(0x10)? as usize; + if packed_size > data.len() - 0x30 { + anyhow::bail!("Data is too smail."); + } + let sig = &header[0..4]; + if sig == b"DPDC" { + let mut decrypted = Vec::with_capacity(packed_size); + decrypted.resize(packed_size, 0); + Self::decode_dpd(&data[0x30..packed_size + 0x30], &mut decrypted)?; + Ok(decrypted) + } else if sig == b"SZLC" { + let reader = MemReaderRef::new(&data[0x30..packed_size + 0x30]); + let mut lzss = LzssReader::new(reader); + let mut result = Vec::with_capacity(unpacked_size); + lzss.read_to_end(&mut result)?; + if result.len() > unpacked_size { + result.truncate(unpacked_size); + } + Ok(result) + } else if sig == b"ELRC" { + let min_repeat = hread.cpeek_u32_at(0x1C)?; + let mut decoded = Vec::with_capacity(unpacked_size); + decoded.resize(unpacked_size, 0); + Self::decode_rle(&data[0x30..packed_size + 0x30], &mut decoded, min_repeat)?; + Ok(decoded) + } else { + anyhow::bail!("Unknown signature: {:?}", sig); + } + } + + fn decode_dpd(src: &[u8], dst: &mut [u8]) -> Result<()> { + let length = src.len(); + if length != dst.len() { + anyhow::bail!("Length no matched."); + } + if length < 8 { + dst.copy_from_slice(src); + return Ok(()); + } + let tail = length & 3; + if tail > 0 { + dst[length - tail..].copy_from_slice(&src[length - tail..]); + } + let length = length / 4; + let mut reader = MemReaderRef::new(src); + let mut writer = MemWriterRef::new(dst); + let mut val = reader.read_u32()?; + for _ in 0..length - 1 { + let nval = reader.read_u32()?; + writer.write_u32(val ^ nval)?; + val = nval; + } + let fdst = writer.peek_u32_at(0)?; + writer.write_u32(fdst ^ val)?; + Ok(()) + } + + fn decode_rle(src: &[u8], dst: &mut [u8], min_repeat: u32) -> Result<()> { + let mut reader = MemReaderRef::new(src); + let mut writer = MemWriterRef::new(dst); + while !reader.is_eof() { + let b = reader.read_u8()?; + let mut repeat = 1; + while repeat < min_repeat && !reader.is_eof() && reader.cpeek_u8()? == b { + repeat += 1; + reader.pos += 1; + } + if repeat == min_repeat { + let ctl = reader.read_u8()?; + if ctl > 0x7F { + repeat += (reader.read_u8()? as u32) + (((ctl & 0x7F) as u32) << 8) + 0x80; + } else { + repeat += ctl as u32; + } + } + for _ in 0..repeat { + writer.write_u8(b)?; + } + } + Ok(()) + } +} + +impl IChainReactionCrypt for ChainReactionCryptBase { + fn get_encryption_limit(&self, entry: &Xp3Entry) -> u32 { + self.encryption_threshold_map + .lock_blocking() + .get(&entry.file_hash) + .map(|s| *s) + .unwrap_or(0x200) + } + fn init(&self, archive: &mut Xp3Archive) -> Result<()> { + let bin = Self::read_list_bin(archive, &self.list_bin)?; + if let Some(bin) = bin { + if bin.len() >= 0x30 { + self.init2(bin)?; + } + } + Ok(()) + } +} + +#[derive(Debug)] +pub struct ChainReactionCrypt { + base: BaseSchema, + inner: Box, +} + +impl ChainReactionCrypt { + pub fn new(base: BaseSchema) -> Self { + Self { + base, + inner: Box::new(ChainReactionCryptBase::new("plugin/list.bin".into())), + } + } +} + +impl AsRef for ChainReactionCrypt { + fn as_ref(&self) -> &BaseSchema { + &self.base + } +} + +impl Crypt for ChainReactionCrypt { + base_schema_impl!(); + fn init(&self, archive: &mut Xp3Archive) -> Result<()> { + self.inner.init(archive) + } + fn decrypt_supported(&self) -> bool { + true + } + fn decrypt_seek_supported(&self) -> bool { + true + } + fn decrypt<'a>( + &self, + entry: &Xp3Entry, + cur_seg: &Segment, + stream: Box, + ) -> Result> { + Ok(Box::new(ChainReactionCryptReader::new( + stream, + cur_seg, + (self.inner.get_encryption_limit(entry), entry.file_hash), + ))) + } + fn decrypt_with_seek<'a>( + &self, + entry: &Xp3Entry, + cur_seg: &Segment, + stream: Box, + ) -> Result> { + Ok(Box::new(ChainReactionCryptReader::new( + stream, + cur_seg, + (self.inner.get_encryption_limit(entry), entry.file_hash), + ))) + } +} + +impl Read for ChainReactionCryptReader { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + let readed = self.inner.read(buf)?; + let (limit, hash) = self.key; + let limit = limit as u64; + let mut offset = self.seg_start + self.pos; + if offset < limit { + let count = (limit - offset).min(readed as u64); + for t in buf[..count as usize].iter_mut() { + *t ^= (offset ^ ((hash >> ((offset & 3) << 3)) as u8) as u64) as u8; + offset += 1; + } + } + self.pos += readed as u64; + Ok(readed) + } +} diff --git a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs index 1e15e42..4e5dfe6 100644 --- a/src/scripts/kirikiri/archive/xp3/crypt/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/crypt/mod.rs @@ -1,3 +1,4 @@ +mod chain_reaction; mod cx; mod cz; @@ -265,6 +266,7 @@ enum CryptType { char_map: String, layer_name_suffix: String, }, + ChainReactionCrypt, } #[derive(Clone, Debug, Deserialize)] @@ -427,6 +429,9 @@ impl Schema { char_map, layer_name_suffix, )?), + CryptType::ChainReactionCrypt => { + Box::new(chain_reaction::ChainReactionCrypt::new(self.base.clone())) + } }) } } @@ -2463,6 +2468,8 @@ impl Crypt for PureMoreCrypt { } } +seek_reader_key_impl!(ChainReactionCryptReader, (u32, u32)); + #[test] fn test_deserialize_crypt() { for (key, schema) in CRYPT_SCHEMA.iter() { diff --git a/src/scripts/kirikiri/archive/xp3/mod.rs b/src/scripts/kirikiri/archive/xp3/mod.rs index 28d70b8..f30daf9 100644 --- a/src/scripts/kirikiri/archive/xp3/mod.rs +++ b/src/scripts/kirikiri/archive/xp3/mod.rs @@ -407,6 +407,15 @@ impl<'a> Entry<'a> { force_decrypt, } } + + fn new2( + reader: Arc>>, + index: archive::Xp3Entry, + base_offset: u64, + crypt: Arc>, + ) -> Self { + Self::new(reader, index, base_offset, crypt, false, false) + } } impl<'b> ArchiveContent for Entry<'b> { diff --git a/src/utils/lzss.rs b/src/utils/lzss.rs new file mode 100644 index 0000000..433cca2 --- /dev/null +++ b/src/utils/lzss.rs @@ -0,0 +1,186 @@ +//! LZSS Stream +use crate::ext::io::*; +use std::io::{Read, Result}; + +/// A Lzss Reader +pub struct LzssReader { + reader: T, + buf: Vec, + buf_pos: usize, + frame: Vec, + frame_mask: usize, + frame_pos: usize, + tmp: u8, + tmp_used: bool, +} + +impl LzssReader { + pub fn new(reader: T) -> Self { + Self::new2(reader, 0x1000, 0, 0xfee) + } + pub fn new2(reader: T, frame_size: usize, frame_fill: u8, frame_init_pos: usize) -> Self { + Self { + reader, + buf: Vec::new(), + buf_pos: 0, + frame: vec![frame_fill; frame_size], + frame_mask: frame_size - 1, + frame_pos: frame_init_pos, + tmp: 0, + tmp_used: false, + } + } + fn push(&mut self, buf: &mut MemWriterRef, data: u8) -> Result<()> { + if buf.is_eof() { + self.buf.push(data); + } else { + buf.write_u8(data)?; + } + Ok(()) + } + fn unpack(&mut self, buf: &mut MemWriterRef) -> Result<()> { + let mut bu = [0; 1]; + let mut readed = if self.tmp_used { + bu[0] = self.tmp; + self.tmp_used = false; + 1 + } else { + self.reader.read(&mut bu)? + }; + if readed == 0 { + // eof + return Ok(()); + } + let ctl = bu[0]; + let mut bit = 1; + readed = self.reader.read(&mut bu)?; + while bit != 0x100 && readed > 0 { + if ((ctl as u32) & bit) != 0 { + let b = bu[0]; + self.frame[self.frame_pos] = b; + self.frame_pos += 1; + self.frame_pos &= self.frame_mask; + self.push(buf, b)?; + } else { + let lo = bu[0]; + readed = self.reader.read(&mut bu)?; + if readed == 0 { + return Ok(()); + } + let hi = bu[0]; + let mut offset = (((hi as usize) & 0xF0) << 4) | (lo as usize); + let mut count = 3 + (hi & 0xF); + while count != 0 { + let v = self.frame[offset]; + offset += 1; + offset &= self.frame_mask; + self.frame[self.frame_pos] = v; + self.frame_pos += 1; + self.frame_pos &= self.frame_mask; + self.push(buf, v)?; + count -= 1; + } + } + bit <<= 1; + readed = self.reader.read(&mut bu)?; + } + if readed > 0 { + self.tmp = bu[0]; + self.tmp_used = true; + } + Ok(()) + } +} + +impl Read for LzssReader { + fn read(&mut self, buf: &mut [u8]) -> Result { + if !self.buf.is_empty() && self.buf_pos < self.buf.len() { + let readed = buf.len().min(self.buf.len() - self.buf_pos); + buf[..readed].copy_from_slice(&self.buf[self.buf_pos..self.buf_pos + readed]); + self.buf_pos += readed; + if self.buf_pos == self.buf.len() { + self.buf.clear(); + self.buf_pos = 0; + } + return Ok(readed); + } + let mut writer = MemWriterRef::new(buf); + self.unpack(&mut writer)?; + Ok(writer.pos) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_literal_only() { + let data = [0xFF, 0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80]; + let mut reader = LzssReader::new(&data[..]); + let mut result = Vec::new(); + reader.read_to_end(&mut result).unwrap(); + assert_eq!(result, vec![0x10, 0x20, 0x30, 0x40, 0x50, 0x60, 0x70, 0x80]); + } + + #[test] + fn test_back_ref() { + // "ABCABCABCABCABCABC": 3 literals + 5 back-refs (offset=0, count=3) + let data = [0x07, b'A', b'B', b'C', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + let mut reader = LzssReader::new2(&data[..], 256, 0, 0); + let mut result = Vec::new(); + reader.read_to_end(&mut result).unwrap(); + assert_eq!(result, b"ABCABCABCABCABCABC"); + } + + #[test] + fn test_chunked_read() { + let data = [0x07, b'A', b'B', b'C', 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + let mut reader = LzssReader::new2(&data[..], 256, 0, 0); + let mut buf = [0u8; 5]; + let mut result = Vec::new(); + loop { + let n = reader.read(&mut buf).unwrap(); + if n == 0 { + break; + } + result.extend_from_slice(&buf[..n]); + } + assert_eq!(result, b"ABCABCABCABCABCABC"); + } + + #[test] + fn test_run_length() { + // "AAAAA" via literal 'A' + back-ref (offset=0, count=4) for expanding run + let data = [0x01, b'A', 0, 0x01]; + let mut reader = LzssReader::new2(&data[..], 256, 0, 0); + let mut result = Vec::new(); + reader.read_to_end(&mut result).unwrap(); + assert_eq!(result, b"AAAAA"); + } + + #[test] + fn test_back_ref_offset() { + // "XXXXXABCDEABCDE": literals + back-ref at non-zero offset + let data = [ + 0xFF, b'X', b'X', b'X', b'X', b'X', b'A', b'B', b'C', 0x03, b'D', b'E', 0x05, 0x02, + ]; + let mut reader = LzssReader::new2(&data[..], 256, 0, 0); + let mut result = Vec::new(); + reader.read_to_end(&mut result).unwrap(); + assert_eq!(result, b"XXXXXABCDEABCDE"); + } + + #[test] + fn test_multi_control_byte() { + // 16 literals across 2 control bytes + let data = [ + 0xFF, b'A', b'B', b'C', b'D', b'E', b'F', b'G', b'H', 0xFF, b'I', b'J', b'K', b'L', + b'M', b'N', b'O', b'P', + ]; + let mut reader = LzssReader::new(&data[..]); + let mut result = Vec::new(); + reader.read_to_end(&mut result).unwrap(); + assert_eq!(result, b"ABCDEFGHIJKLMNOP"); + } +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs index 8f63749..d1249b4 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -24,6 +24,8 @@ pub mod img; pub mod jxl; #[cfg(feature = "lossless-audio")] pub mod lossless_audio; +#[cfg(feature = "utils-lzss")] +pub mod lzss; mod macros; #[cfg(feature = "utils-mmx")] pub mod mmx;