use std::fmt;
use std::str::FromStr;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Condition {
KeyValue { key: ConditionKey, value: String },
All(Vec<Condition>),
Any(Vec<Condition>),
Not(Box<Condition>),
}
impl Condition {
pub fn parse_cfg(input: &str) -> Result<Self, ConditionParseError> {
let trimmed = input.trim();
let inner = trimmed
.strip_prefix("cfg")
.ok_or_else(|| ConditionParseError::ExpectedCfgPrefix(trimmed.to_owned()))?
.trim_start();
let inner = inner
.strip_prefix('(')
.ok_or_else(|| ConditionParseError::ExpectedCfgPrefix(trimmed.to_owned()))?;
let inner = inner
.strip_suffix(')')
.ok_or_else(|| ConditionParseError::UnbalancedParens(trimmed.to_owned()))?;
Self::parse_inner(inner)
}
pub fn parse_inner(input: &str) -> Result<Self, ConditionParseError> {
let mut parser = Parser::new(input);
let cond = parser.parse_condition()?;
parser.expect_eof()?;
Ok(cond)
}
pub fn evaluate(&self, platform: &TargetPlatform) -> bool {
match self {
Condition::KeyValue { key, value } => key.lookup(platform) == value,
Condition::All(items) => items.iter().all(|c| c.evaluate(platform)),
Condition::Any(items) => items.iter().any(|c| c.evaluate(platform)),
Condition::Not(inner) => !inner.evaluate(platform),
}
}
}
impl fmt::Display for Condition {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Condition::KeyValue { key, value } => write!(f, "{} = \"{}\"", key.as_str(), value),
Condition::All(items) => {
f.write_str("all(")?;
write_list(f, items)?;
f.write_str(")")
}
Condition::Any(items) => {
f.write_str("any(")?;
write_list(f, items)?;
f.write_str(")")
}
Condition::Not(inner) => write!(f, "not({inner})"),
}
}
}
fn write_list(f: &mut fmt::Formatter<'_>, items: &[Condition]) -> fmt::Result {
for (i, c) in items.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
write!(f, "{c}")?;
}
Ok(())
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ConditionKey {
Os,
Arch,
Family,
Env,
Abi,
Target,
}
impl ConditionKey {
pub const fn as_str(self) -> &'static str {
match self {
ConditionKey::Os => "os",
ConditionKey::Arch => "arch",
ConditionKey::Family => "family",
ConditionKey::Env => "env",
ConditionKey::Abi => "abi",
ConditionKey::Target => "target",
}
}
pub const fn all() -> &'static [ConditionKey] {
&[
ConditionKey::Os,
ConditionKey::Arch,
ConditionKey::Family,
ConditionKey::Env,
ConditionKey::Abi,
ConditionKey::Target,
]
}
fn lookup(self, platform: &TargetPlatform) -> &str {
match self {
ConditionKey::Os => platform.os.as_str(),
ConditionKey::Arch => platform.arch.as_str(),
ConditionKey::Family => platform.family.as_str(),
ConditionKey::Env => platform.env.as_str(),
ConditionKey::Abi => platform.abi.as_str(),
ConditionKey::Target => platform.target.as_str(),
}
}
}
impl FromStr for ConditionKey {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"os" => Ok(ConditionKey::Os),
"arch" => Ok(ConditionKey::Arch),
"family" => Ok(ConditionKey::Family),
"env" => Ok(ConditionKey::Env),
"abi" => Ok(ConditionKey::Abi),
"target" => Ok(ConditionKey::Target),
_ => Err(()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct TargetPlatform {
pub os: String,
pub arch: String,
pub family: String,
pub env: String,
pub abi: String,
pub target: String,
}
impl TargetPlatform {
pub fn current() -> Self {
let os = normalize_os(std::env::consts::OS);
let arch = normalize_arch(std::env::consts::ARCH);
let family = normalize_family(std::env::consts::FAMILY, &os);
let env = normalize_env(&os);
let abi = "unknown".to_owned();
let target = format!("{arch}-{family}-{os}");
Self {
os,
arch,
family,
env,
abi,
target,
}
}
}
fn normalize_os(raw: &str) -> String {
match raw {
"linux" | "macos" | "windows" | "freebsd" | "openbsd" | "netbsd" | "dragonfly"
| "android" | "ios" => raw.to_owned(),
"darwin" => "macos".to_owned(),
"" => "unknown".to_owned(),
other => other.to_owned(),
}
}
fn normalize_arch(raw: &str) -> String {
match raw {
"x86_64" | "aarch64" | "arm" | "riscv64" | "wasm32" => raw.to_owned(),
"" => "unknown".to_owned(),
other => other.to_owned(),
}
}
fn normalize_family(raw: &str, os: &str) -> String {
match raw {
"unix" | "windows" | "wasm" => raw.to_owned(),
_ => match os {
"linux" | "macos" | "freebsd" | "openbsd" | "netbsd" | "dragonfly" | "android"
| "ios" => "unix".to_owned(),
"windows" => "windows".to_owned(),
_ => "unknown".to_owned(),
},
}
}
fn normalize_env(os: &str) -> String {
match os {
"linux" => "gnu".to_owned(),
"macos" | "ios" => "apple".to_owned(),
"windows" => "msvc".to_owned(),
_ => "unknown".to_owned(),
}
}
#[derive(Debug, Clone, PartialEq, Eq, Error)]
pub enum ConditionParseError {
#[error("expected a `cfg(...)` expression but found {0:?}")]
ExpectedCfgPrefix(String),
#[error("`cfg(...)` expression has unbalanced parentheses: {0:?}")]
UnbalancedParens(String),
#[error(
"unsupported target cfg key {key:?}; supported keys are os, arch, family, env, abi, and target"
)]
UnsupportedKey { key: String },
#[error("expected `=` after key {key:?} in cfg expression")]
ExpectedEquals { key: String },
#[error("expected a quoted string value for key {key:?} in cfg expression; got {found:?}")]
ExpectedQuotedValue { key: String, found: String },
#[error("unterminated string literal in cfg expression: {0:?}")]
UnterminatedString(String),
#[error("trailing input after cfg expression: {0:?}")]
TrailingInput(String),
#[error("`all()` requires at least one condition")]
EmptyAll,
#[error("`any()` requires at least one condition")]
EmptyAny,
#[error("`not()` takes exactly one condition; found {0}")]
NotArity(usize),
#[error("expected `(` after {0}")]
ExpectedOpenParen(&'static str),
#[error("expected `)` to close {0}")]
ExpectedCloseParen(&'static str),
#[error("unexpected token in cfg expression: {0:?}")]
UnexpectedToken(String),
#[error("empty cfg expression")]
Empty,
}
struct Parser<'a> {
src: &'a str,
pos: usize,
}
impl<'a> Parser<'a> {
fn new(src: &'a str) -> Self {
Self { src, pos: 0 }
}
fn skip_whitespace(&mut self) {
while let Some(c) = self.peek_char() {
if c.is_whitespace() {
self.pos += c.len_utf8();
} else {
break;
}
}
}
fn peek_char(&self) -> Option<char> {
self.src[self.pos..].chars().next()
}
fn expect_eof(&mut self) -> Result<(), ConditionParseError> {
self.skip_whitespace();
if self.pos < self.src.len() {
Err(ConditionParseError::TrailingInput(
self.src[self.pos..].to_owned(),
))
} else {
Ok(())
}
}
fn parse_condition(&mut self) -> Result<Condition, ConditionParseError> {
self.skip_whitespace();
if self.pos >= self.src.len() {
return Err(ConditionParseError::Empty);
}
let ident = self.read_ident()?;
self.skip_whitespace();
match ident.as_str() {
"all" => {
self.expect_open_paren("all")?;
let items = self.parse_condition_list()?;
self.expect_close_paren("all")?;
if items.is_empty() {
return Err(ConditionParseError::EmptyAll);
}
Ok(Condition::All(items))
}
"any" => {
self.expect_open_paren("any")?;
let items = self.parse_condition_list()?;
self.expect_close_paren("any")?;
if items.is_empty() {
return Err(ConditionParseError::EmptyAny);
}
Ok(Condition::Any(items))
}
"not" => {
self.expect_open_paren("not")?;
let items = self.parse_condition_list()?;
self.expect_close_paren("not")?;
if items.len() != 1 {
return Err(ConditionParseError::NotArity(items.len()));
}
let inner = items.into_iter().next().expect("len==1 above");
Ok(Condition::Not(Box::new(inner)))
}
other => {
let key = ConditionKey::from_str(other).map_err(|()| {
ConditionParseError::UnsupportedKey {
key: other.to_owned(),
}
})?;
self.skip_whitespace();
if self.peek_char() != Some('=') {
return Err(ConditionParseError::ExpectedEquals {
key: other.to_owned(),
});
}
self.pos += 1; self.skip_whitespace();
let value = self.read_quoted_string(other)?;
Ok(Condition::KeyValue { key, value })
}
}
}
fn parse_condition_list(&mut self) -> Result<Vec<Condition>, ConditionParseError> {
let mut items = Vec::new();
self.skip_whitespace();
if self.peek_char() == Some(')') {
return Ok(items);
}
loop {
let cond = self.parse_condition()?;
items.push(cond);
self.skip_whitespace();
match self.peek_char() {
Some(',') => {
self.pos += 1;
self.skip_whitespace();
}
_ => break,
}
}
Ok(items)
}
fn expect_open_paren(&mut self, what: &'static str) -> Result<(), ConditionParseError> {
self.skip_whitespace();
if self.peek_char() == Some('(') {
self.pos += 1;
Ok(())
} else {
Err(ConditionParseError::ExpectedOpenParen(what))
}
}
fn expect_close_paren(&mut self, what: &'static str) -> Result<(), ConditionParseError> {
self.skip_whitespace();
if self.peek_char() == Some(')') {
self.pos += 1;
Ok(())
} else {
Err(ConditionParseError::ExpectedCloseParen(what))
}
}
fn read_ident(&mut self) -> Result<String, ConditionParseError> {
let start = self.pos;
while let Some(c) = self.peek_char() {
if c.is_ascii_alphanumeric() || c == '_' {
self.pos += c.len_utf8();
} else {
break;
}
}
if start == self.pos {
return Err(ConditionParseError::UnexpectedToken(
self.src[self.pos..].to_owned(),
));
}
Ok(self.src[start..self.pos].to_owned())
}
fn read_quoted_string(&mut self, key: &str) -> Result<String, ConditionParseError> {
if self.peek_char() != Some('"') {
let rest_start = self.pos;
while let Some(c) = self.peek_char() {
if c == ',' || c == ')' || c.is_whitespace() {
break;
}
self.pos += c.len_utf8();
}
return Err(ConditionParseError::ExpectedQuotedValue {
key: key.to_owned(),
found: self.src[rest_start..self.pos].to_owned(),
});
}
self.pos += 1;
let start = self.pos;
while let Some(c) = self.peek_char() {
if c == '"' {
let value = self.src[start..self.pos].to_owned();
self.pos += 1;
return Ok(value);
}
self.pos += c.len_utf8();
}
Err(ConditionParseError::UnterminatedString(
self.src[start..].to_owned(),
))
}
}
impl Serialize for Condition {
fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
s.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Condition {
fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
let raw = String::deserialize(d)?;
Condition::parse_inner(&raw).map_err(serde::de::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn linux_x86_64() -> TargetPlatform {
TargetPlatform {
os: "linux".into(),
arch: "x86_64".into(),
family: "unix".into(),
env: "gnu".into(),
abi: "unknown".into(),
target: "x86_64-unix-linux".into(),
}
}
fn macos_aarch64() -> TargetPlatform {
TargetPlatform {
os: "macos".into(),
arch: "aarch64".into(),
family: "unix".into(),
env: "apple".into(),
abi: "unknown".into(),
target: "aarch64-unix-macos".into(),
}
}
#[test]
fn parses_simple_key_value() {
let cond = Condition::parse_cfg(r#"cfg(os = "linux")"#).unwrap();
assert_eq!(
cond,
Condition::KeyValue {
key: ConditionKey::Os,
value: "linux".into()
}
);
}
#[test]
fn parses_each_supported_key() {
for (raw, key) in [
(r#"cfg(os = "linux")"#, ConditionKey::Os),
(r#"cfg(arch = "x86_64")"#, ConditionKey::Arch),
(r#"cfg(family = "unix")"#, ConditionKey::Family),
(r#"cfg(env = "gnu")"#, ConditionKey::Env),
(r#"cfg(abi = "eabi")"#, ConditionKey::Abi),
(
r#"cfg(target = "x86_64-unknown-linux-gnu")"#,
ConditionKey::Target,
),
] {
let cond = Condition::parse_cfg(raw).unwrap();
match cond {
Condition::KeyValue { key: k, .. } => assert_eq!(k, key, "{raw}"),
other => panic!("{raw}: expected key/value, got {other:?}"),
}
}
}
#[test]
fn parses_all_any_not() {
let all = Condition::parse_cfg(r#"cfg(all(os = "linux", arch = "x86_64"))"#).unwrap();
let any = Condition::parse_cfg(r#"cfg(any(os = "macos", os = "linux"))"#).unwrap();
let not = Condition::parse_cfg(r#"cfg(not(os = "windows"))"#).unwrap();
assert!(matches!(all, Condition::All(ref v) if v.len() == 2));
assert!(matches!(any, Condition::Any(ref v) if v.len() == 2));
assert!(matches!(not, Condition::Not(_)));
}
#[test]
fn rejects_unquoted_value() {
let err = Condition::parse_cfg(r"cfg(os = linux)").unwrap_err();
match err {
ConditionParseError::ExpectedQuotedValue { key, .. } => assert_eq!(key, "os"),
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn rejects_unsupported_key() {
let err = Condition::parse_cfg(r#"cfg(compiler = "clang")"#).unwrap_err();
match err {
ConditionParseError::UnsupportedKey { key } => assert_eq!(key, "compiler"),
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn rejects_empty_all_and_any() {
assert!(matches!(
Condition::parse_cfg("cfg(all())").unwrap_err(),
ConditionParseError::EmptyAll
));
assert!(matches!(
Condition::parse_cfg("cfg(any())").unwrap_err(),
ConditionParseError::EmptyAny
));
}
#[test]
fn rejects_not_with_arity_other_than_one() {
let err = Condition::parse_cfg(r#"cfg(not(os = "linux", arch = "x86_64"))"#).unwrap_err();
assert!(matches!(err, ConditionParseError::NotArity(2)));
}
#[test]
fn rejects_missing_cfg_prefix() {
assert!(matches!(
Condition::parse_cfg(r#"os = "linux""#).unwrap_err(),
ConditionParseError::ExpectedCfgPrefix(_)
));
}
#[test]
fn rejects_unbalanced_parens() {
assert!(matches!(
Condition::parse_cfg("cfg(os = \"linux\"").unwrap_err(),
ConditionParseError::UnbalancedParens(_)
));
}
#[test]
fn evaluates_simple_key_value() {
let linux = linux_x86_64();
let macos = macos_aarch64();
let cond = Condition::parse_cfg(r#"cfg(os = "linux")"#).unwrap();
assert!(cond.evaluate(&linux));
assert!(!cond.evaluate(&macos));
}
#[test]
fn evaluates_all_any_not() {
let linux = linux_x86_64();
let macos = macos_aarch64();
let all = Condition::parse_cfg(r#"cfg(all(os = "linux", arch = "x86_64"))"#).unwrap();
let any = Condition::parse_cfg(r#"cfg(any(os = "macos", os = "linux"))"#).unwrap();
let not = Condition::parse_cfg(r#"cfg(not(os = "windows"))"#).unwrap();
assert!(all.evaluate(&linux));
assert!(!all.evaluate(&macos));
assert!(any.evaluate(&linux));
assert!(any.evaluate(&macos));
assert!(not.evaluate(&linux));
assert!(not.evaluate(&macos));
}
#[test]
fn display_round_trips_through_parse_inner() {
for raw in [
r#"os = "linux""#,
r#"all(os = "linux", arch = "x86_64")"#,
r#"any(os = "macos", os = "linux")"#,
r#"not(os = "windows")"#,
r#"all(any(os = "linux", os = "macos"), not(arch = "wasm32"))"#,
] {
let cond = Condition::parse_inner(raw).unwrap();
let rendered = cond.to_string();
assert_eq!(rendered, raw, "round-trip should be byte-identical");
let again = Condition::parse_inner(&rendered).unwrap();
assert_eq!(cond, again);
}
}
#[test]
fn current_target_platform_is_internally_consistent() {
let p = TargetPlatform::current();
for v in [&p.os, &p.arch, &p.family, &p.env, &p.abi, &p.target] {
assert!(!v.is_empty());
assert!(v.chars().all(|c| !c.is_ascii_uppercase()));
}
}
#[test]
fn deterministic_serialization_for_metadata_round_trip() {
let cond = Condition::parse_cfg(
r#"cfg(all(os = "linux", any(arch = "x86_64", arch = "aarch64")))"#,
)
.unwrap();
let json = serde_json::to_string(&cond).unwrap();
let parsed: Condition = serde_json::from_str(&json).unwrap();
assert_eq!(cond, parsed);
}
}