浏览代码

multiboot2-common: init

Although the commit history doesn't reflect it, this is the outcome
after drying dozens of designs in much more time than I'd like to
admit :D

However, although this is complex, it solves real problems and is a
true value-add. Also, it reflects so many findings and learnings.
Philipp Schuster 7 月之前
父节点
当前提交
bbcc8d8f2d

+ 6 - 0
multiboot2-common/Cargo.toml

@@ -26,9 +26,15 @@ documentation = "https://docs.rs/multiboot2-common"
 #rust-version = "1.70"
 
 [features]
+default = ["builder"]
+alloc = []
+builder = ["alloc"]
+unstable = []
 
 
 [dependencies]
+derive_more.workspace = true
+ptr_meta.workspace = true
 
 [package.metadata.docs.rs]
 all-features = true

+ 116 - 0
multiboot2-common/src/boxed.rs

@@ -0,0 +1,116 @@
+//! Module for [`new_boxed`].
+
+use crate::{increase_to_alignment, Header, MaybeDynSized, ALIGNMENT};
+use alloc::boxed::Box;
+use core::alloc::Layout;
+use core::mem;
+use core::ops::Deref;
+use core::ptr;
+
+/// Creates a new tag implementing [`MaybeDynSized`] on the heap. This works for
+/// sized and unsized tags. However, it only makes sense to use this for tags
+/// that are DSTs (unsized). For regular sized structs, you can just create a
+/// typical constructor and box the result.
+///
+/// The provided `header`' total size (see [`Header`]) will be set dynamically
+/// by this function using [`Header::set_size`]. However, it must contain all
+/// other relevant metadata or update it in the `set_size` callback.
+///
+/// # Parameters
+/// - `additional_bytes_slices`: Array of byte slices that should be included
+///   without additional padding in-between. You don't need to add the bytes
+///   for [`Header`], but only additional payload.
+#[must_use]
+pub fn new_boxed<T: MaybeDynSized<Metadata = usize> + ?Sized>(
+    mut header: T::Header,
+    additional_bytes_slices: &[&[u8]],
+) -> Box<T> {
+    let additional_size = additional_bytes_slices
+        .iter()
+        .map(|b| b.len())
+        .sum::<usize>();
+
+    let tag_size = mem::size_of::<T::Header>() + additional_size;
+    header.set_size(tag_size);
+
+    // Allocation size is multiple of alignment.
+    // See <https://doc.rust-lang.org/reference/type-layout.html>
+    let alloc_size = increase_to_alignment(tag_size);
+    let layout = Layout::from_size_align(alloc_size, ALIGNMENT).unwrap();
+    let heap_ptr = unsafe { alloc::alloc::alloc(layout) };
+    assert!(!heap_ptr.is_null());
+
+    // write header
+    {
+        let len = mem::size_of::<T::Header>();
+        let ptr = core::ptr::addr_of!(header);
+        unsafe {
+            ptr::copy_nonoverlapping(ptr.cast::<u8>(), heap_ptr, len);
+        }
+    }
+
+    // write body
+    {
+        let mut write_offset = mem::size_of::<T::Header>();
+        for &bytes in additional_bytes_slices {
+            let len = bytes.len();
+            let src = bytes.as_ptr();
+            unsafe {
+                let dst = heap_ptr.add(write_offset);
+                ptr::copy_nonoverlapping(src, dst, len);
+                write_offset += len;
+            }
+        }
+    }
+
+    // This is a fat pointer for DSTs and a thin pointer for sized `T`s.
+    let ptr: *mut T = ptr_meta::from_raw_parts_mut(heap_ptr.cast(), T::dst_len(&header));
+    let reference = unsafe { Box::from_raw(ptr) };
+
+    // If this panic triggers, there is a fundamental flaw in my logic. This is
+    // not the fault of an API user.
+    assert_eq!(
+        mem::size_of_val(reference.deref()),
+        alloc_size,
+        "Allocation should match Rusts expectation"
+    );
+
+    reference
+}
+
+/// Clones a [`MaybeDynSized`] by calling [`new_boxed`].
+#[must_use]
+pub fn clone_dyn<T: MaybeDynSized<Metadata = usize> + ?Sized>(tag: &T) -> Box<T> {
+    new_boxed(tag.header().clone(), &[tag.payload()])
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::test_utils::{DummyDstTag, DummyTestHeader};
+    use crate::Tag;
+
+    #[test]
+    fn test_new_boxed() {
+        let header = DummyTestHeader::new(DummyDstTag::ID, 0);
+        let tag = new_boxed::<DummyDstTag>(header, &[&[0, 1, 2, 3]]);
+        assert_eq!(tag.header().typ(), 42);
+        assert_eq!(tag.payload(), &[0, 1, 2, 3]);
+
+        // Test that bytes are added consecutively without gaps.
+        let header = DummyTestHeader::new(0xdead_beef, 0);
+        let tag = new_boxed::<DummyDstTag>(header, &[&[0], &[1], &[2, 3]]);
+        assert_eq!(tag.header().typ(), 0xdead_beef);
+        assert_eq!(tag.payload(), &[0, 1, 2, 3]);
+    }
+
+    #[test]
+    fn test_clone_tag() {
+        let header = DummyTestHeader::new(DummyDstTag::ID, 0);
+        let tag = new_boxed::<DummyDstTag>(header, &[&[0, 1, 2, 3]]);
+        assert_eq!(tag.header().typ(), 42);
+        assert_eq!(tag.payload(), &[0, 1, 2, 3]);
+
+        let _cloned = clone_dyn(tag.as_ref());
+    }
+}

+ 87 - 0
multiboot2-common/src/bytes_ref.rs

@@ -0,0 +1,87 @@
+//! Module for [`BytesRef`].
+
+use crate::{Header, MemoryError, ALIGNMENT};
+use core::marker::PhantomData;
+use core::mem;
+use core::ops::Deref;
+
+/// Wraps a byte slice representing a Multiboot2 structure including an optional
+/// terminating padding, if necessary. It guarantees that the memory
+/// requirements promised in the crates description are respected.
+#[derive(Clone, Debug, PartialEq, Eq)]
+#[repr(transparent)]
+pub struct BytesRef<'a, H: Header> {
+    bytes: &'a [u8],
+    // Ensure that consumers can rely on the size properties for `H` that
+    // already have been verified when this type was constructed.
+    _h: PhantomData<H>,
+}
+
+impl<'a, H: Header> TryFrom<&'a [u8]> for BytesRef<'a, H> {
+    type Error = MemoryError;
+
+    fn try_from(bytes: &'a [u8]) -> Result<Self, Self::Error> {
+        if bytes.len() < mem::size_of::<H>() {
+            return Err(MemoryError::MinLengthNotSatisfied);
+        }
+        // Doesn't work as expected: if align_of_val(&value[0]) < ALIGNMENT {
+        if bytes.as_ptr().align_offset(ALIGNMENT) != 0 {
+            return Err(MemoryError::WrongAlignment);
+        }
+        let padding_bytes = bytes.len() % ALIGNMENT;
+        if padding_bytes != 0 {
+            return Err(MemoryError::MissingPadding);
+        }
+        Ok(Self {
+            bytes,
+            _h: PhantomData,
+        })
+    }
+}
+
+impl<'a, H: Header> Deref for BytesRef<'a, H> {
+    type Target = &'a [u8];
+
+    fn deref(&self) -> &Self::Target {
+        &self.bytes
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::test_utils::{AlignedBytes, DummyTestHeader};
+
+    #[test]
+    fn test_bytes_ref() {
+        let empty: &[u8] = &[];
+        assert_eq!(
+            BytesRef::<'_, DummyTestHeader>::try_from(empty),
+            Err(MemoryError::MinLengthNotSatisfied)
+        );
+
+        let slice = &[0_u8, 1, 2, 3, 4, 5, 6];
+        assert_eq!(
+            BytesRef::<'_, DummyTestHeader>::try_from(&slice[..]),
+            Err(MemoryError::MinLengthNotSatisfied)
+        );
+
+        let slice = AlignedBytes([0_u8, 1, 2, 3, 4, 5, 6, 7, 0, 0, 0]);
+        // Guaranteed wrong alignment
+        let unaligned_slice = &slice[3..];
+        assert_eq!(
+            BytesRef::<'_, DummyTestHeader>::try_from(unaligned_slice),
+            Err(MemoryError::WrongAlignment)
+        );
+
+        let slice = AlignedBytes([0_u8, 1, 2, 3, 4, 5, 6, 7]);
+        let slice = &slice[..];
+        assert_eq!(
+            BytesRef::try_from(slice),
+            Ok(BytesRef {
+                bytes: slice,
+                _h: PhantomData::<DummyTestHeader>
+            })
+        );
+    }
+}

+ 125 - 0
multiboot2-common/src/iter.rs

@@ -0,0 +1,125 @@
+//! Iterator over Multiboot2 structures. Technically, the process for iterating
+//! Multiboot2 information tags and iterating Multiboot2 header tags is the
+//! same.
+
+use crate::{increase_to_alignment, DynSizedStructure, Header, ALIGNMENT};
+use core::marker::PhantomData;
+use core::mem;
+
+/// Iterates over the tags (modelled by [`DynSizedStructure`]) of the underlying
+/// byte slice. Each tag is expected to have the same common [`Header`].
+///
+/// As the iterator emits elements of type [`DynSizedStructure`], users should
+/// casted them to specific [`Tag`]s using [`DynSizedStructure::cast`] following
+/// a user policy. This can for example happen on the basis of some ID.
+///
+/// This type ensures the memory safety guarantees promised by this crates
+/// documentation.
+///
+/// [`Tag`]: crate::Tag
+#[derive(Clone, Debug)]
+pub struct TagIter<'a, H: Header> {
+    /// Absolute offset to next tag and updated in each iteration.
+    next_tag_offset: usize,
+    buffer: &'a [u8],
+    // Ensure that all instances are bound to a specific `Header`.
+    // Otherwise, UB can happen.
+    _t: PhantomData<H>,
+}
+
+impl<'a, H: Header> TagIter<'a, H> {
+    /// Creates a new iterator.
+    #[must_use]
+    pub fn new(mem: &'a [u8]) -> Self {
+        // Assert alignment.
+        assert_eq!(mem.as_ptr().align_offset(ALIGNMENT), 0);
+
+        TagIter {
+            next_tag_offset: 0,
+            buffer: mem,
+            _t: PhantomData,
+        }
+    }
+}
+
+impl<'a, H: Header + 'a> Iterator for TagIter<'a, H> {
+    type Item = &'a DynSizedStructure<H>;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        if self.next_tag_offset == self.buffer.len() {
+            return None;
+        }
+        assert!(self.next_tag_offset < self.buffer.len());
+
+        let ptr = unsafe { self.buffer.as_ptr().add(self.next_tag_offset) }.cast::<H>();
+        let tag_hdr = unsafe { &*ptr };
+
+        // Get relevant byte portion for the next tag. This includes padding
+        // bytes to fulfill Rust memory guarantees. Otherwise, Miri complains.
+        // See <https://doc.rust-lang.org/reference/type-layout.html>.
+        let slice = {
+            let from = self.next_tag_offset;
+            let len = mem::size_of::<H>() + tag_hdr.payload_len();
+            let to = from + len;
+
+            // The size of (the allocation for) a value is always a multiple of
+            // its alignment.
+            // https://doc.rust-lang.org/reference/type-layout.html
+            let to = increase_to_alignment(to);
+
+            // Update ptr for next iteration.
+            self.next_tag_offset += to - from;
+
+            &self.buffer[from..to]
+        };
+
+        // unwrap: We should not fail at this point.
+        let tag = DynSizedStructure::ref_from_slice(slice).unwrap();
+        Some(tag)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use crate::test_utils::{AlignedBytes, DummyTestHeader};
+    use crate::TagIter;
+    use core::borrow::Borrow;
+
+    #[test]
+    fn test_tag_iter() {
+        #[rustfmt::skip]
+        let bytes = AlignedBytes::new(
+            [
+                /* Some minimal tag.  */
+                0xff, 0, 0, 0,
+                8, 0, 0, 0,
+                /* Some tag with payload.  */
+                0xfe, 0, 0, 0,
+                12, 0, 0, 0,
+                1, 2, 3, 4,
+                // Padding
+                0, 0, 0, 0,
+                /* End tag */
+                0, 0, 0, 0,
+                8, 0, 0, 0,
+            ],
+        );
+        let mut iter = TagIter::<DummyTestHeader>::new(bytes.borrow());
+        let first = iter.next().unwrap();
+        assert_eq!(first.header().typ(), 0xff);
+        assert_eq!(first.header().size(), 8);
+        assert!(first.payload().is_empty());
+
+        let second = iter.next().unwrap();
+        assert_eq!(second.header().typ(), 0xfe);
+        assert_eq!(second.header().size(), 12);
+        assert_eq!(&second.payload(), &[1, 2, 3, 4]);
+
+        let third = iter.next().unwrap();
+        assert_eq!(third.header().typ(), 0);
+        assert_eq!(third.header().size(), 8);
+        assert!(first.payload().is_empty());
+
+        assert_eq!(iter.next(), None);
+    }
+}

+ 388 - 14
multiboot2-common/src/lib.rs

@@ -1,10 +1,13 @@
 //! Common helpers for the `multiboot2` and `multiboot2-header` crates.
 //!
-//! The main objective here is to encapsulate the memory-sensible part of
-//! parsing and iterating Multiboot2 structures. This crate empowers
-//! `multiboot2` and `multiboot2-header` to use rusty types for the
-//! corresponding structures, such as non-trait DSTs (structs with a
-//! terminating `[u8]` field). The abstractions can be used for:
+//! # Value-add
+//!
+//! The main value-add of this crate is to abstract away the parsing and
+//! construction of Multiboot2 structures. This is more complex as it may sound
+//! at first due to the difficulties listed below.
+//!
+//! The abstractions provided by this crate serve as the base to work with the
+//! following structures:
 //! - multiboot2:
 //!   - boot information structure (whole)
 //!   - boot information tags
@@ -12,21 +15,392 @@
 //!   - header structure (whole)
 //!   - header tags
 //!
-//! Not meant as stable public API for others.
+//! # The Problem / Difficulties
+//!
+//! ## Multiboot2 Structures
+//!
+//! Multiboot2 structures are a consecutive chunk of bytes in memory. They use
+//! the "header pattern", which means a fixed size and known [`Header`] type
+//! indicates the total size of the structure. This is roughly translated to the
+//! following rusty base type:
+//!
+//! ```ignore
+//! #[repr(C, align(8))]
+//! struct DynStructure {
+//!     header: MyHeader,
+//!     payload: [u8]
+//! }
+//! ```
+//!
+//! Note that these structures can also be nested. So for example, the
+//! Multiboot2 boot information contains Multiboot2 tags, and the Multiboot2
+//! header contains Multiboot2 header tags - both are itself dynamic structures.
+//!
+//! A final `[u8]` field in the structs is the most rusty way to model this.
+//! However, this makes the type a Dynamically Sized Type (DST). To create
+//! references to these types from a byte slice, one needs fat pointers. They
+//! are a language feature currently not constructable with stable Rust.
+//! Luckily, we can utilize [`ptr_meta`].
+//!
+//! ## Dynamic and Sized Structs
+//!
+//! Note that we also have structures (tags) in Multiboot2 that looks like this:
+//!
+//! ```ignore
+//! #[repr(C, align(8))]
+//! struct DynStructure {
+//!     header: MyHeader,
+//!     // Not just [`u8`]
+//!     payload: [SomeType]
+//! }
+//! ```
+//!
+//! or
+//!
+//! ```ignore
+//! #[repr(C, align(8))]
+//! struct CommandLineTag {
+//!     header: TagHeader,
+//!     start: u32,
+//!     end: u32,
+//!     // More than just the base header before the dynamic portion
+//!     data: [u8]
+//! }
+//! ```
+//!
+//! ## Fat Pointer Requirements
+//!
+//! To create fat pointers with [`ptr_meta`], each tag needs a `Metadata` type
+//! which is either `usize` (for DSTs) or `()`. A trait is needed to abstract
+//! above sized or unsized types.
+//!
+//! ## Multiboot2 Requirements
+//!
+//! All tags must be 8-byte aligned. Space between multiple tags may be
+//! filled with zeroes if necessary. These zeroes are not reflected in the
+//! previous tag's size.
+//!
+//! ## Rustc Requirements
+//!
+//! The allocation space that Rust requires for types is a multiple of the
+//! alignment. This means that if we cast between byte slices and specific
+//! types, Rust doesn't just see the size reported by the header but also
+//! any necessary padding bytes. If this is not the case, for example we
+//! cast to a structure from a `&[u8; 15]`, Miri will complain as it expects
+//! `&[u8; 16]`
+//!
+//! See <https://doc.rust-lang.org/reference/type-layout.html> for information.
+//!
+//! # Provided Abstractions
+//!
+//! ## Parsing and Casting
+//!
+//! First, we need byte slices which are guaranteed to be aligned and are a
+//! multiple of the alignment. We have [`BytesRef`] for that. With that, we can
+//! create a [`DynSizedStructure`]. This is a rusty type that owns all the bytes
+//! it owns, according to the size reported by its header. Using this type
+//! and with the help of [`MaybeDynSized`], we can call
+//! [`DynSizedStructure::cast`] to cast this to arbitrary sized or unsized
+//! struct types fulfilling the corresponding requirements.
+//!
+//! This way, one can create nice rusty structs modeling the structure of the
+//! tags, and we only need a single "complicated" type, namely
+//! [`DynSizedStructure`].
+//!
+//! ## Iterating Tags
+//!
+//! To iterate over the tags of a structure, use [`TagIter`].
+//!
+//! # Memory Guarantees and Safety Promises
+//!
+//! For the parsing and construction of Multiboot2 structures, the alignment
+//! and necessary padding bytes as discussed above are guaranteed. When types
+//! are constructed, they return Results with appropriate error types. If
+//! during runtime something goes wrong, for example due to malformed tags,
+//! panics guarantee that no UB will happen.
+//!
+//! # No Public API
+//!
+//! Not meant as stable public API for others outside Multiboot2.
 
 #![no_std]
+#![cfg_attr(feature = "unstable", feature(error_in_core))]
 // --- BEGIN STYLE CHECKS ---
 #![deny(
     clippy::all,
     clippy::cargo,
-    clippy::nursery,
     clippy::must_use_candidate,
-    // clippy::restriction,
-    // clippy::pedantic
+    clippy::nursery,
+    missing_debug_implementations,
+    missing_docs,
+    rustdoc::all
 )]
-// now allow a few rules which are denied by the above statement
-// --> They are either ridiculous, not necessary, or we can't fix them.
-#![deny(missing_docs)]
-#![deny(missing_debug_implementations)]
-#![deny(rustdoc::all)]
+#![allow(clippy::multiple_crate_versions)]
 // --- END STYLE CHECKS ---
+
+#[cfg_attr(test, macro_use)]
+#[cfg(test)]
+extern crate std;
+
+#[cfg(feature = "alloc")]
+extern crate alloc;
+
+#[allow(unused)]
+pub mod test_utils;
+
+#[cfg(feature = "alloc")]
+mod boxed;
+mod bytes_ref;
+mod iter;
+mod tag;
+
+#[cfg(feature = "alloc")]
+pub use boxed::{clone_dyn, new_boxed};
+pub use bytes_ref::BytesRef;
+pub use iter::TagIter;
+pub use tag::{MaybeDynSized, Tag};
+
+use core::fmt::Debug;
+use core::mem;
+use core::ptr;
+use core::ptr::NonNull;
+use core::slice;
+
+/// The alignment of all Multiboot2 data structures.
+pub const ALIGNMENT: usize = 8;
+
+/// A sized header type for [`DynSizedStructure`]. Note that `header` refers to
+/// the header pattern. Thus, depending on the use case, this is not just a
+/// tag header. Instead, it refers to all bytes that are fixed and not part of
+/// any optional terminating dynamic `[u8]` slice in a [`DynSizedStructure`].
+///
+/// The alignment of implementors **must** be the compatible with the demands
+/// for the corresponding structure, which typically is [`ALIGNMENT`].
+pub trait Header: Clone + Sized + PartialEq + Eq + Debug {
+    /// Returns the length of the payload, i.e., the bytes that are additional
+    /// to the header. The value is measured in bytes.
+    #[must_use]
+    fn payload_len(&self) -> usize;
+
+    /// Returns the total size of the struct, thus the size of the header itself
+    /// plus [`Header::payload_len`].
+    #[must_use]
+    fn total_size(&self) -> usize {
+        mem::size_of::<Self>() + self.payload_len()
+    }
+
+    /// Updates the header with the given `total_size`.
+    fn set_size(&mut self, total_size: usize);
+}
+
+/// An C ABI-compatible dynamically sized type with a common sized [`Header`]
+/// and a dynamic amount of bytes. This structures owns all its bytes, unlike
+/// [`Header`]. Instances guarantees that the memory requirements promised in
+/// the crates description are respected.
+///
+/// This can be a Multiboot2 header tag, information tag, boot information, or
+/// a Multiboot2 header. Depending on the context, the [`Header`] is different.
+///
+/// # ABI
+/// This type uses the C ABI. The fixed [`Header`] portion is always there.
+/// Further, there is a variable amount of payload bytes. Thus, this type can
+/// only exist on the heap or references to it can be made by cast via fat
+/// pointers.
+#[derive(Debug, PartialEq, Eq, ptr_meta::Pointee)]
+#[repr(C, align(8))]
+pub struct DynSizedStructure<H: Header> {
+    header: H,
+    payload: [u8],
+    // Plus optional padding bytes to next alignment boundary, which are not
+    // reflected here. However, Rustc allocates them anyway and expects them
+    // to be there.
+    // See <https://doc.rust-lang.org/reference/type-layout.html>.
+}
+
+impl<H: Header> DynSizedStructure<H> {
+    /// Creates a new fat-pointer backed reference to a [`DynSizedStructure`]
+    /// from the given [`BytesRef`].
+    pub fn ref_from_bytes(bytes: BytesRef<H>) -> Result<&Self, MemoryError> {
+        let ptr = bytes.as_ptr().cast::<H>();
+        let hdr = unsafe { &*ptr };
+
+        if hdr.payload_len() > bytes.len() {
+            return Err(MemoryError::InvalidHeaderSize);
+        }
+
+        // At this point we know that the memory slice fulfills the base
+        // assumptions and requirements. Now, we safety can create the fat
+        // pointer.
+
+        let dst_size = hdr.payload_len();
+        // Create fat pointer for the DST.
+        let ptr = ptr_meta::from_raw_parts(ptr.cast(), dst_size);
+        let reference = unsafe { &*ptr };
+        Ok(reference)
+    }
+
+    /// Creates a new fat-pointer backed reference to a [`DynSizedStructure`]
+    /// from the given `&[u8]`.
+    pub fn ref_from_slice(bytes: &[u8]) -> Result<&Self, MemoryError> {
+        let bytes = BytesRef::<H>::try_from(bytes)?;
+        Self::ref_from_bytes(bytes)
+    }
+
+    /// Creates a new fat-pointer backed reference to a [`DynSizedStructure`]
+    /// from the given thin pointer to the [`Header`]. It reads the total size
+    /// from the header.
+    ///
+    /// # Safety
+    /// The caller must ensure that the function operates on valid memory.
+    pub unsafe fn ref_from_ptr<'a>(ptr: NonNull<H>) -> Result<&'a Self, MemoryError> {
+        let ptr = ptr.as_ptr().cast_const();
+        let hdr = unsafe { &*ptr };
+
+        let slice = unsafe { slice::from_raw_parts(ptr.cast::<u8>(), hdr.total_size()) };
+        Self::ref_from_slice(slice)
+    }
+
+    /// Returns the underlying [`Header`].
+    pub const fn header(&self) -> &H {
+        &self.header
+    }
+
+    /// Returns the underlying payload.
+    pub const fn payload(&self) -> &[u8] {
+        &self.payload
+    }
+
+    /// Casts the structure tag to a specific [`MaybeDynSized`] implementation which
+    /// may be a ZST or DST typed tag. The output type will have the exact same
+    /// size as `*self`. The target type must be sufficient for that. If not,
+    /// the function will panic.
+    ///
+    /// # Safety
+    /// This function is safe due to various sanity checks and the overall
+    /// memory assertions done while constructing this type.
+    ///
+    /// # Panics
+    /// This panics if there is a size mismatch. However, this should never be
+    /// the case if all types follow their documented requirements.
+    pub fn cast<T: MaybeDynSized<Header = H> + ?Sized>(&self) -> &T {
+        let base_ptr = ptr::addr_of!(*self);
+
+        // This should be a compile-time assertion. However, this is the best
+        // location to place it for now.
+        assert!(T::BASE_SIZE >= mem::size_of::<H>());
+
+        let t_dst_size = T::dst_len(self.header());
+        let t_ptr = ptr_meta::from_raw_parts(base_ptr.cast(), t_dst_size);
+        let t_ref = unsafe { &*t_ptr };
+
+        assert_eq!(mem::size_of_val(self), mem::size_of_val(t_ref));
+
+        t_ref
+    }
+}
+
+/// Errors that may occur when working with memory.
+#[derive(Copy, Clone, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, derive_more::Display)]
+pub enum MemoryError {
+    /// The memory points to null.
+    Null,
+    /// The memory must be at least [`ALIGNMENT`]-aligned.
+    WrongAlignment,
+    /// The memory must cover at least the length of the sized structure header
+    /// type.
+    MinLengthNotSatisfied,
+    /// The buffer misses the terminating padding to the next alignment
+    /// boundary. The padding is relevant to satisfy Rustc/Miri, but also the
+    /// spec mandates that the padding is added.
+    MissingPadding,
+    /// The size-property has an illegal value that can't be fulfilled with the
+    /// given bytes.
+    InvalidHeaderSize,
+}
+
+#[cfg(feature = "unstable")]
+impl core::error::Error for MemoryError {}
+
+/// Increases the given size to the next alignment boundary, if it is not a
+/// multiple of the alignment yet. This is relevant as in Rust's [type layout],
+/// the allocated size of a type is always a multiple of the alignment, even
+/// if the type is smaller.
+///
+/// [type layout]: https://doc.rust-lang.org/reference/type-layout.html
+#[must_use]
+pub const fn increase_to_alignment(size: usize) -> usize {
+    let mask = ALIGNMENT - 1;
+    (size + mask) & !mask
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    use crate::test_utils::{AlignedBytes, DummyTestHeader};
+    use core::borrow::Borrow;
+
+    #[test]
+    fn test_increase_to_alignment() {
+        assert_eq!(increase_to_alignment(0), 0);
+        assert_eq!(increase_to_alignment(1), 8);
+        assert_eq!(increase_to_alignment(7), 8);
+        assert_eq!(increase_to_alignment(8), 8);
+        assert_eq!(increase_to_alignment(9), 16);
+    }
+
+    #[test]
+    fn test_cast_generic_tag_to_sized_tag() {
+        #[repr(C)]
+        struct CustomSizedTag {
+            tag_header: DummyTestHeader,
+            a: u32,
+            b: u32,
+        }
+
+        impl MaybeDynSized for CustomSizedTag {
+            type Header = DummyTestHeader;
+
+            const BASE_SIZE: usize = mem::size_of::<Self>();
+
+            fn dst_len(_header: &DummyTestHeader) -> Self::Metadata {}
+        }
+
+        let bytes = AlignedBytes([
+            /* id: 0xffff_ffff */
+            0xff_u8, 0xff_u8, 0xff_u8, 0xff_u8, /* id: 16 */
+            16, 0, 0, 0, /* field a: 0xdead_beef */
+            0xef, 0xbe, 0xad, 0xde, /* field b: 0x1337_1337 */
+            0x37, 0x13, 0x37, 0x13,
+        ]);
+        let tag = DynSizedStructure::ref_from_slice(bytes.borrow()).unwrap();
+        let custom_tag = tag.cast::<CustomSizedTag>();
+
+        assert_eq!(mem::size_of_val(custom_tag), 16);
+        assert_eq!(custom_tag.a, 0xdead_beef);
+        assert_eq!(custom_tag.b, 0x1337_1337);
+    }
+
+    #[test]
+    fn test_cast_generic_tag_to_self() {
+        #[rustfmt::skip]
+        let bytes = AlignedBytes::new(
+            [
+                0x37, 0x13, 0, 0,
+                /* Tag size */
+                18, 0, 0, 0,
+                /* Some payload.  */
+                0, 1, 2, 3,
+                4, 5, 6, 7,
+                8, 9,
+                // Padding
+                0, 0, 0, 0, 0, 0
+            ],
+        );
+        let tag = DynSizedStructure::ref_from_slice(bytes.borrow()).unwrap();
+
+        // Main objective here is also that this test passes Miri.
+        let tag = tag.cast::<DynSizedStructure<DummyTestHeader>>();
+        assert_eq!(tag.header().typ(), 0x1337);
+        assert_eq!(tag.header().size(), 18);
+    }
+}

+ 94 - 0
multiboot2-common/src/tag.rs

@@ -0,0 +1,94 @@
+//! Module for the traits [`MaybeDynSized`] and [`Tag`].
+
+use crate::{BytesRef, DynSizedStructure, Header};
+use core::mem;
+use core::slice;
+use ptr_meta::Pointee;
+
+/// A trait to abstract sized and unsized structures (DSTs). It enables
+/// casting a [`DynSizedStructure`] to sized or unsized structures using
+/// [`DynSizedStructure::cast`].
+///
+/// Structs that are a DST must provide a **correct**
+/// [`MaybeDynSized::dst_len`] implementation.
+///
+/// [`ID`]: Tag::ID
+/// [`DynSizedStructure`]: crate::DynSizedStructure
+pub trait MaybeDynSized: Pointee {
+    /// The associated [`Header`] of this tag.
+    type Header: Header;
+
+    /// The true base size of the struct without any implicit or additional
+    /// padding. Note that `size_of::<T>()` isn't sufficient, as for example
+    /// the type could have three `u32` fields, which would add an implicit
+    /// `u32` padding. However, this constant **must always** fulfill
+    /// `BASE_SIZE >= size_of::<Self::Header>()`.
+    ///
+    /// The main purpose of this constant is to create awareness when you
+    /// implement [`Self::dst_len`], where you should use this. If this value
+    /// is correct, we prevent situations where we read uninitialized bytes,
+    /// especially when creating tags in builders.
+    const BASE_SIZE: usize;
+
+    /// Returns the amount of items in the dynamically sized portion of the
+    /// DST. Note that this is not the amount of bytes. So if the dynamically
+    /// sized portion is 16 bytes in size and each element is 4 bytes big, then
+    /// this function must return 4.
+    ///
+    /// For sized tags, this just returns `()`. For DSTs, this returns an
+    /// `usize`.
+    fn dst_len(header: &Self::Header) -> Self::Metadata;
+
+    /// Returns the corresponding [`Header`].
+    fn header(&self) -> &Self::Header {
+        let ptr = core::ptr::addr_of!(*self);
+        unsafe { &*ptr.cast::<Self::Header>() }
+    }
+
+    /// Returns the payload, i.e., all memory that is not occupied by the
+    /// [`Header`] of the type.
+    fn payload(&self) -> &[u8] {
+        let from = mem::size_of::<Self::Header>();
+        &self.as_bytes()[from..]
+    }
+
+    /// Returns the whole allocated bytes for this structure encapsulated in
+    /// [`BytesRef`]. This includes padding bytes. To only get the "true" tag
+    /// data, read the tag size from [`Self::header`] and create a sub slice.
+    fn as_bytes(&self) -> BytesRef<Self::Header> {
+        let ptr = core::ptr::addr_of!(*self);
+        // Actual tag size, optionally with terminating padding.
+        let size = mem::size_of_val(self);
+        let slice = unsafe { slice::from_raw_parts(ptr.cast::<u8>(), size) };
+        // Unwrap is fine as this type can't exist without the underlying memory
+        // guarantees.
+        BytesRef::try_from(slice).unwrap()
+    }
+
+    /// Returns a pointer to this structure.
+    fn as_ptr(&self) -> *const Self::Header {
+        self.as_bytes().as_ptr().cast()
+    }
+}
+
+/// Extension of [`MaybeDynSized`] for Tags.
+pub trait Tag: MaybeDynSized {
+    /// The ID type that identifies the tag.
+    type IDType: PartialEq + Eq;
+
+    /// The ID of this tag. This should be unique across all implementors.
+    ///
+    /// Although the ID is not yet used in `multiboot2-common`, it ensures
+    /// a consistent API in consumer crates.
+    const ID: Self::IDType;
+}
+
+impl<H: Header> MaybeDynSized for DynSizedStructure<H> {
+    type Header = H;
+
+    const BASE_SIZE: usize = mem::size_of::<H>();
+
+    fn dst_len(header: &Self::Header) -> Self::Metadata {
+        header.payload_len()
+    }
+}

+ 84 - 27
multiboot2/src/test_util.rs → multiboot2-common/src/test_utils.rs

@@ -1,17 +1,22 @@
 //! Various test utilities.
 
-use crate::ALIGNMENT;
+#![allow(missing_docs)]
+
+use crate::{Header, MaybeDynSized, Tag};
 use core::borrow::Borrow;
+use core::mem;
 use core::ops::Deref;
 
 /// Helper to 8-byte align the underlying bytes, as mandated in the Multiboot2
 /// spec. With this type, one can create manual and raw Multiboot2 boot
 /// information or just the bytes for simple tags, in a manual and raw approach.
-#[cfg(test)]
+#[derive(Debug)]
 #[repr(C, align(8))]
 pub struct AlignedBytes<const N: usize>(pub [u8; N]);
 
 impl<const N: usize> AlignedBytes<N> {
+    /// Creates a new type.
+    #[must_use]
     pub const fn new(bytes: [u8; N]) -> Self {
         Self(bytes)
     }
@@ -37,51 +42,103 @@ impl<const N: usize> Deref for AlignedBytes<N> {
     }
 }
 
-// The tests down below are all Miri-approved.
+/// Dummy test header.
+#[derive(Clone, Debug, PartialEq, Eq)]
+#[repr(C, align(8))]
+pub struct DummyTestHeader {
+    typ: u32,
+    size: u32,
+}
+
+impl DummyTestHeader {
+    #[must_use]
+    pub const fn new(typ: u32, size: u32) -> Self {
+        Self { typ, size }
+    }
+
+    #[must_use]
+    pub const fn typ(&self) -> u32 {
+        self.typ
+    }
+
+    #[must_use]
+    pub const fn size(&self) -> u32 {
+        self.size
+    }
+}
+
+impl Header for DummyTestHeader {
+    fn payload_len(&self) -> usize {
+        self.size as usize - mem::size_of::<Self>()
+    }
+
+    fn set_size(&mut self, total_size: usize) {
+        self.size = total_size as u32;
+    }
+}
+
+#[derive(Debug, PartialEq, Eq, ptr_meta::Pointee)]
+#[repr(C, align(8))]
+pub struct DummyDstTag {
+    header: DummyTestHeader,
+    payload: [u8],
+}
+
+impl DummyDstTag {
+    const BASE_SIZE: usize = mem::size_of::<DummyTestHeader>();
+
+    #[must_use]
+    pub const fn header(&self) -> &DummyTestHeader {
+        &self.header
+    }
+
+    #[must_use]
+    pub const fn payload(&self) -> &[u8] {
+        &self.payload
+    }
+}
+
+impl MaybeDynSized for DummyDstTag {
+    type Header = DummyTestHeader;
+
+    const BASE_SIZE: usize = mem::size_of::<DummyTestHeader>();
+
+    fn dst_len(header: &Self::Header) -> Self::Metadata {
+        header.size as usize - Self::BASE_SIZE
+    }
+}
+
+impl Tag for DummyDstTag {
+    type IDType = u32;
+    const ID: Self::IDType = 42;
+}
+
 #[cfg(test)]
 mod tests {
-    use super::*;
-    use crate::tag::TagBytesRef;
     use core::mem;
     use core::ptr::addr_of;
 
+    use crate::ALIGNMENT;
+
+    use super::*;
+
     #[test]
     fn abi() {
+        assert_eq!(mem::align_of::<AlignedBytes<0>>(), ALIGNMENT);
+
         let bytes = AlignedBytes([0]);
-        assert_eq!(mem::align_of_val(&bytes), ALIGNMENT);
         assert_eq!(bytes.as_ptr().align_offset(8), 0);
         assert_eq!((addr_of!(bytes[0])).align_offset(8), 0);
 
         let bytes = AlignedBytes([0, 1, 2, 3, 4, 5, 6, 7]);
-        assert_eq!(mem::align_of_val(&bytes), ALIGNMENT);
         assert_eq!(bytes.as_ptr().align_offset(8), 0);
         assert_eq!((addr_of!(bytes[0])).align_offset(8), 0);
 
         let bytes = AlignedBytes([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
-        assert_eq!(mem::align_of_val(&bytes), ALIGNMENT);
         assert_eq!(bytes.as_ptr().align_offset(8), 0);
         assert_eq!((addr_of!(bytes[0])).align_offset(8), 0);
         assert_eq!((addr_of!(bytes[7])).align_offset(8), 1);
         assert_eq!((addr_of!(bytes[8])).align_offset(8), 0);
         assert_eq!((addr_of!(bytes[9])).align_offset(8), 7);
     }
-
-    #[test]
-    fn compatible_with_tag_bytes_ref_type() {
-        #[rustfmt::skip]
-        let bytes = AlignedBytes(
-            [
-                /* tag id */
-                0, 0, 0, 0,
-                /* size */
-                14, 0, 0, 0,
-                /* arbitrary payload */
-                1, 2, 3, 4,
-                5, 6,
-                /* padding */
-                0, 0,
-            ]
-        );
-        let _a = TagBytesRef::try_from(bytes.borrow()).unwrap();
-    }
 }