Skip to main content

abootimg_oxide/
version.rs

1use core::fmt;
2
3use binrw::{BinRead, BinWrite};
4
5/// OS version and patch level
6///
7/// # Warning
8///
9/// Please note that the version information may be incorrect, for example, on OnePlus devices.
10///
11/// # Bitwise format
12///
13/// * 7 bits indicate first part of version
14/// * 7 bits indicate second part of version
15/// * 7 bits indicate third part of version
16/// * 12 bits indicate patch year
17/// * 4 bits indicate patch month
18#[derive(BinRead, BinWrite, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
19#[br(little)]
20pub struct OsVersionPatch(pub u32);
21
22impl OsVersionPatch {
23    /// Creates a new `OsVersionPatch`.
24    #[must_use]
25    pub const fn new(version: OsVersion, patch: OsPatch) -> Self {
26        Self((version.0 << 11) + patch.0 as u32)
27    }
28    /// Returns the version part.
29    #[must_use]
30    pub const fn version(self) -> OsVersion {
31        OsVersion(self.0 >> 11)
32    }
33    /// Returns the patch part.
34    #[must_use]
35    pub const fn patch(self) -> OsPatch {
36        OsPatch((self.0 & 0x7ff) as u16)
37    }
38}
39
40impl fmt::Debug for OsVersionPatch {
41    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42        write!(f, "OsVersionPatch({}, {})", self.version(), self.patch())
43    }
44}
45
46/// Error returned by [`OsPatch::new`].
47#[non_exhaustive]
48#[derive(thiserror::Error, Debug, PartialEq)]
49pub enum OsPatchError {
50    /// `year` under 2000, which is not supported by the format.
51    #[error("`year` under 2000, which is not supported by the format.")]
52    YearTooSmall,
53    /// `year` over 6095, which is not supported by the format (`year - 2000` won't fit into 12 bits).
54    #[error("`year` over 6095, which is not supported by the format (`year - 2000` won't fit into 12 bits).")]
55    YearWontFit,
56    /// `month` over 15, which is not supported by the format (won't fit into 4 bits).
57    #[error("`month` over 15, which is not supported by the format (won't fit into 4 bits).")]
58    MonthWontFit,
59}
60
61/// OS patch level
62///
63/// # Bitwise format
64///
65/// * 12 bits indicate year
66/// * 4 bits indicate month
67#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
68pub struct OsPatch(pub u16);
69impl OsPatch {
70    /// Creates a new `OsPatch`.
71    ///
72    /// # Errors
73    ///
74    /// Returns `Err` when either of the following occur:
75    ///
76    /// - `year` is under 2000
77    /// - `year` is over 6095 (`year - 2000` won't fit into 12 bits)
78    /// - `month` is over 15 (`month` won't fit into 4 bits)
79    pub const fn new(year: u16, month: u8) -> Result<Self, OsPatchError> {
80        const U12_MAX: u16 = 2_u16.pow(12) - 1;
81        const U4_MAX: u8 = 2_u8.pow(4) - 1;
82
83        let Some(years_after_2000) = year.checked_sub(2000) else {
84            return Err(OsPatchError::YearTooSmall);
85        };
86
87        if years_after_2000 > U12_MAX {
88            return Err(OsPatchError::YearWontFit);
89        }
90
91        if month > U4_MAX {
92            return Err(OsPatchError::MonthWontFit);
93        }
94
95        Ok(Self((years_after_2000 << 4) + month as u16))
96    }
97    /// Returns the year.
98    #[must_use]
99    pub const fn year(self) -> u16 {
100        // Highest 12 bits indicate year
101        (self.0 >> 4) + 2000
102    }
103    /// Returns the month.
104    #[must_use]
105    pub const fn month(self) -> u8 {
106        // Lowest 4 bits indicate month
107        (self.0 & 0xf) as u8
108    }
109}
110
111impl fmt::Display for OsPatch {
112    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
113        write!(f, "{}-{:02}", self.year(), self.month())
114    }
115}
116impl fmt::Debug for OsPatch {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        fmt::Display::fmt(self, f)
119    }
120}
121
122/// Error returned by [`OsVersion::new`].
123///
124/// "A version component is over 127, which is not supported by the format (won't fit into 7 bits)."
125#[derive(thiserror::Error, Debug, PartialEq)]
126#[error("A version component is over 127, which is not supported by the format (won't fit into 7 bits).")]
127pub struct OsVersionWontFitError;
128
129/// OS version
130///
131/// # Warning
132///
133/// Please note that this information may be incorrect, for example, on OnePlus devices.
134///
135/// # Bitwise format
136///
137/// * 7 bits indicate first part
138/// * 7 bits indicate second part
139/// * 7 bits indicate third part
140#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
141pub struct OsVersion(pub u32);
142impl OsVersion {
143    /// Creates a new `OsVersion`.
144    ///
145    /// # Errors
146    ///
147    /// Returns an `Err` if a version component is over 127, which means it won't fit into
148    /// 7 bits.
149    pub const fn new(a: u8, b: u8, c: u8) -> Result<Self, OsVersionWontFitError> {
150        const U7_MAX: u8 = 2_u8.pow(7) - 1;
151
152        if a > U7_MAX || b > U7_MAX || c > U7_MAX {
153            return Err(OsVersionWontFitError);
154        }
155
156        Ok(Self(((a as u32) << 14) | ((b as u32) << 7) | c as u32))
157    }
158    /// Returns the version parts.
159    #[must_use]
160    pub const fn version_parts(self) -> (u8, u8, u8) {
161        let x = self.0;
162        let a = (x >> 14) & 0x7f; // Top 7 bits
163        let b = (x >> 7) & 0x7f; // Middle 7 bits
164        let c = x & 0x7f; // Low 7 bits
165        (a as u8, b as u8, c as u8)
166    }
167}
168
169impl fmt::Display for OsVersion {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        let (a, b, c) = self.version_parts();
172        write!(f, "{a}.{b}.{c}")
173    }
174}
175impl fmt::Debug for OsVersion {
176    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177        fmt::Display::fmt(self, f)
178    }
179}
180
181#[cfg(test)]
182mod tests {
183    use alloc::{format, string::ToString as _};
184
185    use super::*;
186
187    #[test]
188    fn basic() {
189        let vp = OsVersionPatch(0x1800_0186);
190        assert_eq!(format!("{vp:?}"), "OsVersionPatch(12.0.0, 2024-06)");
191        assert_eq!(vp.version().to_string(), "12.0.0");
192        assert_eq!(vp.patch().to_string(), "2024-06");
193        assert_eq!(vp, OsVersionPatch::new(vp.version(), vp.patch()));
194        assert_eq!(Ok(vp.version()), OsVersion::new(12, 0, 0));
195        assert_eq!(Ok(vp.patch()), OsPatch::new(2024, 6));
196    }
197
198    #[test]
199    fn truncating_behavior() {
200        let ver = OsVersion(0b1111_1111 << 14);
201        assert_eq!(ver.version_parts(), (0b0111_1111, 0, 0));
202        assert_eq!(format!("{ver:?}"), "127.0.0");
203        assert_eq!(ver.to_string(), "127.0.0");
204    }
205
206    #[test]
207    fn errors() {
208        assert_eq!(OsVersion::new(0, 0, 0), Ok(OsVersion(0)));
209        assert_eq!(OsVersion::new(128, 0, 0), Err(OsVersionWontFitError));
210        assert_eq!(OsVersion::new(0, 128, 0), Err(OsVersionWontFitError));
211        assert_eq!(OsVersion::new(0, 0, 128), Err(OsVersionWontFitError));
212
213        assert_eq!(OsPatch::new(0, 0), Err(OsPatchError::YearTooSmall));
214        assert_eq!(OsPatch::new(1999, 0), Err(OsPatchError::YearTooSmall));
215        OsPatch::new(2000, 0).unwrap();
216
217        OsPatch::new(6095, 0).unwrap();
218        assert_eq!(OsPatch::new(6096, 0), Err(OsPatchError::YearWontFit));
219
220        OsPatch::new(2000, 15).unwrap();
221        assert_eq!(OsPatch::new(2000, 16), Err(OsPatchError::MonthWontFit));
222    }
223}