feat: add qmc2 cli

This commit is contained in:
鲁树人 2024-09-06 00:52:23 +01:00
parent a9c7ba9fd4
commit e6fcb07ed2
4 changed files with 99 additions and 3 deletions

View File

@ -8,3 +8,4 @@ anyhow = "1.0.86"
clap = { version = "4.5.17", features = ["derive"] }
umc_kuwo = { path = "../um_crypto/kuwo" }
umc_qmc = { path = "../um_crypto/qmc" }
umc_utils = { path = "../um_crypto/utils" }

View File

@ -1,9 +1,12 @@
use clap::Subcommand;
pub mod qmc1;
pub mod qmc2;
#[derive(Subcommand)]
pub enum Commands {
#[command(name = "qmc1")]
QMCv1(qmc1::ArgsQMCv1),
#[command(name = "qmc2")]
QMCv2(qmc2::ArgsQMCv2),
}

91
um_cli/src/cmd/qmc2.rs Normal file
View File

@ -0,0 +1,91 @@
use crate::Cli;
use anyhow::{bail, Result};
use clap::Args;
use std::fs::File;
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::PathBuf;
use umc_qmc::{footer, QMCv2Cipher};
use umc_utils::base64;
/// Decrypt a QMCv1 file
#[derive(Args)]
pub struct ArgsQMCv2 {
/// Path to output file, e.g. /export/Music/song.flac
#[arg(short, long)]
output: Option<PathBuf>,
/// Path to input file, e.g. /export/Music/song.qmcflac
#[arg(name = "input")]
input: PathBuf,
/// Override EKey for this file.
/// Prefix with "decrypted:" to use base64 encoded raw key.
#[arg(short = 'K', long = "ekey")]
ekey: Option<String>,
/// Print info about this file, and do not perform decryption.
#[arg(short = 'I', long, action=clap::ArgAction::SetTrue, default_value_t=false)]
info_only: bool,
}
impl ArgsQMCv2 {
pub fn run(&self, cli: &Cli) -> Result<i32> {
let mut file_input = File::open(&self.input)?;
let mut footer_detection_buffer = vec![0u8; footer::INITIAL_DETECTION_LEN];
file_input.seek(SeekFrom::End(-(footer::INITIAL_DETECTION_LEN as i64)))?;
file_input.read_exact(&mut footer_detection_buffer)?;
let input_size = file_input.stream_position()?;
file_input.seek(SeekFrom::Start(0))?;
let (footer_len, ekey) = match footer::from_byte_slice(&footer_detection_buffer) {
Ok(Some(metadata)) => {
if self.info_only || cli.verbose {
println!("metadata: {:?}", metadata);
}
(metadata.size, metadata.ekey.or_else(|| self.ekey.clone()))
}
Ok(None) => {
eprintln!("could not find any qmc metadata.");
(0usize, self.ekey.clone())
}
Err(err) => {
eprintln!("failed to parse qmc metadata: {}", err);
(0usize, self.ekey.clone())
}
};
if self.info_only {
return Ok(0);
}
let key = match ekey {
None => bail!("--ekey is required when embedded ekey is not present."),
Some(ekey) => match ekey.strip_suffix("decrypted:") {
Some(decrypted) => base64::decode(decrypted)?.into_boxed_slice(),
None => umc_qmc::ekey::decrypt(ekey)?,
},
};
let cipher = QMCv2Cipher::new(key)?;
let mut file_output = match &self.output {
None => bail!("--output is required"),
Some(output) => BufWriter::new(File::create(output)?),
};
let mut buffer = vec![0u8; cli.buffer_size];
let reader = BufReader::with_capacity(cli.buffer_size, file_input);
let mut reader = reader.take(input_size - footer_len as u64);
let mut offset = 0usize;
while let Ok(n) = reader.read(&mut buffer) {
if n == 0 {
break;
}
cipher.decrypt(&mut buffer[..n], offset);
file_output.write_all(&buffer[..n])?;
offset += n;
}
Ok(0)
}
}

View File

@ -16,8 +16,8 @@ pub struct Cli {
command: Option<Commands>,
/// Be more verbose about what is going on.
#[clap(long, short, action=clap::ArgAction::SetTrue)]
verbose: Option<bool>,
#[clap(long, short, action=clap::ArgAction::SetTrue, default_value_t=false)]
verbose: bool,
/// Preferred buffer size when reading file, in bytes.
/// Default to 4MiB.
@ -28,6 +28,7 @@ pub struct Cli {
fn run_command(cli: &Cli) -> Result<i32> {
match &cli.command {
Some(Commands::QMCv1(cmd)) => cmd.run(&cli),
Some(Commands::QMCv2(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");
@ -43,7 +44,7 @@ fn main() {
-1
});
let duration = start.elapsed();
if let Some(true) = cli.verbose {
if cli.verbose {
eprintln!("time: {:?}", duration);
};
exit(code);