version_history/
lib.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use anyhow::{bail, Context};
6use chrono::NaiveDate;
7use itertools::Itertools;
8use log::error;
9use serde::de::{Error, Unexpected};
10use serde::{Deserialize, Deserializer, Serialize};
11use std::array::TryFromSliceError;
12use std::collections::BTreeMap;
13use std::fmt;
14
15const VERSION_HISTORY_SCHEMA_ID: &str = "https://fuchsia.dev/schema/version_history.json";
16const VERSION_HISTORY_NAME: &str = "Platform version map";
17const VERSION_HISTORY_TYPE: &str = "version_history";
18
19/// An `ApiLevel` represents an API level of the Fuchsia platform.
20#[derive(Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
21pub struct ApiLevel(u32);
22
23impl ApiLevel {
24    /// The `NEXT` API level, representing an unstable draft of the next
25    /// numbered stable API level.
26    pub const NEXT: ApiLevel = ApiLevel(4291821568);
27
28    /// The `HEAD` API level, representing the bleeding edge of development.
29    pub const HEAD: ApiLevel = ApiLevel(4292870144);
30
31    /// The `PLATFORM` pseudo-API level, which is used in platform builds.
32    pub const PLATFORM: ApiLevel = ApiLevel(4293918720);
33
34    pub const fn from_u32(value: u32) -> Self {
35        Self(value)
36    }
37
38    pub fn as_u32(&self) -> u32 {
39        self.0
40    }
41
42    #[deprecated = "API levels are 32-bits as of RFC-0246"]
43    pub fn as_u64(&self) -> u64 {
44        self.0 as u64
45    }
46}
47
48impl fmt::Debug for ApiLevel {
49    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
50        match *self {
51            ApiLevel::NEXT => write!(f, "ApiLevel::NEXT"),
52            ApiLevel::HEAD => write!(f, "ApiLevel::HEAD"),
53            ApiLevel::PLATFORM => write!(f, "ApiLevel::PLATFORM"),
54            ApiLevel(l) => f.debug_tuple("ApiLevel").field(&l).finish(),
55        }
56    }
57}
58
59impl fmt::Display for ApiLevel {
60    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
61        match *self {
62            ApiLevel::NEXT => write!(f, "NEXT"),
63            ApiLevel::HEAD => write!(f, "HEAD"),
64            ApiLevel::PLATFORM => write!(f, "PLATFORM"),
65            ApiLevel(l) => write!(f, "{}", l),
66        }
67    }
68}
69
70impl std::str::FromStr for ApiLevel {
71    type Err = std::num::ParseIntError;
72
73    fn from_str(s: &str) -> Result<Self, Self::Err> {
74        match s {
75            "NEXT" => Ok(ApiLevel::NEXT),
76            "HEAD" => Ok(ApiLevel::HEAD),
77            "PLATFORM" => Ok(ApiLevel::PLATFORM),
78            s => Ok(ApiLevel::from_u32(s.parse()?)),
79        }
80    }
81}
82
83impl From<u32> for ApiLevel {
84    fn from(api_level: u32) -> ApiLevel {
85        ApiLevel(api_level)
86    }
87}
88
89impl From<&u32> for ApiLevel {
90    fn from(api_level: &u32) -> ApiLevel {
91        ApiLevel(*api_level)
92    }
93}
94
95impl From<ApiLevel> for u32 {
96    fn from(api_level: ApiLevel) -> u32 {
97        api_level.0
98    }
99}
100
101/// An `AbiRevision` is the 64-bit stamp representing an ABI revision of the
102/// Fuchsia platform.
103#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Clone)]
104pub struct AbiRevision(u64);
105
106/// An AbiRevisionExplanation represents the information that can be extracted
107/// from a raw ABI revision just based on the 64-bit number itself.
108///
109/// See //build/sdk/generate_version_history for the code that generates special
110/// ABI revisions, and the precise definitions of the fields below.
111#[derive(Debug, Copy, Clone, Eq, PartialEq)]
112pub enum AbiRevisionExplanation {
113    /// This is a normal ABI revision, selected randomly. It should correspond
114    /// to a normal API level.
115    Normal,
116
117    /// This is an unstable ABI revision targeted by platform components.
118    Platform {
119        /// Approximate date of the `integration.git` revision at which the
120        /// package was built.
121        date: chrono::NaiveDate,
122        /// Prefix of an `integration.git` revision from around the same time
123        /// that the package was built.
124        git_revision: u16,
125    },
126
127    /// This is an unstable ABI revision targeted by components built with the
128    /// SDK. This corresponds to API levels like `NEXT` and `HEAD`.
129    Unstable {
130        /// Approximate date of the `integration.git` revision from which the
131        /// SDK that built the package was built.
132        date: chrono::NaiveDate,
133        /// Prefix of an `integration.git` revision from around the same time
134        /// that the SDK that built the package was built.
135        git_revision: u16,
136    },
137
138    /// This ABI revision is exactly AbiRevision::INVALID, which is 2^64-1.
139    Invalid,
140
141    /// This is a special ABI revision with an unknown meaning. Presumably it
142    /// was introduced sometime after this code was compiled.
143    Malformed,
144}
145
146impl AbiRevision {
147    /// An ABI revision that is never supported by the platform. To be used when
148    /// an ABI revision is necessary, but none makes sense.
149    pub const INVALID: AbiRevision = AbiRevision(0xFFFF_FFFF_FFFF_FFFF);
150
151    pub const PATH: &'static str = "meta/fuchsia.abi/abi-revision";
152
153    /// Parse the ABI revision from little-endian bytes.
154    pub fn from_bytes(b: [u8; 8]) -> Self {
155        AbiRevision(u64::from_le_bytes(b))
156    }
157
158    /// Encode the ABI revision into little-endian bytes.
159    pub fn as_bytes(&self) -> [u8; 8] {
160        self.0.to_le_bytes()
161    }
162
163    pub const fn from_u64(u: u64) -> AbiRevision {
164        AbiRevision(u)
165    }
166
167    pub fn as_u64(&self) -> u64 {
168        self.0
169    }
170
171    pub fn explanation(self) -> AbiRevisionExplanation {
172        // ABI revisions beginning with 0xFF are "special".
173        const SPECIAL_ABI_REVISION_PREFIX: u64 = 0xFF;
174        const UNSTABLE_ABI_REVISION_PREFIX: u64 = 0xFF00;
175        const PLATFORM_ABI_REVISION_PREFIX: u64 = 0xFF01;
176
177        // Extract the date from an unstable or platform ABI revision.
178        let get_date = || {
179            // The lower 32 bits of an unstable or platform ABI revision encode
180            // the date.
181            const DATE_MASK: u64 = 0x0000_0000_FFFF_FFFF;
182            let date_ordinal = (self.as_u64() & DATE_MASK) as i32;
183            chrono::NaiveDate::from_num_days_from_ce_opt(date_ordinal).unwrap_or_default()
184        };
185
186        // Extract the Git revision prefix from an unstable or platform ABI
187        // revision.
188        let get_git_revision = || {
189            // The second most significant 16 bits are a prefix of the
190            // `integration.git` hash.
191            const GIT_HASH_MASK: u64 = 0x0000_FFFF_0000_0000;
192            ((self.as_u64() & GIT_HASH_MASK) >> 32) as u16
193        };
194
195        if self.as_u64() >> 56 != SPECIAL_ABI_REVISION_PREFIX {
196            AbiRevisionExplanation::Normal
197        } else if self.as_u64() >> 48 == UNSTABLE_ABI_REVISION_PREFIX {
198            AbiRevisionExplanation::Unstable { date: get_date(), git_revision: get_git_revision() }
199        } else if self.as_u64() >> 48 == PLATFORM_ABI_REVISION_PREFIX {
200            AbiRevisionExplanation::Platform { date: get_date(), git_revision: get_git_revision() }
201        } else if self == AbiRevision::INVALID {
202            AbiRevisionExplanation::Invalid
203        } else {
204            AbiRevisionExplanation::Malformed
205        }
206    }
207}
208
209impl fmt::Display for AbiRevision {
210    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
211        // TODO(https://fxbug.dev/370727480): Pad to 16 digits and add 0x prefix.
212        write!(f, "{:x}", self.0)
213    }
214}
215
216impl std::str::FromStr for AbiRevision {
217    type Err = std::num::ParseIntError;
218
219    fn from_str(s: &str) -> Result<Self, Self::Err> {
220        let parsed = if let Some(s) = s.strip_prefix("0x") {
221            u64::from_str_radix(&s, 16)?
222        } else {
223            u64::from_str_radix(&s, 10)?
224        };
225        Ok(AbiRevision::from_u64(parsed))
226    }
227}
228
229impl<'de> Deserialize<'de> for AbiRevision {
230    fn deserialize<D>(deserializer: D) -> Result<AbiRevision, D::Error>
231    where
232        D: Deserializer<'de>,
233    {
234        let s = String::deserialize(deserializer)?;
235        s.parse().map_err(|_| D::Error::invalid_value(Unexpected::Str(&s), &"an unsigned integer"))
236    }
237}
238
239impl Serialize for AbiRevision {
240    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
241    where
242        S: serde::Serializer,
243    {
244        let string = format!("0x{:016x}", self.0);
245        serializer.serialize_str(&string)
246    }
247}
248
249impl From<u64> for AbiRevision {
250    fn from(abi_revision: u64) -> AbiRevision {
251        AbiRevision(abi_revision)
252    }
253}
254
255impl From<&u64> for AbiRevision {
256    fn from(abi_revision: &u64) -> AbiRevision {
257        AbiRevision(*abi_revision)
258    }
259}
260
261impl From<AbiRevision> for u64 {
262    fn from(abi_revision: AbiRevision) -> u64 {
263        abi_revision.0
264    }
265}
266
267impl From<[u8; 8]> for AbiRevision {
268    fn from(abi_revision: [u8; 8]) -> AbiRevision {
269        AbiRevision::from_bytes(abi_revision)
270    }
271}
272
273impl TryFrom<&[u8]> for AbiRevision {
274    type Error = TryFromSliceError;
275
276    fn try_from(abi_revision: &[u8]) -> Result<AbiRevision, Self::Error> {
277        let abi_revision: [u8; 8] = abi_revision.try_into()?;
278        Ok(AbiRevision::from_bytes(abi_revision))
279    }
280}
281
282/// Version represents an API level of the Fuchsia platform API.
283///
284/// See
285/// https://fuchsia.dev/fuchsia-src/contribute/governance/rfcs/0239_platform_versioning_in_practice
286/// for more details.
287#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
288pub struct Version {
289    /// The number associated with this API level.
290    pub api_level: ApiLevel,
291
292    /// The ABI revision associated with this API level.
293    pub abi_revision: AbiRevision,
294
295    /// The Status denotes the current status of the API level.
296    pub status: Status,
297}
298
299impl Version {
300    /// Returns true if this version is runnable - that is, whether components
301    /// targeting this version will be able to run on this device.
302    fn is_runnable(&self) -> bool {
303        match self.status {
304            Status::InDevelopment | Status::Supported => true,
305            Status::Unsupported => false,
306        }
307    }
308}
309
310#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, Hash)]
311pub enum Status {
312    #[serde(rename = "in-development")]
313    InDevelopment,
314    #[serde(rename = "supported")]
315    Supported,
316    #[serde(rename = "unsupported")]
317    Unsupported,
318}
319
320/// VersionHistory stores the history of Fuchsia API levels, and lets callers
321/// query the support status of API levels and ABI revisions.
322#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
323pub struct VersionHistory {
324    versions: &'static [Version],
325}
326
327impl VersionHistory {
328    /// Outside of tests, callers should use the static [HISTORY]
329    /// instance. However, for testing purposes, you may want to build your own
330    /// hermetic "alternate history" that doesn't change over time.
331    ///
332    /// If you're not testing versioning in particular, and you just want an API
333    /// level/ABI revision that works, see
334    /// [get_example_supported_version_for_tests].
335    pub const fn new(versions: &'static [Version]) -> Self {
336        VersionHistory { versions }
337    }
338
339    /// The ABI revision for components and packages that are "part of the
340    /// platform" and never "move between releases". For example:
341    ///
342    /// - Packages produced by the platform build have this ABI revision.
343    /// - Components which are not packaged but are "part of the platform"
344    ///   nonetheless (e.g. components loaded from bootfs) have this ABI
345    ///   revision.
346    /// - Most packages produced by assembly tools have this ABI revision.
347    ///   - The `update` package is a noteworthy exception, since it "moves
348    ///     between releases", in that it is produced by assembly tools from one
349    ///     Fuchsia release, and then later read by the OS from a previous
350    ///     release (that is, the one performing the update).
351    pub fn get_abi_revision_for_platform_components(&self) -> AbiRevision {
352        self.version_from_api_level(ApiLevel::PLATFORM)
353            .expect("API Level PLATFORM not found!")
354            .abi_revision
355    }
356
357    /// ffx currently presents information suggesting that the platform supports
358    /// a _single_ API level and ABI revision. This is misleading and should be
359    /// fixed. Until we do, we have to choose a particular Version for ffx to
360    /// present.
361    ///
362    /// TODO: https://fxbug.dev/326096999 - Remove this, or turn it into
363    /// something that makes more sense.
364    pub fn get_misleading_version_for_ffx(&self) -> Version {
365        self.runnable_versions()
366            .filter(|v| {
367                v.api_level != ApiLevel::NEXT
368                    && v.api_level != ApiLevel::HEAD
369                    && v.api_level != ApiLevel::PLATFORM
370            })
371            .last()
372            .unwrap()
373    }
374
375    /// API level to be used in tests that create components on the fly and need
376    /// to specify a supported API level or ABI revision, but don't particularly
377    /// care which. The returned [Version] will be consistent within a given
378    /// build, but may change from build to build.
379    pub fn get_example_supported_version_for_tests(&self) -> Version {
380        self.runnable_versions()
381            .filter(|v| {
382                v.api_level != ApiLevel::NEXT
383                    && v.api_level != ApiLevel::HEAD
384                    && v.api_level != ApiLevel::PLATFORM
385            })
386            .last()
387            .unwrap()
388    }
389
390    /// Check whether the platform supports building components that target the
391    /// given API level, and if so, returns the ABI revision associated with
392    /// that API level.
393    pub fn check_api_level_for_build(
394        &self,
395        api_level: ApiLevel,
396    ) -> Result<AbiRevision, ApiLevelError> {
397        let Some(version) = self.version_from_api_level(api_level) else {
398            return Err(ApiLevelError::Unknown {
399                api_level,
400                supported: self.runnable_versions().map(|v| v.api_level).collect(),
401            });
402        };
403
404        if version.is_runnable() {
405            Ok(version.abi_revision)
406        } else {
407            Err(ApiLevelError::Unsupported {
408                version,
409                supported: self.runnable_versions().map(|v| v.api_level).collect(),
410            })
411        }
412    }
413
414    /// Check whether the operating system supports running components that
415    /// target the given ABI revision.
416    pub fn check_abi_revision_for_runtime(
417        &self,
418        abi_revision: AbiRevision,
419    ) -> Result<(), AbiRevisionError> {
420        if let Some(version) = self.version_from_abi_revision(abi_revision) {
421            if version.is_runnable() {
422                Ok(())
423            } else {
424                Err(AbiRevisionError::Retired {
425                    version,
426                    supported_versions: self.supported_versions(),
427                })
428            }
429        } else {
430            // We don't recognize this ABI revision... Look at its structure to
431            // understand what's going on.
432            match abi_revision.explanation() {
433                AbiRevisionExplanation::Platform { date, git_revision } => {
434                    let (platform_date, platform_commit_hash) = self.platform_abi_info();
435                    Err(AbiRevisionError::PlatformMismatch {
436                        abi_revision,
437                        package_date: date,
438                        package_commit_hash: git_revision,
439                        platform_date,
440                        platform_commit_hash,
441                    })
442                }
443                AbiRevisionExplanation::Unstable { date, git_revision } => {
444                    let (platform_date, platform_commit_hash) = self.platform_abi_info();
445                    Err(AbiRevisionError::UnstableMismatch {
446                        abi_revision,
447                        package_sdk_date: date,
448                        package_sdk_commit_hash: git_revision,
449                        platform_date,
450                        platform_commit_hash,
451                        supported_versions: self.supported_versions(),
452                    })
453                }
454                AbiRevisionExplanation::Malformed => {
455                    Err(AbiRevisionError::Malformed { abi_revision })
456                }
457                AbiRevisionExplanation::Invalid => Err(AbiRevisionError::Invalid),
458                AbiRevisionExplanation::Normal => Err(AbiRevisionError::TooNew {
459                    abi_revision,
460                    supported_versions: self.supported_versions(),
461                }),
462            }
463        }
464    }
465
466    fn runnable_versions(&self) -> impl Iterator<Item = Version> + '_ {
467        self.versions.iter().filter(|v| v.is_runnable()).cloned()
468    }
469
470    fn supported_versions(&self) -> VersionVec {
471        VersionVec(
472            self.versions
473                .iter()
474                .filter(|v| match v.status {
475                    Status::InDevelopment => false,
476                    Status::Supported => true,
477                    Status::Unsupported => false,
478                })
479                .cloned()
480                .collect(),
481        )
482    }
483
484    fn version_from_abi_revision(&self, abi_revision: AbiRevision) -> Option<Version> {
485        self.versions.iter().find(|v| v.abi_revision == abi_revision).cloned()
486    }
487
488    pub fn version_from_api_level(&self, api_level: ApiLevel) -> Option<Version> {
489        self.versions.iter().find(|v| v.api_level == api_level).cloned()
490    }
491
492    fn platform_abi_info(&self) -> (NaiveDate, u16) {
493        if let Some(platform_version) = self.version_from_api_level(ApiLevel::PLATFORM) {
494            if let AbiRevisionExplanation::Platform { date, git_revision } =
495                platform_version.abi_revision.explanation()
496            {
497                return (date, git_revision);
498            }
499        }
500        error!(
501            "No PLATFORM API level found. This should never happen.
502Returning bogus data instead of panicking."
503        );
504        (NaiveDate::default(), 0)
505    }
506}
507
508#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
509pub enum AbiRevisionError {
510    #[error(
511        "Unknown platform ABI revision: 0x{abi_revision}.
512Found platform component from {package_date} (Git hash {package_commit_hash:x}).
513Platform components must be from {platform_date} (Git hash {platform_commit_hash:x})"
514    )]
515    PlatformMismatch {
516        abi_revision: AbiRevision,
517        package_date: NaiveDate,
518        package_commit_hash: u16,
519        platform_date: NaiveDate,
520        platform_commit_hash: u16,
521    },
522    #[error(
523        "Unknown NEXT or HEAD ABI revision: 0x{abi_revision}.
524SDK is from {package_sdk_date} (Git hash {package_sdk_commit_hash:x}).
525Expected {platform_date} (Git hash {platform_commit_hash:x})
526The following API levels are stable and supported:{supported_versions}"
527    )]
528    UnstableMismatch {
529        abi_revision: AbiRevision,
530        package_sdk_date: NaiveDate,
531        package_sdk_commit_hash: u16,
532        platform_date: NaiveDate,
533        platform_commit_hash: u16,
534        supported_versions: VersionVec,
535    },
536
537    #[error(
538        "Unknown ABI revision 0x{abi_revision}. It was probably created after
539this platform or tool was built. The following API levels are stable and
540supported:{supported_versions}"
541    )]
542    TooNew { abi_revision: AbiRevision, supported_versions: VersionVec },
543
544    #[error("Retired API level {} (0x{}) cannot run.
545The following API levels are stable and supported:{supported_versions}",
546        .version.api_level, .version.abi_revision)]
547    Retired { version: Version, supported_versions: VersionVec },
548
549    #[error("Invalid ABI revision 0x{}.", AbiRevision::INVALID)]
550    Invalid,
551
552    #[error("ABI revision 0x{abi_revision} has an unrecognized format.")]
553    Malformed { abi_revision: AbiRevision },
554}
555
556/// Wrapper for a vector of Versions with a nice implementation of Display.
557#[derive(Debug, Clone, PartialEq, Eq)]
558pub struct VersionVec(pub Vec<Version>);
559
560impl std::fmt::Display for VersionVec {
561    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
562        for version in self.0.iter() {
563            write!(f, "\n└── {} (0x{})", version.api_level, version.abi_revision)?;
564        }
565        Ok(())
566    }
567}
568
569#[derive(Debug, Clone, PartialEq, Eq)]
570pub enum ApiLevelError {
571    /// The user attempted to build a component targeting an API level that was
572    /// not recognized.
573    Unknown { api_level: ApiLevel, supported: Vec<ApiLevel> },
574
575    /// The user attempted to build a component targeting an API level that was
576    /// recognized, but is not supported for building.
577    Unsupported { version: Version, supported: Vec<ApiLevel> },
578}
579
580impl std::error::Error for ApiLevelError {}
581
582impl std::fmt::Display for ApiLevelError {
583    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
584        match self {
585            ApiLevelError::Unknown { api_level, supported } => {
586                write!(
587                    f,
588                    "Unknown target API level: {}. Is the SDK too old to support it?
589The following API levels are supported: {}",
590                    api_level,
591                    supported.iter().join(", ")
592                )
593            }
594            ApiLevelError::Unsupported { version, supported } => {
595                write!(
596                    f,
597                    "The SDK no longer supports API level {}.
598The following API levels are supported: {}",
599                    version.api_level,
600                    supported.iter().join(", ")
601                )
602            }
603        }
604    }
605}
606
607#[derive(Deserialize)]
608struct VersionHistoryDataJson {
609    name: String,
610    #[serde(rename = "type")]
611    element_type: String,
612    api_levels: BTreeMap<String, ApiLevelJson>,
613    special_api_levels: BTreeMap<String, SpecialApiLevelJson>,
614}
615
616#[derive(Deserialize)]
617struct VersionHistoryJson {
618    schema_id: String,
619    data: VersionHistoryDataJson,
620}
621
622#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deserialize)]
623struct ApiLevelJson {
624    pub abi_revision: AbiRevision,
625    pub status: Status,
626}
627
628#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deserialize)]
629struct SpecialApiLevelJson {
630    pub as_u32: u32,
631    pub abi_revision: AbiRevision,
632    pub status: Status,
633}
634
635pub fn parse_version_history(bytes: &[u8]) -> anyhow::Result<Vec<Version>> {
636    let v: VersionHistoryJson = serde_json::from_slice(bytes)?;
637
638    if v.schema_id != VERSION_HISTORY_SCHEMA_ID {
639        bail!("expected schema_id = {:?}; got {:?}", VERSION_HISTORY_SCHEMA_ID, v.schema_id)
640    }
641    if v.data.name != VERSION_HISTORY_NAME {
642        bail!("expected data.name = {:?}; got {:?}", VERSION_HISTORY_NAME, v.data.name)
643    }
644    if v.data.element_type != VERSION_HISTORY_TYPE {
645        bail!("expected data.type = {:?}; got {:?}", VERSION_HISTORY_TYPE, v.data.element_type,)
646    }
647
648    let mut versions = Vec::new();
649
650    for (key, value) in v.data.api_levels {
651        versions.push(Version {
652            api_level: key.parse()?,
653            abi_revision: value.abi_revision,
654            status: value.status,
655        });
656    }
657
658    for (key, value) in v.data.special_api_levels {
659        let api_level: ApiLevel =
660            key.parse().with_context(|| format!("Unknown special API level: {}", key))?;
661        if api_level.as_u32() != value.as_u32 {
662            bail!(
663                "Special API level {} had unexpected numerical value {} (Expected {})",
664                api_level,
665                value.as_u32,
666                api_level.as_u32()
667            )
668        }
669        versions.push(Version {
670            api_level,
671            abi_revision: value.abi_revision,
672            status: value.status,
673        });
674    }
675
676    versions.sort_by_key(|s| s.api_level);
677
678    let Some(latest_api_version) = versions.last() else {
679        bail!("there must be at least one API level")
680    };
681
682    if latest_api_version.status == Status::Unsupported {
683        bail!("most recent API level must not be 'unsupported'")
684    }
685
686    Ok(versions)
687}
688
689#[cfg(test)]
690mod tests {
691    use super::*;
692
693    #[test]
694    fn test_parse_history_works() {
695        let expected_bytes = br#"{
696            "data": {
697                "name": "Platform version map",
698                "type": "version_history",
699                "api_levels": {
700                    "1":{
701                        "abi_revision":"0xBEBE3F5CAAA046D2",
702                        "status":"supported"
703                    },
704                    "2":{
705                        "abi_revision":"0x50cbc6e8a39e1e2c",
706                        "status":"in-development"
707                    }
708                },
709                "special_api_levels": {
710                    "NEXT": {
711                        "as_u32": 4291821568,
712                        "abi_revision": "0xFF0038FA031D0347",
713                        "status": "in-development"
714                    },
715                    "HEAD": {
716                        "as_u32": 4292870144,
717                        "abi_revision": "0xFF0038FA031D0348",
718                        "status": "in-development"
719                    },
720                    "PLATFORM": {
721                        "as_u32": 4293918720,
722                        "abi_revision": "0xFF01A9328DA0C138",
723                        "status": "in-development"
724                    }
725                }
726            },
727            "schema_id": "https://fuchsia.dev/schema/version_history.json"
728        }"#;
729
730        assert_eq!(
731            parse_version_history(&expected_bytes[..]).unwrap(),
732            vec![
733                Version {
734                    api_level: 1.into(),
735                    abi_revision: 0xBEBE3F5CAAA046D2.into(),
736                    status: Status::Supported
737                },
738                Version {
739                    api_level: 2.into(),
740                    abi_revision: 0x50CBC6E8A39E1E2C.into(),
741                    status: Status::InDevelopment
742                },
743                Version {
744                    api_level: ApiLevel::NEXT,
745                    abi_revision: 0xFF0038FA031D0347.into(),
746                    status: Status::InDevelopment
747                },
748                Version {
749                    api_level: ApiLevel::HEAD,
750                    abi_revision: 0xFF0038FA031D0348.into(),
751                    status: Status::InDevelopment
752                },
753                Version {
754                    api_level: ApiLevel::PLATFORM,
755                    abi_revision: 0xFF01A9328DA0C138.into(),
756                    status: Status::InDevelopment
757                },
758            ],
759        );
760    }
761
762    #[test]
763    fn test_parse_history_rejects_invalid_schema() {
764        let expected_bytes = br#"{
765            "data": {
766                "name": "Platform version map",
767                "type": "version_history",
768                "api_levels": {},
769                "special_api_levels": {}
770            },
771            "schema_id": "some-schema"
772        }"#;
773
774        assert_eq!(
775            &parse_version_history(&expected_bytes[..]).unwrap_err().to_string(),
776            r#"expected schema_id = "https://fuchsia.dev/schema/version_history.json"; got "some-schema""#
777        );
778    }
779
780    #[test]
781    fn test_parse_history_rejects_invalid_name() {
782        let expected_bytes = br#"{
783            "data": {
784                "name": "some-name",
785                "type": "version_history",
786                "api_levels": {},
787                "special_api_levels": {}
788            },
789            "schema_id": "https://fuchsia.dev/schema/version_history.json"
790        }"#;
791
792        assert_eq!(
793            &parse_version_history(&expected_bytes[..]).unwrap_err().to_string(),
794            r#"expected data.name = "Platform version map"; got "some-name""#
795        );
796    }
797
798    #[test]
799    fn test_parse_history_rejects_invalid_type() {
800        let expected_bytes = br#"{
801            "data": {
802                "name": "Platform version map",
803                "type": "some-type",
804                "api_levels": {},
805                "special_api_levels": {}
806            },
807            "schema_id": "https://fuchsia.dev/schema/version_history.json"
808        }"#;
809
810        assert_eq!(
811            &parse_version_history(&expected_bytes[..]).unwrap_err().to_string(),
812            r#"expected data.type = "version_history"; got "some-type""#
813        );
814    }
815
816    #[test]
817    fn test_parse_history_rejects_invalid_versions() {
818        for (api_level, abi_revision, err) in [
819            (
820                "some-version",
821                "1",
822                "invalid digit found in string"                ,
823            ),
824            (
825                "-1",
826                "1",
827                "invalid digit found in string"                ,
828            ),
829            (
830                "1",
831                "some-revision",
832                "invalid value: string \"some-revision\", expected an unsigned integer at line 1 column 58",
833            ),
834            (
835                "1",
836                "-1",
837                "invalid value: string \"-1\", expected an unsigned integer at line 1 column 47",
838            ),
839        ] {
840            let expected_bytes = serde_json::to_vec(&serde_json::json!({
841                "data": {
842                    "name": VERSION_HISTORY_NAME,
843                    "type": VERSION_HISTORY_TYPE,
844                    "api_levels": {
845                        api_level:{
846                            "abi_revision": abi_revision,
847                            "status": Status::InDevelopment,
848                        }
849                    },
850                    "special_api_levels": {},
851                },
852                "schema_id": VERSION_HISTORY_SCHEMA_ID,
853            }))
854            .unwrap();
855
856            assert_eq!(parse_version_history(&expected_bytes[..]).unwrap_err().to_string(), err);
857        }
858    }
859
860    #[test]
861    fn test_parse_history_rejects_bogus_special_levels() {
862        let input = br#"{
863            "data": {
864                "name": "Platform version map",
865                "type": "version_history",
866                "api_levels": {},
867                "special_api_levels": {
868                    "WACKY_WAVING_INFLATABLE_ARM_FLAILING_TUBE_MAN": {
869                        "as_u32": 1234,
870                        "abi_revision": "0x50cbc6e8a39e1e2c",
871                        "status": "in-development"
872                    }
873                }
874            },
875            "schema_id": "https://fuchsia.dev/schema/version_history.json"
876        }"#;
877
878        assert_eq!(
879            parse_version_history(&input[..]).unwrap_err().to_string(),
880            "Unknown special API level: WACKY_WAVING_INFLATABLE_ARM_FLAILING_TUBE_MAN"
881        );
882    }
883
884    #[test]
885    fn test_parse_history_rejects_misnumbered_special_levels() {
886        let input = br#"{
887            "data": {
888                "name": "Platform version map",
889                "type": "version_history",
890                "api_levels": {},
891                "special_api_levels": {
892                    "HEAD": {
893                        "as_u32": 1234,
894                        "abi_revision": "0x50cbc6e8a39e1e2c",
895                        "status": "in-development"
896                    }
897                }
898            },
899            "schema_id": "https://fuchsia.dev/schema/version_history.json"
900        }"#;
901
902        assert_eq!(
903            parse_version_history(&input[..]).unwrap_err().to_string(),
904            "Special API level HEAD had unexpected numerical value 1234 (Expected 4292870144)"
905        );
906    }
907
908    #[test]
909    fn test_serialize_abi_revision() {
910        {
911            let abi = AbiRevision::from_u64(16);
912            let str = "\"0x0000000000000010\"";
913            assert_eq!(serde_json::to_string(&abi).unwrap(), str);
914            assert_eq!(serde_json::from_str::<AbiRevision>(str).unwrap(), abi);
915        }
916        {
917            let abi = AbiRevision::from_u64(0x58ea445e942a0004);
918            let str = "\"0x58ea445e942a0004\"";
919            assert_eq!(serde_json::to_string(&abi).unwrap(), str);
920            assert_eq!(serde_json::from_str::<AbiRevision>(str).unwrap(), abi);
921        }
922    }
923
924    pub const FAKE_VERSION_HISTORY: VersionHistory = VersionHistory {
925        versions: &[
926            Version {
927                api_level: ApiLevel::from_u32(4),
928                abi_revision: AbiRevision::from_u64(0x58ea445e942a0004),
929                status: Status::Unsupported,
930            },
931            Version {
932                api_level: ApiLevel::from_u32(5),
933                abi_revision: AbiRevision::from_u64(0x58ea445e942a0005),
934                status: Status::Supported,
935            },
936            Version {
937                api_level: ApiLevel::from_u32(6),
938                abi_revision: AbiRevision::from_u64(0x58ea445e942a0006),
939                status: Status::Supported,
940            },
941            Version {
942                api_level: ApiLevel::from_u32(7),
943                abi_revision: AbiRevision::from_u64(0x58ea445e942a0007),
944                status: Status::Supported,
945            },
946            Version {
947                api_level: ApiLevel::NEXT,
948                abi_revision: AbiRevision::from_u64(0xFF00_8C4D_000B_4751),
949                status: Status::InDevelopment,
950            },
951            Version {
952                api_level: ApiLevel::HEAD,
953                abi_revision: AbiRevision::from_u64(0xFF00_8C4D_000B_4751),
954                status: Status::InDevelopment,
955            },
956            Version {
957                api_level: ApiLevel::PLATFORM,
958                abi_revision: AbiRevision::from_u64(0xFF01_8C4D_000B_4751),
959                status: Status::InDevelopment,
960            },
961        ],
962    };
963
964    #[test]
965    fn test_check_abi_revision() {
966        let supported_versions =
967            VersionVec(FAKE_VERSION_HISTORY.versions[1..4].iter().cloned().collect());
968
969        assert_eq!(
970            FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0xFF01_ABCD_000B_2224.into()),
971            Err(AbiRevisionError::PlatformMismatch {
972                abi_revision: 0xFF01_ABCD_000B_2224.into(),
973                package_date: NaiveDate::from_ymd_opt(1998, 9, 4).unwrap(),
974                package_commit_hash: 0xABCD,
975                platform_date: NaiveDate::from_ymd_opt(2024, 9, 24).unwrap(),
976                platform_commit_hash: 0x8C4D,
977            })
978        );
979
980        assert_eq!(
981            FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0xFF00_ABCD_000B_2224.into()),
982            Err(AbiRevisionError::UnstableMismatch {
983                abi_revision: 0xFF00_ABCD_000B_2224.into(),
984                package_sdk_date: NaiveDate::from_ymd_opt(1998, 9, 4).unwrap(),
985                package_sdk_commit_hash: 0xABCD,
986                platform_date: NaiveDate::from_ymd_opt(2024, 9, 24).unwrap(),
987                platform_commit_hash: 0x8C4D,
988                supported_versions: supported_versions.clone()
989            })
990        );
991
992        assert_eq!(
993            FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0x1234.into()),
994            Err(AbiRevisionError::TooNew {
995                abi_revision: 0x1234.into(),
996                supported_versions: supported_versions.clone()
997            })
998        );
999
1000        assert_eq!(
1001            FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0x58ea445e942a0004.into()),
1002            Err(AbiRevisionError::Retired {
1003                version: FAKE_VERSION_HISTORY.versions[0].clone(),
1004
1005                supported_versions: supported_versions.clone()
1006            })
1007        );
1008
1009        assert_eq!(
1010            FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(AbiRevision::INVALID),
1011            Err(AbiRevisionError::Invalid)
1012        );
1013
1014        assert_eq!(
1015            FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0xFF02_0000_0000_0000.into()),
1016            Err(AbiRevisionError::Malformed { abi_revision: 0xFF02_0000_0000_0000.into() })
1017        );
1018
1019        FAKE_VERSION_HISTORY
1020            .check_abi_revision_for_runtime(0x58ea445e942a0005.into())
1021            .expect("level 5 should be supported");
1022        FAKE_VERSION_HISTORY
1023            .check_abi_revision_for_runtime(0x58ea445e942a0007.into())
1024            .expect("level 7 should be supported");
1025    }
1026
1027    #[test]
1028    fn test_pretty_print_abi_error() {
1029        let supported_versions =
1030            VersionVec(FAKE_VERSION_HISTORY.versions[1..4].iter().cloned().collect());
1031
1032        assert_eq!(
1033            AbiRevisionError::PlatformMismatch {
1034                abi_revision: 0xFF01_ABCD_000B_2224.into(),
1035                package_date: NaiveDate::from_ymd_opt(1998, 9, 4).unwrap(),
1036                package_commit_hash: 0xABCD,
1037                platform_date: NaiveDate::from_ymd_opt(2024, 9, 24).unwrap(),
1038                platform_commit_hash: 0x8C4D,
1039            }
1040            .to_string(),
1041            "Unknown platform ABI revision: 0xff01abcd000b2224.
1042Found platform component from 1998-09-04 (Git hash abcd).
1043Platform components must be from 2024-09-24 (Git hash 8c4d)"
1044        );
1045
1046        assert_eq!(
1047            AbiRevisionError::UnstableMismatch {
1048                abi_revision: 0xFF00_ABCD_000B_2224.into(),
1049                package_sdk_date: NaiveDate::from_ymd_opt(1998, 9, 4).unwrap(),
1050                package_sdk_commit_hash: 0xABCD,
1051                platform_date: NaiveDate::from_ymd_opt(2024, 9, 24).unwrap(),
1052                platform_commit_hash: 0x8C4D,
1053                supported_versions: supported_versions.clone()
1054            }
1055            .to_string(),
1056            "Unknown NEXT or HEAD ABI revision: 0xff00abcd000b2224.
1057SDK is from 1998-09-04 (Git hash abcd).
1058Expected 2024-09-24 (Git hash 8c4d)
1059The following API levels are stable and supported:
1060└── 5 (0x58ea445e942a0005)
1061└── 6 (0x58ea445e942a0006)
1062└── 7 (0x58ea445e942a0007)"
1063        );
1064
1065        assert_eq!(
1066            AbiRevisionError::TooNew {
1067                abi_revision: 0x1234.into(),
1068                supported_versions: supported_versions.clone()
1069            }
1070            .to_string(),
1071            "Unknown ABI revision 0x1234. It was probably created after
1072this platform or tool was built. The following API levels are stable and
1073supported:
1074└── 5 (0x58ea445e942a0005)
1075└── 6 (0x58ea445e942a0006)
1076└── 7 (0x58ea445e942a0007)"
1077        );
1078        assert_eq!(
1079            AbiRevisionError::Retired {
1080                version: FAKE_VERSION_HISTORY.versions[0].clone(),
1081                supported_versions: supported_versions.clone()
1082            }
1083            .to_string(),
1084            "Retired API level 4 (0x58ea445e942a0004) cannot run.
1085The following API levels are stable and supported:
1086└── 5 (0x58ea445e942a0005)
1087└── 6 (0x58ea445e942a0006)
1088└── 7 (0x58ea445e942a0007)"
1089        );
1090
1091        assert_eq!(
1092            AbiRevisionError::Invalid.to_string(),
1093            "Invalid ABI revision 0xffffffffffffffff."
1094        );
1095
1096        assert_eq!(
1097            AbiRevisionError::Malformed { abi_revision: 0xFF02_0000_0000_0000.into() }.to_string(),
1098            "ABI revision 0xff02000000000000 has an unrecognized format."
1099        );
1100    }
1101
1102    #[test]
1103    fn test_pretty_print_api_error() {
1104        let supported: Vec<ApiLevel> =
1105            vec![5.into(), 6.into(), 7.into(), ApiLevel::NEXT, ApiLevel::HEAD, ApiLevel::PLATFORM];
1106
1107        assert_eq!(
1108            ApiLevelError::Unknown { api_level: 42.into(), supported: supported.clone() }
1109                .to_string(),
1110            "Unknown target API level: 42. Is the SDK too old to support it?
1111The following API levels are supported: 5, 6, 7, NEXT, HEAD, PLATFORM",
1112        );
1113        assert_eq!(
1114            ApiLevelError::Unsupported {
1115                version: FAKE_VERSION_HISTORY.versions[0].clone(),
1116                supported: supported.clone()
1117            }
1118            .to_string(),
1119            "The SDK no longer supports API level 4.
1120The following API levels are supported: 5, 6, 7, NEXT, HEAD, PLATFORM"
1121        );
1122    }
1123
1124    #[test]
1125    fn test_check_api_level() {
1126        let supported: Vec<ApiLevel> =
1127            vec![5.into(), 6.into(), 7.into(), ApiLevel::NEXT, ApiLevel::HEAD, ApiLevel::PLATFORM];
1128
1129        assert_eq!(
1130            FAKE_VERSION_HISTORY.check_api_level_for_build(42.into()),
1131            Err(ApiLevelError::Unknown { api_level: 42.into(), supported: supported.clone() })
1132        );
1133
1134        assert_eq!(
1135            FAKE_VERSION_HISTORY.check_api_level_for_build(4.into()),
1136            Err(ApiLevelError::Unsupported {
1137                version: FAKE_VERSION_HISTORY.versions[0].clone(),
1138                supported: supported.clone()
1139            })
1140        );
1141        assert_eq!(
1142            FAKE_VERSION_HISTORY.check_api_level_for_build(6.into()),
1143            Ok(0x58ea445e942a0006.into())
1144        );
1145        assert_eq!(
1146            FAKE_VERSION_HISTORY.check_api_level_for_build(7.into()),
1147            Ok(0x58ea445e942a0007.into())
1148        );
1149        assert_eq!(
1150            FAKE_VERSION_HISTORY.check_api_level_for_build(ApiLevel::NEXT),
1151            Ok(0xFF00_8C4D_000B_4751.into())
1152        );
1153        assert_eq!(
1154            FAKE_VERSION_HISTORY.check_api_level_for_build(ApiLevel::HEAD),
1155            Ok(0xFF00_8C4D_000B_4751.into())
1156        );
1157        assert_eq!(
1158            FAKE_VERSION_HISTORY.check_api_level_for_build(ApiLevel::PLATFORM),
1159            Ok(0xFF01_8C4D_000B_4751.into())
1160        );
1161    }
1162
1163    #[test]
1164    fn test_various_getters() {
1165        assert_eq!(
1166            FAKE_VERSION_HISTORY.get_abi_revision_for_platform_components(),
1167            0xFF01_8C4D_000B_4751.into()
1168        );
1169        assert_eq!(
1170            FAKE_VERSION_HISTORY.get_misleading_version_for_ffx(),
1171            FAKE_VERSION_HISTORY.versions[3].clone()
1172        );
1173        assert_eq!(FAKE_VERSION_HISTORY.get_misleading_version_for_ffx().api_level, 7.into());
1174        assert_eq!(
1175            FAKE_VERSION_HISTORY.get_example_supported_version_for_tests(),
1176            FAKE_VERSION_HISTORY.versions[FAKE_VERSION_HISTORY.versions.len() - 4].clone()
1177        );
1178        assert_eq!(
1179            FAKE_VERSION_HISTORY.get_example_supported_version_for_tests().api_level,
1180            7.into()
1181        );
1182    }
1183
1184    #[test]
1185    fn test_explanations() {
1186        let exp = |abi_revision| AbiRevision::from_u64(abi_revision).explanation();
1187
1188        assert_eq!(exp(0x1234_5678_9abc_deff), AbiRevisionExplanation::Normal);
1189        assert_eq!(
1190            exp(0xFF01_abcd_00ab_eeee),
1191            AbiRevisionExplanation::Platform {
1192                date: chrono::NaiveDate::from_num_days_from_ce_opt(0xab_eeee).unwrap(),
1193                git_revision: 0xabcd
1194            }
1195        );
1196        assert_eq!(
1197            exp(0xFF00_1234_0012_3456),
1198            AbiRevisionExplanation::Unstable {
1199                date: chrono::NaiveDate::from_num_days_from_ce_opt(0x12_3456).unwrap(),
1200                git_revision: 0x1234
1201            }
1202        );
1203        // 0xabcd_9876 is too large a date for chrono. Make sure we return a
1204        // null date, rather than crashing.
1205        assert_eq!(
1206            exp(0xFF00_1234_abcd_9876),
1207            AbiRevisionExplanation::Unstable {
1208                date: chrono::NaiveDate::default(),
1209                git_revision: 0x1234
1210            }
1211        );
1212        assert_eq!(
1213            exp(0xFF01_1234_abcd_9876),
1214            AbiRevisionExplanation::Platform {
1215                date: chrono::NaiveDate::default(),
1216                git_revision: 0x1234
1217            }
1218        );
1219        assert_eq!(exp(0xFFFF_FFFF_FFFF_FFFF), AbiRevisionExplanation::Invalid);
1220        assert_eq!(exp(0xFFFF_FFFF_FFFF_FFFE), AbiRevisionExplanation::Malformed);
1221    }
1222}