diff --git a/README.md b/README.md index f6a0f79..2bca6fb 100644 --- a/README.md +++ b/README.md @@ -201,7 +201,7 @@ msg-tool create -t | Archive Type | Feature Name | Name | Unpack | Pack | Remarks | |---|---|---|---|---|---| -| `musica-arc` | `musica-arc` | Musica Archive Resource File (.paz) | ✔️ | ❌ | | +| `musica-arc` | `musica-arc` | Musica Archive Resource File (.paz) | ✔️ | ✔️ | | ### Silky Engine | Script Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Custom Export | Custom Import | Create | Remarks | |---|---|---|---|---|---|---|---|---|---|---| diff --git a/src/main.rs b/src/main.rs index 04c627c..340e2e9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1487,7 +1487,7 @@ pub fn import_script( } }; if arg.force_script || f.is_script() { - let mut writer = arch.new_file(f.name())?; + let mut writer = arch.new_file(f.name(), None)?; let (script_file, _) = match parse_script_from_archive(&mut f, arg, config.clone(), &script) { Ok(s) => s, @@ -1952,8 +1952,24 @@ pub fn import_script( continue; } } else { - let mut writer = arch.new_file_non_seek(f.name())?; let out_path = std::path::PathBuf::from(&odir).join(f.name()); + let size = if out_path.is_file() { + match std::fs::metadata(&out_path) { + Ok(meta) => Some(meta.len()), + Err(e) => { + eprintln!( + "Error getting metadata for file {}: {}", + out_path.display(), + e + ); + COUNTER.inc_error(); + continue; + } + } + } else { + None + }; + let mut writer = arch.new_file_non_seek(f.name(), size)?; if out_path.is_file() { let f = match std::fs::File::open(&out_path) { Ok(f) => f, @@ -2357,7 +2373,15 @@ pub fn pack_archive( continue; } }; - let mut wf = match archive.new_file_non_seek(name) { + let size = match std::fs::metadata(file) { + Ok(meta) => meta.len(), + Err(e) => { + eprintln!("Error getting metadata for file {}: {}", file, e); + COUNTER.inc_error(); + continue; + } + }; + let mut wf = match archive.new_file_non_seek(name, Some(size)) { Ok(f) => f, Err(e) => { eprintln!("Error creating file {} in archive: {}", name, e); @@ -2487,7 +2511,15 @@ pub fn pack_archive_v2( continue; } }; - let mut wf = match archive.new_file_non_seek(name) { + let size = match std::fs::metadata(file) { + Ok(meta) => meta.len(), + Err(e) => { + eprintln!("Error getting metadata for file {}: {}", file, e); + COUNTER.inc_error(); + continue; + } + }; + let mut wf = match archive.new_file_non_seek(name, Some(size)) { Ok(f) => f, Err(e) => { eprintln!("Error creating file {} in archive: {}", name, e); diff --git a/src/scripts/artemis/archive/pf2.rs b/src/scripts/artemis/archive/pf2.rs index be2447a..7c3c78b 100644 --- a/src/scripts/artemis/archive/pf2.rs +++ b/src/scripts/artemis/archive/pf2.rs @@ -363,7 +363,11 @@ impl ArtemisPf2Writer { } impl Archive for ArtemisPf2Writer { - fn new_file<'a>(&'a mut self, name: &str) -> Result> { + fn new_file<'a>( + &'a mut self, + name: &str, + _size: Option, + ) -> Result> { let entry = self .headers .get_mut(name) diff --git a/src/scripts/artemis/archive/pfs.rs b/src/scripts/artemis/archive/pfs.rs index de9d4ed..e696e31 100644 --- a/src/scripts/artemis/archive/pfs.rs +++ b/src/scripts/artemis/archive/pfs.rs @@ -391,7 +391,11 @@ impl ArtemisArcWriter { } impl Archive for ArtemisArcWriter { - fn new_file<'a>(&'a mut self, name: &str) -> Result> { + fn new_file<'a>( + &'a mut self, + name: &str, + _size: Option, + ) -> Result> { let entry = self .headers .get_mut(name) diff --git a/src/scripts/base.rs b/src/scripts/base.rs index 9560d5f..689ae9b 100644 --- a/src/scripts/base.rs +++ b/src/scripts/base.rs @@ -585,10 +585,19 @@ pub trait Script: std::fmt::Debug + std::any::Any { /// A trait for creating archives. pub trait Archive { /// Creates a new file in the archive. - fn new_file<'a>(&'a mut self, name: &str) -> Result>; + /// + /// size is optional, if provided, size must be exactly the size of the file to be created. + fn new_file<'a>(&'a mut self, name: &str, size: Option) + -> Result>; /// Creates a new file in the archive that does not require seeking. - fn new_file_non_seek<'a>(&'a mut self, name: &str) -> Result> { - self.new_file(name) + /// + /// size is optional, if provided, size must be exactly the size of the file to be created. + fn new_file_non_seek<'a>( + &'a mut self, + name: &str, + size: Option, + ) -> Result> { + self.new_file(name, size) .map(|f| Box::new(f) as Box) } /// Writes the header of the archive. (Must be called after writing all files.) diff --git a/src/scripts/bgi/archive/v1.rs b/src/scripts/bgi/archive/v1.rs index b004464..853d64d 100644 --- a/src/scripts/bgi/archive/v1.rs +++ b/src/scripts/bgi/archive/v1.rs @@ -568,7 +568,11 @@ impl BgiArchiveWriter { } impl Archive for BgiArchiveWriter { - fn new_file<'a>(&'a mut self, name: &str) -> Result> { + fn new_file<'a>( + &'a mut self, + name: &str, + _size: Option, + ) -> Result> { let entry = self .headers .get_mut(name) diff --git a/src/scripts/bgi/archive/v2.rs b/src/scripts/bgi/archive/v2.rs index 87f367c..0423350 100644 --- a/src/scripts/bgi/archive/v2.rs +++ b/src/scripts/bgi/archive/v2.rs @@ -571,7 +571,11 @@ impl BgiArchiveWriter { } impl Archive for BgiArchiveWriter { - fn new_file<'a>(&'a mut self, name: &str) -> Result> { + fn new_file<'a>( + &'a mut self, + name: &str, + _size: Option, + ) -> Result> { let entry = self .headers .get_mut(name) diff --git a/src/scripts/circus/archive/pck.rs b/src/scripts/circus/archive/pck.rs index 6ee3092..e22f514 100644 --- a/src/scripts/circus/archive/pck.rs +++ b/src/scripts/circus/archive/pck.rs @@ -369,7 +369,11 @@ impl PckArchiveWriter { } impl Archive for PckArchiveWriter { - fn new_file<'a>(&'a mut self, name: &str) -> Result> { + fn new_file<'a>( + &'a mut self, + name: &str, + _size: Option, + ) -> Result> { let entry = self .headers .get_mut(name) diff --git a/src/scripts/escude/archive.rs b/src/scripts/escude/archive.rs index 5d6663a..32f4f12 100644 --- a/src/scripts/escude/archive.rs +++ b/src/scripts/escude/archive.rs @@ -350,7 +350,11 @@ impl EscudeBinArchiveWriter { } impl Archive for EscudeBinArchiveWriter { - fn new_file<'a>(&'a mut self, name: &str) -> Result> { + fn new_file<'a>( + &'a mut self, + name: &str, + _size: Option, + ) -> Result> { let entry = self .headers .get_mut(name) diff --git a/src/scripts/kirikiri/archive/xp3pack/writer.rs b/src/scripts/kirikiri/archive/xp3pack/writer.rs index 7d46e1f..e394077 100644 --- a/src/scripts/kirikiri/archive/xp3pack/writer.rs +++ b/src/scripts/kirikiri/archive/xp3pack/writer.rs @@ -164,15 +164,23 @@ impl<'a> Drop for Writer<'a> { } impl Archive for Xp3ArchiveWriter { - fn new_file<'a>(&'a mut self, name: &str) -> Result> { - let inner = self.new_file_non_seek(name)?; + fn new_file<'a>( + &'a mut self, + name: &str, + size: Option, + ) -> Result> { + let inner = self.new_file_non_seek(name, size)?; Ok(Box::new(Writer { inner, mem: MemWriter::new(), })) } - fn new_file_non_seek<'a>(&'a mut self, name: &str) -> Result> { + fn new_file_non_seek<'a>( + &'a mut self, + name: &str, + _size: Option, + ) -> Result> { if self.segmenter.is_none() { self.runner.join(); } diff --git a/src/scripts/musica/archive/paz.rs b/src/scripts/musica/archive/paz.rs index 53b6d0f..4946ce4 100644 --- a/src/scripts/musica/archive/paz.rs +++ b/src/scripts/musica/archive/paz.rs @@ -164,6 +164,20 @@ impl ScriptBuilder for PazArcBuilder { } None } + + fn create_archive( + &self, + filename: &str, + files: &[&str], + encoding: Encoding, + config: &ExtraConfig, + ) -> Result> { + let file = std::fs::File::create(filename)?; + let file = std::io::BufWriter::new(file); + Ok(Box::new(PazArcWriter::new( + file, files, encoding, filename, config, + )?)) + } } #[derive(Debug, StructPack, StructUnpack, Clone)] @@ -449,27 +463,6 @@ impl ArchiveContent for PazFileEntry { } } -#[test] -fn test_deserialize_paz() { - for (game, schema) in PAZ_SCHEMA.iter() { - println!("Game: {}", game); - println!("Version: {}", schema.version); - for (arc_name, arc_key) in schema.arc_keys.iter() { - println!(" Arc Name: {}", arc_name); - println!(" Index Key: {:02X?}", arc_key.index_key.bytes); - if let Some(data_key) = &arc_key.data_key { - println!(" Data Key: {:02X?}", data_key.bytes); - } else { - println!(" Data Key: None"); - } - } - for (type_name, type_key) in schema.type_keys.iter() { - println!(" Type Name: {}, Type Key: {}", type_name, type_key); - } - println!("Signature: {:08X}", schema.signature); - } -} - struct TableEncryptedStream { inner: T, table: Vec, @@ -524,3 +517,327 @@ impl Write for TableEncryptedStream { self.inner.flush() } } + +pub struct PazArcWriter { + writer: T, + headers: HashMap, + encoding: Encoding, + is_audio: bool, + mov_key: Option>, + schema: Schema, + arc_key: ArcKey, +} + +impl PazArcWriter { + pub fn new( + mut writer: T, + files: &[&str], + encoding: Encoding, + filename: &str, + config: &ExtraConfig, + ) -> Result { + let schema = config.musica_game_title.as_ref().ok_or_else(|| { + anyhow::anyhow!( + "Game title not specified. Use --musica-game-title to specify the game title." + ) + })?; + let schema = query_paz_schema(schema).ok_or_else(|| { + anyhow::anyhow!("Unsupported game title '{}' for PAZ archive", schema) + })?; + let arc_name = std::path::Path::new(filename) + .file_stem() + .and_then(|s| s.to_str()) + .ok_or_else(|| anyhow::anyhow!("Invalid filename"))? + .to_lowercase(); + let is_audio = AUDIO_PAZ_NAMES.contains(&arc_name.as_str()); + let is_video = arc_name == "mov"; + let arc_key = schema.arc_keys.get(&arc_name).ok_or_else(|| { + anyhow::anyhow!( + "No ARC key found for archive name '{}' in game schema", + arc_name + ) + })?; + let mov_key = if is_video { + let mut key = vec![0u8; 0x100]; + for i in 0..0x100 { + key[i] = i as u8; + } + Some(key) + } else { + None + }; + let start_offset = if schema.version > 0 { 0x20 } else { 0 }; + if start_offset > 0 { + if schema.signature != 0 { + writer.write_u32(schema.signature)?; + } + writer.seek(SeekFrom::Start(start_offset))?; + } + let mut entries = HashMap::new(); + for file in files { + let entry = PazEntry { + name: file.to_string(), + offset: 0, + unpacked_size: 0, + size: 0, + aligned_size: 0, + flags: 0, + }; + entries.insert(file.to_string(), entry); + } + writer.write_u32(0)?; // Placeholder for index size + { + let blowfish: Blowfish = Blowfish::new(&arc_key.index_key)?; + let mut index_stream = BlowfishEncryptor::new(blowfish, &mut writer); + index_stream.write_u32(entries.len() as u32)?; + if let Some(mov_data) = &mov_key { + index_stream.write_all(mov_data)?; + } + for entry in entries.values() { + index_stream.write_struct(entry, false, encoding)?; + } + } + let index_end = writer.stream_position()?; + let index_size = (index_end - start_offset - 4) as u32; + if index_size >> 24 != 0 { + return Err(anyhow::anyhow!("PAZ index size too large")); + } + writer.write_u32_at(start_offset, index_size)?; + Ok(PazArcWriter { + writer, + headers: entries, + encoding, + is_audio, + mov_key, + schema: schema.clone(), + arc_key: arc_key.clone(), + }) + } +} + +impl Archive for PazArcWriter { + fn new_file<'a>( + &'a mut self, + name: &str, + _size: Option, + ) -> Result> { + let entry = self + .headers + .get_mut(name) + .ok_or_else(|| anyhow::anyhow!("File '{}' not found in PAZ archive headers", name))?; + if entry.offset != 0 || entry.size != 0 { + return Err(anyhow::anyhow!( + "File '{}' already exists in PAZ archive", + name + )); + } + if let Some(data_key) = &self.arc_key.data_key { + let blowfish: Blowfish = Blowfish::new(&data_key.bytes)?; + entry.offset = self.writer.stream_position()?; + let stream = BlowfishEncryptor::new(blowfish, &mut self.writer); + let mut type_key = None; + if self.schema.version > 0 { + if let Some(tkey) = self.schema.get_type_key(&entry, self.is_audio) { + type_key = Some(tkey.to_string()); + } + } + let writer = MemDataKeyWriter { + inner: stream, + cache: MemWriter::new(), + type_key, + entry, + encoding: self.encoding, + version: self.schema.version, + }; + return Ok(Box::new(writer)); + } + Err(anyhow::anyhow!("Data encryption key not found.")) + } + + fn new_file_non_seek<'a>( + &'a mut self, + name: &str, + size: Option, + ) -> Result> { + if let Some(data_key) = &self.arc_key.data_key { + let size = match size { + Some(size) => size, + None => { + return Ok(Box::new(self.new_file(name, None)?)); + } + }; + let entry = self.headers.get_mut(name).ok_or_else(|| { + anyhow::anyhow!("File '{}' not found in PAZ archive headers", name) + })?; + if entry.offset != 0 || entry.size != 0 { + return Err(anyhow::anyhow!( + "File '{}' already exists in PAZ archive", + name + )); + } + let blowfish: Blowfish = Blowfish::new(&data_key.bytes)?; + entry.offset = self.writer.stream_position()?; + let stream = BlowfishEncryptor::new(blowfish, &mut self.writer); + if self.schema.version > 0 { + if let Some(tkey) = self.schema.get_type_key(&entry, self.is_audio) { + let key = format!("{} {:08X} {}", entry.name.to_ascii_lowercase(), size, tkey); + let key = encode_string(self.encoding, &key, false)?; + let mut rc4 = Rc4::new(&key); + if self.schema.version >= 2 { + let crc = crc32fast::hash(&key); + let skip = ((crc >> 12) as i32) & 0xFF; + rc4.skip_bytes(skip as usize); + } + let writer = Rc4Stream::new(stream, rc4); + let writer = DateKeyWriter { + inner: Box::new(writer), + entry, + }; + return Ok(Box::new(writer)); + } + } + let writer = DateKeyWriter { + inner: Box::new(stream), + entry, + }; + return Ok(Box::new(writer)); + } + Err(anyhow::anyhow!("Data encryption key not found.")) + } + + fn write_header(&mut self) -> Result<()> { + let start_offset = if self.schema.version > 0 { 0x24 } else { 4 }; + self.writer.seek(SeekFrom::Start(start_offset))?; + { + let blowfish: Blowfish = Blowfish::new(&self.arc_key.index_key)?; + let mut index_stream = BlowfishEncryptor::new(blowfish, &mut self.writer); + index_stream.write_u32(self.headers.len() as u32)?; + if let Some(mov_data) = &self.mov_key { + index_stream.write_all(mov_data)?; + } + for entry in self.headers.values() { + index_stream.write_struct(entry, false, self.encoding)?; + } + } + Ok(()) + } +} + +struct DateKeyWriter<'a> { + inner: Box, + entry: &'a mut PazEntry, +} + +impl<'a> Write for DateKeyWriter<'a> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let writed = self.inner.write(buf)?; + self.entry.size += writed as u32; + Ok(writed) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.inner.flush() + } +} + +impl<'a> Drop for DateKeyWriter<'a> { + fn drop(&mut self) { + self.entry.unpacked_size = self.entry.size; + self.entry.aligned_size = (self.entry.size + 7) & !7; + } +} + +struct MemDataKeyWriter<'a, T: Write + Seek> { + inner: BlowfishEncryptor, + cache: MemWriter, + type_key: Option, + entry: &'a mut PazEntry, + encoding: Encoding, + version: u32, +} + +impl<'a, T: Write + Seek> Write for MemDataKeyWriter<'a, T> { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + self.cache.write(buf) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.cache.flush() + } +} + +impl<'a, T: Write + Seek> Seek for MemDataKeyWriter<'a, T> { + fn seek(&mut self, pos: std::io::SeekFrom) -> std::io::Result { + self.cache.seek(pos) + } + + fn rewind(&mut self) -> std::io::Result<()> { + self.cache.rewind() + } + + fn stream_position(&mut self) -> std::io::Result { + self.cache.stream_position() + } +} + +impl<'a, T: Write + Seek> Drop for MemDataKeyWriter<'a, T> { + fn drop(&mut self) { + let data = &self.cache.data; + self.entry.unpacked_size = data.len() as u32; + self.entry.size = self.entry.unpacked_size; + self.entry.aligned_size = (self.entry.size + 7) & !7; + let mut stream = if let Some(tkey) = &self.type_key { + let key = format!( + "{} {:08X} {}", + self.entry.name.to_ascii_lowercase(), + self.entry.unpacked_size, + tkey + ); + let key = match encode_string(self.encoding, &key, false) { + Ok(key) => key, + Err(e) => { + eprintln!( + "Error encoding key for PAZ file entry '{}': {}", + self.entry.name, e + ); + crate::COUNTER.inc_error(); + return; + } + }; + let mut rc4 = Rc4::new(&key); + if self.version >= 2 { + let crc = crc32fast::hash(&key); + let skip = ((crc >> 12) as i32) & 0xFF; + rc4.skip_bytes(skip as usize); + } + Box::new(Rc4Stream::new(&mut self.inner, rc4)) as Box + } else { + Box::new(&mut self.inner) as Box + }; + if let Err(e) = stream.write_all(&data) { + eprintln!("Error writing PAZ file entry '{}': {}", self.entry.name, e); + crate::COUNTER.inc_error(); + } + } +} + +#[test] +fn test_deserialize_paz() { + for (game, schema) in PAZ_SCHEMA.iter() { + println!("Game: {}", game); + println!("Version: {}", schema.version); + for (arc_name, arc_key) in schema.arc_keys.iter() { + println!(" Arc Name: {}", arc_name); + println!(" Index Key: {:02X?}", arc_key.index_key.bytes); + if let Some(data_key) = &arc_key.data_key { + println!(" Data Key: {:02X?}", data_key.bytes); + } else { + println!(" Data Key: None"); + } + } + for (type_name, type_key) in schema.type_keys.iter() { + println!(" Type Name: {}, Type Key: {}", type_name, type_key); + } + println!("Signature: {:08X}", schema.signature); + } +} diff --git a/src/scripts/yaneurao/itufuru/archive.rs b/src/scripts/yaneurao/itufuru/archive.rs index 32cec27..c9eba88 100644 --- a/src/scripts/yaneurao/itufuru/archive.rs +++ b/src/scripts/yaneurao/itufuru/archive.rs @@ -330,7 +330,11 @@ impl ItufuruArchiveWriter { } impl Archive for ItufuruArchiveWriter { - fn new_file<'a>(&'a mut self, name: &str) -> Result> { + fn new_file<'a>( + &'a mut self, + name: &str, + _size: Option, + ) -> Result> { let entry = self .headers .get_mut(name) diff --git a/src/utils/blowfish.rs b/src/utils/blowfish.rs index 6b8909d..275be80 100644 --- a/src/utils/blowfish.rs +++ b/src/utils/blowfish.rs @@ -168,7 +168,7 @@ mod consts { use anyhow::Result; use byteorder::{BE, ByteOrder, LE}; -use std::io::{Read, Seek}; +use std::io::{Read, Seek, Write}; use std::marker::PhantomData; /// Blowfish variant which uses Little Endian byte order read/writes.s. @@ -275,6 +275,17 @@ impl Blowfish { } } + /// Encrypts a block of data in-place. + pub fn encrypt_block(&self, buf: &mut [u8]) { + for i in 0..buf.len() / 8 { + let mut l = T::read_u32(&buf[i * 8..]); + let mut r = T::read_u32(&buf[i * 8 + 4..]); + [l, r] = self.encrypt([l, r]); + T::write_u32(&mut buf[i * 8..], l); + T::write_u32(&mut buf[i * 8 + 4..], r); + } + } + /// Creates a new Blowfish cipher instance with the given key. pub fn new(key: &[u8]) -> Result { if key.len() < 4 || key.len() > 56 { @@ -296,7 +307,7 @@ pub struct BlowfishDecryptor { stream: T, } -impl BlowfishDecryptor { +impl BlowfishDecryptor { pub fn new(cipher: Blowfish, stream: T) -> Self { Self { cipher, @@ -388,3 +399,66 @@ impl Seek for BlowfishDecryptor { Ok(newpos) } } + +pub struct BlowfishEncryptor { + cipher: Blowfish, + buffer: [u8; 8], + buffer_len: usize, + stream: T, +} + +impl BlowfishEncryptor { + pub fn new(cipher: Blowfish, stream: T) -> Self { + Self { + cipher, + buffer: [0u8; 8], + buffer_len: 0, + stream, + } + } + + pub fn new_with_key(key: &[u8], stream: T) -> Result { + let cipher = Blowfish::new(key)?; + Ok(Self::new(cipher, stream)) + } +} + +impl Write for BlowfishEncryptor { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + let total_bytes = buf.len() + self.buffer_len; + let mut wbuf = vec![0u8; total_bytes]; + if self.buffer_len > 0 { + wbuf[..self.buffer_len].copy_from_slice(&self.buffer[..self.buffer_len]); + } + wbuf[self.buffer_len..].copy_from_slice(buf); + self.cipher.encrypt_block(&mut wbuf); + self.buffer_len = total_bytes % 8; + if self.buffer_len > 0 { + let start = total_bytes - self.buffer_len; + self.buffer[..self.buffer_len].copy_from_slice(&wbuf[start..]); + self.stream.write_all(&wbuf[..start])?; + } else { + self.stream.write_all(&wbuf)?; + } + Ok(buf.len()) + } + + fn flush(&mut self) -> std::io::Result<()> { + self.stream.flush() + } +} + +impl Drop for BlowfishEncryptor { + fn drop(&mut self) { + if self.buffer_len > 0 { + for i in self.buffer_len..8 { + self.buffer[i] = 0; + } + self.cipher.encrypt_block(&mut self.buffer); + if let Err(e) = self.stream.write_all(&self.buffer) { + eprintln!("Error writing final block in BlowfishEncryptor drop: {}", e); + crate::COUNTER.inc_error(); + } + } + } +}