use std::collections::{BTreeMap, BTreeSet};
use std::ffi::{OsStr, OsString};
use std::path::Path;
use std::process::Command;
pub(super) enum Kind {
Unknown,
Proxy,
Build,
External,
}
pub(super) struct Invocation {
pub arguments: Vec<OsString>,
pub kind: Kind,
}
#[derive(serde::Deserialize)]
#[serde(untagged)]
enum Alias {
String(String),
Array(Vec<String>),
}
impl Alias {
fn arguments(self) -> Vec<OsString> {
match self {
Self::String(value) => value.split_whitespace().map(OsString::from).collect(),
Self::Array(value) => value.into_iter().map(OsString::from).collect(),
}
}
}
fn aliases(cwd: &Path) -> Option<BTreeMap<String, Alias>> {
for path in cargo_config2::Walk::new(cwd) {
let value: toml::Value = toml::from_str(&std::fs::read_to_string(path).ok()?).ok()?;
if value.get("include").is_some() {
return None;
}
}
let config = cargo_config2::de::Config::load_with_cwd(cwd).ok()?;
serde_json::from_value(serde_json::to_value(config.alias).ok()?).ok()
}
fn builtin(command: &str) -> bool {
matches!(
command,
"build"
| "check"
| "test"
| "run"
| "bench"
| "doc"
| "rustc"
| "rustdoc"
| "fix"
| "install"
| "help"
| "new"
| "init"
| "add"
| "remove"
| "update"
| "fetch"
| "clean"
| "config"
| "tree"
| "search"
| "info"
| "login"
| "logout"
| "owner"
| "package"
| "publish"
| "uninstall"
| "yank"
| "locate-project"
| "metadata"
| "generate-lockfile"
| "pkgid"
| "read-manifest"
| "report"
| "vendor"
| "verify-project"
| "version"
| "git-checkout"
)
}
fn default_alias(command: &str) -> Option<&'static str> {
match command {
"b" => Some("build"),
"c" => Some("check"),
"d" => Some("doc"),
"r" => Some("run"),
"t" => Some("test"),
"rm" => Some("remove"),
_ => None,
}
}
fn unsupported(arguments: &[OsString]) -> bool {
arguments.iter().take_while(|arg| *arg != "--").any(|arg| {
let arg = arg.to_string_lossy();
arg.starts_with("-Z") || arg.starts_with("-C") || arg.starts_with("--directory")
})
}
fn config_override(arguments: &[OsString]) -> bool {
arguments
.iter()
.take_while(|arg| *arg != "--")
.any(|arg| arg == "--config" || arg.to_string_lossy().starts_with("--config="))
}
pub(super) fn expanded_arguments(cargo: &OsStr, arguments: &[String]) -> Option<Vec<String>> {
let typed = super::launch::cargo_subcommand(arguments)?;
if builtin(typed) || typed == "clippy" {
return Some(arguments.to_vec());
}
let typed_arguments = arguments.iter().map(OsString::from).collect::<Vec<_>>();
let resolved = super::strings(&resolve_with(cargo, &typed_arguments, false)?.arguments).ok()?;
let separator = |arguments: &[String]| arguments.iter().any(|argument| argument == "--");
if separator(&resolved) && !separator(arguments) {
return None;
}
Some(resolved)
}
pub(super) fn resolve(cargo: &OsStr, arguments: &[OsString]) -> Option<Invocation> {
resolve_with(cargo, arguments, true)
}
fn resolve_with(cargo: &OsStr, arguments: &[OsString], classify: bool) -> Option<Invocation> {
let mut arguments = arguments.to_vec();
let mut aliases = None;
let mut expanded = BTreeSet::new();
loop {
if unsupported(&arguments) {
return None;
}
let (index, command) = super::launch::cargo_subcommand_at(&arguments)?;
if builtin(command) {
let kind = if super::shim::cargo_proxy_passthrough(&arguments) {
Kind::Proxy
} else {
Kind::Build
};
return Some(Invocation { arguments, kind });
}
if aliases.is_none() {
aliases = Some(self::aliases(&std::env::current_dir().ok()?));
}
let alias = match aliases.as_mut()?.as_mut() {
None => None,
Some(aliases) => {
let key = format!("CARGO_ALIAS_{}", command.replace('-', "_").to_uppercase());
let configured = aliases.remove(command);
match (configured, std::env::var_os(key)) {
(Some(Alias::Array(mut words)), Some(value)) => {
words.extend(
value
.into_string()
.ok()?
.split_whitespace()
.map(str::to_owned),
);
Some(Alias::Array(words))
}
(_, Some(value)) => Some(Alias::String(value.into_string().ok()?)),
(value, None) => value.or_else(|| {
default_alias(command).map(|value| Alias::String(value.to_owned()))
}),
}
}
};
if let Some(alias) = alias {
if config_override(&arguments)
|| expanded.len() == 32
|| !expanded.insert(command.to_owned())
{
return None;
}
let expansion = alias.arguments();
if expansion.is_empty() || config_override(&expansion) {
return None;
}
arguments.splice(index..index + 1, expansion);
continue;
}
if expanded.contains(command) {
return None;
}
if !classify {
return Some(Invocation {
arguments,
kind: Kind::Unknown,
});
}
let output = Command::new(cargo)
.args(&arguments[..index])
.args(["--color=never", "--list"])
.output()
.ok()?;
let listing = std::str::from_utf8(&output.stdout).ok()?;
if !output.status.success()
|| listing
.lines()
.next()
.is_none_or(|line| line != "Installed Commands:")
{
return None;
}
let entry = listing.lines().skip(1).find_map(|line| {
let line = line.trim();
let end = line.find(char::is_whitespace).unwrap_or(line.len());
(&line[..end] == command).then(|| line[end..].trim())
});
let kind = match entry {
Some(description) if description.starts_with("alias:") => return None,
Some(_) if super::shim::cargo_proxy_passthrough(&arguments) => Kind::Proxy,
Some(_) => Kind::External,
None => Kind::Unknown,
};
return Some(Invocation { arguments, kind });
}
}