diff --git a/.idea/lib_um_crypto.iml b/.idea/lib_um_crypto.iml
index beec96a..b1d8e6c 100644
--- a/.idea/lib_um_crypto.iml
+++ b/.idea/lib_um_crypto.iml
@@ -6,9 +6,10 @@
+
-
+
\ No newline at end of file
diff --git a/Cargo.lock b/Cargo.lock
index 040b006..c6936f7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,55 @@
# It is not intended for manual editing.
version = 3
+[[package]]
+name = "anstream"
+version = "0.6.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64e15c1ab1f89faffbf04a634d5e1962e9074f2741eef6d97f3c4e322426d526"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bec1de6f59aedf83baf9ff929c98f2ad654b97c9510f4e70cf6f661d49fd5b1"
+
+[[package]]
+name = "anstyle-parse"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb47de1e80c2b463c735db5b217a0ddc39d612e7ac9e2e96a5aed1f57616c1cb"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d36fc52c7f6c869915e99412912f22093507da8d9e942ceaf66fe4b7c14422a"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5bf74e1b6e971609db8ca7a9ce79fd5768ab6ae46441c572e46cf596f59e57f8"
+dependencies = [
+ "anstyle",
+ "windows-sys 0.52.0",
+]
+
[[package]]
name = "anyhow"
version = "1.0.86"
@@ -35,6 +84,52 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+[[package]]
+name = "clap"
+version = "4.5.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e5a21b8495e732f1b3c364c9949b201ca7bae518c502c80256c96ad79eaf6ac"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.5.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8cf2dd12af7a047ad9d6da2b6b249759a22a7abc0f474c1dae1777afa4b21a73"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.5.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "501d359d5f3dcaf6ecdeee48833ae73ec6e42723a1e52419c79abf9507eec0a0"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "0.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1462739cb27611015575c0c11df5df7601141071f07518d56fcc1be504cbec97"
+
+[[package]]
+name = "colorchoice"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0"
+
[[package]]
name = "console_error_panic_hook"
version = "0.1.7"
@@ -51,6 +146,18 @@ version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf"
+
[[package]]
name = "itertools"
version = "0.13.0"
@@ -130,6 +237,12 @@ version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
[[package]]
name = "syn"
version = "2.0.77"
@@ -161,6 +274,16 @@ dependencies = [
"syn",
]
+[[package]]
+name = "um_cli"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "clap",
+ "umc_kuwo",
+ "umc_qmc",
+]
+
[[package]]
name = "um_wasm"
version = "0.1.0"
@@ -199,6 +322,12 @@ version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
[[package]]
name = "walkdir"
version = "2.5.0"
@@ -318,7 +447,16 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
dependencies = [
- "windows-sys",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets",
]
[[package]]
diff --git a/Cargo.toml b/Cargo.toml
index a31b498..2b57826 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[workspace]
resolver = "2"
-members = ["um_crypto/*", "um_wasm"]
+members = ["um_crypto/*", "um_wasm", "um_cli"]
[profile.release.package.um_wasm]
# Tell `rustc` to optimize for small code size.
diff --git a/um_cli/Cargo.toml b/um_cli/Cargo.toml
new file mode 100644
index 0000000..9951f2a
--- /dev/null
+++ b/um_cli/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "um_cli"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+anyhow = "1.0.86"
+clap = { version = "4.5.17", features = ["derive"] }
+umc_kuwo = { path = "../um_crypto/kuwo" }
+umc_qmc = { path = "../um_crypto/qmc" }
diff --git a/um_cli/src/cmd/mod.rs b/um_cli/src/cmd/mod.rs
new file mode 100644
index 0000000..7ae8d2b
--- /dev/null
+++ b/um_cli/src/cmd/mod.rs
@@ -0,0 +1,9 @@
+use clap::Subcommand;
+
+pub mod qmc1;
+
+#[derive(Subcommand)]
+pub enum Commands {
+ #[command(name = "qmc1")]
+ QMCv1(qmc1::ArgsQMCv1),
+}
diff --git a/um_cli/src/cmd/qmc1.rs b/um_cli/src/cmd/qmc1.rs
new file mode 100644
index 0000000..11007cd
--- /dev/null
+++ b/um_cli/src/cmd/qmc1.rs
@@ -0,0 +1,41 @@
+use anyhow::Result;
+use clap::Args;
+use std::fs::File;
+use std::io::{Read, Write};
+use std::path::PathBuf;
+
+/// Decrypt a QMCv1 file
+#[derive(Args)]
+pub struct ArgsQMCv1 {
+ /// Path to output file, e.g. /export/Music/song.flac
+ #[clap(short, long)]
+ output: PathBuf,
+
+ /// Path to input file, e.g. /export/Music/song.qmcflac
+ #[arg(name = "input")]
+ input: PathBuf,
+}
+
+// 4MiB buffer is working well on my machine.
+const BUFFER_SIZE: usize = 4 * 1024 * 1024;
+
+impl ArgsQMCv1 {
+ pub fn run(&self) -> Result {
+ let mut file_input = File::open(&self.input)?;
+ let mut file_output = File::create(&self.output)?;
+
+ let mut offset = 0usize;
+ let mut buffer = vec![0u8; BUFFER_SIZE].into_boxed_slice();
+ while let Ok(n) = file_input.read(&mut buffer) {
+ if n == 0 {
+ break;
+ }
+
+ umc_qmc::v1::decrypt(&mut buffer[..n], offset);
+ file_output.write_all(&buffer[..n])?;
+ offset += n;
+ }
+
+ Ok(0)
+ }
+}
diff --git a/um_cli/src/main.rs b/um_cli/src/main.rs
new file mode 100644
index 0000000..fc034f7
--- /dev/null
+++ b/um_cli/src/main.rs
@@ -0,0 +1,44 @@
+use crate::cmd::Commands;
+use anyhow::Result;
+use clap::Parser;
+use std::process::exit;
+use std::time::Instant;
+
+mod cmd;
+
+#[derive(Parser)]
+#[command(name = "um_cli")]
+#[command(version = "0.1")]
+#[command(about = "um_cli (rust ver.)", long_about = None)]
+struct Cli {
+ #[clap(subcommand)]
+ command: Option,
+
+ /// Time command
+ #[clap(long, short, action=clap::ArgAction::SetTrue)]
+ time: Option,
+}
+
+fn run_command(cli: &Cli) -> Result {
+ match &cli.command {
+ Some(Commands::QMCv1(cmd)) => cmd.run(),
+ None => {
+ // https://github.com/clap-rs/clap/issues/3857#issuecomment-1161796261
+ todo!("implement a sensible default command, similar to um/cli");
+ }
+ }
+}
+
+fn main() {
+ let cli = Cli::parse();
+ let start = Instant::now();
+ let code = run_command(&cli).unwrap_or_else(|err| {
+ eprintln!("failed to run command: {}", err);
+ -1
+ });
+ let duration = start.elapsed();
+ if let Some(true) = cli.time {
+ eprintln!("time: {:?}", duration);
+ };
+ exit(code);
+}