Skip to content

Commit 4fd8155

Browse files
committed
Extend ParseIntError to carry more context
When debugging parsing errors it's very useful to know some context: what the input was and what integer type was parsed. `ParseIntError` from `core` doesn't contain this information. In this commit a custom `ParseIntError` type is crated that carries the one from `core` as well as additional information. Its `Display` implementation displays this additional information as a well-formed English sentence to aid users with understanding the problem. A helper function parses any integer type from common string types returning the new `ParseIntError` type on error. To clean up the error code a bit some new macros are added and used. New modules are added to organize the types, functions and macros. Closes #1113
1 parent cf56672 commit 4fd8155

File tree

7 files changed

+164
-53
lines changed

7 files changed

+164
-53
lines changed

src/blockdata/locktime.rs

Lines changed: 43 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ use core::{mem, fmt};
2121
use core::cmp::{PartialOrd, Ordering};
2222
use core::convert::TryFrom;
2323
use core::str::FromStr;
24-
use core::num::ParseIntError;
24+
use crate::error::ParseIntError;
25+
use crate::parse;
2526

2627
use crate::consensus::encode::{self, Decodable, Encodable};
2728
use crate::io::{self, Read, Write};
@@ -134,30 +135,39 @@ impl From<PackedLockTime> for u32 {
134135
}
135136
}
136137

137-
impl FromStr for PackedLockTime {
138-
type Err = ParseIntError;
138+
/// Implements `TryFrom<$from> for $to` using `parse::int`, mapping the output using `fn`
139+
macro_rules! impl_tryfrom_str_single {
140+
($($from:ty, $to:ident $(, $fn:ident)?);*) => {
141+
$(
142+
impl TryFrom<$from> for $to {
143+
type Error = ParseIntError;
139144

140-
fn from_str(s: &str) -> Result<Self, Self::Err> {
141-
s.parse().map(PackedLockTime)
145+
fn try_from(s: $from) -> Result<Self, Self::Error> {
146+
parse::int(s).map($to $(:: $fn)?)
147+
}
148+
}
149+
)*
142150
}
143151
}
144152

145-
impl TryFrom<&str> for PackedLockTime {
146-
type Error = ParseIntError;
153+
/// Implements `TryFrom<{&str, String, Box<str>}> for $to` using `parse::int`, mapping the output using `fn`
154+
macro_rules! impl_tryfrom_str {
155+
($to:ident $(, $fn:ident)?) => {
156+
impl_tryfrom_str_single!(&str, $to $(, $fn)?; String, $to $(, $fn)?; Box<str>, $to $(, $fn)?);
147157

148-
fn try_from(s: &str) -> Result<Self, Self::Error> {
149-
PackedLockTime::from_str(s)
150-
}
151-
}
158+
impl FromStr for $to {
159+
type Err = ParseIntError;
152160

153-
impl TryFrom<String> for PackedLockTime {
154-
type Error = ParseIntError;
161+
fn from_str(s: &str) -> Result<Self, Self::Err> {
162+
parse::int(s).map($to $(:: $fn)?)
163+
}
164+
}
155165

156-
fn try_from(s: String) -> Result<Self, Self::Error> {
157-
PackedLockTime::from_str(&s)
158166
}
159167
}
160168

169+
impl_tryfrom_str!(PackedLockTime);
170+
161171
impl fmt::LowerHex for PackedLockTime {
162172
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
163173
write!(f, "{:x}", self.0)
@@ -366,29 +376,7 @@ impl LockTime {
366376
}
367377
}
368378

369-
impl FromStr for LockTime {
370-
type Err = ParseIntError;
371-
372-
fn from_str(s: &str) -> Result<Self, Self::Err> {
373-
s.parse().map(LockTime::from_consensus)
374-
}
375-
}
376-
377-
impl TryFrom<&str> for LockTime {
378-
type Error = ParseIntError;
379-
380-
fn try_from(s: &str) -> Result<Self, Self::Error> {
381-
LockTime::from_str(s)
382-
}
383-
}
384-
385-
impl TryFrom<String> for LockTime {
386-
type Error = ParseIntError;
387-
388-
fn try_from(s: String) -> Result<Self, Self::Error> {
389-
LockTime::from_str(&s)
390-
}
391-
}
379+
impl_tryfrom_str!(LockTime, from_consensus);
392380

393381
impl From<Height> for LockTime {
394382
fn from(h: Height) -> Self {
@@ -503,7 +491,7 @@ impl FromStr for Height {
503491
type Err = Error;
504492

505493
fn from_str(s: &str) -> Result<Self, Self::Err> {
506-
let n = s.parse::<u32>().map_err(|e| Error::Parse(e, s.to_owned()))?;
494+
let n = parse::int(s)?;
507495
Height::from_consensus(n)
508496
}
509497
}
@@ -512,15 +500,16 @@ impl TryFrom<&str> for Height {
512500
type Error = Error;
513501

514502
fn try_from(s: &str) -> Result<Self, Self::Error> {
515-
Height::from_str(s)
503+
let n = parse::int(s)?;
504+
Height::from_consensus(n)
516505
}
517506
}
518507

519508
impl TryFrom<String> for Height {
520509
type Error = Error;
521510

522511
fn try_from(s: String) -> Result<Self, Self::Error> {
523-
let n = s.parse::<u32>().map_err(|e| Error::Parse(e, s))?;
512+
let n = parse::int(s)?;
524513
Height::from_consensus(n)
525514
}
526515
}
@@ -585,7 +574,7 @@ impl FromStr for Time {
585574
type Err = Error;
586575

587576
fn from_str(s: &str) -> Result<Self, Self::Err> {
588-
let n = s.parse::<u32>().map_err(|e| Error::Parse(e, s.to_owned()))?;
577+
let n = parse::int(s)?;
589578
Time::from_consensus(n)
590579
}
591580
}
@@ -594,15 +583,16 @@ impl TryFrom<&str> for Time {
594583
type Error = Error;
595584

596585
fn try_from(s: &str) -> Result<Self, Self::Error> {
597-
Time::from_str(s)
586+
let n = parse::int(s)?;
587+
Time::from_consensus(n)
598588
}
599589
}
600590

601591
impl TryFrom<String> for Time {
602592
type Error = Error;
603593

604594
fn try_from(s: String) -> Result<Self, Self::Error> {
605-
let n = s.parse::<u32>().map_err(|e| Error::Parse(e, s))?;
595+
let n = parse::int(s)?;
606596
Time::from_consensus(n)
607597
}
608598
}
@@ -626,7 +616,7 @@ pub enum Error {
626616
/// An error occurred while operating on lock times.
627617
Operation(OperationError),
628618
/// An error occurred while parsing a string into an `u32`.
629-
Parse(ParseIntError, String),
619+
Parse(ParseIntError),
630620
}
631621

632622
impl fmt::Display for Error {
@@ -636,7 +626,7 @@ impl fmt::Display for Error {
636626
match *self {
637627
Conversion(ref e) => write_err!(f, "error converting lock time value"; e),
638628
Operation(ref e) => write_err!(f, "error during lock time operation"; e),
639-
Parse(ref e, ref s) => write_err!(f, "error parsing string: {}", s; e),
629+
Parse(ref e) => write_err!(f, "failed to parse lock time from string"; e),
640630
}
641631
}
642632
}
@@ -650,7 +640,7 @@ impl std::error::Error for Error {
650640
match *self {
651641
Conversion(ref e) => Some(e),
652642
Operation(ref e) => Some(e),
653-
Parse(ref e, _) => Some(e),
643+
Parse(ref e) => Some(e),
654644
}
655645
}
656646
}
@@ -667,6 +657,12 @@ impl From<OperationError> for Error {
667657
}
668658
}
669659

660+
impl From<ParseIntError> for Error {
661+
fn from(e: ParseIntError) -> Self {
662+
Error::Parse(e)
663+
}
664+
}
665+
670666
/// An error that occurs when converting a `u32` to a lock time variant.
671667
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
672668
pub struct ConversionError {

src/blockdata/transaction.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ pub enum ParseOutPointError {
107107
/// Error in TXID part.
108108
Txid(hashes::hex::Error),
109109
/// Error in vout part.
110-
Vout(::core::num::ParseIntError),
110+
Vout(crate::error::ParseIntError),
111111
/// Error in general format.
112112
Format,
113113
/// Size exceeds max.
@@ -151,7 +151,7 @@ fn parse_vout(s: &str) -> Result<u32, ParseOutPointError> {
151151
return Err(ParseOutPointError::VoutNotCanonical);
152152
}
153153
}
154-
s.parse().map_err(ParseOutPointError::Vout)
154+
crate::parse::int(s).map_err(ParseOutPointError::Vout)
155155
}
156156

157157
impl core::str::FromStr for OutPoint {
@@ -1308,7 +1308,7 @@ mod tests {
13081308
assert_eq!(OutPoint::from_str("5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c945X:1"),
13091309
Err(ParseOutPointError::Txid(Txid::from_hex("5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c945X").unwrap_err())));
13101310
assert_eq!(OutPoint::from_str("5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456:lol"),
1311-
Err(ParseOutPointError::Vout(u32::from_str("lol").unwrap_err())));
1311+
Err(ParseOutPointError::Vout(crate::parse::int::<u32, _>("lol").unwrap_err())));
13121312

13131313
assert_eq!(OutPoint::from_str("5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456:42"),
13141314
Ok(OutPoint{

src/error.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//! Contains error types and other error handling tools.
2+
3+
pub use crate::parse::ParseIntError;
4+
5+
/// Impls std::error::Error for the specified type with appropriate attributes, possibly returning
6+
/// source.
7+
#[macro_export]
8+
macro_rules! impl_std_error {
9+
// No source available
10+
($type:ty) => {
11+
#[cfg(feature = "std")]
12+
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
13+
impl std::error::Error for $type {}
14+
};
15+
// Struct with $field as source
16+
($type:ty, $field:ident) => {
17+
#[cfg(feature = "std")]
18+
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
19+
impl std::error::Error for $type {
20+
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
21+
Some(&self.$field)
22+
}
23+
}
24+
};
25+
}

src/internal_macros.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -581,7 +581,7 @@ pub(crate) use user_enum;
581581
/// because `e.source()` is only available in std builds, without this macro the error source is
582582
/// lost for no-std builds.
583583
macro_rules! write_err {
584-
($writer:expr, $string:literal $(, $args:expr),*; $source:expr) => {
584+
($writer:expr, $string:literal $(, $args:expr)*; $source:expr) => {
585585
{
586586
#[cfg(feature = "std")]
587587
{

src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,11 +78,13 @@ mod test_macros;
7878
mod internal_macros;
7979
#[cfg(feature = "serde")]
8080
mod serde_utils;
81+
mod parse;
8182

8283
#[macro_use]
8384
pub mod network;
8485
pub mod blockdata;
8586
pub mod consensus;
87+
pub mod error;
8688
pub mod hash_types;
8789
pub mod policy;
8890
pub mod util;

src/parse.rs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
use crate::internal_macros::write_err;
2+
use crate::impl_std_error;
3+
use core::fmt;
4+
use core::str::FromStr;
5+
use core::convert::TryFrom;
6+
use crate::prelude::*;
7+
8+
/// Error with rich context returned when a string can't be parsed as an integer.
9+
///
10+
/// This is an extension of [`core::num::ParseIntError`], which carries the input that failed to
11+
/// parse as well as type information. As a result it provides very informative error messages that
12+
/// make it easier to understand the problem and correct mistakes.
13+
///
14+
/// Note that this is larger than the type from `core` so if it's passed through a deep call stack
15+
/// in a performance-critical application you may want to box it or throw away the context by
16+
/// converting to `core` type.
17+
#[derive(Debug, Clone, Eq, PartialEq)]
18+
pub struct ParseIntError {
19+
input: String,
20+
// for displaying - see Display impl with nice error message below
21+
bits: u8,
22+
// We could represent this as a single bit but it wouldn't actually derease the cost of moving
23+
// the struct because String contains pointers so there will be padding of bits at least
24+
// pointer_size - 1 bytes: min 1B in practice.
25+
is_signed: bool,
26+
source: core::num::ParseIntError,
27+
}
28+
29+
impl ParseIntError {
30+
/// Returns the input that was attempted to be parsed.
31+
pub fn input(&self) -> &str {
32+
&self.input
33+
}
34+
}
35+
36+
impl From<ParseIntError> for core::num::ParseIntError {
37+
fn from(value: ParseIntError) -> Self {
38+
value.source
39+
}
40+
}
41+
42+
impl AsRef<core::num::ParseIntError> for ParseIntError {
43+
fn as_ref(&self) -> &core::num::ParseIntError {
44+
&self.source
45+
}
46+
}
47+
48+
impl fmt::Display for ParseIntError {
49+
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
50+
let signed = if self.is_signed { "signed" } else { "unsigned" };
51+
let n = if self.bits == 8 { "n" } else { "" };
52+
write_err!(f, "failed to parse '{}' as a{} {}-bit {} integer", self.input, n, self.bits, signed; self.source)
53+
}
54+
}
55+
56+
/// Not strictly neccessary but serves as a lint - avoids weird behavior if someone accidentally
57+
/// passes non-integer to the `parse()` function.
58+
pub(crate) trait Integer: FromStr<Err=core::num::ParseIntError> + TryFrom<i8> + Sized {}
59+
60+
macro_rules! impl_integer {
61+
($($type:ty),* $(,)?) => {
62+
$(
63+
impl Integer for $type {}
64+
)*
65+
}
66+
}
67+
68+
impl_integer!(u8, i8, u16, i16, u32, i32, u64, i64, u128, i128);
69+
70+
/// Parses the input string as an integer returning an error carrying rich context.
71+
///
72+
/// If the caller owns `String` or `Box<str>` which is not used later it's better to pass it as
73+
/// owned since it avoids allocation in error case.
74+
pub(crate) fn int<T: Integer, S: AsRef<str> + Into<String>>(s: S) -> Result<T, ParseIntError> {
75+
s.as_ref().parse().map_err(|error| {
76+
ParseIntError {
77+
input: s.into(),
78+
bits: u8::try_from(core::mem::size_of::<T>() * 8).expect("max is 128 bits for u128"),
79+
// We detect if the type is signed by checking if -1 can be represented by it
80+
// this way we don't have to implement special traits and optimizer will get rid of the
81+
// computation.
82+
is_signed: T::try_from(-1i8).is_ok(),
83+
source: error,
84+
}
85+
})
86+
}
87+
88+
impl_std_error!(ParseIntError, source);

src/util/address.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use crate::prelude::*;
2626

2727
use core::convert::TryFrom;
2828
use core::fmt;
29-
use core::num::ParseIntError;
29+
use crate::error::ParseIntError;
3030
use core::str::FromStr;
3131

3232
use secp256k1::{Secp256k1, Verification, XOnlyPublicKey};
@@ -239,7 +239,7 @@ impl FromStr for WitnessVersion {
239239
type Err = Error;
240240

241241
fn from_str(s: &str) -> Result<Self, Self::Err> {
242-
let version: u8 = s.parse().map_err(Error::UnparsableWitnessVersion)?;
242+
let version: u8 = crate::parse::int(s).map_err(Error::UnparsableWitnessVersion)?;
243243
WitnessVersion::try_from(version)
244244
}
245245
}

0 commit comments

Comments
 (0)