use std::path::PathBuf;
use anyhow::{bail, Context, Result};
use clap::{Parser, Subcommand};
use shadowvpn::config::FileConfig;
use shadowvpn::uri;
#[derive(Parser)]
#[command(
name = "shadowvpn-uri",
about = "Import/export a ShadowVPN client config as a shadowvpn:// URI or QR code."
)]
struct Cli {
#[command(subcommand)]
action: Action,
}
#[derive(Subcommand)]
enum Action {
Export {
#[arg(short = 'c', long = "config")]
config: PathBuf,
#[arg(long)]
qr: bool,
},
Import {
uri: Option<String>,
#[arg(long, value_name = "FILE")]
image: Option<PathBuf>,
#[arg(short = 'o', long = "out")]
out: Option<PathBuf>,
},
}
fn main() -> Result<()> {
match Cli::parse().action {
Action::Export { config, qr } => {
let file = FileConfig::load(&config)
.with_context(|| format!("loading config {}", config.display()))?;
let encoded = uri::encode(&file);
println!("{encoded}");
if qr {
let rendered = uri::render_qr(&encoded).context("rendering QR code")?;
println!("\n{rendered}");
}
Ok(())
}
Action::Import { uri, image, out } => {
let text = match (uri, image) {
(Some(_), Some(_)) => bail!("provide either a URI argument or --image, not both"),
(None, None) => bail!("provide a shadowvpn:// URI argument or --image <FILE>"),
(Some(s), None) => s,
(None, Some(path)) => uri::decode_qr_image(&path)
.with_context(|| format!("decoding QR code from {}", path.display()))?,
};
let file = uri::decode(&text).context("decoding shadowvpn:// URI")?;
let json = serde_json::to_string_pretty(&file).context("serializing config to JSON")?;
match out {
Some(path) => {
std::fs::write(&path, format!("{json}\n"))
.with_context(|| format!("writing {}", path.display()))?;
eprintln!("wrote config to {}", path.display());
}
None => println!("{json}"),
}
Ok(())
}
}
}