Skip to content

Commit

Permalink
Add a "tracing visitor" to make it easier to find decode issues (#52)
Browse files Browse the repository at this point in the history
* WIP Tracing visitor and improving the Value display impl

* Finish formatter and get it working in tracing decoder with type info

* Tidy up and clippy

* no-std-ise

* Don't expose PathSegment
  • Loading branch information
jsdw authored Jul 24, 2024
1 parent 9fc05e9 commit 0d96cc7
Show file tree
Hide file tree
Showing 9 changed files with 987 additions and 50 deletions.
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))?;
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;
64 changes: 64 additions & 0 deletions src/scale_impls/tracing_decoder/path.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
// 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::*;

// [jsdw] This could be internally turned into a linkedlist or something
// to make the "clone and append" faster. Not too concerned right now though
// since the tracing visitor it's used for isn't built for speed.
#[derive(Clone, Debug)]
pub struct Path(Vec<PathSegment>);

impl Path {
pub fn new() -> Path {
Path(vec![])
}
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))
}

fn at(&self, segment: PathSegment) -> Path {
let mut p = self.0.clone();
p.push(segment);
Path(p)
}
}

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)]
enum PathSegment {
Field(String),
Index(usize),
Variant(String),
}
Loading

0 comments on commit 0d96cc7

Please sign in to comment.