diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1493009..3734684 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,7 @@ jobs: - run: cargo check - run: cargo test - run: cd extras/data-tests && cargo run --release + - run: cargo build --tests --features no-panic --release msrv: name: Rust ${{matrix.rust}} diff --git a/Cargo.toml b/Cargo.toml index 0dde35b..52ec25e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,9 @@ rust-version = "1.37" default = ["std"] std = [] +[dependencies] +no-panic = { version = "0.1", optional = true } + [dev-dependencies] lexical-core = "1.0.2" hexf-parse = "0.2.1" diff --git a/README.md b/README.md index 49ea134..5a35d0c 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,11 @@ on little-endian architectures. Since [fast-float-rust](https://github.com/aldanor/fast-float-rust) is unmaintained, this is a fork containing the patches and security updates. +## Features + +`no-panic`: when this feature is enabled, the crate guarantees that it will not trigger a runtime panic. + + ## Testing There are a few ways this crate is tested: diff --git a/src/binary.rs b/src/binary.rs index 88d24ec..7e93f8c 100644 --- a/src/binary.rs +++ b/src/binary.rs @@ -1,6 +1,7 @@ use crate::common::AdjustedMantissa; use crate::float::Float; use crate::table::{LARGEST_POWER_OF_FIVE, POWER_OF_FIVE_128, SMALLEST_POWER_OF_FIVE}; +use crate::GetAt; #[inline] pub fn compute_float(q: i64, mut w: u64) -> AdjustedMantissa { @@ -94,7 +95,7 @@ fn compute_product_approx(q: i64, w: u64, precision: usize) -> (u64, u64) { // comes from a parsed result. Since this is unlikely to have any major // performance implications, as is determined empirically, we keep the // bounds check despite the performance hit. - let (lo5, hi5) = POWER_OF_FIVE_128[index]; + let (lo5, hi5) = *POWER_OF_FIVE_128.at(index); let (mut first_lo, mut first_hi) = full_multiplication(w, lo5); if first_hi & mask == mask { let (_, second_hi) = full_multiplication(w, hi5); diff --git a/src/common.rs b/src/common.rs index 77e7d82..7f1240c 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,6 +1,8 @@ use core::marker::PhantomData; use core::ptr; +use crate::GetAt; + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AsciiStr<'a> { ptr: *const u8, @@ -172,13 +174,13 @@ pub trait ByteSlice: AsRef<[u8]> + AsMut<[u8]> { if s.len() < u.len() { return false; } - let d = (0..u.len()).fold(0, |d, i| d | s[i] ^ u[i]); + let d = (0..u.len()).fold(0, |d, i| d | s.at(i) ^ u.at(i)); d == 0 || d == 32 } #[inline] fn advance(&self, n: usize) -> &[u8] { - &self.as_ref()[n..] + self.as_ref().at(n..) } #[inline] @@ -215,8 +217,7 @@ pub trait ByteSlice: AsRef<[u8]> + AsMut<[u8]> { } } -impl ByteSlice for [u8] { -} +impl ByteSlice for [u8] {} #[inline] pub fn is_8digits(v: u64) -> bool { diff --git a/src/decimal.rs b/src/decimal.rs index 74342f2..7267d1e 100644 --- a/src/decimal.rs +++ b/src/decimal.rs @@ -1,6 +1,8 @@ use core::fmt::{self, Debug}; use crate::common::{is_8digits, parse_digits, ByteSlice}; +use crate::GetAt; +use crate::GetAtMut; #[derive(Clone)] pub struct Decimal { @@ -18,7 +20,7 @@ impl Debug for Decimal { .field("decimal_point", &self.decimal_point) .field("negative", &self.negative) .field("truncated", &self.truncated) - .field("digits", &(&self.digits[..self.num_digits])) + .field("digits", &(&self.digits.at(..self.num_digits))) .finish() } } @@ -33,8 +35,7 @@ impl PartialEq for Decimal { } } -impl Eq for Decimal { -} +impl Eq for Decimal {} impl Default for Decimal { fn default() -> Self { @@ -56,14 +57,14 @@ impl Decimal { #[inline] pub fn try_add_digit(&mut self, digit: u8) { if self.num_digits < Self::MAX_DIGITS { - self.digits[self.num_digits] = digit; + *self.digits.at_mut(self.num_digits) = digit; } self.num_digits += 1; } #[inline] pub fn trim(&mut self) { - while self.num_digits != 0 && self.digits[self.num_digits - 1] == 0 { + while self.num_digits != 0 && *self.digits.at(self.num_digits - 1) == 0 { self.num_digits -= 1; } } @@ -80,14 +81,14 @@ impl Decimal { for i in 0..dp { n *= 10; if i < self.num_digits { - n += self.digits[i] as u64; + n += *self.digits.at(i) as u64; } } let mut round_up = false; if dp < self.num_digits { - round_up = self.digits[dp] >= 5; - if self.digits[dp] == 5 && dp + 1 == self.num_digits { - round_up = self.truncated || ((dp != 0) && (1 & self.digits[dp - 1] != 0)); + round_up = *self.digits.at(dp) >= 5; + if *self.digits.at(dp) == 5 && dp + 1 == self.num_digits { + round_up = self.truncated || ((dp != 0) && (1 & self.digits.at(dp - 1) != 0)); } } if round_up { @@ -108,11 +109,11 @@ impl Decimal { while read_index != 0 { read_index -= 1; write_index -= 1; - n += (self.digits[read_index] as u64) << shift; + n += (*self.digits.at(read_index) as u64) << shift; let quotient = n / 10; let remainder = n - (10 * quotient); if write_index < Self::MAX_DIGITS { - self.digits[write_index] = remainder as u8; + *self.digits.at_mut(write_index) = remainder as u8; } else if remainder > 0 { self.truncated = true; } @@ -123,7 +124,7 @@ impl Decimal { let quotient = n / 10; let remainder = n - (10 * quotient); if write_index < Self::MAX_DIGITS { - self.digits[write_index] = remainder as u8; + *self.digits.at_mut(write_index) = remainder as u8; } else if remainder > 0 { self.truncated = true; } @@ -144,7 +145,7 @@ impl Decimal { let mut n = 0_u64; while (n >> shift) == 0 { if read_index < self.num_digits { - n = (10 * n) + self.digits[read_index] as u64; + n = (10 * n) + *self.digits.at(read_index) as u64; read_index += 1; } else if n == 0 { return; @@ -167,16 +168,16 @@ impl Decimal { let mask = (1_u64 << shift) - 1; while read_index < self.num_digits { let new_digit = (n >> shift) as u8; - n = (10 * (n & mask)) + self.digits[read_index] as u64; + n = (10 * (n & mask)) + *self.digits.at(read_index) as u64; read_index += 1; - self.digits[write_index] = new_digit; + *self.digits.at_mut(write_index) = new_digit; write_index += 1; } while n > 0 { let new_digit = (n >> shift) as u8; n = 10 * (n & mask); if write_index < Self::MAX_DIGITS { - self.digits[write_index] = new_digit; + *self.digits.at_mut(write_index) = new_digit; write_index += 1; } else if new_digit > 0 { self.truncated = true; @@ -190,11 +191,14 @@ impl Decimal { #[inline] pub fn parse_decimal(mut s: &[u8]) -> Decimal { // can't fail since it follows a call to parse_number - assert!(!s.is_empty(), "the buffer cannot be empty since it follows a call to parse_number"); + debug_assert!( + !s.is_empty(), + "the buffer cannot be empty since it follows a call to parse_number" + ); let mut d = Decimal::default(); let start = s; - let c = s[0]; + let c = *s.at(0); d.negative = c == b'-'; if c == b'-' || c == b'+' { s = s.advance(1); @@ -214,7 +218,7 @@ pub fn parse_decimal(mut s: &[u8]) -> Decimal { break; } // SAFETY: Safe since `num_digits + 8 < Decimal::MAX_DIGITS` - unsafe { d.digits[d.num_digits..].write_u64(v - 0x3030_3030_3030_3030) }; + unsafe { d.digits.at_mut(d.num_digits..).write_u64(v - 0x3030_3030_3030_3030) }; d.num_digits += 8; s = s.advance(8); } @@ -224,7 +228,7 @@ pub fn parse_decimal(mut s: &[u8]) -> Decimal { if d.num_digits != 0 { // Ignore the trailing zeros if there are any let mut n_trailing_zeros = 0; - for &c in start[..(start.len() - s.len())].iter().rev() { + for &c in start.at(..(start.len() - s.len())).iter().rev() { if c == b'0' { n_trailing_zeros += 1; } else if c != b'.' { @@ -261,7 +265,7 @@ pub fn parse_decimal(mut s: &[u8]) -> Decimal { }; } for i in d.num_digits..Decimal::MAX_DIGITS_WITHOUT_OVERFLOW { - d.digits[i] = 0; + *d.digits.at_mut(i) = 0; } d } @@ -325,18 +329,18 @@ fn number_of_digits_decimal_left_shift(d: &Decimal, mut shift: usize) -> usize { ]; shift &= 63; - let x_a = TABLE[shift]; - let x_b = TABLE[shift + 1]; + let x_a = *TABLE.at(shift); + let x_b = *TABLE.at(shift + 1); let num_new_digits = (x_a >> 11) as usize; let pow5_a = (0x7FF & x_a) as usize; let pow5_b = (0x7FF & x_b) as usize; - let pow5 = &TABLE_POW5[pow5_a..]; + let pow5 = TABLE_POW5.at(pow5_a..); for (i, &p5) in pow5.iter().enumerate().take(pow5_b - pow5_a) { if i >= d.num_digits { return num_new_digits - 1; - } else if d.digits[i] == p5 { + } else if *d.digits.at(i) == p5 { continue; - } else if d.digits[i] < p5 { + } else if *d.digits.at(i) < p5 { return num_new_digits - 1; } else { return num_new_digits; diff --git a/src/float.rs b/src/float.rs index dee116e..feebf78 100644 --- a/src/float.rs +++ b/src/float.rs @@ -1,6 +1,8 @@ use core::fmt::{Debug, Display}; use core::ops::{Add, Div, Mul, Neg}; +use crate::GetAt; + mod private { pub trait Sealed {} } @@ -45,8 +47,7 @@ pub trait Float: fn pow10_fast_path(exponent: usize) -> Self; } -impl private::Sealed for f32 { -} +impl private::Sealed for f32 {} impl Float for f32 { const INFINITY: Self = core::f32::INFINITY; @@ -81,12 +82,11 @@ impl Float for f32 { #[allow(clippy::use_self)] const TABLE: [f32; 16] = [1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 0., 0., 0., 0., 0.]; - TABLE[exponent & 15] + *TABLE.at(exponent & 15) } } -impl private::Sealed for f64 { -} +impl private::Sealed for f64 {} impl Float for f64 { const INFINITY: Self = core::f64::INFINITY; @@ -123,6 +123,6 @@ impl Float for f64 { 1e0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, 1e20, 1e21, 1e22, 0., 0., 0., 0., 0., 0., 0., 0., 0., ]; - TABLE[exponent & 31] + *TABLE.at(exponent & 31) } } diff --git a/src/lib.rs b/src/lib.rs index d6793ae..ae03425 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -60,7 +60,11 @@ clippy::struct_field_names )] +#[cfg(feature = "no-panic")] +use no_panic::no_panic; + use core::fmt::{self, Display}; +use core::ops::{RangeFrom, RangeTo}; mod binary; mod common; @@ -124,10 +128,8 @@ pub trait FastFloat: float::Float { } } -impl FastFloat for f32 { -} -impl FastFloat for f64 { -} +impl FastFloat for f32 {} +impl FastFloat for f64 {} /// Parse a decimal number from string into float (full). /// @@ -136,6 +138,7 @@ impl FastFloat for f64 { /// Will return an error either if the string is not a valid decimal number /// or if any characters are left remaining unparsed. #[inline] +#[cfg_attr(feature = "no-panic", no_panic)] pub fn parse>(s: S) -> Result { T::parse_float(s) } @@ -154,3 +157,61 @@ pub fn parse>(s: S) -> Result { pub fn parse_partial>(s: S) -> Result<(T, usize)> { T::parse_float_partial(s) } + +pub trait GetAt { + fn at(&self, index: Index) -> &R; +} + +impl GetAt for [T] { + fn at(&self, index: usize) -> &T { + #[cfg(not(feature = "no-panic"))] + let r = &self[index]; + #[cfg(feature = "no-panic")] + let r = unsafe { self.get_unchecked(index) }; + r + } +} + +impl GetAt, [T]> for [T] { + fn at(&self, index: RangeFrom) -> &[T] { + #[cfg(not(feature = "no-panic"))] + let r = &self[index]; + #[cfg(feature = "no-panic")] + let r = unsafe { self.get_unchecked(index) }; + r + } +} + +impl GetAt, [T]> for [T] { + fn at(&self, index: RangeTo) -> &[T] { + #[cfg(not(feature = "no-panic"))] + let r = &self[index]; + #[cfg(feature = "no-panic")] + let r = unsafe { self.get_unchecked(index) }; + r + } +} + +pub trait GetAtMut { + fn at_mut(&mut self, index: Index) -> &mut R; +} + +impl GetAtMut for [T] { + fn at_mut(&mut self, index: usize) -> &mut T { + #[cfg(not(feature = "no-panic"))] + let r = &mut self[index]; + #[cfg(feature = "no-panic")] + let r = unsafe { self.get_unchecked_mut(index) }; + r + } +} + +impl GetAtMut, [T]> for [T] { + fn at_mut(&mut self, index: RangeFrom) -> &mut [T] { + #[cfg(not(feature = "no-panic"))] + let r = &mut self[index]; + #[cfg(feature = "no-panic")] + let r = unsafe { self.get_unchecked_mut(index) }; + r + } +} diff --git a/src/number.rs b/src/number.rs index a13c5be..28754e7 100644 --- a/src/number.rs +++ b/src/number.rs @@ -1,5 +1,6 @@ use crate::common::{is_8digits, AsciiStr, ByteSlice}; use crate::float::Float; +use crate::GetAt; const MIN_19DIGIT_INT: u64 = 100_0000_0000_0000_0000; @@ -54,7 +55,7 @@ impl Number { } else { // disguised fast path let shift = self.exponent - F::MAX_EXPONENT_FAST_PATH; - let mantissa = self.mantissa.checked_mul(INT_POW10[shift as usize])?; + let mantissa = self.mantissa.checked_mul(*INT_POW10.at(shift as usize))?; if mantissa > F::MAX_MANTISSA_FAST_PATH { return None; } @@ -262,7 +263,7 @@ pub fn parse_number(s: &[u8]) -> Option<(Number, usize)> { #[inline] pub fn parse_inf_nan(s: &[u8]) -> Option<(F, usize)> { fn parse_inf_rest(s: &[u8]) -> usize { - if s.len() >= 8 && s[3..].eq_ignore_case(b"inity") { + if s.len() >= 8 && s.at(3..).eq_ignore_case(b"inity") { 8 } else { 3 @@ -274,14 +275,14 @@ pub fn parse_inf_nan(s: &[u8]) -> Option<(F, usize)> { } else if s.eq_ignore_case(b"inf") { return Some((F::INFINITY, parse_inf_rest(s))); } else if s.len() >= 4 { - if s[0] == b'+' { + if *s.at(0) == b'+' { let s = s.advance(1); if s.eq_ignore_case(b"nan") { return Some((F::NAN, 4)); } else if s.eq_ignore_case(b"inf") { return Some((F::INFINITY, 1 + parse_inf_rest(s))); } - } else if s[0] == b'-' { + } else if *s.at(0) == b'-' { let s = s.advance(1); if s.eq_ignore_case(b"nan") { return Some((F::NEG_NAN, 4)); diff --git a/src/simple.rs b/src/simple.rs index f3a724a..ef5ed02 100644 --- a/src/simple.rs +++ b/src/simple.rs @@ -1,6 +1,7 @@ use crate::common::AdjustedMantissa; use crate::decimal::{parse_decimal, Decimal}; use crate::float::Float; +use crate::GetAt; #[inline] pub fn parse_long_mantissa(s: &[u8]) -> AdjustedMantissa { @@ -11,7 +12,7 @@ pub fn parse_long_mantissa(s: &[u8]) -> AdjustedMantissa { let get_shift = |n| { if n < NUM_POWERS { - POWERS[n] as usize + *POWERS.at(n) as usize } else { MAX_SHIFT }