Skip to main content

abootimg_oxide/
standard.rs

1use alloc::{boxed::Box, format};
2use binrw::{
3    binrw,
4    io::{NoSeek, Read, Seek, SeekFrom, Write},
5    BinRead, BinWrite,
6};
7
8use crate::version::OsVersionPatch;
9
10// TODO: extent/part/section type!!!
11
12/// Standard Android boot image header versions 0, 1 and 2
13///
14/// # Section layout in the image
15///
16/// Sections after the header are marked by fields of the form `*_size`, and are stored
17/// consecutively, padded to page size.
18///
19/// Sections in [`HeaderV0`] are also marked with the physical address where a bootloader should
20/// load them to.
21///
22/// ```text
23/// ┌─────────────────────────┐
24/// │boot image header        │
25/// │+ padding to page size   │
26/// ├─────────────────────────┤
27/// │kernel                   │
28/// │+ padding to page size   │
29/// ├─────────────────────────┤
30/// │ramdisk                  │
31/// │+ padding to page size   │
32/// ├─────────────────────────┤
33/// │second stage bootloader  │
34/// │+ padding to page size   │
35/// ├─────────────────────────┤
36/// │recovery dtbo/acpio (v1+)│
37/// │+ padding to page size   │
38/// ├─────────────────────────┤
39/// │dtb (v2)                 │
40/// │+ padding to page size   │
41/// └─────────────────────────┘
42/// ```
43///
44/// # Additional Documentation
45///
46/// - <https://source.android.com/docs/core/architecture/bootloader/boot-image-header>
47/// - <https://docs.u-boot.org/en/latest/android/boot-image.html>
48#[binrw]
49#[derive(Clone, Debug, PartialEq, Eq, Hash)]
50#[brw(little, magic = b"ANDROID!")]
51pub struct HeaderV0 {
52    /// Kernel size
53    pub kernel_size: u32,
54    /// Kernel physical load address
55    pub kernel_addr: u32,
56    /// Ramdisk size
57    pub ramdisk_size: u32,
58    /// Ramdisk physical load address
59    pub ramdisk_addr: u32,
60    /// Second bootloader size
61    pub second_bootloader_size: u32,
62    /// Second bootloader physical load address
63    pub second_bootloader_addr: u32,
64    /// Kernel tags physical load address
65    pub tags_addr: u32,
66    /// Page size in bytes
67    pub page_size: u32,
68    /// Header version
69    #[br(temp)]
70    #[bw(calc = self.header_version())]
71    header_version: u32,
72    /// OS version and patch level
73    pub osversionpatch: OsVersionPatch,
74    /// Board or product name
75    pub board_name: [u8; 16],
76    #[br(temp)]
77    #[bw(calc = *self.cmdline.first_chunk().unwrap())]
78    cmdline_part_1: [u8; 512],
79    /// Hash digest
80    ///
81    /// Usually either a SHA1 (20 bytes of digest, 12 null-bytes) or a SHA256 (32 bytes of digest) digest of the following: kernel, ramdisk, second bootloader, recovery DTBO and DTB.
82    ///
83    /// - If the size is nonzero, hash the contents.
84    /// - Update the hash with the little-endian representation of the 32-bit unsigned size ([`u32::to_le_bytes`]), which may be zero.
85    pub hash_digest: [u8; 32],
86    #[br(temp)]
87    #[bw(calc = *self.cmdline.last_chunk().unwrap())]
88    cmdline_part_2: [u8; 1024],
89    /// Kernel command line
90    #[br(calc = [cmdline_part_1.as_slice(), cmdline_part_2.as_slice()].concat().try_into().unwrap())]
91    #[bw(ignore)]
92    pub cmdline: Box<[u8; 512 + 1024]>,
93    /// Version-specific part of the boot image header.
94    #[br(args(header_version))]
95    pub versioned: HeaderV0Versioned,
96}
97
98impl HeaderV0 {
99    pub(crate) const fn get_padding(&self, size: usize) -> usize {
100        let page_size = self.page_size as usize;
101        (page_size - (size % page_size)) % page_size
102    }
103    /// Returns the boot image header's version number.
104    #[must_use]
105    pub const fn header_version(&self) -> u32 {
106        match self.versioned {
107            HeaderV0Versioned::V0 => 0,
108            HeaderV0Versioned::V1 { .. } => 1,
109            HeaderV0Versioned::V2 { .. } => 2,
110        }
111    }
112    /// Returns the kernel's position in the boot image.
113    #[must_use]
114    pub const fn kernel_position(&self) -> usize {
115        1660 + self.get_padding(1660)
116    }
117    /// Returns the ramdisk's position in the boot image.
118    #[must_use]
119    pub const fn ramdisk_position(&self) -> usize {
120        self.kernel_position()
121            + self.kernel_size as usize
122            + self.get_padding(self.kernel_size as usize)
123    }
124    /// Returns the second stage bootloader's position in the boot image.
125    #[must_use]
126    pub const fn second_bootloader_position(&self) -> usize {
127        self.ramdisk_position()
128            + self.ramdisk_size as usize
129            + self.get_padding(self.ramdisk_size as usize)
130    }
131    /// Returns the recovery DTBO's position in the boot image.
132    #[must_use]
133    pub const fn recovery_dtbo_position(&self) -> usize {
134        self.second_bootloader_position()
135            + self.second_bootloader_size as usize
136            + self.get_padding(self.second_bootloader_size as usize)
137    }
138    /// Returns the DTB's position in the boot image.
139    ///
140    /// This returns `None` in version 0.
141    ///
142    /// Note that this section is undefined in version 1.
143    #[must_use]
144    pub const fn dtb_position(&self) -> Option<usize> {
145        match self.versioned {
146            HeaderV0Versioned::V0 => None,
147            HeaderV0Versioned::V1 {
148                recovery_dtbo_size, ..
149            }
150            | HeaderV0Versioned::V2 {
151                recovery_dtbo_size, ..
152            } => Some(
153                self.second_bootloader_position()
154                    + recovery_dtbo_size as usize
155                    + self.get_padding(recovery_dtbo_size as usize),
156            ),
157        }
158    }
159    /// Returns the size of the boot image.
160    #[must_use]
161    #[expect(
162        clippy::missing_panics_doc,
163        reason = "dtb_position always returns Some on V1 and V2"
164    )]
165    pub const fn boot_image_size(&self) -> usize {
166        match self.versioned {
167            HeaderV0Versioned::V0 => self.recovery_dtbo_position(),
168            HeaderV0Versioned::V1 { .. } => self.dtb_position().unwrap(),
169            HeaderV0Versioned::V2 { dtb_size, .. } => {
170                self.dtb_position().unwrap()
171                    + dtb_size as usize
172                    + self.get_padding(dtb_size as usize)
173            }
174        }
175    }
176
177    /// Finalizes the passed in `hasher` to create a [`Self::hash_digest`].
178    ///
179    /// # Errors
180    ///
181    /// Passes through errors that occur in the readers and errors when more than [`u32::MAX`]
182    /// bytes were read from a single file.
183    #[cfg(feature = "hash")]
184    #[cfg_attr(docsrs, doc(cfg(feature = "hash")))]
185    pub fn compute_hash_digest<R: Read, D: digest::Digest>(
186        kernel: Option<&mut R>,
187        ramdisk: Option<&mut R>,
188        second_bootloader: Option<&mut R>,
189        recovery_dtbo: Option<&mut R>,
190        dtb: Option<&mut R>,
191    ) -> binrw::io::Result<[u8; 32]> {
192        let mut hasher = D::new();
193
194        for r in [kernel, ramdisk, second_bootloader, recovery_dtbo, dtb] {
195            if let Some(r) = r {
196                let mut buf = alloc::vec::Vec::new();
197                r.read_to_end(&mut buf)?;
198                hasher.update(&buf);
199                hasher.update(
200                    u32::try_from(buf.len())
201                        .map_err(|_| binrw::io::ErrorKind::InvalidInput)?
202                        .to_le_bytes(),
203                );
204            } else {
205                hasher.update(0u32.to_le_bytes());
206            }
207        }
208
209        let digest = hasher.finalize();
210        let mut buf = [0; _];
211        buf[..digest.len()].copy_from_slice(&digest);
212        Ok(buf)
213    }
214
215    /// Writes the full Android boot image, including the different parts after the header.
216    ///
217    /// - Requires the Rust standard library for [`std::io::copy`].
218    /// - Assumes that the readers will output exact amounts. That is, `kernel` will only ever output exactly [`Self::kernel_size`] bytes.
219    ///
220    /// # Errors
221    ///
222    /// Passes through errors that occur in the readers or the writer or during serialization
223    /// of the header.
224    #[cfg(feature = "std")]
225    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
226    pub fn full_write<W: Write + Seek, R: Read>(
227        &self,
228        writer: &mut W,
229        kernel: Option<&mut R>,
230        ramdisk: Option<&mut R>,
231        second_bootloader: Option<&mut R>,
232        recovery_dtbo: Option<&mut R>,
233        dtb: Option<&mut R>,
234    ) -> binrw::BinResult<()> {
235        let w = writer;
236
237        self.write(w)?;
238
239        if let Some(r) = kernel {
240            w.seek(SeekFrom::Start(self.kernel_position() as u64))?;
241            std::io::copy(r, w)?;
242        }
243
244        if let Some(r) = ramdisk {
245            w.seek(SeekFrom::Start(self.ramdisk_position() as u64))?;
246            std::io::copy(r, w)?;
247        }
248
249        if let Some(r) = second_bootloader {
250            w.seek(SeekFrom::Start(self.second_bootloader_position() as u64))?;
251            std::io::copy(r, w)?;
252        }
253
254        if let Some(r) = recovery_dtbo {
255            w.seek(SeekFrom::Start(self.recovery_dtbo_position() as u64))?;
256            std::io::copy(r, w)?;
257        }
258
259        if let Some(dtb_position) = self.dtb_position() {
260            if let Some(r) = dtb {
261                w.seek(SeekFrom::Start(dtb_position as u64))?;
262                std::io::copy(r, w)?;
263            }
264        }
265
266        // Final padding to page size
267        w.seek(SeekFrom::Start(self.boot_image_size() as u64))?;
268
269        Ok(())
270    }
271}
272
273/// Version-specific part of boot image headers v0-v2
274#[binrw]
275#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
276#[br(import(header_version: u32))]
277#[br(pre_assert([0,1,2].contains(&header_version), "invalid header version: {header_version}"))]
278pub enum HeaderV0Versioned {
279    /// V0-specific fields
280    #[br(pre_assert(header_version == 0))]
281    V0,
282    /// V1-specific fields
283    #[br(pre_assert(header_version == 1))]
284    V1 {
285        /// Recovery DTBO/ACPIO size
286        recovery_dtbo_size: u32,
287        /// Recovery DTBO/ACPIO physical load address
288        recovery_dtbo_addr: u64,
289        #[br(temp, assert(header_size == 1648))]
290        #[bw(calc = 1648)]
291        header_size: u32,
292    },
293    /// V2-specific fields
294    #[br(pre_assert(header_version == 2))]
295    V2 {
296        /// Recovery DTBO/ACPIO size
297        recovery_dtbo_size: u32,
298        /// Recovery DTBO/ACPIO physical load address
299        recovery_dtbo_addr: u64,
300        #[br(temp, assert(header_size == 1660))]
301        #[bw(calc = 1660)]
302        header_size: u32,
303        /// DTB size
304        dtb_size: u32,
305        /// DTB physical load address
306        dtb_addr: u64,
307    },
308}
309
310/// Standard Android boot image header versions 3 and 4
311///
312/// The page size is always 4096 bytes.
313///
314/// # Section layout in the image
315///
316/// Sections after the header are marked by fields of the form `*_size`, and are stored
317/// consecutively, padded to page size.
318///
319/// ```text
320/// ┌───────────────────────┐
321/// │boot image header      │
322/// │+ padding to page size │
323/// ├───────────────────────┤
324/// │kernel                 │
325/// │+ padding to page size │
326/// ├───────────────────────┤
327/// │ramdisk                │
328/// │+ padding to page size │
329/// ├───────────────────────┤
330/// │boot signature (v4)    │
331/// │+ padding to page size │
332/// └───────────────────────┘
333/// ```
334#[binrw]
335#[derive(Clone, Debug, PartialEq, Eq, Hash)]
336#[brw(little, magic = b"ANDROID!")]
337#[br(assert(header_size == self.header_size(), "invalid header size: {header_size}"))]
338pub struct HeaderV3 {
339    /// Kernel size
340    pub kernel_size: u32,
341    /// Ramdisk size
342    pub ramdisk_size: u32,
343    /// OS version and patch level
344    pub osversionpatch: OsVersionPatch,
345    #[br(temp)]
346    #[bw(calc = self.header_size())]
347    header_size: u32,
348    #[brw(pad_before = 16)]
349    #[br(temp)]
350    #[br(assert(header_version == 3 || header_version == 4, "invalid header version: {header_version}"))]
351    #[bw(calc = self.header_version())]
352    header_version: u32,
353    /// Kernel command line
354    pub cmdline: Box<[u8; 512 + 1024]>,
355    /// Boot signature size.
356    ///
357    /// This is only present in version 4 and the version will be inferred from this field.
358    #[br(if(header_version == 4))]
359    pub v4_signature_size: Option<u32>,
360}
361
362impl HeaderV3 {
363    pub(crate) const PAGE_SIZE: usize = 4096;
364
365    /// Returns the boot image header's version number.
366    #[must_use]
367    pub const fn header_version(&self) -> u32 {
368        if self.v4_signature_size.is_some() {
369            4
370        } else {
371            3
372        }
373    }
374    pub(crate) const fn header_size(&self) -> u32 {
375        if self.v4_signature_size.is_some() {
376            1584
377        } else {
378            1580
379        }
380    }
381    pub(crate) const fn get_padding(size: usize) -> usize {
382        // Equivalent to `size.div_ceil(PAGE_SIZE) * PAGE_SIZE - size`
383        // or `PAGE_SIZE - (size % PAGE_SIZE)) % PAGE_SIZE`, but more efficient.
384        (Self::PAGE_SIZE - (size % Self::PAGE_SIZE)) % Self::PAGE_SIZE
385    }
386    /// Returns the kernel's position in the boot image.
387    ///
388    /// Hardcoded to the page size, which is 4096.
389    #[must_use]
390    pub const fn kernel_position() -> usize {
391        Self::PAGE_SIZE
392    }
393    /// Returns the ramdisk's position in the boot image.
394    #[must_use]
395    pub const fn ramdisk_position(&self) -> usize {
396        Self::kernel_position()
397            + self.kernel_size as usize
398            + Self::get_padding(self.kernel_size as usize)
399    }
400    /// Returns the boot signature's position in the boot image.
401    ///
402    /// Note that this section is undefined in version 3.
403    #[must_use]
404    pub const fn bootsig_position(&self) -> usize {
405        self.ramdisk_position()
406            + self.ramdisk_size as usize
407            + Self::get_padding(self.ramdisk_size as usize)
408    }
409}
410
411/// Standard Android boot image header for versions 0 through 4
412#[derive(Clone, Debug, PartialEq, Eq, Hash)]
413pub enum Header {
414    /// Header for versions 0-2
415    V0(HeaderV0),
416    /// Header for versions 3-4
417    V3(HeaderV3),
418}
419
420impl Header {
421    /// Parses a standard Android boot image header from a reader.
422    ///
423    /// # Errors
424    ///
425    /// This returns an error if reading fails or if the header is invalid.
426    pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self, binrw::Error> {
427        reader.seek(binrw::io::SeekFrom::Start(0x28))?;
428        let mut version_buf = [0u8; 4];
429        reader.read_exact(&mut version_buf)?;
430        reader.seek(binrw::io::SeekFrom::Start(0))?;
431
432        // TODO: on next breaking change bump binrw
433        // TODO: on next breaking change, make `Header` implement/use binrw's traits!
434        Ok(match u32::from_le_bytes(version_buf) {
435            0..=2 => Self::V0(HeaderV0::read(reader)?),
436            3 | 4 => Self::V3(HeaderV3::read(reader)?),
437            version => {
438                return Err(binrw::Error::AssertFail {
439                    pos: 0x28,
440                    message: format!("Unknown header version: {version}"),
441                })
442            }
443        })
444    }
445    /// Serializes a standard Android boot image header to a writer.
446    ///
447    /// Note that you must write the kernel, ramdisk, etc. yourself.
448    ///
449    /// # Errors
450    ///
451    /// This forwards errors from `writer`.
452    pub fn write<W: Write>(&self, writer: &mut W) -> Result<(), binrw::Error> {
453        let writer = &mut NoSeek::new(writer);
454        match self {
455            Self::V0(hdr) => hdr.write(writer),
456            Self::V3(hdr) => hdr.write(writer),
457        }
458    }
459    /// Returns the boot image header's version number.
460    #[must_use]
461    pub const fn header_version(&self) -> u32 {
462        match self {
463            Self::V0(hdr) => hdr.header_version(),
464            Self::V3(hdr) => hdr.header_version(),
465        }
466    }
467    /// Returns the boot image header's OS version and patch level.
468    #[must_use]
469    pub const fn osversionpatch(&self) -> OsVersionPatch {
470        match self {
471            Self::V0(hdr) => hdr.osversionpatch,
472            Self::V3(hdr) => hdr.osversionpatch,
473        }
474    }
475    /// Returns the kernel's position in the boot image.
476    #[must_use]
477    pub const fn kernel_position(&self) -> usize {
478        match self {
479            Self::V0(hdr) => hdr.kernel_position(),
480            Self::V3(_) => HeaderV3::kernel_position(),
481        }
482    }
483    /// Returns the kernel's size.
484    #[must_use]
485    pub const fn kernel_size(&self) -> u32 {
486        match self {
487            Self::V0(hdr) => hdr.kernel_size,
488            Self::V3(hdr) => hdr.kernel_size,
489        }
490    }
491    /// Returns the ramdisk's position in the boot image.
492    #[must_use]
493    pub const fn ramdisk_position(&self) -> usize {
494        match self {
495            Self::V0(hdr) => hdr.ramdisk_position(),
496            Self::V3(hdr) => hdr.ramdisk_position(),
497        }
498    }
499    /// Returns the ramdisk's size.
500    #[must_use]
501    pub const fn ramdisk_size(&self) -> u32 {
502        match self {
503            Self::V0(hdr) => hdr.ramdisk_size,
504            Self::V3(hdr) => hdr.ramdisk_size,
505        }
506    }
507    /// Returns the page size in bytes.
508    #[must_use]
509    pub const fn page_size(&self) -> usize {
510        match self {
511            Self::V0(hdr) => hdr.page_size as usize,
512            Self::V3(_) => HeaderV3::PAGE_SIZE,
513        }
514    }
515    /// Returns the kernel command line.
516    #[must_use]
517    pub const fn cmdline(&self) -> &[u8; 512 + 1024] {
518        match self {
519            Self::V0(hdr) => &hdr.cmdline,
520            Self::V3(hdr) => &hdr.cmdline,
521        }
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use alloc::vec::Vec;
528    use binrw::io::Cursor;
529    use expect_test_bytes::expect_file;
530
531    use super::*;
532
533    #[test]
534    fn simple_write_read() {
535        fn pad_slice_to_array<const N: usize>(slice: &[u8]) -> [u8; N] {
536            let mut arr = [0u8; N];
537            let len = slice.len().min(N);
538            arr[..len].copy_from_slice(&slice[..len]);
539            arr
540        }
541        let expected_header = Header::V3(HeaderV3 {
542            kernel_size: 0x7357_0001,
543            ramdisk_size: 0x7357_0002,
544            osversionpatch: OsVersionPatch(0x7357_0003),
545            cmdline: Box::new(pad_slice_to_array(b"example")),
546            v4_signature_size: None,
547        });
548
549        let mut actual_bytes = Vec::new();
550        expected_header
551            .write(&mut Cursor::new(&mut actual_bytes))
552            .unwrap();
553
554        expect_file!["test_data/standard/simple_write_read"].assert_eq(&actual_bytes);
555
556        let actual_header = Header::parse(&mut Cursor::new(&actual_bytes)).unwrap();
557
558        assert_eq!(expected_header, actual_header);
559
560        let either_header = crate::EitherHeader::read(&mut Cursor::new(&actual_bytes)).unwrap();
561
562        assert_eq!(
563            crate::EitherHeader::Standard(expected_header),
564            either_header
565        );
566    }
567}