3 Commits

Author SHA1 Message Date
c34e9f355c 更新README
Some checks failed
Release / Publish to Github Releases (, macos-latest, aarch64-apple-darwin, true) (push) Has been cancelled
Release / Publish to Github Releases (, macos-latest, x86_64-apple-darwin) (push) Has been cancelled
Release / Publish to Github Releases (, ubuntu-latest, aarch64-unknown-linux-musl, true) (push) Has been cancelled
Release / Publish to Github Releases (, ubuntu-latest, arm-unknown-linux-musleabihf, true) (push) Has been cancelled
Release / Publish to Github Releases (, ubuntu-latest, armv7-unknown-linux-musleabihf, true) (push) Has been cancelled
Release / Publish to Github Releases (, ubuntu-latest, i686-unknown-linux-musl, true) (push) Has been cancelled
Release / Publish to Github Releases (, ubuntu-latest, x86_64-unknown-linux-musl, true) (push) Has been cancelled
Release / Publish to Github Releases (, windows-latest, aarch64-pc-windows-msvc, true) (push) Has been cancelled
Release / Publish to Github Releases (, windows-latest, i686-pc-windows-msvc, true) (push) Has been cancelled
Release / Publish to Github Releases (, windows-latest, x86_64-pc-windows-msvc) (push) Has been cancelled
Release / Publish to crates.io (push) Has been cancelled
2025-08-12 22:27:40 +08:00
e7ecb37878 - 修改默认线程数为cpu核心数
- 修改多线程通信为crossbeam-channel库,增加通讯性能
 - ⬆️ 升级依赖
 - 🔨 优化解密算法,提高解密效率
2025-08-12 22:26:46 +08:00
3ad5be3a01 - 修正了trace级别日志输出时显示debug级别的问题
- 🔨 优雅处理所有的错误
- 🔨 将代码分离为单个文件
2025-08-12 17:49:34 +08:00
15 changed files with 797 additions and 567 deletions

3
.gitignore vendored
View File

@ -2,4 +2,5 @@
src/*.old
src/*.bak
/testfile
/output
/output
/test

View File

@ -67,4 +67,16 @@
- :arrow_up: 升级依赖
- 修正了解密完成后不会自动退出的bug
### Refactoring
- :hammer: 重构部分代码!
- :hammer: 重构部分代码!
## [2.7.20] - 2025.8.12
### Features :sparkles:
- 修改默认线程数为cpu核心数
- 修改多线程通信为crossbeam-channel库增加通讯性能
### Fixed :bug:
- :arrow_up: 升级依赖
- 修正了trace级别日志输出时显示debug级别的问题
### Refactoring
- :hammer: 优雅处理所有的错误
- :hammer: 将代码分离为单个文件
- :hammer: 优化解密算法,提高解密效率

525
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,29 +1,30 @@
[package]
name = "ncmmiao"
version = "2.7.11"
version = "2.7.20"
edition = "2021"
authors = ["Lkhsss <lkhsss1019@gmail.com>"]
description = "A magic tool convert ncm to flac"
repository = "https://github.com/lkhsss/ncmmiao"
license = "GPL-3.0-or-later"
keywords = ["ncm","flac","neteasemusic"]
keywords = ["ncm", "flac", "neteasemusic"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
aes = { version = "0.8.4", default-features = false }
audiotags = {version = "0.5.0",default-features = false}
base64 = {version = "0.22.*"}
chrono = {version = "0.4.*",features = ["clock"],default-features = false}
clap = { version = "4.5.*", features = ["derive","std"]}
colored = {version = "3.0.0",default-features = false}
env_logger = {version = "0.11.7",default-features = false}
hex = "0.4.3"
audiotags = { version = "0.5.0", default-features = false }
base64 = { version = "0.22.*" }
chrono = { version = "0.4.*", features = ["clock"], default-features = false }
clap = { version = "4.5.*", features = ["derive", "std"] }
colored = { version = "3.0.0", default-features = false }
crossbeam-channel = "0.5.15"
env_logger = { version = "0.11.7", default-features = false }
image = "0.25.*"
indicatif = "0.17.9"
lazy_static = "1.5.0"
log = "0.4.20"
num_cpus = "1.17.0"
serde = { version = "1.0.195", features = ["derive"] }
serde_derive = "1.0.195"
serde_json = "1.0.111"
@ -36,6 +37,6 @@ maintenance = { status = "actively-developed" }
#缩小编译后体积
strip = true
# strip = "debuginfo" #仅移除debug信息
lto = true #启用链接时间优化
lto = true #启用链接时间优化
panic = "abort" #panic时直接abort
opt-level = "z" #优化级别

View File

@ -41,6 +41,9 @@ Options:
- [x] 自定义输出文件夹
- [x] 计时功能
- [x] 自动覆盖开关
- [x] 优化并发设置
- [x] 优化信息传递
---
# [Changelog](CHANGELOG.md)

41
src/apperror.rs Normal file
View File

@ -0,0 +1,41 @@
#[derive(Debug, PartialEq)]
#[allow(dead_code)]
pub enum AppError {
NotNcmFile,
CannotReadFileName,
CannotReadMetaInfo,
CoverCannotSave,
FileReadError,
FileSkipError,
FileWriteError,
FullFilenameError,
FileNotFound,
ProtectFile,
FileDataError,
SaveError,
SystemTimeError,
}
impl std::error::Error for AppError {}
impl std::fmt::Display for AppError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
let result = match self {
Self::NotNcmFile => "该文件不为NCM格式",
Self::CannotReadFileName => "无法读取文件名称",
Self::CannotReadMetaInfo => "无法读取歌曲元信息",
Self::CoverCannotSave => "封面无法保存",
Self::FileSkipError => "跳过数据时出错。可能是文件大小小于预期",
Self::FileReadError => "读取文件时发生错误",
Self::FileWriteError => "写入文件时错误",
Self::FullFilenameError => "文件名不符合规范",
Self::FileNotFound => "未找到文件",
Self::ProtectFile => "已关闭文件强制覆盖且文件已存在。使用-f或-forcesave开启强制覆盖。",
Self::FileDataError => "处理文件数据时出错",
Self::SaveError => "保存文件出错",
Self::SystemTimeError => "获取时间戳失败",
// _ => "未知错误",
};
write!(f, "{}", result)
}
}

View File

@ -5,7 +5,7 @@ use clap::Parser;
#[command(author = "lkhsss")]
#[command(version,about = "一个解密ncm文件的神秘程序 By Lkhsss", long_about = None)]
pub struct Cli {
/// 并发的最大线程数,默认为8线程
/// 并发的最大线程数,默认为cpu核心数
#[arg(short, long)]
pub workers: Option<usize>,
/// 需要解密的文件夹或文件
@ -14,7 +14,7 @@ pub struct Cli {
/// 输出目录
#[arg(short, long, name = "输出目录", default_value = "NcmmiaoOutput")]
pub output: Option<String>,
pub output: String,
/// 强制覆盖保存开关
#[arg(short, long, name = "强制覆盖开关")]
@ -23,4 +23,7 @@ pub struct Cli {
/// 自动打开输出目录
#[arg(short, long, name = "自动打开输出目录")]
pub autoopen: bool,
#[arg(short, long, action = clap::ArgAction::Count)]
pub debug: u8,
}

View File

@ -1,7 +1,7 @@
use colored::Color::{Blue, Cyan, Green, Red, Yellow};
use colored::Color::{Cyan, Green, Red, Yellow,Magenta};
use colored::Colorize;
use indicatif::MultiProgress;
use log::{LevelFilter, Log, Metadata, Record, SetLoggerError};
use log::{Log, Metadata, Record, SetLoggerError};
use std::sync::Arc;
// 自定义Logger将日志发送到MultiProgress
@ -20,8 +20,8 @@ impl Log for MultiProgressLogger {
log::Level::Error => ("Error").color(Red),
log::Level::Warn => ("Warn").color(Yellow),
log::Level::Info => ("Info").color(Green),
log::Level::Debug => ("Debug").color(Blue),
log::Level::Trace => ("Debug").color(Cyan),
log::Level::Debug => ("Debug").color(Magenta),
log::Level::Trace => ("Trace").color(Cyan),
};
let message = format!(
"[{}][{}] {}",
@ -42,10 +42,6 @@ pub fn init_logger() -> Result<(), SetLoggerError> {
mp: crate::MP.clone(),
};
log::set_boxed_logger(Box::new(logger))?;
if cfg!(debug_assertions) {
log::set_max_level(LevelFilter::Debug);
} else {
log::set_max_level(LevelFilter::Info);
}
Ok(())
}

View File

@ -1,18 +1,15 @@
use ::clap::Parser;
use colored::{Color, Colorize};
use crossbeam_channel::{bounded, Sender};
use indicatif::{MultiProgress, ProgressBar, ProgressStyle};
use lazy_static::lazy_static;
use log::{error, info, warn};
use messager::Message;
use log::{error, info, warn, LevelFilter};
use messager::{Message, Messager, Signals};
use std::process::exit;
use std::time::Duration;
use std::{
path::Path,
sync::{
mpsc::{self, Sender},
Arc, Mutex,
},
};
use std::{path::Path, sync::Arc};
mod apperror;
mod clap;
mod logger;
mod messager;
@ -21,17 +18,32 @@ mod opendir;
mod pathparse;
mod test;
mod threadpool;
mod time;
use apperror::AppError;
use ncmdump::Ncmfile;
use time::TimeCompare;
const DEFAULT_MAXWORKER: usize = 8;
fn main() {
let timer = ncmdump::TimeCompare::new();
fn main() -> Result<(), AppError> {
let timer = match TimeCompare::new() {
Ok(t) => t,
Err(e) => {
error!("无法初始化时间戳系统。{}", e);
exit(1)
}
};
// 初始化日志系统
logger::init_logger().unwrap();
match logger::init_logger() {
Ok(_) => (),
Err(_) => {
println!("初始化日志系统失败")
}
};
let cli = clap::Cli::parse();
//获取cpu核心数
let cpus = num_cpus::get();
// 最大线程数
let max_workers = match cli.workers {
Some(n) => {
@ -41,85 +53,103 @@ fn main() {
1
}
}
None => DEFAULT_MAXWORKER,
None => cpus,//默认使用cpu核心数作为线程数
};
//输入目录
let input = cli.input;
let outputdir = cli.output.unwrap();
//输出目录
let outputdir = cli.output;
// 强制覆盖
let forcesave = cli.forcesave;
if forcesave {
warn!("文件{}已开启!", "强制覆盖".bright_red())
}
let level = match cli.debug {
0 | 3 => LevelFilter::Info,
1 => LevelFilter::Error,
2 => LevelFilter::Warn,
4 => LevelFilter::Debug,
5 => LevelFilter::Trace,
_ => LevelFilter::Off,
};
info!("日志等级:{}", level.to_string());
log::set_max_level(level);
let undumpfile = pathparse::pathparse(input); // 该列表将存入文件的路径
let taskcount = undumpfile.len();
let mut success_count = 0; //成功任务数
let mut ignore_count = 0; //忽略的任务数
let mut failure_count = 0; //发生错误的
if taskcount == 0 {
error!("没有找到有效文件。使用-i参数输入需要解密的文件或文件夹。")
} else {
// 初始化线程池
let pool = threadpool::Pool::new(max_workers);
info!(
"将启用{}线程",
max_workers.to_string().color(Color::BrightGreen)
);
// 初始化通讯
let (tx, rx) = mpsc::channel();
error!("没有找到有效文件。使用-i参数输入需要解密的文件或文件夹。");
exit(2);
};
// 初始化线程池
let pool = threadpool::Pool::new(max_workers);
// 循环开始
for filepath in undumpfile {
let output = outputdir.clone();
let senderin: Sender<messager::Message> = tx.clone();
let senderon: Sender<messager::Message> = tx.clone();
pool.execute(move || match Ncmfile::new(filepath.as_str()) {
Ok(mut n) => match n.dump(Path::new(&output), senderin, forcesave) {
Ok(_) => {}
Err(e) => {
let messager = messager::Messager::new(n.fullfilename, senderon);
messager.send(messager::Signals::Err(e));
}
},
info!(
"将启用{}线程",
max_workers.to_string().color(Color::BrightGreen)
);
// 初始化通讯
// let (tx, rx) = mpsc::channel();
let (tx, rx) = bounded(taskcount * 6);
// 循环开始
for filepath in undumpfile {
let output = outputdir.clone();
let senderin: Sender<Message> = tx.clone();
let senderon: Sender<Message> = tx.clone();
// 多线程
pool.execute(move || match Ncmfile::new(filepath.as_str()) {
Ok(mut n) => match n.dump(Path::new(&output), senderin, forcesave) {
Ok(_) => {}
Err(e) => {
let messager = messager::Messager::new(filepath, senderon);
messager.send(messager::Signals::Err(e));
let messager = Messager::new(n.fullfilename, senderon);
let _ = messager.send(Signals::Err(e));
}
});
}
//循环到此结束
//进度条
let pb = ProgressBar::new((taskcount * 6) as u64) //长度乘积取决于Signal的数量
.with_elapsed(Duration::from_millis(50))
.with_style(
ProgressStyle::default_bar()
.progress_chars("#>-")
.template("{spinner:.green} [{wide_bar:.cyan/blue}] {percent_precise}% ({eta})")
.unwrap(),
)
.with_message("解密中");
let progressbar = MP.add(pb);
//定义计数器
// 接受消息!!!!!!!!!!
for messages in rx {
match messages.signal{
messager::Signals::End|messager::Signals::Err(_)=>{success_count+=1},
_=>()
},
Err(e) => {
let messager = Messager::new(filepath, senderon);
let _ = messager.send(Signals::Err(e));
}
if success_count < taskcount {
progressbar.inc(1);
messages.log(); //发送log
} else {
break;
}
}
progressbar.finish_and_clear();
});
}
let timecount = timer.compare();
//循环到此结束
//进度条
let pb = ProgressBar::new((taskcount * 6) as u64) //长度乘积取决于Signal的数量
.with_elapsed(Duration::from_millis(50))
.with_style(
ProgressStyle::default_bar()
.progress_chars("#>-")
.template("{spinner:.green} [{wide_bar:.cyan/blue}] {percent_precise}% ({eta})")
.unwrap(),
)
.with_message("解密中");
let progressbar = MP.add(pb);
//定义计数器
// 接受消息!!!!!!!!!!
for messages in rx {
match messages.signal {
Signals::End => success_count += 1,
Signals::Err(AppError::ProtectFile) => ignore_count += 1,
Signals::Err(_) => failure_count += 1,
_ => (),
}
if (success_count+ignore_count+failure_count) < taskcount {
progressbar.inc(1);
// messages.log(); //发送log
} else {
break;
}
}
progressbar.finish_and_clear();
let timecount = timer.compare().unwrap();
let showtime = || {
if timecount > 2000 {
format!("共计用时{}", timecount / 1000)
@ -128,9 +158,10 @@ fn main() {
}
};
info!(
"成功解密{}个文件,{}个文件解密失败,{}",
"成功解密{}个文件,跳过{}个文件,{}个文件解密失败,{}",
success_count.to_string().bright_green(),
(taskcount - success_count).to_string().bright_red(),
ignore_count.to_string().purple(),
failure_count.to_string().bright_red(),
showtime()
);
@ -141,6 +172,7 @@ fn main() {
} else {
info!("输出文件夹:[{}]", outputdir.cyan());
};
Ok(())
}
lazy_static! {

View File

@ -1,40 +1,35 @@
use colored::Colorize;
use log::{error, info, warn};
use crate::{messager, ncmdump};
use crate::{messager, AppError};
use std::fmt::Debug;
use std::sync::mpsc;
// use
pub struct Messager {
name: String,
sender: mpsc::Sender<messager::Message>,
sender: crossbeam_channel::Sender<messager::Message>,
}
pub struct Message {
pub name: String,
pub signal: Signals,
}
impl Message {
// 定义一个公共方法 log用于记录不同信号状态下的日志信息
pub fn log(&self) {
let loginfo = match &self.signal {
Signals::Start => "读取文件",
Signals::GetMetaInfo => "解密歌曲元信息",
Signals::GetCover => "解密封面图片数据",
Signals::Decrypt => "解密歌曲信息",
Signals::Save => "保存文件",
Signals::End => "成功!",
Signals::Err(e)=>&e.to_string(),
};
match &self.signal{
Signals::Err(e)=>{match e{
ncmdump::NcmError::ProtectFile=>warn!("[{}] {}", self.name.cyan(), loginfo),
_=>error!("[{}] {}", self.name.cyan(), loginfo),
}},
_=>info!("[{}] {}", self.name.cyan(), loginfo)
}
}
}
// impl Message {
// // 定义一个公共方法 log用于记录不同信号状态下的日志信息
// pub fn log(&self) {
// match &self.signal {
// Signals::Err(e) => match e {
// AppError::ProtectFile => warn!("[{}] {}", self.name.cyan(), "强制覆盖已关闭。不保存文件"),
// _ => error!("[{}] {}", self.name.cyan(), e),
// },
// Signals::Start => trace!("开始读取文件"),
// Signals::GetMetaInfo => trace!("解密歌曲元信息"),
// Signals::GetCover => trace!("解密封面图片数据"),
// Signals::Decrypt => trace!("解密歌曲信息"),
// Signals::Save => info!("保存文件"),
// Signals::End => trace!("解密流程结束")
// }
// }
// }
#[derive(PartialEq)]
pub enum Signals {
Start,
@ -43,14 +38,14 @@ pub enum Signals {
Decrypt,
Save,
End,
Err(ncmdump::NcmError),
Err(AppError),
}
impl Messager {
pub fn new(name: String, sender: mpsc::Sender<messager::Message>) -> Self {
pub fn new(name: String, sender: crossbeam_channel::Sender<messager::Message>) -> Self {
Self { name, sender }
}
pub fn send(&self, s: Signals) -> Result<(), std::sync::mpsc::SendError<messager::Message>> {
pub fn send(&self, s: Signals) -> Result<(), crossbeam_channel::SendError<messager::Message>> {
self.sender.send(Message {
name: self.name.clone(),
signal: s,
@ -66,8 +61,7 @@ impl Debug for Message {
Signals::End => "破解完成",
Signals::GetMetaInfo => "获取元数据",
Signals::GetCover => "获取封面",
Signals::Err(e)=>&e.to_string(),
Signals::Err(e) => &e.to_string(),
};
write!(f, "[{}] {}", self.name, message)
}

View File

@ -1,3 +1,4 @@
use crate::apperror::AppError;
use crate::messager;
use aes::cipher::generic_array::typenum::U16;
use aes::cipher::{generic_array::GenericArray, BlockDecrypt, KeyInit};
@ -5,10 +6,7 @@ use aes::Aes128;
use audiotags::{MimeType, Picture, Tag};
use base64::{self, Engine};
use colored::*;
use hex::decode;
use lazy_static::lazy_static;
#[allow(unused_imports)]
use log::{debug, error, info, trace, warn};
use log::{debug, info, trace};
use messager::Signals;
use serde_derive::{Deserialize, Serialize};
use serde_json::{self, Value};
@ -17,17 +15,22 @@ use std::fs::{self, File};
use std::io::{BufReader, BufWriter, Read, Seek, SeekFrom, Write};
use std::path::{Path, PathBuf};
use std::str::from_utf8;
use std::sync::mpsc;
use std::vec;
use std::time::{SystemTime, UNIX_EPOCH};
lazy_static! {
// 解密需要的密钥
static ref KEY_CORE: Vec<u8> = decode("687A4852416D736F356B496E62617857").unwrap();
static ref KEY_META: Vec<u8> = decode("2331346C6A6B5F215C5D2630553C2728").unwrap();
}
// lazy_static! {
// // 解密需要的密钥
// static ref KEY_CORE: Vec<u8> = decode("687A4852416D736F356B496E62617857").unwrap();//绝对正确
// static ref KEY_META: Vec<u8> = decode("2331346C6A6B5F215C5D2630553C2728").unwrap();
// }
// 原KEY_CORE数据687A4852416D736F356B496E62617857
const NEW_KEY_CORE: [u8; 16] = [
0x68, 0x7A, 0x48, 0x52, 0x41, 0x6D, 0x73, 0x6F, 0x35, 0x6B, 0x49, 0x6E, 0x62, 0x61, 0x78, 0x57,
];
// 原KEY_META数据2331346C6A6B5F215C5D2630553C2728
const NEW_KEY_META: [u8; 16] = [
0x23, 0x31, 0x34, 0x6C, 0x6A, 0x6B, 0x5F, 0x21, 0x5C, 0x5D, 0x26, 0x30, 0x55, 0x3C, 0x27, 0x28,
];
#[derive(Debug)]
#[allow(unused_variables)]
pub struct Ncmfile {
@ -42,18 +45,28 @@ pub struct Ncmfile {
/// 游标
pub position: u64,
}
impl Ncmfile {
pub fn new(filepath: &str) -> Result<Ncmfile, NcmError> {
/// 各种工具方法
pub fn new(filepath: &str) -> Result<Ncmfile, AppError> {
let file = match File::open(filepath) {
Ok(f) => f,
Err(_) => return Err(NcmError::FileReadError),
Err(_) => return Err(AppError::FileReadError),
};
let path = Path::new(filepath);
let fullfilename = path.file_name().unwrap().to_str().unwrap().to_string();
let size = file.metadata().unwrap().len();
let fullfilename = path
.file_name()
.ok_or(AppError::FileReadError)?
.to_str()
.ok_or(AppError::FileReadError)?
.to_string();
let size = file
.metadata()
.map_err(|_| AppError::CannotReadMetaInfo)?
.len();
let filename = match Path::new(&filepath).file_stem() {
Some(f) => f.to_str().unwrap().to_string(),
None => return Err(NcmError::CannotReadFileName),
Some(f) => f.to_str().ok_or(AppError::FileReadError)?.to_string(),
None => return Err(AppError::CannotReadFileName),
};
Ok(Ncmfile {
file,
@ -67,9 +80,9 @@ impl Ncmfile {
///
/// 该函数可以记录上次读取的位置,下次读取时从上次读取的位置开始
/// - length 想要读取的长度
pub fn seekread(&mut self, length: u64) -> Result<Vec<u8>, NcmError> {
pub fn seekread(&mut self, length: u64) -> Result<Vec<u8>, AppError> {
if self.position + length > self.size {
return Err(NcmError::FileReadError);
Err(AppError::FileReadError)
} else {
let mut reader = BufReader::new(&self.file);
let _ = reader.seek(SeekFrom::Start(self.position));
@ -79,6 +92,7 @@ impl Ncmfile {
Ok(buf[..].to_vec())
}
}
/// 从指定位置开始读取。
///
/// !!!该函数仍然会更新游标
@ -86,9 +100,9 @@ impl Ncmfile {
/// - offset 开始位置
/// - length 想要读取的长度
#[allow(dead_code)]
pub fn seekread_from(&mut self, offset: u64, length: u64) -> Result<Vec<u8>, NcmError> {
pub fn seekread_from(&mut self, offset: u64, length: u64) -> Result<Vec<u8>, AppError> {
if self.position + length > self.size {
return Err(NcmError::FileReadError);
Err(AppError::FileReadError)
} else {
let mut reader = BufReader::new(&self.file);
let _ = reader.seek(SeekFrom::Start(offset));
@ -110,7 +124,7 @@ impl Ncmfile {
pub fn seekread_no_error(&mut self, length: u64) -> Vec<u8> {
if self.position + length > self.size {
if self.position >= self.size {
return vec![];
vec![]
} else {
let mut reader = BufReader::new(&self.file);
let _ = reader.seek(SeekFrom::Start(self.position));
@ -118,7 +132,7 @@ impl Ncmfile {
let mut buf: Vec<u8> = vec![0; (self.size - self.position) as usize];
let _ = reader.read_exact(&mut buf);
self.position += length;
return buf[..].to_vec();
buf[..].to_vec()
}
} else {
let mut reader = BufReader::new(&self.file);
@ -130,9 +144,9 @@ impl Ncmfile {
}
}
/// 跳过某些数据
pub fn skip(&mut self, length: u64) -> Result<(), NcmError> {
pub fn skip(&mut self, length: u64) -> Result<(), AppError> {
if self.position + length > self.size {
return Err(NcmError::FileReadError);
Err(AppError::FileReadError)
} else {
self.position += length;
Ok(())
@ -140,43 +154,59 @@ impl Ncmfile {
}
///按字节进行0x64异或。
fn parse_key(key: &mut [u8]) -> &[u8] {
for i in 0..key.len() {
key[i] ^= 0x64;
for item in &mut *key {
*item ^= 0x64;
}
key
}
fn save(&mut self, path: &PathBuf, data: Vec<u8>) -> Result<(), AppError> {
let music_file = match File::create(path) {
Ok(o) => o,
Err(_) => return Err(AppError::FileWriteError),
};
let mut writer = BufWriter::new(music_file);
let _ = writer.write_all(&data);
// 关闭文件
match writer.flush() {
Ok(o) => o,
Err(_) => return Err(AppError::FileWriteError),
};
Ok(())
}
fn is_ncm(data: Vec<u8>) -> Result<(), AppError> {
let header = from_utf8(&data).map_err(|_| AppError::NotNcmFile)?;
if header != "CTENFDAM" {
Err(AppError::NotNcmFile)
} else {
Ok(())
}
}
/// 使用PKCS5Padding标准去掉填充信息
fn unpad(data: &[u8]) -> Vec<u8> {
data[..data.len() - data[data.len() - 1] as usize].to_vec()
}
}
impl Ncmfile {
/// 解密函数
#[allow(unused_assignments)]
pub fn dump(
&mut self,
outputdir: &Path,
tx: mpsc::Sender<messager::Message>,
tx: crossbeam_channel::Sender<messager::Message>,
force_save: bool,
) -> Result<(), NcmError> {
) -> Result<(), AppError> {
let messager = messager::Messager::new(self.fullfilename.clone(), tx);
let _ = messager.send(Signals::Start);
//TODO 通讯合法化
// info!("开始解密[{}]文件", self.fullfilename.yellow());
// 获取magic header 。应为CTENFDAM
let magic_header = match self.seekread(8) {
Ok(header) => header,
Err(_e) => {
return Err(NcmError::FileReadError); //TODO去除向上传播
}
};
// 获取magic header 应为CTENFDAM
trace!("取magic header");
let magic_header = self.seekread(8)?;
// 判断是否为ncm格式的文件
match from_utf8(&magic_header) {
Ok(header) => {
if header != "CTENFDAM" {
// 传播错误至dump
return Err(NcmError::NotNcmFile);
}
}
// 传播错误至dump
Err(_e) => return Err(NcmError::NotNcmFile),
}
trace!("判断是否为ncm格式的文件");
Self::is_ncm(magic_header)?;
// 跳过2字节
trace!("跳过2字节");
@ -184,20 +214,29 @@ impl Ncmfile {
trace!("获取RC4密钥长度");
//小端模式读取RC4密钥长度 正常情况下应为128
let key_length = u32::from_le_bytes(self.seekread(4).unwrap().try_into().unwrap()) as u64;
// debug!("RC4密钥长度为{}", key_length);
let key_length = u32::from_le_bytes(
self.seekread(4)?
.try_into()
.map_err(|_| AppError::FileReadError)?,
) as u64; //数据长度不够只能使用u32 然后转化为u64
// debug!("RC4密钥长度为{}", key_length);
//读取密钥 开头应为 neteasecloudmusic
trace!("读取RC4密钥");
let mut key_data = self.seekread(key_length).unwrap();
let mut key_data = self.seekread(key_length)?;
//aes128解密
let key_data = &aes128_to_slice(&KEY_CORE, Self::parse_key(&mut key_data[..])); //先把密钥按照字节进行0x64异或
// RC4密钥
let key_data = unpad(&key_data[..])[17..].to_vec(); //去掉neteasecloudmusic
let key_data =
&aes128_to_slice(&NEW_KEY_CORE, Self::parse_key(&mut key_data[..]).to_vec())?; //先把密钥按照字节进行0x64异或
// RC4密钥
let key_data = Self::unpad(&key_data[..])[17..].to_vec(); //去掉neteasecloudmusic
//读取meta信息的数据大小
trace!("获取meta信息数据大小");
let meta_length = u32::from_le_bytes(self.seekread(4)?.try_into().unwrap()) as u64;
let meta_length = u32::from_le_bytes(
self.seekread(4)?
.try_into()
.map_err(|_| AppError::FileDataError)?,
) as u64;
let _ = messager.send(Signals::GetMetaInfo);
// 读取meta信息
@ -205,28 +244,28 @@ impl Ncmfile {
let meta_data = {
let mut meta_data = self.seekread(meta_length)?; //读取源数据
//字节对0x63进行异或。
for i in 0..meta_data.len() {
meta_data[i] ^= 0x63;
for item in &mut meta_data {
*item ^= 0x63;
}
// base64解密
let mut decode_data = Vec::<u8>::new();
let _ = match &base64::engine::general_purpose::STANDARD
if base64::engine::general_purpose::STANDARD
.decode_vec(&mut meta_data[22..], &mut decode_data)
.is_err()
{
Err(_) => return Err(NcmError::CannotReadMetaInfo),
_ => (),
return Err(AppError::CannotReadMetaInfo);
};
// aes128解密
let aes_data = aes128_to_slice(&KEY_META, &decode_data);
let aes_data = aes128_to_slice(&NEW_KEY_META, decode_data)?;
// unpadding
let json_data = match String::from_utf8(unpad(&aes_data)[6..].to_vec()) {
let json_data = match String::from_utf8(Self::unpad(&aes_data)[6..].to_vec()) {
Ok(o) => o,
Err(_) => return Err(NcmError::CannotReadMetaInfo),
Err(_) => return Err(AppError::CannotReadMetaInfo),
};
debug!("json_data: {}", json_data);
let data: Value = match serde_json::from_str(&json_data[..]) {
Ok(o) => o,
Err(_) => return Err(NcmError::CannotReadMetaInfo),
Err(_) => return Err(AppError::CannotReadMetaInfo),
}; //解析json数据
data
};
@ -237,16 +276,19 @@ impl Ncmfile {
let filename = format!(
"{}.{}",
self.filename,
meta_data.get("format").unwrap().as_str().unwrap()
meta_data
.get("format")
.ok_or(AppError::CannotReadMetaInfo)?
.as_str()
.ok_or(AppError::CannotReadMetaInfo)?
);
// let filename = standardize_filename(filename);
debug!("文件名:{}", filename.yellow());
//链级创建输出目录
match fs::create_dir_all(outputdir) {
Err(_) => return Err(NcmError::FileWriteError),
_ => (),
};
if fs::create_dir_all(outputdir).is_err() {
return Err(AppError::FileWriteError);
}
outputdir.join(filename)
};
@ -254,12 +296,13 @@ impl Ncmfile {
// 先检查是否存在
if !force_save && Path::new(&path).exists() {
return Err(NcmError::ProtectFile);
return Err(AppError::ProtectFile);
}
// 跳过4个字节的校验码
trace!("读取校验码");
// let _crc32 = u32::from_le_bytes(self.seekread(4).unwrap().try_into().unwrap()) as u64;
// trace!("读取校验码");
// let _crc32 = u32::from_le_bytes(self.seekread(4)?.try_into().map_err(AppError::FileDataError)?) as u64;
self.skip(4)?;
// 跳过5个字节
@ -269,7 +312,11 @@ impl Ncmfile {
let _ = messager.send(Signals::GetCover);
// 获取图片数据的大小
trace!("获取图片数据的大小");
let image_data_length = u32::from_le_bytes(self.seekread(4)?.try_into().unwrap()) as u64;
let image_data_length = u32::from_le_bytes(
self.seekread(4)?
.try_into()
.map_err(|_| AppError::FileDataError)?,
) as u64;
// 读取图片,并写入文件当中
let image_data = self.seekread(image_data_length)?; //读取图片数据
@ -277,7 +324,7 @@ impl Ncmfile {
trace!("组成密码盒");
let key_box = {
let key_length = key_data.len();
let key_data = Vec::from(key_data);
// let key_data = Vec::from(key_data);
let mut key_box = (0..=255).collect::<Vec<u8>>();
let mut temp = 0;
let mut last_byte = 0;
@ -285,7 +332,7 @@ impl Ncmfile {
for i in 0..=255 {
let swap = key_box[i as usize] as u64;
temp = (swap + last_byte as u64 + key_data[key_offset as usize] as u64) & 0xFF;
temp = (swap + last_byte + key_data[key_offset] as u64) & 0xFF;
key_offset += 1;
if key_offset >= key_length {
key_offset = 0;
@ -298,26 +345,6 @@ impl Ncmfile {
key_box
};
/* let mut s_box = {
let key_length = key_data.len();
let key_box = Vec::from(key_data);
let mut s = (0..=255).collect::<Vec<u8>>();
let mut j = 0;
for i in 0..=255 {
j = (j as usize + s[i] as usize + key_box[i % key_length] as usize) & 0xFF;
//记录 s[j]的值
let temp = &s.get(j as usize).unwrap().to_owned();
s[j as usize] = s[i];
s[i] = temp.to_owned();
}
s
}; */
// let key_box = key_box[0..(key_box.len()-key_box[key_box.len() as usize-1] as usize)].to_vec();
//解密音乐数据
trace!("解密音乐数据");
let _ = messager.send(Signals::Decrypt);
@ -331,7 +358,7 @@ impl Ncmfile {
let j = i & 0xFF;
chunk[i - 1] ^= key_box[(key_box[j] as usize
+ key_box[(key_box[j as usize] as usize + j) & 0xff] as usize)
+ key_box[(key_box[j] as usize + j) & 0xff] as usize)
& 0xff]
// chunk[i - 1] ^= key_box[(key_box[j] + key_box[(key_box[j as usize] as usize + j as usize) & 0xFF]) & 0xFF];
}
@ -342,28 +369,6 @@ impl Ncmfile {
}
}
//组成流密钥
/* let mut stream = Vec::new();
for i in 0..256 {
stream.push(
s_box[(s_box[i] as usize + s_box[(i + s_box[i] as usize) & 0xFF] as usize) & 0xFF],
)
} */
// 解密音乐数据
/* loop {
let chunk = self.seekread_no_error(256); //每次读取256个字节
if chunk.len() != 0 {
for (count, &i) in chunk[..].iter().enumerate() {
music_data.push(i ^ stream[count])
}
} else {
break;
}
} */
// debug!("music_data{:?}", music_data);
// debug!("长度:{}", stream.len());
//退出循环,写入文件
let _ = messager.send(Signals::Save);
@ -373,20 +378,22 @@ impl Ncmfile {
// 保存封面
let mut tag = match Tag::new().read_from_path(&path) {
Ok(o) => o,
Err(_) => return Err(NcmError::CoverCannotSave),
Err(_) => return Err(AppError::CoverCannotSave),
};
let cover = Picture {
mime_type: MimeType::Jpeg,
data: &image_data,
};
tag.set_album_cover(cover); //添加封面
let _ = tag.write_to_path(&path.to_str().unwrap()); //保存
let _ = tag
.write_to_path(path.to_str().ok_or(AppError::SaveError)?)
.map_err(|_| AppError::SaveError); //保存
}
info!(
"[{}] 文件已保存到: {}",
self.filename.yellow(),
path.to_str().unwrap().bright_cyan()
path.to_str().ok_or(AppError::SaveError)?.bright_cyan()
);
info!(
"[{}]{}",
@ -396,20 +403,6 @@ impl Ncmfile {
let _ = messager.send(Signals::End);
Ok(())
}
fn save(&mut self, path: &PathBuf, data: Vec<u8>) -> Result<(), NcmError> {
let music_file = match File::create(path) {
Ok(o) => o,
Err(_) => return Err(NcmError::FileWriteError),
};
let mut writer = BufWriter::new(music_file);
let _ = writer.write_all(&data);
// 关闭文件
match writer.flush() {
Ok(o) => o,
Err(_) => return Err(NcmError::FileWriteError),
};
Ok(())
}
}
/// 存储元数据的结构体
@ -470,57 +463,31 @@ pub struct Key {
// fn read_meta(file: &mut File, meta_length: u32) -> Result<Vec<u8>, Error> {}
fn convert_to_generic_arrays(input: &[u8]) -> Vec<GenericArray<u8, U16>> {
fn convert_to_generic_arrays(input: &[u8]) -> Result<Vec<GenericArray<u8, U16>>, AppError> {
// 确保输入的长度是16的倍数
assert!(
input.len() % 16 == 0,
"Input length must be a multiple of 16"
);
if input.len() % 16 != 0 {
return Err(AppError::FileDataError);
}
input
Ok(input
.chunks(16)
.map(|chunk| {
// 将每个块转换为GenericArray
GenericArray::clone_from_slice(chunk)
})
.collect()
}
/// aes128解密
/// !!!未对齐数据!!!
/// TODO
/// 解密NCM文件的rc4密钥前记得按字节对0x64进行异或
#[allow(dead_code)]
fn aes128(key: &[u8], blocks: &[u8]) -> String {
trace!("进行AES128解密");
let key = GenericArray::from_slice(key);
let mut blocks = convert_to_generic_arrays(blocks);
// 初始化密钥
let cipher = Aes128::new(&key);
// 开始解密
cipher.decrypt_blocks(&mut blocks);
let mut x = String::new();
for block in blocks.iter() {
x.push_str(std::str::from_utf8(&block).unwrap())
}
// 去除所有空格及控制字符
let x = x[..].trim();
x.to_string()
.collect())
}
/// ## AES128解密
fn aes128_to_slice(key: &[u8], blocks: &[u8]) -> Vec<u8> {
/// 解密NCM文件的rc4密钥前记得按字节对0x64进行异或
fn aes128_to_slice<T: AsRef<[u8]>>(key: &T, blocks: Vec<u8>) -> Result<Vec<u8>, AppError> {
trace!("进行AES128解密");
let key = GenericArray::from_slice(key);
let key = GenericArray::from_slice(key.as_ref());
let mut blocks = convert_to_generic_arrays(blocks);
let mut blocks = convert_to_generic_arrays(&blocks)?;
// 初始化密钥
let cipher = Aes128::new(&key);
let cipher = Aes128::new(key);
// 开始解密
cipher.decrypt_blocks(&mut blocks);
@ -532,87 +499,24 @@ fn aes128_to_slice(key: &[u8], blocks: &[u8]) -> Vec<u8> {
x.push(i.to_owned());
}
}
x
Ok(x)
}
/// ## 规范文件名称
/// 防止创建文件失败
/// 符号一一对应:
/// - \ / * ? " : < > |
/// - _ _ ⟨ ⟩ _
#[allow(dead_code)]
fn standardize_filename(old_fullfilename: String) -> String {
trace!("格式化文件名");
let mut new_fullfilename = String::from(old_fullfilename);
// debug!("规范文件名:{}", new_fullfilename);
let standard = ["\\", "/", "*", "?", "\"", ":", "<", ">", "|"];
let resolution = ["_", "_", "", "", "", "", "", "", "_"];
for i in 0..standard.len() {
new_fullfilename =
new_fullfilename.replace(&standard[i].to_string(), &resolution[i].to_string());
}
new_fullfilename
}
/// 使用PKCS5Padding标准去掉填充信息
fn unpad(data: &[u8]) -> Vec<u8> {
data[..data.len() - data[data.len() - 1] as usize].to_vec()
}
#[derive(Debug,PartialEq)]
#[allow(dead_code)]
pub enum NcmError {
NotNcmFile,
CannotReadFileName,
CannotReadMetaInfo,
CoverCannotSave,
FileReadError,
FileSkipError,
FileWriteError,
FullFilenameError,
FileNotFound,
ProtectFile,
}
impl std::error::Error for NcmError {}
impl std::fmt::Display for NcmError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Self::NotNcmFile => write!(f, "该文件不为NCM格式"),
Self::CannotReadFileName => write!(f, "无法读取文件名称"),
Self::CannotReadMetaInfo => write!(f, "无法读取歌曲元信息"),
Self::CoverCannotSave => write!(f, "封面无法保存"),
Self::FileReadError => write!(f, "读取文件时发生错误"),
Self::FileWriteError => write!(f, "写入文件时错误"),
Self::FullFilenameError => write!(f, "文件名不符合规范"),
Self::ProtectFile => write!(
f,
"已关闭文件强制覆盖且文件已存在。使用-f或-forcesave开启强制覆盖。"
),
_ => write!(f, "未知错误"),
}
}
}
#[allow(dead_code)]
pub struct TimeCompare(u128);
impl TimeCompare {
pub fn new() -> Self {
Self(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis(),
)
}
pub fn compare(&self) -> u128 {
let time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap()
.as_millis();
time - self.0
}
}
// ## 规范文件名称
// 防止创建文件失败
// 符号一一对应:
// - \ / * ? " : < > |
// - _ _ ⟨ ⟩ _
// #[allow(dead_code)]
// fn standardize_filename(old_fullfilename: String) -> String {
// trace!("格式化文件名");
// let mut new_fullfilename = String::from(old_fullfilename);
// // debug!("规范文件名:{}", new_fullfilename);
// let standard = ["\\", "/", "*", "?", "\"", ":", "<", ">", "|"];
// let resolution = ["_", "_", "", "", "", "", "⟨", "⟩", "_"];
// for i in 0..standard.len() {
// new_fullfilename =
// new_fullfilename.replace(&standard[i].to_string(), &resolution[i].to_string());
// }
// new_fullfilename
// }

View File

@ -3,32 +3,29 @@ use std::{path::PathBuf, process::Command};
#[cfg(target_os = "windows")]
pub fn opendir(dir: PathBuf) {
match Command::new("explorer")
if Command::new("explorer")
.arg(&dir) // <- Specify the directory you'd like to open.
.spawn()
.spawn().is_err()
{
Err(_) => error!("无法打开输出文件夹:[{}]", dir.display()),
_ => (),
error!("无法打开输出文件夹:[{}]", dir.display())
}
}
#[cfg(target_os = "linux")]
pub fn opendir(dir: PathBuf) {
match Command::new("open")
if Command::new("open")
.arg(&dir) // <- Specify the directory you'd like to open.
.spawn()
.spawn().is_err()
{
Err(_) => error!("无法打开输出文件夹:[{}]", dir.display()),
_ => (),
error!("无法打开输出文件夹:[{}]", dir.display())
}
}
#[cfg(target_os = "macos")]
pub fn opendir(dir: PathBuf) {
match Command::new("open")
if Command::new("open")
.arg(&dir) // <- Specify the directory you'd like to open.
.spawn()
.spawn().is_err()
{
Err(_) => error!("无法打开输出文件夹:[{}]", dir.display()),
_ => (),
error!("无法打开输出文件夹:[{}]", dir.display())
}
}

View File

@ -1,3 +1,4 @@
use log::{debug, error};
use std::path::Path;
use walkdir::WalkDir;
@ -10,23 +11,34 @@ pub fn pathparse(input: Vec<String>) -> Vec<String> {
if path.is_file() {
// 当后缀符合为ncm时才加入列表
match path.extension() {
Some(extension) => {
if extension == "ncm" {
let _ = &mut undumpfile.push(arg.to_owned());
}
if let Some(extension) = path.extension() {
if extension == "ncm" {
let _ = &mut undumpfile.push(arg.to_owned());
}
None => {}
}
} else if path.is_dir() {
for entry in WalkDir::new(path) {
let new_entry = entry.unwrap().clone();
let new_entry = match entry {
Ok(e) => e,
Err(e) => {
error!("无法遍历目录: {}", e);
continue;
}
};
let filepath = new_entry.into_path();
// 当后缀符合为ncm时才加入列表
match filepath.extension() {
Some(extension) => {
if extension == "ncm" {
let _ = &mut undumpfile.push(String::from(filepath.to_str().unwrap()));
match filepath.to_str() {
Some(s) => {
let _ = &mut undumpfile.push(s.into());
}
None => {
debug!("无法获取你文件路径");
continue;
}
};
}
}
None => {

View File

@ -19,11 +19,11 @@ impl Worker {
let message = receiver.lock().unwrap().recv().unwrap();
match message {
Message::NewJob(job) => {
debug!("线程[{}]获得任务", id);
// debug!("线程[{}]获得任务", id);
job();
}
Message::ByeBye => {
debug!("线程[{}]结束任务", id);
// debug!("线程[{}]结束任务", id);
break;
}
}
@ -58,8 +58,8 @@ impl Pool {
}
Pool {
workers: workers,
max_workers: max_workers,
workers,
max_workers,
sender: tx,
}
}

23
src/time.rs Normal file
View File

@ -0,0 +1,23 @@
use std::time::{SystemTime, UNIX_EPOCH};
use crate::AppError;
pub struct TimeCompare(u128);
impl TimeCompare {
pub fn new() -> Result<Self, AppError> {
Ok(Self(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| AppError::SystemTimeError)?
.as_millis(),
))
}
pub fn compare(&self) -> Result<u128, AppError> {
let time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|_| AppError::SystemTimeError)?
.as_millis();
Ok(time - self.0)
}
}