Skip to main content

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::{Context, bail};
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            ("some-version", "1", "invalid digit found in string"),
820            ("-1", "1", "invalid digit found in string"),
821            (
822                "1",
823                "some-revision",
824                "invalid value: string \"some-revision\", expected an unsigned integer at line 1 column 113",
825            ),
826            (
827                "1",
828                "-1",
829                "invalid value: string \"-1\", expected an unsigned integer at line 1 column 102",
830            ),
831        ] {
832            let expected_bytes = serde_json::to_vec(&serde_json::json!({
833                "data": {
834                    "name": VERSION_HISTORY_NAME,
835                    "type": VERSION_HISTORY_TYPE,
836                    "api_levels": {
837                        api_level:{
838                            "abi_revision": abi_revision,
839                            "status": Status::InDevelopment,
840                        }
841                    },
842                    "special_api_levels": {},
843                },
844                "schema_id": VERSION_HISTORY_SCHEMA_ID,
845            }))
846            .unwrap();
847
848            assert_eq!(parse_version_history(&expected_bytes[..]).unwrap_err().to_string(), err);
849        }
850    }
851
852    #[test]
853    fn test_parse_history_rejects_bogus_special_levels() {
854        let input = br#"{
855            "data": {
856                "name": "Platform version map",
857                "type": "version_history",
858                "api_levels": {},
859                "special_api_levels": {
860                    "WACKY_WAVING_INFLATABLE_ARM_FLAILING_TUBE_MAN": {
861                        "as_u32": 1234,
862                        "abi_revision": "0x50cbc6e8a39e1e2c",
863                        "status": "in-development"
864                    }
865                }
866            },
867            "schema_id": "https://fuchsia.dev/schema/version_history.json"
868        }"#;
869
870        assert_eq!(
871            parse_version_history(&input[..]).unwrap_err().to_string(),
872            "Unknown special API level: WACKY_WAVING_INFLATABLE_ARM_FLAILING_TUBE_MAN"
873        );
874    }
875
876    #[test]
877    fn test_parse_history_rejects_misnumbered_special_levels() {
878        let input = br#"{
879            "data": {
880                "name": "Platform version map",
881                "type": "version_history",
882                "api_levels": {},
883                "special_api_levels": {
884                    "HEAD": {
885                        "as_u32": 1234,
886                        "abi_revision": "0x50cbc6e8a39e1e2c",
887                        "status": "in-development"
888                    }
889                }
890            },
891            "schema_id": "https://fuchsia.dev/schema/version_history.json"
892        }"#;
893
894        assert_eq!(
895            parse_version_history(&input[..]).unwrap_err().to_string(),
896            "Special API level HEAD had unexpected numerical value 1234 (Expected 4292870144)"
897        );
898    }
899
900    #[test]
901    fn test_serialize_abi_revision() {
902        {
903            let abi = AbiRevision::from_u64(16);
904            let str = "\"0x0000000000000010\"";
905            assert_eq!(serde_json::to_string(&abi).unwrap(), str);
906            assert_eq!(serde_json::from_str::<AbiRevision>(str).unwrap(), abi);
907        }
908        {
909            let abi = AbiRevision::from_u64(0x58ea445e942a0004);
910            let str = "\"0x58ea445e942a0004\"";
911            assert_eq!(serde_json::to_string(&abi).unwrap(), str);
912            assert_eq!(serde_json::from_str::<AbiRevision>(str).unwrap(), abi);
913        }
914    }
915
916    pub const FAKE_VERSION_HISTORY: VersionHistory = VersionHistory {
917        versions: &[
918            Version {
919                api_level: ApiLevel::from_u32(4),
920                abi_revision: AbiRevision::from_u64(0x58ea445e942a0004),
921                status: Status::Unsupported,
922            },
923            Version {
924                api_level: ApiLevel::from_u32(5),
925                abi_revision: AbiRevision::from_u64(0x58ea445e942a0005),
926                status: Status::Supported,
927            },
928            Version {
929                api_level: ApiLevel::from_u32(6),
930                abi_revision: AbiRevision::from_u64(0x58ea445e942a0006),
931                status: Status::Supported,
932            },
933            Version {
934                api_level: ApiLevel::from_u32(7),
935                abi_revision: AbiRevision::from_u64(0x58ea445e942a0007),
936                status: Status::Supported,
937            },
938            Version {
939                api_level: ApiLevel::NEXT,
940                abi_revision: AbiRevision::from_u64(0xFF00_8C4D_000B_4751),
941                status: Status::InDevelopment,
942            },
943            Version {
944                api_level: ApiLevel::HEAD,
945                abi_revision: AbiRevision::from_u64(0xFF00_8C4D_000B_4751),
946                status: Status::InDevelopment,
947            },
948            Version {
949                api_level: ApiLevel::PLATFORM,
950                abi_revision: AbiRevision::from_u64(0xFF01_8C4D_000B_4751),
951                status: Status::InDevelopment,
952            },
953        ],
954    };
955
956    #[test]
957    fn test_check_abi_revision() {
958        let supported_versions =
959            VersionVec(FAKE_VERSION_HISTORY.versions[1..4].iter().cloned().collect());
960
961        assert_eq!(
962            FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0xFF01_ABCD_000B_2224.into()),
963            Err(AbiRevisionError::PlatformMismatch {
964                abi_revision: 0xFF01_ABCD_000B_2224.into(),
965                package_date: NaiveDate::from_ymd_opt(1998, 9, 4).unwrap(),
966                package_commit_hash: 0xABCD,
967                platform_date: NaiveDate::from_ymd_opt(2024, 9, 24).unwrap(),
968                platform_commit_hash: 0x8C4D,
969            })
970        );
971
972        assert_eq!(
973            FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0xFF00_ABCD_000B_2224.into()),
974            Err(AbiRevisionError::UnstableMismatch {
975                abi_revision: 0xFF00_ABCD_000B_2224.into(),
976                package_sdk_date: NaiveDate::from_ymd_opt(1998, 9, 4).unwrap(),
977                package_sdk_commit_hash: 0xABCD,
978                platform_date: NaiveDate::from_ymd_opt(2024, 9, 24).unwrap(),
979                platform_commit_hash: 0x8C4D,
980                supported_versions: supported_versions.clone()
981            })
982        );
983
984        assert_eq!(
985            FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0x1234.into()),
986            Err(AbiRevisionError::TooNew {
987                abi_revision: 0x1234.into(),
988                supported_versions: supported_versions.clone()
989            })
990        );
991
992        assert_eq!(
993            FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0x58ea445e942a0004.into()),
994            Err(AbiRevisionError::Retired {
995                version: FAKE_VERSION_HISTORY.versions[0].clone(),
996
997                supported_versions: supported_versions.clone()
998            })
999        );
1000
1001        assert_eq!(
1002            FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(AbiRevision::INVALID),
1003            Err(AbiRevisionError::Invalid)
1004        );
1005
1006        assert_eq!(
1007            FAKE_VERSION_HISTORY.check_abi_revision_for_runtime(0xFF02_0000_0000_0000.into()),
1008            Err(AbiRevisionError::Malformed { abi_revision: 0xFF02_0000_0000_0000.into() })
1009        );
1010
1011        FAKE_VERSION_HISTORY
1012            .check_abi_revision_for_runtime(0x58ea445e942a0005.into())
1013            .expect("level 5 should be supported");
1014        FAKE_VERSION_HISTORY
1015            .check_abi_revision_for_runtime(0x58ea445e942a0007.into())
1016            .expect("level 7 should be supported");
1017    }
1018
1019    #[test]
1020    fn test_pretty_print_abi_error() {
1021        let supported_versions =
1022            VersionVec(FAKE_VERSION_HISTORY.versions[1..4].iter().cloned().collect());
1023
1024        assert_eq!(
1025            AbiRevisionError::PlatformMismatch {
1026                abi_revision: 0xFF01_ABCD_000B_2224.into(),
1027                package_date: NaiveDate::from_ymd_opt(1998, 9, 4).unwrap(),
1028                package_commit_hash: 0xABCD,
1029                platform_date: NaiveDate::from_ymd_opt(2024, 9, 24).unwrap(),
1030                platform_commit_hash: 0x8C4D,
1031            }
1032            .to_string(),
1033            "Unknown platform ABI revision: 0xff01abcd000b2224.
1034Found platform component from 1998-09-04 (Git hash abcd).
1035Platform components must be from 2024-09-24 (Git hash 8c4d)"
1036        );
1037
1038        assert_eq!(
1039            AbiRevisionError::UnstableMismatch {
1040                abi_revision: 0xFF00_ABCD_000B_2224.into(),
1041                package_sdk_date: NaiveDate::from_ymd_opt(1998, 9, 4).unwrap(),
1042                package_sdk_commit_hash: 0xABCD,
1043                platform_date: NaiveDate::from_ymd_opt(2024, 9, 24).unwrap(),
1044                platform_commit_hash: 0x8C4D,
1045                supported_versions: supported_versions.clone()
1046            }
1047            .to_string(),
1048            "Unknown NEXT or HEAD ABI revision: 0xff00abcd000b2224.
1049SDK is from 1998-09-04 (Git hash abcd).
1050Expected 2024-09-24 (Git hash 8c4d)
1051The following API levels are stable and supported:
1052└── 5 (0x58ea445e942a0005)
1053└── 6 (0x58ea445e942a0006)
1054└── 7 (0x58ea445e942a0007)"
1055        );
1056
1057        assert_eq!(
1058            AbiRevisionError::TooNew {
1059                abi_revision: 0x1234.into(),
1060                supported_versions: supported_versions.clone()
1061            }
1062            .to_string(),
1063            "Unknown ABI revision 0x1234. It was probably created after
1064this platform or tool was built. The following API levels are stable and
1065supported:
1066└── 5 (0x58ea445e942a0005)
1067└── 6 (0x58ea445e942a0006)
1068└── 7 (0x58ea445e942a0007)"
1069        );
1070        assert_eq!(
1071            AbiRevisionError::Retired {
1072                version: FAKE_VERSION_HISTORY.versions[0].clone(),
1073                supported_versions: supported_versions.clone()
1074            }
1075            .to_string(),
1076            "Retired API level 4 (0x58ea445e942a0004) cannot run.
1077The following API levels are stable and supported:
1078└── 5 (0x58ea445e942a0005)
1079└── 6 (0x58ea445e942a0006)
1080└── 7 (0x58ea445e942a0007)"
1081        );
1082
1083        assert_eq!(
1084            AbiRevisionError::Invalid.to_string(),
1085            "Invalid ABI revision 0xffffffffffffffff."
1086        );
1087
1088        assert_eq!(
1089            AbiRevisionError::Malformed { abi_revision: 0xFF02_0000_0000_0000.into() }.to_string(),
1090            "ABI revision 0xff02000000000000 has an unrecognized format."
1091        );
1092    }
1093
1094    #[test]
1095    fn test_pretty_print_api_error() {
1096        let supported: Vec<ApiLevel> =
1097            vec![5.into(), 6.into(), 7.into(), ApiLevel::NEXT, ApiLevel::HEAD, ApiLevel::PLATFORM];
1098
1099        assert_eq!(
1100            ApiLevelError::Unknown { api_level: 42.into(), supported: supported.clone() }
1101                .to_string(),
1102            "Unknown target API level: 42. Is the SDK too old to support it?
1103The following API levels are supported: 5, 6, 7, NEXT, HEAD, PLATFORM",
1104        );
1105        assert_eq!(
1106            ApiLevelError::Unsupported {
1107                version: FAKE_VERSION_HISTORY.versions[0].clone(),
1108                supported: supported.clone()
1109            }
1110            .to_string(),
1111            "The SDK no longer supports API level 4.
1112The following API levels are supported: 5, 6, 7, NEXT, HEAD, PLATFORM"
1113        );
1114    }
1115
1116    #[test]
1117    fn test_check_api_level() {
1118        let supported: Vec<ApiLevel> =
1119            vec![5.into(), 6.into(), 7.into(), ApiLevel::NEXT, ApiLevel::HEAD, ApiLevel::PLATFORM];
1120
1121        assert_eq!(
1122            FAKE_VERSION_HISTORY.check_api_level_for_build(42.into()),
1123            Err(ApiLevelError::Unknown { api_level: 42.into(), supported: supported.clone() })
1124        );
1125
1126        assert_eq!(
1127            FAKE_VERSION_HISTORY.check_api_level_for_build(4.into()),
1128            Err(ApiLevelError::Unsupported {
1129                version: FAKE_VERSION_HISTORY.versions[0].clone(),
1130                supported: supported.clone()
1131            })
1132        );
1133        assert_eq!(
1134            FAKE_VERSION_HISTORY.check_api_level_for_build(6.into()),
1135            Ok(0x58ea445e942a0006.into())
1136        );
1137        assert_eq!(
1138            FAKE_VERSION_HISTORY.check_api_level_for_build(7.into()),
1139            Ok(0x58ea445e942a0007.into())
1140        );
1141        assert_eq!(
1142            FAKE_VERSION_HISTORY.check_api_level_for_build(ApiLevel::NEXT),
1143            Ok(0xFF00_8C4D_000B_4751.into())
1144        );
1145        assert_eq!(
1146            FAKE_VERSION_HISTORY.check_api_level_for_build(ApiLevel::HEAD),
1147            Ok(0xFF00_8C4D_000B_4751.into())
1148        );
1149        assert_eq!(
1150            FAKE_VERSION_HISTORY.check_api_level_for_build(ApiLevel::PLATFORM),
1151            Ok(0xFF01_8C4D_000B_4751.into())
1152        );
1153    }
1154
1155    #[test]
1156    fn test_various_getters() {
1157        assert_eq!(
1158            FAKE_VERSION_HISTORY.get_abi_revision_for_platform_components(),
1159            0xFF01_8C4D_000B_4751.into()
1160        );
1161        assert_eq!(
1162            FAKE_VERSION_HISTORY.get_misleading_version_for_ffx(),
1163            FAKE_VERSION_HISTORY.versions[3].clone()
1164        );
1165        assert_eq!(FAKE_VERSION_HISTORY.get_misleading_version_for_ffx().api_level, 7.into());
1166        assert_eq!(
1167            FAKE_VERSION_HISTORY.get_example_supported_version_for_tests(),
1168            FAKE_VERSION_HISTORY.versions[FAKE_VERSION_HISTORY.versions.len() - 4].clone()
1169        );
1170        assert_eq!(
1171            FAKE_VERSION_HISTORY.get_example_supported_version_for_tests().api_level,
1172            7.into()
1173        );
1174    }
1175
1176    #[test]
1177    fn test_explanations() {
1178        let exp = |abi_revision| AbiRevision::from_u64(abi_revision).explanation();
1179
1180        assert_eq!(exp(0x1234_5678_9abc_deff), AbiRevisionExplanation::Normal);
1181        assert_eq!(
1182            exp(0xFF01_abcd_00ab_eeee),
1183            AbiRevisionExplanation::Platform {
1184                date: chrono::NaiveDate::from_num_days_from_ce_opt(0xab_eeee).unwrap(),
1185                git_revision: 0xabcd
1186            }
1187        );
1188        assert_eq!(
1189            exp(0xFF00_1234_0012_3456),
1190            AbiRevisionExplanation::Unstable {
1191                date: chrono::NaiveDate::from_num_days_from_ce_opt(0x12_3456).unwrap(),
1192                git_revision: 0x1234
1193            }
1194        );
1195        // 0xabcd_9876 is too large a date for chrono. Make sure we return a
1196        // null date, rather than crashing.
1197        assert_eq!(
1198            exp(0xFF00_1234_abcd_9876),
1199            AbiRevisionExplanation::Unstable {
1200                date: chrono::NaiveDate::default(),
1201                git_revision: 0x1234
1202            }
1203        );
1204        assert_eq!(
1205            exp(0xFF01_1234_abcd_9876),
1206            AbiRevisionExplanation::Platform {
1207                date: chrono::NaiveDate::default(),
1208                git_revision: 0x1234
1209            }
1210        );
1211        assert_eq!(exp(0xFFFF_FFFF_FFFF_FFFF), AbiRevisionExplanation::Invalid);
1212        assert_eq!(exp(0xFFFF_FFFF_FFFF_FFFE), AbiRevisionExplanation::Malformed);
1213    }
1214}