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 From<u64> for AbiRevision {
240    fn from(abi_revision: u64) -> AbiRevision {
241        AbiRevision(abi_revision)
242    }
243}
244
245impl From<&u64> for AbiRevision {
246    fn from(abi_revision: &u64) -> AbiRevision {
247        AbiRevision(*abi_revision)
248    }
249}
250
251impl From<AbiRevision> for u64 {
252    fn from(abi_revision: AbiRevision) -> u64 {
253        abi_revision.0
254    }
255}
256
257impl From<[u8; 8]> for AbiRevision {
258    fn from(abi_revision: [u8; 8]) -> AbiRevision {
259        AbiRevision::from_bytes(abi_revision)
260    }
261}
262
263impl TryFrom<&[u8]> for AbiRevision {
264    type Error = TryFromSliceError;
265
266    fn try_from(abi_revision: &[u8]) -> Result<AbiRevision, Self::Error> {
267        let abi_revision: [u8; 8] = abi_revision.try_into()?;
268        Ok(AbiRevision::from_bytes(abi_revision))
269    }
270}
271
272/// Version represents an API level of the Fuchsia platform API.
273///
274/// See
275/// https://fuchsia.dev/fuchsia-src/contribute/governance/rfcs/0239_platform_versioning_in_practice
276/// for more details.
277#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone)]
278pub struct Version {
279    /// The number associated with this API level.
280    pub api_level: ApiLevel,
281
282    /// The ABI revision associated with this API level.
283    pub abi_revision: AbiRevision,
284
285    /// The Status denotes the current status of the API level.
286    pub status: Status,
287}
288
289impl Version {
290    /// Returns true if this version is runnable - that is, whether components
291    /// targeting this version will be able to run on this device.
292    fn is_runnable(&self) -> bool {
293        match self.status {
294            Status::InDevelopment | Status::Supported => true,
295            Status::Unsupported => false,
296        }
297    }
298}
299
300#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, PartialOrd, Ord, Hash)]
301pub enum Status {
302    #[serde(rename = "in-development")]
303    InDevelopment,
304    #[serde(rename = "supported")]
305    Supported,
306    #[serde(rename = "unsupported")]
307    Unsupported,
308}
309
310/// VersionHistory stores the history of Fuchsia API levels, and lets callers
311/// query the support status of API levels and ABI revisions.
312#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
313pub struct VersionHistory {
314    versions: &'static [Version],
315}
316
317impl VersionHistory {
318    /// Outside of tests, callers should use the static [HISTORY]
319    /// instance. However, for testing purposes, you may want to build your own
320    /// hermetic "alternate history" that doesn't change over time.
321    ///
322    /// If you're not testing versioning in particular, and you just want an API
323    /// level/ABI revision that works, see
324    /// [get_example_supported_version_for_tests].
325    pub const fn new(versions: &'static [Version]) -> Self {
326        VersionHistory { versions }
327    }
328
329    /// The ABI revision for components and packages that are "part of the
330    /// platform" and never "move between releases". For example:
331    ///
332    /// - Packages produced by the platform build have this ABI revision.
333    /// - Components which are not packaged but are "part of the platform"
334    ///   nonetheless (e.g. components loaded from bootfs) have this ABI
335    ///   revision.
336    /// - Most packages produced by assembly tools have this ABI revision.
337    ///   - The `update` package is a noteworthy exception, since it "moves
338    ///     between releases", in that it is produced by assembly tools from one
339    ///     Fuchsia release, and then later read by the OS from a previous
340    ///     release (that is, the one performing the update).
341    pub fn get_abi_revision_for_platform_components(&self) -> AbiRevision {
342        self.version_from_api_level(ApiLevel::PLATFORM)
343            .expect("API Level PLATFORM not found!")
344            .abi_revision
345    }
346
347    /// ffx currently presents information suggesting that the platform supports
348    /// a _single_ API level and ABI revision. This is misleading and should be
349    /// fixed. Until we do, we have to choose a particular Version for ffx to
350    /// present.
351    ///
352    /// TODO: https://fxbug.dev/326096999 - Remove this, or turn it into
353    /// something that makes more sense.
354    pub fn get_misleading_version_for_ffx(&self) -> Version {
355        self.runnable_versions()
356            .filter(|v| {
357                v.api_level != ApiLevel::NEXT
358                    && v.api_level != ApiLevel::HEAD
359                    && v.api_level != ApiLevel::PLATFORM
360            })
361            .last()
362            .unwrap()
363    }
364
365    /// API level to be used in tests that create components on the fly and need
366    /// to specify a supported API level or ABI revision, but don't particularly
367    /// care which. The returned [Version] will be consistent within a given
368    /// build, but may change from build to build.
369    pub fn get_example_supported_version_for_tests(&self) -> Version {
370        self.runnable_versions()
371            .filter(|v| {
372                v.api_level != ApiLevel::NEXT
373                    && v.api_level != ApiLevel::HEAD
374                    && v.api_level != ApiLevel::PLATFORM
375            })
376            .last()
377            .unwrap()
378    }
379
380    /// Check whether the platform supports building components that target the
381    /// given API level, and if so, returns the ABI revision associated with
382    /// that API level.
383    pub fn check_api_level_for_build(
384        &self,
385        api_level: ApiLevel,
386    ) -> Result<AbiRevision, ApiLevelError> {
387        let Some(version) = self.version_from_api_level(api_level) else {
388            return Err(ApiLevelError::Unknown {
389                api_level,
390                supported: self.runnable_versions().map(|v| v.api_level).collect(),
391            });
392        };
393
394        if version.is_runnable() {
395            Ok(version.abi_revision)
396        } else {
397            Err(ApiLevelError::Unsupported {
398                version,
399                supported: self.runnable_versions().map(|v| v.api_level).collect(),
400            })
401        }
402    }
403
404    /// Check whether the operating system supports running components that
405    /// target the given ABI revision.
406    pub fn check_abi_revision_for_runtime(
407        &self,
408        abi_revision: AbiRevision,
409    ) -> Result<(), AbiRevisionError> {
410        if let Some(version) = self.version_from_abi_revision(abi_revision) {
411            if version.is_runnable() {
412                Ok(())
413            } else {
414                Err(AbiRevisionError::Retired {
415                    version,
416                    supported_versions: self.supported_versions(),
417                })
418            }
419        } else {
420            // We don't recognize this ABI revision... Look at its structure to
421            // understand what's going on.
422            match abi_revision.explanation() {
423                AbiRevisionExplanation::Platform { date, git_revision } => {
424                    let (platform_date, platform_commit_hash) = self.platform_abi_info();
425                    Err(AbiRevisionError::PlatformMismatch {
426                        abi_revision,
427                        package_date: date,
428                        package_commit_hash: git_revision,
429                        platform_date,
430                        platform_commit_hash,
431                    })
432                }
433                AbiRevisionExplanation::Unstable { date, git_revision } => {
434                    let (platform_date, platform_commit_hash) = self.platform_abi_info();
435                    Err(AbiRevisionError::UnstableMismatch {
436                        abi_revision,
437                        package_sdk_date: date,
438                        package_sdk_commit_hash: git_revision,
439                        platform_date,
440                        platform_commit_hash,
441                        supported_versions: self.supported_versions(),
442                    })
443                }
444                AbiRevisionExplanation::Malformed => {
445                    Err(AbiRevisionError::Malformed { abi_revision })
446                }
447                AbiRevisionExplanation::Invalid => Err(AbiRevisionError::Invalid),
448                AbiRevisionExplanation::Normal => Err(AbiRevisionError::TooNew {
449                    abi_revision,
450                    supported_versions: self.supported_versions(),
451                }),
452            }
453        }
454    }
455
456    fn runnable_versions(&self) -> impl Iterator<Item = Version> + '_ {
457        self.versions.iter().filter(|v| v.is_runnable()).cloned()
458    }
459
460    fn supported_versions(&self) -> VersionVec {
461        VersionVec(
462            self.versions
463                .iter()
464                .filter(|v| match v.status {
465                    Status::InDevelopment => false,
466                    Status::Supported => true,
467                    Status::Unsupported => false,
468                })
469                .cloned()
470                .collect(),
471        )
472    }
473
474    fn version_from_abi_revision(&self, abi_revision: AbiRevision) -> Option<Version> {
475        self.versions.iter().find(|v| v.abi_revision == abi_revision).cloned()
476    }
477
478    pub fn version_from_api_level(&self, api_level: ApiLevel) -> Option<Version> {
479        self.versions.iter().find(|v| v.api_level == api_level).cloned()
480    }
481
482    fn platform_abi_info(&self) -> (NaiveDate, u16) {
483        if let Some(platform_version) = self.version_from_api_level(ApiLevel::PLATFORM) {
484            if let AbiRevisionExplanation::Platform { date, git_revision } =
485                platform_version.abi_revision.explanation()
486            {
487                return (date, git_revision);
488            }
489        }
490        error!(
491            "No PLATFORM API level found. This should never happen.
492Returning bogus data instead of panicking."
493        );
494        (NaiveDate::default(), 0)
495    }
496}
497
498#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
499pub enum AbiRevisionError {
500    #[error(
501        "Unknown platform ABI revision: 0x{abi_revision}.
502Found platform component from {package_date} (Git hash {package_commit_hash:x}).
503Platform components must be from {platform_date} (Git hash {platform_commit_hash:x})"
504    )]
505    PlatformMismatch {
506        abi_revision: AbiRevision,
507        package_date: NaiveDate,
508        package_commit_hash: u16,
509        platform_date: NaiveDate,
510        platform_commit_hash: u16,
511    },
512    #[error(
513        "Unknown NEXT or HEAD ABI revision: 0x{abi_revision}.
514SDK is from {package_sdk_date} (Git hash {package_sdk_commit_hash:x}).
515Expected {platform_date} (Git hash {platform_commit_hash:x})
516The following API levels are stable and supported:{supported_versions}"
517    )]
518    UnstableMismatch {
519        abi_revision: AbiRevision,
520        package_sdk_date: NaiveDate,
521        package_sdk_commit_hash: u16,
522        platform_date: NaiveDate,
523        platform_commit_hash: u16,
524        supported_versions: VersionVec,
525    },
526
527    #[error(
528        "Unknown ABI revision 0x{abi_revision}. It was probably created after
529this platform or tool was built. The following API levels are stable and
530supported:{supported_versions}"
531    )]
532    TooNew { abi_revision: AbiRevision, supported_versions: VersionVec },
533
534    #[error("Retired API level {} (0x{}) cannot run.
535The following API levels are stable and supported:{supported_versions}",
536        .version.api_level, .version.abi_revision)]
537    Retired { version: Version, supported_versions: VersionVec },
538
539    #[error("Invalid ABI revision 0x{}.", AbiRevision::INVALID)]
540    Invalid,
541
542    #[error("ABI revision 0x{abi_revision} has an unrecognized format.")]
543    Malformed { abi_revision: AbiRevision },
544}
545
546/// Wrapper for a vector of Versions with a nice implementation of Display.
547#[derive(Debug, Clone, PartialEq, Eq)]
548pub struct VersionVec(pub Vec<Version>);
549
550impl std::fmt::Display for VersionVec {
551    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
552        for version in self.0.iter() {
553            write!(f, "\n└── {} (0x{})", version.api_level, version.abi_revision)?;
554        }
555        Ok(())
556    }
557}
558
559#[derive(Debug, Clone, PartialEq, Eq)]
560pub enum ApiLevelError {
561    /// The user attempted to build a component targeting an API level that was
562    /// not recognized.
563    Unknown { api_level: ApiLevel, supported: Vec<ApiLevel> },
564
565    /// The user attempted to build a component targeting an API level that was
566    /// recognized, but is not supported for building.
567    Unsupported { version: Version, supported: Vec<ApiLevel> },
568}
569
570impl std::error::Error for ApiLevelError {}
571
572impl std::fmt::Display for ApiLevelError {
573    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
574        match self {
575            ApiLevelError::Unknown { api_level, supported } => {
576                write!(
577                    f,
578                    "Unknown target API level: {}. Is the SDK too old to support it?
579The following API levels are supported: {}",
580                    api_level,
581                    supported.iter().join(", ")
582                )
583            }
584            ApiLevelError::Unsupported { version, supported } => {
585                write!(
586                    f,
587                    "The SDK no longer supports API level {}.
588The following API levels are supported: {}",
589                    version.api_level,
590                    supported.iter().join(", ")
591                )
592            }
593        }
594    }
595}
596
597#[derive(Deserialize)]
598struct VersionHistoryDataJson {
599    name: String,
600    #[serde(rename = "type")]
601    element_type: String,
602    api_levels: BTreeMap<String, ApiLevelJson>,
603    special_api_levels: BTreeMap<String, SpecialApiLevelJson>,
604}
605
606#[derive(Deserialize)]
607struct VersionHistoryJson {
608    schema_id: String,
609    data: VersionHistoryDataJson,
610}
611
612#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deserialize)]
613struct ApiLevelJson {
614    pub abi_revision: AbiRevision,
615    pub status: Status,
616}
617
618#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deserialize)]
619struct SpecialApiLevelJson {
620    pub as_u32: u32,
621    pub abi_revision: AbiRevision,
622    pub status: Status,
623}
624
625pub fn parse_version_history(bytes: &[u8]) -> anyhow::Result<Vec<Version>> {
626    let v: VersionHistoryJson = serde_json::from_slice(bytes)?;
627
628    if v.schema_id != VERSION_HISTORY_SCHEMA_ID {
629        bail!("expected schema_id = {:?}; got {:?}", VERSION_HISTORY_SCHEMA_ID, v.schema_id)
630    }
631    if v.data.name != VERSION_HISTORY_NAME {
632        bail!("expected data.name = {:?}; got {:?}", VERSION_HISTORY_NAME, v.data.name)
633    }
634    if v.data.element_type != VERSION_HISTORY_TYPE {
635        bail!("expected data.type = {:?}; got {:?}", VERSION_HISTORY_TYPE, v.data.element_type,)
636    }
637
638    let mut versions = Vec::new();
639
640    for (key, value) in v.data.api_levels {
641        versions.push(Version {
642            api_level: key.parse()?,
643            abi_revision: value.abi_revision,
644            status: value.status,
645        });
646    }
647
648    for (key, value) in v.data.special_api_levels {
649        let api_level: ApiLevel =
650            key.parse().with_context(|| format!("Unknown special API level: {}", key))?;
651        if api_level.as_u32() != value.as_u32 {
652            bail!(
653                "Special API level {} had unexpected numerical value {} (Expected {})",
654                api_level,
655                value.as_u32,
656                api_level.as_u32()
657            )
658        }
659        versions.push(Version {
660            api_level,
661            abi_revision: value.abi_revision,
662            status: value.status,
663        });
664    }
665
666    versions.sort_by_key(|s| s.api_level);
667
668    let Some(latest_api_version) = versions.last() else {
669        bail!("there must be at least one API level")
670    };
671
672    if latest_api_version.status == Status::Unsupported {
673        bail!("most recent API level must not be 'unsupported'")
674    }
675
676    Ok(versions)
677}
678
679#[cfg(test)]
680mod tests {
681    use super::*;
682
683    #[test]
684    fn test_parse_history_works() {
685        let expected_bytes = br#"{
686            "data": {
687                "name": "Platform version map",
688                "type": "version_history",
689                "api_levels": {
690                    "1":{
691                        "abi_revision":"0xBEBE3F5CAAA046D2",
692                        "status":"supported"
693                    },
694                    "2":{
695                        "abi_revision":"0x50cbc6e8a39e1e2c",
696                        "status":"in-development"
697                    }
698                },
699                "special_api_levels": {
700                    "NEXT": {
701                        "as_u32": 4291821568,
702                        "abi_revision": "0xFF0038FA031D0347",
703                        "status": "in-development"
704                    },
705                    "HEAD": {
706                        "as_u32": 4292870144,
707                        "abi_revision": "0xFF0038FA031D0348",
708                        "status": "in-development"
709                    },
710                    "PLATFORM": {
711                        "as_u32": 4293918720,
712                        "abi_revision": "0xFF01A9328DA0C138",
713                        "status": "in-development"
714                    }
715                }
716            },
717            "schema_id": "https://fuchsia.dev/schema/version_history.json"
718        }"#;
719
720        assert_eq!(
721            parse_version_history(&expected_bytes[..]).unwrap(),
722            vec![
723                Version {
724                    api_level: 1.into(),
725                    abi_revision: 0xBEBE3F5CAAA046D2.into(),
726                    status: Status::Supported
727                },
728                Version {
729                    api_level: 2.into(),
730                    abi_revision: 0x50CBC6E8A39E1E2C.into(),
731                    status: Status::InDevelopment
732                },
733                Version {
734                    api_level: ApiLevel::NEXT,
735                    abi_revision: 0xFF0038FA031D0347.into(),
736                    status: Status::InDevelopment
737                },
738                Version {
739                    api_level: ApiLevel::HEAD,
740                    abi_revision: 0xFF0038FA031D0348.into(),
741                    status: Status::InDevelopment
742                },
743                Version {
744                    api_level: ApiLevel::PLATFORM,
745                    abi_revision: 0xFF01A9328DA0C138.into(),
746                    status: Status::InDevelopment
747                },
748            ],
749        );
750    }
751
752    #[test]
753    fn test_parse_history_rejects_invalid_schema() {
754        let expected_bytes = br#"{
755            "data": {
756                "name": "Platform version map",
757                "type": "version_history",
758                "api_levels": {},
759                "special_api_levels": {}
760            },
761            "schema_id": "some-schema"
762        }"#;
763
764        assert_eq!(
765            &parse_version_history(&expected_bytes[..]).unwrap_err().to_string(),
766            r#"expected schema_id = "https://fuchsia.dev/schema/version_history.json"; got "some-schema""#
767        );
768    }
769
770    #[test]
771    fn test_parse_history_rejects_invalid_name() {
772        let expected_bytes = br#"{
773            "data": {
774                "name": "some-name",
775                "type": "version_history",
776                "api_levels": {},
777                "special_api_levels": {}
778            },
779            "schema_id": "https://fuchsia.dev/schema/version_history.json"
780        }"#;
781
782        assert_eq!(
783            &parse_version_history(&expected_bytes[..]).unwrap_err().to_string(),
784            r#"expected data.name = "Platform version map"; got "some-name""#
785        );
786    }
787
788    #[test]
789    fn test_parse_history_rejects_invalid_type() {
790        let expected_bytes = br#"{
791            "data": {
792                "name": "Platform version map",
793                "type": "some-type",
794                "api_levels": {},
795                "special_api_levels": {}
796            },
797            "schema_id": "https://fuchsia.dev/schema/version_history.json"
798        }"#;
799
800        assert_eq!(
801            &parse_version_history(&expected_bytes[..]).unwrap_err().to_string(),
802            r#"expected data.type = "version_history"; got "some-type""#
803        );
804    }
805
806    #[test]
807    fn test_parse_history_rejects_invalid_versions() {
808        for (api_level, abi_revision, err) in [
809            (
810                "some-version",
811                "1",
812                "invalid digit found in string"                ,
813            ),
814            (
815                "-1",
816                "1",
817                "invalid digit found in string"                ,
818            ),
819            (
820                "1",
821                "some-revision",
822                "invalid value: string \"some-revision\", expected an unsigned integer at line 1 column 58",
823            ),
824            (
825                "1",
826                "-1",
827                "invalid value: string \"-1\", expected an unsigned integer at line 1 column 47",
828            ),
829        ] {
830            let expected_bytes = serde_json::to_vec(&serde_json::json!({
831                "data": {
832                    "name": VERSION_HISTORY_NAME,
833                    "type": VERSION_HISTORY_TYPE,
834                    "api_levels": {
835                        api_level:{
836                            "abi_revision": abi_revision,
837                            "status": Status::InDevelopment,
838                        }
839                    },
840                    "special_api_levels": {},
841                },
842                "schema_id": VERSION_HISTORY_SCHEMA_ID,
843            }))
844            .unwrap();
845
846            assert_eq!(parse_version_history(&expected_bytes[..]).unwrap_err().to_string(), err);
847        }
848    }
849
850    #[test]
851    fn test_parse_history_rejects_bogus_special_levels() {
852        let input = br#"{
853            "data": {
854                "name": "Platform version map",
855                "type": "version_history",
856                "api_levels": {},
857                "special_api_levels": {
858                    "WACKY_WAVING_INFLATABLE_ARM_FLAILING_TUBE_MAN": {
859                        "as_u32": 1234,
860                        "abi_revision": "0x50cbc6e8a39e1e2c",
861                        "status": "in-development"
862                    }
863                }
864            },
865            "schema_id": "https://fuchsia.dev/schema/version_history.json"
866        }"#;
867
868        assert_eq!(
869            parse_version_history(&input[..]).unwrap_err().to_string(),
870            "Unknown special API level: WACKY_WAVING_INFLATABLE_ARM_FLAILING_TUBE_MAN"
871        );
872    }
873
874    #[test]
875    fn test_parse_history_rejects_misnumbered_special_levels() {
876        let input = br#"{
877            "data": {
878                "name": "Platform version map",
879                "type": "version_history",
880                "api_levels": {},
881                "special_api_levels": {
882                    "HEAD": {
883                        "as_u32": 1234,
884                        "abi_revision": "0x50cbc6e8a39e1e2c",
885                        "status": "in-development"
886                    }
887                }
888            },
889            "schema_id": "https://fuchsia.dev/schema/version_history.json"
890        }"#;
891
892        assert_eq!(
893            parse_version_history(&input[..]).unwrap_err().to_string(),
894            "Special API level HEAD had unexpected numerical value 1234 (Expected 4292870144)"
895        );
896    }
897
898    pub const FAKE_VERSION_HISTORY: VersionHistory = VersionHistory {
899        versions: &[
900            Version {
901                api_level: ApiLevel::from_u32(4),
902                abi_revision: AbiRevision::from_u64(0x58ea445e942a0004),
903                status: Status::Unsupported,
904            },
905            Version {
906                api_level: ApiLevel::from_u32(5),
907                abi_revision: AbiRevision::from_u64(0x58ea445e942a0005),
908                status: Status::Supported,
909            },
910            Version {
911                api_level: ApiLevel::from_u32(6),
912                abi_revision: AbiRevision::from_u64(0x58ea445e942a0006),
913                status: Status::Supported,
914            },
915            Version {
916                api_level: ApiLevel::from_u32(7),
917                abi_revision: AbiRevision::from_u64(0x58ea445e942a0007),
918                status: Status::Supported,
919            },
920            Version {
921                api_level: ApiLevel::NEXT,
922                abi_revision: AbiRevision::from_u64(0xFF00_8C4D_000B_4751),
923                status: Status::InDevelopment,
924            },
925            Version {
926                api_level: ApiLevel::HEAD,
927                abi_revision: AbiRevision::from_u64(0xFF00_8C4D_000B_4751),
928                status: Status::InDevelopment,
929            },
930            Version {
931                api_level: ApiLevel::PLATFORM,
932                abi_revision: AbiRevision::from_u64(0xFF01_8C4D_000B_4751),
933                status: Status::InDevelopment,
934            },
935        ],
936    };
937
938    #[test]
939    fn test_check_abi_revision() {
940        let supported_versions =
941            VersionVec(FAKE_VERSION_HISTORY.versions[1..4].iter().cloned().collect());
942
943        assert_eq!(
944            FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0xFF01_ABCD_000B_2224.into()),
945            Err(AbiRevisionError::PlatformMismatch {
946                abi_revision: 0xFF01_ABCD_000B_2224.into(),
947                package_date: NaiveDate::from_ymd_opt(1998, 9, 4).unwrap(),
948                package_commit_hash: 0xABCD,
949                platform_date: NaiveDate::from_ymd_opt(2024, 9, 24).unwrap(),
950                platform_commit_hash: 0x8C4D,
951            })
952        );
953
954        assert_eq!(
955            FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0xFF00_ABCD_000B_2224.into()),
956            Err(AbiRevisionError::UnstableMismatch {
957                abi_revision: 0xFF00_ABCD_000B_2224.into(),
958                package_sdk_date: NaiveDate::from_ymd_opt(1998, 9, 4).unwrap(),
959                package_sdk_commit_hash: 0xABCD,
960                platform_date: NaiveDate::from_ymd_opt(2024, 9, 24).unwrap(),
961                platform_commit_hash: 0x8C4D,
962                supported_versions: supported_versions.clone()
963            })
964        );
965
966        assert_eq!(
967            FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0x1234.into()),
968            Err(AbiRevisionError::TooNew {
969                abi_revision: 0x1234.into(),
970                supported_versions: supported_versions.clone()
971            })
972        );
973
974        assert_eq!(
975            FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0x58ea445e942a0004.into()),
976            Err(AbiRevisionError::Retired {
977                version: FAKE_VERSION_HISTORY.versions[0].clone(),
978
979                supported_versions: supported_versions.clone()
980            })
981        );
982
983        assert_eq!(
984            FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(AbiRevision::INVALID),
985            Err(AbiRevisionError::Invalid)
986        );
987
988        assert_eq!(
989            FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0xFF02_0000_0000_0000.into()),
990            Err(AbiRevisionError::Malformed { abi_revision: 0xFF02_0000_0000_0000.into() })
991        );
992
993        FAKE_VERSION_HISTORY
994            .check_abi_revision_for_runtime(0x58ea445e942a0005.into())
995            .expect("level 5 should be supported");
996        FAKE_VERSION_HISTORY
997            .check_abi_revision_for_runtime(0x58ea445e942a0007.into())
998            .expect("level 7 should be supported");
999    }
1000
1001    #[test]
1002    fn test_pretty_print_abi_error() {
1003        let supported_versions =
1004            VersionVec(FAKE_VERSION_HISTORY.versions[1..4].iter().cloned().collect());
1005
1006        assert_eq!(
1007            AbiRevisionError::PlatformMismatch {
1008                abi_revision: 0xFF01_ABCD_000B_2224.into(),
1009                package_date: NaiveDate::from_ymd_opt(1998, 9, 4).unwrap(),
1010                package_commit_hash: 0xABCD,
1011                platform_date: NaiveDate::from_ymd_opt(2024, 9, 24).unwrap(),
1012                platform_commit_hash: 0x8C4D,
1013            }
1014            .to_string(),
1015            "Unknown platform ABI revision: 0xff01abcd000b2224.
1016Found platform component from 1998-09-04 (Git hash abcd).
1017Platform components must be from 2024-09-24 (Git hash 8c4d)"
1018        );
1019
1020        assert_eq!(
1021            AbiRevisionError::UnstableMismatch {
1022                abi_revision: 0xFF00_ABCD_000B_2224.into(),
1023                package_sdk_date: NaiveDate::from_ymd_opt(1998, 9, 4).unwrap(),
1024                package_sdk_commit_hash: 0xABCD,
1025                platform_date: NaiveDate::from_ymd_opt(2024, 9, 24).unwrap(),
1026                platform_commit_hash: 0x8C4D,
1027                supported_versions: supported_versions.clone()
1028            }
1029            .to_string(),
1030            "Unknown NEXT or HEAD ABI revision: 0xff00abcd000b2224.
1031SDK is from 1998-09-04 (Git hash abcd).
1032Expected 2024-09-24 (Git hash 8c4d)
1033The following API levels are stable and supported:
1034└── 5 (0x58ea445e942a0005)
1035└── 6 (0x58ea445e942a0006)
1036└── 7 (0x58ea445e942a0007)"
1037        );
1038
1039        assert_eq!(
1040            AbiRevisionError::TooNew {
1041                abi_revision: 0x1234.into(),
1042                supported_versions: supported_versions.clone()
1043            }
1044            .to_string(),
1045            "Unknown ABI revision 0x1234. It was probably created after
1046this platform or tool was built. The following API levels are stable and
1047supported:
1048└── 5 (0x58ea445e942a0005)
1049└── 6 (0x58ea445e942a0006)
1050└── 7 (0x58ea445e942a0007)"
1051        );
1052        assert_eq!(
1053            AbiRevisionError::Retired {
1054                version: FAKE_VERSION_HISTORY.versions[0].clone(),
1055                supported_versions: supported_versions.clone()
1056            }
1057            .to_string(),
1058            "Retired API level 4 (0x58ea445e942a0004) cannot run.
1059The following API levels are stable and supported:
1060└── 5 (0x58ea445e942a0005)
1061└── 6 (0x58ea445e942a0006)
1062└── 7 (0x58ea445e942a0007)"
1063        );
1064
1065        assert_eq!(
1066            AbiRevisionError::Invalid.to_string(),
1067            "Invalid ABI revision 0xffffffffffffffff."
1068        );
1069
1070        assert_eq!(
1071            AbiRevisionError::Malformed { abi_revision: 0xFF02_0000_0000_0000.into() }.to_string(),
1072            "ABI revision 0xff02000000000000 has an unrecognized format."
1073        );
1074    }
1075
1076    #[test]
1077    fn test_pretty_print_api_error() {
1078        let supported: Vec<ApiLevel> =
1079            vec![5.into(), 6.into(), 7.into(), ApiLevel::NEXT, ApiLevel::HEAD, ApiLevel::PLATFORM];
1080
1081        assert_eq!(
1082            ApiLevelError::Unknown { api_level: 42.into(), supported: supported.clone() }
1083                .to_string(),
1084            "Unknown target API level: 42. Is the SDK too old to support it?
1085The following API levels are supported: 5, 6, 7, NEXT, HEAD, PLATFORM",
1086        );
1087        assert_eq!(
1088            ApiLevelError::Unsupported {
1089                version: FAKE_VERSION_HISTORY.versions[0].clone(),
1090                supported: supported.clone()
1091            }
1092            .to_string(),
1093            "The SDK no longer supports API level 4.
1094The following API levels are supported: 5, 6, 7, NEXT, HEAD, PLATFORM"
1095        );
1096    }
1097
1098    #[test]
1099    fn test_check_api_level() {
1100        let supported: Vec<ApiLevel> =
1101            vec![5.into(), 6.into(), 7.into(), ApiLevel::NEXT, ApiLevel::HEAD, ApiLevel::PLATFORM];
1102
1103        assert_eq!(
1104            FAKE_VERSION_HISTORY.check_api_level_for_build(42.into()),
1105            Err(ApiLevelError::Unknown { api_level: 42.into(), supported: supported.clone() })
1106        );
1107
1108        assert_eq!(
1109            FAKE_VERSION_HISTORY.check_api_level_for_build(4.into()),
1110            Err(ApiLevelError::Unsupported {
1111                version: FAKE_VERSION_HISTORY.versions[0].clone(),
1112                supported: supported.clone()
1113            })
1114        );
1115        assert_eq!(
1116            FAKE_VERSION_HISTORY.check_api_level_for_build(6.into()),
1117            Ok(0x58ea445e942a0006.into())
1118        );
1119        assert_eq!(
1120            FAKE_VERSION_HISTORY.check_api_level_for_build(7.into()),
1121            Ok(0x58ea445e942a0007.into())
1122        );
1123        assert_eq!(
1124            FAKE_VERSION_HISTORY.check_api_level_for_build(ApiLevel::NEXT),
1125            Ok(0xFF00_8C4D_000B_4751.into())
1126        );
1127        assert_eq!(
1128            FAKE_VERSION_HISTORY.check_api_level_for_build(ApiLevel::HEAD),
1129            Ok(0xFF00_8C4D_000B_4751.into())
1130        );
1131        assert_eq!(
1132            FAKE_VERSION_HISTORY.check_api_level_for_build(ApiLevel::PLATFORM),
1133            Ok(0xFF01_8C4D_000B_4751.into())
1134        );
1135    }
1136
1137    #[test]
1138    fn test_various_getters() {
1139        assert_eq!(
1140            FAKE_VERSION_HISTORY.get_abi_revision_for_platform_components(),
1141            0xFF01_8C4D_000B_4751.into()
1142        );
1143        assert_eq!(
1144            FAKE_VERSION_HISTORY.get_misleading_version_for_ffx(),
1145            FAKE_VERSION_HISTORY.versions[3].clone()
1146        );
1147        assert_eq!(FAKE_VERSION_HISTORY.get_misleading_version_for_ffx().api_level, 7.into());
1148        assert_eq!(
1149            FAKE_VERSION_HISTORY.get_example_supported_version_for_tests(),
1150            FAKE_VERSION_HISTORY.versions[FAKE_VERSION_HISTORY.versions.len() - 4].clone()
1151        );
1152        assert_eq!(
1153            FAKE_VERSION_HISTORY.get_example_supported_version_for_tests().api_level,
1154            7.into()
1155        );
1156    }
1157
1158    #[test]
1159    fn test_explanations() {
1160        let exp = |abi_revision| AbiRevision::from_u64(abi_revision).explanation();
1161
1162        assert_eq!(exp(0x1234_5678_9abc_deff), AbiRevisionExplanation::Normal);
1163        assert_eq!(
1164            exp(0xFF01_abcd_00ab_eeee),
1165            AbiRevisionExplanation::Platform {
1166                date: chrono::NaiveDate::from_num_days_from_ce_opt(0xab_eeee).unwrap(),
1167                git_revision: 0xabcd
1168            }
1169        );
1170        assert_eq!(
1171            exp(0xFF00_1234_0012_3456),
1172            AbiRevisionExplanation::Unstable {
1173                date: chrono::NaiveDate::from_num_days_from_ce_opt(0x12_3456).unwrap(),
1174                git_revision: 0x1234
1175            }
1176        );
1177        // 0xabcd_9876 is too large a date for chrono. Make sure we return a
1178        // null date, rather than crashing.
1179        assert_eq!(
1180            exp(0xFF00_1234_abcd_9876),
1181            AbiRevisionExplanation::Unstable {
1182                date: chrono::NaiveDate::default(),
1183                git_revision: 0x1234
1184            }
1185        );
1186        assert_eq!(
1187            exp(0xFF01_1234_abcd_9876),
1188            AbiRevisionExplanation::Platform {
1189                date: chrono::NaiveDate::default(),
1190                git_revision: 0x1234
1191            }
1192        );
1193        assert_eq!(exp(0xFFFF_FFFF_FFFF_FFFF), AbiRevisionExplanation::Invalid);
1194        assert_eq!(exp(0xFFFF_FFFF_FFFF_FFFE), AbiRevisionExplanation::Malformed);
1195    }
1196}