diff --git a/src/blob.rs b/src/blob.rs index 3c7d43d..ac1c31a 100644 --- a/src/blob.rs +++ b/src/blob.rs @@ -34,8 +34,7 @@ type Result = core::result::Result; pub(crate) const DTB_OPTIMAL_ALIGN: usize = 8; /// 4-byte magic signature of devicetree blobs. pub const DTB_MAGIC: [u8; 4] = 0xd00d_feed_u32.to_be_bytes(); -pub(crate) const LAST_COMPATIBLE_VERSION: u32 = 16; -const MEM_RESERVE_BLOCK_OPTIMAL_ALIGN: usize = 8; +pub(crate) const LAST_COMPATIBLE_VERSION: u32 = 0x10; const STRUCT_BLOCK_OPTIMAL_ALIGN: usize = 4; #[derive(AsBytes)] @@ -219,6 +218,9 @@ impl Devicetree { .ok_or(BlobError::InvalidTotalsize) } + /// Given that: + /// - header field `totalsize` is valid + /// /// Ensures that: /// - header fields `off_dt_struct` and `size_dt_struct` can be used to obtain an aligned and /// in-bounds block @@ -230,7 +232,7 @@ impl Devicetree { return Err(BlobError::IncompatibleVersion); } - let exact_size = u32::from_be(header.totalsize) as usize; + let exact_size = self.exact_size() as usize; let (offset, size) = Option::zip( usize::try_from(u32::from_be(header.off_dt_struct)).ok(), usize::try_from(u32::from_be(header.size_dt_struct)).ok(), @@ -271,7 +273,7 @@ impl Devicetree { /// The devicetree blob specification version. /// - /// It has been at 17 ever since version 0.1 of the spec. + /// It has been at `0x11` ever since version 0.1 of the spec. pub fn version(&self) -> u32 { u32::from_be(self.header().version) } @@ -375,16 +377,16 @@ impl Devicetree { /// Iterates over the memory reservation block. pub fn mem_reserve_entries(&self) -> Result> { - let offset = usize::try_from(u32::from_be(self.header().off_mem_rsvmap)) - .map_err(|_| BlobError::BlockOutOfBounds)?; - if offset % MEM_RESERVE_BLOCK_OPTIMAL_ALIGN != 0 { + let offset = u32::from_be(self.header().off_mem_rsvmap); + if offset % DTB_OPTIMAL_ALIGN as u32 != 0 { return Err(BlobError::UnalignedBlock); } + let offset = usize::try_from(offset).map_err(|_| BlobError::BlockOutOfBounds)?; Ok(MemReserveEntries { blob: self .blob - .get(offset / MEM_RESERVE_BLOCK_OPTIMAL_ALIGN..) + .get(offset / DTB_OPTIMAL_ALIGN..) .ok_or(BlobError::BlockOutOfBounds)?, }) } @@ -563,5 +565,5 @@ pub(crate) struct RawReserveEntry { } impl RawReserveEntry { - pub const FIELD_COUNT: usize = size_of::() / MEM_RESERVE_BLOCK_OPTIMAL_ALIGN; + pub const FIELD_COUNT: usize = size_of::() / DTB_OPTIMAL_ALIGN; } diff --git a/tests/test.rs b/tests/test.rs index 1871f03..e922f92 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -1,16 +1,87 @@ -use std::{ptr::NonNull, sync::OnceLock}; +use std::{fmt::Debug, ptr::NonNull, sync::OnceLock}; use deforest::{ alloc::DevicetreeBuilder, blob::Devicetree, fallible_iterator::FallibleIterator, prop_value::{self, RegBlock}, - BlobError, DeserializeProperty, NodeContext, + BlobError, DeserializeProperty, Error, NodeContext, }; +use zerocopy::{AsBytes, FromBytes, FromZeroes}; const UNALIGNED_BLOB: &[u8] = include_bytes!("tree.dtb"); const FORMATTED: &str = include_str!("formatted.txt"); +macro_rules! assert_matches { + ($left:expr, $($pattern:pat_param)|+ $(if $guard:expr)?) => { + match $left { + $( $pattern )|+ $( if $guard )? => {} + ref left_val => assert_matches_failed( + left_val, + stringify!($($pattern)|+ $(if $guard)?), + ), + } + }; +} + +#[track_caller] +fn assert_matches_failed(left: &T, right: &str) -> ! { + panic!( + "\ +assertion `left matches right` failed +left: {left:?} +right: {right}" + ); +} + +#[derive(AsBytes, FromBytes, FromZeroes)] +#[repr(C)] +struct Header { + magic: [u8; 4], + totalsize: u32, + off_dt_struct: u32, + off_dt_strings: u32, + off_mem_rsvmap: u32, + version: u32, + last_comp_version: u32, + boot_cpuid_phys: u32, + size_dt_strings: u32, + size_dt_struct: u32, +} + +impl Header { + const EMPTY: Self = Self { + magic: deforest::blob::DTB_MAGIC, + totalsize: 0x48_u32.to_be(), + off_dt_struct: 0x38_u32.to_be(), + off_dt_strings: 0x48_u32.to_be(), + off_mem_rsvmap: 0x28_u32.to_be(), + version: 17_u32.to_be(), + last_comp_version: 16_u32.to_be(), + boot_cpuid_phys: 0, + size_dt_strings: 0, + size_dt_struct: 0x10_u32.to_be(), + }; +} + +/// Zero-initialized header followed by an empty mem-reserve array and these +/// struct tokens: +/// +/// ```text +/// BeginNode { c"" }, EndNode, End +/// ``` +const EMPTY_BUF: [u64; 9] = [ + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0x0000_0001_0000_0000_u64.to_be(), + 0x0000_0002_0000_0009_u64.to_be(), +]; + static DT: OnceLock> = OnceLock::new(); fn dt() -> &'static Devicetree { @@ -25,6 +96,24 @@ fn print() { assert_eq!(formatted, FORMATTED); } +#[test] +fn header() { + let dt = dt(); + assert_eq!(dt.exact_size(), 0x2f3c); + assert_eq!(dt.version(), 0x11); + assert_eq!(dt.last_comp_version(), 0x10); + assert_eq!(dt.boot_core_id(), 0); + + assert_eq!(dt.mem_reserve_entries().unwrap().count().unwrap(), 0); +} + +#[test] +fn get_blob() { + let dt = dt(); + assert_eq!(dt.blob().as_bytes(), dt.blob_u8()); + assert_eq!(dt.blob_u32().as_bytes(), dt.blob_u8()); +} + #[test] fn cpu_count() { let root_node = dt().root_node().unwrap(); @@ -42,7 +131,41 @@ fn paths() { let dt = dt(); let root_node = dt.root_node().unwrap(); + assert_eq!( + dt.get_node("/").unwrap().unwrap().source_name().unwrap(), + "/" + ); + assert_eq!( + dt.get_node_strict("/") + .unwrap() + .unwrap() + .source_name() + .unwrap(), + "/" + ); + assert_eq!( + dt.get_node::<[&str]>(&[]) + .unwrap() + .unwrap() + .source_name() + .unwrap(), + "/" + ); + assert_eq!( + dt.get_node_strict::<[&str]>(&[]) + .unwrap() + .unwrap() + .source_name() + .unwrap(), + "/" + ); + + assert_matches!(dt.get_node(&["leds"]).unwrap(), None); + assert_matches!(dt.get_node_strict(&["leds"]).unwrap(), None); + let soc_node = root_node.get_child_strict("soc").unwrap().unwrap(); + assert_matches!(soc_node.get_property("status").unwrap(), None); + let address_cells = soc_node.get_property("#address-cells").unwrap().unwrap(); let prop_value::AddressCells(address_cells) = address_cells.contextless_parse().unwrap(); let size_cells = soc_node.get_property("#size-cells").unwrap().unwrap(); @@ -61,7 +184,10 @@ fn paths() { [RegBlock(0x7e104000, 0x10)] ); - let node1 = dt.get_node_strict(&["soc", "gpio@7e200000", "i2c0"]).unwrap().unwrap(); + let node1 = dt + .get_node_strict(&["soc", "gpio@7e200000", "i2c0"]) + .unwrap() + .unwrap(); let prop1 = node1.get_property("phandle").unwrap().unwrap(); assert_eq!(prop1.value(), [0, 0, 0, 0x0a]); @@ -70,6 +196,18 @@ fn paths() { assert_eq!(prop2.contextless_parse::<&str>(), Ok("led0")); } +#[test] +fn err_paths() { + let dt = dt(); + + assert_matches!(dt.get_node(""), Err(Error::InvalidPath)); + assert_matches!(dt.get_node_strict(""), Err(Error::InvalidPath)); + assert_matches!(dt.get_node(&["/"]), Ok(None)); + assert_matches!(dt.get_node_strict(&["/"]), Ok(None)); + assert_matches!(dt.get_node(&[""]), Ok(None)); + assert_matches!(dt.get_node_strict(&[""]), Ok(None)); +} + #[test] fn multiple_children() { let clocks_node = dt().get_node_strict(&["clocks"]).unwrap().unwrap(); @@ -89,7 +227,7 @@ fn multiple_children() { .get_children("thermal") .iterator() .map(|res| res.unwrap()); - assert!(iter.next().is_none()); + assert_matches!(iter.next(), None); } #[test] @@ -107,21 +245,157 @@ fn build() { #[cfg_attr(miri, ignore)] // miri warns on pointer provenance violations fn from_ptr() { let original = dt(); - let from_ptr = unsafe { - // kill pointer provenance; this code doesn't work with strict provenance - let ptr = NonNull::new(original.blob().as_ptr() as usize as *mut u64).unwrap(); - Devicetree::from_ptr(ptr) - } - .unwrap(); + let from_ptr = unsafe { Devicetree::from_ptr(NonNull::from(original.blob()).cast()) }.unwrap(); assert_eq!(original.blob().len(), from_ptr.blob().len()); } -#[test] -fn from_unaligned() { - // dt already contains a normal usage of Devicetree::from_unaligned +macro_rules! blob { + { + buf: $buf_init:expr, + header: { $($field:ident: $val:expr),* $(,)? } $(,)? + } => {{ + let mut buf = $buf_init; + let header = Header::mut_from_prefix(buf.as_bytes_mut()).unwrap(); + *header = Header { $($field: $val,)* ..Header::EMPTY }; + buf + }}; +} + +fn all_safe_ctors_return(blob: &[u64], err: Option) { + assert_eq!(Devicetree::from_slice(blob).err(), err); + assert_eq!(Devicetree::from_unaligned(blob.as_bytes()).err(), err); + assert_eq!(Devicetree::from_vec(blob.to_vec()).err(), err); +} + +fn all_ctors_return(blob: &[u64], err: Option) { + #[cfg(not(miri))] assert_eq!( - Devicetree::from_unaligned(&[]).err(), - Some(BlobError::UnexpectedEnd) + unsafe { Devicetree::from_ptr(NonNull::from(blob).cast()) }.err(), + err + ); + all_safe_ctors_return(blob, err); +} + +#[test] +fn empty() { + all_safe_ctors_return(&[], Some(BlobError::UnexpectedEnd)); + all_safe_ctors_return(&[0], Some(BlobError::UnexpectedEnd)); + + let valid_empty = blob! { buf: EMPTY_BUF, header: {} }; + all_safe_ctors_return(&valid_empty, None); + let dt = Devicetree::from_slice(&valid_empty).unwrap(); + assert_matches!(dt.root_node().unwrap().items().next().unwrap(), None); +} + +#[test] +fn err_invalid_header() { + all_ctors_return( + &blob! { + buf: EMPTY_BUF, + header: { magic: [0; 4], last_comp_version: 0x11 }, + }, + Some(BlobError::NoMagicSignature), + ); + all_ctors_return( + &blob! { + buf: EMPTY_BUF, + header: { last_comp_version: 0x11 }, + }, + Some(BlobError::IncompatibleVersion), + ); + all_safe_ctors_return( + &blob! { buf: EMPTY_BUF, header: {} }[..5], + Some(BlobError::InvalidTotalsize), + ); + all_ctors_return( + &blob! { + buf: EMPTY_BUF, + header: { totalsize: 0x20_u32.to_be() }, + }, + Some(BlobError::InvalidTotalsize), + ); +} + +#[test] +fn err_invalid_blocks() { + // struct + all_ctors_return( + &blob! { + buf: EMPTY_BUF, + header: { off_dt_struct: 0x3a_u32.to_be(), size_dt_struct: 8_u32.to_be() }, + }, + Some(BlobError::UnalignedBlock), + ); + all_ctors_return( + &blob! { + buf: EMPTY_BUF, + header: { size_dt_struct: 0x0e_u32.to_be() }, + }, + Some(BlobError::UnalignedBlock), + ); + all_ctors_return( + &blob! { + buf: EMPTY_BUF, + header: { off_dt_struct: 0x40_u32.to_be() }, + }, + Some(BlobError::BlockOutOfBounds), + ); + all_ctors_return( + &blob! { + buf: EMPTY_BUF, + header: { size_dt_struct: 0x18_u32.to_be() }, + }, + Some(BlobError::BlockOutOfBounds), + ); + all_ctors_return( + &blob! { + buf: EMPTY_BUF, + header: { off_dt_struct: 0x8000_0038_u32.to_be(), size_dt_struct: 0x8000_0010_u32.to_be() }, + }, + Some(BlobError::BlockOutOfBounds), + ); + + // strings + all_ctors_return( + &blob! { + buf: EMPTY_BUF, + header: { off_dt_strings: 0x49_u32.to_be() }, + }, + Some(BlobError::BlockOutOfBounds), + ); + all_ctors_return( + &blob! { + buf: EMPTY_BUF, + header: { size_dt_strings: 1_u32.to_be() }, + }, + Some(BlobError::BlockOutOfBounds), + ); + all_ctors_return( + &blob! { + buf: EMPTY_BUF, + header: { off_dt_strings: 0x8000_0048_u32.to_be(), size_dt_strings: 0x8000_0000_u32.to_be() }, + }, + Some(BlobError::BlockOutOfBounds), + ); + + // mem reservation + assert_matches!( + Devicetree::from_slice(&blob! { + buf: EMPTY_BUF, + header: { off_mem_rsvmap: 0x2c_u32.to_be() }, + }) + .unwrap() + .mem_reserve_entries(), + Err(BlobError::UnalignedBlock) + ); + assert_matches!( + Devicetree::from_slice(&blob! { + buf: EMPTY_BUF, + header: { off_mem_rsvmap: 0x8000_0028_u32.to_be() }, + }) + .unwrap() + .mem_reserve_entries(), + Err(BlobError::BlockOutOfBounds) ); } @@ -130,6 +404,32 @@ mod derive { use super::*; use deforest::{blob::Cursor, DeserializeNode}; + #[test] + fn parse_root() { + #[derive(Default, DeserializeNode)] + struct RootNode<'dtb> { + #[dt(name)] + name: &'dtb str, + model: &'dtb str, + #[dt(child)] + chosen: Option>, + } + + #[derive(Default, DeserializeNode)] + struct ChosenNode<'dtb> { + bootargs: &'dtb str, + } + + let dt = dt(); + let root_blob_node = dt.root_node().unwrap(); + let root_node0: RootNode<'_> = dt.parse_root().unwrap(); + let (root_node1, _) = + RootNode::deserialize(&root_blob_node, NodeContext::default()).unwrap(); + assert_eq!(root_node0.name, ""); + assert_eq!(root_node0.model, "Raspberry Pi 2 Model B"); + assert_eq!(root_node1.chosen.unwrap().bootargs, ""); + } + #[test] fn self_fields() { #[derive(Default, DeserializeNode)] @@ -144,9 +444,7 @@ mod derive { let dma_node = dt().get_node(&["soc", "dma"]).unwrap().unwrap(); let cursor = dma_node.start_cursor(); - let dma_node = DmaNode::deserialize(&dma_node, NodeContext::default()) - .unwrap() - .0; + let (dma_node, _) = DmaNode::deserialize(&dma_node, NodeContext::default()).unwrap(); assert_eq!(dma_node.start_cursor, Some(cursor)); assert_eq!(dma_node.name, "dma@7e007000"); assert_eq!(dma_node.unit_address.unwrap(), "7e007000");