diff --git a/Cargo.lock b/Cargo.lock index 189dc96..5ad4c44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -46,6 +46,15 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstream" version = "1.0.0" @@ -330,6 +339,20 @@ dependencies = [ "rand_core", ] +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "cipher" version = "0.5.1" @@ -474,6 +497,12 @@ dependencies = [ "tiny-keccak", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpubits" version = "0.1.1" @@ -923,6 +952,30 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1069,6 +1122,30 @@ dependencies = [ "typenum", ] +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.2.0" @@ -1325,6 +1402,18 @@ dependencies = [ "libc", ] +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + [[package]] name = "json" version = "0.12.4" @@ -1534,6 +1623,7 @@ dependencies = [ "bytes", "cbc", "chacha20", + "chrono", "clap 4.6.1", "crc32fast", "crossbeam", @@ -1661,6 +1751,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_cpus" version = "1.17.0" @@ -1824,6 +1923,12 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + [[package]] name = "pkg-config" version = "0.3.33" @@ -2051,6 +2156,12 @@ version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "ryu" version = "1.0.23" @@ -2202,6 +2313,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + [[package]] name = "smallvec" version = "1.15.1" @@ -2594,6 +2711,51 @@ dependencies = [ "wit-bindgen 0.51.0", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + [[package]] name = "wasm-encoder" version = "0.244.0" @@ -2680,12 +2842,65 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + [[package]] name = "windows-sys" version = "0.59.0" diff --git a/Cargo.toml b/Cargo.toml index 21a2f03..987962d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ byteorder = { version = "1.5", default-features = false, optional = true} bytes = { version = "1.11", optional = true } cbc = { version = "0.2", optional = true } chacha20 = { version = "0.10", optional = true } +chrono = { version = "0.4", optional = true, features = ["serde"] } clap = { version = "4.5", features = ["derive"] } crc32fast = { version = "1.5", optional = true } crossbeam = { version = "0.8", optional = true } @@ -118,7 +119,7 @@ will-plus = ["utils-str"] will-plus-img = ["will-plus", "image"] yaneurao = [] yaneurao-itufuru = ["yaneurao", "utils-xored-stream"] -yuris = ["dep:hex", "utils-serde-base64bytes", "utils-xored-stream"] +yuris = ["dep:chrono", "dep:hex", "utils-serde-base64bytes", "utils-xored-stream"] yuris-img = ["yuris", "image", "qoi", "webp"] # basic feature image = ["dep:png"] diff --git a/README.md b/README.md index 91951c2..27b1eb1 100644 --- a/README.md +++ b/README.md @@ -271,6 +271,7 @@ msg-tool create -t | `yuris-yscfg` | `yuris` | Yu-Ris YSCFG(config) file (.ybn) | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | | | `yuris-ystb` | `yuris` | Yu-Ris YSTB(compiled script) file (.ybn) | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ❌ | | | `yuris-txt` | `yuris` | Yu-Ris scenario text file (.txt) | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | | +| `yuris-ystl` | `yuris` | Yu-Ris YSTL(file list) file (.ybn) | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | | | Image Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Create | Remarks | |---|---|---|---|---|---|---|---|---| diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 0dd45ef..4c6f3d3 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -188,6 +188,8 @@ lazy_static::lazy_static! { Box::new(yuris::txt::YurisTxtBuilder::new()), #[cfg(feature = "yuris-img")] Box::new(yuris::img::ydg::YDGImageBuilder::new()), + #[cfg(feature = "yuris")] + Box::new(yuris::ystl::YSTLBuilder::new()), ]; /// A list of all script extensions. pub static ref ALL_EXTS: Vec = diff --git a/src/scripts/yuris/mod.rs b/src/scripts/yuris/mod.rs index b8fcc48..d4984d1 100644 --- a/src/scripts/yuris/mod.rs +++ b/src/scripts/yuris/mod.rs @@ -7,3 +7,4 @@ pub mod yscfg; pub mod yscm; pub mod yser; pub mod ystb; +pub mod ystl; diff --git a/src/scripts/yuris/ystl.rs b/src/scripts/yuris/ystl.rs new file mode 100644 index 0000000..ade1e5b --- /dev/null +++ b/src/scripts/yuris/ystl.rs @@ -0,0 +1,347 @@ +//! Yu-Ris YSTL(file list) file (.ybn) +use crate::ext::io::*; +use crate::scripts::base::*; +use crate::types::*; +use crate::utils::encoding::*; +use crate::utils::struct_pack::*; +use anyhow::Result; +use chrono::TimeZone; +use chrono::Timelike; +use chrono::{DateTime, Local, Utc}; +use msg_tool_macro::*; +use serde::{Deserialize, Serialize}; +use std::any::Any; +use std::io::{Read, Seek, Write}; +use std::ops::{Deref, DerefMut}; + +#[derive(Debug, Serialize, Deserialize)] +struct YSTLData { + version: u32, + entries: Vec, +} + +impl StructUnpack for YSTLData { + fn unpack( + reader: &mut R, + big: bool, + encoding: Encoding, + info: &Option>, + ) -> Result { + let version = u32::unpack(reader, big, encoding, info)?; + let ninfo = Box::new(version) as Box; + let count = u32::unpack(reader, big, encoding, info)?; + let entries = reader.read_struct_vec(count as usize, big, encoding, &Some(ninfo))?; + Ok(Self { version, entries }) + } +} + +impl StructPack for YSTLData { + fn pack( + &self, + writer: &mut W, + big: bool, + encoding: Encoding, + info: &Option>, + ) -> Result<()> { + self.version.pack(writer, big, encoding, info)?; + let ninfo = Box::new(self.version) as Box; + let count = self.entries.len() as u32; + count.pack(writer, big, encoding, info)?; + let info = &Some(ninfo); + for (i, entry) in self.entries.iter().enumerate() { + if entry.seq == i as u32 { + entry.pack(writer, big, encoding, info)?; + } else { + let mut entry = entry.clone(); + entry.seq = i as u32; + entry.pack(writer, big, encoding, info)?; + } + } + Ok(()) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +#[serde(transparent)] +struct FileSystemTime(DateTime); + +impl Deref for FileSystemTime { + type Target = DateTime; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for FileSystemTime { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl std::fmt::Display for FileSystemTime { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + self.0.fmt(f) + } +} + +impl StructUnpack for FileSystemTime { + fn unpack( + reader: &mut R, + big: bool, + encoding: Encoding, + info: &Option>, + ) -> Result { + let high = u32::unpack(reader, big, encoding, info)?; + let low = u32::unpack(reader, big, encoding, info)?; + let time = low as u64 | ((high as u64) << 32); + const FILETIME_OFFSET: u64 = 116_444_736_000_000_000; + if time < FILETIME_OFFSET { + anyhow::bail!("Time to small."); + } + let intervals_since_1970 = time - FILETIME_OFFSET; + let seconds = (intervals_since_1970 / 10_000_000) as i64; + let nsecs = ((intervals_since_1970 % 10_000_000) * 100) as u32; + let time = Utc + .timestamp_opt(seconds, nsecs) + .single() + .ok_or_else(|| anyhow::anyhow!("Time is not existed or ambiguous."))?; + let time = time.with_timezone(&Local); + Ok(Self(time)) + } +} + +impl StructPack for FileSystemTime { + fn pack( + &self, + writer: &mut W, + big: bool, + encoding: Encoding, + info: &Option>, + ) -> Result<()> { + let time = self.0.with_timezone(&Utc); + let tseconds = time.timestamp(); + let nsecs = time.nanosecond() / 100; + const FILETIME_OFFSET: u64 = 116_444_736_000_000_000; + let seconds = (tseconds as u64) + .checked_mul(10_000_000) + .ok_or_else(|| anyhow::anyhow!("Too big time"))? + .checked_add(nsecs as u64) + .ok_or_else(|| anyhow::anyhow!("Too big time"))? + .checked_add(FILETIME_OFFSET) + .ok_or_else(|| anyhow::anyhow!("Too big time"))?; + let high = (seconds >> 32) as u32; + let low = seconds as u32; + high.pack(writer, big, encoding, info)?; + low.pack(writer, big, encoding, info)?; + Ok(()) + } +} + +#[derive(Debug, Clone, Deserialize, Serialize, StructUnpack, StructPack)] +struct YSTLEntry { + seq: u32, + #[pstring(u32)] + path: String, + modification_time: FileSystemTime, + num_variables: u32, + num_labels: u32, + // TODO: version may need more check + #[skip_pack_if(get_info_as_version(__info)? < 300)] + #[skip_unpack_if(get_info_as_version(__info)? < 300)] + num_texts: u32, +} + +fn get_info_as_version(info: &Option>) -> Result { + Ok(*info + .as_ref() + .ok_or_else(|| anyhow::anyhow!("info not found"))? + .downcast_ref() + .ok_or_else(|| anyhow::anyhow!("not YSTBHeader"))?) +} + +#[derive(Debug)] +pub struct YSTLBuilder {} + +impl YSTLBuilder { + /// Creates a new instance of `YSTLBuilder` + pub const fn new() -> Self { + YSTLBuilder {} + } +} + +impl ScriptBuilder for YSTLBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Cp932 + } + + fn build_script( + &self, + buf: Vec, + _filename: &str, + encoding: Encoding, + _archive_encoding: Encoding, + config: &ExtraConfig, + _archive: Option<&Box>, + ) -> Result> { + Ok(Box::new(YSTL::new(MemReader::new(buf), encoding, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["ybn"] + } + + fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { + if buf_len >= 4 && buf.starts_with(b"YSTL") { + return Some(20); + } + None + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::YurisYSTL + } + + fn can_create_file(&self) -> bool { + true + } + + fn create_file<'a>( + &'a self, + filename: &'a str, + writer: Box, + encoding: Encoding, + file_encoding: Encoding, + config: &ExtraConfig, + ) -> Result<()> { + create_file( + filename, + writer, + encoding, + file_encoding, + config.custom_yaml, + ) + } +} + +#[derive(Debug)] +pub struct YSTL { + data: YSTLData, + custom_yaml: bool, +} + +impl YSTL { + pub fn new( + mut reader: T, + encoding: Encoding, + config: &ExtraConfig, + ) -> Result { + let mut sig = [0; 4]; + reader.read_exact(&mut sig)?; + if &sig != b"YSTL" { + anyhow::bail!("Unsupported YSTL file."); + } + let data = YSTLData::unpack(&mut reader, false, encoding, &None)?; + Ok(Self { + data, + custom_yaml: config.custom_yaml, + }) + } +} + +impl Script for YSTL { + fn default_output_script_type(&self) -> OutputScriptType { + OutputScriptType::Custom + } + + fn is_output_supported(&self, output: OutputScriptType) -> bool { + matches!(output, OutputScriptType::Custom) + } + + fn default_format_type(&self) -> FormatOptions { + FormatOptions::None + } + + fn custom_output_extension(&self) -> &'static str { + if self.custom_yaml { "yaml" } else { "json" } + } + + fn custom_export(&self, filename: &std::path::Path, encoding: Encoding) -> Result<()> { + let s = if self.custom_yaml { + serde_yaml_ng::to_string(&self.data) + .map_err(|e| anyhow::anyhow!("Failed to serialize to YAML: {}", e))? + } else { + serde_json::to_string_pretty(&self.data) + .map_err(|e| anyhow::anyhow!("Failed to serialize to JSON: {}", e))? + }; + let mut writer = crate::utils::files::write_file(filename)?; + let s = encode_string(encoding, &s, false)?; + writer.write_all(&s)?; + writer.flush()?; + Ok(()) + } + + fn custom_import<'a>( + &'a self, + custom_filename: &'a str, + file: Box, + encoding: Encoding, + output_encoding: Encoding, + ) -> Result<()> { + create_file( + custom_filename, + file, + encoding, + output_encoding, + self.custom_yaml, + ) + } +} + +fn create_file<'a>( + custom_filename: &'a str, + mut writer: Box, + encoding: Encoding, + output_encoding: Encoding, + yaml: bool, +) -> Result<()> { + let input = crate::utils::files::read_file(custom_filename)?; + let s = decode_to_string(output_encoding, &input, true)?; + let mut data: YSTLData = if yaml { + serde_yaml_ng::from_str(&s).map_err(|e| anyhow::anyhow!("Failed to parse YAML: {}", e))? + } else { + serde_json::from_str(&s).map_err(|e| anyhow::anyhow!("Failed to parse JSON: {}", e))? + }; + writer.write_all(b"YSTL")?; + writer.write_u32(data.version)?; + writer.write_u32(data.entries.len() as u32)?; + let info = Box::new(data.version) as Box; + let info = &Some(info); + for (i, entry) in data.entries.iter_mut().enumerate() { + entry.seq = i as u32; + entry.pack(&mut writer, false, encoding, info)?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn test_unpack_file_system_time() { + let mut reader = MemReaderRef::new(b" \x0c\xd8\x01\x00k`\xdd"); + let ts = FileSystemTime::unpack(&mut reader, false, Encoding::Cp932, &None).unwrap(); + println!("{}", ts); + let utc = ts.to_utc(); + let t = serde_json::to_string(&utc).unwrap(); + assert_eq!(t, "\"2022-01-18T04:07:10Z\""); + } + #[test] + fn test_pack_file_system_time() { + let ts: FileSystemTime = serde_json::from_str("\"2022-01-18T13:07:10+09:00\"").unwrap(); + let mut buf = [0; 8]; + let mut writer = MemWriterRef::new(&mut buf); + ts.pack(&mut writer, false, Encoding::Cp932, &None).unwrap(); + assert_eq!(&buf, b" \x0c\xd8\x01\x00k`\xdd"); + } +} diff --git a/src/types.rs b/src/types.rs index d71ac9b..fdd1c99 100644 --- a/src/types.rs +++ b/src/types.rs @@ -929,6 +929,8 @@ pub enum ScriptType { #[cfg(feature = "yuris")] /// Yu-Ris scenario text file (.txt) YurisTxt, + /// Yu-Ris YSTL(file list) file (.ybn) + YurisYSTL, #[cfg(feature = "yuris-img")] /// YU-RIS compressed image file (.ydg) YurisYDG,