From bffa3d7c1ae35f76524b96f897aa2b954f933a07 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=91=E5=8F=AF=E8=83=BD=E5=BE=88=E5=BB=A2?= Date: Thu, 29 May 2025 21:35:08 +0800 Subject: [PATCH 1/9] :art: add pedantic clippy warning --- README.md | 2 +- src/bin/mltd/extract.rs | 2 +- src/mltd/asset.rs | 16 +++++-------- src/mltd/extract/audio.rs | 34 +++++++++++++-------------- src/mltd/lib.rs | 5 ++-- src/mltd/manifest.rs | 4 ++++ src/mltd/net/asset_ripper.rs | 45 ++++++++++++++---------------------- src/mltd/net/matsuri_api.rs | 2 +- src/mltd/util.rs | 6 +---- 9 files changed, 49 insertions(+), 67 deletions(-) diff --git a/README.md b/README.md index 016f907..7d731c0 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ Options: The following tools are required: * git -* A rust toolchain ([installation guide](https://www.rust-lang.org/tools/install)) +* Latest stable rust toolchain ([installation guide](https://www.rust-lang.org/tools/install)) * cmake >= 3.6 (for vgmstream) * clang (for bindgen) ([installation guide](https://rust-lang.github.io/rust-bindgen/requirements.html)) * remember to set `LIBCLANG_PATH` environment variable on Windows diff --git a/src/bin/mltd/extract.rs b/src/bin/mltd/extract.rs index 97efd63..e0b045e 100644 --- a/src/bin/mltd/extract.rs +++ b/src/bin/mltd/extract.rs @@ -284,7 +284,7 @@ async fn extract_texture2d_assets( let mut async_reader = asset_ripper .asset_image(bundle_no, collection_no, texture_info.entry.0) .await? - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + .map_err(|e| std::io::Error::other(e)) .into_async_read() .compat(); tokio::io::copy(&mut async_reader, &mut image).await?; diff --git a/src/mltd/asset.rs b/src/mltd/asset.rs index efcbd9e..fa10da7 100644 --- a/src/mltd/asset.rs +++ b/src/mltd/asset.rs @@ -117,11 +117,8 @@ impl Asset<'_> { log::debug!("download {} to buf", asset_info.filename); - let stream_reader = res - .bytes_stream() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) - .into_async_read() - .compat(); + let stream_reader = + res.bytes_stream().map_err(|e| io::Error::other(e)).into_async_read().compat(); let mut stream_reader = ProgressReadAdapter::new(stream_reader, progress_bar); @@ -176,11 +173,8 @@ impl Asset<'_> { log::debug!("save asset to {}", output.display()); - let stream_reader = res - .bytes_stream() - .map_err(|e| io::Error::new(io::ErrorKind::Other, e)) - .into_async_read() - .compat(); + let stream_reader = + res.bytes_stream().map_err(|e| io::Error::other(e)).into_async_read().compat(); let mut stream_reader = ProgressReadAdapter::new(stream_reader, progress_bar); @@ -205,6 +199,7 @@ pub enum Platform { impl Platform { /// Returns the string representation of the [`Platform`]. + #[must_use] pub fn as_str(&self) -> &str { match self { Self::Android => "Android", @@ -213,6 +208,7 @@ impl Platform { } /// Returns the `User-Agent` string of the [`Platform`] in HTTP request. + #[must_use] pub fn user_agent(&self) -> &str { match self { Self::Android => "UnityPlayer/2020.3.32f1 (UnityWebRequest/1.0, libcurl/7.80.0-DEV)", diff --git a/src/mltd/extract/audio.rs b/src/mltd/extract/audio.rs index e4d09ee..0501344 100644 --- a/src/mltd/extract/audio.rs +++ b/src/mltd/extract/audio.rs @@ -1,5 +1,7 @@ //! Audio transcoding. +#![allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap, clippy::cast_sign_loss)] + use std::collections::VecDeque; use std::ffi::{c_int, c_uint}; use std::path::Path; @@ -11,7 +13,7 @@ use vgmstream::{StreamFile, VgmStream}; use crate::Error; /// HCA key used to decrypt MLTD audio asset. -pub const MLTD_HCA_KEY: u64 = 765765765765765; +pub const MLTD_HCA_KEY: u64 = 765_765_765_765_765; /// An encoder that transcodes game audio to the target codec. pub struct Encoder<'a> { @@ -100,7 +102,7 @@ impl<'a> Encoder<'a> { return Err(Error::OutOfRange(subsong_index, acb_fmt.subsong_count as usize)); } - log::trace!("audio format: {:#?}", acb_fmt); + log::trace!("audio format: {acb_fmt:#?}"); let codec = ffmpeg_next::encoder::find_by_name(output_options.codec) .ok_or(Error::Generic(String::from("Failed to find encoder")))?; @@ -108,13 +110,13 @@ impl<'a> Encoder<'a> { let mut encoder = ffmpeg_next::codec::Context::new_with_codec(codec).encoder().audio()?; let supported_formats = get_supported_formats(&encoder)?; - log::trace!("supported formats: {:?}", supported_formats); + log::trace!("supported formats: {supported_formats:?}"); let from_sample_format = to_ffmpeg_sample_format(acb_fmt.sample_format)?; let from_channel_layout = to_ffmpeg_channel_layout(acb_fmt.channel_layout)?; encoder.set_format(choose_format(&supported_formats, from_sample_format)); - encoder.set_bit_rate(320000); + encoder.set_bit_rate(320_000); encoder.set_compression(Some(12)); encoder.set_rate(acb_fmt.sample_rate); encoder.set_channel_layout(from_channel_layout); @@ -154,20 +156,19 @@ impl<'a> Encoder<'a> { let _ = output.add_stream_with(&encoder.0.0.0)?; - let frame_size = match encoder + let frame_size = if encoder .codec() .unwrap() .capabilities() .intersects(ffmpeg_next::codec::Capabilities::VARIABLE_FRAME_SIZE) { - true => { - log::trace!( - "variable frame size detected, using default frame size ({})", - Self::DEFAULT_FRAME_SIZE - ); + log::trace!( + "variable frame size detected, using default frame size ({})", Self::DEFAULT_FRAME_SIZE - } - false => encoder.frame_size(), + ); + Self::DEFAULT_FRAME_SIZE + } else { + encoder.frame_size() } as usize; let mut frame = @@ -202,10 +203,7 @@ impl<'a> Encoder<'a> { /// /// Returns `false` if there is more audio data to encode. fn write_frame(&mut self, eof: bool) -> Result { - match eof { - false => self.encoder.send_frame(&self.frame), - true => self.encoder.send_eof(), - }?; + if eof { self.encoder.send_eof() } else { self.encoder.send_frame(&self.frame) }?; loop { let mut packet = ffmpeg_next::Packet::empty(); @@ -339,7 +337,7 @@ impl<'a> Encoder<'a> { let _ = self.output.write_header_with(o.clone())?; } None => self.output.write_header()?, - }; + } while self.write_audio_frame()? {} @@ -381,7 +379,7 @@ fn to_ffmpeg_channel_layout( vgmstream::ChannelMapping::_7POINT1_SURROUND => { Ok(ffmpeg_next::ChannelLayout::_7POINT1_WIDE) } - _ => Err(Error::Generic(format!("Unsupported channel layout: {:?}", value))), + _ => Err(Error::Generic(format!("Unsupported channel layout: {value:?}"))), } } diff --git a/src/mltd/lib.rs b/src/mltd/lib.rs index 17c0fc4..4586829 100644 --- a/src/mltd/lib.rs +++ b/src/mltd/lib.rs @@ -4,9 +4,8 @@ //! //! [github]: https://img.shields.io/badge/github-333333?style=for-the-badge&labelColor=555555&logo=github -#![warn(clippy::print_stderr)] -#![warn(clippy::print_stdout)] -#![warn(missing_docs)] +#![warn(clippy::all, clippy::pedantic)] +#![allow(clippy::doc_markdown, clippy::similar_names)] pub mod asset; mod error; diff --git a/src/mltd/manifest.rs b/src/mltd/manifest.rs index 644fcd1..96655a8 100644 --- a/src/mltd/manifest.rs +++ b/src/mltd/manifest.rs @@ -42,6 +42,7 @@ impl Manifest { } /// Computes the difference from `other` manifest. + #[must_use] pub fn diff<'a>(&'a self, other: &'a Manifest) -> ManifestDiff<'a> { let mut diff = ManifestDiff::new(); @@ -67,18 +68,21 @@ impl Manifest { /// Returns the number of entries in the manifest. #[inline] + #[must_use] pub fn len(&self) -> usize { self.data[0].len() } /// Returns `true` if the manifest is empty. #[inline] + #[must_use] pub fn is_empty(&self) -> bool { self.len() == 0 } /// Returns the total size of all assets in the manifest. #[inline] + #[must_use] pub fn asset_size(&self) -> usize { self.data[0].values().fold(0, |acc, v| acc + v.2) } diff --git a/src/mltd/net/asset_ripper.rs b/src/mltd/net/asset_ripper.rs index 899d9cb..1ff9e11 100644 --- a/src/mltd/net/asset_ripper.rs +++ b/src/mltd/net/asset_ripper.rs @@ -70,12 +70,12 @@ impl AssetRipper { Err(err) => return Err(err.into()), }; - Ok(Self { base_url: format!("http://localhost:{}", port), process: Some(process) }) + Ok(Self { base_url: format!("http://localhost:{port}"), process: Some(process) }) } /// Connects th an existing AssetRipper instance with the given host and port. pub fn connect(host: &str, port: u16) -> Result { - Ok(Self { base_url: format!("http://{}:{}", host, port), process: None }) + Ok(Self { base_url: format!("http://{host}:{port}"), process: None }) } /// Loads an asset or a folder into the AssetRipper. @@ -83,10 +83,7 @@ impl AssetRipper { where P: AsRef, { - let url = match path.as_ref().is_dir() { - true => "LoadFolder", - false => "LoadFile", - }; + let url = if path.as_ref().is_dir() { "LoadFolder" } else { "LoadFile" }; let url = format!("{}/{}", &self.base_url, url); let mut form = HashMap::new(); @@ -143,7 +140,7 @@ impl AssetRipper { /// Returns a list of collections in the specified bundle. pub async fn collections(&mut self, bundle_no: usize) -> Result, Error> { - let path = format!(r#"{{"P":[{}]}}"#, bundle_no); + let path = format!(r#"{{"P":[{bundle_no}]}}"#); let html = match self.send_request("Bundles/View", &path).await?.text().await { Ok(html) => html, @@ -163,7 +160,7 @@ impl AssetRipper { bundle_no: usize, collection_no: usize, ) -> Result, Error> { - let path = format!(r#"{{"B":{{"P":[{}]}},"I":{}}}"#, bundle_no, collection_no); + let path = format!(r#"{{"B":{{"P":[{bundle_no}]}},"I":{collection_no}}}"#); let html = match self.send_request("Collections/View", &path).await?.text().await { Ok(html) => html, @@ -212,7 +209,7 @@ impl AssetRipper { bundle_no: usize, collection_no: usize, ) -> Result { - let path = format!(r#"{{"B":{{"P":[{}]}},"I":{}}}"#, bundle_no, collection_no); + let path = format!(r#"{{"B":{{"P":[{bundle_no}]}},"I":{collection_no}}}"#); let text = match self.send_request("Bundles/View", &path).await?.text().await { Ok(text) => text, @@ -229,10 +226,8 @@ impl AssetRipper { collection_no: usize, path_id: i64, ) -> Result { - let path = format!( - r#"{{"C":{{"B":{{"P":[{}]}},"I":{}}},"D":{}}}"#, - bundle_no, collection_no, path_id - ); + let path = + format!(r#"{{"C":{{"B":{{"P":[{bundle_no}]}},"I":{collection_no}}},"D":{path_id}}}"#); let html = match self.send_request("Assets/View", &path).await?.text().await { Ok(html) => html, @@ -270,10 +265,8 @@ impl AssetRipper { collection_no: usize, path_id: i64, ) -> Result { - let path = format!( - r#"{{"C":{{"B":{{"P":[{}]}},"I":{}}},"D":{}}}"#, - bundle_no, collection_no, path_id - ); + let path = + format!(r#"{{"C":{{"B":{{"P":[{bundle_no}]}},"I":{collection_no}}},"D":{path_id}}}"#); match self.send_request("Assets/Json", &path).await?.json().await { Ok(json) => Ok(json), @@ -288,10 +281,8 @@ impl AssetRipper { collection_no: usize, path_id: i64, ) -> Result>, Error> { - let path = format!( - r#"{{"C":{{"B":{{"P":[{}]}},"I":{}}},"D":{}}}"#, - bundle_no, collection_no, path_id - ); + let path = + format!(r#"{{"C":{{"B":{{"P":[{bundle_no}]}},"I":{collection_no}}},"D":{path_id}}}"#); Ok(self.send_request("Assets/Text", &path).await?.bytes_stream()) } @@ -303,10 +294,8 @@ impl AssetRipper { collection_no: usize, path_id: i64, ) -> Result> + use<>, Error> { - let path = format!( - r#"{{"C":{{"B":{{"P":[{}]}},"I":{}}},"D":{}}}"#, - bundle_no, collection_no, path_id - ); + let path = + format!(r#"{{"C":{{"B":{{"P":[{bundle_no}]}},"I":{collection_no}}},"D":{path_id}}}"#); let url = format!("{}/Assets/Image", &self.base_url); let client = reqwest::Client::new(); @@ -347,7 +336,7 @@ impl AssetRipper { match process.try_wait() { Ok(None) => Ok(()), Ok(Some(status)) => { - Err(Error::Generic(format!("AssetRipper process died with status {}", status))) + Err(Error::Generic(format!("AssetRipper process died with status {status}"))) } Err(err) => Err(err.into()), }?; @@ -381,7 +370,7 @@ impl AssetRipper { "aarch64" => "arm64", _ => return Err(Error::Generic("unsupported architecture".to_string())), }; - let req = client.get(format!("{}/AssetRipper_{}_{}.zip", base_url, os, arch)); + let req = client.get(format!("{base_url}/AssetRipper_{os}_{arch}.zip")); let res = match req.send().await { Ok(res) => res, @@ -390,7 +379,7 @@ impl AssetRipper { let stream_reader = res .bytes_stream() - .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e)) + .map_err(|e| std::io::Error::other(e)) .into_async_read() .compat(); diff --git a/src/mltd/net/matsuri_api.rs b/src/mltd/net/matsuri_api.rs index 0d7ad96..6fe1274 100644 --- a/src/mltd/net/matsuri_api.rs +++ b/src/mltd/net/matsuri_api.rs @@ -144,7 +144,7 @@ pub async fn get_all_asset_versions() -> Result, Error> { pub async fn get_asset_version(version: u64) -> Result { let client = reqwest::Client::new(); let req = client - .get(format!("{}/version/assets/{}", MATSURI_API_ENDPOINT, version)) + .get(format!("{MATSURI_API_ENDPOINT}/version/assets/{version}")) .query(&[("prettyPrint", "false")]); let res = match req.send().await { diff --git a/src/mltd/util.rs b/src/mltd/util.rs index 6a210c4..5a2a440 100644 --- a/src/mltd/util.rs +++ b/src/mltd/util.rs @@ -28,11 +28,7 @@ pub fn log_formatter(buf: &mut Formatter, record: &Record) -> Result<()> { let timestamp = buf.timestamp_micros(); let body = record.args(); - writeln!( - buf, - "[\x1b[3{}m{}\x1b[0m]{} {} {} - {}", - color_code, level, space, timestamp, target, body - ) + writeln!(buf, "[\x1b[3{color_code}m{level}\x1b[0m]{space} {timestamp} {target} - {body}") } /// Adapter for [`tokio::io::AsyncRead`] to show progress. From 27ab0e0f828fab62aed10607dca66c07ff41324e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=91=E5=8F=AF=E8=83=BD=E5=BE=88=E5=BB=A2?= Date: Thu, 29 May 2025 21:35:36 +0800 Subject: [PATCH 2/9] :arrow_up: upgrade dependencies --- Cargo.lock | 471 ++++++++++++++++++------------------ Cargo.toml | 22 +- crates/vgmstream/Cargo.toml | 2 +- 3 files changed, 252 insertions(+), 243 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b2c03fe..5851d6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -78,20 +78,20 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "3.0.7" +version = "3.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e" +checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" dependencies = [ "anstyle", - "once_cell", + "once_cell_polyfill", "windows-sys 0.59.0", ] [[package]] name = "anyhow" -version = "1.0.97" +version = "1.0.98" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" dependencies = [ "backtrace", ] @@ -107,9 +107,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64" +checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07" dependencies = [ "flate2", "futures-core", @@ -156,9 +156,9 @@ checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" [[package]] name = "backtrace" -version = "0.3.74" +version = "0.3.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" dependencies = [ "addr2line", "cfg-if", @@ -181,7 +181,7 @@ version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cexpr", "clang-sys", "itertools", @@ -199,7 +199,7 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cexpr", "clang-sys", "itertools", @@ -221,9 +221,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" [[package]] name = "block-padding" @@ -242,9 +242,9 @@ checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" [[package]] name = "bytemuck" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6b1fc10dbac614ebc03540c9dbd60e83887fda27794998c6528f1782047d540" +checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" [[package]] name = "byteorder" @@ -307,9 +307,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.17" +version = "1.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fcb57c740ae1daf453ae85f16e37396f672b039e00d9d866e07ddb24e328e3a" +checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" dependencies = [ "jobserver", "libc", @@ -354,9 +354,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.35" +version = "4.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8aa86934b44c19c50f87cc2790e19f54f7a67aedb64101c2e1a2e5ecfb73944" +checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" dependencies = [ "clap_builder", "clap_derive", @@ -364,9 +364,9 @@ dependencies = [ [[package]] name = "clap-verbosity-flag" -version = "3.0.2" +version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2678fade3b77aa3a8ff3aae87e9c008d3fb00473a41c71fbf74e91c8c7b37e84" +checksum = "eeab6a5cdfc795a05538422012f20a5496f050223c91be4e5420bfd13c641fb1" dependencies = [ "clap", "log", @@ -374,9 +374,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.35" +version = "4.5.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2414dbb2dd0695280da6ea9261e327479e9d37b0630f6b53ba2a11c60c679fd9" +checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" dependencies = [ "anstream", "anstyle", @@ -466,12 +466,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - [[package]] name = "crypto-common" version = "0.1.6" @@ -507,9 +501,9 @@ dependencies = [ [[package]] name = "ctor" -version = "0.4.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07e9666f4a9a948d4f1dff0c08a4512b0f7c86414b23960104c243c10d79f4c3" +checksum = "a4735f265ba6a1188052ca32d461028a7d1125868be18e287e756019da7607b5" dependencies = [ "ctor-proc-macro", "dtor", @@ -558,9 +552,9 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.1" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" dependencies = [ "powerfmt", ] @@ -609,9 +603,9 @@ dependencies = [ [[package]] name = "derive_more" -version = "0.99.19" +version = "0.99.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da29a38df43d6f156149c9b43ded5e018ddff2a855cf2cfd62e8cd7d079c69f" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" dependencies = [ "proc-macro2", "quote", @@ -646,9 +640,9 @@ dependencies = [ [[package]] name = "dtor" -version = "0.0.5" +version = "0.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "222ef136a1c687d4aa0395c175f2c4586e379924c352fd02f7870cf7de783c23" +checksum = "97cbdf2ad6846025e8e25df05171abfb30e3ababa12ee0a0e44b9bbe570633a8" dependencies = [ "dtor-proc-macro", ] @@ -714,9 +708,9 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.11" +version = "0.3.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" +checksum = "cea14ef9355e3beab063703aa9dab15afd25f0667c341310c1e5274bb1d0da18" dependencies = [ "libc", "windows-sys 0.59.0", @@ -743,16 +737,16 @@ version = "7.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "da02698288e0275e442a47fc12ca26d50daf0d48b15398ba5906f20ac2e2a9f9" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "ffmpeg-sys-next", "libc", ] [[package]] name = "ffmpeg-sys-next" -version = "7.1.0" +version = "7.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc3234d0a4b2f7d083699d0860c6c9dd83713908771b60f94a96f8704adfe45" +checksum = "f9e9c75ebd4463de9d8998fb134ba26347fe5faee62fabf0a4b4d41bd500b4ad" dependencies = [ "bindgen 0.70.1", "cc", @@ -769,6 +763,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" dependencies = [ "crc32fast", + "libz-rs-sys", "miniz_oxide", ] @@ -931,9 +926,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.15" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", @@ -942,9 +937,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" dependencies = [ "cfg-if", "libc", @@ -966,9 +961,9 @@ checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "h2" -version = "0.4.8" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5017294ff4bb30944501348f6f8e42e6ad28f42c8bbef7a74029aff064a4e3c2" +checksum = "a9421a676d1b147b16b82c9225157dc629087ef8ec4d5e2960f9437a90dac0a5" dependencies = [ "atomic-waker", "bytes", @@ -985,9 +980,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.2" +version = "0.15.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" +checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" [[package]] name = "heck" @@ -1081,11 +1076,10 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.5" +version = "0.27.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" dependencies = [ - "futures-util", "http", "hyper", "hyper-util", @@ -1114,41 +1108,48 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.11" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "497bbc33a26fdd4af9ed9c70d63f61cf56a938375fbb32df34db9b1cd6d643f2" +checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" dependencies = [ + "base64", "bytes", "futures-channel", + "futures-core", "futures-util", "http", "http-body", "hyper", + "ipnet", "libc", + "percent-encoding", "pin-project-lite", "socket2", + "system-configuration", "tokio", "tower-service", "tracing", + "windows-registry", ] [[package]] name = "icu_collections" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" dependencies = [ "displaydoc", + "potential_utf", "yoke", "zerofrom", "zerovec", ] [[package]] -name = "icu_locid" -version = "1.5.0" +name = "icu_locale_core" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" dependencies = [ "displaydoc", "litemap", @@ -1157,31 +1158,11 @@ dependencies = [ "zerovec", ] -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - [[package]] name = "icu_normalizer" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" dependencies = [ "displaydoc", "icu_collections", @@ -1189,67 +1170,54 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", - "utf16_iter", - "utf8_iter", - "write16", "zerovec", ] [[package]] name = "icu_normalizer_data" -version = "1.5.1" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" [[package]] name = "icu_properties" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" dependencies = [ "displaydoc", "icu_collections", - "icu_locid_transform", + "icu_locale_core", "icu_properties_data", "icu_provider", - "tinystr", + "potential_utf", + "zerotrie", "zerovec", ] [[package]] name = "icu_properties_data" -version = "1.5.1" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" [[package]] name = "icu_provider" -version = "1.5.0" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" dependencies = [ "displaydoc", - "icu_locid", - "icu_provider_macros", + "icu_locale_core", "stable_deref_trait", "tinystr", "writeable", "yoke", "zerofrom", + "zerotrie", "zerovec", ] -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "ident_case" version = "1.0.1" @@ -1269,9 +1237,9 @@ dependencies = [ [[package]] name = "idna_adapter" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" dependencies = [ "icu_normalizer", "icu_properties", @@ -1305,9 +1273,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.8.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" +checksum = "cea70ddb795996207ad57735b50c5982d8844f38ba9ee5f1aedcfb708a2aa11e" dependencies = [ "equivalent", "hashbrown", @@ -1341,6 +1309,16 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" +[[package]] +name = "iri-string" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -1364,9 +1342,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.5" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c102670231191d07d37a35af3eb77f1f0dbf7a71be51a962dcd57ea607be7260" +checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" dependencies = [ "jiff-static", "log", @@ -1377,9 +1355,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.5" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cdde31a9d349f1b1f51a0b3714a5940ac022976f4b49485fc04be052b183b4c" +checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" dependencies = [ "proc-macro2", "quote", @@ -1392,7 +1370,7 @@ version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" dependencies = [ - "getrandom 0.3.2", + "getrandom 0.3.3", "libc", ] @@ -1408,18 +1386,27 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.171" +version = "0.2.172" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6" +checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" [[package]] name = "libloading" -version = "0.8.6" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.53.0", +] + +[[package]] +name = "libz-rs-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a" +dependencies = [ + "zlib-rs", ] [[package]] @@ -1433,15 +1420,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" [[package]] name = "litemap" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" @@ -1453,12 +1440,6 @@ dependencies = [ "scopeguard", ] -[[package]] -name = "lockfree-object-pool" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9374ef4228402d4b7e403e5838cb880d9ee663314b0a900d5a6aabf0c213552e" - [[package]] name = "log" version = "0.4.27" @@ -1516,9 +1497,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.7" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff70ce3e48ae43fa075863cef62e8b43b71a4f2382229920e0df362592919430" +checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" dependencies = [ "adler2", "simd-adler32", @@ -1526,13 +1507,13 @@ dependencies = [ [[package]] name = "mio" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.52.0", + "windows-sys 0.59.0", ] [[package]] @@ -1673,13 +1654,19 @@ version = "1.21.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +[[package]] +name = "once_cell_polyfill" +version = "1.70.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" + [[package]] name = "openssl" -version = "0.10.72" +version = "0.10.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fedfea7d58a1f73118430a55da6a286e7b044961736ce96a16a17068ea25e5da" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cfg-if", "foreign-types", "libc", @@ -1707,9 +1694,9 @@ checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" [[package]] name = "openssl-sys" -version = "0.9.107" +version = "0.9.109" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8288979acd84749c744a9014b4382d42b8f7b2592847b5afb2ed29e5d16ede07" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", @@ -1870,6 +1857,15 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1884,9 +1880,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "prettyplease" -version = "0.2.31" +version = "0.2.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" +checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" dependencies = [ "proc-macro2", "syn", @@ -1894,9 +1890,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.94" +version = "1.0.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" dependencies = [ "unicode-ident", ] @@ -1939,11 +1935,11 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "redox_syscall" -version = "0.5.10" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" +checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] [[package]] @@ -1977,9 +1973,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.15" +version = "0.12.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb" +checksum = "e98ff6b0dbbe4d5a37318f433d4fc82babd21631f194d370409ceb2e40b2f0b5" dependencies = [ "async-compression", "base64", @@ -2003,23 +1999,22 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-native-tls", "tokio-util", "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "wasm-streams", "web-sys", - "windows-registry", ] [[package]] @@ -2030,7 +2025,7 @@ checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" dependencies = [ "cc", "cfg-if", - "getrandom 0.2.15", + "getrandom 0.2.16", "libc", "untrusted", "windows-sys 0.52.0", @@ -2087,11 +2082,11 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.5" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" +checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "errno", "libc", "linux-raw-sys", @@ -2100,9 +2095,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.25" +version = "0.23.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "822ee9188ac4ec04a2f0531e55d035fb2de73f18b41a63c70c2712503b6fb13c" +checksum = "730944ca083c1c233a75c09f199e973ca499344a2b7ba9e755c457e86fb4a321" dependencies = [ "once_cell", "rustls-pki-types", @@ -2112,25 +2107,19 @@ dependencies = [ ] [[package]] -name = "rustls-pemfile" -version = "2.2.0" +name = "rustls-pki-types" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" dependencies = [ - "rustls-pki-types", + "zeroize", ] -[[package]] -name = "rustls-pki-types" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "917ce264624a4b4db1c364dcc35bfca9ded014d0a958cd47ad3e960e988ea51c" - [[package]] name = "rustls-webpki" -version = "0.103.1" +version = "0.103.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fef8b8769aaccf73098557a87cd1816b4f9c7c16811c9c77142aa695c16f2c03" +checksum = "e4a72fe2bcf7a6ac6fd7d0b9e5cb68aeb7d4c0a0271730218b3e92d43b4eb435" dependencies = [ "ring", "rustls-pki-types", @@ -2139,9 +2128,9 @@ dependencies = [ [[package]] name = "rustversion" -version = "1.0.20" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" +checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d" [[package]] name = "ryu" @@ -2185,7 +2174,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "core-foundation", "core-foundation-sys", "libc", @@ -2208,7 +2197,7 @@ version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fd568a4c9bb598e291a08244a5c1f5a8a6650bee243b5b0f8dbb3d9cc1d87fe8" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "cssparser", "derive_more", "fxhash", @@ -2312,15 +2301,15 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" +checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" [[package]] name = "socket2" -version = "0.5.9" +version = "0.5.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" dependencies = [ "libc", "windows-sys 0.52.0", @@ -2371,9 +2360,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.100" +version = "2.0.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" +checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" dependencies = [ "proc-macro2", "quote", @@ -2391,9 +2380,9 @@ dependencies = [ [[package]] name = "synstructure" -version = "0.13.1" +version = "0.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2", "quote", @@ -2406,7 +2395,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "core-foundation", "system-configuration-sys", ] @@ -2423,12 +2412,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.19.1" +version = "3.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" dependencies = [ "fastrand", - "getrandom 0.3.2", + "getrandom 0.3.3", "once_cell", "rustix", "windows-sys 0.59.0", @@ -2510,9 +2499,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.7.6" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" dependencies = [ "displaydoc", "zerovec", @@ -2520,9 +2509,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.44.1" +version = "1.45.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f382da615b842244d4b8738c82ed1275e6c5dd90c459a30941cd07080b06c91a" +checksum = "75ef51a33ef1da925cea3e4eb122833cb377c61439ca401b770f54902b806779" dependencies = [ "backtrace", "bytes", @@ -2591,9 +2580,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" +checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df" dependencies = [ "bytes", "futures-core", @@ -2618,6 +2607,24 @@ dependencies = [ "tower-service", ] +[[package]] +name = "tower-http" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "futures-util", + "http", + "http-body", + "iri-string", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -2708,12 +2715,6 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - [[package]] name = "utf8_iter" version = "1.0.4" @@ -2734,9 +2735,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "vergen" -version = "9.0.4" +version = "9.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0d2f179f8075b805a43a2a21728a46f0cc2921b3c58695b28fa8817e103cd9a" +checksum = "6b2bf58be11fc9414104c6d3a2e464163db5ef74b12296bda593cac37b6e4777" dependencies = [ "anyhow", "cargo_metadata", @@ -2749,9 +2750,9 @@ dependencies = [ [[package]] name = "vergen-gitcl" -version = "1.0.5" +version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2f89d70a58a4506a6079cedf575c64cf51649ccbb4e02a63dac539b264b7711" +checksum = "b9dfc1de6eb2e08a4ddf152f1b179529638bedc0ea95e6d667c014506377aefe" dependencies = [ "anyhow", "derive_builder", @@ -2782,7 +2783,7 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" name = "vgmstream" version = "0.1.0" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", "num-derive", "num-traits", "thiserror", @@ -2944,9 +2945,9 @@ dependencies = [ [[package]] name = "windows-result" -version = "0.3.2" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c64fd11a4fd95df68efcfee5f44a294fe71b8bc6a91993e2791938abcc712252" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ "windows-link", ] @@ -3112,26 +3113,20 @@ version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.9.0", + "bitflags 2.9.1", ] -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - [[package]] name = "writeable" -version = "0.5.5" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" [[package]] name = "yoke" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" dependencies = [ "serde", "stable_deref_trait", @@ -3141,9 +3136,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" dependencies = [ "proc-macro2", "quote", @@ -3178,11 +3173,22 @@ version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + [[package]] name = "zerovec" -version = "0.10.4" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428" dependencies = [ "yoke", "zerofrom", @@ -3191,9 +3197,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.10.3" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" dependencies = [ "proc-macro2", "quote", @@ -3202,30 +3208,33 @@ dependencies = [ [[package]] name = "zip" -version = "2.6.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "febbe83a485467affa75a75d28dc7494acd2f819e549536c47d46b3089b56164" +checksum = "153a6fff49d264c4babdcfa6b4d534747f520e56e8f0f384f3b808c4b64cc1fd" dependencies = [ "arbitrary", "crc32fast", - "crossbeam-utils", "flate2", "indexmap", "memchr", "zopfli", ] +[[package]] +name = "zlib-rs" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8" + [[package]] name = "zopfli" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5019f391bac5cf252e93bbcc53d039ffd62c7bfb7c150414d61369afe57e946" +checksum = "edfc5ee405f504cd4984ecc6f14d02d55cfda60fa4b689434ef4102aae150cd7" dependencies = [ "bumpalo", "crc32fast", - "lockfree-object-pool", "log", - "once_cell", "simd-adler32", ] diff --git a/Cargo.toml b/Cargo.toml index 9cb3e26..a344202 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,11 +23,11 @@ version = "3.0.0-alpha.3" [dependencies] aes = { optional = true, version = "0.8.4" } -anyhow = { features = ["backtrace"], version = "1.0.97" } +anyhow = { features = ["backtrace"], version = "1.0.98" } bytes = { optional = true, version = "1.10.1" } cbc = { optional = true, version = "0.1.2" } -clap = { features = ["color", "deprecated", "derive", "unicode", "wrap_help"], version = "4.5.35" } -clap-verbosity-flag = "3.0.2" +clap = { features = ["color", "deprecated", "derive", "unicode", "wrap_help"], version = "4.5.39" } +clap-verbosity-flag = "3.0.3" env_logger = { default-features = false, features = ["humantime"], version = "0.11.8" } ffmpeg-next = { default-features = false, features = ["codec", "format", "software-resampling"], optional = true, version = "7.1.0" } futures = "0.3.31" @@ -40,24 +40,24 @@ log = "0.4.27" num_cpus = { optional = true, version = "1.16.0" } pin-project = "1.1.10" regex = { default-features = false, features = ["perf", "std"], optional = true, version = "1.11.1" } -reqwest = { features = ["deflate", "gzip", "json", "stream", "zstd"], version = "0.12.15" } +reqwest = { features = ["deflate", "gzip", "json", "stream", "zstd"], version = "0.12.18" } rmp-serde = "1.3.0" scraper = { optional = true, version = "0.23.1" } serde = { features = ["derive"], version = "1.0.219" } serde_json = { optional = true, version = "1.0.140" } -tempfile = { optional = true, version = "3.19.1" } +tempfile = { optional = true, version = "3.20.0" } thiserror = "2.0.12" -tokio = { features = ["macros", "rt-multi-thread"], version = "1.44.1" } -tokio-util = { features = ["compat"], version = "0.7.14" } +tokio = { features = ["macros", "rt-multi-thread"], version = "1.45.1" } +tokio-util = { features = ["compat"], version = "0.7.15" } vgmstream = { optional = true, path = "crates/vgmstream" } -zip = { default-features = false, features = ["deflate"], optional = true, version = "2.6.0" } +zip = { default-features = false, features = ["deflate"], optional = true, version = "4.0.0" } [build-dependencies] -vergen = { features = ["cargo", "emit_and_set", "rustc"], version = "9.0.4" } -vergen-gitcl = { version = "1.0.5" } +vergen = { features = ["cargo", "emit_and_set", "rustc"], version = "9.0.6" } +vergen-gitcl = { version = "1.0.8" } [dev-dependencies] -ctor = "0.4.1" +ctor = "0.4.2" tokio-test = "0.4.4" [lib] diff --git a/crates/vgmstream/Cargo.toml b/crates/vgmstream/Cargo.toml index e8be189..38bb254 100644 --- a/crates/vgmstream/Cargo.toml +++ b/crates/vgmstream/Cargo.toml @@ -7,7 +7,7 @@ license.workspace = true repository.workspace = true [dependencies] -bitflags = "2.9.0" +bitflags = "2.9.1" num-derive = "0.4.2" num-traits = "0.2.19" thiserror = "2.0.12" From 1adbf2bda4dfe966fe162c333005da07e3f3671a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=91=E5=8F=AF=E8=83=BD=E5=BE=88=E5=BB=A2?= Date: Wed, 4 Jun 2025 21:30:07 +0800 Subject: [PATCH 3/9] :arrow_up: update vgmstream --- Cargo.lock | 71 ++-- Cargo.toml | 6 +- crates/vgmstream-sys/Cargo.toml | 2 +- crates/vgmstream-sys/src/lib.rs | 2 +- crates/vgmstream-sys/vgmstream | 2 +- crates/vgmstream/Cargo.toml | 4 +- crates/vgmstream/src/error.rs | 15 +- crates/vgmstream/src/lib.rs | 579 ++++++++++++++++++++++++-------- crates/vgmstream/src/sf.rs | 42 ++- src/bin/mltd/extract.rs | 9 +- src/mltd/extract/audio.rs | 13 +- src/mltd/net/asset_ripper.rs | 41 ++- 12 files changed, 554 insertions(+), 232 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5851d6a..b89ab04 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,9 +266,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" [[package]] name = "camino" -version = "1.1.9" +version = "1.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +checksum = "0da45bc31171d8d6960122e222a67740df867c1dd53b4d51caa297084c185cab" dependencies = [ "serde", ] @@ -307,9 +307,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.24" +version = "1.2.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16595d3be041c03b09d08d0858631facccee9221e579704070e6e9e4915d3bc7" +checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" dependencies = [ "jobserver", "libc", @@ -992,9 +992,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.3.9" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" +checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" [[package]] name = "html5ever" @@ -1432,9 +1432,9 @@ checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" [[package]] name = "lock_api" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" dependencies = [ "autocfg", "scopeguard", @@ -1594,17 +1594,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" -[[package]] -name = "num-derive" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1616,9 +1605,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.16.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" dependencies = [ "hermit-abi", "libc", @@ -1706,9 +1695,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.3" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" dependencies = [ "lock_api", "parking_lot_core", @@ -1716,9 +1705,9 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.10" +version = "0.9.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" dependencies = [ "cfg-if", "libc", @@ -1880,9 +1869,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "prettyplease" -version = "0.2.32" +version = "0.2.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" +checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d" dependencies = [ "proc-macro2", "syn", @@ -1973,9 +1962,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.18" +version = "0.12.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e98ff6b0dbbe4d5a37318f433d4fc82babd21631f194d370409ceb2e40b2f0b5" +checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119" dependencies = [ "async-compression", "base64", @@ -2278,6 +2267,15 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook-registry" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410" +dependencies = [ + "libc", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -2518,6 +2516,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -2609,9 +2608,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.4" +version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdb0c213ca27a9f57ab69ddb290fd80d970922355b83ae380b395d3986b8a2e" +checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ "bitflags 2.9.1", "bytes", @@ -2781,18 +2780,16 @@ checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "vgmstream" -version = "0.1.0" +version = "0.2.0" dependencies = [ "bitflags 2.9.1", - "num-derive", - "num-traits", "thiserror", "vgmstream-sys", ] [[package]] name = "vgmstream-sys" -version = "0.2.0-vgmstream-r1980-219-gcaa00ea4" +version = "0.2.0-vgmstream-r2023-16-gb2fc214a" dependencies = [ "bindgen 0.71.1", "cmake", @@ -3274,9 +3271,9 @@ checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-jpeg" -version = "0.4.14" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99a5bab8d7dedf81405c4bb1f2b83ea057643d9cb28778cea9eecddeedd2e028" +checksum = "b63fd466826ec8fe25e1fc010c169213fec4e135ac39caccdba830eaa3895923" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index a344202..842c4bc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,17 +37,17 @@ image = { default-features = false, features = ["jpeg", "png", "webp", "serde"], indicatif = { default-features = false, version = "0.17.11" } linked-hash-map = { features = ["serde_impl"], version = "0.5.6" } log = "0.4.27" -num_cpus = { optional = true, version = "1.16.0" } +num_cpus = { optional = true, version = "1.17.0" } pin-project = "1.1.10" regex = { default-features = false, features = ["perf", "std"], optional = true, version = "1.11.1" } -reqwest = { features = ["deflate", "gzip", "json", "stream", "zstd"], version = "0.12.18" } +reqwest = { features = ["deflate", "gzip", "json", "stream", "zstd"], version = "0.12.19" } rmp-serde = "1.3.0" scraper = { optional = true, version = "0.23.1" } serde = { features = ["derive"], version = "1.0.219" } serde_json = { optional = true, version = "1.0.140" } tempfile = { optional = true, version = "3.20.0" } thiserror = "2.0.12" -tokio = { features = ["macros", "rt-multi-thread"], version = "1.45.1" } +tokio = { features = ["macros", "process", "rt-multi-thread"], version = "1.45.1" } tokio-util = { features = ["compat"], version = "0.7.15" } vgmstream = { optional = true, path = "crates/vgmstream" } zip = { default-features = false, features = ["deflate"], optional = true, version = "4.0.0" } diff --git a/crates/vgmstream-sys/Cargo.toml b/crates/vgmstream-sys/Cargo.toml index 185f0fc..c699bba 100644 --- a/crates/vgmstream-sys/Cargo.toml +++ b/crates/vgmstream-sys/Cargo.toml @@ -5,7 +5,7 @@ license.workspace = true links = "vgmstream" name = "vgmstream-sys" repository.workspace = true -version = "0.2.0-vgmstream-r1980-219-gcaa00ea4" +version = "0.2.0-vgmstream-r2023-16-gb2fc214a" [build-dependencies] bindgen = "0.71.1" diff --git a/crates/vgmstream-sys/src/lib.rs b/crates/vgmstream-sys/src/lib.rs index 855dba2..8fa55ad 100644 --- a/crates/vgmstream-sys/src/lib.rs +++ b/crates/vgmstream-sys/src/lib.rs @@ -1,4 +1,4 @@ -#![allow(non_camel_case_types)] +#![allow(non_camel_case_types, non_snake_case)] #![allow(non_upper_case_globals)] include!(concat!(env!("OUT_DIR"), "/bindings.rs")); diff --git a/crates/vgmstream-sys/vgmstream b/crates/vgmstream-sys/vgmstream index caa00ea..b2fc214 160000 --- a/crates/vgmstream-sys/vgmstream +++ b/crates/vgmstream-sys/vgmstream @@ -1 +1 @@ -Subproject commit caa00ea4313eb644e134eafbe9641ecbff054a36 +Subproject commit b2fc214ad74a7d1e4e01a7544c2b438edb2ecb59 diff --git a/crates/vgmstream/Cargo.toml b/crates/vgmstream/Cargo.toml index 38bb254..ffac24c 100644 --- a/crates/vgmstream/Cargo.toml +++ b/crates/vgmstream/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "vgmstream" -version = "0.1.0" +version = "0.2.0" authors.workspace = true edition.workspace = true license.workspace = true @@ -8,7 +8,5 @@ repository.workspace = true [dependencies] bitflags = "2.9.1" -num-derive = "0.4.2" -num-traits = "0.2.19" thiserror = "2.0.12" vgmstream-sys = { path = "../vgmstream-sys" } diff --git a/crates/vgmstream/src/error.rs b/crates/vgmstream/src/error.rs index 2319383..ec22bfa 100644 --- a/crates/vgmstream/src/error.rs +++ b/crates/vgmstream/src/error.rs @@ -2,12 +2,15 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum Error { - #[error("vgmstream initialization failed")] - InitializationFailed, + #[error("{0} failed")] + VgmStream(String), - #[error("invalid channel mapping: {0}")] - InvalidChannelMapping(u32), + #[error("unknown channel mapping: {0}")] + UnknownChannelMapping(u32), - #[error("vgmstream generic error")] - Generic, + #[error("unknown sample format: {0}")] + UnknownSampleFormat(u32), + + #[error("{0} is null")] + NullPointer(String), } diff --git a/crates/vgmstream/src/lib.rs b/crates/vgmstream/src/lib.rs index 63a2675..406f851 100644 --- a/crates/vgmstream/src/lib.rs +++ b/crates/vgmstream/src/lib.rs @@ -1,39 +1,51 @@ +//! Rust bindings for low level [`vgmstream_sys`] crate. + mod error; mod sf; use std::ffi::{CStr, c_int}; +use std::ptr::NonNull; use bitflags::bitflags; -use num_derive::{FromPrimitive, ToPrimitive}; -use num_traits::{FromPrimitive, ToPrimitive}; pub use crate::error::Error; pub use crate::sf::StreamFile; -#[derive(Debug, Clone, Copy, PartialEq, FromPrimitive, ToPrimitive)] -pub enum SampleType { +pub use vgmstream_sys; + +/// Rust version of [`vgmstream_sys::libvgmstream_sfmt_t`]. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum SampleFormat { Pcm16 = vgmstream_sys::libvgmstream_sfmt_t_LIBVGMSTREAM_SFMT_PCM16 as isize, - // Pcm24 = vgmstream_sys::libvgmstream_sfmt_t_LIBVGMSTREAM_SFMT_PCM24 as isize, - // Pcm32 = vgmstream_sys::libvgmstream_sfmt_t_LIBVGMSTREAM_SFMT_PCM32 as isize, + Pcm24 = vgmstream_sys::libvgmstream_sfmt_t_LIBVGMSTREAM_SFMT_PCM24 as isize, + Pcm32 = vgmstream_sys::libvgmstream_sfmt_t_LIBVGMSTREAM_SFMT_PCM32 as isize, Float = vgmstream_sys::libvgmstream_sfmt_t_LIBVGMSTREAM_SFMT_FLOAT as isize, } -impl From for SampleType { - fn from(value: vgmstream_sys::libvgmstream_sfmt_t) -> Self { - #[allow(clippy::unnecessary_cast)] // libvgmstream_sfmt_t is i32 on windows - SampleType::from_u32(value as u32).expect("Invalid sample type") +impl TryFrom for SampleFormat { + type Error = Error; + fn try_from(value: vgmstream_sys::libvgmstream_sfmt_t) -> Result { + match value { + vgmstream_sys::libvgmstream_sfmt_t_LIBVGMSTREAM_SFMT_PCM16 => Ok(Self::Pcm16), + vgmstream_sys::libvgmstream_sfmt_t_LIBVGMSTREAM_SFMT_PCM24 => Ok(Self::Pcm24), + vgmstream_sys::libvgmstream_sfmt_t_LIBVGMSTREAM_SFMT_PCM32 => Ok(Self::Pcm32), + vgmstream_sys::libvgmstream_sfmt_t_LIBVGMSTREAM_SFMT_FLOAT => Ok(Self::Float), + _ => Err(Error::UnknownSampleFormat(value as _)), + } } } -impl From for vgmstream_sys::libvgmstream_sfmt_t { - fn from(value: SampleType) -> Self { - value.to_u32().expect("Invalid sample type") as vgmstream_sys::libvgmstream_sfmt_t +impl From for vgmstream_sys::libvgmstream_sfmt_t { + fn from(value: SampleFormat) -> Self { + value as vgmstream_sys::libvgmstream_sfmt_t } } bitflags! { /// The `speaker_t` type in vgmstream. - #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + /// + /// Standard WAVEFORMATEXTENSIBLE speaker positions. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Speaker: u32 { /// front left const FL = (1 << 0); @@ -74,7 +86,12 @@ bitflags! { const TBR = (1 << 17); } - #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + /// The `channel_layout_t` type in vgmstream. + /// + /// Typical mappings that metas may use to set channel_layout (but plugin must actually use it) + /// (in order, so 3ch file could be mapped to FL FR FC or FL FR LFE but not LFE FL FR) + /// Not too sure about names but no clear standards. + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ChannelMapping: u32 { const MONO = Speaker::FC.bits(); const STEREO = Speaker::FL.bits() | Speaker::FR.bits(); @@ -94,12 +111,17 @@ bitflags! { } } +/// A wrapper around the [`vgmstream_sys::libvgmstream_t`] pointer. +/// +/// vgmstream context/handle. pub struct VgmStream { - pub(crate) inner: *mut vgmstream_sys::libvgmstream_t, + pub(crate) inner: NonNull, } -///configures how vgmstream behaves internally when playing a file -#[derive(Debug, Clone)] +/// Rust version of [`vgmstream_sys::libvgmstream_config_t`]. +/// +/// Configures how vgmstream behaves internally when playing a file. +#[derive(Debug, Clone, PartialEq)] pub struct Config { // ignore forced (TXTP) config pub disable_config_override: bool, @@ -136,19 +158,44 @@ pub struct Config { pub auto_downmix_channels: i32, /// forces output buffer to be remixed into some sample format - pub force_sfmt: SampleType, + pub force_sfmt: SampleFormat, +} + +impl From for vgmstream_sys::libvgmstream_config_t { + fn from(value: Config) -> Self { + Self { + disable_config_override: value.disable_config_override, + allow_play_forever: value.allow_play_forever, + + play_forever: value.play_forever, + ignore_loop: value.ignore_loop, + force_loop: value.force_loop, + really_force_loop: value.really_force_loop, + ignore_fade: value.ignore_fade, + + loop_count: value.loop_count, + fade_time: value.fade_time, + fade_delay: value.fade_delay, + + stereo_track: value.stereo_track, + auto_downmix_channels: value.auto_downmix_channels, + force_sfmt: value.force_sfmt.into(), + } + } } -#[derive(Debug, Clone)] +/// Rust version of [`vgmstream_sys::libvgmstream_format_t`]. +/// +/// Current song info, may be copied around (values are info-only) +#[derive(Debug, Clone, PartialEq)] pub struct Format { /* main (always set) */ /// output channels pub channels: i32, /// output sample rate pub sample_rate: i32, - /// output buffer's sample type - pub sample_format: SampleType, + pub sample_format: SampleFormat, /// derived from sample_type (pcm16=0x02, float=0x04, etc) pub sample_size: i32, @@ -162,6 +209,7 @@ pub struct Format { /// N = has N subsongs /// 1 = format has subsongs, and only 1 for current file pub subsong_count: i32, + /// original file's channels before downmixing (if any) pub input_channels: i32, @@ -206,8 +254,46 @@ pub struct Format { pub format_id: i32, } +impl TryFrom for Format { + type Error = Error; + fn try_from(value: vgmstream_sys::libvgmstream_format_t) -> Result { + let codec_name = + unsafe { CStr::from_ptr(value.codec_name.as_ptr()) }.to_string_lossy().to_string(); + let layout_name = + unsafe { CStr::from_ptr(value.layout_name.as_ptr()) }.to_string_lossy().to_string(); + let meta_name = + unsafe { CStr::from_ptr(value.meta_name.as_ptr()) }.to_string_lossy().to_string(); + let stream_name = + unsafe { CStr::from_ptr(value.stream_name.as_ptr()) }.to_string_lossy().to_string(); + + Ok(Self { + channels: value.channels, + sample_rate: value.sample_rate, + sample_format: SampleFormat::try_from(value.sample_format)?, + sample_size: value.sample_size, + channel_layout: ChannelMapping::from_bits(value.channel_layout) + .ok_or(Error::UnknownChannelMapping(value.channel_layout))?, + subsong_index: value.subsong_index, + subsong_count: value.subsong_count, + input_channels: value.input_channels, + stream_samples: value.stream_samples, + loop_start: value.loop_start, + loop_end: value.loop_end, + loop_flag: value.loop_flag, + play_forever: value.play_forever, + play_samples: value.play_samples, + stream_bitrate: value.stream_bitrate, + codec_name, + layout_name, + meta_name, + stream_name, + format_id: value.format_id, + }) + } +} + impl VgmStream { - /// Creates a new `libvgmstream_t`. + /// Inits the vgmstream context. /// /// # Errors /// @@ -215,8 +301,6 @@ impl VgmStream { /// /// # Example /// - /// Initialize libvgmstream: - /// /// ```no_run /// use vgmstream::VgmStream; /// @@ -225,181 +309,365 @@ impl VgmStream { pub fn new() -> Result { let inner = match unsafe { vgmstream_sys::libvgmstream_init().as_mut() } { Some(v) => v, - None => return Err(Error::InitializationFailed), + None => return Err(Error::VgmStream("libvgmstream_init".to_string())), }; - Ok(Self { inner: inner as *mut _ }) + Ok(Self { inner: inner.into() }) } - pub fn with_config(config: &Config) -> Result { - let vgmstream = Self::new()?; - - let mut config = vgmstream_sys::libvgmstream_config_t { - disable_config_override: config.disable_config_override, - allow_play_forever: config.allow_play_forever, - - play_forever: config.play_forever, - ignore_loop: config.ignore_loop, - force_loop: config.force_loop, - really_force_loop: config.really_force_loop, - ignore_fade: config.ignore_fade, - - loop_count: config.loop_count, - fade_time: config.fade_time, - fade_delay: config.fade_delay, - - stereo_track: config.stereo_track, - auto_downmix_channels: config.auto_downmix_channels, - force_sfmt: config.force_sfmt.into(), - }; - - unsafe { - // libvgmstream_setup expects a mutable pointer, but it actually doesn't modify it - vgmstream_sys::libvgmstream_setup(vgmstream.inner, &mut config as *mut _) + /// Pass config to apply to next [`Self::open`], or current stream if + /// already loaded and not setup previously. + /// + /// * some settings may be ignored in invalid or complex cases + /// (ex. TXTP with pre-configured options) + /// * once config is applied to current stream new [`Self::setup`] calls + /// only apply to next [`Self::open`] + /// * pass [`None`] to clear current config + /// * remember config may change format info like channels or output format + /// (recheck if calling after loading song) + pub fn setup(&mut self, config: Option<&Config>) -> () { + match config { + Some(cfg) => unsafe { + vgmstream_sys::libvgmstream_setup( + self.inner.as_mut(), + &mut cfg.clone().into() as *mut _, + ); + }, + None => unsafe { + vgmstream_sys::libvgmstream_setup(self.inner.as_mut(), std::ptr::null_mut()) + }, }; - - Ok(vgmstream) } - pub fn open_song<'a>( - &'a mut self, - libsf: &'a mut sf::StreamFile<'a>, - subsong: usize, - ) -> Result<(), Error> { + /// Opens file based on config and prepares it to play if supported. + /// + /// * returns < 0 on error (file not recognised, invalid subsong index, etc) + /// * will close currently loaded song if needed + /// * libsf (custom IO) is not needed after [`Self::open`] and should be closed, + /// as vgmstream re-opens as needed + /// * subsong can be 1..N or 0 = default/first + /// * to check if a file has subsongs, [`Self::open`] default and check + /// [`Format::subsong_count`] + pub fn open(&mut self, libsf: &mut StreamFile, subsong: usize) -> Result<(), Error> { if unsafe { - vgmstream_sys::libvgmstream_open_stream(self.inner, libsf.inner, subsong as c_int) + vgmstream_sys::libvgmstream_open_stream( + self.inner.as_mut(), + libsf.inner.as_ptr(), + subsong as c_int, + ) } != 0 { - return Err(Error::Generic); + return Err(Error::VgmStream("libvgmstream_open_stream".to_string())); } Ok(()) } - pub(crate) fn as_ref(&self) -> Result<&vgmstream_sys::libvgmstream_t, Error> { - match unsafe { self.inner.as_ref() } { - Some(i) => Ok(i), - None => Err(Error::Generic), + /// Closes current song. + /// + /// Can still use `self` to open other songs. + pub fn close(&mut self) { + unsafe { + vgmstream_sys::libvgmstream_close_stream(self.inner.as_mut()); } } - pub(crate) fn as_mut(&mut self) -> Result<&mut vgmstream_sys::libvgmstream_t, Error> { - match unsafe { self.inner.as_mut() } { - Some(i) => Ok(i), - None => Err(Error::Generic), - } + pub(crate) fn as_ref(&self) -> &vgmstream_sys::libvgmstream_t { + unsafe { self.inner.as_ref() } + } + + pub(crate) fn as_mut(&mut self) -> &mut vgmstream_sys::libvgmstream_t { + unsafe { self.inner.as_mut() } } + /// Returns the current file format. + /// + /// # Errors + /// + /// Returns `Error::NullPointer` if `vgmstream->format` is null. + /// + /// # Examples + /// + /// ```no_run + /// use vgmstream::{StreamFile, VgmStream}; + /// + /// let mut vgmstream = VgmStream::new().unwrap(); + /// let mut sf = StreamFile::open("path/to/file").unwrap(); + /// + /// vgmstream.open(&mut sf, 0).unwrap(); + /// println!("{:?}", vgmstream.format().unwrap()); + /// ``` pub fn format(&self) -> Result { - let format = match unsafe { self.as_ref()?.format.as_ref() } { - Some(f) => f, - None => return Err(Error::Generic), + match unsafe { self.as_ref().format.as_ref() } { + Some(f) => f.clone().try_into(), + None => return Err(Error::NullPointer("vgmstream->format".to_string())), + } + } + + /// Decodes next batch of samples. + /// + /// vgmstream supplies its own buffer, updated on `vgmstream->decoder` attributes + /// + /// # Errors + /// + /// Returns `Error::VgmStream` if decoding failed. + /// + /// Returns `Error::NullPointer` if `vgmstream->decoder` is null. + /// + /// # Examples + /// + /// ```no_run + /// use vgmstream::{StreamFile, VgmStream}; + /// + /// let mut vgmstream = VgmStream::new().unwrap(); + /// let mut sf = StreamFile::open("path/to/file").unwrap(); + /// + /// vgmstream.open(&mut sf, 0).unwrap(); + /// while let Ok(buf) = vgmstream.render() { + /// println!("{}", buf.len()); + /// } + /// ``` + pub fn render<'a>(&'a mut self) -> Result<&'a [u8], Error> { + if unsafe { vgmstream_sys::libvgmstream_render(self.inner.as_mut()) } < 0 { + return Err(Error::VgmStream("libvgmstream_render".to_string())); + } + + let decoder = match unsafe { self.as_mut().decoder.as_ref() } { + Some(d) => d, + None => return Err(Error::NullPointer("vgmstream->decoder".to_string())), }; - let sample_format = SampleType::from(format.sample_format); + let vgmstream_sys::libvgmstream_decoder_t { buf, buf_bytes, .. } = decoder; - let codec_name = - unsafe { CStr::from_ptr(format.codec_name.as_ptr()) }.to_string_lossy().to_string(); - let layout_name = - unsafe { CStr::from_ptr(format.layout_name.as_ptr()) }.to_string_lossy().to_string(); - let meta_name = - unsafe { CStr::from_ptr(format.meta_name.as_ptr()) }.to_string_lossy().to_string(); - let stream_name = - unsafe { CStr::from_ptr(format.stream_name.as_ptr()) }.to_string_lossy().to_string(); - - Ok(Format { - channels: format.channels, - sample_rate: format.sample_rate, - sample_format, - sample_size: format.sample_size, - channel_layout: ChannelMapping::from_bits(format.channel_layout) - .ok_or(Error::InvalidChannelMapping(format.channel_layout))?, - subsong_index: format.subsong_index, - subsong_count: format.subsong_count, - input_channels: format.input_channels, - stream_samples: format.stream_samples, - loop_start: format.loop_start, - loop_end: format.loop_end, - loop_flag: format.loop_flag, - play_forever: format.play_forever, - play_samples: format.play_samples, - stream_bitrate: format.stream_bitrate, - codec_name, - layout_name, - meta_name, - stream_name, - format_id: format.format_id, - }) + let buf: &[u8] = + unsafe { std::slice::from_raw_parts(*buf as *const _, *buf_bytes as usize) }; + + Ok(buf) } - pub fn render(&mut self) -> Result, Error> { - if unsafe { vgmstream_sys::libvgmstream_render(self.inner) } < 0 { - return Err(Error::Generic); + /// Same as [`Self::render`], but fills some external buffer (also updates lib->decoder attributes) + /// + /// It decodes `buf.len() / sample_size / input_channels` samples. Note that may return less than + /// requested samples. (such as near EOF) + /// + /// # Errors + /// + /// Returns [`Error::NullPointer`] if `vgmstream->decoder` is null. + /// + /// Returns [`Error::VgmStream`] if `libvgmstream_fill` fails. + /// + /// # Performance + /// + /// This function needs copying around from internal bufs so may be slightly slower; + /// mainly for cases when you have buf constraints. + /// + /// # Examples + /// + /// ```no_run + /// use vgmstream::{StreamFile, VgmStream}; + /// + /// let mut vgmstream = VgmStream::new().unwrap(); + /// let mut sf = StreamFile::open("path/to/file").unwrap(); + /// + /// vgmstream.open(&mut sf, 0).unwrap(); + /// let mut buf = vec![0u8; 1024]; + /// while let Ok(len) = vgmstream.fill(&mut buf) { + /// println!("{}", len); + /// } + /// ``` + pub fn fill(&mut self, buf: &mut [u8]) -> Result { + let Format { sample_size, input_channels, .. } = self.format()?; + if unsafe { + vgmstream_sys::libvgmstream_fill( + self.inner.as_mut(), + buf.as_mut_ptr() as *mut _, + (buf.len() / sample_size as usize / input_channels as usize) as c_int, + ) + } < 0 + { + return Err(Error::VgmStream("libvgmstream_fill".to_string())); } - let decoder = match unsafe { self.as_mut()?.decoder.as_ref() } { + let decoder = match unsafe { self.as_mut().decoder.as_ref() } { Some(d) => d, - None => return Err(Error::Generic), + None => return Err(Error::NullPointer("vgmstream->decoder".to_string())), }; - let vgmstream_sys::libvgmstream_decoder_t { buf, buf_bytes, .. } = decoder; + let vgmstream_sys::libvgmstream_decoder_t { buf_bytes, .. } = decoder; + + Ok(*buf_bytes as usize) + } + + /// Gets current position within the song. + /// + /// # Errors + /// + /// Returns `Error::VgmStream` if getting position failed. + pub fn get_pos(&mut self) -> Result { + let pos = unsafe { vgmstream_sys::libvgmstream_get_play_position(self.inner.as_mut()) }; + if pos < 0 { + return Err(Error::VgmStream("libvgmstream_get_play_position".to_string())); + } - let buf = unsafe { std::slice::from_raw_parts(*buf as *const u8, *buf_bytes as usize) }; + Ok(pos) + } - Ok(buf.to_vec()) + /// Seeks to absolute position. + /// + /// Will clamp incorrect values such as seeking before/past playable length. + pub fn seek(&mut self, pos: i64) { + unsafe { vgmstream_sys::libvgmstream_seek(self.inner.as_mut(), pos) }; + } + + /// Reset current song. + pub fn reset(&mut self) { + unsafe { vgmstream_sys::libvgmstream_reset(self.inner.as_mut()) }; } } impl Drop for VgmStream { fn drop(&mut self) { - unsafe { vgmstream_sys::libvgmstream_free(self.inner) } + unsafe { vgmstream_sys::libvgmstream_free(self.inner.as_mut()) } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum LogLevel { + All = vgmstream_sys::libvgmstream_loglevel_t_LIBVGMSTREAM_LOG_LEVEL_ALL as isize, + Debug = vgmstream_sys::libvgmstream_loglevel_t_LIBVGMSTREAM_LOG_LEVEL_DEBUG as isize, + Info = vgmstream_sys::libvgmstream_loglevel_t_LIBVGMSTREAM_LOG_LEVEL_INFO as isize, + None = vgmstream_sys::libvgmstream_loglevel_t_LIBVGMSTREAM_LOG_LEVEL_NONE as isize, +} + +impl From for vgmstream_sys::libvgmstream_loglevel_t { + fn from(level: LogLevel) -> Self { + level as vgmstream_sys::libvgmstream_loglevel_t } } +/// Defines a global log callback, as vgmstream sometimes communicates format issues to the user. +/// +/// * Note that log is currently set globally rather than per [`VgmStream`]. +/// * Call with [`LogLevel::None`] to disable current callback. +/// * Call with [`None`] callback to use default stdout callback. +/// +pub fn set_log(level: LogLevel, callback: Option) { + unsafe { vgmstream_sys::libvgmstream_set_log(level.into(), callback) }; +} + +/// Returns a list of supported extensions, such as "adx", "dsp", etc. +/// Mainly for plugins that want to know which extensions are supported. +pub fn extensions() -> &'static [&'static str] { + static EXTENSIONS: std::sync::LazyLock> = std::sync::LazyLock::new(|| { + let mut ext_count = 0; + + unsafe { + let exts = vgmstream_sys::libvgmstream_get_extensions(&mut ext_count); + std::slice::from_raw_parts(exts, ext_count as usize) + } + .into_iter() + .map(|p| unsafe { CStr::from_ptr(*p) }.to_str().unwrap()) + .collect() + }); + + &EXTENSIONS +} + +/// Same as [`extensions`], buf returns a list what vgmstream considers "common" formats, +/// such as "wav", "ogg", which usually one doesn't want to associate to vgmstream. +pub fn common_extensions() -> &'static [&'static str] { + static COMMON_EXTENSIONS: std::sync::LazyLock> = + std::sync::LazyLock::new(|| { + let mut ext_count = 0; + + unsafe { + let exts = vgmstream_sys::libvgmstream_get_common_extensions(&mut ext_count); + std::slice::from_raw_parts(exts, ext_count as usize) + } + .into_iter() + .map(|p| unsafe { CStr::from_ptr(*p) }.to_str().unwrap()) + .collect() + }); + + &COMMON_EXTENSIONS +} + #[cfg(test)] mod tests { + use std::cell::LazyCell; + use super::*; const ACB_PATH: &str = concat!(env!("CARGO_MANIFEST_DIR"), "/../../tests/test.acb"); - - #[test] - fn test_vgmstream() { - let vgmstream = VgmStream::new().unwrap(); - assert!(!vgmstream.inner.is_null()); - } - - #[test] - fn test_vgmstream_open_song() { + const ACB_FORMAT: LazyCell = LazyCell::new(|| Format { + channel_layout: ChannelMapping::STEREO, + codec_name: "CRI HCA".to_string(), + input_channels: 2, + channels: 2, + play_samples: 882000, + play_forever: false, + loop_start: 0, + loop_end: 0, + layout_name: "flat".to_string(), + loop_flag: false, + meta_name: "CRI HCA header".to_string(), + sample_format: SampleFormat::Float, + sample_rate: 44100, + sample_size: 4, + stream_bitrate: 117615, + stream_name: "song3_00test_bgm".to_string(), + stream_samples: 882000, + subsong_count: 1, + subsong_index: 0, + format_id: 378, + }); + + fn open_vgmstream() -> VgmStream { let mut vgmstream = VgmStream::new().unwrap(); - let mut sf = StreamFile::open(&vgmstream, ACB_PATH).unwrap(); + let mut sf = StreamFile::open(ACB_PATH).unwrap(); + + vgmstream.open(&mut sf, 0).unwrap(); - assert!(vgmstream.open_song(&mut sf, 0).is_ok()); + vgmstream } #[test] fn test_vgmstream_format() { - let mut vgmstream = VgmStream::new().unwrap(); - let mut sf = StreamFile::open(&vgmstream, ACB_PATH).unwrap(); - - vgmstream.open_song(&mut sf, 0).unwrap(); + let vgmstream = open_vgmstream(); let format = vgmstream.format().unwrap(); - assert_eq!(format.channels, 2); - assert_eq!(format.sample_rate, 44100); - assert_eq!(format.sample_format, SampleType::Pcm16); - assert_eq!(format.sample_size, 2); - assert_eq!(format.codec_name, "CRI HCA"); - assert_eq!(format.layout_name, "flat"); - assert!(!format.stream_name.is_empty()); + assert_eq!(format, ACB_FORMAT.to_owned()); } #[test] - fn test_vgmstream_render() { + fn test_vgmstream_setup() { let mut vgmstream = VgmStream::new().unwrap(); - let mut sf = StreamFile::open(&vgmstream, ACB_PATH).unwrap(); + let mut sf = StreamFile::open(ACB_PATH).unwrap(); + + let config = Config { + disable_config_override: false, + allow_play_forever: false, + play_forever: false, + ignore_loop: false, + force_loop: false, + really_force_loop: false, + ignore_fade: false, + loop_count: 0f64, + fade_time: 0f64, + fade_delay: 0f64, + stereo_track: 0, + auto_downmix_channels: 0, + force_sfmt: SampleFormat::Float, + }; + vgmstream.setup(Some(&config)); - vgmstream.open_song(&mut sf, 0).unwrap(); + vgmstream.open(&mut sf, 0).unwrap(); + assert_eq!(vgmstream.format().unwrap(), ACB_FORMAT.to_owned()); + } + #[test] + fn test_vgmstream_render() { + let mut vgmstream = open_vgmstream(); let mut size = 0usize; while let Ok(buf) = vgmstream.render() { if buf.is_empty() { @@ -409,6 +677,35 @@ mod tests { size += buf.len(); } - assert_eq!(size, 3528000); + assert_eq!( + size, + ACB_FORMAT.sample_size as usize + * ACB_FORMAT.channels as usize + * ACB_FORMAT.stream_samples as usize + ); + } + + #[test] + fn test_vgmstream_fill() { + let mut f = + std::fs::File::create(concat!(env!("CARGO_MANIFEST_DIR"), "/test.wav")).unwrap(); + let mut vgmstream = open_vgmstream(); + let mut buf = [0u8; 4096]; + let mut total_size = 0usize; + while let Ok(size) = vgmstream.fill(&mut buf) { + if size == 0 { + break; + } + + std::io::Write::write_all(&mut f, &buf).unwrap(); + total_size += size; + } + + assert_eq!( + total_size, + ACB_FORMAT.sample_size as usize + * ACB_FORMAT.channels as usize + * ACB_FORMAT.stream_samples as usize + ); } } diff --git a/crates/vgmstream/src/sf.rs b/crates/vgmstream/src/sf.rs index 6036349..d8097c4 100644 --- a/crates/vgmstream/src/sf.rs +++ b/crates/vgmstream/src/sf.rs @@ -1,16 +1,14 @@ use std::ffi::CString; -use std::marker::PhantomData; use std::path::Path; +use std::ptr::NonNull; -use crate::{Error, VgmStream}; +use crate::Error; -pub struct StreamFile<'a> { - pub(crate) inner: *mut vgmstream_sys::libstreamfile_t, - - phantom: PhantomData<&'a VgmStream>, +pub struct StreamFile { + pub(crate) inner: NonNull, } -impl StreamFile<'_> { +impl StreamFile { /// Creates a new `libstreamfile_t`. /// /// # Errors @@ -22,12 +20,11 @@ impl StreamFile<'_> { /// Initialize libvgmstream: /// /// ```no_run - /// use vgmstream::{StreamFile, VgmStream}; + /// use vgmstream::StreamFile; /// - /// let vgmstream = VgmStream::new().unwrap(); /// let stream = StreamFile::open(&vgmstream, "path/to/file").unwrap(); /// ``` - pub fn open

(_: &VgmStream, path: P) -> Result + pub fn open

(path: P) -> Result where P: AsRef, { @@ -35,10 +32,27 @@ impl StreamFile<'_> { let inner = unsafe { vgmstream_sys::libstreamfile_open_from_stdio(path.as_ptr()) }; if inner.is_null() { - return Err(Error::InitializationFailed); + return Err(Error::VgmStream("libstreamfile_open_from_stdio".to_string())); + } + + Ok(Self { inner: unsafe { NonNull::new_unchecked(inner) } }) + } + + pub fn buffered(mut self) -> Result { + let inner = unsafe { vgmstream_sys::libstreamfile_open_buffered(self.inner.as_ptr()) }; + if inner.is_null() { + return Err(Error::VgmStream("libstreamfile_open_buffered".to_string())); } - Ok(Self { inner, phantom: PhantomData }) + self.inner = unsafe { NonNull::new_unchecked(inner) }; + + Ok(self) + } +} + +impl Drop for StreamFile { + fn drop(&mut self) { + unsafe { self.inner.as_ref().close.unwrap()(self.inner.as_ptr()) }; } } @@ -50,8 +64,6 @@ mod tests { #[test] fn test_streamfile() { - let vgmstream = VgmStream::new().unwrap(); - let sf = StreamFile::open(&vgmstream, ACB_PATH).unwrap(); - assert!(!sf.inner.is_null()); + StreamFile::open(ACB_PATH).unwrap().buffered().unwrap(); } } diff --git a/src/bin/mltd/extract.rs b/src/bin/mltd/extract.rs index e0b045e..c3fbae2 100644 --- a/src/bin/mltd/extract.rs +++ b/src/bin/mltd/extract.rs @@ -132,15 +132,16 @@ pub async fn extract_files(args: &ExtractorArgs) -> Result<(), Error> { args.asset_ripper_path.display() ); - let mut port_start = 50000; + let mut port = 50000; let mut asset_rippers = Vec::new(); while asset_rippers.len() < args.parallel as usize { - match AssetRipper::new(&args.asset_ripper_path, port_start) { + match AssetRipper::new(&args.asset_ripper_path, port) { Ok(ripper) => { + log::trace!("created AssetRipper on port {}", port); asset_rippers.push(Mutex::new(ripper)); - port_start += 1; + port += 1; } - Err(Error::IO(e)) if e.kind() == std::io::ErrorKind::AddrInUse => port_start += 1, + Err(Error::IO(e)) if e.kind() == std::io::ErrorKind::AddrInUse => port += 1, Err(e) => return Err(e), }; } diff --git a/src/mltd/extract/audio.rs b/src/mltd/extract/audio.rs index 0501344..f7f73db 100644 --- a/src/mltd/extract/audio.rs +++ b/src/mltd/extract/audio.rs @@ -93,8 +93,8 @@ impl<'a> Encoder<'a> { P: AsRef, { let mut vgmstream = VgmStream::new()?; - let mut sf = StreamFile::open(&vgmstream, input_file.as_ref())?; - vgmstream.open_song(&mut sf, subsong_index)?; + let mut sf = StreamFile::open(input_file.as_ref())?; + vgmstream.open(&mut sf, subsong_index)?; let acb_fmt = vgmstream.format()?; @@ -348,13 +348,16 @@ impl<'a> Encoder<'a> { } fn to_ffmpeg_sample_format( - value: vgmstream::SampleType, + value: vgmstream::SampleFormat, ) -> Result { match value { - vgmstream::SampleType::Pcm16 => { + vgmstream::SampleFormat::Pcm16 => { Ok(ffmpeg_next::format::Sample::I16(ffmpeg_next::format::sample::Type::Packed)) } - vgmstream::SampleType::Float => { + vgmstream::SampleFormat::Pcm24 | vgmstream::SampleFormat::Pcm32 => { + Ok(ffmpeg_next::format::Sample::I32(ffmpeg_next::format::sample::Type::Packed)) + } + vgmstream::SampleFormat::Float => { Ok(ffmpeg_next::format::Sample::F32(ffmpeg_next::format::sample::Type::Packed)) } } diff --git a/src/mltd/net/asset_ripper.rs b/src/mltd/net/asset_ripper.rs index 1ff9e11..a02bb1d 100644 --- a/src/mltd/net/asset_ripper.rs +++ b/src/mltd/net/asset_ripper.rs @@ -46,14 +46,13 @@ impl AssetRipper { where P: AsRef, { - let process = match Command::new(path.as_ref().as_os_str()) + let process = match Command::new(path.as_ref()) .args(["--port", &port.to_string()]) .args(["--launch-browser", "false"]) .args(["--log", "false"]) .stdin(Stdio::null()) .stdout(Stdio::piped()) .stderr(Stdio::null()) - .env_clear() .spawn() { Ok(mut process) => { @@ -83,11 +82,13 @@ impl AssetRipper { where P: AsRef, { - let url = if path.as_ref().is_dir() { "LoadFolder" } else { "LoadFile" }; + let path = path.as_ref(); + + let url = if path.is_dir() { "LoadFolder" } else { "LoadFile" }; let url = format!("{}/{}", &self.base_url, url); let mut form = HashMap::new(); - form.insert("path", path.as_ref().to_string_lossy().to_string()); + form.insert("path", path.to_string_lossy().to_string()); let client = reqwest::Client::new(); let req = client.post(url).form(&form); @@ -211,7 +212,7 @@ impl AssetRipper { ) -> Result { let path = format!(r#"{{"B":{{"P":[{bundle_no}]}},"I":{collection_no}}}"#); - let text = match self.send_request("Bundles/View", &path).await?.text().await { + let text = match self.send_request("Collections/Count", &path).await?.text().await { Ok(text) => text, Err(e) => return Err(Error::ResponseDeserialize(e)), }; @@ -422,9 +423,16 @@ impl Drop for AssetRipper { #[cfg(test)] mod tests { + use std::path::{MAIN_SEPARATOR_STR, Path}; + use super::AssetRipper; - const ASSET_RIPPER_PATH: &str = - concat!(env!("CARGO_MANIFEST_DIR"), "/AssetRipper/AssetRipper.GUI.Free"); + + const ASSET_RIPPER_PATH: &str = if cfg!(windows) { + concat!(env!("CARGO_MANIFEST_DIR"), "/AssetRipper/AssetRipper.GUI.Free.exe") + } else { + concat!(env!("CARGO_MANIFEST_DIR"), "/AssetRipper/AssetRipper.GUI.Free") + }; + const TEST_CASES: &[(&str, usize, usize)] = &[ (concat!(env!("CARGO_MANIFEST_DIR"), "/tests/test_acb.unity3d"), 1, 2), (concat!(env!("CARGO_MANIFEST_DIR"), "/tests/test_sprite.unity3d"), 1, 14), @@ -432,10 +440,10 @@ mod tests { ]; #[tokio::test] - #[ignore] + #[ignore = "need to download AssetRipper"] async fn test_bundles() { let mut asset_ripper = AssetRipper::new(ASSET_RIPPER_PATH, 56789).unwrap(); - asset_ripper.load(concat!(env!("CARGO_MANIFEST_DIR"), "/tests")).await.unwrap(); + asset_ripper.load(Path::new(env!("CARGO_MANIFEST_DIR")).join("tests")).await.unwrap(); let bundles = asset_ripper.bundles().await.unwrap(); @@ -443,7 +451,7 @@ mod tests { } #[tokio::test] - #[ignore] + #[ignore = "need to download AssetRipper"] async fn test_collections() { let mut asset_ripper = AssetRipper::new(ASSET_RIPPER_PATH, 56790).unwrap(); @@ -455,19 +463,19 @@ mod tests { } #[tokio::test] - #[ignore] + #[ignore = "need to download AssetRipper"] async fn test_asset_count() { let mut asset_ripper = AssetRipper::new(ASSET_RIPPER_PATH, 56791).unwrap(); for (path, _, asset_count) in TEST_CASES { - asset_ripper.load(path).await.unwrap(); + asset_ripper.load(*path).await.unwrap(); let count = asset_ripper.asset_count(0, 0).await.unwrap(); assert_eq!(count, *asset_count); } } #[tokio::test] - #[ignore] + #[ignore = "need to download AssetRipper"] async fn test_assets() { let mut asset_ripper = AssetRipper::new(ASSET_RIPPER_PATH, 56792).unwrap(); @@ -479,7 +487,7 @@ mod tests { } #[tokio::test] - #[ignore] + #[ignore = "need to download AssetRipper"] async fn test_asset_info() { let mut asset_ripper = AssetRipper::new(ASSET_RIPPER_PATH, 56793).unwrap(); @@ -490,7 +498,10 @@ mod tests { assert_eq!( asset_info.original_path, - Some(String::from("Assets/imas/resources/adx2/song3/song3_00test.acb.bytes")) + Some( + ["Assets", "imas", "resources", "adx2", "song3", "song3_00test.acb.bytes"] + .join(MAIN_SEPARATOR_STR) + ) ); } } From 7f753f83547dc59a51ddcd2dbbf8836420becd14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=91=E5=8F=AF=E8=83=BD=E5=BE=88=E5=BB=A2?= Date: Mon, 16 Jun 2025 10:40:06 +0800 Subject: [PATCH 4/9] :recycle: seal error types --- .cargo/config.toml | 4 + Cargo.lock | 223 ++++++++++--------- Cargo.toml | 6 +- crates/vgmstream-sys/Cargo.toml | 4 +- crates/vgmstream-sys/vgmstream | 2 +- crates/vgmstream/src/lib.rs | 22 +- src/bin/mltd/download.rs | 3 +- src/bin/mltd/extract.rs | 54 ++--- src/bin/mltd/manifest.rs | 10 +- src/mltd/asset.rs | 57 +++-- src/mltd/error.rs | 162 +++++++++----- src/mltd/extract/audio.rs | 127 +++++++---- src/mltd/extract/puzzle.rs | 31 ++- src/mltd/extract/text.rs | 62 ++++-- src/mltd/lib.rs | 2 +- src/mltd/manifest.rs | 75 +++++-- src/mltd/net/asset_ripper.rs | 365 ++++++++++++++++++-------------- src/mltd/net/matsuri_api.rs | 91 ++++---- src/mltd/net/mod.rs | 37 ++++ src/mltd/util.rs | 4 + 20 files changed, 819 insertions(+), 522 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 1919d04..6bbc96d 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,7 @@ [alias] build-windows = "build --config .cargo/config-windows.toml" +check-windows = "check --config .cargo/config-windows.toml" test-windows = "test --config .cargo/config-windows.toml" +bw = "build-windows" +cw = "check-windows" +tw = "test-windows" diff --git a/Cargo.lock b/Cargo.lock index b89ab04..839e6bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "adler2" -version = "2.0.0" +version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" [[package]] name = "aes" @@ -39,9 +39,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -54,33 +54,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys 0.59.0", ] [[package]] name = "anstyle-wincon" -version = "3.0.8" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", "once_cell_polyfill", @@ -107,9 +107,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.23" +version = "0.4.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b37fc50485c4f3f736a4fb14199f6d5f5ba008d7f28fe710306c92780f004c07" +checksum = "d615619615a650c571269c00dca41db04b9210037fa76ed8239f70404ab56985" dependencies = [ "flate2", "futures-core", @@ -195,9 +195,9 @@ dependencies = [ [[package]] name = "bindgen" -version = "0.71.1" +version = "0.72.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" +checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f" dependencies = [ "bitflags 2.9.1", "cexpr", @@ -236,15 +236,15 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" [[package]] name = "bytemuck" -version = "1.23.0" +version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9134a6ef01ce4b366b50689c94f82c14bc72bc5d0386829828a2e2752ef7958c" +checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" [[package]] name = "byteorder" @@ -307,9 +307,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.25" +version = "1.2.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" +checksum = "d487aa071b5f64da6f19a3e848e3578944b726ee5a4854b82172f02aa876bfdc" dependencies = [ "jobserver", "libc", @@ -327,9 +327,9 @@ dependencies = [ [[package]] name = "cfg-if" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" [[package]] name = "cipher" @@ -354,9 +354,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.39" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" +checksum = "40b6887a1d8685cebccf115538db5c0efe625ccac9696ad45c409d96566e910f" dependencies = [ "clap_builder", "clap_derive", @@ -374,9 +374,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.39" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" +checksum = "e0c66c08ce9f0c698cbce5c0279d0bb6ac936d8674174fe48f736533b964f59e" dependencies = [ "anstream", "anstyle", @@ -384,14 +384,14 @@ dependencies = [ "strsim", "terminal_size", "unicase", - "unicode-width 0.2.0", + "unicode-width", ] [[package]] name = "clap_derive" -version = "4.5.32" +version = "4.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" +checksum = "d2c7947ae4cc3d851207c1adb5b5e260ff0cca11446b1d6d1423788e442257ce" dependencies = [ "heck", "proc-macro2", @@ -401,9 +401,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" [[package]] name = "cmake" @@ -416,9 +416,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "console" @@ -758,9 +758,9 @@ dependencies = [ [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "libz-rs-sys", @@ -917,11 +917,11 @@ dependencies = [ [[package]] name = "getopts" -version = "0.2.21" +version = "0.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +checksum = "cba6ae63eb948698e300f645f87c70f76630d505f23b8907cf1e193ee85048c1" dependencies = [ - "unicode-width 0.1.14", + "unicode-width", ] [[package]] @@ -932,7 +932,7 @@ checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" dependencies = [ "cfg-if", "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", ] [[package]] @@ -980,9 +980,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" [[package]] name = "heck" @@ -992,9 +992,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" [[package]] name = "html5ever" @@ -1076,9 +1076,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.6" +version = "0.27.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03a01595e11bdcec50946522c32dde3fc6914743000a68b93000965f2f02406d" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" dependencies = [ "http", "hyper", @@ -1108,9 +1108,9 @@ dependencies = [ [[package]] name = "hyper-util" -version = "0.1.13" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1c293b6b3d21eca78250dc7dbebd6b9210ec5530e038cbfe0661b5c47ab06e8" +checksum = "dc2fdfdbff08affe55bb779f33b053aa1fe5dd5b54c257343c17edfa55711bdb" dependencies = [ "base64", "bytes", @@ -1263,9 +1263,9 @@ dependencies = [ [[package]] name = "image-webp" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b77d01e822461baa8409e156015a1d91735549f0f2c17691bd2d996bef238f7f" +checksum = "14d75c7014ddab93c232bc6bb9f64790d3dfd1d605199acd4b40b6d69e691e9f" dependencies = [ "byteorder-lite", "quick-error", @@ -1342,9 +1342,9 @@ checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" [[package]] name = "jiff" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a194df1107f33c79f4f93d02c80798520551949d59dfad22b6157048a88cca93" +checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49" dependencies = [ "jiff-static", "log", @@ -1355,9 +1355,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c6e1db7ed32c6c71b759497fae34bf7933636f75a251b9e736555da426f6442" +checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4" dependencies = [ "proc-macro2", "quote", @@ -1386,9 +1386,9 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.172" +version = "0.2.173" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" +checksum = "d8cfeafaffdbc32176b64fb251369d52ea9f0a8fbc6f8759edffef7b525d64bb" [[package]] name = "libloading" @@ -1397,14 +1397,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" dependencies = [ "cfg-if", - "windows-targets 0.53.0", + "windows-targets 0.53.2", ] [[package]] name = "libz-rs-sys" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" dependencies = [ "zlib-rs", ] @@ -1479,9 +1479,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.4" +version = "2.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" [[package]] name = "mime" @@ -1497,9 +1497,9 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "miniz_oxide" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" dependencies = [ "adler2", "simd-adler32", @@ -1512,7 +1512,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", - "wasi 0.11.0+wasi-snapshot-preview1", + "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -1833,9 +1833,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" [[package]] name = "portable-atomic-util" @@ -1869,9 +1869,9 @@ checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" [[package]] name = "prettyplease" -version = "0.2.33" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dee91521343f4c5c6a63edd65e54f31f5c92fe8978c40a4282f8372194c6a7d" +checksum = "6837b9e10d61f45f987d50808f83d1ee3d206c66acf650c3e4ae2e1f6ddedf55" dependencies = [ "proc-macro2", "syn", @@ -1924,9 +1924,9 @@ checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" [[package]] name = "redox_syscall" -version = "0.5.12" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "928fca9cf2aa042393a8325b9ead81d2f0df4cb12e1e24cef072922ccd99c5af" +checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6" dependencies = [ "bitflags 2.9.1", ] @@ -1962,9 +1962,9 @@ checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "reqwest" -version = "0.12.19" +version = "0.12.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2f8e5513d63f2e5b386eb5106dc67eaf3f84e95258e210489136b8b92ad6119" +checksum = "eabf4c97d9130e2bf606614eb937e86edac8292eaa6f422f995d7e8de1eb1813" dependencies = [ "async-compression", "base64", @@ -1980,12 +1980,10 @@ dependencies = [ "hyper-rustls", "hyper-tls", "hyper-util", - "ipnet", "js-sys", "log", "mime", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", "rustls-pki-types", @@ -2044,9 +2042,9 @@ dependencies = [ [[package]] name = "rustc-demangle" -version = "0.1.24" +version = "0.1.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f" +checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f" [[package]] name = "rustc-hash" @@ -2254,9 +2252,9 @@ dependencies = [ [[package]] name = "servo_arc" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae65c4249478a2647db249fb43e23cec56a2c8974a427e7bd8cb5a1d0964921a" +checksum = "204ea332803bd95a0b60388590d59cf6468ec9becf626e2451f1d26a1d972de4" dependencies = [ "stable_deref_trait", ] @@ -2290,18 +2288,15 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "slab" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] +checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "socket2" @@ -2358,9 +2353,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.101" +version = "2.0.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" +checksum = "e4307e30089d6fd6aff212f2da3a1f9e32f3223b1f010fb09b7c95f90f3ca1e8" dependencies = [ "proc-macro2", "quote", @@ -2648,9 +2643,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.33" +version = "0.1.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" dependencies = [ "once_cell", ] @@ -2681,15 +2676,9 @@ checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" [[package]] name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-width" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" +checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" [[package]] name = "untrusted" @@ -2789,9 +2778,9 @@ dependencies = [ [[package]] name = "vgmstream-sys" -version = "0.2.0-vgmstream-r2023-16-gb2fc214a" +version = "0.2.0-vgmstream-r2023-27-g7b0c835c" dependencies = [ - "bindgen 0.71.1", + "bindgen 0.72.0", "cmake", ] @@ -2806,9 +2795,9 @@ dependencies = [ [[package]] name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" +version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] name = "wasi" @@ -2925,19 +2914,19 @@ dependencies = [ [[package]] name = "windows-link" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76840935b766e1b0a05c0066835fb9ec80071d4c09a16f6bd5f7e655e3c14c38" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" [[package]] name = "windows-registry" -version = "0.4.0" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4286ad90ddb45071efd1a66dfa43eb02dd0dfbae1545ad6cc3c51cf34d7e8ba3" +checksum = "b3bab093bdd303a1240bb99b8aba8ea8a69ee19d34c9e2ef9594e708a4878820" dependencies = [ + "windows-link", "windows-result", "windows-strings", - "windows-targets 0.53.0", ] [[package]] @@ -2951,9 +2940,9 @@ dependencies = [ [[package]] name = "windows-strings" -version = "0.3.1" +version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87fa48cc5d406560701792be122a10132491cff9d0aeb23583cc2dcafc847319" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ "windows-link", ] @@ -2994,9 +2983,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.53.0" +version = "0.53.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1e4c7e8ceaaf9cb7d7507c974735728ab453b67ef8f18febdd7c11fe59dca8b" +checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef" dependencies = [ "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", @@ -3205,9 +3194,9 @@ dependencies = [ [[package]] name = "zip" -version = "4.0.0" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "153a6fff49d264c4babdcfa6b4d534747f520e56e8f0f384f3b808c4b64cc1fd" +checksum = "af7dcdb4229c0e79c2531a24de7726a0e980417a74fb4d030a35f535665439a0" dependencies = [ "arbitrary", "crc32fast", @@ -3219,9 +3208,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" [[package]] name = "zopfli" @@ -3271,9 +3260,9 @@ checksum = "3f423a2c17029964870cfaabb1f13dfab7d092a62a29a89264f4d36990ca414a" [[package]] name = "zune-jpeg" -version = "0.4.15" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63fd466826ec8fe25e1fc010c169213fec4e135ac39caccdba830eaa3895923" +checksum = "0f6fe2e33d02a98ee64423802e16df3de99c43e5cf5ff983767e1128b394c8ac" dependencies = [ "zune-core", ] diff --git a/Cargo.toml b/Cargo.toml index 842c4bc..d1aff98 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,7 +26,7 @@ aes = { optional = true, version = "0.8.4" } anyhow = { features = ["backtrace"], version = "1.0.98" } bytes = { optional = true, version = "1.10.1" } cbc = { optional = true, version = "0.1.2" } -clap = { features = ["color", "deprecated", "derive", "unicode", "wrap_help"], version = "4.5.39" } +clap = { features = ["color", "deprecated", "derive", "unicode", "wrap_help"], version = "4.5.40" } clap-verbosity-flag = "3.0.3" env_logger = { default-features = false, features = ["humantime"], version = "0.11.8" } ffmpeg-next = { default-features = false, features = ["codec", "format", "software-resampling"], optional = true, version = "7.1.0" } @@ -40,7 +40,7 @@ log = "0.4.27" num_cpus = { optional = true, version = "1.17.0" } pin-project = "1.1.10" regex = { default-features = false, features = ["perf", "std"], optional = true, version = "1.11.1" } -reqwest = { features = ["deflate", "gzip", "json", "stream", "zstd"], version = "0.12.19" } +reqwest = { features = ["deflate", "gzip", "json", "stream", "zstd"], version = "0.12.20" } rmp-serde = "1.3.0" scraper = { optional = true, version = "0.23.1" } serde = { features = ["derive"], version = "1.0.219" } @@ -50,7 +50,7 @@ thiserror = "2.0.12" tokio = { features = ["macros", "process", "rt-multi-thread"], version = "1.45.1" } tokio-util = { features = ["compat"], version = "0.7.15" } vgmstream = { optional = true, path = "crates/vgmstream" } -zip = { default-features = false, features = ["deflate"], optional = true, version = "4.0.0" } +zip = { default-features = false, features = ["deflate"], optional = true, version = "4.1.0" } [build-dependencies] vergen = { features = ["cargo", "emit_and_set", "rustc"], version = "9.0.6" } diff --git a/crates/vgmstream-sys/Cargo.toml b/crates/vgmstream-sys/Cargo.toml index c699bba..cfe13fd 100644 --- a/crates/vgmstream-sys/Cargo.toml +++ b/crates/vgmstream-sys/Cargo.toml @@ -5,10 +5,10 @@ license.workspace = true links = "vgmstream" name = "vgmstream-sys" repository.workspace = true -version = "0.2.0-vgmstream-r2023-16-gb2fc214a" +version = "0.2.0-vgmstream-r2023-27-g7b0c835c" [build-dependencies] -bindgen = "0.71.1" +bindgen = "0.72.0" cmake = "0.1.54" [features] diff --git a/crates/vgmstream-sys/vgmstream b/crates/vgmstream-sys/vgmstream index b2fc214..7b0c835 160000 --- a/crates/vgmstream-sys/vgmstream +++ b/crates/vgmstream-sys/vgmstream @@ -1 +1 @@ -Subproject commit b2fc214ad74a7d1e4e01a7544c2b438edb2ecb59 +Subproject commit 7b0c835cacb7717a142a77b3af6d9466b83ce7ee diff --git a/crates/vgmstream/src/lib.rs b/crates/vgmstream/src/lib.rs index 406f851..c3b960b 100644 --- a/crates/vgmstream/src/lib.rs +++ b/crates/vgmstream/src/lib.rs @@ -297,11 +297,11 @@ impl VgmStream { /// /// # Errors /// - /// Returns `Error::InitializationFailed` if the initialization failed. + /// Returns [`Error::InitializationFailed`] if the initialization failed. /// /// # Example /// - /// ```no_run + /// ``` /// use vgmstream::VgmStream; /// /// let vgmstream = VgmStream::new().unwrap(); @@ -325,7 +325,7 @@ impl VgmStream { /// * pass [`None`] to clear current config /// * remember config may change format info like channels or output format /// (recheck if calling after loading song) - pub fn setup(&mut self, config: Option<&Config>) -> () { + pub fn setup(&mut self, config: Option<&Config>) { match config { Some(cfg) => unsafe { vgmstream_sys::libvgmstream_setup( @@ -384,7 +384,7 @@ impl VgmStream { /// /// # Errors /// - /// Returns `Error::NullPointer` if `vgmstream->format` is null. + /// Returns [`Error::NullPointer`] if `vgmstream->format` is null. /// /// # Examples /// @@ -399,8 +399,8 @@ impl VgmStream { /// ``` pub fn format(&self) -> Result { match unsafe { self.as_ref().format.as_ref() } { - Some(f) => f.clone().try_into(), - None => return Err(Error::NullPointer("vgmstream->format".to_string())), + Some(f) => (*f).try_into(), + None => Err(Error::NullPointer("vgmstream->format".to_string())), } } @@ -410,9 +410,9 @@ impl VgmStream { /// /// # Errors /// - /// Returns `Error::VgmStream` if decoding failed. + /// Returns [`Error::VgmStream`] if decoding failed. /// - /// Returns `Error::NullPointer` if `vgmstream->decoder` is null. + /// Returns [`Error::NullPointer`] if `vgmstream->decoder` is null. /// /// # Examples /// @@ -427,7 +427,7 @@ impl VgmStream { /// println!("{}", buf.len()); /// } /// ``` - pub fn render<'a>(&'a mut self) -> Result<&'a [u8], Error> { + pub fn render(&mut self) -> Result<&[u8], Error> { if unsafe { vgmstream_sys::libvgmstream_render(self.inner.as_mut()) } < 0 { return Err(Error::VgmStream("libvgmstream_render".to_string())); } @@ -565,7 +565,7 @@ pub fn extensions() -> &'static [&'static str] { let exts = vgmstream_sys::libvgmstream_get_extensions(&mut ext_count); std::slice::from_raw_parts(exts, ext_count as usize) } - .into_iter() + .iter() .map(|p| unsafe { CStr::from_ptr(*p) }.to_str().unwrap()) .collect() }); @@ -584,7 +584,7 @@ pub fn common_extensions() -> &'static [&'static str] { let exts = vgmstream_sys::libvgmstream_get_common_extensions(&mut ext_count); std::slice::from_raw_parts(exts, ext_count as usize) } - .into_iter() + .iter() .map(|p| unsafe { CStr::from_ptr(*p) }.to_str().unwrap()) .collect() }); diff --git a/src/bin/mltd/download.rs b/src/bin/mltd/download.rs index 929690c..bf24679 100644 --- a/src/bin/mltd/download.rs +++ b/src/bin/mltd/download.rs @@ -1,5 +1,6 @@ use std::path::{Path, PathBuf}; +use anyhow::Result; use clap::Args; use futures::{StreamExt, stream}; use human_bytes::human_bytes; @@ -90,7 +91,7 @@ where Ok(()) } -pub async fn download_assets(args: &DownloaderArgs) -> Result<(), Error> { +pub async fn download_assets(args: &DownloaderArgs) -> Result<()> { log::debug!("create output directory at {}", args.output_dir.display()); create_dir_all(&args.output_dir).await?; diff --git a/src/bin/mltd/extract.rs b/src/bin/mltd/extract.rs index c3fbae2..d3fda57 100644 --- a/src/bin/mltd/extract.rs +++ b/src/bin/mltd/extract.rs @@ -2,11 +2,12 @@ use std::collections::BTreeMap; use std::io::{Cursor, Write}; use std::path::{Path, PathBuf}; +use anyhow::{Result, anyhow}; use clap::{Args, value_parser}; use futures::lock::Mutex; use futures::{StreamExt, TryStreamExt, stream}; use image::GenericImageView; -use mltd::Error; +use mltd::ErrorKind; use mltd::extract::audio::{Encoder, EncoderOutputOptions, MLTD_HCA_KEY}; use mltd::extract::puzzle::solve_puzzle; use mltd::extract::text::decrypt_text; @@ -64,21 +65,24 @@ pub struct ExtractorArgs { } /// Parses a single key-value pair -fn parse_key_val(s: &str) -> Result<(String, String), Error> { +fn parse_key_val(s: &str) -> Result<(String, String)> { if !s.starts_with('-') { - return Err(Error::Generic(format!("invalid -KEY=value: no `-` found in `{}`", s))); + return Err(anyhow!("invalid -KEY=value: no `-` found in `{}`", s)); } - let pos = s - .find('=') - .ok_or_else(|| Error::Generic(format!("invalid -KEY=value: no `=` found in `{}`", s)))?; + let pos = match s.find('=') { + Some(p) => p, + None => return Err(anyhow!("invalid -KEY=value: no `=` found in `{}`", s))?, + }; Ok((s[1..pos].to_owned(), s[pos + 1..].to_owned())) } /// Parses string to image format -fn parse_image_format(s: &str) -> Result { - let image_format = image::ImageFormat::from_extension(s) - .ok_or_else(|| Error::Generic(format!("invalid image format `{}`", s)))?; +fn parse_image_format(s: &str) -> Result { + let image_format = match image::ImageFormat::from_extension(s) { + Some(f) => f, + None => return Err(anyhow!("invalid image format: {}", s)), + }; Ok(image_format) } @@ -97,7 +101,7 @@ fn default_asset_ripper_path() -> PathBuf { path } -pub async fn extract_files(args: &ExtractorArgs) -> Result<(), Error> { +pub async fn extract_files(args: &ExtractorArgs) -> Result<()> { ensure_asset_ripper_installed(&args.asset_ripper_path).await?; let files = args @@ -141,8 +145,8 @@ pub async fn extract_files(args: &ExtractorArgs) -> Result<(), Error> { asset_rippers.push(Mutex::new(ripper)); port += 1; } - Err(Error::IO(e)) if e.kind() == std::io::ErrorKind::AddrInUse => port += 1, - Err(e) => return Err(e), + Err(e) if e.kind() == ErrorKind::Network => port += 1, + Err(e) => return Err(e.into()), }; } @@ -160,7 +164,7 @@ pub async fn extract_files(args: &ExtractorArgs) -> Result<(), Error> { Ok(()) } -pub async fn ensure_asset_ripper_installed

(path: P) -> Result<(), Error> +pub async fn ensure_asset_ripper_installed

(path: P) -> Result<()> where P: AsRef, { @@ -183,7 +187,7 @@ where if !input.trim().eq_ignore_ascii_case("y") { log::error!("User refused to install AssetRipper"); - return Err(Error::Generic("AssetRipper not installed".to_owned())); + return Err(anyhow!("AssetRipper not installed")); } let mut path = path.as_ref().to_path_buf(); @@ -201,7 +205,7 @@ async fn extract_file

( path: P, asset_ripper: &mut AssetRipper, args: &ExtractorArgs, -) -> Result<(), Error> +) -> Result<()> where P: AsRef, { @@ -236,7 +240,7 @@ async fn extract_assets( infos: Vec, asset_ripper: &mut AssetRipper, args: &ExtractorArgs, -) -> Result<(), Error> { +) -> Result<()> { for info in &infos { match info.entry.1.as_str() { "TextAsset" => extract_text_asset(info, asset_ripper, args).await?, @@ -275,7 +279,7 @@ async fn extract_texture2d_assets( sprite_infos: &[&AssetInfo], asset_ripper: &mut AssetRipper, args: &ExtractorArgs, -) -> Result<(), Error> { +) -> Result<()> { let asset_original_path = texture_info.original_path.as_ref().expect("original path of Texture2D should exist"); let mut asset_output_dir = args.output.join(asset_original_path); @@ -285,10 +289,12 @@ async fn extract_texture2d_assets( let mut async_reader = asset_ripper .asset_image(bundle_no, collection_no, texture_info.entry.0) .await? - .map_err(|e| std::io::Error::other(e)) + .map_err(std::io::Error::other) .into_async_read() .compat(); + tokio::io::copy(&mut async_reader, &mut image).await?; + drop(async_reader); let image = image::load_from_memory_with_format( image.into_inner().as_slice(), @@ -360,7 +366,7 @@ async fn extract_text_asset( info: &AssetInfo, asset_ripper: &mut AssetRipper, args: &ExtractorArgs, -) -> Result<(), Error> { +) -> Result<()> { let asset_original_path = info.original_path.as_ref().expect("original path of TextAsset should exist"); let mut output_dir = args.output.join(asset_original_path); @@ -371,7 +377,7 @@ async fn extract_text_asset( && !info.entry.2.ends_with(".awb") && !info.entry.2.ends_with(".gtx") { - return Err(Error::Generic(format!("unknown text asset: {}", info.entry.2))); + return Err(anyhow!("unknown text asset: {}", info.entry.2)); } let tmpdir = tempfile::tempdir()?; @@ -431,10 +437,8 @@ async fn extract_text_asset( }) .await?; - match result { - Ok(()) => (), - Err(Error::VGMStream(_)) | Err(Error::OutOfRange(..)) => break, - Err(e) => return Err(e), + if result.is_err() { + break; } } } @@ -447,7 +451,7 @@ async fn extract_text_asset( let buf = tokio::fs::read(&file_path).await?; tokio::fs::write(&output_path, decrypt_text(&buf)?).await?; } - _ => return Err(Error::Generic(String::from("this shouldn't happen"))), + _ => unreachable!("this shouldn't happen"), }; Ok(()) diff --git a/src/bin/mltd/manifest.rs b/src/bin/mltd/manifest.rs index 245f8c8..b06ee67 100644 --- a/src/bin/mltd/manifest.rs +++ b/src/bin/mltd/manifest.rs @@ -1,8 +1,8 @@ use std::fs::read; use std::path::PathBuf; +use anyhow::Result; use clap::{Args, Subcommand}; -use mltd::Error; use mltd::asset::{Asset, AssetInfo, Platform}; use mltd::manifest::Manifest; use mltd::net::{get_all_asset_versions, get_asset_version, latest_asset_version}; @@ -54,7 +54,7 @@ pub struct ManifestDownloadArgs { pub output: Option, } -pub async fn download_manifest(args: &ManifestDownloadArgs) -> Result<(), Error> { +pub async fn download_manifest(args: &ManifestDownloadArgs) -> Result<()> { let asset_version = match args.asset_version { None => latest_asset_version().await, Some(v) => get_asset_version(v).await, @@ -73,7 +73,7 @@ pub async fn download_manifest(args: &ManifestDownloadArgs) -> Result<(), Error> Ok(()) } -pub fn diff_manifest(args: &ManifestDiffArgs) -> Result<(), Error> { +pub fn diff_manifest(args: &ManifestDiffArgs) -> Result<()> { let first_manifest = Manifest::from_slice(&read(&args.first)?)?; let second_manifest = Manifest::from_slice(&read(&args.second)?)?; @@ -94,7 +94,7 @@ pub fn diff_manifest(args: &ManifestDiffArgs) -> Result<(), Error> { Ok(()) } -pub async fn list_manifests() -> Result<(), Error> { +pub async fn list_manifests() -> Result<()> { let versions = get_all_asset_versions().await?; for version in versions { @@ -107,7 +107,7 @@ pub async fn list_manifests() -> Result<(), Error> { Ok(()) } -pub async fn manifest_main(args: &ManifestArgs) -> Result<(), Error> { +pub async fn manifest_main(args: &ManifestArgs) -> Result<()> { match &args.command { ManifestCommand::Diff(args) => diff_manifest(args), ManifestCommand::Download(args) => download_manifest(args).await, diff --git a/src/mltd/asset.rs b/src/mltd/asset.rs index fa10da7..a3dc52d 100644 --- a/src/mltd/asset.rs +++ b/src/mltd/asset.rs @@ -15,7 +15,8 @@ use tokio::io::BufWriter; use tokio_util::compat::FuturesAsyncReadCompatExt; use crate::Error; -use crate::net::AssetVersion; +use crate::error::{Repr, Result}; +use crate::net::{AssetVersion, Error as NetworkError, ErrorKind as NetworkErrorKind}; use crate::util::ProgressReadAdapter; /// Base URL of MLTD asset server. @@ -65,16 +66,32 @@ impl Asset<'_> { /// # Errors /// /// - [`Error::Request`]: if it cannot send request to MLTD asset server. - async fn send_request(asset_info: &AssetInfo) -> Result { + async fn send_request(asset_info: &AssetInfo) -> Result { let client = reqwest::Client::new(); let mut headers = HeaderMap::new(); - headers.insert("X-Unity-Version", UNITY_VERSION.parse().unwrap()); - headers.insert("User-Agent", asset_info.platform.user_agent().parse().unwrap()); + headers.insert( + "X-Unity-Version", + UNITY_VERSION.parse().map_err(|_| Repr::bug("unity version should be valid"))?, + ); + headers.insert( + "User-Agent", + asset_info + .platform + .user_agent() + .parse() + .map_err(|_| Repr::bug("user agent should be valid"))?, + ); let req = client.get(asset_info.to_url()).headers(headers); - req.send().await.map_err(Error::Request) + let res = req.send().await.map_err(|e| NetworkError { + kind: NetworkErrorKind::Request, + url: asset_info.to_url(), + source: Some(e), + })?; + + Ok(res) } /// Download the specified asset from MLTD asset server. @@ -108,7 +125,7 @@ impl Asset<'_> { pub async fn download( asset_info: AssetInfo, progress_bar: Option<&mut ProgressBar>, - ) -> Result { + ) -> Result { let res = Self::send_request(&asset_info).await?; if let Some(ref pb) = progress_bar { @@ -117,13 +134,14 @@ impl Asset<'_> { log::debug!("download {} to buf", asset_info.filename); - let stream_reader = - res.bytes_stream().map_err(|e| io::Error::other(e)).into_async_read().compat(); + let stream_reader = res.bytes_stream().map_err(io::Error::other).into_async_read().compat(); let mut stream_reader = ProgressReadAdapter::new(stream_reader, progress_bar); let mut buf = Cursor::new(Vec::new()); - tokio::io::copy(&mut stream_reader, &mut buf).await?; + tokio::io::copy(&mut stream_reader, &mut buf) + .await + .map_err(|e| Repr::io("failed to download asset", Some(e)))?; Ok(Self { data: Cow::from(buf.into_inner()), info: asset_info }) } @@ -161,9 +179,13 @@ impl Asset<'_> { asset_info: &AssetInfo, output: Option<&Path>, progress_bar: Option<&mut ProgressBar>, - ) -> Result<(), Error> { + ) -> Result<()> { let output = output.unwrap_or(asset_info.filename.as_ref()); - let mut out = BufWriter::new(File::create(output).await?); + let mut out = BufWriter::new( + File::create(output) + .await + .map_err(|e| Repr::io("failed to create output file", Some(e)))?, + ); let res = Self::send_request(asset_info).await?; @@ -173,12 +195,13 @@ impl Asset<'_> { log::debug!("save asset to {}", output.display()); - let stream_reader = - res.bytes_stream().map_err(|e| io::Error::other(e)).into_async_read().compat(); + let stream_reader = res.bytes_stream().map_err(io::Error::other).into_async_read().compat(); let mut stream_reader = ProgressReadAdapter::new(stream_reader, progress_bar); - tokio::io::copy(&mut stream_reader, &mut out).await?; + tokio::io::copy(&mut stream_reader, &mut out) + .await + .map_err(|e| Repr::io("failed to download asset", Some(e)))?; Ok(()) } @@ -227,10 +250,10 @@ impl FromStr for Platform { type Err = Error; fn from_str(s: &str) -> Result { - match s.to_lowercase().as_str() { + Ok(match s.to_lowercase().as_str() { "android" => Ok(Self::Android), "ios" => Ok(Self::IOS), - s => Err(Error::UnknownPlatform(s.to_string())), - } + s => Err(Repr::UnknownPlatform(s.to_string())), + }?) } } diff --git a/src/mltd/error.rs b/src/mltd/error.rs index 9e94b82..0c1b92f 100644 --- a/src/mltd/error.rs +++ b/src/mltd/error.rs @@ -1,78 +1,132 @@ //! Error type definitions. -use std::io; - -use tokio::task::JoinError; +use std::backtrace::Backtrace; +use std::io::Error as IoError; +use std::panic::Location; + +use thiserror::Error as ThisError; + +use crate::extract::audio::AudioDecodeError; +use crate::extract::puzzle::PuzzleError; +use crate::extract::text::AesError; +use crate::manifest::ManifestError; +use crate::net::Error as NetworkError; + +#[derive(Debug, ThisError)] +#[error("{repr}")] +pub struct Error { + repr: Box, +} -/// Error type for this crate. -#[derive(thiserror::Error, Debug)] -#[non_exhaustive] -pub enum Error { - /// Manifest deserialization failed. - #[error("manifest deserialization failed: {0}")] - ManifestDeserialize(#[from] rmp_serde::decode::Error), +pub(crate) type Result = std::result::Result; - /// Manifest serialization failed. - #[error("manifest serialization failed: {0}")] - ManifestSerialize(#[from] rmp_serde::encode::Error), +impl Error { + #[must_use] + pub fn kind(&self) -> ErrorKind { + self.repr.as_ref().into() + } +} - /// VGMStream error. - #[error("vgmstream error: {0}")] - VGMStream(#[from] vgmstream::Error), +impl From for Error { + fn from(repr: Repr) -> Self { + Self { repr: Box::new(repr) } + } +} - /// FFmpeg Error. - #[error("ffmpeg error: {0}")] - FFmpeg(#[from] ffmpeg_next::Error), +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum ErrorKind { + Io, + UnknownPlatform, + Aes, + AudioDecode, + Puzzle, + Manifest, + Network, + OutOfRange, + Bug, +} - /// Reqwest response serialization failed. - #[error("response deserialization failed: {0}")] - ResponseDeserialize(reqwest::Error), +impl From<&Repr> for ErrorKind { + fn from(value: &Repr) -> Self { + match value { + Repr::Io { .. } => ErrorKind::Io, + Repr::UnknownPlatform(_) => ErrorKind::UnknownPlatform, + Repr::Aes(_) => ErrorKind::Aes, + Repr::AudioDecode(_) => ErrorKind::AudioDecode, + Repr::Puzzle(_) => ErrorKind::Puzzle, + Repr::Manifest(_) => ErrorKind::Manifest, + Repr::Network(_) => ErrorKind::Network, + Repr::OutOfRange { .. } => ErrorKind::OutOfRange, + Repr::Bug { .. } => ErrorKind::Bug, + } + } +} +/// Error type for this crate. +#[derive(ThisError, Debug)] +pub(crate) enum Repr { /// IO operation failed. - #[error("IO operation failed: {0}")] - IO(#[from] io::Error), - - /// Glob error. - #[error("glob error: {0}")] - Glob(#[from] glob::PatternError), - - /// Reqwest request failed. - #[error("failed to send request: {0}")] - Request(reqwest::Error), + #[error("{reason}, cause: {source:?}")] + Io { reason: String, source: Option }, /// Unknown platform. #[error("unknown platform: {0}")] UnknownPlatform(String), - /// Thread join failed. - #[error("failed to join thread: {0}")] - ThreadJoin(#[from] JoinError), - - /// Failed to parse integer from string. - #[error("failed to parse int: {0}")] - ParseInt(#[from] std::num::ParseIntError), - - /// AES related error. - #[error("AES error: {0}")] - Aes(String), - - /// zip related error. - #[error("zip error: {0}")] - Zip(#[from] zip::result::ZipError), + /// AES related error for text assets. + #[error("{0}")] + Aes(#[from] AesError), - /// image crate related error. - #[error("image error: {0}")] - Image(#[from] image::ImageError), + #[error("failed to decode audio: {0}")] + AudioDecode(#[from] AudioDecodeError), /// Puzzle solving failed. #[error("failed to solve puzzle: {0}")] - Puzzle(String), + Puzzle(#[from] PuzzleError), + + /// manifest related error. + #[error("{0}")] + Manifest(#[from] ManifestError), + + /// network related error. + #[error("{0}")] + Network(#[from] NetworkError), /// Array index out of range error. #[error("try to access index {0} but the length is {1}")] OutOfRange(usize, usize), - /// Generic error. - #[error("{0}")] - Generic(String), + /// Bug occurred. + #[error("bug: {msg}, at {location}\nsee backtraces above for more details")] + Bug { msg: String, location: &'static Location<'static> }, +} + +impl Repr { + #[track_caller] + pub fn bug(msg: &str) -> Self { + let backtrace = Backtrace::force_capture(); + println!("{backtrace}"); + + Self::Bug { msg: msg.to_string(), location: Location::caller() } + } + + pub fn io(reason: &str, source: Option) -> Self { + Self::Io { reason: reason.to_string(), source } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_send_sync() + where + T: Send + Sync, + { + } + + #[test] + fn test_error() { + assert_send_sync::(); + } } diff --git a/src/mltd/extract/audio.rs b/src/mltd/extract/audio.rs index f7f73db..9134296 100644 --- a/src/mltd/extract/audio.rs +++ b/src/mltd/extract/audio.rs @@ -8,9 +8,34 @@ use std::path::Path; use ffmpeg_next::Rescale; use ffmpeg_next::packet::Mut; +use thiserror::Error as ThisError; use vgmstream::{StreamFile, VgmStream}; -use crate::Error; +use crate::error::{Error, Repr, Result}; + +#[derive(Debug, ThisError)] +pub(crate) enum AudioDecodeError { + #[error("vgmstream error: {0}")] + VgmStream(#[from] vgmstream::Error), + + #[error("ffmpeg error: {reason}")] + FFmpeg { reason: &'static str, source: Option }, + + #[error("unsupported channel layout: {0:?}")] + UnsupportedChannelLayout(vgmstream::ChannelMapping), +} + +impl AudioDecodeError { + pub fn ffmpeg(reason: &'static str, source: Option) -> Self { + Self::FFmpeg { reason, source } + } +} + +impl From for Error { + fn from(value: AudioDecodeError) -> Self { + Repr::from(value).into() + } +} /// HCA key used to decrypt MLTD audio asset. pub const MLTD_HCA_KEY: u64 = 765_765_765_765_765; @@ -88,31 +113,34 @@ impl<'a> Encoder<'a> { subsong_index: usize, output_dir: P, output_options: EncoderOutputOptions<'a>, - ) -> Result + ) -> Result where P: AsRef, { - let mut vgmstream = VgmStream::new()?; - let mut sf = StreamFile::open(input_file.as_ref())?; - vgmstream.open(&mut sf, subsong_index)?; + let mut vgmstream = VgmStream::new().map_err(AudioDecodeError::from)?; + let mut sf = StreamFile::open(input_file.as_ref()).map_err(AudioDecodeError::from)?; + vgmstream.open(&mut sf, subsong_index).map_err(AudioDecodeError::from)?; - let acb_fmt = vgmstream.format()?; + let acb_fmt = vgmstream.format().map_err(AudioDecodeError::from)?; if subsong_index >= acb_fmt.subsong_count as usize { - return Err(Error::OutOfRange(subsong_index, acb_fmt.subsong_count as usize)); + return Err(Repr::OutOfRange(subsong_index, acb_fmt.subsong_count as usize).into()); } log::trace!("audio format: {acb_fmt:#?}"); let codec = ffmpeg_next::encoder::find_by_name(output_options.codec) - .ok_or(Error::Generic(String::from("Failed to find encoder")))?; + .ok_or(AudioDecodeError::ffmpeg("codec not found", None))?; - let mut encoder = ffmpeg_next::codec::Context::new_with_codec(codec).encoder().audio()?; + let mut encoder = ffmpeg_next::codec::Context::new_with_codec(codec) + .encoder() + .audio() + .map_err(|e| AudioDecodeError::ffmpeg("failed to create encoder", Some(e)))?; let supported_formats = get_supported_formats(&encoder)?; log::trace!("supported formats: {supported_formats:?}"); - let from_sample_format = to_ffmpeg_sample_format(acb_fmt.sample_format)?; + let from_sample_format = to_ffmpeg_sample_format(acb_fmt.sample_format); let from_channel_layout = to_ffmpeg_channel_layout(acb_fmt.channel_layout)?; encoder.set_format(choose_format(&supported_formats, from_sample_format)); @@ -139,26 +167,30 @@ impl<'a> Encoder<'a> { let mut output = match output_options.options { Some(ref o) => ffmpeg_next::format::output_with(&output_path, o.clone()), None => ffmpeg_next::format::output(output_dir.as_ref()), - }?; + } + .map_err(|e| AudioDecodeError::ffmpeg("failed to open output", Some(e)))?; if output.format().flags().contains(ffmpeg_next::format::Flags::GLOBAL_HEADER) { let flag = ffmpeg_next::codec::Flags::from_bits( unsafe { *encoder.as_mut_ptr() }.flags as c_uint, ) - .unwrap(); + .ok_or_else(|| AudioDecodeError::ffmpeg("invalid encoder flags", None))?; encoder.set_flags(flag | ffmpeg_next::codec::Flags::GLOBAL_HEADER); } let encoder = match output_options.options { Some(ref o) => encoder.open_with(o.clone()), None => encoder.open(), - }?; + } + .map_err(|e| AudioDecodeError::ffmpeg("failed to open encoder", Some(e)))?; - let _ = output.add_stream_with(&encoder.0.0.0)?; + let _ = output + .add_stream_with(&encoder.0.0.0) + .map_err(|e| AudioDecodeError::ffmpeg("failed to add stream", Some(e))); let frame_size = if encoder .codec() - .unwrap() + .ok_or_else(|| AudioDecodeError::ffmpeg("encoder does not have a codec", None))? .capabilities() .intersects(ffmpeg_next::codec::Capabilities::VARIABLE_FRAME_SIZE) { @@ -179,7 +211,8 @@ impl<'a> Encoder<'a> { let resampler = ffmpeg_next::software::resampler( (from_sample_format, from_channel_layout, acb_fmt.sample_rate as u32), (encoder.format(), encoder.channel_layout(), encoder.rate()), - )?; + ) + .map_err(|e| AudioDecodeError::ffmpeg("failed to create resampler", Some(e)))?; Ok(Self { vgmstream, @@ -202,7 +235,7 @@ impl<'a> Encoder<'a> { /// Encodes the next audio frame and writes the encoded packets to the output file. /// /// Returns `false` if there is more audio data to encode. - fn write_frame(&mut self, eof: bool) -> Result { + fn write_frame(&mut self, eof: bool) -> Result { if eof { self.encoder.send_eof() } else { self.encoder.send_frame(&self.frame) }?; loop { @@ -216,7 +249,7 @@ impl<'a> Encoder<'a> { return Ok(false); } - return Err(Error::FFmpeg(e)); + return Err(e); } packet.rescale_ts( @@ -290,7 +323,7 @@ impl<'a> Encoder<'a> { /// Encodes the next audio frame. /// /// Returns `false` if there is more audio data to encode. - fn write_audio_frame(&mut self) -> Result { + fn write_audio_frame(&mut self) -> Result { if let Some(frame) = self.get_audio_frame() { assert_eq!(self.resampler.delay(), None, "there should be no delay"); @@ -331,42 +364,51 @@ impl<'a> Encoder<'a> { /// # Errors /// /// [`Error::FFmpeg`]: if encoding failed. - pub fn encode(&mut self) -> Result<(), Error> { + pub fn encode(&mut self) -> Result<()> { match self.options { Some(ref o) => { - let _ = self.output.write_header_with(o.clone())?; + let _ = self.output.write_header_with(o.clone()).map_err(|e| { + AudioDecodeError::ffmpeg("failed to write audio header", Some(e)) + })?; } - None => self.output.write_header()?, + None => self + .output + .write_header() + .map_err(|e| AudioDecodeError::ffmpeg("failed to write audio header", Some(e)))?, } - while self.write_audio_frame()? {} + while self + .write_audio_frame() + .map_err(|e| AudioDecodeError::ffmpeg("failed to write audio frame", Some(e)))? + { + } - self.output.write_trailer()?; + self.output + .write_trailer() + .map_err(|e| AudioDecodeError::ffmpeg("failed to write audio trailer", Some(e)))?; Ok(()) } } -fn to_ffmpeg_sample_format( - value: vgmstream::SampleFormat, -) -> Result { +fn to_ffmpeg_sample_format(value: vgmstream::SampleFormat) -> ffmpeg_next::format::Sample { match value { vgmstream::SampleFormat::Pcm16 => { - Ok(ffmpeg_next::format::Sample::I16(ffmpeg_next::format::sample::Type::Packed)) + ffmpeg_next::format::Sample::I16(ffmpeg_next::format::sample::Type::Packed) } vgmstream::SampleFormat::Pcm24 | vgmstream::SampleFormat::Pcm32 => { - Ok(ffmpeg_next::format::Sample::I32(ffmpeg_next::format::sample::Type::Packed)) + ffmpeg_next::format::Sample::I32(ffmpeg_next::format::sample::Type::Packed) } vgmstream::SampleFormat::Float => { - Ok(ffmpeg_next::format::Sample::F32(ffmpeg_next::format::sample::Type::Packed)) + ffmpeg_next::format::Sample::F32(ffmpeg_next::format::sample::Type::Packed) } } } fn to_ffmpeg_channel_layout( - value: vgmstream::ChannelMapping, -) -> Result { - match value { + layout: vgmstream::ChannelMapping, +) -> Result { + match layout { vgmstream::ChannelMapping::MONO => Ok(ffmpeg_next::ChannelLayout::MONO), vgmstream::ChannelMapping::STEREO => Ok(ffmpeg_next::ChannelLayout::STEREO), vgmstream::ChannelMapping::_2POINT1 => Ok(ffmpeg_next::ChannelLayout::_2POINT1), @@ -382,7 +424,7 @@ fn to_ffmpeg_channel_layout( vgmstream::ChannelMapping::_7POINT1_SURROUND => { Ok(ffmpeg_next::ChannelLayout::_7POINT1_WIDE) } - _ => Err(Error::Generic(format!("Unsupported channel layout: {value:?}"))), + _ => Err(AudioDecodeError::UnsupportedChannelLayout(layout).into()), } } @@ -392,10 +434,17 @@ fn to_ffmpeg_channel_layout( /// `get_supported_formats_new` below. fn get_supported_formats( encoder: &ffmpeg_next::codec::encoder::Encoder, -) -> Result, Error> { - match encoder.codec().unwrap().audio()?.formats() { +) -> Result> { + let codec = encoder + .codec() + .ok_or_else(|| AudioDecodeError::ffmpeg("encoder does not have a codec", None))?; + + let codec = + codec.audio().map_err(|e| AudioDecodeError::ffmpeg("codec is not audio", Some(e)))?; + + match codec.formats() { Some(f) => Ok(f.collect()), - None => Err(Error::Generic(String::from("no supported audio formats found"))), + None => Err(AudioDecodeError::ffmpeg("codec has no formats", None).into()), } } @@ -403,7 +452,7 @@ fn get_supported_formats( #[cfg(any())] fn get_supported_formats_new( encoder: &ffmpeg_next::codec::encoder::Encoder, -) -> Result, Error> { +) -> Result> { let mut supported_formats = std::ptr::null(); let mut num_formats = 0; unsafe { @@ -417,7 +466,7 @@ fn get_supported_formats_new( ) }; if supported_formats.is_null() { - return Err(Error::Generic(String::from("Failed to get supported configs"))); + return Err(AudioDecodeError::ffmpeg("Failed to get supported configs", None).into()); } Ok(unsafe { diff --git a/src/mltd/extract/puzzle.rs b/src/mltd/extract/puzzle.rs index f07868d..609352b 100644 --- a/src/mltd/extract/puzzle.rs +++ b/src/mltd/extract/puzzle.rs @@ -1,8 +1,24 @@ //! Puzzle handling. use image::{DynamicImage, GenericImageView, SubImage, imageops}; +use thiserror::Error as ThisError; -use crate::Error; +use crate::error::{Error, Repr}; + +#[derive(Debug, ThisError)] +pub(crate) enum PuzzleError { + #[error("puzzle not found for texture {0}")] + PuzzleNotFound(String), + + #[error("puzzle {0} is not implemented yet")] + NotImplemented(String), +} + +impl From for Error { + fn from(value: PuzzleError) -> Self { + Repr::from(value).into() + } +} /// Solves a puzzle. pub fn solve_puzzle( @@ -12,11 +28,11 @@ pub fn solve_puzzle( ) -> Result, Error> { let (puzzle_name, _) = NAME_PUZZLE_MAP .iter() - .find(|(_, r)| regex::Regex::new(r).expect("regex error").is_match(texture_name)) - .ok_or(Error::Puzzle(String::from("cannot find puzzle")))?; + .find(|(_, r)| regex::Regex::new(r).map_or_else(|_| false, |r| r.is_match(texture_name))) + .ok_or_else(|| PuzzleError::PuzzleNotFound(texture_name.to_string()))?; - let puzzle = - piece_map(puzzle_name).ok_or(Error::Puzzle(String::from("puzzle not implemented")))?; + let puzzle = piece_map(puzzle_name) + .ok_or_else(|| PuzzleError::NotImplemented((*puzzle_name).to_string()))?; let mut puzzle_imgs = Vec::new(); @@ -25,9 +41,8 @@ pub fn solve_puzzle( let mut puzzle_img = DynamicImage::new_rgba8(puzzle.width, puzzle.height); for j in 0..piece_count { - let piece = pieces - .get(i * piece_count + j) - .ok_or(Error::Puzzle(String::from("piece not found")))?; + let piece_idx = i * piece_count + j; + let piece = pieces.get(piece_idx).ok_or(Repr::OutOfRange(piece_idx, pieces.len()))?; let mut piece = img .crop_imm(piece.offsets().0, piece.offsets().1, piece.width(), piece.height()) diff --git a/src/mltd/extract/text.rs b/src/mltd/extract/text.rs index 334084e..e3d3508 100644 --- a/src/mltd/extract/text.rs +++ b/src/mltd/extract/text.rs @@ -5,8 +5,41 @@ use aes::cipher::block_padding::Pkcs7; use aes::cipher::inout::InOutBufReserved; use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}; use cbc::{Decryptor, Encryptor}; +use thiserror::Error as ThisError; -use crate::Error; +use crate::error::{Error, Repr, Result}; + +#[derive(Debug, ThisError)] +#[error("AES error: {kind}")] +pub(crate) struct AesError { + pub kind: AesErrorKind, + pub input: Vec, +} + +impl AesError { + pub fn pad(input: Vec) -> Self { + Self { kind: AesErrorKind::Pad, input } + } + + pub fn unpad(input: Vec) -> Self { + Self { kind: AesErrorKind::Unpad, input } + } +} + +impl From for Error { + fn from(value: AesError) -> Self { + Repr::from(value).into() + } +} + +#[derive(Debug, ThisError)] +pub(crate) enum AesErrorKind { + #[error("failed to pad input")] + Pad, + + #[error("failed to unpad output")] + Unpad, +} /// The key used to derive [`MLTD_TEXT_DECRYPT_KEY`] and /// [`MLTD_TEXT_DECRYPT_IV`]. @@ -65,17 +98,20 @@ pub type MltdTextDecryptor = Decryptor; /// let text = b"Hello, world!"; /// let cipher = encrypt_text(text).unwrap(); /// ``` -pub fn encrypt_text(text: &[u8]) -> Result, Error> { +pub fn encrypt_text(plaintext: &[u8]) -> Result> { let encryptor = MltdTextEncryptor::new(MLTD_TEXT_DECRYPT_KEY.into(), MLTD_TEXT_DECRYPT_IV.into()); - let mut buf = text.to_owned(); + let mut buf = plaintext.to_owned(); - let buf = InOutBufReserved::from_mut_slice(&mut buf, text.len()) - .map_err(|e| Error::Aes(e.to_string()))?; - let buf = - encryptor.encrypt_padded_inout_mut::(buf).map_err(|e| Error::Aes(e.to_string()))?; + debug_assert_eq!(buf.len(), plaintext.len()); - Ok(buf.to_owned()) + let buf = InOutBufReserved::from_mut_slice(&mut buf, plaintext.len()) + .map_err(|e| Repr::bug(&e.to_string()))?; + let cipher = encryptor + .encrypt_padded_inout_mut::(buf) + .map_err(|_| AesError::pad(plaintext.to_owned()))?; + + Ok(cipher.to_owned()) } /// Decrypts text using AES-192-CBC with MLTD's key and IV. @@ -94,14 +130,14 @@ pub fn encrypt_text(text: &[u8]) -> Result, Error> { /// let cipher = b"Hello, world!"; /// let text = decrypt_text(cipher).unwrap(); /// ``` -pub fn decrypt_text(cipher: &[u8]) -> Result, Error> { +pub fn decrypt_text(cipher: &[u8]) -> Result> { let decryptor = MltdTextDecryptor::new(MLTD_TEXT_DECRYPT_KEY.into(), MLTD_TEXT_DECRYPT_IV.into()); - let mut buf = cipher.to_owned(); - let buf = decryptor + let mut buf = cipher.to_owned(); + let plaintext = decryptor .decrypt_padded_inout_mut::(buf.as_mut_slice().into()) - .map_err(|e| Error::Aes(e.to_string()))?; + .map_err(|_| AesError::unpad(cipher.to_owned()))?; - Ok(buf.to_owned()) + Ok(plaintext.to_owned()) } diff --git a/src/mltd/lib.rs b/src/mltd/lib.rs index 4586829..9e07976 100644 --- a/src/mltd/lib.rs +++ b/src/mltd/lib.rs @@ -17,4 +17,4 @@ pub mod manifest; pub mod net; pub mod util; -pub use self::error::Error; +pub use self::error::{Error, ErrorKind}; diff --git a/src/mltd/manifest.rs b/src/mltd/manifest.rs index 96655a8..33be0f7 100644 --- a/src/mltd/manifest.rs +++ b/src/mltd/manifest.rs @@ -5,9 +5,40 @@ use std::ops::Deref; use linked_hash_map::LinkedHashMap; use serde::{Deserialize, Serialize}; +use thiserror::Error as ThisError; use crate::asset::Asset; -use crate::error::Error; +use crate::error::{Error, Repr, Result}; + +#[derive(Debug, ThisError)] +#[error("manifest error: {kind}")] +pub(crate) struct ManifestError { + pub kind: ManifestErrorKind, + pub manifest: Vec, +} + +#[derive(Debug, ThisError)] +pub(crate) enum ManifestErrorKind { + /// Manifest deserialization failed. + #[error("cannot deserialize manifest: {0}")] + Deserialize(#[from] rmp_serde::decode::Error), + + /// Manifest serialization failed. + #[error("cannot serialize manifest: {0}")] + Serialize(#[from] rmp_serde::encode::Error), +} + +impl ManifestError { + pub fn new(kind: ManifestErrorKind, manifest: Vec) -> Self { + Self { kind, manifest } + } +} + +impl From for Error { + fn from(value: ManifestError) -> Self { + Repr::from(value).into() + } +} /// An entry in the manifest file. /// @@ -24,23 +55,10 @@ pub struct ManifestEntry(pub String, pub String, pub usize); #[serde(transparent)] pub struct Manifest { /// The underlying raw manifest data. - pub data: [LinkedHashMap; 1], + data: [LinkedHashMap; 1], } impl Manifest { - /// Deserializes the specified bytes into a raw manifest. - /// - /// The bytes must be in message pack format. - /// - /// # Errors - /// - /// This function will return [`Error::ManifestDeserialize`] if - /// it cannot deserialize the message pack bytes. - #[inline] - pub fn from_slice(value: &[u8]) -> Result { - Ok(Self { data: rmp_serde::from_slice(value)? }) - } - /// Computes the difference from `other` manifest. #[must_use] pub fn diff<'a>(&'a self, other: &'a Manifest) -> ManifestDiff<'a> { @@ -86,6 +104,11 @@ impl Manifest { pub fn asset_size(&self) -> usize { self.data[0].values().fold(0, |acc, v| acc + v.2) } + + pub fn from_slice(value: &[u8]) -> Result { + rmp_serde::from_slice(value) + .map_err(|e| ManifestError::new(e.into(), value.to_vec()).into()) + } } impl Deref for Manifest { @@ -104,6 +127,22 @@ impl TryFrom> for Manifest { } } +impl TryFrom<&[u8]> for Manifest { + type Error = Error; + + fn try_from(value: &[u8]) -> Result { + Self::from_slice(value) + } +} + +impl TryFrom for Vec { + type Error = Error; + + fn try_from(value: Manifest) -> Result { + rmp_serde::to_vec(&value).map_err(|e| ManifestError::new(e.into(), Vec::new()).into()) + } +} + /// A diff of two manifests. #[derive(Debug, Serialize)] pub struct ManifestDiff<'a> { @@ -131,14 +170,16 @@ fn init() { #[cfg(test)] mod tests { + use std::ops::Deref; + use super::Manifest; #[test] fn test_raw_manifest_from_slice() { let expected = include_bytes!("../../tests/test1.msgpack"); - let manifest: Manifest = Manifest::from_slice(expected).unwrap(); + let manifest: Manifest = Manifest::try_from(&expected[..]).unwrap(); - assert_eq!(*expected, *rmp_serde::to_vec(&[&*manifest]).unwrap()); + assert_eq!(*expected, *rmp_serde::to_vec(&[manifest.deref()]).unwrap()); } #[test] diff --git a/src/mltd/net/asset_ripper.rs b/src/mltd/net/asset_ripper.rs index a02bb1d..b0dfe12 100644 --- a/src/mltd/net/asset_ripper.rs +++ b/src/mltd/net/asset_ripper.rs @@ -8,9 +8,11 @@ use std::process::{Child, Command, Stdio}; use futures::TryStreamExt; use indicatif::ProgressBar; use reqwest::Response; +use scraper::error::SelectorErrorKind; use tokio_util::compat::FuturesAsyncReadCompatExt; -use crate::Error; +use crate::error::{Repr, Result}; +use crate::net::Error; use crate::util::ProgressReadAdapter; /// Asset entry on `/Collections/View`. @@ -40,9 +42,46 @@ pub struct AssetRipper { process: Option, } +fn selector_should_be_valid(_: SelectorErrorKind) -> Repr { + Repr::bug("selector should be valid") +} + impl AssetRipper { + fn full_url(&self, url: &str) -> Result { + let url = format!("{}/{}", &self.base_url, url); + reqwest::Url::parse(&url).map_err(|_| Repr::bug("url should be valid").into()) + } + + /// Sends a GET request with the given path parameter to AssetRipper. + async fn send_request(&mut self, url: &reqwest::Url, path: &str) -> Result { + let client = reqwest::Client::new(); + + let req = client.get(url.clone()); + let req = if path.is_empty() { req } else { req.query(&[("Path", path)]) }; + + let res = match req.send().await { + Ok(r) => Ok(r), + Err(e) => Err(Error::request(url.clone(), Some(e))), + }?; + + Ok(res) + } + + async fn get_text(&mut self, url: &str, path: &str) -> Result { + let url = self.full_url(url)?; + + let res = self.send_request(&url, path).await?; + + let text = match res.text().await { + Ok(t) => Ok(t), + Err(e) => Err(Error::decode(url, Some(e))), + }?; + + Ok(text) + } + /// Starts a new AssetRipper instance with the given executable path and port. - pub fn new

(path: P, port: u16) -> Result + pub fn new

(path: P, port: u16) -> Result where P: AsRef, { @@ -56,81 +95,63 @@ impl AssetRipper { .spawn() { Ok(mut process) => { - let reader = BufReader::new(process.stdout.take().unwrap()); + let reader = BufReader::new( + process.stdout.take().ok_or_else(|| Repr::bug("failed to get stdout"))?, + ); for line in reader.lines() { - let line = line.expect("failed to read line"); + let line = line.map_err(|e| { + Repr::io("failed to read line from AssetRipper output", Some(e)) + })?; if line.contains("listening on: http://") { break; } } - process + Ok(process) } - Err(err) => return Err(err.into()), - }; + Err(err) => Err(Repr::io("failed to start AssetRipper", Some(err))), + }?; Ok(Self { base_url: format!("http://localhost:{port}"), process: Some(process) }) } /// Connects th an existing AssetRipper instance with the given host and port. - pub fn connect(host: &str, port: u16) -> Result { - Ok(Self { base_url: format!("http://{host}:{port}"), process: None }) + #[must_use] + pub fn connect(host: &str, port: u16) -> Self { + Self { base_url: format!("http://{host}:{port}"), process: None } } /// Loads an asset or a folder into the AssetRipper. - pub async fn load

(&mut self, path: P) -> Result<(), Error> + pub async fn load

(&mut self, path: P) -> Result<()> where P: AsRef, { let path = path.as_ref(); let url = if path.is_dir() { "LoadFolder" } else { "LoadFile" }; - let url = format!("{}/{}", &self.base_url, url); + let url = self.full_url(url)?; let mut form = HashMap::new(); form.insert("path", path.to_string_lossy().to_string()); let client = reqwest::Client::new(); - let req = client.post(url).form(&form); - - self.check_process()?; + let req = client.post(url.clone()).form(&form); if let Err(e) = req.send().await { - return Err(Error::Request(e)); + return Err(Error::request(url, Some(e)).into()); } Ok(()) } - /// Sends a GET request with the given path parameter to AssetRipper. - pub async fn send_request(&mut self, url: &str, path: &str) -> Result { - let url = format!("{}/{}", &self.base_url, url); - let client = reqwest::Client::new(); - - let req = client.get(url).query(&[("Path", path)]); - - self.check_process()?; - - let res = match req.send().await { - Ok(r) => r, - Err(e) => return Err(Error::Request(e)), - }; - - Ok(res) - } - /// Returns a list of loaded bundles. - pub async fn bundles(&mut self) -> Result, Error> { + pub async fn bundles(&mut self) -> Result> { let path = r#"{"P":[]}"#; - - let html = match self.send_request("Bundles/View", path).await?.text().await { - Ok(html) => html, - Err(e) => return Err(Error::ResponseDeserialize(e)), - }; - + let html = self.get_text("Bundles/View", path).await?; let html = scraper::Html::parse_document(&html); - let selector = - scraper::Selector::parse("#app > ul:nth-child(3) a").expect("cannot create selector"); + + let selector = scraper::Selector::parse("#app > ul:nth-child(3) a") + .map_err(selector_should_be_valid)?; let mut bundles: Vec<_> = html.select(&selector).map(|node| node.inner_html()).collect(); // Remove the last two items (Generated Engine Collections, Generated Hierarchy Assets) @@ -140,17 +161,13 @@ impl AssetRipper { } /// Returns a list of collections in the specified bundle. - pub async fn collections(&mut self, bundle_no: usize) -> Result, Error> { + pub async fn collections(&mut self, bundle_no: usize) -> Result> { let path = format!(r#"{{"P":[{bundle_no}]}}"#); - - let html = match self.send_request("Bundles/View", &path).await?.text().await { - Ok(html) => html, - Err(e) => return Err(Error::ResponseDeserialize(e)), - }; - + let html = self.get_text("Bundles/View", &path).await?; let html = scraper::Html::parse_document(&html); - let selector = - scraper::Selector::parse("#app > ul:nth-child(5) a").expect("cannot create selector"); + + let selector = scraper::Selector::parse("#app > ul:nth-child(5) a") + .map_err(selector_should_be_valid)?; Ok(html.select(&selector).map(|node| node.inner_html()).collect()) } @@ -160,42 +177,42 @@ impl AssetRipper { &mut self, bundle_no: usize, collection_no: usize, - ) -> Result, Error> { + ) -> Result> { let path = format!(r#"{{"B":{{"P":[{bundle_no}]}},"I":{collection_no}}}"#); + let html = self.get_text("Collections/View", &path).await?; + let html = scraper::Html::parse_document(&html); - let html = match self.send_request("Collections/View", &path).await?.text().await { - Ok(html) => html, - Err(e) => return Err(Error::ResponseDeserialize(e)), - }; + let selector = scraper::Selector::parse("tbody > tr").map_err(selector_should_be_valid)?; - let html = scraper::Html::parse_document(&html); - let selector = scraper::Selector::parse("tbody > tr").expect("cannot create selector"); + let first_child_should_exist = || Repr::bug("first child should exist"); + let inner_text_should_exist = || Repr::bug("inner text should exist"); let mut assets = Vec::new(); for nodes in html.select(&selector) { let children = nodes.children().collect::>(); let path_id: i64 = children[0] .first_child() - .expect("") + .ok_or_else(first_child_should_exist)? .value() .as_text() - .expect("inner text") - .parse()?; + .ok_or_else(inner_text_should_exist)? + .parse() + .map_err(|_| Repr::bug("inner text should be integer"))?; let class = children[1] .first_child() - .expect("") + .ok_or_else(first_child_should_exist)? .value() .as_text() - .expect("inner text") + .ok_or_else(inner_text_should_exist)? .to_string(); let name = children[2] .first_child() - .expect("") + .ok_or_else(first_child_should_exist)? .first_child() - .expect("") + .ok_or_else(first_child_should_exist)? .value() .as_text() - .expect("inner text") + .ok_or_else(inner_text_should_exist)? .to_string(); assets.push(AssetEntry(path_id, class, name)); @@ -205,42 +222,40 @@ impl AssetRipper { } /// Returns the number of assets in the specified collection. - pub async fn asset_count( - &mut self, - bundle_no: usize, - collection_no: usize, - ) -> Result { + pub async fn asset_count(&mut self, bundle_no: usize, collection_no: usize) -> Result { let path = format!(r#"{{"B":{{"P":[{bundle_no}]}},"I":{collection_no}}}"#); + let text = self.get_text("Collections/Count", &path).await?; - let text = match self.send_request("Collections/Count", &path).await?.text().await { - Ok(text) => text, - Err(e) => return Err(Error::ResponseDeserialize(e)), - }; + let count = text.parse().map_err(|_| Repr::bug("text should be integer"))?; - Ok(text.parse()?) + Ok(count) } /// Returns information about the specified asset. + /// + /// # Errors + /// + /// Returns [`crate::Error`] with [`crate::ErrorKind::Network`] if the request fails. pub async fn asset_info( &mut self, bundle_no: usize, collection_no: usize, path_id: i64, - ) -> Result { + ) -> Result { let path = format!(r#"{{"C":{{"B":{{"P":[{bundle_no}]}},"I":{collection_no}}},"D":{path_id}}}"#); - - let html = match self.send_request("Assets/View", &path).await?.text().await { - Ok(html) => html, - Err(e) => return Err(Error::ResponseDeserialize(e)), - }; - + let html = self.get_text("Assets/View", &path).await?; let html = scraper::Html::parse_document(&html); - let name_selector = scraper::Selector::parse("h1").expect("cannot create selector"); + + let name_selector = scraper::Selector::parse("h1").map_err(selector_should_be_valid)?; let info_selector = - scraper::Selector::parse("#nav-information td").expect("cannot create selector"); + scraper::Selector::parse("#nav-information td").map_err(selector_should_be_valid)?; - let name = html.select(&name_selector).next().unwrap().inner_html(); + let name = html + .select(&name_selector) + .next() + .ok_or_else(|| Repr::bug("asset name should exist"))? + .inner_html(); let info = html.select(&info_selector).map(|node| node.inner_html()).collect::>(); let class = info[3].clone(); @@ -260,154 +275,184 @@ impl AssetRipper { } /// Returns the JSON representation of the specified asset. + /// + /// # Errors + /// + /// Returns [`crate::Error`] with [`crate::ErrorKind::Network`] if the request fails. pub async fn asset_json( &mut self, bundle_no: usize, collection_no: usize, path_id: i64, - ) -> Result { + ) -> Result { let path = format!(r#"{{"C":{{"B":{{"P":[{bundle_no}]}},"I":{collection_no}}},"D":{path_id}}}"#); - match self.send_request("Assets/Json", &path).await?.json().await { + let url = self.full_url("Assets/Json")?; + let value = match self.send_request(&url, &path).await?.json().await { Ok(json) => Ok(json), - Err(e) => Err(Error::ResponseDeserialize(e)), - } + Err(e) => Err(Error::decode(url, Some(e))), + }?; + + Ok(value) } /// Returns the text data stream of the specified asset. + /// + /// # Errors + /// + /// Returns [`crate::Error`] with [`crate::ErrorKind::Network`] if the request fails. pub async fn asset_text( &mut self, bundle_no: usize, collection_no: usize, path_id: i64, - ) -> Result>, Error> { + ) -> Result>> { let path = format!(r#"{{"C":{{"B":{{"P":[{bundle_no}]}},"I":{collection_no}}},"D":{path_id}}}"#); + let url = self.full_url("Assets/Text")?; - Ok(self.send_request("Assets/Text", &path).await?.bytes_stream()) + Ok(self.send_request(&url, &path).await?.bytes_stream()) } /// Returns the image (in png) data stream of the specified asset. + /// + /// # Errors + /// + /// Returns [`crate::Error`] with [`crate::ErrorKind::Network`] if the request fails. pub async fn asset_image( &mut self, bundle_no: usize, collection_no: usize, path_id: i64, - ) -> Result> + use<>, Error> { + ) -> Result>> { let path = format!(r#"{{"C":{{"B":{{"P":[{bundle_no}]}},"I":{collection_no}}},"D":{path_id}}}"#); - let url = format!("{}/Assets/Image", &self.base_url); + let url = self.full_url("Assets/Image")?; let client = reqwest::Client::new(); - let req = client.get(url).query(&[("Path", path)]).query(&[("Extension", "png")]); - - self.check_process()?; + let req = client.get(url.clone()).query(&[("Path", path)]).query(&[("Extension", "png")]); let res = match req.send().await { - Ok(r) => r, - Err(e) => return Err(Error::Request(e)), - }; + Ok(r) => Ok(r), + Err(e) => Err(Error::request(url, Some(e))), + }?; Ok(res.bytes_stream()) } /// Exports the primary content on the AssetRipper. - pub async fn export_primary

(&mut self, path: P) -> Result<(), Error> + /// + /// # Errors + /// + /// Returns [`crate::Error`] with [`crate::ErrorKind::Network`] if the export request fails. + pub async fn export_primary

(&mut self, path: P) -> Result<()> where P: AsRef, { - let url = format!("{}/Export/PrimaryContent", &self.base_url); + let url = self.full_url("Export/PrimaryContent")?; let mut form = HashMap::new(); form.insert("path", path.as_ref().to_string_lossy().to_string()); let client = reqwest::Client::new(); - let req = client.post(&url).form(&form); + let req = client.post(url.clone()).form(&form); if let Err(e) = req.send().await { - return Err(Error::Request(e)); - }; - - Ok(()) - } - - fn check_process(&mut self) -> Result<(), Error> { - if let Some(ref mut process) = self.process { - match process.try_wait() { - Ok(None) => Ok(()), - Ok(Some(status)) => { - Err(Error::Generic(format!("AssetRipper process died with status {status}"))) - } - Err(err) => Err(err.into()), - }?; + return Err(Error::request(url, Some(e)).into()); } Ok(()) } /// Downloads the latest release zip of AssetRipper from GitHub to the given path. + /// + /// # Errors + /// + /// Returns [`crate::Error`] with [`crate::ErrorKind::Network`] if the download fails. + /// + /// Returns [`crate::Error`] with [`crate::ErrorKind::Io`] if the file cannot be accessed. + #[cfg(all( + any(target_os = "windows", target_os = "linux", target_os = "macos"), + any(target_arch = "x86_64", target_arch = "aarch64"), + ))] pub async fn download_latest_zip

( path: P, progress_bar: Option<&mut ProgressBar>, - ) -> Result<(), Error> + ) -> Result<()> where P: AsRef, { let client = reqwest::Client::new(); - let base_url = "https://github.com/AssetRipper/AssetRipper/releases/latest/download/"; let zip_path = path.as_ref().join("AssetRipper.zip"); - let file = match tokio::fs::File::create_new(&zip_path).await { - Ok(mut file) => { - let os = match std::env::consts::OS { - "windows" => "win", - "linux" => "linux", - "macos" => "mac", - _ => return Err(Error::Generic("unsupported OS".to_string())), - }; - let arch = match std::env::consts::ARCH { - "x86_64" => "x64", - "aarch64" => "arm64", - _ => return Err(Error::Generic("unsupported architecture".to_string())), - }; - let req = client.get(format!("{base_url}/AssetRipper_{os}_{arch}.zip")); - - let res = match req.send().await { - Ok(res) => res, - Err(e) => return Err(Error::Request(e)), - }; - - let stream_reader = res - .bytes_stream() - .map_err(|e| std::io::Error::other(e)) - .into_async_read() - .compat(); - - let mut stream_reader = ProgressReadAdapter::new(stream_reader, progress_bar); - tokio::io::copy(&mut stream_reader, &mut file).await?; - - file + + let mut downloaded = false; + + let mut file = match tokio::fs::File::create_new(&zip_path).await { + Ok(file) => Ok(file), + Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => { + log::debug!("AssetRipper.zip already exists, skipping download"); + downloaded = true; + let f = tokio::fs::File::open(&zip_path) + .await + .map_err(|e| Repr::io("failed to open file", Some(e)))?; + + Ok(f) } - Err(e) => match e.kind() { - std::io::ErrorKind::AlreadyExists => { - log::debug!("AssetRipper.zip already exists, skipping download"); - tokio::fs::File::open(&zip_path).await? - } - _ => return Err(e.into()), - }, - }; + Err(e) => Err(Repr::io("failed to create file", Some(e))), + }?; + + if !downloaded { + let os = match std::env::consts::OS { + "windows" => "win", + "linux" => "linux", + "macos" => "mac", + _ => unreachable!("unsupported OS"), + }; + let arch = match std::env::consts::ARCH { + "x86_64" => "x64", + "aarch64" => "arm64", + _ => unreachable!("unsupported architecture"), + }; + + let base_url = "https://github.com/AssetRipper/AssetRipper/releases/latest/download/"; + let url = reqwest::Url::parse(&format!("{base_url}/AssetRipper_{os}_{arch}.zip")) + .map_err(|_| Repr::bug("url should be valid"))?; + + let res = match client.get(url.clone()).send().await { + Ok(res) => Ok(res), + Err(e) => Err(Error::request(url, Some(e))), + }?; - let mut file = zip::ZipArchive::new(file.into_std().await)?; + let stream_reader = + res.bytes_stream().map_err(std::io::Error::other).into_async_read().compat(); + + let mut stream_reader = ProgressReadAdapter::new(stream_reader, progress_bar); + tokio::io::copy(&mut stream_reader, &mut file) + .await + .map_err(|e| Repr::io("failed to download zip file", Some(e)))?; + } + + let mut file = zip::ZipArchive::new(file.into_std().await).map_err(|e| match e { + zip::result::ZipError::Io(e) => Repr::io("failed to open zip file", Some(e)), + _ => Repr::bug("zip file should be valid"), + })?; let mut output_path = zip_path.clone(); output_path.pop(); output_path.push("AssetRipper"); + log::debug!("unzip to {}", output_path.display()); - file.extract(output_path)?; + file.extract(output_path).map_err(|e| match e { + zip::result::ZipError::Io(e) => Repr::io("failed to extract zip file", Some(e)), + _ => Repr::bug("zip file should be valid"), + })?; drop(file); - tokio::fs::remove_file(zip_path).await?; + tokio::fs::remove_file(zip_path) + .await + .map_err(|e| Repr::io("failed to remove zip file", Some(e)))?; Ok(()) } diff --git a/src/mltd/net/matsuri_api.rs b/src/mltd/net/matsuri_api.rs index 6fe1274..81c2778 100644 --- a/src/mltd/net/matsuri_api.rs +++ b/src/mltd/net/matsuri_api.rs @@ -1,8 +1,10 @@ //! Functions to fetch manifest version from `api.matsurihi.me`. use serde::Deserialize; +use serde::de::DeserializeOwned; -use crate::Error; +use crate::error::{Repr, Result}; +use crate::net::Error; /// matshrihi.me MLTD v2 API `/version/assets/:app` response body structure. #[derive(Debug, Clone, Deserialize)] @@ -47,7 +49,36 @@ pub struct VersionInfo { pub asset_version: AssetVersion, } -const MATSURI_API_ENDPOINT: &str = "https://api.matsurihi.me/api/mltd/v2"; +macro_rules! matsuri_api_endpoint { + () => { + "https://api.matsurihi.me/api/mltd/v2" + }; +} + +pub const MATSURI_API_ENDPOINT: & str = matsuri_api_endpoint!(); + +async fn send_request(url: &str) -> Result { + let client = reqwest::Client::new(); + + let url = match reqwest::Url::parse(url) { + Ok(u) => Ok(u), + Err(_) => Err(Repr::bug(&format!("invalid url: {url:?}"))), + }?; + + let req = client.get(url.clone()).query(&[("prettyPrint", "false")]); + + let res = match req.send().await { + Ok(r) => Ok(r), + Err(e) => Err(Error::request(url.clone(), Some(e))), + }?; + + let value: T = match res.json().await { + Ok(v) => Ok(v), + Err(e) => Err(Error::decode(url, Some(e))), + }?; + + Ok(value) +} /// Gets the latest manifest filename and version from matsurihi.me. /// @@ -70,21 +101,9 @@ const MATSURI_API_ENDPOINT: &str = "https://api.matsurihi.me/api/mltd/v2"; /// let asset_version = latest_asset_version().await.unwrap().version; /// }); /// ``` -pub async fn latest_asset_version() -> Result { - let client = reqwest::Client::new(); - let req = client - .get(format!("{}{}", MATSURI_API_ENDPOINT, "/version/latest")) - .query(&[("prettyPrint", "false")]); - - let res = match req.send().await { - Ok(r) => r, - Err(e) => return Err(Error::Request(e)), - }; - - let version_info: VersionInfo = match res.json().await { - Ok(info) => info, - Err(e) => return Err(Error::ResponseDeserialize(e)), - }; +pub async fn latest_asset_version() -> Result { + let url = concat!(matsuri_api_endpoint!(), "/version/latest"); + let version_info: VersionInfo = send_request(url).await?; Ok(version_info.asset_version) } @@ -100,21 +119,9 @@ pub async fn latest_asset_version() -> Result { /// [`Error::Request`]: if it cannot send request to `api.matsurihi.me`. /// /// [`Error::ResponseDeserialize`]: if it cannot deserialize response. -pub async fn get_all_asset_versions() -> Result, Error> { - let client = reqwest::Client::new(); - let req = client - .get(format!("{}{}", MATSURI_API_ENDPOINT, "/version/assets")) - .query(&[("prettyPrint", "false")]); - - let res = match req.send().await { - Ok(r) => r, - Err(e) => return Err(Error::Request(e)), - }; - - let versions: Vec = match res.json().await { - Ok(v) => v, - Err(e) => return Err(Error::ResponseDeserialize(e)), - }; +pub async fn get_all_asset_versions() -> Result> { + let url = concat!(matsuri_api_endpoint!(), "/version/assets"); + let versions: Vec = send_request(url).await?; Ok(versions) } @@ -141,23 +148,11 @@ pub async fn get_all_asset_versions() -> Result, Error> { /// assert_eq!(asset_version.version, 1); /// }); /// ``` -pub async fn get_asset_version(version: u64) -> Result { - let client = reqwest::Client::new(); - let req = client - .get(format!("{MATSURI_API_ENDPOINT}/version/assets/{version}")) - .query(&[("prettyPrint", "false")]); - - let res = match req.send().await { - Ok(r) => r, - Err(e) => return Err(Error::Request(e)), - }; - - let asset_version: AssetVersion = match res.json().await { - Ok(v) => v, - Err(e) => return Err(Error::ResponseDeserialize(e)), - }; +pub async fn get_asset_version(version: u64) -> Result { + let url = format!("{MATSURI_API_ENDPOINT}/version/assets/{version}"); + let version: AssetVersion = send_request(&url).await?; - Ok(asset_version) + Ok(version) } #[cfg(test)] diff --git a/src/mltd/net/mod.rs b/src/mltd/net/mod.rs index 8478937..294a53a 100644 --- a/src/mltd/net/mod.rs +++ b/src/mltd/net/mod.rs @@ -3,5 +3,42 @@ mod asset_ripper; mod matsuri_api; +pub use thiserror::Error as ThisError; + +use crate::error::Repr; + pub use self::asset_ripper::*; pub use self::matsuri_api::*; + +#[derive(Debug, ThisError)] +#[error("network error: {kind}")] +pub(crate) struct Error { + pub kind: ErrorKind, + pub url: reqwest::Url, + pub source: Option, +} + +#[derive(Debug, ThisError)] +pub(crate) enum ErrorKind { + #[error("failed to send request")] + Request, + + #[error("failed to decode response body")] + Decode, +} + +impl Error { + pub fn request(url: reqwest::Url, source: Option) -> Self { + Self { kind: ErrorKind::Request, url, source } + } + + pub fn decode(url: reqwest::Url, source: Option) -> Self { + Self { kind: ErrorKind::Decode, url, source } + } +} + +impl From for crate::Error { + fn from(err: Error) -> Self { + Repr::from(err).into() + } +} diff --git a/src/mltd/util.rs b/src/mltd/util.rs index 5a2a440..25dd955 100644 --- a/src/mltd/util.rs +++ b/src/mltd/util.rs @@ -11,6 +11,10 @@ use pin_project::pin_project; use tokio::io::{AsyncRead, ReadBuf}; /// Custom log formatter used in this crate. +/// +/// # Errors +/// +/// Returns [`std::io::Error`] if failed to write to [`std::io::Stdout`]. pub fn log_formatter(buf: &mut Formatter, record: &Record) -> Result<()> { let color_code = match record.level() { log::Level::Error => 1, // red From 3375cd3d694b8f79ff1080c117c0a82a2c276540 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=91=E5=8F=AF=E8=83=BD=E5=BE=88=E5=BB=A2?= Date: Mon, 16 Jun 2025 13:33:52 +0800 Subject: [PATCH 5/9] :bug: fix text asset decryption --- Cargo.lock | 54 +++++++++++++++++ Cargo.toml | 7 +++ src/mltd/extract/text.rs | 122 +++++++++++++++++++++++---------------- 3 files changed, 133 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 839e6bb..436a943 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -225,6 +225,15 @@ version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block-padding" version = "0.3.3" @@ -612,6 +621,17 @@ dependencies = [ "syn", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + [[package]] name = "displaydoc" version = "0.2.5" @@ -996,6 +1016,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "html5ever" version = "0.29.1" @@ -1524,6 +1553,7 @@ dependencies = [ "anyhow", "bytes", "cbc", + "cipher", "clap", "clap-verbosity-flag", "ctor", @@ -1537,6 +1567,7 @@ dependencies = [ "linked-hash-map", "log", "num_cpus", + "pbkdf2", "pin-project", "regex", "reqwest", @@ -1544,6 +1575,7 @@ dependencies = [ "scraper", "serde", "serde_json", + "sha1", "tempfile", "thiserror", "tokio", @@ -1722,6 +1754,17 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", + "sha1", +] + [[package]] name = "percent-encoding" version = "2.3.1" @@ -2259,6 +2302,17 @@ dependencies = [ "stable_deref_trait", ] +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shlex" version = "1.3.0" diff --git a/Cargo.toml b/Cargo.toml index d1aff98..8744bfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ aes = { optional = true, version = "0.8.4" } anyhow = { features = ["backtrace"], version = "1.0.98" } bytes = { optional = true, version = "1.10.1" } cbc = { optional = true, version = "0.1.2" } +cipher = { features = ["alloc", "block-padding"], optional = true, version = "0.4.4" } clap = { features = ["color", "deprecated", "derive", "unicode", "wrap_help"], version = "4.5.40" } clap-verbosity-flag = "3.0.3" env_logger = { default-features = false, features = ["humantime"], version = "0.11.8" } @@ -38,6 +39,7 @@ indicatif = { default-features = false, version = "0.17.11" } linked-hash-map = { features = ["serde_impl"], version = "0.5.6" } log = "0.4.27" num_cpus = { optional = true, version = "1.17.0" } +pbkdf2 = { features = ["sha1"], optional = true, version = "0.12.2" } pin-project = "1.1.10" regex = { default-features = false, features = ["perf", "std"], optional = true, version = "1.11.1" } reqwest = { features = ["deflate", "gzip", "json", "stream", "zstd"], version = "0.12.20" } @@ -45,6 +47,7 @@ rmp-serde = "1.3.0" scraper = { optional = true, version = "0.23.1" } serde = { features = ["derive"], version = "1.0.219" } serde_json = { optional = true, version = "1.0.140" } +sha1 = { optional = true, version = "0.10.6" } tempfile = { optional = true, version = "3.20.0" } thiserror = "2.0.12" tokio = { features = ["macros", "process", "rt-multi-thread"], version = "1.45.1" } @@ -86,13 +89,17 @@ extract = [ "dep:aes", "dep:bytes", "dep:cbc", + "dep:cipher", "dep:ffmpeg-next", "dep:image", "dep:regex", "dep:num_cpus", + "dep:pbkdf2", "dep:scraper", + "dep:sha1", "dep:serde_json", "dep:tempfile", "dep:vgmstream", "dep:zip" ] +cipher = [] diff --git a/src/mltd/extract/text.rs b/src/mltd/extract/text.rs index e3d3508..cca80f0 100644 --- a/src/mltd/extract/text.rs +++ b/src/mltd/extract/text.rs @@ -1,10 +1,11 @@ //! Encrypt and decrypt text assets in MLTD. +use std::cell::LazyCell; + use aes::Aes192; -use aes::cipher::block_padding::Pkcs7; -use aes::cipher::inout::InOutBufReserved; -use aes::cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}; use cbc::{Decryptor, Encryptor}; +use cipher::block_padding::Pkcs7; +use cipher::{BlockDecryptMut, BlockEncryptMut, KeyIvInit}; use thiserror::Error as ThisError; use crate::error::{Error, Repr, Result}; @@ -17,10 +18,6 @@ pub(crate) struct AesError { } impl AesError { - pub fn pad(input: Vec) -> Self { - Self { kind: AesErrorKind::Pad, input } - } - pub fn unpad(input: Vec) -> Self { Self { kind: AesErrorKind::Unpad, input } } @@ -34,9 +31,6 @@ impl From for Error { #[derive(Debug, ThisError)] pub(crate) enum AesErrorKind { - #[error("failed to pad input")] - Pad, - #[error("failed to unpad output")] Unpad, } @@ -51,19 +45,23 @@ pub const MLTD_TEXT_PBKDF2_HMAC_SHA1_SALT: &[u8; 9] = b"DAISUL___"; /// The number of iterations used to derive [`MLTD_TEXT_DECRYPT_KEY`] and /// [`MLTD_TEXT_DECRYPT_IV`]. -pub const MLTD_TEXT_PBKDF2_HMAC_SHA1_ROUNDS: u32 = 1000; +pub const MLTD_TEXT_PBKDF2_HMAC_SHA1_ROUNDS: u32 = 1_000; + +const MLTD_TEXT_DECRYPT_KEY_IV: LazyCell<[u8; 40]> = LazyCell::new(|| { + pbkdf2::pbkdf2_hmac_array::( + MLTD_TEXT_PBKDF2_HMAC_SHA1_KEY, + MLTD_TEXT_PBKDF2_HMAC_SHA1_SALT, + MLTD_TEXT_PBKDF2_HMAC_SHA1_ROUNDS, + ) +}); /// The AES-192-CBC key used to decrypt the text asset. -/// +/// /// It is derived from [`MLTD_TEXT_PBKDF2_HMAC_SHA1_KEY`] and /// [`MLTD_TEXT_PBKDF2_HMAC_SHA1_SALT`] using PBKDF2-HMAC-SHA1, where /// the first 24 bytes of the derived key are used as the actual key. -#[rustfmt::skip] -pub const MLTD_TEXT_DECRYPT_KEY: &[u8; 24] = &[ - 0xad, 0x3f, 0x0f, 0x89, 0xee, 0x51, 0xc5, 0x37, - 0x73, 0x1f, 0x17, 0x96, 0xf7, 0x5c, 0x71, 0x84, - 0x01, 0x61, 0x75, 0x6d, 0xa0, 0xd4, 0x86, 0xc9, -]; +pub const MLTD_TEXT_DECRYPT_KEY: LazyCell<[u8; 24]> = + LazyCell::new(|| (&MLTD_TEXT_DECRYPT_KEY_IV[0..24]).try_into().unwrap()); /// The AES-192-CBC initialization vector used to decrypt the text asset. /// @@ -71,10 +69,8 @@ pub const MLTD_TEXT_DECRYPT_KEY: &[u8; 24] = &[ /// [`MLTD_TEXT_PBKDF2_HMAC_SHA1_SALT`] using PBKDF2-HMAC-SHA1, where /// the last 16 bytes of the derived key are used as the actual IV. #[rustfmt::skip] -pub const MLTD_TEXT_DECRYPT_IV: &[u8; 16] = &[ - 0x4e, 0x40, 0xb3, 0x8a, 0xeb, 0xf1, 0xa8, 0x53, - 0x12, 0x2c, 0x5f, 0xad, 0xcc, 0xa3, 0x68, 0x5d, -]; +pub const MLTD_TEXT_DECRYPT_IV: LazyCell<[u8; 16]> = + LazyCell::new(|| (&MLTD_TEXT_DECRYPT_KEY_IV[24..40]).try_into().unwrap()); /// AES-192-CBC encryptor for text assets in MLTD. pub type MltdTextEncryptor = Encryptor; @@ -86,32 +82,21 @@ pub type MltdTextDecryptor = Decryptor; /// /// The input text is padded with PKCS7 padding. /// -/// # Errors -/// -/// [`Error::Aes`]: if encryption failed. -/// /// # Example /// /// ```no_run /// use mltd::extract::text::encrypt_text; /// /// let text = b"Hello, world!"; -/// let cipher = encrypt_text(text).unwrap(); +/// let cipher = encrypt_text(text); /// ``` -pub fn encrypt_text(plaintext: &[u8]) -> Result> { - let encryptor = - MltdTextEncryptor::new(MLTD_TEXT_DECRYPT_KEY.into(), MLTD_TEXT_DECRYPT_IV.into()); - let mut buf = plaintext.to_owned(); - - debug_assert_eq!(buf.len(), plaintext.len()); +pub fn encrypt_text(plaintext: &[u8]) -> Vec { + let encryptor = MltdTextEncryptor::new( + MLTD_TEXT_DECRYPT_KEY.as_ref().into(), + MLTD_TEXT_DECRYPT_IV.as_ref().into(), + ); - let buf = InOutBufReserved::from_mut_slice(&mut buf, plaintext.len()) - .map_err(|e| Repr::bug(&e.to_string()))?; - let cipher = encryptor - .encrypt_padded_inout_mut::(buf) - .map_err(|_| AesError::pad(plaintext.to_owned()))?; - - Ok(cipher.to_owned()) + encryptor.encrypt_padded_vec_mut::(plaintext) } /// Decrypts text using AES-192-CBC with MLTD's key and IV. @@ -124,20 +109,57 @@ pub fn encrypt_text(plaintext: &[u8]) -> Result> { /// /// # Example /// -/// ```no_run +/// ``` /// use mltd::extract::text::decrypt_text; /// -/// let cipher = b"Hello, world!"; -/// let text = decrypt_text(cipher).unwrap(); +/// let cipher = [ +/// 0xca, 0x64, 0x14, 0x8e, 0x80, 0x9e, 0x50, 0xc9, +/// 0xe3, 0x4e, 0x18, 0x6f, 0x1e, 0x9c, 0x3e, 0xe2, +/// ]; +/// let text = decrypt_text(&cipher).unwrap(); +/// assert_eq!(b"Hello, world!", text.as_slice()); /// ``` pub fn decrypt_text(cipher: &[u8]) -> Result> { - let decryptor = - MltdTextDecryptor::new(MLTD_TEXT_DECRYPT_KEY.into(), MLTD_TEXT_DECRYPT_IV.into()); + let decryptor = MltdTextDecryptor::new( + MLTD_TEXT_DECRYPT_KEY.as_ref().into(), + MLTD_TEXT_DECRYPT_IV.as_ref().into(), + ); + + let plaintext = match decryptor.decrypt_padded_vec_mut::(&cipher) { + Ok(plaintext) => Ok(plaintext), + Err(_) => Err(AesError::unpad(cipher.to_owned())), + }?; + + Ok(plaintext) +} - let mut buf = cipher.to_owned(); - let plaintext = decryptor - .decrypt_padded_inout_mut::(buf.as_mut_slice().into()) - .map_err(|_| AesError::unpad(cipher.to_owned()))?; +#[cfg(test)] +mod tests { + use cipher::BlockSizeUser; - Ok(plaintext.to_owned()) + use super::*; + + #[test] + fn test_key_iv() { + #[rustfmt::skip] + let expected = [ + 0xad, 0x3f, 0x0f, 0x89, 0xee, 0x51, 0xc5, 0x37, + 0x73, 0x1f, 0x17, 0x96, 0xf7, 0x5c, 0x71, 0x84, + 0x01, 0x61, 0x75, 0x6d, 0xa0, 0xd4, 0x86, 0xc9, + 0x4e, 0x40, 0xb3, 0x8a, 0xeb, 0xf1, 0xa8, 0x53, + 0x12, 0x2c, 0x5f, 0xad, 0xcc, 0xa3, 0x68, 0x5d, + ]; + + assert_eq!(MLTD_TEXT_DECRYPT_KEY_IV.as_ref(), &expected); + } + + #[test] + fn test_encrypt_decrypt() { + let expect = b"Hello, world!"; + let cipher = encrypt_text(expect); + assert_eq!(cipher.len(), Aes192::block_size()); + + let got = decrypt_text(&cipher).unwrap(); + assert_eq!(expect, got.as_slice()); + } } From df1b83bb512a00ea596316471000f34c0251d1f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=91=E5=8F=AF=E8=83=BD=E5=BE=88=E5=BB=A2?= Date: Mon, 16 Jun 2025 13:55:03 +0800 Subject: [PATCH 6/9] :rotating_light: clippy --- src/bin/mltd/extract.rs | 2 +- src/mltd/extract/puzzle.rs | 8 +++++-- src/mltd/extract/text.rs | 18 +++++++-------- src/mltd/manifest.rs | 5 +++++ src/mltd/net/asset_ripper.rs | 43 ++++++++++++++++++++++++++++++++---- 5 files changed, 60 insertions(+), 16 deletions(-) diff --git a/src/bin/mltd/extract.rs b/src/bin/mltd/extract.rs index d3fda57..6819c92 100644 --- a/src/bin/mltd/extract.rs +++ b/src/bin/mltd/extract.rs @@ -321,7 +321,7 @@ async fn extract_texture2d_assets( let width = json.pointer("/m_Rect/m_Width").unwrap().as_u64().unwrap() as u32; let height = json.pointer("/m_Rect/m_Height").unwrap().as_u64().unwrap() as u32; - let sprite_id = info.entry.2.rsplit_once("_").unwrap().1.parse::().expect(""); + let sprite_id = info.entry.2.rsplit_once("_").unwrap().1.parse::().unwrap(); let piece = image.view(x, y, width, height); rects.insert(sprite_id, piece); } diff --git a/src/mltd/extract/puzzle.rs b/src/mltd/extract/puzzle.rs index 609352b..50d63bb 100644 --- a/src/mltd/extract/puzzle.rs +++ b/src/mltd/extract/puzzle.rs @@ -3,7 +3,7 @@ use image::{DynamicImage, GenericImageView, SubImage, imageops}; use thiserror::Error as ThisError; -use crate::error::{Error, Repr}; +use crate::error::{Error, Repr, Result}; #[derive(Debug, ThisError)] pub(crate) enum PuzzleError { @@ -21,11 +21,15 @@ impl From for Error { } /// Solves a puzzle. +/// +/// # Errors +/// +/// Returns [`crate::Error`] with [`crate::ErrorKind::Puzzle`] if the puzzle cannot be solved. pub fn solve_puzzle( texture_name: &str, img: &DynamicImage, pieces: &[SubImage<&DynamicImage>], -) -> Result, Error> { +) -> Result> { let (puzzle_name, _) = NAME_PUZZLE_MAP .iter() .find(|(_, r)| regex::Regex::new(r).map_or_else(|_| false, |r| r.is_match(texture_name))) diff --git a/src/mltd/extract/text.rs b/src/mltd/extract/text.rs index cca80f0..28128eb 100644 --- a/src/mltd/extract/text.rs +++ b/src/mltd/extract/text.rs @@ -1,6 +1,6 @@ //! Encrypt and decrypt text assets in MLTD. -use std::cell::LazyCell; +use std::sync::LazyLock; use aes::Aes192; use cbc::{Decryptor, Encryptor}; @@ -47,7 +47,7 @@ pub const MLTD_TEXT_PBKDF2_HMAC_SHA1_SALT: &[u8; 9] = b"DAISUL___"; /// [`MLTD_TEXT_DECRYPT_IV`]. pub const MLTD_TEXT_PBKDF2_HMAC_SHA1_ROUNDS: u32 = 1_000; -const MLTD_TEXT_DECRYPT_KEY_IV: LazyCell<[u8; 40]> = LazyCell::new(|| { +static MLTD_TEXT_DECRYPT_KEY_IV: LazyLock<[u8; 40]> = LazyLock::new(|| { pbkdf2::pbkdf2_hmac_array::( MLTD_TEXT_PBKDF2_HMAC_SHA1_KEY, MLTD_TEXT_PBKDF2_HMAC_SHA1_SALT, @@ -60,8 +60,8 @@ const MLTD_TEXT_DECRYPT_KEY_IV: LazyCell<[u8; 40]> = LazyCell::new(|| { /// It is derived from [`MLTD_TEXT_PBKDF2_HMAC_SHA1_KEY`] and /// [`MLTD_TEXT_PBKDF2_HMAC_SHA1_SALT`] using PBKDF2-HMAC-SHA1, where /// the first 24 bytes of the derived key are used as the actual key. -pub const MLTD_TEXT_DECRYPT_KEY: LazyCell<[u8; 24]> = - LazyCell::new(|| (&MLTD_TEXT_DECRYPT_KEY_IV[0..24]).try_into().unwrap()); +pub static MLTD_TEXT_DECRYPT_KEY: LazyLock<[u8; 24]> = + LazyLock::new(|| (&MLTD_TEXT_DECRYPT_KEY_IV[0..24]).try_into().unwrap()); /// The AES-192-CBC initialization vector used to decrypt the text asset. /// @@ -69,8 +69,8 @@ pub const MLTD_TEXT_DECRYPT_KEY: LazyCell<[u8; 24]> = /// [`MLTD_TEXT_PBKDF2_HMAC_SHA1_SALT`] using PBKDF2-HMAC-SHA1, where /// the last 16 bytes of the derived key are used as the actual IV. #[rustfmt::skip] -pub const MLTD_TEXT_DECRYPT_IV: LazyCell<[u8; 16]> = - LazyCell::new(|| (&MLTD_TEXT_DECRYPT_KEY_IV[24..40]).try_into().unwrap()); +pub static MLTD_TEXT_DECRYPT_IV: LazyLock<[u8; 16]> = + LazyLock::new(|| (&MLTD_TEXT_DECRYPT_KEY_IV[24..40]).try_into().unwrap()); /// AES-192-CBC encryptor for text assets in MLTD. pub type MltdTextEncryptor = Encryptor; @@ -84,7 +84,7 @@ pub type MltdTextDecryptor = Decryptor; /// /// # Example /// -/// ```no_run +/// ``` /// use mltd::extract::text::encrypt_text; /// /// let text = b"Hello, world!"; @@ -105,7 +105,7 @@ pub fn encrypt_text(plaintext: &[u8]) -> Vec { /// /// # Errors /// -/// [`Error::Aes`]: if decryption failed. +/// Returns [`crate::Error`] with [`crate::ErrorKind::Aes`] if decryption failed. /// /// # Example /// @@ -125,7 +125,7 @@ pub fn decrypt_text(cipher: &[u8]) -> Result> { MLTD_TEXT_DECRYPT_IV.as_ref().into(), ); - let plaintext = match decryptor.decrypt_padded_vec_mut::(&cipher) { + let plaintext = match decryptor.decrypt_padded_vec_mut::(cipher) { Ok(plaintext) => Ok(plaintext), Err(_) => Err(AesError::unpad(cipher.to_owned())), }?; diff --git a/src/mltd/manifest.rs b/src/mltd/manifest.rs index 33be0f7..8f1a98e 100644 --- a/src/mltd/manifest.rs +++ b/src/mltd/manifest.rs @@ -105,6 +105,11 @@ impl Manifest { self.data[0].values().fold(0, |acc, v| acc + v.2) } + /// Deserializes the raw manifest. + /// + /// # Errors + /// + /// Returns [`crate::Error`] with [`crate::ErrorKind::Manifest`] if the deserialization fails. pub fn from_slice(value: &[u8]) -> Result { rmp_serde::from_slice(value) .map_err(|e| ManifestError::new(e.into(), value.to_vec()).into()) diff --git a/src/mltd/net/asset_ripper.rs b/src/mltd/net/asset_ripper.rs index b0dfe12..a529321 100644 --- a/src/mltd/net/asset_ripper.rs +++ b/src/mltd/net/asset_ripper.rs @@ -81,6 +81,10 @@ impl AssetRipper { } /// Starts a new AssetRipper instance with the given executable path and port. + /// + /// # Errors + /// + /// Returns [`crate::Error`] with [`crate::ErrorKind::Io`] if the process fails to start. pub fn new

(path: P, port: u16) -> Result where P: AsRef, @@ -115,13 +119,28 @@ impl AssetRipper { Ok(Self { base_url: format!("http://localhost:{port}"), process: Some(process) }) } - /// Connects th an existing AssetRipper instance with the given host and port. - #[must_use] - pub fn connect(host: &str, port: u16) -> Self { - Self { base_url: format!("http://{host}:{port}"), process: None } + /// Connects to an existing AssetRipper instance with the given host and port. + /// + /// # Errors + /// + /// Returns [`crate::Error`] with [`crate::ErrorKind::Network`] if the request fails. + pub async fn connect(host: &str, port: u16) -> Result { + let client = reqwest::Client::new(); + let this = Self { base_url: format!("http://{host}:{port}"), process: None }; + + let url = this.full_url("")?; + let req = client.head(url.clone()); + + let _ = req.send().await.map_err(|e| Error::request(url, Some(e)))?; + + Ok(this) } /// Loads an asset or a folder into the AssetRipper. + /// + /// # Errors + /// + /// Returns [`crate::Error`] with [`crate::ErrorKind::Network`] if the request fails. pub async fn load

(&mut self, path: P) -> Result<()> where P: AsRef, @@ -145,6 +164,10 @@ impl AssetRipper { } /// Returns a list of loaded bundles. + /// + /// # Errors + /// + /// Returns [`crate::Error`] with [`crate::ErrorKind::Network`] if the request fails. pub async fn bundles(&mut self) -> Result> { let path = r#"{"P":[]}"#; let html = self.get_text("Bundles/View", path).await?; @@ -161,6 +184,10 @@ impl AssetRipper { } /// Returns a list of collections in the specified bundle. + /// + /// # Errors + /// + /// Returns [`crate::Error`] with [`crate::ErrorKind::Network`] if the request fails. pub async fn collections(&mut self, bundle_no: usize) -> Result> { let path = format!(r#"{{"P":[{bundle_no}]}}"#); let html = self.get_text("Bundles/View", &path).await?; @@ -173,6 +200,10 @@ impl AssetRipper { } /// Returns the number of assets in the specified collection. + /// + /// # Errors + /// + /// Returns [`crate::Error`] with [`crate::ErrorKind::Network`] if the request fails. pub async fn assets( &mut self, bundle_no: usize, @@ -222,6 +253,10 @@ impl AssetRipper { } /// Returns the number of assets in the specified collection. + /// + /// # Errors + /// + /// Returns [`crate::Error`] with [`crate::ErrorKind::Network`] if the request fails. pub async fn asset_count(&mut self, bundle_no: usize, collection_no: usize) -> Result { let path = format!(r#"{{"B":{{"P":[{bundle_no}]}},"I":{collection_no}}}"#); let text = self.get_text("Collections/Count", &path).await?; From db8aa8256ffad91af3e51c4a8080939828d0fe39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=91=E5=8F=AF=E8=83=BD=E5=BE=88=E5=BB=A2?= Date: Mon, 16 Jun 2025 15:31:32 +0800 Subject: [PATCH 7/9] :bug: fix text asset output path --- src/bin/mltd/extract.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/bin/mltd/extract.rs b/src/bin/mltd/extract.rs index 6819c92..eb79c52 100644 --- a/src/bin/mltd/extract.rs +++ b/src/bin/mltd/extract.rs @@ -373,10 +373,7 @@ async fn extract_text_asset( output_dir.pop(); create_dir_all(&output_dir).await?; - if !info.entry.2.ends_with(".acb") - && !info.entry.2.ends_with(".awb") - && !info.entry.2.ends_with(".gtx") - { + if ["acb", "awb", "gtx"].iter().find(|ext| info.entry.2.ends_with(*ext)) == None { return Err(anyhow!("unknown text asset: {}", info.entry.2)); } @@ -394,7 +391,8 @@ async fn extract_text_asset( tokio::fs::rename(&file_path, file_path.with_extension("")).await?; // According to https://github.com/vgmstream/vgmstream/blob/master/doc/USAGE.md#decryption-keys, - // we can specify the decryption key in the .hcakey file. + // we can specify the decryption key in the .hcakey file so that vgmstream doesn't have + // to brute-force the key. let mut key_file = tokio::fs::File::create(file_path.with_file_name(".hcakey")).await?; key_file.write_all(MLTD_HCA_KEY.to_string().as_bytes()).await?; @@ -444,7 +442,8 @@ async fn extract_text_asset( } // AES-192-CBC encrypted plot text n if n.ends_with(".gtx") => { - let output_path = output_dir.with_extension("").with_extension("txt"); + let output_path = + output_dir.join(&info.entry.2).with_extension("txt"); log::info!("extracting text to {}", output_path.display()); From 0c5a9c466d5f85b86149ec126588a363ebaa4c69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=91=E5=8F=AF=E8=83=BD=E5=BE=88=E5=BB=A2?= Date: Mon, 16 Jun 2025 21:39:51 +0800 Subject: [PATCH 8/9] :sparkles: support normal text extraction --- src/bin/mltd/extract.rs | 133 ++++++++++++++++++++++++++--------- src/mltd/extract/text.rs | 66 +++++++++-------- src/mltd/net/asset_ripper.rs | 18 +++++ 3 files changed, 152 insertions(+), 65 deletions(-) diff --git a/src/bin/mltd/extract.rs b/src/bin/mltd/extract.rs index eb79c52..49d68e7 100644 --- a/src/bin/mltd/extract.rs +++ b/src/bin/mltd/extract.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeMap; +use std::collections::{BTreeMap}; use std::io::{Cursor, Write}; use std::path::{Path, PathBuf}; @@ -10,7 +10,7 @@ use image::GenericImageView; use mltd::ErrorKind; use mltd::extract::audio::{Encoder, EncoderOutputOptions, MLTD_HCA_KEY}; use mltd::extract::puzzle::solve_puzzle; -use mltd::extract::text::decrypt_text; +use mltd::extract::text::{ENCRYPTED_FILE_EXTENSIONS, UNENCRYPTED_FILE_EXTENSIONS, decrypt_text}; use mltd::net::{AssetInfo, AssetRipper}; use tokio::fs::create_dir_all; use tokio::io::AsyncWriteExt; @@ -174,12 +174,11 @@ where log::info!("AssetRipper is not found at {}", path.as_ref().display()); - println!( - "Trying to download AssetRipper. This project is not affiliated with, sponsored, or endorsed by AssetRipper." - ); - println!("By downloading, you agree to the terms of the license of AssetRipper."); - - print!("Do you want to install it now? (y/N) "); + print!(concat!( + "Trying to download AssetRipper. This project is not affiliated with, sponsored, or endorsed by AssetRipper.\n", + "By downloading, you agree to the terms of the license of AssetRipper.\n", + "Do you want to install it now? (y/N) " + )); std::io::stdout().flush()?; let mut input = String::new(); @@ -243,7 +242,28 @@ async fn extract_assets( ) -> Result<()> { for info in &infos { match info.entry.1.as_str() { - "TextAsset" => extract_text_asset(info, asset_ripper, args).await?, + "TextAsset" => { + let file_name = PathBuf::from( + info.original_path + .as_ref() + .ok_or_else(|| anyhow!("original path should exist"))?, + ) + .file_stem() + .ok_or_else(|| anyhow!("file stem should exist"))? + .to_str() + .ok_or_else(|| anyhow!("file stem should be string"))? + .to_owned(); + + if ENCRYPTED_FILE_EXTENSIONS + .iter() + .chain(UNENCRYPTED_FILE_EXTENSIONS) + .any(|ext| file_name.ends_with(ext)) + { + extract_binary_asset(info, asset_ripper, args).await?; + } else { + extract_text_asset(bundle_no, collection_no, info, asset_ripper, args).await?; + } + } // Texture2D requires all relavent Sprite infos to be extracted "Texture2D" => { @@ -260,7 +280,7 @@ async fn extract_assets( asset_ripper, args, ) - .await? + .await?; } } "Sprite" => (), // sprites should be handled in Texture2D extractor @@ -359,23 +379,31 @@ async fn extract_texture2d_assets( Ok(()) } -/// Extracts a TextAsset. +/// Extracts a TextAsset with binary content. /// -/// Audio assets are binary TextAsset, so they are handled here as well. -async fn extract_text_asset( +/// # Panics +/// +/// panics if the asset is not a TextAsset with binary content. +async fn extract_binary_asset( info: &AssetInfo, asset_ripper: &mut AssetRipper, args: &ExtractorArgs, ) -> Result<()> { - let asset_original_path = - info.original_path.as_ref().expect("original path of TextAsset should exist"); + let asset_original_path = &PathBuf::from( + info.original_path.as_ref().expect("original path of TextAsset should exist"), + ); + + // remove .bytes extension + let asset_original_filename = + asset_original_path.with_extension("").file_name().unwrap().to_string_lossy().into_owned(); + let asset_original_extension = + asset_original_path.with_extension("").extension().unwrap().to_string_lossy().into_owned(); + let mut output_dir = args.output.join(asset_original_path); output_dir.pop(); - create_dir_all(&output_dir).await?; + let output_dir = output_dir; - if ["acb", "awb", "gtx"].iter().find(|ext| info.entry.2.ends_with(*ext)) == None { - return Err(anyhow!("unknown text asset: {}", info.entry.2)); - } + create_dir_all(&output_dir).await?; let tmpdir = tempfile::tempdir()?; @@ -383,27 +411,28 @@ async fn extract_text_asset( // function to get the text data. asset_ripper.export_primary(tmpdir.path()).await?; - let file_path = tmpdir.path().join(asset_original_path); - match &info.entry.2 { + let extracted_file_path = tmpdir.path().join(asset_original_path); + match asset_original_extension { // CRI .acb and .awb audio - n if n.ends_with(".acb") || n.ends_with(".awb") => { + n if n == "acb" || n == "awb" => { + let input_file_path = extracted_file_path.with_extension(""); // remove .bytes extension for vgmstream - tokio::fs::rename(&file_path, file_path.with_extension("")).await?; + tokio::fs::rename(&extracted_file_path, &input_file_path).await?; // According to https://github.com/vgmstream/vgmstream/blob/master/doc/USAGE.md#decryption-keys, // we can specify the decryption key in the .hcakey file so that vgmstream doesn't have // to brute-force the key. - let mut key_file = tokio::fs::File::create(file_path.with_file_name(".hcakey")).await?; + let mut key_file = + tokio::fs::File::create(extracted_file_path.with_file_name(".hcakey")).await?; key_file.write_all(MLTD_HCA_KEY.to_string().as_bytes()).await?; - let file_path = file_path.with_extension(""); let output_prefix = - file_path.file_name().expect("file name should exist").to_os_string(); + input_file_path.file_name().expect("file name should exist").to_os_string(); log::info!("extracting audio to {}", output_dir.display()); for subsong_index in 0.. { - let file_path = file_path.clone(); + let input_file_path = input_file_path.clone(); let output_dir = output_dir.clone(); let output_prefix = output_prefix.clone(); @@ -421,9 +450,9 @@ async fn extract_text_asset( log::trace!("audio options: {:#?}", options); } let mut encoder = Encoder::open( - &file_path.clone(), + &input_file_path, subsong_index, - &output_dir.clone(), + &output_dir, EncoderOutputOptions { prefix: output_prefix.as_os_str().to_str().unwrap(), codec: &audio_codec, @@ -440,18 +469,52 @@ async fn extract_text_asset( } } } - // AES-192-CBC encrypted plot text - n if n.ends_with(".gtx") => { - let output_path = - output_dir.join(&info.entry.2).with_extension("txt"); + // AES-192-CBC encrypted text + n if ENCRYPTED_FILE_EXTENSIONS.contains(&n.as_str()) => { + let output_path = output_dir.join(asset_original_filename); log::info!("extracting text to {}", output_path.display()); - let buf = tokio::fs::read(&file_path).await?; + let buf = tokio::fs::read(&extracted_file_path).await?; tokio::fs::write(&output_path, decrypt_text(&buf)?).await?; } - _ => unreachable!("this shouldn't happen"), + // MP4 video + n if n.ends_with(".mp4") => { + let output_path = output_dir.join(asset_original_filename); + + log::info!("extracting video to {}", output_path.display()); + + tokio::fs::copy(&extracted_file_path, &output_path).await?; + } + _ => panic!("this is not a TextAsset with binary content"), }; Ok(()) } + +async fn extract_text_asset( + bundle_no: usize, + collection_no: usize, + info: &AssetInfo, + asset_ripper: &mut AssetRipper, + args: &ExtractorArgs, +) -> Result<()> { + let asset_original_path = &PathBuf::from( + info.original_path.as_ref().expect("original path of TextAsset should exist"), + ); + + let output_path = args.output.join(asset_original_path); + let output_dir = output_path.parent().unwrap(); + + create_dir_all(output_dir).await?; + let mut f = tokio::fs::File::create(&output_path).await?; + + log::info!("extracting text to {}", output_path.display()); + + let mut stream = asset_ripper.asset_text(bundle_no, collection_no, info.entry.0).await?; + while let Some(item) = stream.next().await { + f.write_all(item?.as_ref()).await?; + } + + Ok(()) +} diff --git a/src/mltd/extract/text.rs b/src/mltd/extract/text.rs index 28128eb..2c0bc4b 100644 --- a/src/mltd/extract/text.rs +++ b/src/mltd/extract/text.rs @@ -35,48 +35,54 @@ pub(crate) enum AesErrorKind { Unpad, } -/// The key used to derive [`MLTD_TEXT_DECRYPT_KEY`] and -/// [`MLTD_TEXT_DECRYPT_IV`]. -pub const MLTD_TEXT_PBKDF2_HMAC_SHA1_KEY: &[u8; 8] = b"Millicon"; +/// The key used to derive [`TEXT_DECRYPT_KEY`] and +/// [`TEXT_DECRYPT_IV`]. +pub const TEXT_PBKDF2_HMAC_SHA1_KEY: &[u8; 8] = b"Millicon"; -/// The salt used to derive [`MLTD_TEXT_DECRYPT_KEY`] and -/// [`MLTD_TEXT_DECRYPT_IV`]. -pub const MLTD_TEXT_PBKDF2_HMAC_SHA1_SALT: &[u8; 9] = b"DAISUL___"; +/// The salt used to derive [`TEXT_DECRYPT_KEY`] and +/// [`TEXT_DECRYPT_IV`]. +pub const TEXT_PBKDF2_HMAC_SHA1_SALT: &[u8; 9] = b"DAISUL___"; -/// The number of iterations used to derive [`MLTD_TEXT_DECRYPT_KEY`] and -/// [`MLTD_TEXT_DECRYPT_IV`]. -pub const MLTD_TEXT_PBKDF2_HMAC_SHA1_ROUNDS: u32 = 1_000; +/// The number of iterations used to derive [`TEXT_DECRYPT_KEY`] and +/// [`TEXT_DECRYPT_IV`]. +pub const TEXT_PBKDF2_HMAC_SHA1_ROUNDS: u32 = 1_000; -static MLTD_TEXT_DECRYPT_KEY_IV: LazyLock<[u8; 40]> = LazyLock::new(|| { +static TEXT_DECRYPT_KEY_IV: LazyLock<[u8; 40]> = LazyLock::new(|| { pbkdf2::pbkdf2_hmac_array::( - MLTD_TEXT_PBKDF2_HMAC_SHA1_KEY, - MLTD_TEXT_PBKDF2_HMAC_SHA1_SALT, - MLTD_TEXT_PBKDF2_HMAC_SHA1_ROUNDS, + TEXT_PBKDF2_HMAC_SHA1_KEY, + TEXT_PBKDF2_HMAC_SHA1_SALT, + TEXT_PBKDF2_HMAC_SHA1_ROUNDS, ) }); /// The AES-192-CBC key used to decrypt the text asset. /// -/// It is derived from [`MLTD_TEXT_PBKDF2_HMAC_SHA1_KEY`] and -/// [`MLTD_TEXT_PBKDF2_HMAC_SHA1_SALT`] using PBKDF2-HMAC-SHA1, where +/// It is derived from [`TEXT_PBKDF2_HMAC_SHA1_KEY`] and +/// [`TEXT_PBKDF2_HMAC_SHA1_SALT`] using PBKDF2-HMAC-SHA1, where /// the first 24 bytes of the derived key are used as the actual key. -pub static MLTD_TEXT_DECRYPT_KEY: LazyLock<[u8; 24]> = - LazyLock::new(|| (&MLTD_TEXT_DECRYPT_KEY_IV[0..24]).try_into().unwrap()); +pub static TEXT_DECRYPT_KEY: LazyLock<[u8; 24]> = + LazyLock::new(|| (&TEXT_DECRYPT_KEY_IV[0..24]).try_into().unwrap()); /// The AES-192-CBC initialization vector used to decrypt the text asset. /// -/// It is derived from [`MLTD_TEXT_PBKDF2_HMAC_SHA1_KEY`] and -/// [`MLTD_TEXT_PBKDF2_HMAC_SHA1_SALT`] using PBKDF2-HMAC-SHA1, where +/// It is derived from [`TEXT_PBKDF2_HMAC_SHA1_KEY`] and +/// [`TEXT_PBKDF2_HMAC_SHA1_SALT`] using PBKDF2-HMAC-SHA1, where /// the last 16 bytes of the derived key are used as the actual IV. #[rustfmt::skip] -pub static MLTD_TEXT_DECRYPT_IV: LazyLock<[u8; 16]> = - LazyLock::new(|| (&MLTD_TEXT_DECRYPT_KEY_IV[24..40]).try_into().unwrap()); +pub static TEXT_DECRYPT_IV: LazyLock<[u8; 16]> = + LazyLock::new(|| (&TEXT_DECRYPT_KEY_IV[24..40]).try_into().unwrap()); /// AES-192-CBC encryptor for text assets in MLTD. -pub type MltdTextEncryptor = Encryptor; +pub type TextEncryptor = Encryptor; /// AES-192-CBC decryptor for text assets in MLTD. -pub type MltdTextDecryptor = Decryptor; +pub type TextDecryptor = Decryptor; + +/// File extensions for encrypted binary TextAssets in MLTD. +pub const ENCRYPTED_FILE_EXTENSIONS: &[&str; 2] = &["gtx", "mld"]; + +/// File extensions for unencrypted binary TextAssets in MLTD. +pub const UNENCRYPTED_FILE_EXTENSIONS: &[&str; 3] = &["acb", "awb", "mp4"]; /// Encrypts text using AES-192-CBC with MLTD's key and IV. /// @@ -91,9 +97,9 @@ pub type MltdTextDecryptor = Decryptor; /// let cipher = encrypt_text(text); /// ``` pub fn encrypt_text(plaintext: &[u8]) -> Vec { - let encryptor = MltdTextEncryptor::new( - MLTD_TEXT_DECRYPT_KEY.as_ref().into(), - MLTD_TEXT_DECRYPT_IV.as_ref().into(), + let encryptor = TextEncryptor::new( + TEXT_DECRYPT_KEY.as_ref().into(), + TEXT_DECRYPT_IV.as_ref().into(), ); encryptor.encrypt_padded_vec_mut::(plaintext) @@ -120,9 +126,9 @@ pub fn encrypt_text(plaintext: &[u8]) -> Vec { /// assert_eq!(b"Hello, world!", text.as_slice()); /// ``` pub fn decrypt_text(cipher: &[u8]) -> Result> { - let decryptor = MltdTextDecryptor::new( - MLTD_TEXT_DECRYPT_KEY.as_ref().into(), - MLTD_TEXT_DECRYPT_IV.as_ref().into(), + let decryptor = TextDecryptor::new( + TEXT_DECRYPT_KEY.as_ref().into(), + TEXT_DECRYPT_IV.as_ref().into(), ); let plaintext = match decryptor.decrypt_padded_vec_mut::(cipher) { @@ -150,7 +156,7 @@ mod tests { 0x12, 0x2c, 0x5f, 0xad, 0xcc, 0xa3, 0x68, 0x5d, ]; - assert_eq!(MLTD_TEXT_DECRYPT_KEY_IV.as_ref(), &expected); + assert_eq!(TEXT_DECRYPT_KEY_IV.as_ref(), &expected); } #[test] diff --git a/src/mltd/net/asset_ripper.rs b/src/mltd/net/asset_ripper.rs index a529321..2ae1e4d 100644 --- a/src/mltd/net/asset_ripper.rs +++ b/src/mltd/net/asset_ripper.rs @@ -400,6 +400,24 @@ impl AssetRipper { Ok(()) } + /// Updates the settings on the AssetRipper. + /// + /// # Errors + /// + /// Returns [`crate::Error`] with [`crate::ErrorKind::Network`] if the update request fails. + pub async fn set(&mut self, form: HashMap) -> Result<()> { + let url = self.full_url("Settings/update")?; + + let client = reqwest::Client::new(); + let req = client.post(url.clone()).form(&form); + + if let Err(e) = req.send().await { + return Err(Error::request(url, Some(e)).into()); + } + + Ok(()) + } + /// Downloads the latest release zip of AssetRipper from GitHub to the given path. /// /// # Errors From 6b567cc1b76c4906923896bfad8f192778ecf1aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=88=91=E5=8F=AF=E8=83=BD=E5=BE=88=E5=BB=A2?= Date: Mon, 16 Jun 2025 21:46:20 +0800 Subject: [PATCH 9/9] :art: fmt --- crates/vgmstream/src/lib.rs | 6 +++--- src/bin/mltd/extract.rs | 2 +- src/mltd/extract/text.rs | 16 ++++++---------- src/mltd/net/matsuri_api.rs | 2 +- src/mltd/net/mod.rs | 3 +-- 5 files changed, 12 insertions(+), 17 deletions(-) diff --git a/crates/vgmstream/src/lib.rs b/crates/vgmstream/src/lib.rs index c3b960b..9db4103 100644 --- a/crates/vgmstream/src/lib.rs +++ b/crates/vgmstream/src/lib.rs @@ -7,12 +7,11 @@ use std::ffi::{CStr, c_int}; use std::ptr::NonNull; use bitflags::bitflags; +pub use vgmstream_sys; pub use crate::error::Error; pub use crate::sf::StreamFile; -pub use vgmstream_sys; - /// Rust version of [`vgmstream_sys::libvgmstream_sfmt_t`]. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SampleFormat { @@ -24,6 +23,7 @@ pub enum SampleFormat { impl TryFrom for SampleFormat { type Error = Error; + fn try_from(value: vgmstream_sys::libvgmstream_sfmt_t) -> Result { match value { vgmstream_sys::libvgmstream_sfmt_t_LIBVGMSTREAM_SFMT_PCM16 => Ok(Self::Pcm16), @@ -256,6 +256,7 @@ pub struct Format { impl TryFrom for Format { type Error = Error; + fn try_from(value: vgmstream_sys::libvgmstream_format_t) -> Result { let codec_name = unsafe { CStr::from_ptr(value.codec_name.as_ptr()) }.to_string_lossy().to_string(); @@ -550,7 +551,6 @@ impl From for vgmstream_sys::libvgmstream_loglevel_t { /// * Note that log is currently set globally rather than per [`VgmStream`]. /// * Call with [`LogLevel::None`] to disable current callback. /// * Call with [`None`] callback to use default stdout callback. -/// pub fn set_log(level: LogLevel, callback: Option) { unsafe { vgmstream_sys::libvgmstream_set_log(level.into(), callback) }; } diff --git a/src/bin/mltd/extract.rs b/src/bin/mltd/extract.rs index 49d68e7..5be4aeb 100644 --- a/src/bin/mltd/extract.rs +++ b/src/bin/mltd/extract.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap}; +use std::collections::BTreeMap; use std::io::{Cursor, Write}; use std::path::{Path, PathBuf}; diff --git a/src/mltd/extract/text.rs b/src/mltd/extract/text.rs index 2c0bc4b..249b354 100644 --- a/src/mltd/extract/text.rs +++ b/src/mltd/extract/text.rs @@ -97,10 +97,8 @@ pub const UNENCRYPTED_FILE_EXTENSIONS: &[&str; 3] = &["acb", "awb", "mp4"]; /// let cipher = encrypt_text(text); /// ``` pub fn encrypt_text(plaintext: &[u8]) -> Vec { - let encryptor = TextEncryptor::new( - TEXT_DECRYPT_KEY.as_ref().into(), - TEXT_DECRYPT_IV.as_ref().into(), - ); + let encryptor = + TextEncryptor::new(TEXT_DECRYPT_KEY.as_ref().into(), TEXT_DECRYPT_IV.as_ref().into()); encryptor.encrypt_padded_vec_mut::(plaintext) } @@ -119,17 +117,15 @@ pub fn encrypt_text(plaintext: &[u8]) -> Vec { /// use mltd::extract::text::decrypt_text; /// /// let cipher = [ -/// 0xca, 0x64, 0x14, 0x8e, 0x80, 0x9e, 0x50, 0xc9, -/// 0xe3, 0x4e, 0x18, 0x6f, 0x1e, 0x9c, 0x3e, 0xe2, +/// 0xca, 0x64, 0x14, 0x8e, 0x80, 0x9e, 0x50, 0xc9, 0xe3, 0x4e, 0x18, 0x6f, 0x1e, 0x9c, 0x3e, +/// 0xe2, /// ]; /// let text = decrypt_text(&cipher).unwrap(); /// assert_eq!(b"Hello, world!", text.as_slice()); /// ``` pub fn decrypt_text(cipher: &[u8]) -> Result> { - let decryptor = TextDecryptor::new( - TEXT_DECRYPT_KEY.as_ref().into(), - TEXT_DECRYPT_IV.as_ref().into(), - ); + let decryptor = + TextDecryptor::new(TEXT_DECRYPT_KEY.as_ref().into(), TEXT_DECRYPT_IV.as_ref().into()); let plaintext = match decryptor.decrypt_padded_vec_mut::(cipher) { Ok(plaintext) => Ok(plaintext), diff --git a/src/mltd/net/matsuri_api.rs b/src/mltd/net/matsuri_api.rs index 81c2778..f4f04cd 100644 --- a/src/mltd/net/matsuri_api.rs +++ b/src/mltd/net/matsuri_api.rs @@ -55,7 +55,7 @@ macro_rules! matsuri_api_endpoint { }; } -pub const MATSURI_API_ENDPOINT: & str = matsuri_api_endpoint!(); +pub const MATSURI_API_ENDPOINT: &str = matsuri_api_endpoint!(); async fn send_request(url: &str) -> Result { let client = reqwest::Client::new(); diff --git a/src/mltd/net/mod.rs b/src/mltd/net/mod.rs index 294a53a..668f52d 100644 --- a/src/mltd/net/mod.rs +++ b/src/mltd/net/mod.rs @@ -5,10 +5,9 @@ mod matsuri_api; pub use thiserror::Error as ThisError; -use crate::error::Repr; - pub use self::asset_ripper::*; pub use self::matsuri_api::*; +use crate::error::Repr; #[derive(Debug, ThisError)] #[error("network error: {kind}")]