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
This commit is contained in:
Martin Habovstiak 2022-09-20 17:11:35 +02:00
parent 783e1e81dc
commit 2b6bcf085c
11 changed files with 370 additions and 2 deletions

View File

@ -64,6 +64,9 @@ dependencies = [
[[package]] [[package]]
name = "bitcoin-internals" name = "bitcoin-internals"
version = "0.1.0" version = "0.1.0"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bitcoin_hashes" name = "bitcoin_hashes"

View File

@ -63,6 +63,9 @@ dependencies = [
[[package]] [[package]]
name = "bitcoin-internals" name = "bitcoin-internals"
version = "0.1.0" version = "0.1.0"
dependencies = [
"serde",
]
[[package]] [[package]]
name = "bitcoin_hashes" name = "bitcoin_hashes"

View File

@ -16,7 +16,7 @@ exclude = ["tests", "contrib"]
default = [ "std", "secp-recovery" ] default = [ "std", "secp-recovery" ]
rand-std = ["secp256k1/rand-std"] rand-std = ["secp256k1/rand-std"]
rand = ["secp256k1/rand"] rand = ["secp256k1/rand"]
serde = ["actual-serde", "hashes/serde", "secp256k1/serde"] serde = ["actual-serde", "hashes/serde", "secp256k1/serde", "internals/serde"]
secp-lowmemory = ["secp256k1/lowmemory"] secp-lowmemory = ["secp256k1/lowmemory"]
secp-recovery = ["secp256k1/recovery"] secp-recovery = ["secp256k1/recovery"]
bitcoinconsensus-std = ["bitcoinconsensus/std", "std"] bitcoinconsensus-std = ["bitcoinconsensus/std", "std"]

View File

@ -22,5 +22,6 @@ all-features = true
rustdoc-args = ["--cfg", "docsrs"] rustdoc-args = ["--cfg", "docsrs"]
[dependencies] [dependencies]
serde = { version = "1.0.103", default-features = false, optional = true }
[dev-dependencies] [dev-dependencies]

View File

@ -26,7 +26,8 @@ fn main() {
// print cfg for all interesting versions less than or equal to minor // print cfg for all interesting versions less than or equal to minor
// 46 adds `track_caller` // 46 adds `track_caller`
for version in &[46] { // 55 adds `kind()` to `ParseIntError`
for version in &[46, 55] {
if *version <= minor { if *version <= minor {
println!("cargo:rustc-cfg=rust_v_1_{}", version); println!("cargo:rustc-cfg=rust_v_1_{}", version);
} }

View File

@ -5,6 +5,11 @@
//! Error handling macros and helpers. //! Error handling macros and helpers.
//! //!
pub mod input_string;
mod parse_error;
pub use input_string::InputString;
/// Formats error. /// Formats error.
/// ///
/// If `std` feature is OFF appends error source (delimited by `: `). We do this because /// If `std` feature is OFF appends error source (delimited by `: `). We do this because

View File

@ -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 <self> as <what>`.
///
/// 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 `<self> is not a known <what>`.
///
/// This is normally used in leaf parse errors (with no source) when parsing an enum.
pub fn unknown_variant<T>(&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 <input> as <what>`.
///
/// 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("<unknown input string - compiled without the `alloc` feature>")
}
}
impl From<&str> for Storage {
fn from(_value: &str) -> Self { Storage }
}
pub(super) fn cannot_parse<W>(_: &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<W>(_: &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<W>(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<W>(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<str>, alloc::borrow::Cow<'_, str>);
}

View File

@ -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<T: Into<$crate::error::InputString>>(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);
}
}

View File

@ -21,6 +21,8 @@ extern crate std;
pub mod error; pub mod error;
pub mod hex; pub mod hex;
pub mod macros; pub mod macros;
mod parse;
pub mod serde;
/// Mainly reexports based on features. /// Mainly reexports based on features.
pub(crate) mod prelude { pub(crate) mod prelude {

109
internals/src/parse.rs Normal file
View File

@ -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<Self, Self::Error> {
$func(AsRef::<str>::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<str>`
/// * `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<Self, Self::Err> {
$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<str>, $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<D>(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<E>(self, s: &str) -> Result<Self::Value, E>
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: $crate::serde::Serializer,
{
serializer.collect_str(&self)
}
}
}
}

73
internals/src/serde.rs Normal file
View File

@ -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<E: 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<E>(self, expected: Option<&dyn de::Expected>) -> Result<E, Self>
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<E: de::Error>(self, _expected: Option<&dyn de::Expected>) -> E {
match self {}
}
}
impl IntoDeError for core::num::ParseIntError {
fn into_de_error<E: 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<E>(self, expected: Option<&dyn de::Expected>) -> Result<E, Self>
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<E>(self, _expected: Option<&dyn de::Expected>) -> Result<E, Self>
where
E: de::Error,
{
Err(self)
}
}
}