use std::ffi::OsStr;
use crate::Error;
use crate::spec::CommandMeta;
pub fn declared_names<'a>(root: &CommandMeta<'a>) -> Vec<&'a str> {
let mut names = Vec::new();
let mut seen: Vec<*const CommandMeta<'a>> = Vec::new();
collect(root, &mut names, &mut seen);
names.sort_unstable();
names.dedup();
names
}
fn collect<'a>(
meta: &CommandMeta<'a>,
names: &mut Vec<&'a str>,
seen: &mut Vec<*const CommandMeta<'a>>,
) {
let ptr: *const CommandMeta<'a> = meta;
if seen.contains(&ptr) {
return;
}
seen.push(ptr);
let clause = meta.extra.clause;
let flags = meta
.flags
.iter()
.chain(clause.into_iter().flat_map(|c| c.flags.iter()));
for flag in flags {
names.extend(flag.env());
names.extend_from_slice(flag.env_fallback());
names.extend_from_slice(flag.deprecated_env());
}
let args = meta
.args
.iter()
.chain(clause.into_iter().flat_map(|c| c.args.iter()));
for arg in args {
names.extend(arg.env());
names.extend_from_slice(arg.env_fallback());
names.extend_from_slice(arg.deprecated_env());
}
for sub in meta.subcommands {
collect(sub, names, seen);
}
}
pub fn unknown<'n>(
root: &CommandMeta<'_>,
prefix: &str,
known: &[&str],
names: impl IntoIterator<Item = &'n OsStr>,
) -> Vec<String> {
if prefix.is_empty() {
return Vec::new();
}
let declared = declared_names(root);
let mut unknown: Vec<&OsStr> = names
.into_iter()
.filter(|name| {
starts_with_env(valid_head(name), prefix)
&& name
.to_str()
.is_none_or(|name| !declared.iter().chain(known).any(|k| same_env(k, name)))
})
.collect();
unknown.sort_by_cached_key(|name| (fold_env(&name.to_string_lossy()).into_owned(), *name));
unknown.dedup_by(|a, b| match (a.to_str(), b.to_str()) {
(Some(a), Some(b)) => same_env(a, b),
_ => a == b,
});
unknown
.into_iter()
.map(|name| name.to_string_lossy().into_owned())
.collect()
}
pub fn refuse_unknown<'t, 'v>(
root: &CommandMeta<'_>,
prefix: &'t str,
known: &[&str],
) -> Result<(), Error<'t, 'v>> {
let process: Vec<_> = std::env::vars_os().map(|(name, _)| name).collect();
let names = unknown(root, prefix, known, process.iter().map(|n| n.as_os_str()));
if names.is_empty() {
return Ok(());
}
Err(Error::UnknownEnv(::std::boxed::Box::new(
crate::UnknownEnv { prefix, names },
)))
}
fn fold_env(name: &str) -> std::borrow::Cow<'_, str> {
if !cfg!(windows) {
return name.into();
}
name.chars()
.map(|c| {
let mut upper = c.to_uppercase();
match (upper.next(), upper.next()) {
(Some(u), None) => u,
_ => c,
}
})
.collect::<String>()
.into()
}
fn same_env(a: &str, b: &str) -> bool {
fold_env(a) == fold_env(b)
}
fn starts_with_env(name: &str, prefix: &str) -> bool {
fold_env(name).starts_with(&*fold_env(prefix))
}
fn valid_head(name: &OsStr) -> &str {
let bytes = name.as_encoded_bytes();
match std::str::from_utf8(bytes) {
Ok(name) => name,
Err(e) => std::str::from_utf8(&bytes[..e.valid_up_to()]).unwrap_or_default(),
}
}