libfdt: Introduce Libfdt traits & Move node funcs

Decouple the Fdt type from the C functions, to allow easily supporting
the libfdt API through other types (e.g. a heap-allocated FdtOwned). The
trait will be intended as the single point for implementing thin
wrappers around the bindgen. Higher-level and user-facing types will be
provided with a safe API by these traits. The traits are decoupled
between mutable and immutable to afford the highest level of flexibility
to implementers. Note that this allows the RO API to be used by mutable
abstractions without risk of confusing the borrow checker as the API
deals with c_int offsets.

To make this easier to review, this commit only transfer basic node
functions dealing with node offsets and the rest of the FFI will be
moved in chuncks by future commits.

Reduce the unsafe blocks to the strict minimum (i.e. the FFI calls).

Test: m pvmfw
Test: atest liblibfdt.integration_test
Change-Id: Iee53dbd357499bc6c1562264d23be94de2c0196e
diff --git a/libs/libfdt/src/lib.rs b/libs/libfdt/src/lib.rs
index 72cd3e9..ce2a9f6 100644
--- a/libs/libfdt/src/lib.rs
+++ b/libs/libfdt/src/lib.rs
@@ -29,11 +29,12 @@
 
 use core::ffi::{c_int, c_void, CStr};
 use core::ops::Range;
-use core::ptr;
 use cstr::cstr;
 use result::{fdt_err, fdt_err_expect_zero, fdt_err_or_option};
 use zerocopy::AsBytes as _;
 
+use crate::libfdt::{Libfdt, LibfdtMut};
+
 /// Value of a #address-cells property.
 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
 enum AddrCells {
@@ -164,25 +165,14 @@
 impl<'a> FdtNode<'a> {
     /// Returns parent node.
     pub fn parent(&self) -> Result<Self> {
-        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
-        let ret = unsafe { libfdt_bindgen::fdt_parent_offset(self.fdt.as_ptr(), self.offset) };
-        let offset = fdt_err(ret)?;
+        let offset = self.fdt.parent_offset(self.offset)?;
 
         Ok(Self { fdt: self.fdt, offset })
     }
 
     /// Returns supernode with depth. Note that root is at depth 0.
     pub fn supernode_at_depth(&self, depth: usize) -> Result<Self> {
-        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
-        let ret = unsafe {
-            libfdt_bindgen::fdt_supernode_atdepth_offset(
-                self.fdt.as_ptr(),
-                self.offset,
-                depth.try_into().unwrap(),
-                ptr::null_mut(),
-            )
-        };
-        let offset = fdt_err(ret)?;
+        let offset = self.fdt.supernode_atdepth_offset(self.offset, depth)?;
 
         Ok(Self { fdt: self.fdt, offset })
     }
@@ -321,15 +311,7 @@
 
     /// Returns the compatible node of the given name that is next after this node.
     pub fn next_compatible(self, compatible: &CStr) -> Result<Option<Self>> {
-        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
-        let ret = unsafe {
-            libfdt_bindgen::fdt_node_offset_by_compatible(
-                self.fdt.as_ptr(),
-                self.offset,
-                compatible.as_ptr(),
-            )
-        };
-        let offset = fdt_err_or_option(ret)?;
+        let offset = self.fdt.node_offset_by_compatible(self.offset, compatible)?;
 
         Ok(offset.map(|offset| Self { fdt: self.fdt, offset }))
     }
@@ -340,17 +322,11 @@
     }
 
     fn address_cells(&self) -> Result<AddrCells> {
-        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor).
-        let ret = unsafe { libfdt_bindgen::fdt_address_cells(self.fdt.as_ptr(), self.offset) };
-
-        usize::try_from(ret).map_err(|_| FdtError::Internal)?.try_into()
+        self.fdt.address_cells(self.offset)?.try_into()
     }
 
     fn size_cells(&self) -> Result<SizeCells> {
-        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor).
-        let ret = unsafe { libfdt_bindgen::fdt_size_cells(self.fdt.as_ptr(), self.offset) };
-
-        usize::try_from(ret).map_err(|_| FdtError::Internal)?.try_into()
+        self.fdt.size_cells(self.offset)?.try_into()
     }
 
     /// Returns an iterator of subnodes
@@ -359,17 +335,13 @@
     }
 
     fn first_subnode(&self) -> Result<Option<Self>> {
-        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
-        let ret = unsafe { libfdt_bindgen::fdt_first_subnode(self.fdt.as_ptr(), self.offset) };
-        let offset = fdt_err_or_option(ret)?;
+        let offset = self.fdt.first_subnode(self.offset)?;
 
         Ok(offset.map(|offset| Self { fdt: self.fdt, offset }))
     }
 
     fn next_subnode(&self) -> Result<Option<Self>> {
-        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
-        let ret = unsafe { libfdt_bindgen::fdt_next_subnode(self.fdt.as_ptr(), self.offset) };
-        let offset = fdt_err_or_option(ret)?;
+        let offset = self.fdt.next_subnode(self.offset)?;
 
         Ok(offset.map(|offset| Self { fdt: self.fdt, offset }))
     }
@@ -380,15 +352,7 @@
     }
 
     fn next_node(&self, depth: usize) -> Result<Option<(Self, usize)>> {
-        let mut next_depth: c_int = depth.try_into().unwrap();
-        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
-        let ret = unsafe {
-            libfdt_bindgen::fdt_next_node(self.fdt.as_ptr(), self.offset, &mut next_depth)
-        };
-        let Ok(depth) = usize::try_from(next_depth) else {
-            return Ok(None);
-        };
-        if let Some(offset) = fdt_err_or_option(ret)? {
+        if let Some((offset, depth)) = self.fdt.next_node(self.offset, depth)? {
             Ok(Some((Self { fdt: self.fdt, offset }, depth)))
         } else {
             Ok(None)
@@ -427,31 +391,17 @@
     /// Returns the subnode of the given name. The name doesn't need to be nul-terminated.
     pub fn subnode(&self, name: &CStr) -> Result<Option<Self>> {
         let name = name.to_bytes();
-        let offset = self.subnode_offset(name)?;
+        let offset = self.fdt.subnode_offset_namelen(self.offset, name)?;
 
         Ok(offset.map(|offset| Self { fdt: self.fdt, offset }))
     }
 
     /// Returns the subnode of the given name bytes
     pub fn subnode_with_name_bytes(&self, name: &[u8]) -> Result<Option<Self>> {
-        let offset = self.subnode_offset(name)?;
+        let offset = self.fdt.subnode_offset_namelen(self.offset, name)?;
 
         Ok(offset.map(|offset| Self { fdt: self.fdt, offset }))
     }
-
-    fn subnode_offset(&self, name: &[u8]) -> Result<Option<c_int>> {
-        let namelen = name.len().try_into().unwrap();
-        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor).
-        let ret = unsafe {
-            libfdt_bindgen::fdt_subnode_offset_namelen(
-                self.fdt.as_ptr(),
-                self.offset,
-                name.as_ptr().cast::<_>(),
-                namelen,
-            )
-        };
-        fdt_err_or_option(ret)
-    }
 }
 
 impl<'a> PartialEq for FdtNode<'a> {
@@ -652,7 +602,7 @@
     /// Adds new subnodes to the given node.
     pub fn add_subnodes(&mut self, names: &[&CStr]) -> Result<()> {
         for name in names {
-            self.add_subnode_offset(name.to_bytes())?;
+            self.fdt.add_subnode_namelen(self.offset, name.to_bytes())?;
         }
         Ok(())
     }
@@ -660,7 +610,7 @@
     /// Adds a new subnode to the given node and return it as a FdtNodeMut on success.
     pub fn add_subnode(&'a mut self, name: &CStr) -> Result<Self> {
         let name = name.to_bytes();
-        let offset = self.add_subnode_offset(name)?;
+        let offset = self.fdt.add_subnode_namelen(self.offset, name)?;
 
         Ok(Self { fdt: self.fdt, offset })
     }
@@ -669,80 +619,46 @@
     /// on success.
     pub fn add_subnode_with_namelen(&'a mut self, name: &CStr, namelen: usize) -> Result<Self> {
         let name = &name.to_bytes()[..namelen];
-        let offset = self.add_subnode_offset(name)?;
+        let offset = self.fdt.add_subnode_namelen(self.offset, name)?;
 
         Ok(Self { fdt: self.fdt, offset })
     }
 
-    fn add_subnode_offset(&mut self, name: &[u8]) -> Result<c_int> {
-        let namelen = name.len().try_into().unwrap();
-        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor).
-        let ret = unsafe {
-            libfdt_bindgen::fdt_add_subnode_namelen(
-                self.fdt.as_mut_ptr(),
-                self.offset,
-                name.as_ptr().cast::<_>(),
-                namelen,
-            )
-        };
-        fdt_err(ret)
-    }
-
     /// Returns the first subnode of this
     pub fn first_subnode(&'a mut self) -> Result<Option<Self>> {
-        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
-        let ret = unsafe { libfdt_bindgen::fdt_first_subnode(self.fdt.as_ptr(), self.offset) };
-        let offset = fdt_err_or_option(ret)?;
+        let offset = self.fdt.first_subnode(self.offset)?;
 
         Ok(offset.map(|offset| Self { fdt: self.fdt, offset }))
     }
 
     /// Returns the next subnode that shares the same parent with this
     pub fn next_subnode(self) -> Result<Option<Self>> {
-        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
-        let ret = unsafe { libfdt_bindgen::fdt_next_subnode(self.fdt.as_ptr(), self.offset) };
-        let offset = fdt_err_or_option(ret)?;
+        let offset = self.fdt.next_subnode(self.offset)?;
 
         Ok(offset.map(|offset| Self { fdt: self.fdt, offset }))
     }
 
     /// Deletes the current node and returns the next subnode
     pub fn delete_and_next_subnode(self) -> Result<Option<Self>> {
-        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
-        let ret = unsafe { libfdt_bindgen::fdt_next_subnode(self.fdt.as_ptr(), self.offset) };
-
-        let next_offset = fdt_err_or_option(ret)?;
+        let next_offset = self.fdt.next_subnode(self.offset)?;
 
         self.delete_and_next(next_offset)
     }
 
-    fn next_node_offset(&self, depth: usize) -> Result<Option<(c_int, usize)>> {
-        let mut next_depth: c_int = depth.try_into().or(Err(FdtError::BadValue))?;
-        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
-        let ret = unsafe {
-            libfdt_bindgen::fdt_next_node(self.fdt.as_ptr(), self.offset, &mut next_depth)
-        };
-        let Ok(next_depth) = usize::try_from(next_depth) else {
-            return Ok(None);
-        };
-        Ok(fdt_err_or_option(ret)?.map(|offset| (offset, next_depth)))
-    }
-
     /// Returns the next node
     pub fn next_node(self, depth: usize) -> Result<Option<(Self, usize)>> {
-        let next = self.next_node_offset(depth)?;
+        let next = self.fdt.next_node(self.offset, depth)?;
 
         Ok(next.map(|(offset, depth)| (Self { fdt: self.fdt, offset }, depth)))
     }
 
     fn next_node_skip_subnodes(&mut self, depth: usize) -> Result<Option<(c_int, usize)>> {
-        let mut iter = self.next_node_offset(depth)?;
+        let mut iter = self.fdt.next_node(self.offset, depth)?;
         while let Some((descendant_offset, descendant_depth)) = iter {
             if descendant_depth <= depth {
                 return Ok(Some((descendant_offset, descendant_depth)));
             }
-            let descendant = FdtNodeMut { fdt: self.fdt, offset: descendant_offset };
-            iter = descendant.next_node_offset(descendant_depth)?;
+            iter = self.fdt.next_node(descendant_offset, descendant_depth)?;
         }
 
         Ok(None)
@@ -765,15 +681,7 @@
 
     /// Returns the compatible node of the given name that is next after this node.
     pub fn next_compatible(self, compatible: &CStr) -> Result<Option<Self>> {
-        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
-        let ret = unsafe {
-            libfdt_bindgen::fdt_node_offset_by_compatible(
-                self.fdt.as_ptr(),
-                self.offset,
-                compatible.as_ptr(),
-            )
-        };
-        let offset = fdt_err_or_option(ret)?;
+        let offset = self.fdt.node_offset_by_compatible(self.offset, compatible)?;
 
         Ok(offset.map(|offset| Self { fdt: self.fdt, offset }))
     }
@@ -791,46 +699,24 @@
     // mutable reference to DT, so we can't use current node (which also has a mutable reference to
     // DT).
     pub fn delete_and_next_compatible(self, compatible: &CStr) -> Result<Option<Self>> {
-        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
-        let ret = unsafe {
-            libfdt_bindgen::fdt_node_offset_by_compatible(
-                self.fdt.as_ptr(),
-                self.offset,
-                compatible.as_ptr(),
-            )
-        };
-        let next_offset = fdt_err_or_option(ret)?;
+        let next_offset = self.fdt.node_offset_by_compatible(self.offset, compatible)?;
 
         self.delete_and_next(next_offset)
     }
 
-    fn delete_and_next(mut self, next_offset: Option<c_int>) -> Result<Option<Self>> {
+    fn delete_and_next(self, next_offset: Option<c_int>) -> Result<Option<Self>> {
         if Some(self.offset) == next_offset {
             return Err(FdtError::Internal);
         }
 
-        // SAFETY: nop_self() only touches bytes of the self and its properties and subnodes, and
-        // doesn't alter any other blob in the tree. self.fdt and next_offset would remain valid.
-        unsafe { self.nop_self()? };
+        self.fdt.nop_node(self.offset)?;
 
         Ok(next_offset.map(|offset| Self { fdt: self.fdt, offset }))
     }
 
     /// Deletes this node effectively from DT, by setting it with FDT_NOP
-    pub fn nop(mut self) -> Result<()> {
-        // SAFETY: This consumes self, so invalid node wouldn't be used any further
-        unsafe { self.nop_self() }
-    }
-
-    /// Deletes this node effectively from DT, by setting it with FDT_NOP.
-    /// This only changes bytes of the node and its properties and subnodes, and doesn't alter or
-    /// move any other part of the tree.
-    /// SAFETY: This node is no longer valid.
-    unsafe fn nop_self(&mut self) -> Result<()> {
-        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor).
-        let ret = unsafe { libfdt_bindgen::fdt_nop_node(self.fdt.as_mut_ptr(), self.offset) };
-
-        fdt_err_expect_zero(ret)
+    pub fn nop(self) -> Result<()> {
+        self.fdt.nop_node(self.offset)
     }
 }
 
@@ -841,6 +727,22 @@
     buffer: [u8],
 }
 
+// SAFETY: Fdt calls check_full() before safely returning a &Self, making it impossible for trait
+// methods to be called on invalid device trees.
+unsafe impl Libfdt for Fdt {
+    fn as_fdt_slice(&self) -> &[u8] {
+        &self.buffer[..self.totalsize()]
+    }
+}
+
+// SAFETY: Fdt calls check_full() before safely returning a &Self, making it impossible for trait
+// methods to be called on invalid device trees.
+unsafe impl LibfdtMut for Fdt {
+    fn as_fdt_slice_mut(&mut self) -> &mut [u8] {
+        &mut self.buffer
+    }
+}
+
 impl Fdt {
     /// Wraps a slice containing a Flattened Device Tree.
     ///
@@ -994,7 +896,7 @@
 
     /// Returns a tree node by its full path.
     pub fn node(&self, path: &CStr) -> Result<Option<FdtNode>> {
-        let offset = self.path_offset(path.to_bytes())?;
+        let offset = self.path_offset_namelen(path.to_bytes())?;
 
         Ok(offset.map(|offset| FdtNode { fdt: self, offset }))
     }
@@ -1039,26 +941,14 @@
 
     /// Returns a mutable tree node by its full path.
     pub fn node_mut(&mut self, path: &CStr) -> Result<Option<FdtNodeMut>> {
-        let offset = self.path_offset(path.to_bytes())?;
+        let offset = self.path_offset_namelen(path.to_bytes())?;
 
         Ok(offset.map(|offset| FdtNodeMut { fdt: self, offset }))
     }
 
     /// Returns the device tree as a slice (may be smaller than the containing buffer).
     pub fn as_slice(&self) -> &[u8] {
-        &self.buffer[..self.totalsize()]
-    }
-
-    fn path_offset(&self, path: &[u8]) -> Result<Option<c_int>> {
-        let len = path.len().try_into().map_err(|_| FdtError::BadPath)?;
-        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor) and the
-        // function respects the passed number of characters.
-        let ret = unsafe {
-            // *_namelen functions don't include the trailing nul terminator in 'len'.
-            libfdt_bindgen::fdt_path_offset_namelen(self.as_ptr(), path.as_ptr().cast::<_>(), len)
-        };
-
-        fdt_err_or_option(ret)
+        self.as_fdt_slice()
     }
 
     fn get_from_ptr(&self, ptr: *const c_void, len: usize) -> Result<&[u8]> {
diff --git a/libs/libfdt/src/libfdt.rs b/libs/libfdt/src/libfdt.rs
index bd9ae1e..7e3b65a 100644
--- a/libs/libfdt/src/libfdt.rs
+++ b/libs/libfdt/src/libfdt.rs
@@ -13,8 +13,15 @@
 // limitations under the License.
 
 //! Low-level libfdt_bindgen wrapper, easy to integrate safely in higher-level APIs.
+//!
+//! These traits decouple the safe libfdt C function calls from the representation of those
+//! user-friendly higher-level types, allowing the trait to be shared between different ones,
+//! adapted to their use-cases (e.g. alloc-based userspace or statically allocated no_std).
 
-use crate::{fdt_err_expect_zero, Result};
+use core::ffi::{c_int, CStr};
+use core::ptr;
+
+use crate::{fdt_err, fdt_err_expect_zero, fdt_err_or_option, FdtError, Result};
 
 // Function names are the C function names without the `fdt_` prefix.
 
@@ -43,3 +50,170 @@
 
     fdt_err_expect_zero(ret)
 }
+
+/// Wrapper for the read-only libfdt.h functions.
+///
+/// # Safety
+///
+/// Implementors must ensure that at any point where a method of this trait is called, the
+/// underlying type returns the bytes of a valid device tree (as validated by `check_full`)
+/// through its `.as_fdt_slice` method.
+pub(crate) unsafe trait Libfdt {
+    /// Provides an immutable slice containing the device tree.
+    ///
+    /// The implementation must ensure that the size of the returned slice and
+    /// `fdt_header::totalsize` match.
+    fn as_fdt_slice(&self) -> &[u8];
+
+    /// Safe wrapper around `fdt_path_offset_namelen()` (C function).
+    fn path_offset_namelen(&self, path: &[u8]) -> Result<Option<c_int>> {
+        let fdt = self.as_fdt_slice().as_ptr().cast();
+        // *_namelen functions don't include the trailing nul terminator in 'len'.
+        let len = path.len().try_into().map_err(|_| FdtError::BadPath)?;
+        let path = path.as_ptr().cast();
+        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor) and the
+        // function respects the passed number of characters.
+        let ret = unsafe { libfdt_bindgen::fdt_path_offset_namelen(fdt, path, len) };
+
+        fdt_err_or_option(ret)
+    }
+
+    /// Safe wrapper around `fdt_node_offset_by_compatible()` (C function).
+    fn node_offset_by_compatible(&self, prev: c_int, compatible: &CStr) -> Result<Option<c_int>> {
+        let fdt = self.as_fdt_slice().as_ptr().cast();
+        let compatible = compatible.as_ptr();
+        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
+        let ret = unsafe { libfdt_bindgen::fdt_node_offset_by_compatible(fdt, prev, compatible) };
+
+        fdt_err_or_option(ret)
+    }
+
+    /// Safe wrapper around `fdt_next_node()` (C function).
+    fn next_node(&self, node: c_int, depth: usize) -> Result<Option<(c_int, usize)>> {
+        let fdt = self.as_fdt_slice().as_ptr().cast();
+        let mut depth = depth.try_into().unwrap();
+        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
+        let ret = unsafe { libfdt_bindgen::fdt_next_node(fdt, node, &mut depth) };
+
+        match fdt_err_or_option(ret)? {
+            Some(offset) if depth >= 0 => {
+                let depth = depth.try_into().unwrap();
+                Ok(Some((offset, depth)))
+            }
+            _ => Ok(None),
+        }
+    }
+
+    /// Safe wrapper around `fdt_parent_offset()` (C function).
+    ///
+    /// Note that this function returns a `Err` when called on a root.
+    fn parent_offset(&self, node: c_int) -> Result<c_int> {
+        let fdt = self.as_fdt_slice().as_ptr().cast();
+        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
+        let ret = unsafe { libfdt_bindgen::fdt_parent_offset(fdt, node) };
+
+        fdt_err(ret)
+    }
+
+    /// Safe wrapper around `fdt_supernode_atdepth_offset()` (C function).
+    ///
+    /// Note that this function returns a `Err` when called on a node at a depth shallower than
+    /// the provided `depth`.
+    fn supernode_atdepth_offset(&self, node: c_int, depth: usize) -> Result<c_int> {
+        let fdt = self.as_fdt_slice().as_ptr().cast();
+        let depth = depth.try_into().unwrap();
+        let nodedepth = ptr::null_mut();
+        let ret =
+            // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
+            unsafe { libfdt_bindgen::fdt_supernode_atdepth_offset(fdt, node, depth, nodedepth) };
+
+        fdt_err(ret)
+    }
+
+    /// Safe wrapper around `fdt_subnode_offset_namelen()` (C function).
+    fn subnode_offset_namelen(&self, parent: c_int, name: &[u8]) -> Result<Option<c_int>> {
+        let fdt = self.as_fdt_slice().as_ptr().cast();
+        let namelen = name.len().try_into().unwrap();
+        let name = name.as_ptr().cast();
+        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor).
+        let ret = unsafe { libfdt_bindgen::fdt_subnode_offset_namelen(fdt, parent, name, namelen) };
+
+        fdt_err_or_option(ret)
+    }
+    /// Safe wrapper around `fdt_first_subnode()` (C function).
+    fn first_subnode(&self, node: c_int) -> Result<Option<c_int>> {
+        let fdt = self.as_fdt_slice().as_ptr().cast();
+        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
+        let ret = unsafe { libfdt_bindgen::fdt_first_subnode(fdt, node) };
+
+        fdt_err_or_option(ret)
+    }
+
+    /// Safe wrapper around `fdt_next_subnode()` (C function).
+    fn next_subnode(&self, node: c_int) -> Result<Option<c_int>> {
+        let fdt = self.as_fdt_slice().as_ptr().cast();
+        // SAFETY: Accesses (read-only) are constrained to the DT totalsize.
+        let ret = unsafe { libfdt_bindgen::fdt_next_subnode(fdt, node) };
+
+        fdt_err_or_option(ret)
+    }
+
+    /// Safe wrapper around `fdt_address_cells()` (C function).
+    fn address_cells(&self, node: c_int) -> Result<usize> {
+        let fdt = self.as_fdt_slice().as_ptr().cast();
+        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor).
+        let ret = unsafe { libfdt_bindgen::fdt_address_cells(fdt, node) };
+
+        Ok(fdt_err(ret)?.try_into().unwrap())
+    }
+
+    /// Safe wrapper around `fdt_size_cells()` (C function).
+    fn size_cells(&self, node: c_int) -> Result<usize> {
+        let fdt = self.as_fdt_slice().as_ptr().cast();
+        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor).
+        let ret = unsafe { libfdt_bindgen::fdt_size_cells(fdt, node) };
+
+        Ok(fdt_err(ret)?.try_into().unwrap())
+    }
+}
+
+/// Wrapper for the read-write libfdt.h functions.
+///
+/// # Safety
+///
+/// Implementors must ensure that at any point where a method of this trait is called, the
+/// underlying type returns the bytes of a valid device tree (as validated by `check_full`)
+/// through its `.as_fdt_slice_mut` method.
+///
+/// Some methods may make previously returned values such as node or string offsets or phandles
+/// invalid by modifying the device tree (e.g. by inserting or removing new nodes or properties).
+/// As most methods take or return such values, instead of marking them all as unsafe, this trait
+/// is marked as unsafe as implementors must ensure that methods that modify the validity of those
+/// values are never called while the values are still in use.
+pub(crate) unsafe trait LibfdtMut {
+    /// Provides a mutable pointer to a buffer containing the device tree.
+    ///
+    /// The implementation must ensure that the size of the returned slice is at least
+    /// `fdt_header::totalsize`, to allow for device tree growth.
+    fn as_fdt_slice_mut(&mut self) -> &mut [u8];
+
+    /// Safe wrapper around `fdt_nop_node()` (C function).
+    fn nop_node(&mut self, node: c_int) -> Result<()> {
+        let fdt = self.as_fdt_slice_mut().as_mut_ptr().cast();
+        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor).
+        let ret = unsafe { libfdt_bindgen::fdt_nop_node(fdt, node) };
+
+        fdt_err_expect_zero(ret)
+    }
+
+    /// Safe wrapper around `fdt_add_subnode_namelen()` (C function).
+    fn add_subnode_namelen(&mut self, node: c_int, name: &[u8]) -> Result<c_int> {
+        let fdt = self.as_fdt_slice_mut().as_mut_ptr().cast();
+        let namelen = name.len().try_into().unwrap();
+        let name = name.as_ptr().cast();
+        // SAFETY: Accesses are constrained to the DT totalsize (validated by ctor).
+        let ret = unsafe { libfdt_bindgen::fdt_add_subnode_namelen(fdt, node, name, namelen) };
+
+        fdt_err(ret)
+    }
+}