1use std::collections::BTreeSet;
30use std::fmt;
31use std::str::FromStr;
32
33use serde::{Deserialize, Deserializer, Serialize, Serializer};
34use thiserror::Error;
35
36#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
44pub enum Condition {
45 KeyValue { key: ConditionKey, value: String },
49 Feature(String),
58 All(Vec<Condition>),
61 Any(Vec<Condition>),
64 Not(Box<Condition>),
66}
67
68impl Condition {
69 pub fn parse_cfg(input: &str) -> Result<Self, ConditionParseError> {
79 let trimmed = input.trim();
80 let inner = trimmed
81 .strip_prefix("cfg")
82 .ok_or_else(|| ConditionParseError::ExpectedCfgPrefix(trimmed.to_owned()))?
83 .trim_start();
84 let inner = inner
85 .strip_prefix('(')
86 .ok_or_else(|| ConditionParseError::ExpectedCfgPrefix(trimmed.to_owned()))?;
87 let inner = inner
88 .strip_suffix(')')
89 .ok_or_else(|| ConditionParseError::UnbalancedParens(trimmed.to_owned()))?;
90 Self::parse_inner(inner)
91 }
92
93 pub fn parse_inner(input: &str) -> Result<Self, ConditionParseError> {
103 let mut parser = Parser::new(input);
104 let cond = parser.parse_condition()?;
105 parser.expect_eof()?;
106 Ok(cond)
107 }
108
109 pub fn evaluate(&self, platform: &TargetPlatform, features: &BTreeSet<String>) -> bool {
121 match self {
122 Condition::KeyValue { key, value } => key.lookup(platform) == value,
123 Condition::Feature(name) => features.contains(name),
124 Condition::All(items) => items.iter().all(|c| c.evaluate(platform, features)),
125 Condition::Any(items) => items.iter().any(|c| c.evaluate(platform, features)),
126 Condition::Not(inner) => !inner.evaluate(platform, features),
127 }
128 }
129
130 pub fn references_feature(&self) -> bool {
135 match self {
136 Condition::Feature(_) => true,
137 Condition::KeyValue { .. } => false,
138 Condition::All(items) | Condition::Any(items) => {
139 items.iter().any(Condition::references_feature)
140 }
141 Condition::Not(inner) => inner.references_feature(),
142 }
143 }
144}
145
146impl fmt::Display for Condition {
147 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
150 match self {
151 Condition::KeyValue { key, value } => write!(f, "{} = \"{}\"", key.as_str(), value),
152 Condition::Feature(name) => write!(f, "feature = \"{name}\""),
153 Condition::All(items) => {
154 f.write_str("all(")?;
155 write_list(f, items)?;
156 f.write_str(")")
157 }
158 Condition::Any(items) => {
159 f.write_str("any(")?;
160 write_list(f, items)?;
161 f.write_str(")")
162 }
163 Condition::Not(inner) => write!(f, "not({inner})"),
164 }
165 }
166}
167
168fn write_list(f: &mut fmt::Formatter<'_>, items: &[Condition]) -> fmt::Result {
169 for (i, c) in items.iter().enumerate() {
170 if i > 0 {
171 f.write_str(", ")?;
172 }
173 write!(f, "{c}")?;
174 }
175 Ok(())
176}
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
181pub enum ConditionKey {
182 Os,
184 Arch,
186 Family,
188 Env,
190 Abi,
192 Target,
194}
195
196impl ConditionKey {
197 pub const fn as_str(self) -> &'static str {
198 match self {
199 ConditionKey::Os => "os",
200 ConditionKey::Arch => "arch",
201 ConditionKey::Family => "family",
202 ConditionKey::Env => "env",
203 ConditionKey::Abi => "abi",
204 ConditionKey::Target => "target",
205 }
206 }
207
208 pub const fn all() -> &'static [ConditionKey] {
210 &[
211 ConditionKey::Os,
212 ConditionKey::Arch,
213 ConditionKey::Family,
214 ConditionKey::Env,
215 ConditionKey::Abi,
216 ConditionKey::Target,
217 ]
218 }
219
220 fn lookup(self, platform: &TargetPlatform) -> &str {
221 match self {
222 ConditionKey::Os => platform.os.as_str(),
223 ConditionKey::Arch => platform.arch.as_str(),
224 ConditionKey::Family => platform.family.as_str(),
225 ConditionKey::Env => platform.env.as_str(),
226 ConditionKey::Abi => platform.abi.as_str(),
227 ConditionKey::Target => platform.target.as_str(),
228 }
229 }
230}
231
232impl FromStr for ConditionKey {
233 type Err = ();
234
235 fn from_str(s: &str) -> Result<Self, Self::Err> {
236 match s {
237 "os" => Ok(ConditionKey::Os),
238 "arch" => Ok(ConditionKey::Arch),
239 "family" => Ok(ConditionKey::Family),
240 "env" => Ok(ConditionKey::Env),
241 "abi" => Ok(ConditionKey::Abi),
242 "target" => Ok(ConditionKey::Target),
243 _ => Err(()),
244 }
245 }
246}
247
248#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
253pub struct TargetPlatform {
254 pub os: String,
255 pub arch: String,
256 pub family: String,
257 pub env: String,
258 pub abi: String,
259 pub target: String,
260}
261
262impl TargetPlatform {
263 pub fn current() -> Self {
269 let os = normalize_os(std::env::consts::OS);
270 let arch = normalize_arch(std::env::consts::ARCH);
271 let family = normalize_family(std::env::consts::FAMILY, &os);
272 let env = normalize_env(&os);
273 let abi = "unknown".to_owned();
274 let target = format!("{arch}-{family}-{os}");
275 Self {
276 os,
277 arch,
278 family,
279 env,
280 abi,
281 target,
282 }
283 }
284}
285
286fn normalize_os(raw: &str) -> String {
287 match raw {
288 "linux" | "macos" | "windows" | "freebsd" | "openbsd" | "netbsd" | "dragonfly"
289 | "android" | "ios" => raw.to_owned(),
290 "darwin" => "macos".to_owned(),
292 "" => "unknown".to_owned(),
293 other => other.to_owned(),
294 }
295}
296
297fn normalize_arch(raw: &str) -> String {
298 match raw {
299 "x86_64" | "aarch64" | "arm" | "riscv64" | "wasm32" => raw.to_owned(),
300 "" => "unknown".to_owned(),
301 other => other.to_owned(),
302 }
303}
304
305fn normalize_family(raw: &str, os: &str) -> String {
306 match raw {
307 "unix" | "windows" | "wasm" => raw.to_owned(),
308 _ => match os {
309 "linux" | "macos" | "freebsd" | "openbsd" | "netbsd" | "dragonfly" | "android"
310 | "ios" => "unix".to_owned(),
311 "windows" => "windows".to_owned(),
312 _ => "unknown".to_owned(),
313 },
314 }
315}
316
317fn normalize_env(os: &str) -> String {
318 match os {
324 "linux" => "gnu".to_owned(),
325 "macos" | "ios" => "apple".to_owned(),
326 "windows" => "msvc".to_owned(),
327 _ => "unknown".to_owned(),
328 }
329}
330
331#[derive(Debug, Clone, PartialEq, Eq, Error)]
337pub enum ConditionParseError {
338 #[error("expected a `cfg(...)` expression but found {0:?}")]
339 ExpectedCfgPrefix(String),
340
341 #[error("`cfg(...)` expression has unbalanced parentheses: {0:?}")]
342 UnbalancedParens(String),
343
344 #[error(
345 "unsupported target cfg key {key:?}; supported keys are os, arch, family, env, abi, and target"
346 )]
347 UnsupportedKey { key: String },
348
349 #[error("expected `=` after key {key:?} in cfg expression")]
350 ExpectedEquals { key: String },
351
352 #[error("expected a quoted string value for key {key:?} in cfg expression; got {found:?}")]
353 ExpectedQuotedValue { key: String, found: String },
354
355 #[error("unterminated string literal in cfg expression: {0:?}")]
356 UnterminatedString(String),
357
358 #[error("trailing input after cfg expression: {0:?}")]
359 TrailingInput(String),
360
361 #[error("`all()` requires at least one condition")]
362 EmptyAll,
363
364 #[error("`any()` requires at least one condition")]
365 EmptyAny,
366
367 #[error("`not()` takes exactly one condition; found {0}")]
368 NotArity(usize),
369
370 #[error("expected `(` after {0}")]
371 ExpectedOpenParen(&'static str),
372
373 #[error("expected `)` to close {0}")]
374 ExpectedCloseParen(&'static str),
375
376 #[error("unexpected token in cfg expression: {0:?}")]
377 UnexpectedToken(String),
378
379 #[error("empty cfg expression")]
380 Empty,
381}
382
383struct Parser<'a> {
384 src: &'a str,
385 pos: usize,
386}
387
388impl<'a> Parser<'a> {
389 fn new(src: &'a str) -> Self {
390 Self { src, pos: 0 }
391 }
392
393 fn skip_whitespace(&mut self) {
394 while let Some(c) = self.peek_char() {
395 if c.is_whitespace() {
396 self.pos += c.len_utf8();
397 } else {
398 break;
399 }
400 }
401 }
402
403 fn peek_char(&self) -> Option<char> {
404 self.src[self.pos..].chars().next()
405 }
406
407 fn expect_eof(&mut self) -> Result<(), ConditionParseError> {
408 self.skip_whitespace();
409 if self.pos < self.src.len() {
410 Err(ConditionParseError::TrailingInput(
411 self.src[self.pos..].to_owned(),
412 ))
413 } else {
414 Ok(())
415 }
416 }
417
418 fn parse_condition(&mut self) -> Result<Condition, ConditionParseError> {
419 self.skip_whitespace();
420 if self.pos >= self.src.len() {
421 return Err(ConditionParseError::Empty);
422 }
423 let ident = self.read_ident()?;
426 self.skip_whitespace();
427 match ident.as_str() {
428 "all" => {
429 self.expect_open_paren("all")?;
430 let items = self.parse_condition_list()?;
431 self.expect_close_paren("all")?;
432 if items.is_empty() {
433 return Err(ConditionParseError::EmptyAll);
434 }
435 Ok(Condition::All(items))
436 }
437 "any" => {
438 self.expect_open_paren("any")?;
439 let items = self.parse_condition_list()?;
440 self.expect_close_paren("any")?;
441 if items.is_empty() {
442 return Err(ConditionParseError::EmptyAny);
443 }
444 Ok(Condition::Any(items))
445 }
446 "not" => {
447 self.expect_open_paren("not")?;
448 let items = self.parse_condition_list()?;
449 self.expect_close_paren("not")?;
450 if items.len() != 1 {
451 return Err(ConditionParseError::NotArity(items.len()));
452 }
453 let inner = items.into_iter().next().expect("len==1 above");
454 Ok(Condition::Not(Box::new(inner)))
455 }
456 other => {
457 self.skip_whitespace();
461 if self.peek_char() != Some('=') {
462 return Err(ConditionParseError::ExpectedEquals {
463 key: other.to_owned(),
464 });
465 }
466 self.pos += 1; self.skip_whitespace();
468 let value = self.read_quoted_string(other)?;
469 if other == "feature" {
470 Ok(Condition::Feature(value))
471 } else {
472 let key = ConditionKey::from_str(other).map_err(|()| {
473 ConditionParseError::UnsupportedKey {
474 key: other.to_owned(),
475 }
476 })?;
477 Ok(Condition::KeyValue { key, value })
478 }
479 }
480 }
481 }
482
483 fn parse_condition_list(&mut self) -> Result<Vec<Condition>, ConditionParseError> {
484 let mut items = Vec::new();
485 self.skip_whitespace();
486 if self.peek_char() == Some(')') {
487 return Ok(items);
488 }
489 loop {
490 let cond = self.parse_condition()?;
491 items.push(cond);
492 self.skip_whitespace();
493 match self.peek_char() {
494 Some(',') => {
495 self.pos += 1;
496 self.skip_whitespace();
497 }
498 _ => break,
499 }
500 }
501 Ok(items)
502 }
503
504 fn expect_open_paren(&mut self, what: &'static str) -> Result<(), ConditionParseError> {
505 self.skip_whitespace();
506 if self.peek_char() == Some('(') {
507 self.pos += 1;
508 Ok(())
509 } else {
510 Err(ConditionParseError::ExpectedOpenParen(what))
511 }
512 }
513
514 fn expect_close_paren(&mut self, what: &'static str) -> Result<(), ConditionParseError> {
515 self.skip_whitespace();
516 if self.peek_char() == Some(')') {
517 self.pos += 1;
518 Ok(())
519 } else {
520 Err(ConditionParseError::ExpectedCloseParen(what))
521 }
522 }
523
524 fn read_ident(&mut self) -> Result<String, ConditionParseError> {
525 let start = self.pos;
526 while let Some(c) = self.peek_char() {
527 if c.is_ascii_alphanumeric() || c == '_' {
528 self.pos += c.len_utf8();
529 } else {
530 break;
531 }
532 }
533 if start == self.pos {
534 return Err(ConditionParseError::UnexpectedToken(
535 self.src[self.pos..].to_owned(),
536 ));
537 }
538 Ok(self.src[start..self.pos].to_owned())
539 }
540
541 fn read_quoted_string(&mut self, key: &str) -> Result<String, ConditionParseError> {
542 if self.peek_char() != Some('"') {
543 let rest_start = self.pos;
547 while let Some(c) = self.peek_char() {
548 if c == ',' || c == ')' || c.is_whitespace() {
549 break;
550 }
551 self.pos += c.len_utf8();
552 }
553 return Err(ConditionParseError::ExpectedQuotedValue {
554 key: key.to_owned(),
555 found: self.src[rest_start..self.pos].to_owned(),
556 });
557 }
558 self.pos += 1;
559 let start = self.pos;
560 while let Some(c) = self.peek_char() {
561 if c == '"' {
562 let value = self.src[start..self.pos].to_owned();
563 self.pos += 1;
564 return Ok(value);
565 }
566 self.pos += c.len_utf8();
567 }
568 Err(ConditionParseError::UnterminatedString(
569 self.src[start..].to_owned(),
570 ))
571 }
572}
573
574impl Serialize for Condition {
580 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
581 s.serialize_str(&self.to_string())
582 }
583}
584
585impl<'de> Deserialize<'de> for Condition {
586 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
587 let raw = String::deserialize(d)?;
588 Condition::parse_inner(&raw).map_err(serde::de::Error::custom)
589 }
590}
591
592#[cfg(test)]
593mod tests {
594 use super::*;
595
596 fn linux_x86_64() -> TargetPlatform {
597 TargetPlatform {
598 os: "linux".into(),
599 arch: "x86_64".into(),
600 family: "unix".into(),
601 env: "gnu".into(),
602 abi: "unknown".into(),
603 target: "x86_64-unix-linux".into(),
604 }
605 }
606
607 fn macos_aarch64() -> TargetPlatform {
608 TargetPlatform {
609 os: "macos".into(),
610 arch: "aarch64".into(),
611 family: "unix".into(),
612 env: "apple".into(),
613 abi: "unknown".into(),
614 target: "aarch64-unix-macos".into(),
615 }
616 }
617
618 #[test]
619 fn parses_simple_key_value() {
620 let cond = Condition::parse_cfg(r#"cfg(os = "linux")"#).unwrap();
621 assert_eq!(
622 cond,
623 Condition::KeyValue {
624 key: ConditionKey::Os,
625 value: "linux".into()
626 }
627 );
628 }
629
630 #[test]
631 fn parses_each_supported_key() {
632 for (raw, key) in [
633 (r#"cfg(os = "linux")"#, ConditionKey::Os),
634 (r#"cfg(arch = "x86_64")"#, ConditionKey::Arch),
635 (r#"cfg(family = "unix")"#, ConditionKey::Family),
636 (r#"cfg(env = "gnu")"#, ConditionKey::Env),
637 (r#"cfg(abi = "eabi")"#, ConditionKey::Abi),
638 (
639 r#"cfg(target = "x86_64-unknown-linux-gnu")"#,
640 ConditionKey::Target,
641 ),
642 ] {
643 let cond = Condition::parse_cfg(raw).unwrap();
644 match cond {
645 Condition::KeyValue { key: k, .. } => assert_eq!(k, key, "{raw}"),
646 other => panic!("{raw}: expected key/value, got {other:?}"),
647 }
648 }
649 }
650
651 #[test]
652 fn parses_all_any_not() {
653 let all = Condition::parse_cfg(r#"cfg(all(os = "linux", arch = "x86_64"))"#).unwrap();
654 let any = Condition::parse_cfg(r#"cfg(any(os = "macos", os = "linux"))"#).unwrap();
655 let not = Condition::parse_cfg(r#"cfg(not(os = "windows"))"#).unwrap();
656 assert!(matches!(all, Condition::All(ref v) if v.len() == 2));
657 assert!(matches!(any, Condition::Any(ref v) if v.len() == 2));
658 assert!(matches!(not, Condition::Not(_)));
659 }
660
661 #[test]
662 fn rejects_unquoted_value() {
663 let err = Condition::parse_cfg(r"cfg(os = linux)").unwrap_err();
664 match err {
665 ConditionParseError::ExpectedQuotedValue { key, .. } => assert_eq!(key, "os"),
666 other => panic!("unexpected: {other:?}"),
667 }
668 }
669
670 #[test]
671 fn rejects_unsupported_key() {
672 let err = Condition::parse_cfg(r#"cfg(compiler = "clang")"#).unwrap_err();
673 match err {
674 ConditionParseError::UnsupportedKey { key } => assert_eq!(key, "compiler"),
675 other => panic!("unexpected: {other:?}"),
676 }
677 }
678
679 #[test]
680 fn rejects_empty_all_and_any() {
681 assert!(matches!(
682 Condition::parse_cfg("cfg(all())").unwrap_err(),
683 ConditionParseError::EmptyAll
684 ));
685 assert!(matches!(
686 Condition::parse_cfg("cfg(any())").unwrap_err(),
687 ConditionParseError::EmptyAny
688 ));
689 }
690
691 #[test]
692 fn rejects_not_with_arity_other_than_one() {
693 let err = Condition::parse_cfg(r#"cfg(not(os = "linux", arch = "x86_64"))"#).unwrap_err();
694 assert!(matches!(err, ConditionParseError::NotArity(2)));
695 }
696
697 #[test]
698 fn rejects_missing_cfg_prefix() {
699 assert!(matches!(
700 Condition::parse_cfg(r#"os = "linux""#).unwrap_err(),
701 ConditionParseError::ExpectedCfgPrefix(_)
702 ));
703 }
704
705 #[test]
706 fn rejects_unbalanced_parens() {
707 assert!(matches!(
708 Condition::parse_cfg("cfg(os = \"linux\"").unwrap_err(),
709 ConditionParseError::UnbalancedParens(_)
710 ));
711 }
712
713 #[test]
714 fn evaluates_simple_key_value() {
715 let linux = linux_x86_64();
716 let macos = macos_aarch64();
717 let cond = Condition::parse_cfg(r#"cfg(os = "linux")"#).unwrap();
718 assert!(cond.evaluate(&linux, &BTreeSet::new()));
719 assert!(!cond.evaluate(&macos, &BTreeSet::new()));
720 }
721
722 #[test]
723 fn evaluates_all_any_not() {
724 let linux = linux_x86_64();
725 let macos = macos_aarch64();
726 let all = Condition::parse_cfg(r#"cfg(all(os = "linux", arch = "x86_64"))"#).unwrap();
727 let any = Condition::parse_cfg(r#"cfg(any(os = "macos", os = "linux"))"#).unwrap();
728 let not = Condition::parse_cfg(r#"cfg(not(os = "windows"))"#).unwrap();
729 assert!(all.evaluate(&linux, &BTreeSet::new()));
730 assert!(!all.evaluate(&macos, &BTreeSet::new()));
731 assert!(any.evaluate(&linux, &BTreeSet::new()));
732 assert!(any.evaluate(&macos, &BTreeSet::new()));
733 assert!(not.evaluate(&linux, &BTreeSet::new()));
734 assert!(not.evaluate(&macos, &BTreeSet::new()));
735 }
736
737 #[test]
738 fn parses_and_evaluates_feature_leaf() {
739 let linux = linux_x86_64();
740 let cond = Condition::parse_cfg(r#"cfg(feature = "simd")"#).unwrap();
741 assert_eq!(cond, Condition::Feature("simd".to_owned()));
742 assert!(cond.references_feature());
743 let enabled: BTreeSet<String> = BTreeSet::from(["simd".to_owned()]);
744 assert!(cond.evaluate(&linux, &enabled));
745 assert!(!cond.evaluate(&linux, &BTreeSet::new()));
746 }
747
748 #[test]
749 fn evaluates_feature_combined_with_platform() {
750 let linux = linux_x86_64();
751 let macos = macos_aarch64();
752 let cond = Condition::parse_cfg(r#"cfg(all(feature = "simd", arch = "x86_64"))"#).unwrap();
753 assert!(cond.references_feature());
754 let enabled: BTreeSet<String> = BTreeSet::from(["simd".to_owned()]);
755 assert!(cond.evaluate(&linux, &enabled));
757 assert!(!cond.evaluate(&macos, &enabled)); assert!(!cond.evaluate(&linux, &BTreeSet::new())); }
760
761 #[test]
762 fn references_feature_is_false_for_platform_only_conditions() {
763 for raw in [
764 r#"cfg(os = "linux")"#,
765 r#"cfg(all(os = "linux", arch = "x86_64"))"#,
766 r#"cfg(not(os = "windows"))"#,
767 ] {
768 assert!(!Condition::parse_cfg(raw).unwrap().references_feature());
769 }
770 }
771
772 #[test]
773 fn display_round_trips_through_parse_inner() {
774 for raw in [
775 r#"os = "linux""#,
776 r#"feature = "simd""#,
777 r#"all(feature = "simd", arch = "x86_64")"#,
778 r#"all(os = "linux", arch = "x86_64")"#,
779 r#"any(os = "macos", os = "linux")"#,
780 r#"not(os = "windows")"#,
781 r#"all(any(os = "linux", os = "macos"), not(arch = "wasm32"))"#,
782 ] {
783 let cond = Condition::parse_inner(raw).unwrap();
784 let rendered = cond.to_string();
785 assert_eq!(rendered, raw, "round-trip should be byte-identical");
786 let again = Condition::parse_inner(&rendered).unwrap();
787 assert_eq!(cond, again);
788 }
789 }
790
791 #[test]
792 fn current_target_platform_is_internally_consistent() {
793 let p = TargetPlatform::current();
794 for v in [&p.os, &p.arch, &p.family, &p.env, &p.abi, &p.target] {
796 assert!(!v.is_empty());
797 assert!(v.chars().all(|c| !c.is_ascii_uppercase()));
798 }
799 }
800
801 #[test]
802 fn deterministic_serialization_for_metadata_round_trip() {
803 let cond = Condition::parse_cfg(
804 r#"cfg(all(os = "linux", any(arch = "x86_64", arch = "aarch64")))"#,
805 )
806 .unwrap();
807 let json = serde_json::to_string(&cond).unwrap();
808 let parsed: Condition = serde_json::from_str(&json).unwrap();
809 assert_eq!(cond, parsed);
810 }
811}