1use std::fmt;
30use std::str::FromStr;
31
32use serde::{Deserialize, Deserializer, Serialize, Serializer};
33use thiserror::Error;
34
35#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
43pub enum Condition {
44 KeyValue { key: ConditionKey, value: String },
48 All(Vec<Condition>),
51 Any(Vec<Condition>),
54 Not(Box<Condition>),
56}
57
58impl Condition {
59 pub fn parse_cfg(input: &str) -> Result<Self, ConditionParseError> {
69 let trimmed = input.trim();
70 let inner = trimmed
71 .strip_prefix("cfg")
72 .ok_or_else(|| ConditionParseError::ExpectedCfgPrefix(trimmed.to_owned()))?
73 .trim_start();
74 let inner = inner
75 .strip_prefix('(')
76 .ok_or_else(|| ConditionParseError::ExpectedCfgPrefix(trimmed.to_owned()))?;
77 let inner = inner
78 .strip_suffix(')')
79 .ok_or_else(|| ConditionParseError::UnbalancedParens(trimmed.to_owned()))?;
80 Self::parse_inner(inner)
81 }
82
83 pub fn parse_inner(input: &str) -> Result<Self, ConditionParseError> {
93 let mut parser = Parser::new(input);
94 let cond = parser.parse_condition()?;
95 parser.expect_eof()?;
96 Ok(cond)
97 }
98
99 pub fn evaluate(&self, platform: &TargetPlatform) -> bool {
103 match self {
104 Condition::KeyValue { key, value } => key.lookup(platform) == value,
105 Condition::All(items) => items.iter().all(|c| c.evaluate(platform)),
106 Condition::Any(items) => items.iter().any(|c| c.evaluate(platform)),
107 Condition::Not(inner) => !inner.evaluate(platform),
108 }
109 }
110}
111
112impl fmt::Display for Condition {
113 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
116 match self {
117 Condition::KeyValue { key, value } => write!(f, "{} = \"{}\"", key.as_str(), value),
118 Condition::All(items) => {
119 f.write_str("all(")?;
120 write_list(f, items)?;
121 f.write_str(")")
122 }
123 Condition::Any(items) => {
124 f.write_str("any(")?;
125 write_list(f, items)?;
126 f.write_str(")")
127 }
128 Condition::Not(inner) => write!(f, "not({inner})"),
129 }
130 }
131}
132
133fn write_list(f: &mut fmt::Formatter<'_>, items: &[Condition]) -> fmt::Result {
134 for (i, c) in items.iter().enumerate() {
135 if i > 0 {
136 f.write_str(", ")?;
137 }
138 write!(f, "{c}")?;
139 }
140 Ok(())
141}
142
143#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
146pub enum ConditionKey {
147 Os,
149 Arch,
151 Family,
153 Env,
155 Abi,
157 Target,
159}
160
161impl ConditionKey {
162 pub const fn as_str(self) -> &'static str {
163 match self {
164 ConditionKey::Os => "os",
165 ConditionKey::Arch => "arch",
166 ConditionKey::Family => "family",
167 ConditionKey::Env => "env",
168 ConditionKey::Abi => "abi",
169 ConditionKey::Target => "target",
170 }
171 }
172
173 pub const fn all() -> &'static [ConditionKey] {
175 &[
176 ConditionKey::Os,
177 ConditionKey::Arch,
178 ConditionKey::Family,
179 ConditionKey::Env,
180 ConditionKey::Abi,
181 ConditionKey::Target,
182 ]
183 }
184
185 fn lookup(self, platform: &TargetPlatform) -> &str {
186 match self {
187 ConditionKey::Os => platform.os.as_str(),
188 ConditionKey::Arch => platform.arch.as_str(),
189 ConditionKey::Family => platform.family.as_str(),
190 ConditionKey::Env => platform.env.as_str(),
191 ConditionKey::Abi => platform.abi.as_str(),
192 ConditionKey::Target => platform.target.as_str(),
193 }
194 }
195}
196
197impl FromStr for ConditionKey {
198 type Err = ();
199
200 fn from_str(s: &str) -> Result<Self, Self::Err> {
201 match s {
202 "os" => Ok(ConditionKey::Os),
203 "arch" => Ok(ConditionKey::Arch),
204 "family" => Ok(ConditionKey::Family),
205 "env" => Ok(ConditionKey::Env),
206 "abi" => Ok(ConditionKey::Abi),
207 "target" => Ok(ConditionKey::Target),
208 _ => Err(()),
209 }
210 }
211}
212
213#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
218pub struct TargetPlatform {
219 pub os: String,
220 pub arch: String,
221 pub family: String,
222 pub env: String,
223 pub abi: String,
224 pub target: String,
225}
226
227impl TargetPlatform {
228 pub fn current() -> Self {
234 let os = normalize_os(std::env::consts::OS);
235 let arch = normalize_arch(std::env::consts::ARCH);
236 let family = normalize_family(std::env::consts::FAMILY, &os);
237 let env = normalize_env(&os);
238 let abi = "unknown".to_owned();
239 let target = format!("{arch}-{family}-{os}");
240 Self {
241 os,
242 arch,
243 family,
244 env,
245 abi,
246 target,
247 }
248 }
249}
250
251fn normalize_os(raw: &str) -> String {
252 match raw {
253 "linux" | "macos" | "windows" | "freebsd" | "openbsd" | "netbsd" | "dragonfly"
254 | "android" | "ios" => raw.to_owned(),
255 "darwin" => "macos".to_owned(),
257 "" => "unknown".to_owned(),
258 other => other.to_owned(),
259 }
260}
261
262fn normalize_arch(raw: &str) -> String {
263 match raw {
264 "x86_64" | "aarch64" | "arm" | "riscv64" | "wasm32" => raw.to_owned(),
265 "" => "unknown".to_owned(),
266 other => other.to_owned(),
267 }
268}
269
270fn normalize_family(raw: &str, os: &str) -> String {
271 match raw {
272 "unix" | "windows" | "wasm" => raw.to_owned(),
273 _ => match os {
274 "linux" | "macos" | "freebsd" | "openbsd" | "netbsd" | "dragonfly" | "android"
275 | "ios" => "unix".to_owned(),
276 "windows" => "windows".to_owned(),
277 _ => "unknown".to_owned(),
278 },
279 }
280}
281
282fn normalize_env(os: &str) -> String {
283 match os {
289 "linux" => "gnu".to_owned(),
290 "macos" | "ios" => "apple".to_owned(),
291 "windows" => "msvc".to_owned(),
292 _ => "unknown".to_owned(),
293 }
294}
295
296#[derive(Debug, Clone, PartialEq, Eq, Error)]
302pub enum ConditionParseError {
303 #[error("expected a `cfg(...)` expression but found {0:?}")]
304 ExpectedCfgPrefix(String),
305
306 #[error("`cfg(...)` expression has unbalanced parentheses: {0:?}")]
307 UnbalancedParens(String),
308
309 #[error(
310 "unsupported target cfg key {key:?}; supported keys are os, arch, family, env, abi, and target"
311 )]
312 UnsupportedKey { key: String },
313
314 #[error("expected `=` after key {key:?} in cfg expression")]
315 ExpectedEquals { key: String },
316
317 #[error("expected a quoted string value for key {key:?} in cfg expression; got {found:?}")]
318 ExpectedQuotedValue { key: String, found: String },
319
320 #[error("unterminated string literal in cfg expression: {0:?}")]
321 UnterminatedString(String),
322
323 #[error("trailing input after cfg expression: {0:?}")]
324 TrailingInput(String),
325
326 #[error("`all()` requires at least one condition")]
327 EmptyAll,
328
329 #[error("`any()` requires at least one condition")]
330 EmptyAny,
331
332 #[error("`not()` takes exactly one condition; found {0}")]
333 NotArity(usize),
334
335 #[error("expected `(` after {0}")]
336 ExpectedOpenParen(&'static str),
337
338 #[error("expected `)` to close {0}")]
339 ExpectedCloseParen(&'static str),
340
341 #[error("unexpected token in cfg expression: {0:?}")]
342 UnexpectedToken(String),
343
344 #[error("empty cfg expression")]
345 Empty,
346}
347
348struct Parser<'a> {
349 src: &'a str,
350 pos: usize,
351}
352
353impl<'a> Parser<'a> {
354 fn new(src: &'a str) -> Self {
355 Self { src, pos: 0 }
356 }
357
358 fn skip_whitespace(&mut self) {
359 while let Some(c) = self.peek_char() {
360 if c.is_whitespace() {
361 self.pos += c.len_utf8();
362 } else {
363 break;
364 }
365 }
366 }
367
368 fn peek_char(&self) -> Option<char> {
369 self.src[self.pos..].chars().next()
370 }
371
372 fn expect_eof(&mut self) -> Result<(), ConditionParseError> {
373 self.skip_whitespace();
374 if self.pos < self.src.len() {
375 Err(ConditionParseError::TrailingInput(
376 self.src[self.pos..].to_owned(),
377 ))
378 } else {
379 Ok(())
380 }
381 }
382
383 fn parse_condition(&mut self) -> Result<Condition, ConditionParseError> {
384 self.skip_whitespace();
385 if self.pos >= self.src.len() {
386 return Err(ConditionParseError::Empty);
387 }
388 let ident = self.read_ident()?;
391 self.skip_whitespace();
392 match ident.as_str() {
393 "all" => {
394 self.expect_open_paren("all")?;
395 let items = self.parse_condition_list()?;
396 self.expect_close_paren("all")?;
397 if items.is_empty() {
398 return Err(ConditionParseError::EmptyAll);
399 }
400 Ok(Condition::All(items))
401 }
402 "any" => {
403 self.expect_open_paren("any")?;
404 let items = self.parse_condition_list()?;
405 self.expect_close_paren("any")?;
406 if items.is_empty() {
407 return Err(ConditionParseError::EmptyAny);
408 }
409 Ok(Condition::Any(items))
410 }
411 "not" => {
412 self.expect_open_paren("not")?;
413 let items = self.parse_condition_list()?;
414 self.expect_close_paren("not")?;
415 if items.len() != 1 {
416 return Err(ConditionParseError::NotArity(items.len()));
417 }
418 let inner = items.into_iter().next().expect("len==1 above");
419 Ok(Condition::Not(Box::new(inner)))
420 }
421 other => {
422 let key = ConditionKey::from_str(other).map_err(|()| {
423 ConditionParseError::UnsupportedKey {
424 key: other.to_owned(),
425 }
426 })?;
427 self.skip_whitespace();
428 if self.peek_char() != Some('=') {
429 return Err(ConditionParseError::ExpectedEquals {
430 key: other.to_owned(),
431 });
432 }
433 self.pos += 1; self.skip_whitespace();
435 let value = self.read_quoted_string(other)?;
436 Ok(Condition::KeyValue { key, value })
437 }
438 }
439 }
440
441 fn parse_condition_list(&mut self) -> Result<Vec<Condition>, ConditionParseError> {
442 let mut items = Vec::new();
443 self.skip_whitespace();
444 if self.peek_char() == Some(')') {
445 return Ok(items);
446 }
447 loop {
448 let cond = self.parse_condition()?;
449 items.push(cond);
450 self.skip_whitespace();
451 match self.peek_char() {
452 Some(',') => {
453 self.pos += 1;
454 self.skip_whitespace();
455 }
456 _ => break,
457 }
458 }
459 Ok(items)
460 }
461
462 fn expect_open_paren(&mut self, what: &'static str) -> Result<(), ConditionParseError> {
463 self.skip_whitespace();
464 if self.peek_char() == Some('(') {
465 self.pos += 1;
466 Ok(())
467 } else {
468 Err(ConditionParseError::ExpectedOpenParen(what))
469 }
470 }
471
472 fn expect_close_paren(&mut self, what: &'static str) -> Result<(), ConditionParseError> {
473 self.skip_whitespace();
474 if self.peek_char() == Some(')') {
475 self.pos += 1;
476 Ok(())
477 } else {
478 Err(ConditionParseError::ExpectedCloseParen(what))
479 }
480 }
481
482 fn read_ident(&mut self) -> Result<String, ConditionParseError> {
483 let start = self.pos;
484 while let Some(c) = self.peek_char() {
485 if c.is_ascii_alphanumeric() || c == '_' {
486 self.pos += c.len_utf8();
487 } else {
488 break;
489 }
490 }
491 if start == self.pos {
492 return Err(ConditionParseError::UnexpectedToken(
493 self.src[self.pos..].to_owned(),
494 ));
495 }
496 Ok(self.src[start..self.pos].to_owned())
497 }
498
499 fn read_quoted_string(&mut self, key: &str) -> Result<String, ConditionParseError> {
500 if self.peek_char() != Some('"') {
501 let rest_start = self.pos;
505 while let Some(c) = self.peek_char() {
506 if c == ',' || c == ')' || c.is_whitespace() {
507 break;
508 }
509 self.pos += c.len_utf8();
510 }
511 return Err(ConditionParseError::ExpectedQuotedValue {
512 key: key.to_owned(),
513 found: self.src[rest_start..self.pos].to_owned(),
514 });
515 }
516 self.pos += 1;
517 let start = self.pos;
518 while let Some(c) = self.peek_char() {
519 if c == '"' {
520 let value = self.src[start..self.pos].to_owned();
521 self.pos += 1;
522 return Ok(value);
523 }
524 self.pos += c.len_utf8();
525 }
526 Err(ConditionParseError::UnterminatedString(
527 self.src[start..].to_owned(),
528 ))
529 }
530}
531
532impl Serialize for Condition {
538 fn serialize<S: Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
539 s.serialize_str(&self.to_string())
540 }
541}
542
543impl<'de> Deserialize<'de> for Condition {
544 fn deserialize<D: Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
545 let raw = String::deserialize(d)?;
546 Condition::parse_inner(&raw).map_err(serde::de::Error::custom)
547 }
548}
549
550#[cfg(test)]
551mod tests {
552 use super::*;
553
554 fn linux_x86_64() -> TargetPlatform {
555 TargetPlatform {
556 os: "linux".into(),
557 arch: "x86_64".into(),
558 family: "unix".into(),
559 env: "gnu".into(),
560 abi: "unknown".into(),
561 target: "x86_64-unix-linux".into(),
562 }
563 }
564
565 fn macos_aarch64() -> TargetPlatform {
566 TargetPlatform {
567 os: "macos".into(),
568 arch: "aarch64".into(),
569 family: "unix".into(),
570 env: "apple".into(),
571 abi: "unknown".into(),
572 target: "aarch64-unix-macos".into(),
573 }
574 }
575
576 #[test]
577 fn parses_simple_key_value() {
578 let cond = Condition::parse_cfg(r#"cfg(os = "linux")"#).unwrap();
579 assert_eq!(
580 cond,
581 Condition::KeyValue {
582 key: ConditionKey::Os,
583 value: "linux".into()
584 }
585 );
586 }
587
588 #[test]
589 fn parses_each_supported_key() {
590 for (raw, key) in [
591 (r#"cfg(os = "linux")"#, ConditionKey::Os),
592 (r#"cfg(arch = "x86_64")"#, ConditionKey::Arch),
593 (r#"cfg(family = "unix")"#, ConditionKey::Family),
594 (r#"cfg(env = "gnu")"#, ConditionKey::Env),
595 (r#"cfg(abi = "eabi")"#, ConditionKey::Abi),
596 (
597 r#"cfg(target = "x86_64-unknown-linux-gnu")"#,
598 ConditionKey::Target,
599 ),
600 ] {
601 let cond = Condition::parse_cfg(raw).unwrap();
602 match cond {
603 Condition::KeyValue { key: k, .. } => assert_eq!(k, key, "{raw}"),
604 other => panic!("{raw}: expected key/value, got {other:?}"),
605 }
606 }
607 }
608
609 #[test]
610 fn parses_all_any_not() {
611 let all = Condition::parse_cfg(r#"cfg(all(os = "linux", arch = "x86_64"))"#).unwrap();
612 let any = Condition::parse_cfg(r#"cfg(any(os = "macos", os = "linux"))"#).unwrap();
613 let not = Condition::parse_cfg(r#"cfg(not(os = "windows"))"#).unwrap();
614 assert!(matches!(all, Condition::All(ref v) if v.len() == 2));
615 assert!(matches!(any, Condition::Any(ref v) if v.len() == 2));
616 assert!(matches!(not, Condition::Not(_)));
617 }
618
619 #[test]
620 fn rejects_unquoted_value() {
621 let err = Condition::parse_cfg(r"cfg(os = linux)").unwrap_err();
622 match err {
623 ConditionParseError::ExpectedQuotedValue { key, .. } => assert_eq!(key, "os"),
624 other => panic!("unexpected: {other:?}"),
625 }
626 }
627
628 #[test]
629 fn rejects_unsupported_key() {
630 let err = Condition::parse_cfg(r#"cfg(compiler = "clang")"#).unwrap_err();
631 match err {
632 ConditionParseError::UnsupportedKey { key } => assert_eq!(key, "compiler"),
633 other => panic!("unexpected: {other:?}"),
634 }
635 }
636
637 #[test]
638 fn rejects_empty_all_and_any() {
639 assert!(matches!(
640 Condition::parse_cfg("cfg(all())").unwrap_err(),
641 ConditionParseError::EmptyAll
642 ));
643 assert!(matches!(
644 Condition::parse_cfg("cfg(any())").unwrap_err(),
645 ConditionParseError::EmptyAny
646 ));
647 }
648
649 #[test]
650 fn rejects_not_with_arity_other_than_one() {
651 let err = Condition::parse_cfg(r#"cfg(not(os = "linux", arch = "x86_64"))"#).unwrap_err();
652 assert!(matches!(err, ConditionParseError::NotArity(2)));
653 }
654
655 #[test]
656 fn rejects_missing_cfg_prefix() {
657 assert!(matches!(
658 Condition::parse_cfg(r#"os = "linux""#).unwrap_err(),
659 ConditionParseError::ExpectedCfgPrefix(_)
660 ));
661 }
662
663 #[test]
664 fn rejects_unbalanced_parens() {
665 assert!(matches!(
666 Condition::parse_cfg("cfg(os = \"linux\"").unwrap_err(),
667 ConditionParseError::UnbalancedParens(_)
668 ));
669 }
670
671 #[test]
672 fn evaluates_simple_key_value() {
673 let linux = linux_x86_64();
674 let macos = macos_aarch64();
675 let cond = Condition::parse_cfg(r#"cfg(os = "linux")"#).unwrap();
676 assert!(cond.evaluate(&linux));
677 assert!(!cond.evaluate(&macos));
678 }
679
680 #[test]
681 fn evaluates_all_any_not() {
682 let linux = linux_x86_64();
683 let macos = macos_aarch64();
684 let all = Condition::parse_cfg(r#"cfg(all(os = "linux", arch = "x86_64"))"#).unwrap();
685 let any = Condition::parse_cfg(r#"cfg(any(os = "macos", os = "linux"))"#).unwrap();
686 let not = Condition::parse_cfg(r#"cfg(not(os = "windows"))"#).unwrap();
687 assert!(all.evaluate(&linux));
688 assert!(!all.evaluate(&macos));
689 assert!(any.evaluate(&linux));
690 assert!(any.evaluate(&macos));
691 assert!(not.evaluate(&linux));
692 assert!(not.evaluate(&macos));
693 }
694
695 #[test]
696 fn display_round_trips_through_parse_inner() {
697 for raw in [
698 r#"os = "linux""#,
699 r#"all(os = "linux", arch = "x86_64")"#,
700 r#"any(os = "macos", os = "linux")"#,
701 r#"not(os = "windows")"#,
702 r#"all(any(os = "linux", os = "macos"), not(arch = "wasm32"))"#,
703 ] {
704 let cond = Condition::parse_inner(raw).unwrap();
705 let rendered = cond.to_string();
706 assert_eq!(rendered, raw, "round-trip should be byte-identical");
707 let again = Condition::parse_inner(&rendered).unwrap();
708 assert_eq!(cond, again);
709 }
710 }
711
712 #[test]
713 fn current_target_platform_is_internally_consistent() {
714 let p = TargetPlatform::current();
715 for v in [&p.os, &p.arch, &p.family, &p.env, &p.abi, &p.target] {
717 assert!(!v.is_empty());
718 assert!(v.chars().all(|c| !c.is_ascii_uppercase()));
719 }
720 }
721
722 #[test]
723 fn deterministic_serialization_for_metadata_round_trip() {
724 let cond = Condition::parse_cfg(
725 r#"cfg(all(os = "linux", any(arch = "x86_64", arch = "aarch64")))"#,
726 )
727 .unwrap();
728 let json = serde_json::to_string(&cond).unwrap();
729 let parsed: Condition = serde_json::from_str(&json).unwrap();
730 assert_eq!(cond, parsed);
731 }
732}