diff --git a/.idea/lib_um_crypto.iml b/.idea/lib_um_crypto.iml
index 0148a35..6af76e9 100644
--- a/.idea/lib_um_crypto.iml
+++ b/.idea/lib_um_crypto.iml
@@ -13,6 +13,7 @@
+
diff --git a/Cargo.lock b/Cargo.lock
index 1302607..50d30f9 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -528,6 +528,7 @@ dependencies = [
"umc_ncm",
"umc_qmc",
"umc_utils",
+ "umc_xiami",
"umc_xmly",
]
@@ -544,6 +545,7 @@ dependencies = [
"umc_kuwo",
"umc_ncm",
"umc_qmc",
+ "umc_xiami",
"umc_xmly",
"wasm-bindgen",
"wasm-bindgen-test",
@@ -623,6 +625,13 @@ dependencies = [
"md-5",
]
+[[package]]
+name = "umc_xiami"
+version = "0.1.0"
+dependencies = [
+ "thiserror",
+]
+
[[package]]
name = "umc_xmly"
version = "0.1.0"
diff --git a/um_cli/Cargo.toml b/um_cli/Cargo.toml
index a4ca510..a92d285 100644
--- a/um_cli/Cargo.toml
+++ b/um_cli/Cargo.toml
@@ -11,5 +11,6 @@ umc_kgm = { path = "../um_crypto/kgm" }
umc_kuwo = { path = "../um_crypto/kuwo" }
umc_ncm = { path = "../um_crypto/ncm" }
umc_qmc = { path = "../um_crypto/qmc" }
+umc_xiami = { path = "../um_crypto/xiami" }
umc_xmly = { path = "../um_crypto/xmly" }
umc_utils = { path = "../um_crypto/utils" }
diff --git a/um_cli/src/cmd/mod.rs b/um_cli/src/cmd/mod.rs
index 1b19bfa..3ae580b 100644
--- a/um_cli/src/cmd/mod.rs
+++ b/um_cli/src/cmd/mod.rs
@@ -5,6 +5,7 @@ pub mod kgm;
pub mod ncm;
pub mod qmc1;
pub mod qmc2;
+pub mod xiami;
pub mod xmly;
#[derive(Subcommand)]
@@ -21,4 +22,6 @@ pub enum Commands {
JOOX(joox::ArgsJoox),
#[command(name = "xmly")]
XMLY(xmly::ArgsXimalaya),
+ #[command(name = "xiami")]
+ Xiami(xiami::ArgsXiami),
}
diff --git a/um_cli/src/cmd/xiami.rs b/um_cli/src/cmd/xiami.rs
new file mode 100644
index 0000000..eaae205
--- /dev/null
+++ b/um_cli/src/cmd/xiami.rs
@@ -0,0 +1,44 @@
+use crate::Cli;
+use anyhow::Result;
+use clap::Args;
+use std::fs::File;
+use std::io;
+use std::io::{BufReader, BufWriter, Read, Write};
+use std::path::PathBuf;
+
+/// Decrypt a XM file (Xiami)
+#[derive(Args)]
+pub struct ArgsXiami {
+ /// Path to output file, e.g. /export/Music/song.flac
+ #[clap(short, long)]
+ output: PathBuf,
+
+ /// Path to input file, e.g. /export/Music/song.xm
+ #[arg(name = "input")]
+ input: PathBuf,
+}
+
+impl ArgsXiami {
+ pub fn run(&self, cli: &Cli) -> Result {
+ let mut reader = BufReader::with_capacity(cli.buffer_size, File::open(&self.input)?);
+ let mut writer = BufWriter::with_capacity(cli.buffer_size, File::create(&self.output)?);
+
+ let mut header = [0u8; 0x10];
+ reader.read_exact(&mut header)?;
+ let xm = umc_xiami::XiamiFile::from_header(&header)?;
+ let mut copy_reader = (&mut reader).take(xm.copy_len as u64);
+ io::copy(&mut copy_reader, &mut writer)?;
+
+ let mut buffer = vec![0u8; cli.buffer_size];
+ loop {
+ let n = reader.read(&mut buffer[..])?;
+ if n == 0 {
+ break;
+ }
+ xm.decrypt(&mut buffer[..n]);
+ writer.write_all(&buffer[..n])?;
+ }
+
+ Ok(0)
+ }
+}
diff --git a/um_cli/src/main.rs b/um_cli/src/main.rs
index 1f0caed..75716b9 100644
--- a/um_cli/src/main.rs
+++ b/um_cli/src/main.rs
@@ -33,6 +33,7 @@ fn run_command(cli: &Cli) -> Result {
Some(Commands::KGM(cmd)) => cmd.run(&cli),
Some(Commands::JOOX(cmd)) => cmd.run(&cli),
Some(Commands::XMLY(cmd)) => cmd.run(&cli),
+ Some(Commands::Xiami(cmd)) => cmd.run(&cli),
None => {
// https://github.com/clap-rs/clap/issues/3857#issuecomment-1161796261
todo!("implement a sensible default command, similar to um/cli");
diff --git a/um_crypto/xiami/Cargo.toml b/um_crypto/xiami/Cargo.toml
new file mode 100644
index 0000000..21d17d7
--- /dev/null
+++ b/um_crypto/xiami/Cargo.toml
@@ -0,0 +1,7 @@
+[package]
+name = "umc_xiami"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+thiserror = "1.0.63"
diff --git a/um_crypto/xiami/src/lib.rs b/um_crypto/xiami/src/lib.rs
new file mode 100644
index 0000000..03b838f
--- /dev/null
+++ b/um_crypto/xiami/src/lib.rs
@@ -0,0 +1,45 @@
+use thiserror::Error;
+
+#[derive(Error, Debug)]
+pub enum XiamiError {
+ #[error("header too small, require at least {0} bytes")]
+ HeaderTooSmall(usize),
+
+ #[error("not a xiami file")]
+ NotXiamiFile,
+}
+
+pub struct XiamiFile {
+ pub copy_len: usize,
+ pub format: [u8; 4],
+ key: u8,
+}
+
+impl XiamiFile {
+ pub fn from_header(buffer: &[u8]) -> Result {
+ if buffer.len() < 0x10 {
+ Err(XiamiError::HeaderTooSmall(0x10))?;
+ }
+
+ let (format, copy_len, key) = match buffer[..0x10] {
+ [b'i', b'f', b'm', b't', f1, f2, f3, f4, 0xfe, 0xfe, 0xfe, 0xfe, a, b, c, key] => {
+ let copy_len = (a as usize) | ((b as usize) << 8) | ((c as usize) << 16);
+ let format = [f1, f2, f3, f4];
+ (format, copy_len, key.wrapping_sub(1))
+ }
+ _ => Err(XiamiError::NotXiamiFile)?,
+ };
+
+ Ok(Self {
+ copy_len,
+ format,
+ key,
+ })
+ }
+
+ pub fn decrypt(&self, buffer: &mut [u8]) {
+ for b in buffer.iter_mut() {
+ *b = self.key.wrapping_sub(*b);
+ }
+ }
+}
diff --git a/um_wasm/Cargo.toml b/um_wasm/Cargo.toml
index 08def2b..0a24e90 100644
--- a/um_wasm/Cargo.toml
+++ b/um_wasm/Cargo.toml
@@ -28,6 +28,7 @@ umc_kgm = { path = "../um_crypto/kgm" }
umc_kuwo = { path = "../um_crypto/kuwo" }
umc_ncm = { path = "../um_crypto/ncm" }
umc_qmc = { path = "../um_crypto/qmc" }
+umc_xiami = { path = "../um_crypto/xiami" }
umc_xmly = { path = "../um_crypto/xmly" }
um_audio = { path = "../um_audio" }
diff --git a/um_wasm/src/exports/mod.rs b/um_wasm/src/exports/mod.rs
index e29a581..62c622c 100644
--- a/um_wasm/src/exports/mod.rs
+++ b/um_wasm/src/exports/mod.rs
@@ -4,4 +4,5 @@ pub mod kgm;
pub mod kuwo;
pub mod ncm;
pub mod qmc;
+mod xiami;
pub mod xmly;
diff --git a/um_wasm/src/exports/xiami.rs b/um_wasm/src/exports/xiami.rs
new file mode 100644
index 0000000..f4882e7
--- /dev/null
+++ b/um_wasm/src/exports/xiami.rs
@@ -0,0 +1,27 @@
+use umc_xiami::XiamiFile;
+use wasm_bindgen::prelude::wasm_bindgen;
+use wasm_bindgen::JsError;
+
+/// Xiami XM file decipher.
+#[wasm_bindgen(js_name=Xiami)]
+pub struct JsXiami(XiamiFile);
+
+#[wasm_bindgen(js_class = Xiami)]
+impl JsXiami {
+ /// Parse the Xiami header (0x400 bytes)
+ pub fn from_header(header: &[u8]) -> Result {
+ let hdr = XiamiFile::from_header(header)?;
+ Ok(JsXiami(hdr))
+ }
+
+ /// Decrypt encrypted buffer part.
+ pub fn decrypt(&self, buffer: &mut [u8]) {
+ self.0.decrypt(buffer)
+ }
+
+ /// After header (0x10 bytes), the number of bytes should be copied without decryption.
+ #[wasm_bindgen(getter, js_name=copyPlainLength)]
+ pub fn get_copy_plain_length(&self) -> usize {
+ self.0.copy_len
+ }
+}