Skip to content

Commit

Permalink
[WIP] TryFromBytes
Browse files Browse the repository at this point in the history
Makes progress on #5
  • Loading branch information
joshlf committed Aug 21, 2023
1 parent 514cc58 commit 62a8c0f
Show file tree
Hide file tree
Showing 2 changed files with 341 additions and 23 deletions.
31 changes: 31 additions & 0 deletions src/derive_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,34 @@ macro_rules! union_has_padding {
false $(|| core::mem::size_of::<$t>() != core::mem::size_of::<$ts>())*
};
}

#[doc(hidden)]
pub use project::project as __project;

#[doc(hidden)] // `#[macro_export]` bypasses this module's `#[doc(hidden)]`.
#[macro_export]
macro_rules! impl_try_from_bytes {
($ty:ty { $($f:tt: $f_ty:ty),* } $(=> $validation_method:ident)?) => {
#[allow(unused_qualifications)]
unsafe impl zerocopy::TryFromBytes for $ty {
fn is_bit_valid(bytes: &zerocopy::MaybeValid<Self>) -> bool {
true $(&& {
let f: &zerocopy::MaybeValid<$f_ty>
= zerocopy::derive_util::__project!(&bytes.$f);
zerocopy::TryFromBytes::is_bit_valid(f)
})*
$(&& {
let bytes_ptr: *const zerocopy::MaybeValid<Self> = bytes;
let slf = unsafe { &*bytes_ptr.cast::<Self>() };
// TODO: What about interior mutability? One approach would
// be to have the validation method operate on a
// `#[repr(transparent)]` `Freeze` container that implements
// `Projectable`. If we eventually get a `Freeze` or
// `NoCell` trait, that container could implement `Deref`
// for types which don't contain any cells.
slf.$validation_method()
})?
}
}
}
}
333 changes: 310 additions & 23 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ use core::{
ptr, slice,
};

use project::Projectable;

#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(feature = "alloc")]
Expand All @@ -174,6 +176,29 @@ mod zerocopy {
pub(crate) use crate::*;
}

/// Documents multiple unsafe blocks with a single safety comment.
///
/// Invoked as:
///
/// ```rust,ignore
/// safety_comment! {
/// // Non-doc comments come first.
/// /// SAFETY:
/// /// Safety comment starts on its own line.
/// macro_1!(args);
/// macro_2! { args };
/// }
/// ```
///
/// The macro invocations are emitted, each decorated with the following
/// attribute: `#[allow(clippy::undocumented_unsafe_blocks)]`.
macro_rules! safety_comment {
(#[doc = r" SAFETY:"] $(#[doc = $_doc:literal])* $($macro:ident!$args:tt;)*) => {
#[allow(clippy::undocumented_unsafe_blocks)]
const _: () = { $($macro!$args;)* };
}
}

/// Types for which a sequence of bytes all set to zero represents a valid
/// instance of the type.
///
Expand Down Expand Up @@ -499,6 +524,291 @@ pub unsafe trait FromBytes: FromZeroes {
}
}

/// TODO
///
/// # Safety
///
/// `AsMaybeUninit` must only be implemented for types which are `Sized` or
/// whose last field is a slice whose element type is `Sized` (this includes
/// slice types themselves; in a slice type, the "last field" simply refers to
/// the slice itself).
pub unsafe trait AsMaybeUninit {
/// TODO
///
/// # Safety
///
/// For `T: AsMaybeUninit`, the following must hold:
/// - Given `m: T::MaybeUninit`, it is sound to write uninitialized bytes at
/// every byte offset in `m` (this description avoids the "what lengths
/// are valid" problem)
/// - `T` and `T::MaybeUninit` have the same alignment requirement (can't
/// use `align_of` to describe this because it requires that its argument
/// is sized)
/// - `T` and `T::MaybeUninit` are either both `Sized` or both `!Sized`
/// - If they are `Sized`, `size_of::<T>() == size_of::<T::MaybeUninit>()`
/// - If they are `!Sized`, then they are both DSTs with a trailing slice.
/// Given `t: &T` and `m: &T::MaybeUninit` with the same number of
/// elements in their trailing slices, `size_of_val(t) == size_of_val(m)`.
type MaybeUninit: ?Sized + FromBytes;
}

unsafe impl<T: Sized> AsMaybeUninit for T {
type MaybeUninit = MaybeUninit<T>;
}

unsafe impl<T: Sized> AsMaybeUninit for [T] {
type MaybeUninit = [MaybeUninit<T>];
}

unsafe impl AsMaybeUninit for str {
type MaybeUninit = <[u8] as AsMaybeUninit>::MaybeUninit;
}

/// A value which might or might not constitute a valid instance of `T`.
///
/// `MaybeValid<T>` has the same layout (size and alignment) and field offsets
/// as `T`. However, it may contain any bit pattern with a few restrictions:
/// Given `m: MaybeValid<T>` and a byte offset, `b` in the range `[0,
/// size_of::<MaybeValid<T>>())`:
/// - If, in all valid instances `t: T`, the byte at offset `b` in `t` is
/// initialized, then the byte at offset `b` within `m` is guaranteed to be
/// initialized.
/// - Let `s` be the sequence of bytes of length `b` in the offset range `[0,
/// b)` in `m`. Let `TT` be the subset of valid instances of `T` which contain
/// this sequence in the offset range `[0, b)`. If, for all instances of `t:
/// T` in `TT`, the byte at offset `b` in `t` is initialized, then the byte at
/// offset `b` in `m` is guaranteed to be initialized.
///
/// Pragmatically, this means that if `m` is guaranteed to contain an enum
/// type at a particular offset, and the enum discriminant stored in `m`
/// corresponds to a valid variant of that enum type, then it is guaranteed
/// that the appropriate bytes of `m` are initialized as defined by that
/// variant's layout (although note that the variant's layout may contain
/// another enum type, in which case the same rules apply depending on the
/// state of its discriminant, and so on recursively).
///
/// # Safety
///
/// TODO (make sure to mention enum layout)
#[derive(FromZeroes, FromBytes, AsBytes, Unaligned)]
#[repr(transparent)]
pub struct MaybeValid<T: AsMaybeUninit + ?Sized> {
inner: T::MaybeUninit,
}

impl<T> MaybeValid<T> {
/// TODO
pub const unsafe fn assume_valid(self) -> T {
unsafe { self.inner.assume_init() }
}
}

impl<T> MaybeValid<[T]> {
/// TODO
pub const fn as_slice_of_maybe_valids(&self) -> &[MaybeValid<T>] {
let inner: &[<T as AsMaybeUninit>::MaybeUninit] = &self.inner;
// SAFETY: Since `inner` is a `&[T::MaybeUninit]`, and `MaybeValid<T>`
// is a `repr(transparent)` struct around `T::MaybeUninit`, `inner` has
// the same layout as `&[MaybeValid<T>]`.
unsafe { mem::transmute(inner) }
}
}

impl<const N: usize, T> MaybeValid<[T; N]> {
/// TODO
pub const fn as_slice(&self) -> &MaybeValid<[T]> {
todo!()
}
}

unsafe impl<T, F> Projectable<F, MaybeValid<F>> for MaybeValid<T> {
type Inner = T;
}

impl<T> Debug for MaybeValid<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.pad(core::any::type_name::<Self>())
}
}

/// TODO
pub unsafe trait TryFromBytes: AsMaybeUninit {
/// TODO
fn is_bit_valid(candidate: &MaybeValid<Self>) -> bool;

/// TODO
// Note that, in a future in which we distinguish between `FromBytes` and `RefFromBytes`,
// this requires `where Self: RefFromBytes` to disallow interior mutability.
fn try_from_ref(bytes: &[u8]) -> Option<&Self>
where
// TODO: Remove this bound.
Self: Sized,
{
// TODO(https://github.com/rust-lang/rust/issues/115080): Inline this
// function once #115080 is resolved.
#[inline(always)]
fn try_read_from_inner<T: Sized, F: FnOnce(&MaybeValid<T>) -> bool>(
bytes: &[u8],
is_bit_valid: F,
) -> Option<&T> {
let maybe_valid = Ref::<_, MaybeValid<T>>::new(bytes)?.into_ref();

if is_bit_valid(maybe_valid) {
Some(unsafe { &*bytes.as_ptr().cast::<T>() })
} else {
None
}
}

try_read_from_inner(bytes, Self::is_bit_valid)
}

/// TODO
fn try_from_mut(bytes: &mut [u8]) -> Option<&mut Self>
where
// TODO: Remove the `Sized` bound.
Self: AsBytes + Sized,
{
// TODO(https://github.com/rust-lang/rust/issues/115080): Inline this
// function once #115080 is resolved.
#[inline(always)]
fn try_read_from_mut_inner<T: Sized, F: FnOnce(&MaybeValid<T>) -> bool>(
bytes: &mut [u8],
is_bit_valid: F,
) -> Option<&mut T> {
let maybe_valid = Ref::<_, MaybeValid<T>>::new(&*bytes)?.into_ref();

if is_bit_valid(maybe_valid) {
Some(unsafe { &mut *bytes.as_mut_ptr().cast::<T>() })
} else {
None
}
}

try_read_from_mut_inner(bytes, Self::is_bit_valid)
}

/// TODO
fn try_read_from(bytes: &[u8]) -> Option<Self>
where
Self: Sized,
{
// TODO(https://github.com/rust-lang/rust/issues/115080): Inline this
// function once #115080 is resolved.
#[inline(always)]
fn try_read_from_inner<T: Sized, F: FnOnce(&MaybeValid<T>) -> bool>(
bytes: &[u8],
is_bit_valid: F,
) -> Option<T> {
let maybe_valid = MaybeValid::<T>::read_from(bytes)?;

if is_bit_valid(&maybe_valid) {
Some(unsafe { maybe_valid.assume_valid() })
} else {
None
}
}

try_read_from_inner(bytes, Self::is_bit_valid)
}
}

unsafe impl<T: FromBytes> TryFromBytes for T {
fn is_bit_valid(_candidate: &MaybeValid<T>) -> bool {
true
}
}

// TODO: This conflicts with the blanket impl for `T: TryFromBytes`.

// unsafe impl<const N: usize, T: TryFromBytes + Sized> TryFromBytes for [T; N]
// where
// // TODO: Why can't Rust infer this based on the blanket impl for `T: Sized`?
// // It sets `MaybeUninit = MaybeUninit<T>`, which is `Sized`.
// <T as AsMaybeUninit>::MaybeUninit: Sized,
// {
// fn is_bit_valid(candidate: &MaybeValid<[T; N]>) -> bool {
// candidate.as_slice().as_slice_of_maybe_valids().iter().all(|c| T::is_bit_valid(c))
// }
// }

unsafe impl<T: TryFromBytes + Sized> TryFromBytes for [T]
where
// TODO: Why can't Rust infer this based on the blanket impl for `T: Sized`?
// It sets `MaybeUninit = MaybeUninit<T>`, which is `Sized`.
<T as AsMaybeUninit>::MaybeUninit: Sized,
{
fn is_bit_valid(candidate: &MaybeValid<[T]>) -> bool {
candidate.as_slice_of_maybe_valids().iter().all(|c| T::is_bit_valid(c))
}
}

/// # Safety
///
/// It must be sound to transmute `&MaybeValid<$ty>` into `&$repr`.
macro_rules! unsafe_impl_try_from_bytes {
($ty:ty, $repr:ty, |$candidate:ident| $is_bit_valid:expr) => {
unsafe impl TryFromBytes for $ty {
fn is_bit_valid(candidate: &MaybeValid<$ty>) -> bool {
let $candidate = unsafe { &*(candidate as *const MaybeValid<$ty> as *const $repr) };
$is_bit_valid
}
}
};
}

safety_comment! {
/// SAFETY:
/// All of the `NonZeroXxx` types have the same layout as `Xxx`. Also, every
/// byte of such a type is required to be initialized, so it is guaranteed
/// that every byte of a `MaybeValid<NonZeroXxx>` must also be initialized.
/// Thus, it is sound to transmute a `&MaybeValid<NonZeroXxx>` to a `&Xxx`.
///
/// TODO: Why are these impls correct (ie, ensure valid NonZeroXxx types)?
unsafe_impl_try_from_bytes!(NonZeroU8, u8, |n| *n != 0);
unsafe_impl_try_from_bytes!(NonZeroU16, u16, |n| *n != 0);
unsafe_impl_try_from_bytes!(NonZeroU32, u32, |n| *n != 0);
unsafe_impl_try_from_bytes!(NonZeroU64, u64, |n| *n != 0);
unsafe_impl_try_from_bytes!(NonZeroU128, u128, |n| *n != 0);
unsafe_impl_try_from_bytes!(NonZeroUsize, usize, |n| *n != 0);
unsafe_impl_try_from_bytes!(NonZeroI8, i8, |n| *n != 0);
unsafe_impl_try_from_bytes!(NonZeroI16, i16, |n| *n != 0);
unsafe_impl_try_from_bytes!(NonZeroI32, i32, |n| *n != 0);
unsafe_impl_try_from_bytes!(NonZeroI64, i64, |n| *n != 0);
unsafe_impl_try_from_bytes!(NonZeroI128, i128, |n| *n != 0);
unsafe_impl_try_from_bytes!(NonZeroIsize, isize, |n| *n != 0);
}

unsafe_impl_try_from_bytes!(bool, u8, |byte| *byte < 2);

unsafe_impl_try_from_bytes!(char, [u8; 4], |bytes| {
let c = u32::from_ne_bytes(*bytes);
char::from_u32(c).is_some()
});

unsafe_impl_try_from_bytes!(str, [u8], |bytes| core::str::from_utf8(bytes).is_ok());

mod try_from_bytes_derive_example {
use super::*;

struct Foo {
a: u8,
b: u16,
}

impl_try_from_bytes!(Foo { a: u8, b: u16 });

struct Bar(Foo);

impl Bar {
fn is_valid(&self) -> bool {
u16::from(self.0.a) < self.0.b
}
}

impl_try_from_bytes!(Bar { 0: Foo } => is_valid);
}

/// Types which are safe to treat as an immutable byte slice.
///
/// WARNING: Do not implement this trait yourself! Instead, use
Expand Down Expand Up @@ -696,29 +1006,6 @@ pub unsafe trait Unaligned {
Self: Sized;
}

/// Documents multiple unsafe blocks with a single safety comment.
///
/// Invoked as:
///
/// ```rust,ignore
/// safety_comment! {
/// // Non-doc comments come first.
/// /// SAFETY:
/// /// Safety comment starts on its own line.
/// macro_1!(args);
/// macro_2! { args };
/// }
/// ```
///
/// The macro invocations are emitted, each decorated with the following
/// attribute: `#[allow(clippy::undocumented_unsafe_blocks)]`.
macro_rules! safety_comment {
(#[doc = r" SAFETY:"] $(#[doc = $_doc:literal])* $($macro:ident!$args:tt;)*) => {
#[allow(clippy::undocumented_unsafe_blocks)]
const _: () = { $($macro!$args;)* };
}
}

/// Unsafely implements trait(s) for a type.
macro_rules! unsafe_impl {
// Implement `$trait` for `$ty` with no bounds.
Expand Down

0 comments on commit 62a8c0f

Please sign in to comment.