-
Notifications
You must be signed in to change notification settings - Fork 105
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Overhaul error reporting (enum version) #1144
Conversation
940a3cb
to
2d2c41f
Compare
src/error.rs
Outdated
#[inline] | ||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { | ||
f.write_str("the conversion failed because the address of the source (")?; | ||
f.write_fmt(format_args!("{:p}", self.src.deref()))?; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Leaking the pointer might be considered security sensitive by some users. It can reveal aspects of the program's execution and help with memory corruption exploits (e.g. to help an attacker know where their attack payload will land in memory). We might want to omit this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good call!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
f3c7f8f
to
aedf979
Compare
4dd7102
to
24037ed
Compare
src/error.rs
Outdated
mod private { | ||
#[cfg(doc)] | ||
use super::*; | ||
|
||
/// Zerocopy's generic error type. | ||
/// | ||
/// Generally speaking, zerocopy's conversions may fail for one of up to three reasons: | ||
/// - [`AlignmentError`]: the conversion source was improperly aligned | ||
/// - [`SizeError`]: the conversion source was of incorrect size | ||
/// - [`ValidityError`]: the conversion source contained invalid data | ||
/// | ||
/// However, not all conversions produce all errors. For instance, | ||
/// [`FromBytes::ref_from`] may fail due to alignment or size issues, but not | ||
/// validity issues. This generic error type captures these (im)possibilities | ||
/// via parameterization: `A` is parameterized with [`AlignmentError`], `S` is | ||
/// parameterized with [`SizeError`], and `V` is parameterized with | ||
/// [`Infallible`]. | ||
/// | ||
/// Zerocopy never uses this type directly in its API. Rather, we provide three | ||
/// pre-parameterized aliases: | ||
/// - [`CastError`]: the error type of ref-conversions. | ||
/// - [`TryCastError`]: the error type of fallible ref-conversions. | ||
/// - [`TryReadError`]: the error type of fallible read-conversions. | ||
#[derive(PartialEq, Eq)] | ||
pub enum ConvertError<A, S, V> { | ||
/// The conversion source was improperly aligned. | ||
#[doc(hidden)] | ||
Alignment(A), | ||
/// The conversion source was of incorrect size. | ||
#[doc(hidden)] | ||
Size(S), | ||
/// The conversion source contained invalid data. | ||
#[doc(hidden)] | ||
Validity(V), | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@joshlf I've gone ahead and encapsulated the ConvertError
in a private module, so the public API consists solely of:
AlignmentError<Src, Dst>
: the conversion source was improperly alignedSizeError<Src, Dst>
: the conversion source was of incorrect sizeValidityError<Src, Dst>
: the conversion source contained invalid dataCastError<Src, Dst>
: the compound error type of ref-conversionsTryCastError<Src, Dst>
: the compound error type of fallible ref-conversionsTryReadError<Src, Dst>
: the compound error type of fallible read-conversions
I could go further and put it in a private struct, too, but that doesn't buy us anything as far as SemVer is concerned — it just makes all of our match
s in this PR more verbose.
5fdf9b1
to
fcaf824
Compare
src/error.rs
Outdated
//! - [`CastError`]: the error type of ref-conversions. | ||
//! - [`TryCastError`]: the error type of fallible ref-conversions. | ||
//! - [`TryReadError`]: the error type of fallible read-conversions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
//! - [`CastError`]: the error type of ref-conversions. | |
//! - [`TryCastError`]: the error type of fallible ref-conversions. | |
//! - [`TryReadError`]: the error type of fallible read-conversions. | |
//! - [`CastError`]: the error type of reference conversions | |
//! - [`TryCastError`]: the error type of fallible reference conversions | |
//! - [`TryReadError`]: the error type of fallible read conversions |
Just a stylistic preference; take it or leave it. IIUC, you're using "ref" to be consistent w/ method names, but IMO that makes sense specifically because method names are often abbreviated for code style reasons, which is a justification that doesn't apply for prose. Note, for example, that our FromBytes
method doc comments use the noun "reference" even for methods with ref
in the name. (Btw if you're going to accept this suggestion, the same applies elsewhere in this PR.)
(Removed periods to be consistent with the preceding bulleted list, and since these aren't full sentences)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
#[cfg(doc)] | ||
use crate::{FromBytes, Ref, TryFromBytes}; | ||
|
||
mod private { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment explaining why this is private and linking to the relevant tracking issue?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/error.rs
Outdated
/// - [`CastError`]: the error type of ref-conversions. | ||
/// - [`TryCastError`]: the error type of fallible ref-conversions. | ||
/// - [`TryReadError`]: the error type of fallible read-conversions. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same here as above
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add tests for the Display
impls (mostly so we have a place where we can write out how they're formatted - especially so we can see a diff if we change the format in the future)?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/lib.rs
Outdated
@@ -281,6 +281,11 @@ mod macros; | |||
|
|||
pub mod byteorder; | |||
mod deprecated; | |||
// Thid module is `pub` so that zerocopy's error types and error handling |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
// Thid module is `pub` so that zerocopy's error types and error handling | |
// This module is `pub` so that zerocopy's error types and error handling |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/pointer/ptr.rs
Outdated
I: Invariants<Validity = Initialized>, | ||
T: Immutable, | ||
{ | ||
/// Casts this pointer-to-initialized into a pointer-to-bytes. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Safety precondition?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/lib.rs
Outdated
.map_src(|src| { | ||
// SAFETY: The provided `size`, below, is computed from | ||
// `bytes.len()`. By contract on `try_into_valid` we can | ||
// count on `src` being equal to `candidate`. Since | ||
// `candidate` was derived from the entire extent of | ||
// `bytes`, the size of `bytes` is exactly equal to the | ||
// size of `src`. | ||
let bytes = unsafe { src.as_bytes_with_size(size) }; | ||
bytes.as_mut() | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It feels sketchy to me that we have to make this implementation so much messier (esp with introducing new unsafe
) than it was before just to support this change to our API. I wonder if there's something we can do to refactor this so it stays reasonable?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Perhaps; I'll poke at this a bit more. This is all done to work around our as_bytes
method having a Sized
bound. We'll be able to remove that bound once std::mem::size_of_val_raw
is stabilized, but until then, the workaround is to stash the size while we have it. I don't think it's likely we'll be able to avoid the new unsafe
here.
src/pointer/ptr.rs
Outdated
/// Unsafe code may rely on this method's returned `ValidityError` | ||
/// containing `self`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
/// Unsafe code may rely on this method's returned `ValidityError` | |
/// containing `self`. | |
/// On error, unsafe code may rely on this method's returned `ValidityError` | |
/// containing `self`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
src/pointer/ptr.rs
Outdated
@@ -1081,7 +1105,7 @@ mod _casts { | |||
/// alignment of `[u8]` is 1. | |||
impl<'a, I> Ptr<'a, [u8], I> | |||
where | |||
I: Invariants<Validity = Valid>, | |||
I: Invariants<Alignment = Aligned, Validity = Valid>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed. This is a holdover from some earlier experimentation.
This PR overhauls error reporting, replacing our `Option` returns with `Result`s. The `Err` type of these results provide both the cause of the failure, and access to the underlying source of the conversion. Makes progress towards #1139.
match Ref::new(bytes) { | ||
Ok(dst) => Ok(dst), | ||
Err(CastError::Size(e)) => Err(e), | ||
Err(CastError::Alignment(_)) => unreachable!(), | ||
Err(CastError::Validity(i)) => match i {}, | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We're doing this a lot; any way we can factor this out somehow?
This is an implementation of the ideas presented in #1139.
This PR:
error
module (whose items are also re-exported)Result
s with informative error types, rather thanOption
sFundamental Error Types
Generally speaking, zerocopy’s conversions may fail for one of up to three reasons, each reflected in a fundamental error type:
AlignmentError<Src, Dst>
: the conversion source was improperly alignedSizeError<Src, Dst>
: the conversion source was of incorrect sizeValidityError<Src, Dst>
: the conversion source contained invalid dataThese fundamental error types are parameterized with the failed conversion's:
Src
: the input value of the conversion, stored within the error; andDst
: the output type of the conversion, retained as phantom type for error reporting purposes.Methods that only have one failure mode return that mode's corresponding error type directly. For example,
Ref::new_unaligned
's only failure mode is a size mismatch. This PR revises its return type fromOption<Ref<B, T>>
toResult<Ref<B, T>, SizeError<B, T>>
.The input value of the conversion is retrievable from each of these fundamental types with an
into_src
method.Compound Error Types
Conversion methods that have either two or three possible failure modes return one of these error types:
CastError<Src, Dst>
: the error type of ref-conversions.TryCastError<Src, Dst>
: the error type of fallible ref-conversions.TryReadError<Src, Dst>
: the error type of fallible read-conversions.The input value of the conversion is retrievable from each of these compound types with an
into_src
method.These three types are parameterized aliases for the enum
ConvertError<A, S, V>
:(This enum is never used directly in zerocopy's public API, and is only referred to via these three parameterized aliases.)
These aliases parameterize their reachable branches of
ConvertError
with the appropriate fundamental error type, and their unreachable branches withInfallible
.Shared Interfaces
Both the compound and fundamental error types provide a shared set of affordances; they:
Src
Debug
,PartialEq
andEq
Display
User Experience Examples
Accessing the source and treating the error as opaque:
Accessing the source and reflecting on the error:
Future Outlook
The stabilization of
min_exhaustive_patterns
(rust-lang/rust#119612) will allow omitting infallible variants from matches.Comparison to #1150
Surface Area
This PR introduces seven public types to support error reporting. By contrast, #1150 introduces eleven.
API Complexity
In comparison to #1150, this PR slightly pessimizes access to the underlying byte slice: it provides by-value access via a
into_src()
method rather than via.src
. By contrast, this PR comparatively optimizes access to failure mode: it can be determined by matching directly on the returned error, as opposed to first accessing the failure mode via.err
.Conceptual Complexity
With the possible exception of
ConvertError
, this PR's types all implement a common, consistent interface: they all provide access to the kind of failure that occurred, access to underlying byte slice, and consistently rich error messages.By contrast, the capabilities provided by #1150's types vary depending on the type. Access to the source value is provided by
Error
,AlignmentError
,SizeError
,ValidityError
,CastError
,TryCastError
andTryReadError
, but not byConvertError
,Alignment
,Size
, orValidity
.The fidelity of the
Display
implementations vary by type:Error
,AlignmentError
,SizeError
,ValidityError
,CastError
,TryCastError
andTryReadError
provide high-fidelity error messages, whileConvertError
,Alignment
,Size
, orValidity
provide low fidelity error messages.Future Work
The return type of
try_transmute!
(which has not yet been implemented) should beResult<Src, ValidityError<Src, Dst>>
.The type
ConvertError
is not used directly in any public APIs and is defined in a private module to remove it and its variants (which are also marked#[doc(hidden)]
from our SemVer obligations. Before0.8
, we should strive to make these itemspub
so that we can demonstrate matching on error kind in our documentation and release notes.Makes progress towards #1139.