use std::env;
use std::ffi::OsStr;
use std::io;
use std::net::{AddrParseError, IpAddr, SocketAddr};
use std::num::{FpCategory, IntErrorKind};
use std::path::PathBuf;
use clap::{AppSettings, Parser};
use humantime::{parse_duration, DurationError};
const DEFAULT_PORT: u16 = 3000;
pub fn addr_from_str(s: &str) -> Result<SocketAddr, AddrParseError> {
if let Ok(port) = s.parse::<u16>() {
return Ok(SocketAddr::from(([127, 0, 0, 1], port)));
}
if let Ok(host) = s.parse::<IpAddr>() {
return Ok(SocketAddr::from((
host,
env::var("PORT")
.ok()
.and_then(|p| p.parse().ok())
.unwrap_or(DEFAULT_PORT),
)));
}
s.parse::<SocketAddr>()
}
#[derive(Debug)]
pub struct CanonicalizedPath {
pub raw: PathBuf,
pub canonical: PathBuf,
}
impl CanonicalizedPath {
pub fn is_current_dir(&self) -> bool {
env::current_dir().map_or(false, |cwd| cwd == self.canonical)
}
}
fn parse_canonicalize_dir(s: &OsStr) -> Result<CanonicalizedPath, io::Error> {
let raw = PathBuf::from(s);
let canonical = raw.canonicalize()?;
Ok(CanonicalizedPath { raw, canonical })
}
fn parse_cache_time(s: &str) -> color_eyre::Result<u32> {
let duration = match parse_duration(s) {
Ok(duration) => duration,
Err(err) => {
if matches!(
err,
DurationError::UnknownUnit { ref unit, .. }
if unit.is_empty()
) {
match s.parse::<u32>() {
Ok(seconds) => return Ok(seconds),
Err(err) if *err.kind() == IntErrorKind::PosOverflow => {
return Err(color_eyre::eyre::eyre!("cache time is too large"))
}
Err(_) => {}
}
}
return Err(err.into());
}
};
let secs = duration.as_secs_f64();
assert!(
matches!(secs.classify(), FpCategory::Normal | FpCategory::Zero),
"humantime should not return NaN or infinite values"
);
if secs != secs.trunc() {
return Err(color_eyre::eyre::eyre!(
"cache time in seconds cannot be fractional"
));
}
if secs > u32::MAX as f64 {
return Err(color_eyre::eyre::eyre!("cache time is too large"));
}
Ok(secs as u32)
}
#[derive(Debug, Parser)]
#[clap(name = "Zy")]
#[clap(about, version, setting = AppSettings::DeriveDisplayOrder)]
pub struct Args {
#[clap(default_value = ".", parse(try_from_os_str = parse_canonicalize_dir))]
pub dir: CanonicalizedPath,
#[clap(short, long, value_name = "URI", multiple_occurrences = true)]
#[clap(verbatim_doc_comment, parse(try_from_str = addr_from_str))]
pub listen: Vec<SocketAddr>,
#[clap(short, long)]
pub spa: bool,
#[clap(short, long, value_name = "FILE", default_value = "index.html")]
pub index: String,
#[clap(long = "404", value_name = "FILE", default_value = "404.html")]
pub not_found: String,
#[clap(short, long, value_name = "TIME", verbatim_doc_comment)]
#[clap(default_value = "1h", hide_default_value = true)]
#[clap(parse(try_from_str = parse_cache_time))]
pub cache: u32,
#[clap(long)]
pub no_cors: bool,
#[clap(short, long)]
pub all: bool,
#[clap(short, long)]
pub follow_links: bool,
#[clap(short, long)]
pub verbose: bool,
#[clap(short = 'x', long)]
pub confirm_exit: bool,
#[clap(short = 'Z', long, alias = "anon")]
pub anonymize: bool,
}