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)]
159pub enum PlatformValidationError {
160 #[error("error when parsing platform identifier: {0}")]
161 ParseError(String),
162
163 #[error("conflicting supported platform entries")]
164 ConflictingEntries,
165}
166
167impl PlatformSupport {
168 fn validate_platforms(
169 platforms: &[String],
170 ) -> Result<HashMap<PlatformIdentifier, bool>, PlatformValidationError> {
171 platforms
172 .iter()
173 .try_fold(HashMap::new(), |mut platforms, platform| {
174 let (is_positive_assertion, platform) = platform
178 .strip_prefix('!')
179 .map(|str| (false, str))
180 .unwrap_or((true, platform));
181
182 let platform_identifier = platform
183 .parse::<PlatformIdentifier>()
184 .map_err(|err| PlatformValidationError::ParseError(err.to_string()))?;
185
186 if platforms
190 .get(&platform_identifier)
191 .unwrap_or(&is_positive_assertion)
192 != &is_positive_assertion
193 {
194 return Err(PlatformValidationError::ConflictingEntries);
195 }
196
197 platforms.insert(platform_identifier.clone(), is_positive_assertion);
198
199 let subset_or_extended_platforms = if is_positive_assertion {
200 platform_identifier.get_extended_platforms()
201 } else {
202 platform_identifier.get_subsets()
203 };
204
205 for sub_platform in subset_or_extended_platforms {
206 if platforms
207 .get(&sub_platform)
208 .unwrap_or(&is_positive_assertion)
209 != &is_positive_assertion
210 {
211 return Err(PlatformValidationError::ConflictingEntries);
213 }
214
215 platforms.insert(sub_platform, is_positive_assertion);
216 }
217
218 Ok(platforms)
219 })
220 }
221
222 pub fn parse(platforms: &[String]) -> Result<Self, PlatformValidationError> {
223 match platforms {
227 [] => Ok(Self::default()),
228 platforms if platforms.iter().any(|platform| platform.starts_with('!')) => {
229 let mut platform_map = Self::validate_platforms(platforms)?;
230
231 for identifier in PlatformIdentifier::iter() {
234 if !matches!(identifier, PlatformIdentifier::Unknown(_)) {
235 platform_map.entry(identifier).or_insert(true);
236 }
237 }
238
239 Ok(Self { platform_map })
240 }
241 platforms => Ok(Self {
243 platform_map: Self::validate_platforms(platforms)?,
244 }),
245 }
246 }
247
248 pub fn is_supported(&self, platform: &PlatformIdentifier) -> bool {
249 self.platform_map.get(platform).cloned().unwrap_or(false)
250 }
251
252 pub(crate) fn platforms(&self) -> &HashMap<PlatformIdentifier, bool> {
253 &self.platform_map
254 }
255}
256
257pub trait PartialOverride: Sized {
258 type Err: std::error::Error;
259
260 fn apply_overrides(&self, override_val: &Self) -> Result<Self, Self::Err>;
261}
262
263pub trait PlatformOverridable: PartialOverride {
264 type Err: std::error::Error;
265
266 fn on_nil<T>() -> Result<PerPlatform<T>, <Self as PlatformOverridable>::Err>
267 where
268 T: PlatformOverridable,
269 T: Default;
270}
271
272#[derive(Clone, Debug, PartialEq)]
274pub struct PerPlatform<T> {
275 pub(crate) default: T,
277 pub(crate) per_platform: HashMap<PlatformIdentifier, T>,
279}
280
281impl<T> PerPlatform<T> {
282 pub(crate) fn new(default: T) -> Self {
283 Self {
284 default,
285 per_platform: HashMap::default(),
286 }
287 }
288
289 pub fn current_platform(&self) -> &T {
292 self.for_platform_identifier(&target_identifier())
293 }
294
295 fn for_platform_identifier(&self, identifier: &PlatformIdentifier) -> &T {
296 self.get(identifier)
297 }
298
299 pub fn get(&self, platform: &PlatformIdentifier) -> &T {
300 self.per_platform.get(platform).unwrap_or(
301 platform
302 .get_subsets()
303 .into_iter()
304 .sorted_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal))
308 .find(|identifier| self.per_platform.contains_key(identifier))
309 .and_then(|identifier| self.per_platform.get(&identifier))
310 .unwrap_or(&self.default),
311 )
312 }
313
314 pub(crate) fn map<U, F>(&self, cb: F) -> PerPlatform<U>
315 where
316 F: Fn(&T) -> U,
317 {
318 PerPlatform {
319 default: cb(&self.default),
320 per_platform: self
321 .per_platform
322 .iter()
323 .map(|(identifier, value)| (identifier.clone(), cb(value)))
324 .collect(),
325 }
326 }
327}
328
329impl<U, E> PerPlatform<Result<U, E>>
330where
331 E: std::error::Error,
332{
333 pub fn transpose(self) -> Result<PerPlatform<U>, E> {
334 Ok(PerPlatform {
335 default: self.default?,
336 per_platform: self
337 .per_platform
338 .into_iter()
339 .map(|(identifier, value)| Ok((identifier, value?)))
340 .try_collect()?,
341 })
342 }
343}
344
345impl<T: Default> Default for PerPlatform<T> {
346 fn default() -> Self {
347 Self {
348 default: T::default(),
349 per_platform: HashMap::default(),
350 }
351 }
352}
353
354struct PerPlatformVisitor<T>(std::marker::PhantomData<T>);
355
356impl<'de, T> Visitor<'de> for PerPlatformVisitor<T>
357where
358 T: DeserializeOwned,
359 T: PlatformOverridable,
360 T: Default,
361 T: Clone,
362{
363 type Value = PerPlatform<T>;
364
365 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
366 formatter.write_str("a table or nil")
367 }
368
369 fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
370 where
371 A: de::MapAccess<'de>,
372 {
373 use serde_value::Value;
374
375 let mut platforms_val: Option<Value> = None;
376 let mut other_entries: Vec<(Value, Value)> = Vec::new();
377
378 while let Some(key) = map.next_key_seed(LuaValueSeed)? {
379 if key == Value::String("platforms".to_string()) {
380 platforms_val = Some(map.next_value_seed(LuaValueSeed)?);
381 } else {
382 other_entries.push((key, map.next_value_seed(LuaValueSeed)?));
383 }
384 }
385
386 let mut per_platform = match platforms_val {
387 Some(val) => match val {
388 Value::Map(_) => val
389 .deserialize_into::<HashMap<PlatformIdentifier, T>>()
390 .map_err(de::Error::custom)?,
391 Value::Unit => HashMap::default(),
392 val => {
393 return Err(de::Error::custom(format!(
394 "Expected platforms to be a table or nil, but got {val:?}",
395 )))
396 }
397 },
398 None => HashMap::default(),
399 };
400
401 let default = if other_entries.is_empty() {
402 T::default()
403 } else {
404 let obj = normalize_lua_value(Value::Map(other_entries.into_iter().collect()));
408 T::deserialize(obj.into_deserializer()).map_err(de::Error::custom)?
409 };
410 apply_per_platform_overrides(&mut per_platform, &default)
411 .map_err(|err: <T as PartialOverride>::Err| de::Error::custom(err.to_string()))?;
412 Ok(PerPlatform {
413 default,
414 per_platform,
415 })
416 }
417
418 fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
419 where
420 A: de::SeqAccess<'de>,
421 {
422 let default = T::deserialize(de::value::SeqAccessDeserializer::new(seq))?;
426 Ok(PerPlatform::new(default))
427 }
428
429 fn visit_unit<E>(self) -> Result<Self::Value, E>
430 where
431 E: de::Error,
432 {
433 T::on_nil().map_err(de::Error::custom)
434 }
435
436 fn visit_bytes<E>(self, v: &[u8]) -> Result<Self::Value, E>
437 where
438 E: de::Error,
439 {
440 let s = std::str::from_utf8(v).map_err(de::Error::custom)?;
441 self.visit_str(s)
442 }
443
444 fn visit_byte_buf<E>(self, v: Vec<u8>) -> Result<Self::Value, E>
445 where
446 E: de::Error,
447 {
448 self.visit_bytes(&v)
449 }
450
451 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
452 where
453 E: de::Error,
454 {
455 let default = T::deserialize(v.into_deserializer())?;
456 Ok(PerPlatform::new(default))
457 }
458}
459
460impl<'de, T> Deserialize<'de> for PerPlatform<T>
461where
462 T: DeserializeOwned,
463 T: PlatformOverridable,
464 T: Default,
465 T: Clone,
466{
467 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
468 deserializer.deserialize_any(PerPlatformVisitor(std::marker::PhantomData))
469 }
470}
471
472pub(crate) fn per_platform_from_intermediate<'de, D, I, T>(
476 deserializer: D,
477) -> Result<PerPlatform<T>, D::Error>
478where
479 D: Deserializer<'de>,
480 I: PlatformOverridable<Err: ToString>,
481 I: DeserializeOwned,
482 I: Default,
483 I: Clone,
484 T: TryFrom<I, Error: ToString>,
485{
486 PerPlatform::<I>::deserialize(deserializer)?
487 .map(|internal| {
488 T::try_from(internal.clone()).map_err(|err| serde::de::Error::custom(err.to_string()))
489 })
490 .transpose()
491}
492
493fn apply_per_platform_overrides<T>(
494 per_platform: &mut HashMap<PlatformIdentifier, T>,
495 base: &T,
496) -> Result<(), T::Err>
497where
498 T: PartialOverride,
499 T: Clone,
500{
501 let per_platform_raw = per_platform.clone();
502 for (platform, overrides) in per_platform.clone() {
503 let overridden = base.apply_overrides(&overrides)?;
505 per_platform.insert(platform, overridden);
506 }
507 for (platform, overrides) in per_platform_raw {
508 for extended_platform in &platform.get_extended_platforms() {
510 if let Some(extended_overrides) = per_platform.get(extended_platform) {
511 per_platform.insert(
512 extended_platform.to_owned(),
513 extended_overrides.apply_overrides(&overrides)?,
514 );
515 }
516 }
517 }
518 Ok(())
519}
520
521#[cfg(test)]
522mod tests {
523
524 use super::*;
525 use proptest::prelude::*;
526
527 fn platform_identifier_strategy() -> impl Strategy<Value = PlatformIdentifier> {
528 prop_oneof![
529 Just(PlatformIdentifier::Unix),
530 Just(PlatformIdentifier::Windows),
531 Just(PlatformIdentifier::Win32),
532 Just(PlatformIdentifier::Cygwin),
533 Just(PlatformIdentifier::MacOSX),
534 Just(PlatformIdentifier::Linux),
535 Just(PlatformIdentifier::FreeBSD),
536 ]
537 }
538
539 #[tokio::test]
540 async fn sort_platform_identifier_more_specific_last() {
541 let mut platforms = vec![
542 PlatformIdentifier::Cygwin,
543 PlatformIdentifier::Linux,
544 PlatformIdentifier::Unix,
545 ];
546 platforms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
547 assert_eq!(
548 platforms,
549 vec![
550 PlatformIdentifier::Unix,
551 PlatformIdentifier::Cygwin,
552 PlatformIdentifier::Linux
553 ]
554 );
555 let mut platforms = vec![PlatformIdentifier::Windows, PlatformIdentifier::Win32];
556 platforms.sort_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal));
557 assert_eq!(
558 platforms,
559 vec![PlatformIdentifier::Win32, PlatformIdentifier::Windows]
560 )
561 }
562
563 #[tokio::test]
564 async fn test_is_subset_of() {
565 assert!(PlatformIdentifier::Unix.is_subset_of(&PlatformIdentifier::Linux));
566 assert!(PlatformIdentifier::Unix.is_subset_of(&PlatformIdentifier::MacOSX));
567 assert!(!PlatformIdentifier::Linux.is_subset_of(&PlatformIdentifier::Unix));
568 }
569
570 #[tokio::test]
571 async fn test_is_extension_of() {
572 assert!(PlatformIdentifier::Linux.is_extension_of(&PlatformIdentifier::Unix));
573 assert!(PlatformIdentifier::MacOSX.is_extension_of(&PlatformIdentifier::Unix));
574 assert!(!PlatformIdentifier::Unix.is_extension_of(&PlatformIdentifier::Linux));
575 }
576
577 #[tokio::test]
578 async fn per_platform() {
579 let foo = PerPlatform {
580 default: "default",
581 per_platform: vec![
582 (PlatformIdentifier::Unix, "unix"),
583 (PlatformIdentifier::FreeBSD, "freebsd"),
584 (PlatformIdentifier::Cygwin, "cygwin"),
585 (PlatformIdentifier::Linux, "linux"),
586 ]
587 .into_iter()
588 .collect(),
589 };
590 assert_eq!(*foo.get(&PlatformIdentifier::MacOSX), "unix");
591 assert_eq!(*foo.get(&PlatformIdentifier::Linux), "linux");
592 assert_eq!(*foo.get(&PlatformIdentifier::FreeBSD), "freebsd");
593 assert_eq!(*foo.get(&PlatformIdentifier::Cygwin), "cygwin");
594 assert_eq!(*foo.get(&PlatformIdentifier::Windows), "default");
595 }
596
597 #[cfg(target_os = "linux")]
598 #[tokio::test]
599 async fn test_target_identifier() {
600 run_test_target_identifier(PlatformIdentifier::Linux)
601 }
602
603 #[cfg(target_os = "macos")]
604 #[tokio::test]
605 async fn test_target_identifier() {
606 run_test_target_identifier(PlatformIdentifier::MacOSX)
607 }
608
609 #[cfg(target_env = "msvc")]
610 #[tokio::test]
611 async fn test_target_identifier() {
612 run_test_target_identifier(PlatformIdentifier::Windows)
613 }
614
615 #[cfg(target_os = "android")]
616 #[tokio::test]
617 async fn test_target_identifier() {
618 run_test_target_identifier(PlatformIdentifier::Unix)
619 }
620
621 fn run_test_target_identifier(expected: PlatformIdentifier) {
622 assert_eq!(expected, target_identifier());
623 }
624
625 proptest! {
626 #[test]
627 fn supported_platforms(identifier in platform_identifier_strategy()) {
628 let identifier_str = identifier.to_string();
629 let platforms = vec![identifier_str];
630 let platform_support = PlatformSupport::parse(&platforms).unwrap();
631 prop_assert!(platform_support.is_supported(&identifier))
632 }
633
634 #[test]
635 fn unsupported_platforms_only(unsupported in platform_identifier_strategy(), supported in platform_identifier_strategy()) {
636 if supported == unsupported
637 || unsupported.is_extension_of(&supported) {
638 return Ok(());
639 }
640 let identifier_str = format!("!{unsupported}");
641 let platforms = vec![identifier_str];
642 let platform_support = PlatformSupport::parse(&platforms).unwrap();
643 prop_assert!(!platform_support.is_supported(&unsupported));
644 prop_assert!(platform_support.is_supported(&supported))
645 }
646
647 #[test]
648 fn supported_and_unsupported_platforms(unsupported in platform_identifier_strategy(), unspecified in platform_identifier_strategy()) {
649 if unspecified == unsupported
650 || unsupported.is_extension_of(&unspecified) {
651 return Ok(());
652 }
653 let supported_str = unspecified.to_string();
654 let unsupported_str = format!("!{unsupported}");
655 let platforms = vec![supported_str, unsupported_str];
656 let platform_support = PlatformSupport::parse(&platforms).unwrap();
657 prop_assert!(platform_support.is_supported(&unspecified));
658 prop_assert!(!platform_support.is_supported(&unsupported));
659 }
660
661 #[test]
662 fn all_platforms_supported_if_none_are_specified(identifier in platform_identifier_strategy()) {
663 let platforms = vec![];
664 let platform_support = PlatformSupport::parse(&platforms).unwrap();
665 prop_assert!(platform_support.is_supported(&identifier))
666 }
667
668 #[test]
669 fn conflicting_platforms(identifier in platform_identifier_strategy()) {
670 let identifier_str = identifier.to_string();
671 let identifier_str_negated = format!("!{identifier}");
672 let platforms = vec![identifier_str, identifier_str_negated];
673 let _ = PlatformSupport::parse(&platforms).unwrap_err();
674 }
675
676 #[test]
677 fn extended_platforms_supported_if_supported(identifier in platform_identifier_strategy()) {
678 let identifier_str = identifier.to_string();
679 let platforms = vec![identifier_str];
680 let platform_support = PlatformSupport::parse(&platforms).unwrap();
681 for identifier in identifier.get_extended_platforms() {
682 prop_assert!(platform_support.is_supported(&identifier))
683 }
684 }
685
686 #[test]
687 fn sub_platforms_unsupported_if_unsupported(identifier in platform_identifier_strategy()) {
688 let identifier_str = format!("!{identifier}");
689 let platforms = vec![identifier_str];
690 let platform_support = PlatformSupport::parse(&platforms).unwrap();
691 for identifier in identifier.get_subsets() {
692 prop_assert!(!platform_support.is_supported(&identifier))
693 }
694 }
695
696 #[test]
697 fn conflicting_extended_platform_definitions(identifier in platform_identifier_strategy()) {
698 let extended_platforms = identifier.get_extended_platforms();
699 if extended_platforms.is_empty() {
700 return Ok(());
701 }
702 let supported_str = identifier.to_string();
703 let mut platforms: Vec<String> = extended_platforms.into_iter().map(|ident| format!("!{ident}")).collect();
704 platforms.push(supported_str);
705 let _ = PlatformSupport::parse(&platforms).unwrap_err();
706 }
707 }
708}