refactor: P0-P5 全面架构重构
P1 thiserror 类型化错误: 新增 core/src/error.rs QrError 枚举, 全链 String -> QrError, 包括 EmptyInput/InvalidVersion/DataTooLong/DecodeFail 等 8 种变体 P2 text_builder Tauri 统一: 新增 build_qr_text Tauri command, 删除前端 qrText.ts, 所有 mode 组件改为 invoke 调用 Rust 端构建文本 P3 QrConfig 颜色字段移除: 从 QrConfig/QrCode 移除 fg_color/bg_color, 改为 to_svg/to_image_bytes 参数传递 P4 前端 4 项合并: Context 拆分为 StateContext+DispatchContext (H10), 新建 useModeForm 通用 hook (M11), VCardMode grid-cols-2 网格布局 (M13), persistHistory/loadHistory 迁至 utils/storage.ts (L9) P5 算法优化: MaskedView 懒计算替代 8 次 Matrix 克隆 (H9), encoding_rs 精确 Kanji Shift JIS 映射 (H12) 验证: cargo check+clippy 通过, 81+24+7 全部测试通过
This commit is contained in:
+281
-105
@@ -1,3 +1,4 @@
|
||||
use anyhow::{bail, Context, Result};
|
||||
use clap::{CommandFactory, Parser, Subcommand};
|
||||
use clap_complete::{generate, Shell};
|
||||
use indicatif::{ProgressBar, ProgressStyle};
|
||||
@@ -49,19 +50,25 @@ enum Command {
|
||||
|
||||
#[derive(clap::Args, Clone)]
|
||||
struct EncodeOpts {
|
||||
#[arg(short = 'l', long, default_value = "M")] level: String,
|
||||
#[arg(short = 'V', long, value_parser = clap::value_parser!(u8).range(1..=40))] version: Option<u8>,
|
||||
#[arg(short = 's', long, default_value = "4")] size: u8,
|
||||
#[arg(short = 'm', long, default_value = "4")] margin: u8,
|
||||
#[arg(short = 'l', long, default_value = "M")]
|
||||
level: String,
|
||||
#[arg(short = 'V', long, value_parser = clap::value_parser!(u8).range(1..=40))]
|
||||
version: Option<u8>,
|
||||
#[arg(short = 's', long, default_value = "4", value_parser = clap::value_parser!(u8).range(1..=20))]
|
||||
size: u8,
|
||||
#[arg(short = 'm', long, default_value = "4")]
|
||||
margin: u8,
|
||||
#[arg(long)] fg: Option<String>,
|
||||
#[arg(long)] bg: Option<String>,
|
||||
#[arg(long)] logo: Option<String>,
|
||||
#[arg(short = 'f', long, default_value = "png")] format: String,
|
||||
#[arg(short = 'f', long, default_value = "png")]
|
||||
format: String,
|
||||
#[arg(long)] mode: Option<String>,
|
||||
// WiFi
|
||||
#[arg(long)] ssid: Option<String>,
|
||||
#[arg(long)] password: Option<String>,
|
||||
#[arg(long, default_value = "WPA")] encryption: String,
|
||||
#[arg(long, default_value = "WPA")]
|
||||
encryption: String,
|
||||
#[arg(long)] hidden: bool,
|
||||
// vCard
|
||||
#[arg(long)] name: Option<String>,
|
||||
@@ -90,31 +97,24 @@ struct EncodeOpts {
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct BatchEntry {
|
||||
content: Option<String>, level: Option<String>,
|
||||
ssid: Option<String>, password: Option<String>, encryption: Option<String>,
|
||||
#[serde(default)] hidden: Option<bool>,
|
||||
name: Option<String>, phone: Option<String>, email: Option<String>,
|
||||
company: Option<String>, address: Option<String>,
|
||||
to: Option<String>, subject: Option<String>, body: Option<String>,
|
||||
number: Option<String>, message: Option<String>, url: Option<String>,
|
||||
}
|
||||
|
||||
// ──────────────────── 错误类型 ────────────────────
|
||||
|
||||
struct E { msg: String, code: i32 }
|
||||
impl E {
|
||||
fn new(m: impl Into<String>, c: i32) -> Self { Self { msg: m.into(), code: c } }
|
||||
fn exit(&self) -> ! { eprintln!("qrgen: {}", self.msg); process::exit(self.code); }
|
||||
}
|
||||
impl From<std::io::Error> for E {
|
||||
fn from(e: std::io::Error) -> Self { E::new(format!("IO 错误: {e}"), 2) }
|
||||
}
|
||||
impl From<image::ImageError> for E {
|
||||
fn from(e: image::ImageError) -> Self { E::new(format!("图像错误: {e}"), 2) }
|
||||
}
|
||||
macro_rules! bail {
|
||||
($msg:expr, $code:expr) => { return Err(E::new($msg, $code)) };
|
||||
($fmt:expr, $code:expr, $($arg:tt)*) => { return Err(E::new(format!($fmt, $($arg)*), $code)) };
|
||||
content: Option<String>,
|
||||
level: Option<String>,
|
||||
ssid: Option<String>,
|
||||
password: Option<String>,
|
||||
encryption: Option<String>,
|
||||
#[serde(default)]
|
||||
hidden: Option<bool>,
|
||||
name: Option<String>,
|
||||
phone: Option<String>,
|
||||
email: Option<String>,
|
||||
company: Option<String>,
|
||||
address: Option<String>,
|
||||
to: Option<String>,
|
||||
subject: Option<String>,
|
||||
body: Option<String>,
|
||||
number: Option<String>,
|
||||
message: Option<String>,
|
||||
url: Option<String>,
|
||||
}
|
||||
|
||||
// ──────────────────── 入口 ────────────────────
|
||||
@@ -131,124 +131,238 @@ fn main() {
|
||||
Command::Encode { content, output, opts } => cmd_encode(&content, &output, &opts),
|
||||
Command::Decode { file } => cmd_decode(&file),
|
||||
};
|
||||
if let Err(e) = r { e.exit(); }
|
||||
if let Err(e) = r {
|
||||
eprintln!("qrgen: {:#}", e);
|
||||
process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────── I/O 辅助 ────────────────────
|
||||
|
||||
fn stdin_bytes() -> Result<Vec<u8>, E> {
|
||||
/// 最大 stdin 读取量:10 MB
|
||||
const STDIN_MAX: u64 = 10 * 1024 * 1024;
|
||||
|
||||
fn stdin_bytes() -> Result<Vec<u8>> {
|
||||
let mut b = Vec::new();
|
||||
io::stdin().read_to_end(&mut b).map_err(|e| E::new(format!("无法读取 stdin: {e}"), 2))?;
|
||||
io::stdin()
|
||||
.take(STDIN_MAX)
|
||||
.read_to_end(&mut b)
|
||||
.with_context(|| "无法读取 stdin")?;
|
||||
Ok(b)
|
||||
}
|
||||
fn stdin_text() -> Result<String, E> {
|
||||
fn stdin_text() -> Result<String> {
|
||||
let mut s = String::new();
|
||||
io::stdin().read_to_string(&mut s).map_err(|e| E::new(format!("无法读取 stdin: {e}"), 2))?;
|
||||
io::stdin()
|
||||
.take(STDIN_MAX)
|
||||
.read_to_string(&mut s)
|
||||
.with_context(|| "无法读取 stdin")?;
|
||||
let t = s.trim().to_string();
|
||||
if t.is_empty() { bail!("stdin 为空", 2); }
|
||||
if t.is_empty() {
|
||||
bail!("stdin 为空");
|
||||
}
|
||||
Ok(t)
|
||||
}
|
||||
|
||||
// ──────────────────── 编码 ────────────────────
|
||||
|
||||
fn cmd_encode(content: &str, output: &Option<String>, opts: &EncodeOpts) -> Result<(), E> {
|
||||
let text = if content == "-" { stdin_text()? } else { content.to_string() };
|
||||
fn cmd_encode(content: &str, output: &Option<String>, opts: &EncodeOpts) -> Result<()> {
|
||||
let text = if content == "-" {
|
||||
stdin_text()?
|
||||
} else {
|
||||
content.to_string()
|
||||
};
|
||||
let final_text = if let Some(m) = &opts.mode {
|
||||
build_mode(m, opts, &text)?
|
||||
} else if let Some(ref bf) = opts.batch {
|
||||
return do_batch(bf, opts);
|
||||
} else { text };
|
||||
} else {
|
||||
text
|
||||
};
|
||||
|
||||
let level = parse_level(&opts.level)?;
|
||||
let logo = opts.logo.as_ref().map(std::fs::read).transpose()
|
||||
.map_err(|e| E::new(format!("无法读取 logo: {e}"), 2))?;
|
||||
let level: EcLevel = opts.level.parse().map_err(|e: String| anyhow::anyhow!(e))?;
|
||||
let logo = opts
|
||||
.logo
|
||||
.as_ref()
|
||||
.map(std::fs::read)
|
||||
.transpose()
|
||||
.with_context(|| "无法读取 logo")?;
|
||||
|
||||
let config = QrConfig {
|
||||
level, version: opts.version.map(VersionMode::Fixed).unwrap_or(VersionMode::Auto),
|
||||
margin: opts.margin, fg_color: opts.fg.clone(), bg_color: opts.bg.clone(),
|
||||
level,
|
||||
version: opts
|
||||
.version
|
||||
.map(VersionMode::Fixed)
|
||||
.unwrap_or(VersionMode::Auto),
|
||||
margin: opts.margin,
|
||||
};
|
||||
let qr = QrCode::encode(&final_text, config).map_err(|e| E::new(format!("编码失败: {e}"), 1))?;
|
||||
let qr = QrCode::encode(&final_text, config).map_err(|e| anyhow::anyhow!("编码失败: {e}"))?;
|
||||
|
||||
let fg = opts.fg.as_deref();
|
||||
let bg = opts.bg.as_deref();
|
||||
|
||||
match output {
|
||||
Some(p) => {
|
||||
check_path(p)?;
|
||||
let ext = Path::new(p).extension().and_then(|e| e.to_str()).unwrap_or("").to_lowercase();
|
||||
let ext = Path::new(p)
|
||||
.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("")
|
||||
.to_lowercase();
|
||||
if ext.is_empty() {
|
||||
eprintln!("警告: 无法从扩展名推断输出格式,回退到 PNG");
|
||||
}
|
||||
match ext.as_str() {
|
||||
"svg" => { std::fs::write(p, qr.to_svg(logo.as_deref()))?; eprintln!("已生成: {p} (版本 {}, SVG)", qr.version.0); }
|
||||
"svg" => {
|
||||
std::fs::write(p, qr.to_svg(logo.as_deref(), fg, bg))?;
|
||||
eprintln!("已生成: {p} (版本 {}, SVG)", qr.version.0);
|
||||
}
|
||||
_ => {
|
||||
let fmt = qr_core::render::image::OutputFormat::from_ext(&ext)
|
||||
.or_else(|| qr_core::render::image::OutputFormat::from_ext(&opts.format))
|
||||
.unwrap_or(qr_core::render::image::OutputFormat::Png);
|
||||
std::fs::write(p, qr.to_image_bytes(opts.size, logo.as_deref(), Some(fmt))?)?;
|
||||
eprintln!("已生成: {p} (版本 {}, {}×{}, {:?}, {})", qr.version.0, qr.size(), qr.size(), qr.level, fmt.extension());
|
||||
.unwrap_or_else(|| {
|
||||
eprintln!("警告: 无法识别扩展名 '{}',回退到 PNG", ext);
|
||||
qr_core::render::image::OutputFormat::Png
|
||||
});
|
||||
std::fs::write(
|
||||
p,
|
||||
qr.to_image_bytes(opts.size, logo.as_deref(), Some(fmt), fg, bg)?,
|
||||
)?;
|
||||
eprintln!(
|
||||
"已生成: {p} (版本 {}, {}×{}, {:?}, {})",
|
||||
qr.version.0,
|
||||
qr.size(),
|
||||
qr.size(),
|
||||
qr.level,
|
||||
fmt.extension()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
None => { println!("{}", qr.to_ascii(false)); }
|
||||
None => {
|
||||
println!("{}", qr.to_ascii(false));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn build_mode(mode: &str, opts: &EncodeOpts, fb: &str) -> Result<String, E> {
|
||||
fn build_mode(mode: &str, opts: &EncodeOpts, fb: &str) -> Result<String> {
|
||||
match mode {
|
||||
"wifi" => {
|
||||
let s = opts.ssid.as_deref().ok_or_else(|| E::new("WiFi 模式需要 --ssid", 1))?;
|
||||
Ok(text_builder::build_wifi_text(s, opts.password.as_deref().unwrap_or(""), &opts.encryption, opts.hidden))
|
||||
let s = opts
|
||||
.ssid
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow::anyhow!("WiFi 模式需要 --ssid"))?;
|
||||
Ok(text_builder::build_wifi_text(
|
||||
s,
|
||||
opts.password.as_deref().unwrap_or(""),
|
||||
&opts.encryption,
|
||||
opts.hidden,
|
||||
))
|
||||
}
|
||||
"vcard" => Ok(text_builder::build_vcard_text(
|
||||
opts.name.as_deref().unwrap_or(""), opts.phone.as_deref().unwrap_or(""),
|
||||
opts.email.as_deref().unwrap_or(""), opts.company.as_deref().unwrap_or(""),
|
||||
opts.address.as_deref().unwrap_or(""), opts.title.as_deref().unwrap_or(""),
|
||||
opts.vcard_url.as_deref().unwrap_or(""), opts.birthday.as_deref().unwrap_or(""),
|
||||
opts.note.as_deref().unwrap_or(""), opts.photo.as_deref().unwrap_or(""),
|
||||
opts.name.as_deref().unwrap_or(""),
|
||||
opts.phone.as_deref().unwrap_or(""),
|
||||
opts.email.as_deref().unwrap_or(""),
|
||||
opts.company.as_deref().unwrap_or(""),
|
||||
opts.address.as_deref().unwrap_or(""),
|
||||
opts.title.as_deref().unwrap_or(""),
|
||||
opts.vcard_url.as_deref().unwrap_or(""),
|
||||
opts.birthday.as_deref().unwrap_or(""),
|
||||
opts.note.as_deref().unwrap_or(""),
|
||||
opts.photo.as_deref().unwrap_or(""),
|
||||
)),
|
||||
"email" => {
|
||||
let t = opts.to.as_deref().ok_or_else(|| E::new("Email 模式需要 --to", 1))?;
|
||||
Ok(text_builder::build_email_text(t, opts.subject.as_deref().unwrap_or(""), opts.body.as_deref().unwrap_or("")))
|
||||
let t = opts
|
||||
.to
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow::anyhow!("Email 模式需要 --to"))?;
|
||||
Ok(text_builder::build_email_text(
|
||||
t,
|
||||
opts.subject.as_deref().unwrap_or(""),
|
||||
opts.body.as_deref().unwrap_or(""),
|
||||
))
|
||||
}
|
||||
"phone" => Ok(text_builder::build_phone_text(opts.number.as_deref().ok_or_else(|| E::new("需要 --number", 1))?)),
|
||||
"phone" => Ok(text_builder::build_phone_text(
|
||||
opts.number
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow::anyhow!("需要 --number"))?,
|
||||
)),
|
||||
"sms" => Ok(text_builder::build_sms_text(
|
||||
opts.number.as_deref().ok_or_else(|| E::new("需要 --number", 1))?,
|
||||
opts.number
|
||||
.as_deref()
|
||||
.ok_or_else(|| anyhow::anyhow!("需要 --number"))?,
|
||||
opts.message.as_deref().unwrap_or(""),
|
||||
)),
|
||||
"url" => opts.url.clone().ok_or_else(|| E::new("URL 模式需要 --url", 1)),
|
||||
"url" => opts
|
||||
.url
|
||||
.clone()
|
||||
.ok_or_else(|| anyhow::anyhow!("URL 模式需要 --url")),
|
||||
"text" => Ok(fb.to_string()),
|
||||
_m => bail!("未知模式: {_m},支持 text/url/wifi/vcard/email/phone/sms/batch", 1),
|
||||
_m => bail!("未知模式: {_m},支持 text/url/wifi/vcard/email/phone/sms/batch"),
|
||||
}
|
||||
}
|
||||
|
||||
// ──────────────────── 解码 ────────────────────
|
||||
|
||||
fn cmd_decode(file: &str) -> Result<(), E> {
|
||||
let bytes = if file == "-" { stdin_bytes()? } else { std::fs::read(file)? };
|
||||
let r = qr_core::decoder::decode_image(&bytes).map_err(|e| E::new(format!("解码失败: {e}"), 1))?;
|
||||
fn cmd_decode(file: &str) -> Result<()> {
|
||||
if file != "-" {
|
||||
check_path(file)?;
|
||||
}
|
||||
let bytes = if file == "-" {
|
||||
stdin_bytes()?
|
||||
} else {
|
||||
std::fs::read(file)?
|
||||
};
|
||||
let r = qr_core::decoder::decode_image(&bytes)
|
||||
.map_err(|e| anyhow::anyhow!("解码失败: {e}"))?;
|
||||
println!("{}", r.text);
|
||||
eprintln!("版本: {} 级别: {:?} 掩码: {} 纠错: {} 码字", r.version, r.level, r.mask, r.errors_corrected);
|
||||
eprintln!(
|
||||
"版本: {} 级别: {:?} 掩码: {} 纠错: {} 码字",
|
||||
r.version, r.level, r.mask, r.errors_corrected
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// ──────────────────── 批量 ────────────────────
|
||||
|
||||
fn do_batch(file: &str, opts: &EncodeOpts) -> Result<(), E> {
|
||||
fn do_batch(file: &str, opts: &EncodeOpts) -> Result<()> {
|
||||
let input = std::fs::read_to_string(file)
|
||||
.map_err(|e| E::new(format!("无法读取批量文件 '{file}': {e}"), 2))?;
|
||||
.with_context(|| format!("无法读取批量文件 '{file}'"))?;
|
||||
let entries: Vec<BatchEntry> = serde_json::from_str(&input)
|
||||
.or_else(|_| parse_csv(&input))
|
||||
.map_err(|e| E::new(format!("解析失败: {e}"), 2))?;
|
||||
.map_err(|e| anyhow::anyhow!("解析失败: {e}"))?;
|
||||
let out = opts.output_dir.as_deref().unwrap_or("batch_output");
|
||||
std::fs::create_dir_all(out).map_err(|e| E::new(format!("无法创建目录 '{out}': {e}"), 2))?;
|
||||
std::fs::create_dir_all(out)
|
||||
.with_context(|| format!("无法创建目录 '{out}'"))?;
|
||||
|
||||
let total = entries.len();
|
||||
let pb = ProgressBar::new(total as u64);
|
||||
pb.set_style(ProgressStyle::default_bar().template("{spinner:.green} [{bar:30.cyan/blue}] {pos}/{len} {msg}").unwrap());
|
||||
pb.set_style(
|
||||
ProgressStyle::default_bar()
|
||||
.template("{spinner:.green} [{bar:30.cyan/blue}] {pos}/{len} {msg}")
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
for (i, e) in entries.iter().enumerate() {
|
||||
let text = batch_text(e)?;
|
||||
let lvl = e.level.as_deref().map(parse_level).unwrap_or(Ok(EcLevel::M))?;
|
||||
let cfg = QrConfig { level: lvl, version: VersionMode::Auto, margin: opts.margin, fg_color: opts.fg.clone(), bg_color: opts.bg.clone() };
|
||||
let qr = QrCode::encode(&text, cfg).map_err(|e| E::new(e, 1))?;
|
||||
let lvl: EcLevel = e
|
||||
.level
|
||||
.as_deref()
|
||||
.map(|s| s.parse())
|
||||
.unwrap_or(Ok(EcLevel::M))
|
||||
.map_err(|e: String| anyhow::anyhow!(e))?;
|
||||
let cfg = QrConfig {
|
||||
level: lvl,
|
||||
version: VersionMode::Auto,
|
||||
margin: opts.margin,
|
||||
};
|
||||
let qr = QrCode::encode(&text, cfg).map_err(|e| anyhow::anyhow!(e))?;
|
||||
let path = format!("{out}/qr_{:04}.png", i + 1);
|
||||
std::fs::write(&path, qr.to_png_bytes(opts.size, None).map_err(|e| E::new(format!("{e}"), 2))?)?;
|
||||
std::fs::write(
|
||||
&path,
|
||||
qr.to_png_bytes(opts.size, None)
|
||||
.map_err(|e| anyhow::anyhow!("{e}"))?,
|
||||
)?;
|
||||
pb.set_message(path.clone());
|
||||
pb.inc(1);
|
||||
}
|
||||
@@ -256,55 +370,117 @@ fn do_batch(file: &str, opts: &EncodeOpts) -> Result<(), E> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn batch_text(e: &BatchEntry) -> Result<String, E> {
|
||||
if let Some(c) = &e.content { return Ok(c.clone()); }
|
||||
if let Some(u) = &e.url { return Ok(u.clone()); }
|
||||
fn batch_text(e: &BatchEntry) -> Result<String> {
|
||||
if let Some(c) = &e.content {
|
||||
return Ok(c.clone());
|
||||
}
|
||||
if let Some(u) = &e.url {
|
||||
return Ok(u.clone());
|
||||
}
|
||||
if let Some(s) = &e.ssid {
|
||||
return Ok(text_builder::build_wifi_text(s, e.password.as_deref().unwrap_or(""), e.encryption.as_deref().unwrap_or("WPA"), e.hidden.unwrap_or(false)));
|
||||
return Ok(text_builder::build_wifi_text(
|
||||
s,
|
||||
e.password.as_deref().unwrap_or(""),
|
||||
e.encryption.as_deref().unwrap_or("WPA"),
|
||||
e.hidden.unwrap_or(false),
|
||||
));
|
||||
}
|
||||
if let Some(n) = &e.name {
|
||||
return Ok(text_builder::build_vcard_text(n, e.phone.as_deref().unwrap_or(""), e.email.as_deref().unwrap_or(""), e.company.as_deref().unwrap_or(""), e.address.as_deref().unwrap_or(""), "", "", "", "", ""));
|
||||
return Ok(text_builder::build_vcard_text(
|
||||
n,
|
||||
e.phone.as_deref().unwrap_or(""),
|
||||
e.email.as_deref().unwrap_or(""),
|
||||
e.company.as_deref().unwrap_or(""),
|
||||
e.address.as_deref().unwrap_or(""),
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
));
|
||||
}
|
||||
if let Some(t) = &e.to {
|
||||
return Ok(text_builder::build_email_text(t, e.subject.as_deref().unwrap_or(""), e.body.as_deref().unwrap_or("")));
|
||||
return Ok(text_builder::build_email_text(
|
||||
t,
|
||||
e.subject.as_deref().unwrap_or(""),
|
||||
e.body.as_deref().unwrap_or(""),
|
||||
));
|
||||
}
|
||||
if let Some(n) = &e.number {
|
||||
if let Some(m) = &e.message { return Ok(text_builder::build_sms_text(n, m)); }
|
||||
if let Some(m) = &e.message {
|
||||
return Ok(text_builder::build_sms_text(n, m));
|
||||
}
|
||||
return Ok(text_builder::build_phone_text(n));
|
||||
}
|
||||
bail!("无法识别的条目格式", 1)
|
||||
bail!("无法识别的条目格式")
|
||||
}
|
||||
|
||||
// ──────────────────── 工具函数 ────────────────────
|
||||
|
||||
fn parse_csv(input: &str) -> Result<Vec<BatchEntry>, String> {
|
||||
let mut lines = input.lines();
|
||||
let cols: Vec<&str> = lines.next().ok_or("CSV 为空")?.split(',').map(|s| s.trim()).collect();
|
||||
let cols: Vec<&str> = lines
|
||||
.next()
|
||||
.ok_or("CSV 为空")?
|
||||
.split(',')
|
||||
.map(|s| s.trim())
|
||||
.collect();
|
||||
let mut out = Vec::new();
|
||||
for line in lines {
|
||||
if line.trim().is_empty() { continue; }
|
||||
let v: Vec<String> = line.split(',').map(|s| s.trim().trim_matches('"').to_string()).collect();
|
||||
let (mut c,mut l,mut ss,mut pw,mut en,mut hi,mut na,mut ph,mut em,mut co,mut ad,mut to_,mut su,mut bo,mut nu,mut ms,mut ur) =
|
||||
(None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None,None);
|
||||
for (i, col) in cols.iter().enumerate() {
|
||||
let val = v.get(i).cloned();
|
||||
match *col { "content"=>c=val,"level"=>l=val,"ssid"=>ss=val,"password"=>pw=val,"encryption"=>en=val,"hidden"=>hi=val.map(|x|x=="true"),"name"=>na=val,"phone"=>ph=val,"email"=>em=val,"company"=>co=val,"address"=>ad=val,"to"=>to_=val,"subject"=>su=val,"body"=>bo=val,"number"=>nu=val,"message"=>ms=val,"url"=>ur=val, _=>{} }
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
out.push(BatchEntry{content:c,level:l,ssid:ss,password:pw,encryption:en,hidden:hi,name:na,phone:ph,email:em,company:co,address:ad,to:to_,subject:su,body:bo,number:nu,message:ms,url:ur});
|
||||
let values: Vec<String> = line
|
||||
.split(',')
|
||||
.map(|s| s.trim().trim_matches('"').to_string())
|
||||
.collect();
|
||||
|
||||
// 使用 HashMap 替代 17 字段元组解构
|
||||
let mut map = std::collections::HashMap::new();
|
||||
for (i, col) in cols.iter().enumerate() {
|
||||
if let Some(val) = values.get(i).cloned() {
|
||||
map.insert(*col, val);
|
||||
}
|
||||
}
|
||||
|
||||
let get = |k: &str| map.get(k).cloned();
|
||||
let hidden = get("hidden").map(|x| x == "true");
|
||||
|
||||
out.push(BatchEntry {
|
||||
content: get("content"),
|
||||
level: get("level"),
|
||||
ssid: get("ssid"),
|
||||
password: get("password"),
|
||||
encryption: get("encryption"),
|
||||
hidden,
|
||||
name: get("name"),
|
||||
phone: get("phone"),
|
||||
email: get("email"),
|
||||
company: get("company"),
|
||||
address: get("address"),
|
||||
to: get("to"),
|
||||
subject: get("subject"),
|
||||
body: get("body"),
|
||||
number: get("number"),
|
||||
message: get("message"),
|
||||
url: get("url"),
|
||||
});
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn parse_level(s: &str) -> Result<EcLevel, E> {
|
||||
match s.to_uppercase().as_str() {
|
||||
"L"=>Ok(EcLevel::L),"M"=>Ok(EcLevel::M),"Q"=>Ok(EcLevel::Q),"H"=>Ok(EcLevel::H),
|
||||
_ => bail!("无效纠错级别: '{s}',支持 L/M/Q/H", 1),
|
||||
fn check_path(p: &str) -> Result<()> {
|
||||
let path = Path::new(p);
|
||||
// 禁止绝对路径
|
||||
if path.is_absolute() {
|
||||
bail!("不允许使用绝对路径: {p}");
|
||||
}
|
||||
}
|
||||
|
||||
fn check_path(p: &str) -> Result<(), E> {
|
||||
if Path::new(p).components().any(|c| matches!(c, std::path::Component::ParentDir)) {
|
||||
bail!("路径不允许包含 '..'", 2);
|
||||
// 禁止父目录遍历
|
||||
if path
|
||||
.components()
|
||||
.any(|c| matches!(c, std::path::Component::ParentDir))
|
||||
{
|
||||
bail!("路径不允许包含 '..': {p}");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user