// The flattened device tree header.
//
// A blob starts with a 40-byte header that frames four blocks. Reading it is
// the first thing every entry point does, and it is also where a malformed
// blob has to be refused: the four size fields are the only statement about
// where the blocks live, so if they do not tile the file exactly there is no
// way to tell which one is wrong.
///|
/// The on-disk magic, `0xd00dfeed`.
///
/// Spelled out rather than derived so a reader comparing against a hex dump
/// can check it at a glance.
const DTB_MAGIC : UInt32 = 0xD00D_FEED
///|
/// Size of the header in bytes, constant across header versions.
const DTB_HEADER_SIZE : Int64 = 40L
///|
/// The last header version this module accepts.
///
/// Version 17 is what dtc has emitted since 2011. Earlier versions differ only
/// in the meaning of `last_comp_version` and in whether the blocks are
/// 8-byte aligned, so they are accepted but reported through
/// `Header::is_modern` rather than silently treated as 17.
const DTB_LAST_COMP_VERSION : UInt32 = 17
///|
/// The four blocks a blob is made of, with their offsets resolved against the
/// start of the file.
///
/// Offsets here are relative to the start of the blob, which is what every
/// tool that walks the tree needs. Absolute offsets would break the moment a
/// blob is embedded inside another container.
pub(all) struct Header {
/// Header version that produced the blob.
totalsize : Int64
/// Where the struct block begins.
off_dt_struct : Int64
/// Where the strings block begins.
off_dt_strings : Int64
/// Where the memory reservation map begins.
off_mem_rsvmap : Int64
/// Header version.
version : UInt32
/// Oldest header version a reader must implement to parse this blob.
last_comp_version : UInt32
/// Physical ID of the boot CPU.
boot_cpuid_phys : UInt32
/// Length of the strings block.
size_dt_strings : Int64
/// Length of the struct block.
size_dt_struct : Int64
}
///|
/// Reads the header at the start of a blob and validates that the four blocks
/// tile the file.
///
/// The alignment requirements from the specification are checked here rather
/// than at each use site: the struct block is 4-byte aligned, and in version 17
/// and later so is the strings block and the memory reservation map. A blob
/// that violates them is rejected before any block is decoded, so no later
/// parser has to defend against a misaligned read.
pub fn parse_header(bytes : Bytes) -> Header raise DtbError {
let bounds = Bounds::of_bytes(bytes)
let total = bounds.length()
if total < DTB_HEADER_SIZE {
raise UnexpectedEnd(offset=0L, needed=DTB_HEADER_SIZE, available=total)
}
let cur = Cursor::big(0L)
let (magic, cur) = bounds.read_u32(cur)
if magic != DTB_MAGIC {
raise BadMagic(magic~)
}
let (totalsize, cur) = bounds.read_u32(cur)
let (off_dt_struct, cur) = bounds.read_u32(cur)
let (off_dt_strings, cur) = bounds.read_u32(cur)
let (off_mem_rsvmap, cur) = bounds.read_u32(cur)
let (version, cur) = bounds.read_u32(cur)
let (last_comp_version, cur) = bounds.read_u32(cur)
let (boot_cpuid_phys, cur) = bounds.read_u32(cur)
let (size_dt_strings, _) = bounds.read_u32(cur)
// Version 17 grew a second size word. Older blobs stop at 36 bytes of
// content and leave the struct size implied by the remaining space, which
// the caller cannot reconstruct reliably, so the version gate comes first.
let size_dt_struct = if version >= DTB_LAST_COMP_VERSION {
let (v, _) = bounds.read_u32(Cursor::big(36L))
v
} else {
raise Unsupported(
what="header version \{version.reinterpret_as_int()}, need \{DTB_LAST_COMP_VERSION.reinterpret_as_int()} or later",
)
}
let header = Header::{
totalsize: totalsize.to_int64(),
off_dt_struct: off_dt_struct.to_int64(),
off_dt_strings: off_dt_strings.to_int64(),
off_mem_rsvmap: off_mem_rsvmap.to_int64(),
version,
last_comp_version,
boot_cpuid_phys,
size_dt_strings: size_dt_strings.to_int64(),
size_dt_struct: size_dt_struct.to_int64(),
}
header.validate(bounds)
header
}
///|
/// Checks that the header describes four blocks that fit inside the file
/// without overlapping the header or each other.
///
/// A single damaged size field usually still leaves every individual block
/// readable, so the check that catches it is the tiling one: the blocks have to
/// appear in the order the format requires and together cover the file with
/// nothing but padding between them.
fn Header::validate(self : Header, bounds : Bounds) -> Unit raise DtbError {
let total = bounds.length()
if self.totalsize <= DTB_HEADER_SIZE || self.totalsize > total {
raise BadValue(field="totalsize", value=self.totalsize)
}
// Trusting totalsize over the actual buffer length is deliberate: a blob may
// be followed by padding inside a larger file (a FIT image, say), and the
// header is the only thing that says where it ends.
let end = self.totalsize
bounds.require(self.off_mem_rsvmap, 16L, "mem_rsvmap")
bounds.require(self.off_dt_struct, self.size_dt_struct, "struct block")
bounds.require(self.off_dt_strings, self.size_dt_strings, "strings block")
if self.off_mem_rsvmap < DTB_HEADER_SIZE {
raise Inconsistent(
what="mem_rsvmap",
detail="starts at \{self.off_mem_rsvmap} inside the header",
)
}
if self.off_dt_struct % 4L != 0L {
raise Inconsistent(what="struct block", detail="not 4-byte aligned")
}
if self.off_dt_strings % 4L != 0L {
raise Inconsistent(what="strings block", detail="not 4-byte aligned")
}
if self.off_dt_struct + self.size_dt_struct > self.off_dt_strings {
raise Inconsistent(
what="struct block",
detail="ends at \{self.off_dt_struct + self.size_dt_struct} past the strings block at \{self.off_dt_strings}",
)
}
if self.off_dt_strings + self.size_dt_strings > end {
raise Inconsistent(
what="strings block",
detail="ends at \{self.off_dt_strings + self.size_dt_strings} past the blob end at \{end}",
)
}
if self.version < self.last_comp_version {
raise BadValue(
field="last_comp_version",
value=self.last_comp_version.to_int64(),
)
}
}
///|
/// Whether the blob uses the version 17 alignment rules.
///
/// Version 16 and earlier packed the strings block without padding, so callers
/// that care about byte-identical round-tripping have to know which rules were
/// in force when the input was produced.
pub fn Header::is_modern(self : Header) -> Bool {
self.last_comp_version >= 16
}
///|
/// Total size the header claims, which is also the length a verbatim copy has
/// to preserve.
pub fn Header::size(self : Header) -> Int64 {
self.totalsize
}