use std::{
collections::BTreeSet,
ffi::{OsStr, OsString},
path::{Component, Path},
};
#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
pub enum SelectionFlag {
NonBuilding,
TargetDirectory,
InputSelection,
TargetSelection,
Library,
MessageFormat,
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct CargoSelection {
pub(crate) arguments: Vec<OsString>,
pub(crate) metadata_arguments: Vec<OsString>,
pub(crate) profile: Result<String, &'static str>,
pub(crate) flags: BTreeSet<SelectionFlag>,
}
impl CargoSelection {
pub(crate) fn profile_target_dir(&self) -> Result<&str, &'static str> {
let profile = self.profile.as_deref().map_err(|message| *message)?;
let directory = match profile {
"dev" | "test" => "debug",
"bench" => "release",
custom => custom,
};
let mut components = Path::new(directory).components();
if !matches!(
(components.next(), components.next()),
(Some(Component::Normal(_)), None)
) {
return Err("Cargo profile target directory must be one normal path component");
}
Ok(directory)
}
pub(crate) fn parse(arguments: Vec<OsString>) -> Self {
let mut metadata_arguments = Vec::new();
let mut flags = BTreeSet::new();
let mut profile = Ok(None);
let mut metadata_next = 0;
let mut profile_next = 0;
for (index, argument) in arguments.iter().enumerate() {
let bytes = argument.as_encoded_bytes();
let short = short_cargo_option(argument);
if let Some(flag) = selection_flag(argument, short) {
flags.insert(flag);
}
if index >= profile_next && profile.is_ok() {
let selected = if argument == "--profile" {
profile_next = index + 2;
arguments
.get(index + 1)
.and_then(|value| value.to_str())
.map(Some)
.ok_or("Cargo --profile requires a UTF-8 profile name")
} else if let Some(name) = argument
.to_str()
.and_then(|text| text.strip_prefix("--profile="))
{
Ok(Some(name))
} else {
let flags_end = short.map_or(bytes.len(), |(_, offset)| offset);
let release = argument == "--release"
|| (bytes.starts_with(b"-")
&& !bytes.starts_with(b"--")
&& bytes[..flags_end].contains(&b'r'));
if matches!(short, Some((_, offset)) if offset + 1 == bytes.len()) {
profile_next = index + 2;
}
Ok(release.then_some("release"))
};
match selected {
Err(error) => profile = Err(error),
Ok(Some(name)) => {
if name.is_empty() || profile.as_mut().unwrap().replace(name).is_some() {
profile = Err("select one nonempty Cargo profile");
}
}
Ok(None) => {}
}
}
if index < metadata_next {
continue;
}
if let Some((b'F', offset)) = short {
metadata_arguments.push(argument.to_str().map_or_else(
|| argument.clone(),
|text| OsString::from(format!("-F{}", &text[offset + 1..])),
));
if offset + 1 == bytes.len()
&& let Some(value) = arguments.get(index + 1)
{
metadata_arguments.push(value.clone());
metadata_next = index + 2;
}
continue;
}
let text = argument.to_string_lossy();
match text.as_ref() {
"--all-features"
| "--no-default-features"
| "--locked"
| "--offline"
| "--frozen" => metadata_arguments.push(argument.clone()),
"--features" | "--filter-platform" => {
metadata_arguments.push(argument.clone());
if let Some(value) = arguments.get(index + 1) {
metadata_arguments.push(value.clone());
metadata_next = index + 2;
}
}
_ if text.starts_with("--features=") || text.starts_with("--filter-platform=") => {
metadata_arguments.push(argument.clone());
}
_ => {}
}
}
let profile = profile.map(|name| name.unwrap_or("dev").to_owned());
Self {
arguments,
metadata_arguments,
profile,
flags,
}
}
}
fn selection_flag(argument: &OsStr, short: Option<(u8, usize)>) -> Option<SelectionFlag> {
let bytes = argument.as_encoded_bytes();
let option = bytes.split(|byte| *byte == b'=').next().unwrap_or_default();
if option == b"--message-format" {
return Some(SelectionFlag::MessageFormat);
}
if matches!(argument.to_str(), Some("--help" | "--unit-graph"))
|| matches!(short, Some((b'h', _)))
{
return Some(SelectionFlag::NonBuilding);
}
if argument == "--target-dir" || bytes.starts_with(b"--target-dir=") {
return Some(SelectionFlag::TargetDirectory);
}
if matches!(
option,
b"--manifest-path"
| b"--target"
| b"--package"
| b"--workspace"
| b"--all"
| b"--exclude"
| b"--config"
) || matches!(short, Some((b'm' | b'p' | b'C', _)))
{
return Some(SelectionFlag::InputSelection);
}
if matches!(
option,
b"--bin"
| b"--bins"
| b"--example"
| b"--examples"
| b"--test"
| b"--tests"
| b"--bench"
| b"--benches"
| b"--all-targets"
) {
return Some(SelectionFlag::TargetSelection);
}
if argument == "--lib" {
return Some(SelectionFlag::Library);
}
None
}
fn short_cargo_option(argument: &OsStr) -> Option<(u8, usize)> {
let bytes = argument.as_encoded_bytes();
if !bytes.starts_with(b"-") || bytes.starts_with(b"--") {
return None;
}
for (index, byte) in bytes.iter().copied().enumerate().skip(1) {
match byte {
b'v' | b'q' | b'r' => {}
b'h' | b'F' | b'j' | b'Z' | b'm' | b'p' | b'C' => return Some((byte, index)),
_ => return None,
}
}
None
}
#[cfg(test)]
mod tests;