use anyhow::{bail, Context, Result}; use clap::{CommandFactory, Parser, Subcommand}; use clap_complete::{generate, Shell}; use indicatif::{ProgressBar, ProgressStyle}; use qr_core::qr::{QrCode, QrConfig, VersionMode}; use qr_core::text_builder; use qr_core::version::EcLevel; use qr_core::QrError; use serde::Deserialize; use std::io::{self, Read}; use std::path::Path; use std::process; // ──────────────────── 结构定义 ──────────────────── #[derive(Parser)] #[command( name = "qrgen", version, about = "QR 码生成/解码工具 — 从零手搓的 ISO/IEC 18004 实现", after_help = "示例:\n qrgen encode \"Hello\" -o qr.png\n qrgen encode --mode wifi --ssid MyWiFi --password pass123\n qrgen decode qr.png\n echo \"Hello\" | qrgen encode -\n\n补全:\n qrgen --generate-completions bash > /usr/share/bash-completion/completions/qrgen" )] struct Cli { #[command(subcommand)] command: Command, #[arg(long, value_name = "SHELL", value_parser = ["bash", "zsh", "fish", "powershell", "elvish"])] generate_completions: Option, } #[derive(Subcommand)] #[allow(clippy::large_enum_variant)] enum Command { /// 编码:文本 → QR 码 Encode { /// 要编码的内容(传 `-` 从 stdin 读取) #[arg(default_value = "-")] content: String, /// 输出文件;不指定则终端 ASCII #[arg(short = 'o', long)] output: Option, #[command(flatten)] opts: EncodeOpts, }, /// 解码:QR 码图片 → 文本 Decode { /// 图片文件路径(传 `-` 从 stdin 读取) #[arg(default_value = "-")] file: String, }, } #[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, #[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", value_parser = clap::value_parser!(u8).range(0..=100))] margin: u8, #[arg(long)] fg: Option, #[arg(long)] bg: Option, #[arg(long)] logo: Option, #[arg(short = 'f', long, default_value = "png")] format: String, #[arg(long)] mode: Option, // WiFi — 密码优先从环境变量 QRGEN_WIFI_PASSWORD 读取,避免命令行泄露 #[arg(long)] ssid: Option, #[arg(long)] password: Option, #[arg(long, default_value = "WPA")] encryption: String, #[arg(long)] hidden: bool, // vCard #[arg(long)] name: Option, #[arg(long)] phone: Option, #[arg(long)] email: Option, #[arg(long)] company: Option, #[arg(long)] title: Option, #[arg(long)] address: Option, #[arg(long = "vcard-url")] vcard_url: Option, #[arg(long)] birthday: Option, #[arg(long)] note: Option, #[arg(long)] photo: Option, // Email #[arg(long)] to: Option, #[arg(long)] subject: Option, #[arg(long)] body: Option, // Phone/SMS #[arg(long)] number: Option, #[arg(long)] message: Option, // URL #[arg(long)] url: Option, // Batch #[arg(long)] batch: Option, #[arg(long)] output_dir: Option, } #[derive(Deserialize)] struct BatchEntry { content: Option, level: Option, ssid: Option, password: Option, encryption: Option, #[serde(default)] hidden: Option, name: Option, phone: Option, email: Option, company: Option, address: Option, to: Option, subject: Option, body: Option, number: Option, message: Option, url: Option, } // ──────────────────── 入口 ──────────────────── fn main() { let cli = Cli::parse(); if let Some(s) = cli.generate_completions { if let Ok(sh) = s.parse::() { generate(sh, &mut Cli::command(), "qrgen", &mut io::stdout()); return; } } let r = match cli.command { Command::Encode { content, output, opts } => cmd_encode(&content, &output, &opts), Command::Decode { file } => cmd_decode(&file), }; if let Err(e) = r { eprintln!("qrgen: {:#}", e); process::exit(1); } } // ──────────────────── I/O 辅助 ──────────────────── /// 最大 stdin 读取量:10 MB const STDIN_MAX: u64 = 10 * 1024 * 1024; fn stdin_bytes() -> Result> { let mut b = Vec::new(); io::stdin() .take(STDIN_MAX) .read_to_end(&mut b) .with_context(|| "无法读取 stdin")?; Ok(b) } fn stdin_text() -> Result { let mut s = String::new(); 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 为空"); } Ok(t) } // ──────────────────── 编码 ──────────────────── fn cmd_encode(content: &str, output: &Option, 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 { check_path(bf)?; // 批量文件路径检查 if let Some(od) = &opts.output_dir { check_path(od)?; // 输出目录路径检查 } return do_batch(bf, opts); } else { text }; let level: EcLevel = opts.level.parse().map_err(|e: QrError| anyhow::anyhow!(e))?; // --logo 文件路径也需安全检查 if let Some(logo_path) = &opts.logo { check_path(logo_path)?; } 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, }; 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(); if ext.is_empty() { eprintln!("警告: 无法从扩展名推断输出格式,回退到 PNG"); } match ext.as_str() { "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_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)); } } Ok(()) } fn build_mode(mode: &str, opts: &EncodeOpts, fb: &str) -> Result { match mode { "wifi" => { let s = opts .ssid .as_deref() .ok_or_else(|| anyhow::anyhow!("WiFi 模式需要 --ssid"))?; // 密码优先从 --password 读取,未提供时尝试环境变量 QRGEN_WIFI_PASSWORD let env_pwd = std::env::var("QRGEN_WIFI_PASSWORD").ok(); let pwd = opts .password .as_deref() .or(env_pwd.as_deref()) .unwrap_or(""); Ok(text_builder::build_wifi_text( s, pwd, &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(""), )), "email" => { 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(|| anyhow::anyhow!("需要 --number"))?, )), "sms" => Ok(text_builder::build_sms_text( opts.number .as_deref() .ok_or_else(|| anyhow::anyhow!("需要 --number"))?, opts.message.as_deref().unwrap_or(""), )), "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"), } } // ──────────────────── 解码 ──────────────────── 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 ); Ok(()) } // ──────────────────── 批量 ──────────────────── fn do_batch(file: &str, opts: &EncodeOpts) -> Result<()> { let input = std::fs::read_to_string(file) .with_context(|| format!("无法读取批量文件 '{file}'"))?; let entries: Vec = serde_json::from_str(&input) .or_else(|_| parse_csv(&input)) .map_err(|e| anyhow::anyhow!("解析失败: {e}"))?; let out = opts.output_dir.as_deref().unwrap_or("batch_output"); 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(), ); for (i, e) in entries.iter().enumerate() { let text = batch_text(e)?; let lvl: EcLevel = e .level .as_deref() .map(|s| s.parse()) .unwrap_or(Ok(EcLevel::M)) .map_err(|e: QrError| 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| anyhow::anyhow!("{e}"))?, )?; pb.set_message(path.clone()); pb.inc(1); } pb.finish_with_message(format!("完成: {total} 个 QR → {out}")); Ok(()) } fn batch_text(e: &BatchEntry) -> Result { 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), )); } 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(""), "", "", "", "", "", )); } 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(""), )); } if let Some(n) = &e.number { if let Some(m) = &e.message { return Ok(text_builder::build_sms_text(n, m)); } return Ok(text_builder::build_phone_text(n)); } bail!("无法识别的条目格式") } // ──────────────────── 工具函数 ──────────────────── fn parse_csv(input: &str) -> Result, String> { let mut lines = input.lines(); 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 values: Vec = 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 check_path(p: &str) -> Result<()> { let path = Path::new(p); // 禁止绝对路径 if path.is_absolute() { bail!("不允许使用绝对路径: {p}"); } // 禁止父目录遍历 if path .components() .any(|c| matches!(c, std::path::Component::ParentDir)) { bail!("路径不允许包含 '..': {p}"); } Ok(()) }