consensus_encoding: drop Either for phantom data
What changed, and why it matters
This commit is a routine internal refactoring of how composite data decoders combine error types. It removes a custom Either sum type and instead uses Rust's standard trait conversion (From) plus phantom data to enforce that all inner decoder errors can be converted into a single common error type. There is no indication this fixes a security bug; it is a design/API cleanup.
No security action required. Treat as normal code-quality/API refactor during review.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The patch rewrites Decoder2/Decoder3/Decoder4/Decoder6 in consensus_encoding to drop the Either
Changed components
consensus_encoding/src/decode/decoders.rsconsensus_encoding/src/lib.rsconsensus_encoding/tests/composition.rsInspect captured patch +248 / −74
diff --git a/consensus_encoding/src/decode/decoders.rs b/consensus_encoding/src/decode/decoders.rs
index ba0041d6..a502516c 100644
--- a/consensus_encoding/src/decode/decoders.rs
+++ b/consensus_encoding/src/decode/decoders.rs
@@ -4,42 +4,6 @@
use super::Decoder;
-/// A sum type representing one of two possible decoder errors.
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum Either<F, S> {
- /// The first variant.
- First(F),
- /// The second variant.
- Second(S),
-}
-
-impl<F, S> core::fmt::Display for Either<F, S>
-where
- F: core::fmt::Display,
- S: core::fmt::Display,
-{
- fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
- match self {
- Either::First(first) => first.fmt(f),
- Either::Second(second) => second.fmt(f),
- }
- }
-}
-
-#[cfg(feature = "std")]
-impl<F, S> std::error::Error for Either<F, S>
-where
- F: std::error::Error,
- S: std::error::Error,
-{
- fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
- match self {
- Either::First(first) => first.source(),
- Either::Second(second) => second.source(),
- }
- }
-}
-
/// Not enough bytes given to decoder.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UnexpectedEof {
@@ -100,13 +64,15 @@ impl<const N: usize> Decoder for ArrayDecoder<N> {
}
}
-/// A decoder which decodes two objects, one after the other.
-pub struct Decoder2<A, B>
+/// A decoder which wraps two inner decoders and returns the output of both.
+/// The error types of the inner decoders are mapped to a common type.
+pub struct Decoder2<A, B, Err>
where
A: Decoder,
B: Decoder,
{
state: Decoder2State<A, B>,
+ _error: core::marker::PhantomData<Err>,
}
enum Decoder2State<A: Decoder, B: Decoder> {
@@ -138,28 +104,31 @@ impl<A: Decoder, B: Decoder> Decoder2State<A, B> {
}
}
-impl<A, B> Decoder2<A, B>
+impl<A, B, Err> Decoder2<A, B, Err>
where
A: Decoder,
B: Decoder,
{
/// Constructs a new composite decoder.
- pub fn new(first: A, second: B) -> Self { Self { state: Decoder2State::First(first, second) } }
+ pub fn new(first: A, second: B) -> Self {
+ Self { state: Decoder2State::First(first, second), _error: core::marker::PhantomData }
+ }
}
-impl<A, B> Decoder for Decoder2<A, B>
+impl<A, B, Err> Decoder for Decoder2<A, B, Err>
where
A: Decoder,
B: Decoder,
+ Err: From<A::Error> + From<B::Error>,
{
type Output = (A::Output, B::Output);
- type Error = Either<A::Error, B::Error>;
+ type Error = Err;
fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result<bool, Self::Error> {
loop {
match &mut self.state {
Decoder2State::First(first_decoder, _) => {
- if first_decoder.push_bytes(bytes).map_err(Either::First)? {
+ if first_decoder.push_bytes(bytes).map_err(Err::from)? {
// First decoder wants more data.
return Ok(true);
}
@@ -168,14 +137,14 @@ where
let (first, second) = self.state.transition();
let first_result = first.end().map_err(|error| {
self.state = Decoder2State::Errored;
- Either::First(error)
+ Err::from(error)
})?;
self.state = Decoder2State::Second(first_result, second);
}
Decoder2State::Second(_, second_decoder) => {
return second_decoder.push_bytes(bytes).map_err(|error| {
self.state = Decoder2State::Errored;
- Either::Second(error)
+ Err::from(error)
});
}
Decoder2State::Errored => {
@@ -194,12 +163,12 @@ where
// This branch is most likely an error since the decoder
// never got to the second one. But letting the error bubble
// up naturally from the child decoders.
- let first_result = first_decoder.end().map_err(Either::First)?;
- let second_result = second_decoder.end().map_err(Either::Second)?;
+ let first_result = first_decoder.end().map_err(Err::from)?;
+ let second_result = second_decoder.end().map_err(Err::from)?;
Ok((first_result, second_result))
}
Decoder2State::Second(first_result, second_decoder) => {
- let second_result = second_decoder.end().map_err(Either::Second)?;
+ let second_result = second_decoder.end().map_err(Err::from)?;
Ok((first_result, second_result))
}
Decoder2State::Errored => {
@@ -213,35 +182,42 @@ where
}
/// A decoder which decodes three objects, one after the other.
-pub struct Decoder3<A, B, C>
+pub struct Decoder3<A, B, C, Err>
where
A: Decoder,
B: Decoder,
C: Decoder,
+ Err: From<A::Error> + From<B::Error> + From<C::Error>,
{
- inner: Decoder2<Decoder2<A, B>, C>,
+ inner: Decoder2<Decoder2<A, B, Err>, C, Err>,
+ _error: core::marker::PhantomData<Err>,
}
-impl<A, B, C> Decoder3<A, B, C>
+impl<A, B, C, Err> Decoder3<A, B, C, Err>
where
A: Decoder,
B: Decoder,
C: Decoder,
+ Err: From<A::Error> + From<B::Error> + From<C::Error>,
{
/// Constructs a new composite decoder.
pub fn new(dec_1: A, dec_2: B, dec_3: C) -> Self {
- Self { inner: Decoder2::new(Decoder2::new(dec_1, dec_2), dec_3) }
+ Self {
+ inner: Decoder2::new(Decoder2::new(dec_1, dec_2), dec_3),
+ _error: core::marker::PhantomData,
+ }
}
}
-impl<A, B, C> Decoder for Decoder3<A, B, C>
+impl<A, B, C, Err> Decoder for Decoder3<A, B, C, Err>
where
A: Decoder,
B: Decoder,
C: Decoder,
+ Err: From<A::Error> + From<B::Error> + From<C::Error>,
{
type Output = (A::Output, B::Output, C::Output);
- type Error = Either<Either<A::Error, B::Error>, C::Error>;
+ type Error = Err;
fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result<bool, Self::Error> {
self.inner.push_bytes(bytes)
@@ -254,38 +230,45 @@ where
}
/// A decoder which decodes four objects, one after the other.
-pub struct Decoder4<A, B, C, D>
+pub struct Decoder4<A, B, C, D, Err>
where
A: Decoder,
B: Decoder,
C: Decoder,
D: Decoder,
+ Err: From<A::Error> + From<B::Error> + From<C::Error> + From<D::Error>,
{
- inner: Decoder2<Decoder2<A, B>, Decoder2<C, D>>,
+ inner: Decoder2<Decoder2<A, B, Err>, Decoder2<C, D, Err>, Err>,
+ _error: core::marker::PhantomData<Err>,
}
-impl<A, B, C, D> Decoder4<A, B, C, D>
+impl<A, B, C, D, Err> Decoder4<A, B, C, D, Err>
where
A: Decoder,
B: Decoder,
C: Decoder,
D: Decoder,
+ Err: From<A::Error> + From<B::Error> + From<C::Error> + From<D::Error>,
{
/// Constructs a new composite decoder.
pub fn new(dec_1: A, dec_2: B, dec_3: C, dec_4: D) -> Self {
- Self { inner: Decoder2::new(Decoder2::new(dec_1, dec_2), Decoder2::new(dec_3, dec_4)) }
+ Self {
+ inner: Decoder2::new(Decoder2::new(dec_1, dec_2), Decoder2::new(dec_3, dec_4)),
+ _error: core::marker::PhantomData,
+ }
}
}
-impl<A, B, C, D> Decoder for Decoder4<A, B, C, D>
+impl<A, B, C, D, Err> Decoder for Decoder4<A, B, C, D, Err>
where
A: Decoder,
B: Decoder,
C: Decoder,
D: Decoder,
+ Err: From<A::Error> + From<B::Error> + From<C::Error> + From<D::Error>,
{
type Output = (A::Output, B::Output, C::Output, D::Output);
- type Error = Either<Either<A::Error, B::Error>, Either<C::Error, D::Error>>;
+ type Error = Err;
fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result<bool, Self::Error> {
self.inner.push_bytes(bytes)
@@ -298,7 +281,8 @@ where
}
/// A decoder which decodes six objects, one after the other.
-pub struct Decoder6<A, B, C, D, E, F>
+#[allow(clippy::type_complexity)] // Nested composition is easier than flattened alternatives.
+pub struct Decoder6<A, B, C, D, E, F, Err>
where
A: Decoder,
B: Decoder,
@@ -306,11 +290,18 @@ where
D: Decoder,
E: Decoder,
F: Decoder,
+ Err: From<A::Error>
+ + From<B::Error>
+ + From<C::Error>
+ + From<D::Error>
+ + From<E::Error>
+ + From<F::Error>,
{
- inner: Decoder2<Decoder3<A, B, C>, Decoder3<D, E, F>>,
+ inner: Decoder2<Decoder3<A, B, C, Err>, Decoder3<D, E, F, Err>, Err>,
+ _error: core::marker::PhantomData<Err>,
}
-impl<A, B, C, D, E, F> Decoder6<A, B, C, D, E, F>
+impl<A, B, C, D, E, F, Err> Decoder6<A, B, C, D, E, F, Err>
where
A: Decoder,
B: Decoder,
@@ -318,6 +309,12 @@ where
D: Decoder,
E: Decoder,
F: Decoder,
+ Err: From<A::Error>
+ + From<B::Error>
+ + From<C::Error>
+ + From<D::Error>
+ + From<E::Error>
+ + From<F::Error>,
{
/// Constructs a new composite decoder.
pub fn new(dec_1: A, dec_2: B, dec_3: C, dec_4: D, dec_5: E, dec_6: F) -> Self {
@@ -326,11 +323,12 @@ where
Decoder3::new(dec_1, dec_2, dec_3),
Decoder3::new(dec_4, dec_5, dec_6),
),
+ _error: core::marker::PhantomData,
}
}
}
-impl<A, B, C, D, E, F> Decoder for Decoder6<A, B, C, D, E, F>
+impl<A, B, C, D, E, F, Err> Decoder for Decoder6<A, B, C, D, E, F, Err>
where
A: Decoder,
B: Decoder,
@@ -338,12 +336,15 @@ where
D: Decoder,
E: Decoder,
F: Decoder,
+ Err: From<A::Error>
+ + From<B::Error>
+ + From<C::Error>
+ + From<D::Error>
+ + From<E::Error>
+ + From<F::Error>,
{
type Output = (A::Output, B::Output, C::Output, D::Output, E::Output, F::Output);
- type Error = Either<
- Either<Either<A::Error, B::Error>, C::Error>,
- Either<Either<D::Error, E::Error>, F::Error>,
- >;
+ type Error = Err;
fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result<bool, Self::Error> {
self.inner.push_bytes(bytes)
diff --git a/consensus_encoding/src/lib.rs b/consensus_encoding/src/lib.rs
index 3246d1c3..723eeae1 100644
--- a/consensus_encoding/src/lib.rs
+++ b/consensus_encoding/src/lib.rs
@@ -23,7 +23,7 @@ mod decode;
mod encode;
pub use self::decode::decoders::{
- ArrayDecoder, Decoder2, Decoder3, Decoder4, Decoder6, Either, UnexpectedEof,
+ ArrayDecoder, Decoder2, Decoder3, Decoder4, Decoder6, UnexpectedEof,
};
pub use self::decode::{Decodable, Decoder};
#[cfg(feature = "alloc")]
diff --git a/consensus_encoding/tests/composition.rs b/consensus_encoding/tests/composition.rs
index aec4306d..778073f1 100644
--- a/consensus_encoding/tests/composition.rs
+++ b/consensus_encoding/tests/composition.rs
@@ -4,7 +4,7 @@
use consensus_encoding::{
ArrayDecoder, ArrayEncoder, Decodable, Decoder, Decoder2, Decoder6, Encodable, Encoder,
- Encoder2, Encoder6,
+ Encoder2, Encoder6, UnexpectedEof,
};
const EMPTY: &[u8] = &[];
@@ -27,9 +27,27 @@ impl Encodable for CompositeData {
}
}
+/// A unified error type for [`CompositeDataDecoder`].
+#[derive(Debug, Clone, PartialEq, Eq)]
+enum CompositeError {
+ Eof(UnexpectedEof),
+}
+
+impl From<UnexpectedEof> for CompositeError {
+ fn from(eof: UnexpectedEof) -> Self { CompositeError::Eof(eof) }
+}
+
+impl core::fmt::Display for CompositeError {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ match self {
+ CompositeError::Eof(eof) => write!(f, "Error in first array: {}", eof),
+ }
+ }
+}
+
/// A wrapper decoder that converts the tuple output to [`CompositeData`].
struct CompositeDataDecoder {
- inner: Decoder2<ArrayDecoder<4>, ArrayDecoder<2>>,
+ inner: Decoder2<ArrayDecoder<4>, ArrayDecoder<2>, CompositeError>,
}
impl CompositeDataDecoder {
@@ -40,7 +58,7 @@ impl CompositeDataDecoder {
impl Decoder for CompositeDataDecoder {
type Output = CompositeData;
- type Error = <Decoder2<ArrayDecoder<4>, ArrayDecoder<2>> as Decoder>::Error;
+ type Error = CompositeError;
fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result<bool, Self::Error> {
self.inner.push_bytes(bytes)
@@ -97,7 +115,7 @@ fn composition_nested() {
}
assert_eq!(encoded_bytes, data);
- let mut decoder6 = Decoder6::new(
+ let mut decoder6: Decoder6<_, _, _, _, _, _, UnexpectedEof> = Decoder6::new(
ArrayDecoder::<1>::new(),
ArrayDecoder::<1>::new(),
ArrayDecoder::<1>::new(),
@@ -121,7 +139,8 @@ fn composition_nested() {
#[test]
fn composition_extra_bytes() {
// Test that Decoder2 consumes exactly what it needs and leaves extra bytes unconsumed.
- let mut decoder2 = Decoder2::new(ArrayDecoder::<2>::new(), ArrayDecoder::<3>::new());
+ let mut decoder2: Decoder2<_, _, UnexpectedEof> =
+ Decoder2::new(ArrayDecoder::<2>::new(), ArrayDecoder::<3>::new());
let mut bytes = &[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08][..];
let original_len = bytes.len();
@@ -137,3 +156,157 @@ fn composition_extra_bytes() {
assert_eq!(first, [0x01, 0x02], "First decoder should get first 2 bytes");
assert_eq!(second, [0x03, 0x04, 0x05], "Second decoder should get next 3 bytes");
}
+
+#[test]
+#[allow(clippy::too_many_lines)]
+fn composition_error_unification() {
+ // Demonstrates how decoders unify error types into
+ // a single target error type through `From` conversions.
+
+ /// Error for the lower level decoders.
+ #[derive(Debug, Clone, PartialEq, Eq)]
+ enum NestedError {
+ BadChecksum,
+ UnexpectedEof(UnexpectedEof),
+ }
+
+ impl From<UnexpectedEof> for NestedError {
+ fn from(eof: UnexpectedEof) -> Self { NestedError::UnexpectedEof(eof) }
+ }
+
+ /// Error for top level encoder.
+ #[derive(Debug, Clone, PartialEq, Eq)]
+ enum TopLevelError {
+ UnexpectedEof(UnexpectedEof),
+ Validation(NestedError),
+ }
+
+ impl From<UnexpectedEof> for TopLevelError {
+ fn from(eof: UnexpectedEof) -> Self { TopLevelError::UnexpectedEof(eof) }
+ }
+
+ impl From<NestedError> for TopLevelError {
+ fn from(err: NestedError) -> Self {
+ match err {
+ NestedError::UnexpectedEof(eof) => TopLevelError::UnexpectedEof(eof),
+ NestedError::BadChecksum => TopLevelError::Validation(err),
+ }
+ }
+ }
+
+ /// A test composite decoder.
+ struct HeaderDecoder {
+ inner: Decoder2<ArrayDecoder<1>, ArrayDecoder<1>, NestedError>,
+ }
+
+ impl HeaderDecoder {
+ fn new() -> Self {
+ Self { inner: Decoder2::new(ArrayDecoder::<1>::new(), ArrayDecoder::<1>::new()) }
+ }
+ }
+
+ impl Decoder for HeaderDecoder {
+ type Output = ([u8; 1], [u8; 1]);
+ type Error = NestedError;
+
+ fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result<bool, Self::Error> {
+ self.inner.push_bytes(bytes)
+ }
+
+ fn end(self) -> Result<Self::Output, Self::Error> {
+ let (first, second) = self.inner.end()?;
+ Ok((first, second))
+ }
+ }
+
+ /// Another test composite decoder.
+ struct PayloadDecoder {
+ inner: ArrayDecoder<4>,
+ }
+
+ impl PayloadDecoder {
+ fn new() -> Self { Self { inner: ArrayDecoder::<4>::new() } }
+ }
+
+ impl Decoder for PayloadDecoder {
+ type Output = [u8; 4];
+ type Error = TopLevelError;
+
+ fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result<bool, Self::Error> {
+ Ok(self.inner.push_bytes(bytes)?)
+ }
+
+ fn end(self) -> Result<Self::Output, Self::Error> {
+ let result = self.inner.end()?;
+ Ok(result)
+ }
+ }
+
+ /// A decoder which can fail.
+ struct FailingDecoder {
+ inner: ArrayDecoder<1>,
+ should_fail: bool,
+ }
+
+ impl FailingDecoder {
+ fn new(should_fail: bool) -> Self { Self { inner: ArrayDecoder::<1>::new(), should_fail } }
+ }
+
+ impl Decoder for FailingDecoder {
+ type Output = [u8; 1];
+ type Error = NestedError;
+
+ fn push_bytes(&mut self, bytes: &mut &[u8]) -> Result<bool, Self::Error> {
+ self.inner.push_bytes(bytes).map_err(NestedError::from)
+ }
+
+ fn end(self) -> Result<Self::Output, Self::Error> {
+ if self.should_fail {
+ Err(NestedError::BadChecksum)
+ } else {
+ self.inner.end().map_err(NestedError::from)
+ }
+ }
+ }
+
+ // A multi-layer, nested, decoder structure with a unified top level error type.
+ let mut nested_decoder: Decoder6<
+ HeaderDecoder,
+ PayloadDecoder,
+ ArrayDecoder<1>,
+ HeaderDecoder,
+ PayloadDecoder,
+ ArrayDecoder<2>,
+ TopLevelError,
+ > = Decoder6::new(
+ HeaderDecoder::new(),
+ PayloadDecoder::new(),
+ ArrayDecoder::<1>::new(),
+ HeaderDecoder::new(),
+ PayloadDecoder::new(),
+ ArrayDecoder::<2>::new(),
+ );
+
+ let test_data = b"abcdefghijklmno";
+ let mut bytes = &test_data[..];
+ let push_result = nested_decoder.push_bytes(&mut bytes);
+ assert!(push_result.is_ok(), "push_bytes should succeed, got error: {:?}", push_result.err());
+ let end_result = nested_decoder.end();
+ assert!(end_result.is_ok(), "end should succeed, got error: {:?}", end_result.err());
+
+ // Test error during decoding.
+ let mut failing_decoder: Decoder2<FailingDecoder, ArrayDecoder<1>, TopLevelError> =
+ Decoder2::new(FailingDecoder::new(true), ArrayDecoder::<1>::new());
+ let test_data = b"ab";
+ let mut bytes = &test_data[..];
+ let push_result = failing_decoder.push_bytes(&mut bytes);
+ assert!(push_result.is_err(), "push_bytes should fail when first decoder fails in end()");
+ assert!(
+ matches!(
+ push_result.as_ref().unwrap_err(),
+ TopLevelError::Validation(NestedError::BadChecksum)
+ ),
+ "Expected TopLevelError::Validation(NestedError::BadChecksum), got {:?}",
+ push_result.unwrap_err()
+ );
+}
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.