use alloc::{boxed::Box, format};
use binrw::{
binrw,
io::{NoSeek, Read, Seek, SeekFrom, Write},
BinRead, BinWrite,
};
use crate::version::OsVersionPatch;
#[binrw]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[brw(little, magic = b"ANDROID!")]
pub struct HeaderV0 {
pub kernel_size: u32,
pub kernel_addr: u32,
pub ramdisk_size: u32,
pub ramdisk_addr: u32,
pub second_bootloader_size: u32,
pub second_bootloader_addr: u32,
pub tags_addr: u32,
pub page_size: u32,
#[br(temp)]
#[bw(calc = self.header_version())]
header_version: u32,
pub osversionpatch: OsVersionPatch,
pub board_name: [u8; 16],
#[br(temp)]
#[bw(calc = *self.cmdline.first_chunk().unwrap())]
cmdline_part_1: [u8; 512],
pub hash_digest: [u8; 32],
#[br(temp)]
#[bw(calc = *self.cmdline.last_chunk().unwrap())]
cmdline_part_2: [u8; 1024],
#[br(calc = [cmdline_part_1.as_slice(), cmdline_part_2.as_slice()].concat().try_into().unwrap())]
#[bw(ignore)]
pub cmdline: Box<[u8; 512 + 1024]>,
#[br(args(header_version))]
pub versioned: HeaderV0Versioned,
}
impl HeaderV0 {
pub(crate) const fn get_padding(&self, size: usize) -> usize {
let page_size = self.page_size as usize;
(page_size - (size % page_size)) % page_size
}
#[must_use]
pub const fn header_version(&self) -> u32 {
match self.versioned {
HeaderV0Versioned::V0 => 0,
HeaderV0Versioned::V1 { .. } => 1,
HeaderV0Versioned::V2 { .. } => 2,
}
}
#[must_use]
pub const fn kernel_position(&self) -> usize {
1660 + self.get_padding(1660)
}
#[must_use]
pub const fn ramdisk_position(&self) -> usize {
self.kernel_position()
+ self.kernel_size as usize
+ self.get_padding(self.kernel_size as usize)
}
#[must_use]
pub const fn second_bootloader_position(&self) -> usize {
self.ramdisk_position()
+ self.ramdisk_size as usize
+ self.get_padding(self.ramdisk_size as usize)
}
#[must_use]
pub const fn recovery_dtbo_position(&self) -> usize {
self.second_bootloader_position()
+ self.second_bootloader_size as usize
+ self.get_padding(self.second_bootloader_size as usize)
}
#[must_use]
pub const fn dtb_position(&self) -> Option<usize> {
match self.versioned {
HeaderV0Versioned::V0 => None,
HeaderV0Versioned::V1 {
recovery_dtbo_size, ..
}
| HeaderV0Versioned::V2 {
recovery_dtbo_size, ..
} => Some(
self.second_bootloader_position()
+ recovery_dtbo_size as usize
+ self.get_padding(recovery_dtbo_size as usize),
),
}
}
#[must_use]
#[expect(
clippy::missing_panics_doc,
reason = "dtb_position always returns Some on V1 and V2"
)]
pub const fn boot_image_size(&self) -> usize {
match self.versioned {
HeaderV0Versioned::V0 => self.recovery_dtbo_position(),
HeaderV0Versioned::V1 { .. } => self.dtb_position().unwrap(),
HeaderV0Versioned::V2 { dtb_size, .. } => {
self.dtb_position().unwrap()
+ dtb_size as usize
+ self.get_padding(dtb_size as usize)
}
}
}
#[cfg(feature = "hash")]
#[cfg_attr(docsrs, doc(cfg(feature = "hash")))]
pub fn compute_hash_digest<R: Read, D: digest::Digest>(
kernel: Option<&mut R>,
ramdisk: Option<&mut R>,
second_bootloader: Option<&mut R>,
recovery_dtbo: Option<&mut R>,
dtb: Option<&mut R>,
) -> binrw::io::Result<[u8; 32]> {
let mut hasher = D::new();
for r in [kernel, ramdisk, second_bootloader, recovery_dtbo, dtb] {
if let Some(r) = r {
let mut buf = alloc::vec::Vec::new();
r.read_to_end(&mut buf)?;
hasher.update(&buf);
hasher.update(
u32::try_from(buf.len())
.map_err(|_| binrw::io::ErrorKind::InvalidInput)?
.to_le_bytes(),
);
} else {
hasher.update(0u32.to_le_bytes());
}
}
let digest = hasher.finalize();
let mut buf = [0; _];
buf[..digest.len()].copy_from_slice(&digest);
Ok(buf)
}
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
pub fn full_write<W: Write + Seek, R: Read>(
&self,
writer: &mut W,
kernel: Option<&mut R>,
ramdisk: Option<&mut R>,
second_bootloader: Option<&mut R>,
recovery_dtbo: Option<&mut R>,
dtb: Option<&mut R>,
) -> binrw::BinResult<()> {
let w = writer;
self.write(w)?;
if let Some(r) = kernel {
w.seek(SeekFrom::Start(self.kernel_position() as u64))?;
std::io::copy(r, w)?;
}
if let Some(r) = ramdisk {
w.seek(SeekFrom::Start(self.ramdisk_position() as u64))?;
std::io::copy(r, w)?;
}
if let Some(r) = second_bootloader {
w.seek(SeekFrom::Start(self.second_bootloader_position() as u64))?;
std::io::copy(r, w)?;
}
if let Some(r) = recovery_dtbo {
w.seek(SeekFrom::Start(self.recovery_dtbo_position() as u64))?;
std::io::copy(r, w)?;
}
if let Some(dtb_position) = self.dtb_position() {
if let Some(r) = dtb {
w.seek(SeekFrom::Start(dtb_position as u64))?;
std::io::copy(r, w)?;
}
}
w.seek(SeekFrom::Start(self.boot_image_size() as u64))?;
Ok(())
}
}
#[binrw]
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
#[br(import(header_version: u32))]
#[br(pre_assert([0,1,2].contains(&header_version), "invalid header version: {header_version}"))]
pub enum HeaderV0Versioned {
#[br(pre_assert(header_version == 0))]
V0,
#[br(pre_assert(header_version == 1))]
V1 {
recovery_dtbo_size: u32,
recovery_dtbo_addr: u64,
#[br(temp, assert(header_size == 1648))]
#[bw(calc = 1648)]
header_size: u32,
},
#[br(pre_assert(header_version == 2))]
V2 {
recovery_dtbo_size: u32,
recovery_dtbo_addr: u64,
#[br(temp, assert(header_size == 1660))]
#[bw(calc = 1660)]
header_size: u32,
dtb_size: u32,
dtb_addr: u64,
},
}
#[binrw]
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
#[brw(little, magic = b"ANDROID!")]
#[br(assert(header_size == self.header_size(), "invalid header size: {header_size}"))]
pub struct HeaderV3 {
pub kernel_size: u32,
pub ramdisk_size: u32,
pub osversionpatch: OsVersionPatch,
#[br(temp)]
#[bw(calc = self.header_size())]
header_size: u32,
#[brw(pad_before = 16)]
#[br(temp)]
#[br(assert(header_version == 3 || header_version == 4, "invalid header version: {header_version}"))]
#[bw(calc = self.header_version())]
header_version: u32,
pub cmdline: Box<[u8; 512 + 1024]>,
#[br(if(header_version == 4))]
pub v4_signature_size: Option<u32>,
}
impl HeaderV3 {
pub(crate) const PAGE_SIZE: usize = 4096;
#[must_use]
pub const fn header_version(&self) -> u32 {
if self.v4_signature_size.is_some() {
4
} else {
3
}
}
pub(crate) const fn header_size(&self) -> u32 {
if self.v4_signature_size.is_some() {
1584
} else {
1580
}
}
pub(crate) const fn get_padding(size: usize) -> usize {
(Self::PAGE_SIZE - (size % Self::PAGE_SIZE)) % Self::PAGE_SIZE
}
#[must_use]
pub const fn kernel_position() -> usize {
Self::PAGE_SIZE
}
#[must_use]
pub const fn ramdisk_position(&self) -> usize {
Self::kernel_position()
+ self.kernel_size as usize
+ Self::get_padding(self.kernel_size as usize)
}
#[must_use]
pub const fn bootsig_position(&self) -> usize {
self.ramdisk_position()
+ self.ramdisk_size as usize
+ Self::get_padding(self.ramdisk_size as usize)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub enum Header {
V0(HeaderV0),
V3(HeaderV3),
}
impl Header {
pub fn parse<R: Read + Seek>(reader: &mut R) -> Result<Self, binrw::Error> {
reader.seek(binrw::io::SeekFrom::Start(0x28))?;
let mut version_buf = [0u8; 4];
reader.read_exact(&mut version_buf)?;
reader.seek(binrw::io::SeekFrom::Start(0))?;
Ok(match u32::from_le_bytes(version_buf) {
0..=2 => Self::V0(HeaderV0::read(reader)?),
3 | 4 => Self::V3(HeaderV3::read(reader)?),
version => {
return Err(binrw::Error::AssertFail {
pos: 0x28,
message: format!("Unknown header version: {version}"),
})
}
})
}
pub fn write<W: Write>(&self, writer: &mut W) -> Result<(), binrw::Error> {
let writer = &mut NoSeek::new(writer);
match self {
Self::V0(hdr) => hdr.write(writer),
Self::V3(hdr) => hdr.write(writer),
}
}
#[must_use]
pub const fn header_version(&self) -> u32 {
match self {
Self::V0(hdr) => hdr.header_version(),
Self::V3(hdr) => hdr.header_version(),
}
}
#[must_use]
pub const fn osversionpatch(&self) -> OsVersionPatch {
match self {
Self::V0(hdr) => hdr.osversionpatch,
Self::V3(hdr) => hdr.osversionpatch,
}
}
#[must_use]
pub const fn kernel_position(&self) -> usize {
match self {
Self::V0(hdr) => hdr.kernel_position(),
Self::V3(_) => HeaderV3::kernel_position(),
}
}
#[must_use]
pub const fn kernel_size(&self) -> u32 {
match self {
Self::V0(hdr) => hdr.kernel_size,
Self::V3(hdr) => hdr.kernel_size,
}
}
#[must_use]
pub const fn ramdisk_position(&self) -> usize {
match self {
Self::V0(hdr) => hdr.ramdisk_position(),
Self::V3(hdr) => hdr.ramdisk_position(),
}
}
#[must_use]
pub const fn ramdisk_size(&self) -> u32 {
match self {
Self::V0(hdr) => hdr.ramdisk_size,
Self::V3(hdr) => hdr.ramdisk_size,
}
}
#[must_use]
pub const fn page_size(&self) -> usize {
match self {
Self::V0(hdr) => hdr.page_size as usize,
Self::V3(_) => HeaderV3::PAGE_SIZE,
}
}
#[must_use]
pub const fn cmdline(&self) -> &[u8; 512 + 1024] {
match self {
Self::V0(hdr) => &hdr.cmdline,
Self::V3(hdr) => &hdr.cmdline,
}
}
}
#[cfg(test)]
mod tests {
use alloc::vec::Vec;
use binrw::io::Cursor;
use expect_test_bytes::expect_file;
use super::*;
#[test]
fn simple_write_read() {
fn pad_slice_to_array<const N: usize>(slice: &[u8]) -> [u8; N] {
let mut arr = [0u8; N];
let len = slice.len().min(N);
arr[..len].copy_from_slice(&slice[..len]);
arr
}
let expected_header = Header::V3(HeaderV3 {
kernel_size: 0x7357_0001,
ramdisk_size: 0x7357_0002,
osversionpatch: OsVersionPatch(0x7357_0003),
cmdline: Box::new(pad_slice_to_array(b"example")),
v4_signature_size: None,
});
let mut actual_bytes = Vec::new();
expected_header
.write(&mut Cursor::new(&mut actual_bytes))
.unwrap();
expect_file!["test_data/standard/simple_write_read"].assert_eq(&actual_bytes);
let actual_header = Header::parse(&mut Cursor::new(&actual_bytes)).unwrap();
assert_eq!(expected_header, actual_header);
let either_header = crate::EitherHeader::read(&mut Cursor::new(&actual_bytes)).unwrap();
assert_eq!(
crate::EitherHeader::Standard(expected_header),
either_header
);
}
}