Skip to main content

lux_lib/lua_rockspec/
platform.rs

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/// Identifier by a platform.
17/// The `PartialOrd` instance views more specific platforms as `Greater`
18#[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    // TODO: Add undocumented platform identifiers from luarocks codebase?
23    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
40// Order by specificity -> less specific = `Less`
41impl 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
60/// Retrieves the platform identifier for the target platform
61///
62/// NOTE: This is the platform lux was built with.
63/// As we don't support cross-compiling luarocks packages, we currently expect
64/// users to use a version of lux that was built with the same platform
65/// as the one they are targeting
66fn 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    /// Get identifiers that are a subset of this identifier.
84    /// For example, Unix is a subset of Linux
85    pub fn get_subsets(&self) -> Vec<Self> {
86        PlatformIdentifier::iter()
87            .filter(|identifier| identifier.is_subset_of(self))
88            .collect()
89    }
90
91    /// Get identifiers that are an extension of this identifier.
92    /// For example, Linux is an extension of Unix
93    pub fn get_extended_platforms(&self) -> Vec<Self> {
94        PlatformIdentifier::iter()
95            .filter(|identifier| identifier.is_extension_of(self))
96            .collect()
97    }
98
99    /// e.g. Unix is a subset of Linux
100    fn is_subset_of(&self, other: &PlatformIdentifier) -> bool {
101        self.partial_cmp(other) == Some(Ordering::Less)
102    }
103
104    /// e.g. Linux is an extension of Unix
105    fn is_extension_of(&self, other: &PlatformIdentifier) -> bool {
106        self.partial_cmp(other) == Some(Ordering::Greater)
107    }
108}
109
110/// Used to specify which platforms a rock can be built for
111#[derive(Clone, Debug, PartialEq)]
112pub struct PlatformSupport {
113    /// Do not match this platform
114    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                // Platform assertions can exist in one of the following forms:
176                // - `platform` - a positive assertion for the platform (the platform must be present)
177                // - `!platform` - a negative assertion for the platform (any platform *but* this one must be present)
178                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 a platform with the same name exists already and is contradictory
188                // then throw an error. An example of such a contradiction is e.g.:
189                // [`win32`, `!win32`]
190                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                        // TODO(vhyrro): More detailed errors
213                        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        // Platforms are matched in one of two ways: exclusively or inclusively.
225        // If only positive matches are present, then the platforms are matched inclusively (as you only support the matches that you specified).
226        // If any negative matches are present, then the platforms are matched exclusively (as you want to support any operating system *other* than the ones you negated).
227        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                // Loop through all identifiers and set them to true if they are not present in
233                // the map (exclusive matching).
234                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            // Only validate positive matches (inclusive matching)
243            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/// Data that that can vary per platform
274#[derive(Clone, Debug, PartialEq)]
275pub struct PerPlatform<T> {
276    /// The base data, applicable if no platform is specified
277    pub(crate) default: T,
278    /// The per-platform override, if present.
279    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    /// Merge per-platform overrides for the configured build target platform,
291    /// with more specific platform overrides having higher priority.
292    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                // More specific platforms first.
306                // This is safe because a platform's subsets
307                // can be totally ordered among each other.
308                .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            // Build a Map from remaining entries, then normalise: if all keys are
406            // integers (a Lua sequence), normalize_lua_value converts it to a
407            // Value::Seq so downstream struct deserializers work correctly.
408            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        // Any table without explicit keys is considered a sequence in Lua.
424        // Sequences cannot have platform overrides, so we simply deserialize
425        // them as the default value.
426        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
473/// Deserializes a Lua value into a `PerPlatform<T>` through a proxy representation `G`. This is
474/// useful when `T` cannot be directly deserialized from a flexible format like Lua, but can be
475/// constructed from a simpler type `G`.
476pub(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        // Add base values for each platform
505        let overridden = base.apply_overrides(&overrides)?;
506        per_platform.insert(platform, overridden);
507    }
508    for (platform, overrides) in per_platform_raw {
509        // Add extended platform dependencies (without base deps) for each platform
510        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}