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

Parse int error #1129

Merged
merged 2 commits into from Jul 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
78 changes: 22 additions & 56 deletions src/blockdata/locktime.rs
Expand Up @@ -21,12 +21,14 @@ use core::{mem, fmt};
use core::cmp::{PartialOrd, Ordering};
use core::convert::TryFrom;
use core::str::FromStr;
use core::num::ParseIntError;
use crate::error::ParseIntError;
use crate::parse;

use crate::consensus::encode::{self, Decodable, Encodable};
use crate::io::{self, Read, Write};
use crate::prelude::*;
use crate::internal_macros::write_err;
use crate::impl_parse_str_through_int;

/// The Threshold for deciding whether a lock time value is a height or a time (see [Bitcoin Core]).
///
Expand Down Expand Up @@ -134,29 +136,7 @@ impl From<PackedLockTime> for u32 {
}
}

impl FromStr for PackedLockTime {
type Err = ParseIntError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse().map(PackedLockTime)
}
}

impl TryFrom<&str> for PackedLockTime {
type Error = ParseIntError;

fn try_from(s: &str) -> Result<Self, Self::Error> {
PackedLockTime::from_str(s)
}
}

impl TryFrom<String> for PackedLockTime {
type Error = ParseIntError;

fn try_from(s: String) -> Result<Self, Self::Error> {
PackedLockTime::from_str(&s)
}
}
impl_parse_str_through_int!(PackedLockTime);

impl fmt::LowerHex for PackedLockTime {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Expand Down Expand Up @@ -366,29 +346,7 @@ impl LockTime {
}
}

impl FromStr for LockTime {
type Err = ParseIntError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
s.parse().map(LockTime::from_consensus)
}
}

impl TryFrom<&str> for LockTime {
type Error = ParseIntError;

fn try_from(s: &str) -> Result<Self, Self::Error> {
LockTime::from_str(s)
}
}

impl TryFrom<String> for LockTime {
type Error = ParseIntError;

fn try_from(s: String) -> Result<Self, Self::Error> {
LockTime::from_str(&s)
}
}
impl_parse_str_through_int!(LockTime, from_consensus);

impl From<Height> for LockTime {
fn from(h: Height) -> Self {
Expand Down Expand Up @@ -503,7 +461,7 @@ impl FromStr for Height {
type Err = Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let n = s.parse::<u32>().map_err(|e| Error::Parse(e, s.to_owned()))?;
let n = parse::int(s)?;
Height::from_consensus(n)
}
}
Expand All @@ -512,15 +470,16 @@ impl TryFrom<&str> for Height {
type Error = Error;

fn try_from(s: &str) -> Result<Self, Self::Error> {
Height::from_str(s)
let n = parse::int(s)?;
Height::from_consensus(n)
}
}

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

fn try_from(s: String) -> Result<Self, Self::Error> {
let n = s.parse::<u32>().map_err(|e| Error::Parse(e, s))?;
let n = parse::int(s)?;
Height::from_consensus(n)
}
}
Expand Down Expand Up @@ -585,7 +544,7 @@ impl FromStr for Time {
type Err = Error;

fn from_str(s: &str) -> Result<Self, Self::Err> {
let n = s.parse::<u32>().map_err(|e| Error::Parse(e, s.to_owned()))?;
let n = parse::int(s)?;
Time::from_consensus(n)
}
}
Expand All @@ -594,15 +553,16 @@ impl TryFrom<&str> for Time {
type Error = Error;

fn try_from(s: &str) -> Result<Self, Self::Error> {
Time::from_str(s)
let n = parse::int(s)?;
Time::from_consensus(n)
}
}

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

fn try_from(s: String) -> Result<Self, Self::Error> {
let n = s.parse::<u32>().map_err(|e| Error::Parse(e, s))?;
let n = parse::int(s)?;
Time::from_consensus(n)
}
}
Expand All @@ -626,7 +586,7 @@ pub enum Error {
/// An error occurred while operating on lock times.
Operation(OperationError),
/// An error occurred while parsing a string into an `u32`.
Parse(ParseIntError, String),
Parse(ParseIntError),
}

impl fmt::Display for Error {
Expand All @@ -636,7 +596,7 @@ impl fmt::Display for Error {
match *self {
Conversion(ref e) => write_err!(f, "error converting lock time value"; e),
Operation(ref e) => write_err!(f, "error during lock time operation"; e),
Parse(ref e, ref s) => write_err!(f, "error parsing string: {}", s; e),
Parse(ref e) => write_err!(f, "failed to parse lock time from string"; e),
}
}
}
Expand All @@ -650,7 +610,7 @@ impl std::error::Error for Error {
match *self {
Conversion(ref e) => Some(e),
Operation(ref e) => Some(e),
Parse(ref e, _) => Some(e),
Parse(ref e) => Some(e),
}
}
}
Expand All @@ -667,6 +627,12 @@ impl From<OperationError> for Error {
}
}

impl From<ParseIntError> for Error {
fn from(e: ParseIntError) -> Self {
Error::Parse(e)
}
}

/// An error that occurs when converting a `u32` to a lock time variant.
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub struct ConversionError {
Expand Down
9 changes: 6 additions & 3 deletions src/blockdata/transaction.rs
Expand Up @@ -32,6 +32,7 @@ use crate::hash_types::{Sighash, Txid, Wtxid};
use crate::VarInt;
use crate::util::sighash::UINT256_ONE;
use crate::internal_macros::{impl_consensus_encoding, serde_string_impl, serde_struct_human_string_impl, write_err};
use crate::impl_parse_str_through_int;

#[cfg(doc)]
use crate::util::sighash::SchnorrSighashType;
Expand Down Expand Up @@ -107,7 +108,7 @@ pub enum ParseOutPointError {
/// Error in TXID part.
Txid(hashes::hex::Error),
/// Error in vout part.
Vout(::core::num::ParseIntError),
Vout(crate::error::ParseIntError),
/// Error in general format.
Format,
/// Size exceeds max.
Expand Down Expand Up @@ -151,7 +152,7 @@ fn parse_vout(s: &str) -> Result<u32, ParseOutPointError> {
return Err(ParseOutPointError::VoutNotCanonical);
}
}
s.parse().map_err(ParseOutPointError::Vout)
crate::parse::int(s).map_err(ParseOutPointError::Vout)
}

impl core::str::FromStr for OutPoint {
Expand Down Expand Up @@ -422,6 +423,8 @@ impl fmt::Display for RelativeLockTimeError {
}
}

impl_parse_str_through_int!(Sequence);

#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl std::error::Error for RelativeLockTimeError {
Expand Down Expand Up @@ -1308,7 +1311,7 @@ mod tests {
assert_eq!(OutPoint::from_str("5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c945X:1"),
Err(ParseOutPointError::Txid(Txid::from_hex("5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c945X").unwrap_err())));
assert_eq!(OutPoint::from_str("5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456:lol"),
Err(ParseOutPointError::Vout(u32::from_str("lol").unwrap_err())));
Err(ParseOutPointError::Vout(crate::parse::int::<u32, _>("lol").unwrap_err())));

assert_eq!(OutPoint::from_str("5df6e0e2761359d30a8275058e299fcc0381534545f55cf43e41983f5d4c9456:42"),
Ok(OutPoint{
Expand Down
25 changes: 25 additions & 0 deletions src/error.rs
@@ -0,0 +1,25 @@
//! Contains error types and other error handling tools.

pub use crate::parse::ParseIntError;

/// Impls std::error::Error for the specified type with appropriate attributes, possibly returning
/// source.
#[macro_export]
Copy link
Member

@tcharding tcharding Jul 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: We do not need to export this. We can make it pub crate only by using the trick that we use in internal_macros.rs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, this is the way! Damn! I was confused why the others work and this one not. Macro visibility is confusing me.

macro_rules! impl_std_error {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are a bunch of other struct errors that we can use this for too

  • ConversionError
  • SighashTypeParseError
  • CommandStringError
  • ParseLengthError

I did all of these, I'll pushed a branch review-parse-int-error on my fork in case you want to look at it like that.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I was planning to do these in a followup PR.

// No source available
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// No source available
// Field is not an error source.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a nit. My aim was to make the macro more approachable for devs new to our error code boiler plate code and/or macros in general.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There could also be no field. :) Maybe "No field is an error source"?

($type:ty) => {
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl std::error::Error for $type {}
};
// Struct with $field as source
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// Struct with $field as source
// $field is an error source (implements `std::error::Error`)

($type:ty, $field:ident) => {
#[cfg(feature = "std")]
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
impl std::error::Error for $type {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&self.$field)
}
}
};
}
2 changes: 1 addition & 1 deletion src/internal_macros.rs
Expand Up @@ -581,7 +581,7 @@ pub(crate) use user_enum;
/// because `e.source()` is only available in std builds, without this macro the error source is
/// lost for no-std builds.
macro_rules! write_err {
($writer:expr, $string:literal $(, $args:expr),*; $source:expr) => {
($writer:expr, $string:literal $(, $args:expr)*; $source:expr) => {
{
#[cfg(feature = "std")]
{
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Expand Up @@ -78,11 +78,13 @@ mod test_macros;
mod internal_macros;
#[cfg(feature = "serde")]
mod serde_utils;
mod parse;

#[macro_use]
pub mod network;
pub mod blockdata;
pub mod consensus;
pub mod error;
pub mod hash_types;
pub mod policy;
pub mod util;
Expand Down
123 changes: 123 additions & 0 deletions src/parse.rs
@@ -0,0 +1,123 @@
use crate::internal_macros::write_err;
use crate::impl_std_error;
use core::fmt;
use core::str::FromStr;
use core::convert::TryFrom;
use crate::prelude::*;

/// Error with rich context returned when a string can't be parsed as an integer.
///
/// This is an extension of [`core::num::ParseIntError`], which carries the input that failed to
/// parse as well as type information. As a result it provides very informative error messages that
/// make it easier to understand the problem and correct mistakes.
///
/// Note that this is larger than the type from `core` so if it's passed through a deep call stack
/// in a performance-critical application you may want to box it or throw away the context by
/// converting to `core` type.
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct ParseIntError {
input: String,
// for displaying - see Display impl with nice error message below
bits: u8,
// We could represent this as a single bit but it wouldn't actually derease the cost of moving
// the struct because String contains pointers so there will be padding of bits at least
// pointer_size - 1 bytes: min 1B in practice.
is_signed: bool,
source: core::num::ParseIntError,
}

impl ParseIntError {
/// Returns the input that was attempted to be parsed.
pub fn input(&self) -> &str {
&self.input
}
}

impl From<ParseIntError> for core::num::ParseIntError {
fn from(value: ParseIntError) -> Self {
value.source
}
}

impl AsRef<core::num::ParseIntError> for ParseIntError {
fn as_ref(&self) -> &core::num::ParseIntError {
&self.source
}
}

impl fmt::Display for ParseIntError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let signed = if self.is_signed { "signed" } else { "unsigned" };
let n = if self.bits == 8 { "n" } else { "" };
write_err!(f, "failed to parse '{}' as a{} {}-bit {} integer", self.input, n, self.bits, signed; self.source)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These three lines are hot ;)

}
}

/// Not strictly neccessary but serves as a lint - avoids weird behavior if someone accidentally
/// passes non-integer to the `parse()` function.
pub(crate) trait Integer: FromStr<Err=core::num::ParseIntError> + TryFrom<i8> + Sized {}

macro_rules! impl_integer {
($($type:ty),* $(,)?) => {
$(
impl Integer for $type {}
)*
}
}

impl_integer!(u8, i8, u16, i16, u32, i32, u64, i64, u128, i128);

/// Parses the input string as an integer returning an error carrying rich context.
///
/// If the caller owns `String` or `Box<str>` which is not used later it's better to pass it as
/// owned since it avoids allocation in error case.
pub(crate) fn int<T: Integer, S: AsRef<str> + Into<String>>(s: S) -> Result<T, ParseIntError> {
s.as_ref().parse().map_err(|error| {
ParseIntError {
input: s.into(),
bits: u8::try_from(core::mem::size_of::<T>() * 8).expect("max is 128 bits for u128"),
// We detect if the type is signed by checking if -1 can be represented by it
// this way we don't have to implement special traits and optimizer will get rid of the
// computation.
is_signed: T::try_from(-1i8).is_ok(),
source: error,
}
})
}

impl_std_error!(ParseIntError, source);

/// Implements `TryFrom<$from> for $to` using `parse::int`, mapping the output using `fn`
#[macro_export]
macro_rules! impl_tryfrom_str_through_int_single {
($($from:ty, $to:ident $(, $fn:ident)?);*) => {
$(
impl core::convert::TryFrom<$from> for $to {
type Error = $crate::error::ParseIntError;

fn try_from(s: $from) -> Result<Self, Self::Error> {
$crate::parse::int(s).map($to $(:: $fn)?)
}
}
)*
}
}

/// Implements `FromStr` and `TryFrom<{&str, String, Box<str>}> for $to` using `parse::int`, mapping the output using `fn`
///
/// The `Error` type is `ParseIntError`
#[macro_export]
macro_rules! impl_parse_str_through_int {
($to:ident $(, $fn:ident)?) => {
$crate::impl_tryfrom_str_through_int_single!(&str, $to $(, $fn)?; String, $to $(, $fn)?; Box<str>, $to $(, $fn)?);

impl core::str::FromStr for $to {
type Err = $crate::error::ParseIntError;

fn from_str(s: &str) -> Result<Self, Self::Err> {
$crate::parse::int(s).map($to $(:: $fn)?)
}
}

}
}