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    /// - Assumes that the writer supports seeking up to [`Self::boot_image_size()`]. Note that
220    ///   POSIX-compatible filesystems automatically extend the file so you shouldn't need to
221    ///   worry about calling [`std::fs::File::set_len()`]).
222    ///
223    /// # Errors
224    ///
225    /// Passes through errors that occur in the readers or the writer or during serialization
226    /// of the header.
227    #[cfg(feature = "std")]
228    #[cfg_attr(docsrs, doc(cfg(feature = "std")))]
229    pub fn full_write<W: Write + Seek, R: Read>(
230        &self,
231        writer: &mut W,
232        kernel: Option<&mut R>,
233        ramdisk: Option<&mut R>,
234        second_bootloader: Option<&mut R>,
235        recovery_dtbo: Option<&mut R>,
236        dtb: Option<&mut R>,
237    ) -> binrw::BinResult<()> {
238        let w = writer;
239
240        self.write(w)?;
241
242        if let Some(r) = kernel {
243            w.seek(SeekFrom::Start(self.kernel_position() as u64))?;
244            std::io::copy(r, w)?;
245        }
246
247        if let Some(r) = ramdisk {
248            w.seek(SeekFrom::Start(self.ramdisk_position() as u64))?;
249            std::io::copy(r, w)?;
250        }
251
252        if let Some(r) = second_bootloader {
253            w.seek(SeekFrom::Start(self.second_bootloader_position() as u64))?;
254            std::io::copy(r, w)?;
255        }
256
257        if let Some(r) = recovery_dtbo {
258            w.seek(SeekFrom::Start(self.recovery_dtbo_position() as u64))?;
259            std::io::copy(r, w)?;
260        }
261
262        if let Some(dtb_position) = self.dtb_position() {
263            if let Some(r) = dtb {
264                w.seek(SeekFrom::Start(dtb_position as u64))?;
265                std::io::copy(r, w)?;
266            }
267        }
268
269        // Final padding to page size
270        w.seek(SeekFrom::Start(self.boot_image_size() as u64))?;
271
272        Ok(())
273    }
274}
275
276/// Version-specific part of boot image headers v0-v2
277#[binrw]
278#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
279#[br(import(header_version: u32))]
280#[br(pre_assert([0,1,2].contains(&header_version), "invalid header version: {header_version}"))]
281pub enum HeaderV0Versioned {
282    /// V0-specific fields
283    #[br(pre_assert(header_version == 0))]
284    V0,
285    /// V1-specific fields
286    #[br(pre_assert(header_version == 1))]
287    V1 {
288        /// Recovery DTBO/ACPIO size
289        recovery_dtbo_size: u32,
290        /// Recovery DTBO/ACPIO physical load address
291        recovery_dtbo_addr: u64,
292        #[br(temp, assert(header_size == 1648))]
293        #[bw(calc = 1648)]
294        header_size: u32,
295    },
296    /// V2-specific fields
297    #[br(pre_assert(header_version == 2))]
298    V2 {
299        /// Recovery DTBO/ACPIO size
300        recovery_dtbo_size: u32,
301        /// Recovery DTBO/ACPIO physical load address
302        recovery_dtbo_addr: u64,
303        #[br(temp, assert(header_size == 1660))]
304        #[bw(calc = 1660)]
305        header_size: u32,
306        /// DTB size
307        dtb_size: u32,
308        /// DTB physical load address
309        dtb_addr: u64,
310    },
311}
312
313/// Standard Android boot image header versions 3 and 4
314///
315/// The page size is always 4096 bytes.
316///
317/// # Section layout in the image
318///
319/// Sections after the header are marked by fields of the form `*_size`, and are stored
320/// consecutively, padded to page size.
321///
322/// ```text
323/// ┌───────────────────────┐
324/// │boot image header      │
325/// │+ padding to page size │
326/// ├───────────────────────┤
327/// │kernel                 │
328/// │+ padding to page size │
329/// ├───────────────────────┤
330/// │ramdisk                │
331/// │+ padding to page size │
332/// ├───────────────────────┤
333/// │boot signature (v4)    │
334/// │+ padding to page size │
335/// └───────────────────────┘
336/// ```
337#[binrw]
338#[derive(Clone, Debug, PartialEq, Eq, Hash)]
339#[brw(little, magic = b"ANDROID!")]
340#[br(assert(header_size == self.header_size(), "invalid header size: {header_size}"))]
341pub struct HeaderV3 {
342    /// Kernel size
343    pub kernel_size: u32,
344    /// Ramdisk size
345    pub ramdisk_size: u32,
346    /// OS version and patch level
347    pub osversionpatch: OsVersionPatch,
348    #[br(temp)]
349    #[bw(calc = self.header_size())]
350    header_size: u32,
351    #[brw(pad_before = 16)]
352    #[br(temp)]
353    #[br(assert(header_version == 3 || header_version == 4, "invalid header version: {header_version}"))]
354    #[bw(calc = self.header_version())]
355    header_version: u32,
356    /// Kernel command line
357    pub cmdline: Box<[u8; 512 + 1024]>,
358    /// Boot signature size.
359    ///
360    /// This is only present in version 4 and the version will be inferred from this field.
361    #[br(if(header_version == 4))]
362    pub v4_signature_size: Option<u32>,
363}
364
365impl HeaderV3 {
366    pub(crate) const PAGE_SIZE: usize = 4096;
367
368    /// Returns the boot image header's version number.
369    #[must_use]
370    pub const fn header_version(&self) -> u32 {
371        if self.v4_signature_size.is_some() {
372            4
373        } else {
374            3
375        }
376    }
377    pub(crate) const fn header_size(&self) -> u32 {
378        if self.v4_signature_size.is_some() {
379            1584
380        } else {
381            1580
382        }
383    }
384    pub(crate) const fn get_padding(size: usize) -> usize {
385        // Equivalent to `size.div_ceil(PAGE_SIZE) * PAGE_SIZE - size`
386        // or `PAGE_SIZE - (size % PAGE_SIZE)) % PAGE_SIZE`, but more efficient.
387        (Self::PAGE_SIZE - (size % Self::PAGE_SIZE)) % Self::PAGE_SIZE
388    }
389    /// Returns the kernel's position in the boot image.
390    ///
391    /// Hardcoded to the page size, which is 4096.
392    #[must_use]
393    pub const fn kernel_position() -> usize {
394        Self::PAGE_SIZE
395    }
396    /// Returns the ramdisk's position in the boot image.
397    #[must_use]
398    pub const fn ramdisk_position(&self) -> usize {
399        Self::kernel_position()
400            + self.kernel_size as usize
401            + Self::get_padding(self.kernel_size as usize)
402    }
403    /// Returns the boot signature's position in the boot image.
404    ///
405    /// Note that this section is undefined in version 3.
406    #[must_use]
407    pub const fn bootsig_position(&self) -> usize {
408        self.ramdisk_position()
409            + self.ramdisk_size as usize
410            + Self::get_padding(self.ramdisk_size as usize)
411    }
412}
413
414/// Standard Android boot image header for versions 0 through 4
415#[derive(Clone, Debug, PartialEq, Eq, Hash)]
416pub enum Header {
417    /// Header for versions 0-2
418    V0(HeaderV0),
419    /// Header for versions 3-4
420    V3(HeaderV3),
421}
422
423impl Header {
424    /// Parses a standard Android boot image header from a reader.
425    ///
426    /// # Errors
427    ///
428    /// This returns an error if reading fails or if the header is invalid.
429    pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self, binrw::Error> {
430        reader.seek(binrw::io::SeekFrom::Start(0x28))?;
431        let mut version_buf = [0u8; 4];
432        reader.read_exact(&mut version_buf)?;
433        reader.seek(binrw::io::SeekFrom::Start(0))?;
434
435        // TODO: on next breaking change bump binrw
436        // TODO: on next breaking change, make `Header` implement/use binrw's traits!
437        Ok(match u32::from_le_bytes(version_buf) {
438            0..=2 => Self::V0(HeaderV0::read(reader)?),
439            3 | 4 => Self::V3(HeaderV3::read(reader)?),
440            version => {
441                return Err(binrw::Error::AssertFail {
442                    pos: 0x28,
443                    message: format!("Unknown header version: {version}"),
444                })
445            }
446        })
447    }
448    /// Serializes a standard Android boot image header to a writer.
449    ///
450    /// Note that you must write the kernel, ramdisk, etc. yourself.
451    ///
452    /// # Errors
453    ///
454    /// This forwards errors from `writer`.
455    pub fn write<W: Write>(&self, writer: &mut W) -> Result<(), binrw::Error> {
456        let writer = &mut NoSeek::new(writer);
457        match self {
458            Self::V0(hdr) => hdr.write(writer),
459            Self::V3(hdr) => hdr.write(writer),
460        }
461    }
462    /// Returns the boot image header's version number.
463    #[must_use]
464    pub const fn header_version(&self) -> u32 {
465        match self {
466            Self::V0(hdr) => hdr.header_version(),
467            Self::V3(hdr) => hdr.header_version(),
468        }
469    }
470    /// Returns the boot image header's OS version and patch level.
471    #[must_use]
472    pub const fn osversionpatch(&self) -> OsVersionPatch {
473        match self {
474            Self::V0(hdr) => hdr.osversionpatch,
475            Self::V3(hdr) => hdr.osversionpatch,
476        }
477    }
478    /// Returns the kernel's position in the boot image.
479    #[must_use]
480    pub const fn kernel_position(&self) -> usize {
481        match self {
482            Self::V0(hdr) => hdr.kernel_position(),
483            Self::V3(_) => HeaderV3::kernel_position(),
484        }
485    }
486    /// Returns the kernel's size.
487    #[must_use]
488    pub const fn kernel_size(&self) -> u32 {
489        match self {
490            Self::V0(hdr) => hdr.kernel_size,
491            Self::V3(hdr) => hdr.kernel_size,
492        }
493    }
494    /// Returns the ramdisk's position in the boot image.
495    #[must_use]
496    pub const fn ramdisk_position(&self) -> usize {
497        match self {
498            Self::V0(hdr) => hdr.ramdisk_position(),
499            Self::V3(hdr) => hdr.ramdisk_position(),
500        }
501    }
502    /// Returns the ramdisk's size.
503    #[must_use]
504    pub const fn ramdisk_size(&self) -> u32 {
505        match self {
506            Self::V0(hdr) => hdr.ramdisk_size,
507            Self::V3(hdr) => hdr.ramdisk_size,
508        }
509    }
510    /// Returns the page size in bytes.
511    #[must_use]
512    pub const fn page_size(&self) -> usize {
513        match self {
514            Self::V0(hdr) => hdr.page_size as usize,
515            Self::V3(_) => HeaderV3::PAGE_SIZE,
516        }
517    }
518    /// Returns the kernel command line.
519    #[must_use]
520    pub const fn cmdline(&self) -> &[u8; 512 + 1024] {
521        match self {
522            Self::V0(hdr) => &hdr.cmdline,
523            Self::V3(hdr) => &hdr.cmdline,
524        }
525    }
526}
527
528#[cfg(test)]
529mod tests {
530    use alloc::vec::Vec;
531    use binrw::io::Cursor;
532    use expect_test_bytes::expect_file;
533
534    use super::*;
535
536    #[test]
537    fn simple_write_read() {
538        fn pad_slice_to_array<const N: usize>(slice: &[u8]) -> [u8; N] {
539            let mut arr = [0u8; N];
540            let len = slice.len().min(N);
541            arr[..len].copy_from_slice(&slice[..len]);
542            arr
543        }
544        let expected_header = Header::V3(HeaderV3 {
545            kernel_size: 0x7357_0001,
546            ramdisk_size: 0x7357_0002,
547            osversionpatch: OsVersionPatch(0x7357_0003),
548            cmdline: Box::new(pad_slice_to_array(b"example")),
549            v4_signature_size: None,
550        });
551
552        let mut actual_bytes = Vec::new();
553        expected_header
554            .write(&mut Cursor::new(&mut actual_bytes))
555            .unwrap();
556
557        expect_file!["test_data/standard/simple_write_read"].assert_eq(&actual_bytes);
558
559        let actual_header = Header::parse(&mut Cursor::new(&actual_bytes)).unwrap();
560
561        assert_eq!(expected_header, actual_header);
562
563        let either_header = crate::EitherHeader::read(&mut Cursor::new(&actual_bytes)).unwrap();
564
565        assert_eq!(
566            crate::EitherHeader::Standard(expected_header),
567            either_header
568        );
569    }
570}