1use itertools::Itertools;
2
3use miette::Diagnostic;
4use serde::{
5 de::{self, DeserializeOwned, IntoDeserializer, Visitor},
6 Deserialize, Deserializer,
7};
8use serde_enum_str::{Deserialize_enum_str, Serialize_enum_str};
9use std::{cmp::Ordering, collections::HashMap};
10use strum::IntoEnumIterator;
11use strum_macros::EnumIter;
12use thiserror::Error;
13
14use super::{normalize_lua_value, DisplayAsLuaKV, DisplayLuaKV, DisplayLuaValue, LuaValueSeed};
15
16#[derive(Deserialize_enum_str, Serialize_enum_str, PartialEq, Eq, Hash, Debug, Clone, EnumIter)]
19#[serde(rename_all = "lowercase")]
20#[strum(serialize_all = "lowercase")]
21pub enum PlatformIdentifier {
22 Unix,
24 Windows,
25 Win32,
26 Cygwin,
27 MacOSX,
28 Linux,
29 FreeBSD,
30 #[serde(other)]
31 Unknown(String),
32}
33
34impl Default for PlatformIdentifier {
35 fn default() -> Self {
36 target_identifier()
37 }
38}
39
40impl PartialOrd for PlatformIdentifier {
42 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
43 match (self, other) {
44 (PlatformIdentifier::Unix, PlatformIdentifier::Cygwin) => Some(Ordering::Less),
45 (PlatformIdentifier::Unix, PlatformIdentifier::MacOSX) => Some(Ordering::Less),
46 (PlatformIdentifier::Unix, PlatformIdentifier::Linux) => Some(Ordering::Less),
47 (PlatformIdentifier::Unix, PlatformIdentifier::FreeBSD) => Some(Ordering::Less),
48 (PlatformIdentifier::Windows, PlatformIdentifier::Win32) => Some(Ordering::Greater),
49 (PlatformIdentifier::Win32, PlatformIdentifier::Windows) => Some(Ordering::Less),
50 (PlatformIdentifier::Cygwin, PlatformIdentifier::Unix) => Some(Ordering::Greater),
51 (PlatformIdentifier::MacOSX, PlatformIdentifier::Unix) => Some(Ordering::Greater),
52 (PlatformIdentifier::Linux, PlatformIdentifier::Unix) => Some(Ordering::Greater),
53 (PlatformIdentifier::FreeBSD, PlatformIdentifier::Unix) => Some(Ordering::Greater),
54 _ if self == other => Some(Ordering::Equal),
55 _ => None,
56 }
57 }
58}
59
60fn target_identifier() -> PlatformIdentifier {
67 if cfg!(target_env = "msvc") {
68 PlatformIdentifier::Windows
69 } else if cfg!(target_os = "linux") {
70 PlatformIdentifier::Linux
71 } else if cfg!(target_os = "macos") || cfg!(target_vendor = "apple") {
72 PlatformIdentifier::MacOSX
73 } else if cfg!(target_os = "freebsd") {
74 PlatformIdentifier::FreeBSD
75 } else if which::which("cygpath").is_ok() {
76 PlatformIdentifier::Cygwin
77 } else {
78 PlatformIdentifier::Unix
79 }
80}
81
82impl PlatformIdentifier {
83 pub fn get_subsets(&self) -> Vec<Self> {
86 PlatformIdentifier::iter()
87 .filter(|identifier| identifier.is_subset_of(self))
88 .collect()
89 }
90
91 pub fn get_extended_platforms(&self) -> Vec<Self> {
94 PlatformIdentifier::iter()
95 .filter(|identifier| identifier.is_extension_of(self))
96 .collect()
97 }
98
99 fn is_subset_of(&self, other: &PlatformIdentifier) -> bool {
101 self.partial_cmp(other) == Some(Ordering::Less)
102 }
103
104 fn is_extension_of(&self, other: &PlatformIdentifier) -> bool {
106 self.partial_cmp(other) == Some(Ordering::Greater)
107 }
108}
109
110#[derive(Clone, Debug, PartialEq)]
112pub struct PlatformSupport {
113 platform_map: HashMap<PlatformIdentifier, bool>,
115}
116
117impl Default for PlatformSupport {
118 fn default() -> Self {
119 Self {
120 platform_map: PlatformIdentifier::iter()
121 .filter(|identifier| !matches!(identifier, PlatformIdentifier::Unknown(_)))
122 .map(|identifier| (identifier, true))
123 .collect(),
124 }
125 }
126}
127
128impl<'de> Deserialize<'de> for PlatformSupport {
129 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
130 where
131 D: Deserializer<'de>,
132 {
133 let platforms: Vec<String> = Vec::deserialize(deserializer)?;
134 Self::parse(&platforms).map_err(de::Error::custom)
135 }
136}
137
138impl DisplayAsLuaKV for PlatformSupport {
139 fn display_lua(&self) -> DisplayLuaKV {
140 DisplayLuaKV {
141 key: "supported_platforms".to_string(),
142 value: DisplayLuaValue::List(
143 self.platforms()
144 .iter()
145 .map(|(platform, supported)| {
146 DisplayLuaValue::String(format!(
147 "{}{}",
148 if *supported { "" } else { "!" },
149 platform,
150 ))
151 })
152 .collect(),
153 ),
154 }
155 }
156}
157
158#[derive(Error, Debug, Diagnostic)]
159#[non_exhaustive]
160pub enum PlatformValidationError {
161 #[error("error when parsing platform identifier: {0}")]
162 ParseError(String),
163
164 #[error("conflicting supported platform entries")]
165 ConflictingEntries,
166}
167
168impl PlatformSupport {
169 fn validate_platforms(
170 platforms: &[String],
171 ) -> Result<HashMap<PlatformIdentifier, bool>, PlatformValidationError> {
172 platforms
173 .iter()
174 .try_fold(HashMap::new(), |mut platforms, platform| {
175 let (is_positive_assertion, platform) = platform
179 .strip_prefix('!')
180 .map(|str| (false, str))
181 .unwrap_or((true, platform));
182
183 let platform_identifier = platform
184 .parse::<PlatformIdentifier>()
185 .map_err(|err| PlatformValidationError::ParseError(err.to_string()))?;
186
187 if platforms
191 .get(&platform_identifier)
192 .unwrap_or(&is_positive_assertion)
193 != &is_positive_assertion
194 {
195 return Err(PlatformValidationError::ConflictingEntries);
196 }
197
198 platforms.insert(platform_identifier.clone(), is_positive_assertion);
199
200 let subset_or_extended_platforms = if is_positive_assertion {
201 platform_identifier.get_extended_platforms()
202 } else {
203 platform_identifier.get_subsets()
204 };
205
206 for sub_platform in subset_or_extended_platforms {
207 if platforms
208 .get(&sub_platform)
209 .unwrap_or(&is_positive_assertion)
210 != &is_positive_assertion
211 {
212 return Err(PlatformValidationError::ConflictingEntries);
214 }
215
216 platforms.insert(sub_platform, is_positive_assertion);
217 }
218
219 Ok(platforms)
220 })
221 }
222
223 pub fn parse(platforms: &[String]) -> Result<Self, PlatformValidationError> {
224 match platforms {
228 [] => Ok(Self::default()),
229 platforms if platforms.iter().any(|platform| platform.starts_with('!')) => {
230 let mut platform_map = Self::validate_platforms(platforms)?;
231
232 for identifier in PlatformIdentifier::iter() {
235 if !matches!(identifier, PlatformIdentifier::Unknown(_)) {
236 platform_map.entry(identifier).or_insert(true);
237 }
238 }
239
240 Ok(Self { platform_map })
241 }
242 platforms => Ok(Self {
244 platform_map: Self::validate_platforms(platforms)?,
245 }),
246 }
247 }
248
249 pub fn is_supported(&self, platform: &PlatformIdentifier) -> bool {
250 self.platform_map.get(platform).cloned().unwrap_or(false)
251 }
252
253 pub(crate) fn platforms(&self) -> &HashMap<PlatformIdentifier, bool> {
254 &self.platform_map
255 }
256}
257
258pub trait PartialOverride: Sized {
259 type Err: std::error::Error;
260
261 fn apply_overrides(&self, override_val: &Self) -> Result<Self, Self::Err>;
262}
263
264pub trait PlatformOverridable: PartialOverride {
265 type Err: std::error::Error;
266
267 fn on_nil<T>() -> Result<PerPlatform<T>, <Self as PlatformOverridable>::Err>
268 where
269 T: PlatformOverridable,
270 T: Default;
271}
272
273#[derive(Clone, Debug, PartialEq)]
275pub struct PerPlatform<T> {
276 pub(crate) default: T,
278 pub(crate) per_platform: HashMap<PlatformIdentifier, T>,
280}
281
282impl<T> PerPlatform<T> {
283 pub(crate) fn new(default: T) -> Self {
284 Self {
285 default,
286 per_platform: HashMap::default(),
287 }
288 }
289
290 pub fn current_platform(&self) -> &T {
293 self.for_platform_identifier(&target_identifier())
294 }
295
296 fn for_platform_identifier(&self, identifier: &PlatformIdentifier) -> &T {
297 self.get(identifier)
298 }
299
300 pub fn get(&self, platform: &PlatformIdentifier) -> &T {
301 self.per_platform.get(platform).unwrap_or(
302 platform
303 .get_subsets()
304 .into_iter()
305 .sorted_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal))
309 .find(|identifier| self.per_platform.contains_key(identifier))
310 .and_then(|identifier| self.per_platform.get(&identifier))
311 .unwrap_or(&self.default),
312 )
313 }
314
315 pub(crate) fn map<U, F>(&self, cb: F) -> PerPlatform<U>
316 where
317 F: Fn(&T) -> U,
318 {
319 PerPlatform {
320 default: cb(&self.default),
321 per_platform: self
322 .per_platform
323 .iter()
324 .map(|(identifier, value)| (identifier.clone(), cb(value)))
325 .collect(),
326 }
327 }
328}
329
330impl<U, E> PerPlatform<Result<U, E>>
331where
332 E: std::error::Error,
333{
334 pub fn transpose(self) -> Result<PerPlatform<U>, E> {
335 Ok(PerPlatform {
336 default: self.default?,
337 per_platform: self
338 .per_platform
339 .into_iter()
340 .map(|(identifier, value)| Ok((identifier, value?)))
341 .try_collect()?,
342 })
343 }
344}
345
346impl<T: Default> Default for PerPlatform<T> {
347 fn default() -> Self {
348 Self {
349 default: T::default(),
350 per_platform: HashMap::default(),
351 }
352 }
353}
354
355struct PerPlatformVisitor<T>(std::marker::PhantomData<T>);
356
357impl<'de, T> Visitor<'de> for PerPlatformVisitor<T>
358where
359 T: DeserializeOwned,
360 T: PlatformOverridable,
361 T: Default,
362 T: Clone,
363{
364 type Value = PerPlatform<T>;
365
366 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
367 formatter.write_str("a table or nil")
368 }
369
370 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
371 where
372 A: de::MapAccess<'de>,
373 {
374 use serde_value::Value;
375
376 let mut platforms_val: Option<Value> = None;
377 let mut other_entries: Vec<(Value, Value)> = Vec::new();
378
379 while let Some(key) = map.next_key_seed(LuaValueSeed)? {
380 if key == Value::String("platforms".to_string()) {
381 platforms_val = Some(map.next_value_seed(LuaValueSeed)?);
382 } else {
383 other_entries.push((key, map.next_value_seed(LuaValueSeed)?));
384 }
385 }
386
387 let mut per_platform = match platforms_val {
388 Some(val) => match val {
389 Value::Map(_) => val
390 .deserialize_into::<HashMap<PlatformIdentifier, T>>()
391 .map_err(de::Error::custom)?,
392 Value::Unit => HashMap::default(),
393 val => {
394 return Err(de::Error::custom(format!(
395 "Expected platforms to be a table or nil, but got {val:?}",
396 )))
397 }
398 },
399 None => HashMap::default(),
400 };
401
402 let default = if other_entries.is_empty() {
403 T::default()
404 } else {
405 let obj = normalize_lua_value(Value::Map(other_entries.into_iter().collect()));
409 T::deserialize(obj.into_deserializer()).map_err(de::Error::custom)?
410 };
411 apply_per_platform_overrides(&mut per_platform, &default)
412 .map_err(|err: <T as PartialOverride>::Err| de::Error::custom(err.to_string()))?;
413 Ok(PerPlatform {
414 default,
415 per_platform,
416 })
417 }
418
419 fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
420 where
421 A: de::SeqAccess<'de>,
422 {
423 let default = T::deserialize(de::value::SeqAccessDeserializer::new(seq))?;
427 Ok(PerPlatform::new(default))
428 }
429
430 fn visit_unit<E>(self) -> Result<Self::Value, E>
431 where
432 E: de::Error,
433 {
434 T::on_nil().map_err(de::Error::custom)
435 }
436
437 fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
438 where
439 E: de::Error,
440 {
441 let s = std::str::from_utf8(v).map_err(de::Error::custom)?;
442 self.visit_str(s)
443 }
444
445 fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
446 where
447 E: de::Error,
448 {
449 self.visit_bytes(&v)
450 }
451
452 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
453 where
454 E: de::Error,
455 {
456 let default = T::deserialize(v.into_deserializer())?;
457 Ok(PerPlatform::new(default))
458 }
459}
460
461impl<'de, T> Deserialize<'de> for PerPlatform<T>
462where
463 T: DeserializeOwned,
464 T: PlatformOverridable,
465 T: Default,
466 T: Clone,
467{
468 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
469 deserializer.deserialize_any(PerPlatformVisitor(std::marker::PhantomData))
470 }
471}
472
473pub(crate) fn per_platform_from_intermediate<'de, D, I, T>(
477 deserializer: D,
478) -> Result<PerPlatform<T>, D::Error>
479where
480 D: Deserializer<'de>,
481 I: PlatformOverridable<Err: ToString>,
482 I: DeserializeOwned,
483 I: Default,
484 I: Clone,
485 T: TryFrom<I, Error: ToString>,
486{
487 PerPlatform::<I>::deserialize(deserializer)?
488 .map(|internal| {
489 T::try_from(internal.clone()).map_err(|err| serde::de::Error::custom(err.to_string()))
490 })
491 .transpose()
492}
493
494fn apply_per_platform_overrides<T>(
495 per_platform: &mut HashMap<PlatformIdentifier, T>,
496 base: &T,
497) -> Result<(), T::Err>
498where
499 T: PartialOverride,
500 T: Clone,
501{
502 let per_platform_raw = per_platform.clone();
503 for (platform, overrides) in per_platform.clone() {
504 let overridden = base.apply_overrides(&overrides)?;
506 per_platform.insert(platform, overridden);
507 }
508 for (platform, overrides) in per_platform_raw {
509 for extended_platform in &platform.get_extended_platforms() {
511 if let Some(extended_overrides) = per_platform.get(extended_platform) {
512 per_platform.insert(
513 extended_platform.to_owned(),
514 extended_overrides.apply_overrides(&overrides)?,
515 );
516 }
517 }
518 }
519 Ok(())
520}
521
522#[cfg(test)]
523mod tests {
524
525 use super::*;
526 use proptest::prelude::*;
527
528 fn platform_identifier_strategy() -> impl Strategy<Value = PlatformIdentifier> {
529 prop_oneof![
530 Just(PlatformIdentifier::Unix),
531 Just(PlatformIdentifier::Windows),
532 Just(PlatformIdentifier::Win32),
533 Just(PlatformIdentifier::Cygwin),
534 Just(PlatformIdentifier::MacOSX),
535 Just(PlatformIdentifier::Linux),
536 Just(PlatformIdentifier::FreeBSD),
537 ]
538 }
539
540 #[tokio::test]
541 async fn sort_platform_identifier_more_specific_last() {
542 let mut platforms = vec![
543 PlatformIdentifier::Cygwin,
544 PlatformIdentifier::Linux,
545 PlatformIdentifier::Unix,
546 ];
547 platforms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
548 assert_eq!(
549 platforms,
550 vec![
551 PlatformIdentifier::Unix,
552 PlatformIdentifier::Cygwin,
553 PlatformIdentifier::Linux
554 ]
555 );
556 let mut platforms = vec![PlatformIdentifier::Windows, PlatformIdentifier::Win32];
557 platforms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
558 assert_eq!(
559 platforms,
560 vec![PlatformIdentifier::Win32, PlatformIdentifier::Windows]
561 )
562 }
563
564 #[tokio::test]
565 async fn test_is_subset_of() {
566 assert!(PlatformIdentifier::Unix.is_subset_of(&PlatformIdentifier::Linux));
567 assert!(PlatformIdentifier::Unix.is_subset_of(&PlatformIdentifier::MacOSX));
568 assert!(!PlatformIdentifier::Linux.is_subset_of(&PlatformIdentifier::Unix));
569 }
570
571 #[tokio::test]
572 async fn test_is_extension_of() {
573 assert!(PlatformIdentifier::Linux.is_extension_of(&PlatformIdentifier::Unix));
574 assert!(PlatformIdentifier::MacOSX.is_extension_of(&PlatformIdentifier::Unix));
575 assert!(!PlatformIdentifier::Unix.is_extension_of(&PlatformIdentifier::Linux));
576 }
577
578 #[tokio::test]
579 async fn per_platform() {
580 let foo = PerPlatform {
581 default: "default",
582 per_platform: vec![
583 (PlatformIdentifier::Unix, "unix"),
584 (PlatformIdentifier::FreeBSD, "freebsd"),
585 (PlatformIdentifier::Cygwin, "cygwin"),
586 (PlatformIdentifier::Linux, "linux"),
587 ]
588 .into_iter()
589 .collect(),
590 };
591 assert_eq!(*foo.get(&PlatformIdentifier::MacOSX), "unix");
592 assert_eq!(*foo.get(&PlatformIdentifier::Linux), "linux");
593 assert_eq!(*foo.get(&PlatformIdentifier::FreeBSD), "freebsd");
594 assert_eq!(*foo.get(&PlatformIdentifier::Cygwin), "cygwin");
595 assert_eq!(*foo.get(&PlatformIdentifier::Windows), "default");
596 }
597
598 #[cfg(target_os = "linux")]
599 #[tokio::test]
600 async fn test_target_identifier() {
601 run_test_target_identifier(PlatformIdentifier::Linux)
602 }
603
604 #[cfg(target_os = "macos")]
605 #[tokio::test]
606 async fn test_target_identifier() {
607 run_test_target_identifier(PlatformIdentifier::MacOSX)
608 }
609
610 #[cfg(target_env = "msvc")]
611 #[tokio::test]
612 async fn test_target_identifier() {
613 run_test_target_identifier(PlatformIdentifier::Windows)
614 }
615
616 #[cfg(target_os = "android")]
617 #[tokio::test]
618 async fn test_target_identifier() {
619 run_test_target_identifier(PlatformIdentifier::Unix)
620 }
621
622 fn run_test_target_identifier(expected: PlatformIdentifier) {
623 assert_eq!(expected, target_identifier());
624 }
625
626 proptest! {
627 #[test]
628 fn supported_platforms(identifier in platform_identifier_strategy()) {
629 let identifier_str = identifier.to_string();
630 let platforms = vec![identifier_str];
631 let platform_support = PlatformSupport::parse(&platforms).unwrap();
632 prop_assert!(platform_support.is_supported(&identifier))
633 }
634
635 #[test]
636 fn unsupported_platforms_only(unsupported in platform_identifier_strategy(), supported in platform_identifier_strategy()) {
637 if supported == unsupported
638 || unsupported.is_extension_of(&supported) {
639 return Ok(());
640 }
641 let identifier_str = format!("!{unsupported}");
642 let platforms = vec![identifier_str];
643 let platform_support = PlatformSupport::parse(&platforms).unwrap();
644 prop_assert!(!platform_support.is_supported(&unsupported));
645 prop_assert!(platform_support.is_supported(&supported))
646 }
647
648 #[test]
649 fn supported_and_unsupported_platforms(unsupported in platform_identifier_strategy(), unspecified in platform_identifier_strategy()) {
650 if unspecified == unsupported
651 || unsupported.is_extension_of(&unspecified) {
652 return Ok(());
653 }
654 let supported_str = unspecified.to_string();
655 let unsupported_str = format!("!{unsupported}");
656 let platforms = vec![supported_str, unsupported_str];
657 let platform_support = PlatformSupport::parse(&platforms).unwrap();
658 prop_assert!(platform_support.is_supported(&unspecified));
659 prop_assert!(!platform_support.is_supported(&unsupported));
660 }
661
662 #[test]
663 fn all_platforms_supported_if_none_are_specified(identifier in platform_identifier_strategy()) {
664 let platforms = vec![];
665 let platform_support = PlatformSupport::parse(&platforms).unwrap();
666 prop_assert!(platform_support.is_supported(&identifier))
667 }
668
669 #[test]
670 fn conflicting_platforms(identifier in platform_identifier_strategy()) {
671 let identifier_str = identifier.to_string();
672 let identifier_str_negated = format!("!{identifier}");
673 let platforms = vec![identifier_str, identifier_str_negated];
674 let _ = PlatformSupport::parse(&platforms).unwrap_err();
675 }
676
677 #[test]
678 fn extended_platforms_supported_if_supported(identifier in platform_identifier_strategy()) {
679 let identifier_str = identifier.to_string();
680 let platforms = vec![identifier_str];
681 let platform_support = PlatformSupport::parse(&platforms).unwrap();
682 for identifier in identifier.get_extended_platforms() {
683 prop_assert!(platform_support.is_supported(&identifier))
684 }
685 }
686
687 #[test]
688 fn sub_platforms_unsupported_if_unsupported(identifier in platform_identifier_strategy()) {
689 let identifier_str = format!("!{identifier}");
690 let platforms = vec![identifier_str];
691 let platform_support = PlatformSupport::parse(&platforms).unwrap();
692 for identifier in identifier.get_subsets() {
693 prop_assert!(!platform_support.is_supported(&identifier))
694 }
695 }
696
697 #[test]
698 fn conflicting_extended_platform_definitions(identifier in platform_identifier_strategy()) {
699 let extended_platforms = identifier.get_extended_platforms();
700 if extended_platforms.is_empty() {
701 return Ok(());
702 }
703 let supported_str = identifier.to_string();
704 let mut platforms: Vec<String> = extended_platforms.into_iter().map(|ident| format!("!{ident}")).collect();
705 platforms.push(supported_str);
706 let _ = PlatformSupport::parse(&platforms).unwrap_err();
707 }
708 }
709}