Repository navigation
Support custom validators for TryFromBytes #1330
Copy link
Copy link
Open
Labels
customer-requestDocuments customer requests.Documents customer requests.
Description
Blocking before release
- Introduce unstable
#[zerocopy(invariant(...))]library invariant checking #3681 (comment) - Introduce unstable
#[zerocopy(invariant(...))]library invariant checking #3681 (comment) - Introduce unstable
#[zerocopy(invariant(...))]library invariant checking #3681 (comment)
These are kinds of validity that users may need to have checked before transmutation from &[u8] to &T:
- The language-level validity of the bits for the type of each field in
T, e.g. aboolmust be either0or1. This is implemented by thederive. - The library-level validity of the bits in
T, e.g. an invariant that the first field is less than the second. This can be referenced by thederivebut inherently must be user-controlled. - The library-level validity of the individual fields in
T, based on the above library-level check applied to each field. This is also implemented by thederive. - The library-level validity of the length of the struct given the header contents, e.g. the
lengthfield is equal to the size of the tail slice. This only applies to dynamically sized structs ending in a slice.
The plan discussed in #5 and #372 is to support the concept of a custom validator, a function or closure provided to derive(TryFromBytes) that will always be called before allowing a TryFromBytes transmute to succeed.
Open Questions
- Should users be able to provide their own error type, to communicate the way in which the validation failed? How would this be exposed to the higher-level
TryFromBytesAPIs? - How will data validation interact with custom DSTs? Is the validator also responsible for validating the correct length of the tail slice, or even deriving the correct length (as proposed in Support casting to a
KnownLayouttype with a user-provided length computed dynamically #1289 (comment))?- What happens if the user provides both a
#[length]and custom validator function and they disagree? - Given
struct Outer { x: u8, y: Inner }/struct Inner { a: [u8; 4], b: [u8] }, is the validator forOuterallowed to communicate a maximum length for the tail slice located inside ofInner? What ifInnerhas a custom validator that returns "valid if the tail slice is truncated to N", butOuterhas a different return from its custom validator?
- What happens if the user provides both a
- What are the required signature(s) for the custom validators? In theory the derive can support many return types simultaneously, including
bool,Result<(), CustomError>, orResult<Option<usize>, CustomError>(to communicate a required length). - When zerocopy's expectations surrounding validator behavior are violated, should it panic (terrible for embedded), only panic in debug mode (middle ground a la
+overflow checking), or always reject the input (could miss bugs in validators). - Should we ban mutation inside of a validator (either programmatically or as a safety invariant)? See also:
TryFromBytes::is_bit_validshould promise not to mutate its referent #1831
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
customer-requestDocuments customer requests.Documents customer requests.
Activity
joshlf commentedon May 20, 2024
See also: #590
joshlf commentedon Oct 4, 2024
@jswrenn and I met and discussed potential designs. He has his own design proposal that he'll share at some point. Here's mine.
This design is based on the observation that some users may want to not only validate safety conditions, but actively transform their type - for example to construct a new witness wrapper type. It permits a distinction between the type being constructed and its "raw" equivalent.
This design is also explicitly meant to support custom length fields (#1289). In order to do that, validation can return a rich error message which might be required when a length field cannot be parsed.
We didn't have time during our discussion to think deeply about how this composes with a
#[length]attribute. For example, is the length extracted before or after calling the user's custom validator? Can the#[length]attribute itself provide a "custom extractor" that also has error cases? These will need to be thought through.Here's how this would be used by a hypothetical user:
Our derive would emit the following impl:
As written, this gives the user full power: they are responsible for converting
Self::Raw::ValidationErrorintoSelf::ValidationErrorandMaybeAligned<Self::Raw>intoMaybeAligned<Self>. However, the user may not want to deal with all of these details. Thus, we can abstract somewhat and support the following simplifications:Result; the framework will handle propagating errorsboolrather than aResult; the framework will handle generatingOkorErrvalues as appropriateFirst, we introduce the following trait and implement it for different function types. Each impl (except for the most general one, which puts all of the onus on the user) carries restrictions:
Result, we add aT::ValidationError: From<<T::Raw as TryFromBytes>::ValidationError>bound to ensure that, if an error is encountered, the framework can convert the errorbool, we additionally requireT::ValidationError: Defaultto ensure that our framework can synthesize new errors when the validator returnsfalseFinally, we can change the derive-generated code like so:
Note that this also supports closures rather than named validators. For example, if the user specifies
#[zerocopy(validator = |foo| foo.0.is_valid())], this desugars as expected:@djkoloski if we added validation context to this design, would it support your rkyv use case? In particular, is the ability to perform mutation in the validator enough to do your fix-up operation?
We'd at a minimum need a slight tweak: we'd have to have a way of signaling that a
TryFromBytesimpl only works on mutable input in order to do the fix-up. But let's assume we've done that for the sake of this question.newpavlov commentedon Dec 27, 2024
@joshlf
What is motivation for passing
Resulttotry_from_raw? I would assume that if we have failed to get a raw value, then there is no reason to run user-provided validators.As for errors, in my use cases returning
boolshould be sufficient since failed validation means we got corrupted data and we can not do any reasonable error processing based on failure kind inside the program. Maybe it can be done by usingsimple_validatorforbool-based functions andvalidatorforResult-based?joshlf commentedon Nov 6, 2025
It's just to give us a consistent place to map the validity error so that there's a consistent error type in the final
Resulttype.Hmmm maybe – interesting idea!
Use `ReadOnly` in `TryFromBytes::is_bit_valid` (#2873)
Introduce unstable `#[zerocopy(invariant(...))]` library invariant ch…