From 2b6bcf085c77a50642d00f7763dacc63274b85c9 Mon Sep 17 00:00:00 2001 From: Martin Habovstiak Date: Tue, 20 Sep 2022 17:11:35 +0200 Subject: [PATCH] Implement support for `alloc`-free parse errors This implements basic facilities to conditionally carry string inputs in parse errors. This includes: * `InputString` type that may carry the input and format it * `parse_error_type!` macro creating a special type for parse errors * `impl_parse` implementing parsing for various types as well as its `serde`-supporting alternative --- Cargo-minimal.lock | 3 + Cargo-recent.lock | 3 + bitcoin/Cargo.toml | 2 +- internals/Cargo.toml | 1 + internals/build.rs | 3 +- internals/src/error.rs | 5 ++ internals/src/error/input_string.rs | 126 ++++++++++++++++++++++++++++ internals/src/error/parse_error.rs | 45 ++++++++++ internals/src/lib.rs | 2 + internals/src/parse.rs | 109 ++++++++++++++++++++++++ internals/src/serde.rs | 73 ++++++++++++++++ 11 files changed, 370 insertions(+), 2 deletions(-) create mode 100644 internals/src/error/input_string.rs create mode 100644 internals/src/error/parse_error.rs create mode 100644 internals/src/parse.rs create mode 100644 internals/src/serde.rs diff --git a/Cargo-minimal.lock b/Cargo-minimal.lock index afba03d5..a5f10ce0 100644 --- a/Cargo-minimal.lock +++ b/Cargo-minimal.lock @@ -64,6 +64,9 @@ dependencies = [ [[package]] name = "bitcoin-internals" version = "0.1.0" +dependencies = [ + "serde", +] [[package]] name = "bitcoin_hashes" diff --git a/Cargo-recent.lock b/Cargo-recent.lock index ca9c366a..5e0c682e 100644 --- a/Cargo-recent.lock +++ b/Cargo-recent.lock @@ -63,6 +63,9 @@ dependencies = [ [[package]] name = "bitcoin-internals" version = "0.1.0" +dependencies = [ + "serde", +] [[package]] name = "bitcoin_hashes" diff --git a/bitcoin/Cargo.toml b/bitcoin/Cargo.toml index b7aaa3c5..24c0088b 100644 --- a/bitcoin/Cargo.toml +++ b/bitcoin/Cargo.toml @@ -16,7 +16,7 @@ exclude = ["tests", "contrib"] default = [ "std", "secp-recovery" ] rand-std = ["secp256k1/rand-std"] rand = ["secp256k1/rand"] -serde = ["actual-serde", "hashes/serde", "secp256k1/serde"] +serde = ["actual-serde", "hashes/serde", "secp256k1/serde", "internals/serde"] secp-lowmemory = ["secp256k1/lowmemory"] secp-recovery = ["secp256k1/recovery"] bitcoinconsensus-std = ["bitcoinconsensus/std", "std"] diff --git a/internals/Cargo.toml b/internals/Cargo.toml index 2f79a2f1..99a3b049 100644 --- a/internals/Cargo.toml +++ b/internals/Cargo.toml @@ -22,5 +22,6 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [dependencies] +serde = { version = "1.0.103", default-features = false, optional = true } [dev-dependencies] diff --git a/internals/build.rs b/internals/build.rs index 6f366409..ab1f0411 100644 --- a/internals/build.rs +++ b/internals/build.rs @@ -26,7 +26,8 @@ fn main() { // print cfg for all interesting versions less than or equal to minor // 46 adds `track_caller` - for version in &[46] { + // 55 adds `kind()` to `ParseIntError` + for version in &[46, 55] { if *version <= minor { println!("cargo:rustc-cfg=rust_v_1_{}", version); } diff --git a/internals/src/error.rs b/internals/src/error.rs index d6049fbd..31580fc7 100644 --- a/internals/src/error.rs +++ b/internals/src/error.rs @@ -5,6 +5,11 @@ //! Error handling macros and helpers. //! +pub mod input_string; +mod parse_error; + +pub use input_string::InputString; + /// Formats error. /// /// If `std` feature is OFF appends error source (delimited by `: `). We do this because diff --git a/internals/src/error/input_string.rs b/internals/src/error/input_string.rs new file mode 100644 index 00000000..b69751c0 --- /dev/null +++ b/internals/src/error/input_string.rs @@ -0,0 +1,126 @@ +//! Implements the [`InputString`] type storing the parsed input. + +use core::fmt; + +use storage::Storage; + +/// Conditionally stores the input string in parse errors. +/// +/// This type stores the input string of a parse function depending on whether `alloc` feature is +/// enabled. When it is enabled, the string is stored inside as `String`. When disabled this is a +/// zero-sized type and attempt to store a string does nothing. +/// +/// This provides two methods to format the error strings depending on the context. +#[derive(Debug, Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] +pub struct InputString(Storage); + +impl InputString { + /// Displays a message saying `failed to parse as `. + /// + /// This is normally used whith the `write_err!` macro. + pub fn display_cannot_parse<'a, T>(&'a self, what: &'a T) -> CannotParse<'a, T> + where + T: fmt::Display + ?Sized, + { + CannotParse { input: self, what } + } + + /// Formats a message saying ` is not a known `. + /// + /// This is normally used in leaf parse errors (with no source) when parsing an enum. + pub fn unknown_variant(&self, what: &T, f: &mut fmt::Formatter) -> fmt::Result + where + T: fmt::Display + ?Sized, + { + storage::unknown_variant(&self.0, what, f) + } +} + +macro_rules! impl_from { + ($($type:ty),+ $(,)?) => { + $( + impl From<$type> for InputString { + fn from(input: $type) -> Self { + #[allow(clippy::useless_conversion)] + InputString(input.into()) + } + } + )+ + } +} + +impl_from!(&str); + +/// Displays message saying `failed to parse as `. +/// +/// This is created by `display_cannot_parse` method and should be used as +/// `write_err!("{}", self.input.display_cannot_parse("what is parsed"); self.source)` in parse +/// error [`Display`](fmt::Display) imlementation if the error has source. If the error doesn't +/// have a source just use regular `write!` with same formatting arguments. +pub struct CannotParse<'a, T: fmt::Display + ?Sized> { + input: &'a InputString, + what: &'a T, +} + +impl<'a, T: fmt::Display + ?Sized> fmt::Display for CannotParse<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + storage::cannot_parse(&self.input.0, &self.what, f) + } +} + +#[cfg(not(feature = "alloc"))] +mod storage { + use core::fmt; + + #[derive(Clone, Eq, PartialEq, Hash, Ord, PartialOrd)] + pub(super) struct Storage; + + impl fmt::Debug for Storage { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("") + } + } + + impl From<&str> for Storage { + fn from(_value: &str) -> Self { Storage } + } + + pub(super) fn cannot_parse(_: &Storage, what: &W, f: &mut fmt::Formatter) -> fmt::Result + where + W: fmt::Display + ?Sized, + { + write!(f, "failed to parse {}", what) + } + + pub(super) fn unknown_variant(_: &Storage, what: &W, f: &mut fmt::Formatter) -> fmt::Result + where + W: fmt::Display + ?Sized, + { + write!(f, "unknown {}", what) + } +} + +#[cfg(feature = "alloc")] +mod storage { + use core::fmt; + + use super::InputString; + + pub(super) type Storage = alloc::string::String; + + pub(super) fn cannot_parse(input: &Storage, what: &W, f: &mut fmt::Formatter) -> fmt::Result + where + W: fmt::Display + ?Sized, + { + write!(f, "failed to parse '{}' as {}", input, what) + } + + pub(super) fn unknown_variant(inp: &Storage, what: &W, f: &mut fmt::Formatter) -> fmt::Result + where + W: fmt::Display + ?Sized, + { + write!(f, "'{}' is not a known {}", inp, what) + } + + impl_from!(alloc::string::String, alloc::boxed::Box, alloc::borrow::Cow<'_, str>); +} diff --git a/internals/src/error/parse_error.rs b/internals/src/error/parse_error.rs new file mode 100644 index 00000000..15552fbd --- /dev/null +++ b/internals/src/error/parse_error.rs @@ -0,0 +1,45 @@ +//! Contains helpers for parsing-related errors. + +/// Creates an error type intended for string parsing errors. +/// +/// The resulting error type has two fields: `input` and `source`. The type of `input` is +/// [`InputString`](super::InputString), the type of `source` is specified as the second argument +/// to the macro. +/// +/// The resulting type is public, conditionally implements [`std::error::Error`] and has a private +/// `new()` method for convenience. +/// +/// ## Parameters +/// +/// * `name` - the name of the error type +/// * `source` - the type of the source type +/// * `subject` - English description of the type being parsed (e.g. "a bitcoin amount") +/// * `derive` - list of derives to add +#[macro_export] +macro_rules! parse_error_type { + ($vis:vis $name:ident, $source:ty, $subject:expr $(, $derive:path)* $(,)?) => { + #[derive(Debug $(, $derive)*)] + $vis struct $name { + input: $crate::error::InputString, + source: $source, + } + + impl $name { + /// Creates `Self`. + fn new>(input: T, source: $source) -> Self { + $name { + input: input.into(), + source, + } + } + } + + impl core::fmt::Display for $name { + fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { + $crate::error::write_err!("{}", self.input.display_cannot_parse($subject); self.source) + } + } + + $crate::error::impl_std_error!($name, source); + } +} diff --git a/internals/src/lib.rs b/internals/src/lib.rs index 9e794aed..f85e0b8f 100644 --- a/internals/src/lib.rs +++ b/internals/src/lib.rs @@ -21,6 +21,8 @@ extern crate std; pub mod error; pub mod hex; pub mod macros; +mod parse; +pub mod serde; /// Mainly reexports based on features. pub(crate) mod prelude { diff --git a/internals/src/parse.rs b/internals/src/parse.rs new file mode 100644 index 00000000..6b2d20ae --- /dev/null +++ b/internals/src/parse.rs @@ -0,0 +1,109 @@ +/// Support for parsing strings. + +// Impls a single TryFrom conversion +#[doc(hidden)] +#[macro_export] +macro_rules! impl_try_from_stringly { + ($from:ty, $to:ty, $error:ty, $func:expr $(, $attr:meta)?) => { + $(#[$attr])? + impl core::convert::TryFrom<$from> for $to { + type Error = $error; + + fn try_from(s: $from) -> Result { + $func(AsRef::::as_ref(s)).map_err(|source| <$error>::new(s, source)) + } + } + + } +} + +/// Implements conversions from various string types. +/// +/// This macro implements `FromStr` as well as `TryFrom<{stringly}` where `{stringly}` is one of +/// these types: +/// +/// * `&str` +/// * `String` +/// * `Box` +/// * `Cow<'_, str>` +/// +/// The last three are only available with `alloc` feature turned on. +#[macro_export] +macro_rules! impl_parse { + ($type:ty, $descr:expr, $func:expr, $vis:vis $error:ident, $error_source:ty $(, $error_derive:path)*) => { + $crate::parse_error_type!($vis $error, $error_source, $descr $(, $error_derive)*); + + impl core::str::FromStr for $type { + type Err = $error; + + fn from_str(s: &str) -> Result { + $func(s).map_err(|source| <$error>::new(s, source)) + } + } + + impl_try_from_stringly!(&str); + + #[cfg(feature = "alloc")] + impl_try_from_stringly!(alloc::string::String, $type, $error, $func, cfg_attr(docsrs, doc(feature = "alloc"))); + #[cfg(feature = "alloc")] + impl_try_from_stringly!(alloc::borrow::Cow<'_, str>, $type, $error, $func, cfg_attr(docsrs, doc(feature = "alloc"))); + #[cfg(feature = "alloc")] + impl_try_from_stringly!(alloc::boxed::Box, $type, $error, $func, cfg_attr(docsrs, doc(feature = "alloc"))); + } +} + +/// Implements conversions from various string types as well as `serde` (de)serialization. +/// +/// This calls `impl_parse` macro and implements serde deserialization by expecting and parsing a +/// string and serialization by outputting a string. +#[macro_export] +macro_rules! impl_parse_and_serde { + ($type:ty, $descr:expr, $func:expr, $error:ident, $error_source:ty $(, $error_derive:path)*) => { + impl_parse!($type, $descr, $func, $error, $error_source $(, $error_derive)*); + + // We don't use `serde_string_impl` because we want to avoid allocating input. + #[cfg(feature = "serde")] + #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] + impl<'de> $crate::serde::Deserialize<'de> for $type { + fn deserialize(deserializer: D) -> Result<$name, D::Error> + where + D: $crate::serde::de::Deserializer<'de>, + { + use core::fmt::{self, Formatter}; + use core::str::FromStr; + + struct Visitor; + impl<'de> $crate::serde::de::Visitor<'de> for Visitor { + type Value = $name; + + fn expecting(&self, f: &mut Formatter) -> fmt::Result { + f.write_str($descr) + } + + fn visit_str(self, s: &str) -> Result + where + E: $crate::serde::de::Error, + { + s.parse().map_err(|error| { + $crate::serde::IntoDeError::try_into_de_error(error) + .unwrap_or_else(|_| E::invalid_value(Unexpected::Str(s), &self)) + }) + } + } + + deserializer.deserialize_str(Visitor) + } + } + + #[cfg(feature = "serde")] + #[cfg_attr(docsrs, doc(cfg(feature = "serde")))] + impl $crate::serde::Serialize for $name { + fn serialize(&self, serializer: S) -> Result + where + S: $crate::serde::Serializer, + { + serializer.collect_str(&self) + } + } + } +} diff --git a/internals/src/serde.rs b/internals/src/serde.rs new file mode 100644 index 00000000..14c5a49c --- /dev/null +++ b/internals/src/serde.rs @@ -0,0 +1,73 @@ +//! Contains extensions of `serde` and internal reexports. + +#[cfg(feature = "serde")] +#[doc(hidden)] +pub use serde::{de, ser, Deserialize, Deserializer, Serialize, Serializer}; + +/// Converts given error type to a type implementing [`de::Error`]. +/// +/// This is used in [`Deserialize`] implementations to convert specialized errors into serde +/// errors. +#[cfg(feature = "serde")] +pub trait IntoDeError: Sized { + /// Converts to deserializer error possibly outputting vague message. + /// + /// This method is allowed to return a vague error message if the error type doesn't contain + /// enough information to explain the error precisely. + fn into_de_error(self, expected: Option<&dyn de::Expected>) -> E; + + /// Converts to deserializer error without outputting vague message. + /// + /// If the error type doesn't contain enough information to explain the error precisely this + /// should return `Err(self)` allowing the caller to use its information instead. + fn try_into_de_error(self, expected: Option<&dyn de::Expected>) -> Result + where + E: de::Error, + { + Ok(self.into_de_error(expected)) + } +} + +#[cfg(feature = "serde")] +mod impls { + use super::*; + + impl IntoDeError for core::convert::Infallible { + fn into_de_error(self, _expected: Option<&dyn de::Expected>) -> E { + match self {} + } + } + + impl IntoDeError for core::num::ParseIntError { + fn into_de_error(self, expected: Option<&dyn de::Expected>) -> E { + self.try_into_de_error(expected).unwrap_or_else(|_| { + let expected = expected.unwrap_or(&"an integer"); + + E::custom(format_args!("invalid string, expected {}", expected)) + }) + } + + #[cfg(rust_v_1_55)] + fn try_into_de_error(self, expected: Option<&dyn de::Expected>) -> Result + where + E: de::Error, + { + use core::num::IntErrorKind::Empty; + + let expected = expected.unwrap_or(&"an integer"); + + match self.kind() { + Empty => Ok(E::invalid_value(de::Unexpected::Str(""), expected)), + _ => Err(self), + } + } + + #[cfg(not(rust_v_1_55))] + fn try_into_de_error(self, _expected: Option<&dyn de::Expected>) -> Result + where + E: de::Error, + { + Err(self) + } + } +}