Skip to content
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

Add a "tracing visitor" to make it easier to find decode issues #52

Merged
merged 5 commits into from
Jul 24, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,33 @@ pub mod scale {
) -> Result<(), EncodeError> {
value.encode_as_type_to(ty_id, types, buf)
}

/// A visitor and function to decode some bytes into a [`crate::Value`] while tracing the current
/// decoding state so that a more detailed error can be returned in the event of a failure.
pub mod tracing {
pub use crate::scale_impls::{TraceDecodingError, TraceDecodingVisitor};

/// Decode a value using the [`TraceDecodingVisitor`], which internally keeps track of the current decoding state, and as
/// a result hands back a much more detailed error than [`crate::scale::decode_as_type()`] if decoding fails.
///
/// One approach is to use the standard visitor for decoding on the "happy path", and if you need more information about
/// the decode error, to try decoding the same bytes again using this function to obtain more information about what failed.
pub fn decode_as_type<R>(
data: &mut &[u8],
ty_id: R::TypeId,
types: &R,
) -> Result<crate::Value<R::TypeId>, TraceDecodingError<crate::Value<R::TypeId>>>
where
R: scale_type_resolver::TypeResolver,
{
scale_decode::visitor::decode_with_visitor(
data,
ty_id,
types,
TraceDecodingVisitor::new(),
)
}
}
}

/// Converting a [`crate::Value`] to or from strings.
Expand Down
2 changes: 2 additions & 0 deletions src/scale_impls/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,7 @@

mod decode;
mod encode;
mod tracing_decoder;

pub use decode::{decode_composite_as_fields, decode_value_as_type, DecodeError};
pub use tracing_decoder::{TraceDecodingError, TraceDecodingVisitor};
198 changes: 198 additions & 0 deletions src/scale_impls/tracing_decoder/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
// Copyright (C) 2022-2024 Parity Technologies (UK) Ltd. (admin@parity.io)
// This file is a part of the scale-value crate.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use super::path::Path;
use crate::prelude::*;
use crate::scale::DecodeError;
use crate::{Composite, Primitive, Value, ValueDef};
use core::fmt::Write;

/// An error encountered when decoding some bytes using the [`crate::scale::tracing`] module.
#[derive(Clone, Debug)]
pub struct TraceDecodingError<Val> {
inner: TraceDecodingErrorInner<Val>,
}

impl<Val> TraceDecodingError<Val> {
pub(crate) fn map_decoded_so_far<NewVal>(
self,
f: impl FnOnce(Val) -> NewVal,
) -> TraceDecodingError<NewVal> {
match self.inner {
TraceDecodingErrorInner::FromDecodeError(e) => {
TraceDecodingErrorInner::FromDecodeError(e).into()
}
TraceDecodingErrorInner::FromVisitor(e) => {
TraceDecodingErrorInner::FromVisitor(VisitorError {
at: e.at,
decode_error: e.decode_error,
decoded_so_far: f(e.decoded_so_far),
})
.into()
}
}
}
pub(crate) fn with_outer_context<NewVal>(
self,
outer_path: impl FnOnce() -> Path,
default_outer_value: impl FnOnce() -> NewVal,
into_outer_value: impl FnOnce(Val) -> NewVal,
) -> TraceDecodingError<NewVal> {
match self.inner {
TraceDecodingErrorInner::FromDecodeError(e) => {
TraceDecodingErrorInner::FromVisitor(VisitorError {
at: outer_path(),
decoded_so_far: default_outer_value(),
decode_error: e,
})
.into()
}
TraceDecodingErrorInner::FromVisitor(e) => {
TraceDecodingErrorInner::FromVisitor(VisitorError {
at: e.at,
decoded_so_far: into_outer_value(e.decoded_so_far),
decode_error: e.decode_error,
})
.into()
}
}
}
}

impl<Val> From<TraceDecodingErrorInner<Val>> for TraceDecodingError<Val> {
fn from(value: TraceDecodingErrorInner<Val>) -> Self {
TraceDecodingError { inner: value }
}
}

#[derive(Clone, Debug)]
enum TraceDecodingErrorInner<Val> {
FromDecodeError(DecodeError),
FromVisitor(VisitorError<Val>),
}

#[derive(Clone, Debug)]
struct VisitorError<Val> {
at: Path,
decoded_so_far: Val,
decode_error: DecodeError,
}

impl<Ctx: core::fmt::Debug> core::fmt::Display for TraceDecodingError<Value<Ctx>> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match &self.inner {
TraceDecodingErrorInner::FromDecodeError(e) => {
write!(f, "Error decoding value: {e}")
}
TraceDecodingErrorInner::FromVisitor(e) => {
write!(
f,
"Error decoding value at {}: {}\nDecoded so far:\n\n",
e.at, e.decode_error,
)?;
display_value_with_typeid(f, &e.decoded_so_far)
}
}
}
}

#[cfg(feature = "std")]
impl<Ctx: core::fmt::Debug> std::error::Error for TraceDecodingError<Value<Ctx>> {}

impl<TypeId> From<DecodeError> for TraceDecodingError<TypeId> {
fn from(value: DecodeError) -> Self {
TraceDecodingErrorInner::FromDecodeError(value).into()
}
}

impl<TypeId> From<codec::Error> for TraceDecodingError<TypeId> {
fn from(value: codec::Error) -> Self {
TraceDecodingErrorInner::FromDecodeError(value.into()).into()
}
}

fn display_value_with_typeid<Id: core::fmt::Debug>(
f: &mut core::fmt::Formatter<'_>,
value: &Value<Id>,
) -> core::fmt::Result {
use crate::string_impls::{fmt_value, FormatOpts, Formatter};

let format_opts = FormatOpts::new()
.spaced()
.context(|type_id, writer: &mut &mut core::fmt::Formatter| write!(writer, "{type_id:?}"))
.custom_formatter(|value, writer| custom_hex_formatter(value, writer));
let mut formatter = Formatter::new(f, format_opts);

fmt_value(value, &mut formatter)
}

fn custom_hex_formatter<T, W: core::fmt::Write>(
value: &Value<T>,
writer: W,
) -> Option<core::fmt::Result> {
// Print unnamed sequences of u8s as hex strings; ignore anything else.
if let ValueDef::Composite(Composite::Unnamed(vals)) = &value.value {
for val in vals {
if !matches!(val.value, ValueDef::Primitive(Primitive::U128(n)) if n < 256) {
return None;
}
}
Some(value_to_hex(vals, writer))
} else {
None
}
}

// Just to avoid needing to import the `hex` dependency just for this.
fn value_to_hex<T, W: core::fmt::Write>(vals: &Vec<Value<T>>, mut writer: W) -> core::fmt::Result {
writer.write_str("0x")?;
for val in vals {
if let ValueDef::Primitive(Primitive::U128(n)) = &val.value {
let n = *n as u8;
writer.write_char(u4_to_hex(n >> 4))?;
pkhry marked this conversation as resolved.
Show resolved Hide resolved
writer.write_char(u4_to_hex(n & 0b00001111))?;
}
}
Ok(())
}

fn u4_to_hex(n: u8) -> char {
static HEX: [char; 16] =
['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
*HEX.get(n as usize).expect("Expected a u4 (value between 0..=15")
}

#[cfg(test)]
mod test {
use super::*;
use crate::value;

#[test]
fn test_value_to_hex() {
let mut s = String::new();
custom_hex_formatter(&value! {(0usize,230usize,255usize,15usize,12usize,4usize)}, &mut s)
.expect("decided not to convert to hex")
.expect("can't write to writer without issues");

assert_eq!(s, "0x00E6FF0F0C04");
}

#[test]
fn test_value_not_to_hex() {
let mut s = String::new();
// 256 is too big to be a u8, so this value isn't valid hex.
assert_eq!(custom_hex_formatter(&value! {(0usize,230usize,256usize)}, &mut s), None);
}
}
21 changes: 21 additions & 0 deletions src/scale_impls/tracing_decoder/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (C) 2022-2024 Parity Technologies (UK) Ltd. (admin@parity.io)
// This file is a part of the scale-value crate.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

mod error;
mod path;
mod visitor;

pub use error::TraceDecodingError;
pub use visitor::TraceDecodingVisitor;
60 changes: 60 additions & 0 deletions src/scale_impls/tracing_decoder/path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// Copyright (C) 2022-2024 Parity Technologies (UK) Ltd. (admin@parity.io)
// This file is a part of the scale-value crate.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

use crate::prelude::*;

#[derive(Clone, Debug)]
pub struct Path(Vec<PathSegment>);

impl Path {
pub fn new() -> Path {
Path(vec![])
}
pub fn at(&self, segment: PathSegment) -> Path {
let mut p = self.0.clone();
p.push(segment);
Path(p)
}
pub fn at_idx(&self, idx: usize) -> Path {
self.at(PathSegment::Index(idx))
}
pub fn at_field(&self, field: String) -> Path {
self.at(PathSegment::Field(field))
}
pub fn at_variant(&self, variant: String) -> Path {
self.at(PathSegment::Variant(variant))
}
}

impl core::fmt::Display for Path {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
for segment in &self.0 {
write!(f, ".")?;
match segment {
PathSegment::Index(idx) => write!(f, "[{idx}]")?,
PathSegment::Field(field) => write!(f, "{field}")?,
PathSegment::Variant(variant) => write!(f, "{variant}")?,
}
}
Ok(())
}
}

#[derive(Clone, Debug)]
pub enum PathSegment {
Field(String),
Index(usize),
Variant(String),
}
Loading
Loading