Skip to content

Commit 2d12bbc

Browse files
committed
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.
1 parent 6454aee commit 2d12bbc

File tree

7 files changed

+894
-41
lines changed

7 files changed

+894
-41
lines changed

multiboot2-common/Cargo.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,15 @@ documentation = "https://docs.rs/multiboot2-common"
2626
#rust-version = "1.70"
2727

2828
[features]
29+
default = ["builder"]
30+
alloc = []
31+
builder = ["alloc"]
32+
unstable = []
2933

3034

3135
[dependencies]
36+
derive_more.workspace = true
37+
ptr_meta.workspace = true
3238

3339
[package.metadata.docs.rs]
3440
all-features = true

multiboot2-common/src/boxed.rs

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
//! Module for [`new_boxed`].
2+
3+
use crate::{increase_to_alignment, Header, MaybeDynSized, ALIGNMENT};
4+
use alloc::boxed::Box;
5+
use core::alloc::Layout;
6+
use core::mem;
7+
use core::ops::Deref;
8+
use core::ptr;
9+
10+
/// Creates a new tag implementing [`MaybeDynSized`] on the heap. This works for
11+
/// sized and unsized tags. However, it only makes sense to use this for tags
12+
/// that are DSTs (unsized). For regular sized structs, you can just create a
13+
/// typical constructor and box the result.
14+
///
15+
/// The provided `header`' total size (see [`Header`]) will be set dynamically
16+
/// by this function using [`Header::set_size`]. However, it must contain all
17+
/// other relevant metadata or update it in the `set_size` callback.
18+
///
19+
/// # Parameters
20+
/// - `additional_bytes_slices`: Array of byte slices that should be included
21+
/// without additional padding in-between. You don't need to add the bytes
22+
/// for [`Header`], but only additional payload.
23+
#[must_use]
24+
pub fn new_boxed<T: MaybeDynSized<Metadata = usize> + ?Sized>(
25+
mut header: T::Header,
26+
additional_bytes_slices: &[&[u8]],
27+
) -> Box<T> {
28+
let additional_size = additional_bytes_slices
29+
.iter()
30+
.map(|b| b.len())
31+
.sum::<usize>();
32+
33+
let tag_size = mem::size_of::<T::Header>() + additional_size;
34+
header.set_size(tag_size);
35+
36+
// Allocation size is multiple of alignment.
37+
// See <https://doc.rust-lang.org/reference/type-layout.html>
38+
let alloc_size = increase_to_alignment(tag_size);
39+
let layout = Layout::from_size_align(alloc_size, ALIGNMENT).unwrap();
40+
let heap_ptr = unsafe { alloc::alloc::alloc(layout) };
41+
assert!(!heap_ptr.is_null());
42+
43+
// write header
44+
{
45+
let len = mem::size_of::<T::Header>();
46+
let ptr = core::ptr::addr_of!(header);
47+
unsafe {
48+
ptr::copy_nonoverlapping(ptr.cast::<u8>(), heap_ptr, len);
49+
}
50+
}
51+
52+
// write body
53+
{
54+
let mut write_offset = mem::size_of::<T::Header>();
55+
for &bytes in additional_bytes_slices {
56+
let len = bytes.len();
57+
let src = bytes.as_ptr();
58+
unsafe {
59+
let dst = heap_ptr.add(write_offset);
60+
ptr::copy_nonoverlapping(src, dst, len);
61+
write_offset += len;
62+
}
63+
}
64+
}
65+
66+
// This is a fat pointer for DSTs and a thin pointer for sized `T`s.
67+
let ptr: *mut T = ptr_meta::from_raw_parts_mut(heap_ptr.cast(), T::dst_len(&header));
68+
let reference = unsafe { Box::from_raw(ptr) };
69+
70+
// If this panic triggers, there is a fundamental flaw in my logic. This is
71+
// not the fault of an API user.
72+
assert_eq!(
73+
mem::size_of_val(reference.deref()),
74+
alloc_size,
75+
"Allocation should match Rusts expectation"
76+
);
77+
78+
reference
79+
}
80+
81+
/// Clones a [`MaybeDynSized`] by calling [`new_boxed`].
82+
#[must_use]
83+
pub fn clone_dyn<T: MaybeDynSized<Metadata = usize> + ?Sized>(tag: &T) -> Box<T> {
84+
new_boxed(tag.header().clone(), &[tag.payload()])
85+
}
86+
87+
#[cfg(test)]
88+
mod tests {
89+
use super::*;
90+
use crate::test_utils::{DummyDstTag, DummyTestHeader};
91+
use crate::Tag;
92+
93+
#[test]
94+
fn test_new_boxed() {
95+
let header = DummyTestHeader::new(DummyDstTag::ID, 0);
96+
let tag = new_boxed::<DummyDstTag>(header, &[&[0, 1, 2, 3]]);
97+
assert_eq!(tag.header().typ(), 42);
98+
assert_eq!(tag.payload(), &[0, 1, 2, 3]);
99+
100+
// Test that bytes are added consecutively without gaps.
101+
let header = DummyTestHeader::new(0xdead_beef, 0);
102+
let tag = new_boxed::<DummyDstTag>(header, &[&[0], &[1], &[2, 3]]);
103+
assert_eq!(tag.header().typ(), 0xdead_beef);
104+
assert_eq!(tag.payload(), &[0, 1, 2, 3]);
105+
}
106+
107+
#[test]
108+
fn test_clone_tag() {
109+
let header = DummyTestHeader::new(DummyDstTag::ID, 0);
110+
let tag = new_boxed::<DummyDstTag>(header, &[&[0, 1, 2, 3]]);
111+
assert_eq!(tag.header().typ(), 42);
112+
assert_eq!(tag.payload(), &[0, 1, 2, 3]);
113+
114+
let _cloned = clone_dyn(tag.as_ref());
115+
}
116+
}

multiboot2-common/src/bytes_ref.rs

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//! Module for [`BytesRef`].
2+
3+
use crate::{Header, MemoryError, ALIGNMENT};
4+
use core::marker::PhantomData;
5+
use core::mem;
6+
use core::ops::Deref;
7+
8+
/// Wraps a byte slice representing a Multiboot2 structure including an optional
9+
/// terminating padding, if necessary. It guarantees that the memory
10+
/// requirements promised in the crates description are respected.
11+
#[derive(Clone, Debug, PartialEq, Eq)]
12+
#[repr(transparent)]
13+
pub struct BytesRef<'a, H: Header> {
14+
bytes: &'a [u8],
15+
// Ensure that consumers can rely on the size properties for `H` that
16+
// already have been verified when this type was constructed.
17+
_h: PhantomData<H>,
18+
}
19+
20+
impl<'a, H: Header> TryFrom<&'a [u8]> for BytesRef<'a, H> {
21+
type Error = MemoryError;
22+
23+
fn try_from(bytes: &'a [u8]) -> Result<Self, Self::Error> {
24+
if bytes.len() < mem::size_of::<H>() {
25+
return Err(MemoryError::MinLengthNotSatisfied);
26+
}
27+
// Doesn't work as expected: if align_of_val(&value[0]) < ALIGNMENT {
28+
if bytes.as_ptr().align_offset(ALIGNMENT) != 0 {
29+
return Err(MemoryError::WrongAlignment);
30+
}
31+
let padding_bytes = bytes.len() % ALIGNMENT;
32+
if padding_bytes != 0 {
33+
return Err(MemoryError::MissingPadding);
34+
}
35+
Ok(Self {
36+
bytes,
37+
_h: PhantomData,
38+
})
39+
}
40+
}
41+
42+
impl<'a, H: Header> Deref for BytesRef<'a, H> {
43+
type Target = &'a [u8];
44+
45+
fn deref(&self) -> &Self::Target {
46+
&self.bytes
47+
}
48+
}
49+
50+
#[cfg(test)]
51+
mod tests {
52+
use super::*;
53+
use crate::test_utils::{AlignedBytes, DummyTestHeader};
54+
55+
#[test]
56+
fn test_bytes_ref() {
57+
let empty: &[u8] = &[];
58+
assert_eq!(
59+
BytesRef::<'_, DummyTestHeader>::try_from(empty),
60+
Err(MemoryError::MinLengthNotSatisfied)
61+
);
62+
63+
let slice = &[0_u8, 1, 2, 3, 4, 5, 6];
64+
assert_eq!(
65+
BytesRef::<'_, DummyTestHeader>::try_from(&slice[..]),
66+
Err(MemoryError::MinLengthNotSatisfied)
67+
);
68+
69+
let slice = AlignedBytes([0_u8, 1, 2, 3, 4, 5, 6, 7, 0, 0, 0]);
70+
// Guaranteed wrong alignment
71+
let unaligned_slice = &slice[3..];
72+
assert_eq!(
73+
BytesRef::<'_, DummyTestHeader>::try_from(unaligned_slice),
74+
Err(MemoryError::WrongAlignment)
75+
);
76+
77+
let slice = AlignedBytes([0_u8, 1, 2, 3, 4, 5, 6, 7]);
78+
let slice = &slice[..];
79+
assert_eq!(
80+
BytesRef::try_from(slice),
81+
Ok(BytesRef {
82+
bytes: slice,
83+
_h: PhantomData::<DummyTestHeader>
84+
})
85+
);
86+
}
87+
}

multiboot2-common/src/iter.rs

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
//! Iterator over Multiboot2 structures. Technically, the process for iterating
2+
//! Multiboot2 information tags and iterating Multiboot2 header tags is the
3+
//! same.
4+
5+
use crate::{increase_to_alignment, DynSizedStructure, Header, ALIGNMENT};
6+
use core::marker::PhantomData;
7+
use core::mem;
8+
9+
/// Iterates over the tags (modelled by [`DynSizedStructure`]) of the underlying
10+
/// byte slice. Each tag is expected to have the same common [`Header`].
11+
///
12+
/// As the iterator emits elements of type [`DynSizedStructure`], users should
13+
/// casted them to specific [`Tag`]s using [`DynSizedStructure::cast`] following
14+
/// a user policy. This can for example happen on the basis of some ID.
15+
///
16+
/// This type ensures the memory safety guarantees promised by this crates
17+
/// documentation.
18+
///
19+
/// [`Tag`]: crate::Tag
20+
#[derive(Clone, Debug)]
21+
pub struct TagIter<'a, H: Header> {
22+
/// Absolute offset to next tag and updated in each iteration.
23+
next_tag_offset: usize,
24+
buffer: &'a [u8],
25+
// Ensure that all instances are bound to a specific `Header`.
26+
// Otherwise, UB can happen.
27+
_t: PhantomData<H>,
28+
}
29+
30+
impl<'a, H: Header> TagIter<'a, H> {
31+
/// Creates a new iterator.
32+
#[must_use]
33+
pub fn new(mem: &'a [u8]) -> Self {
34+
// Assert alignment.
35+
assert_eq!(mem.as_ptr().align_offset(ALIGNMENT), 0);
36+
37+
TagIter {
38+
next_tag_offset: 0,
39+
buffer: mem,
40+
_t: PhantomData,
41+
}
42+
}
43+
}
44+
45+
impl<'a, H: Header + 'a> Iterator for TagIter<'a, H> {
46+
type Item = &'a DynSizedStructure<H>;
47+
48+
fn next(&mut self) -> Option<Self::Item> {
49+
if self.next_tag_offset == self.buffer.len() {
50+
return None;
51+
}
52+
assert!(self.next_tag_offset < self.buffer.len());
53+
54+
let ptr = unsafe { self.buffer.as_ptr().add(self.next_tag_offset) }.cast::<H>();
55+
let tag_hdr = unsafe { &*ptr };
56+
57+
// Get relevant byte portion for the next tag. This includes padding
58+
// bytes to fulfill Rust memory guarantees. Otherwise, Miri complains.
59+
// See <https://doc.rust-lang.org/reference/type-layout.html>.
60+
let slice = {
61+
let from = self.next_tag_offset;
62+
let len = mem::size_of::<H>() + tag_hdr.payload_len();
63+
let to = from + len;
64+
65+
// The size of (the allocation for) a value is always a multiple of
66+
// its alignment.
67+
// https://doc.rust-lang.org/reference/type-layout.html
68+
let to = increase_to_alignment(to);
69+
70+
// Update ptr for next iteration.
71+
self.next_tag_offset += to - from;
72+
73+
&self.buffer[from..to]
74+
};
75+
76+
// unwrap: We should not fail at this point.
77+
let tag = DynSizedStructure::ref_from_slice(slice).unwrap();
78+
Some(tag)
79+
}
80+
}
81+
82+
#[cfg(test)]
83+
mod tests {
84+
use crate::test_utils::{AlignedBytes, DummyTestHeader};
85+
use crate::TagIter;
86+
use core::borrow::Borrow;
87+
88+
#[test]
89+
fn test_tag_iter() {
90+
#[rustfmt::skip]
91+
let bytes = AlignedBytes::new(
92+
[
93+
/* Some minimal tag. */
94+
0xff, 0, 0, 0,
95+
8, 0, 0, 0,
96+
/* Some tag with payload. */
97+
0xfe, 0, 0, 0,
98+
12, 0, 0, 0,
99+
1, 2, 3, 4,
100+
// Padding
101+
0, 0, 0, 0,
102+
/* End tag */
103+
0, 0, 0, 0,
104+
8, 0, 0, 0,
105+
],
106+
);
107+
let mut iter = TagIter::<DummyTestHeader>::new(bytes.borrow());
108+
let first = iter.next().unwrap();
109+
assert_eq!(first.header().typ(), 0xff);
110+
assert_eq!(first.header().size(), 8);
111+
assert!(first.payload().is_empty());
112+
113+
let second = iter.next().unwrap();
114+
assert_eq!(second.header().typ(), 0xfe);
115+
assert_eq!(second.header().size(), 12);
116+
assert_eq!(&second.payload(), &[1, 2, 3, 4]);
117+
118+
let third = iter.next().unwrap();
119+
assert_eq!(third.header().typ(), 0);
120+
assert_eq!(third.header().size(), 8);
121+
assert!(first.payload().is_empty());
122+
123+
assert_eq!(iter.next(), None);
124+
}
125+
}

0 commit comments

Comments
 (0)