Skip to content

Commit c41bb3d

Browse files
committed
Restore rational boundary validation
1 parent 55429b3 commit c41bb3d

3 files changed

Lines changed: 97 additions & 12 deletions

File tree

src/rational/arithmetic.rs

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use crate::structural::{RationalFacts, RationalStorageClass};
33
use num::bigint::Sign::{self, *};
44
use num::{BigInt, BigUint, ToPrimitive};
55
use num::{One, Zero};
6-
use serde::{Deserialize, Serialize};
6+
use serde::{Deserialize, Deserializer, Serialize};
77
use std::cmp::Ordering;
88
use std::sync::LazyLock;
99

@@ -49,13 +49,35 @@ use std::sync::LazyLock;
4949
/// assert_eq!(four, Rational::new(4));
5050
/// ```
5151
52-
#[derive(Clone, Debug, Serialize, Deserialize)]
52+
#[derive(Clone, Debug, Serialize)]
5353
pub struct Rational {
5454
sign: Sign,
5555
numerator: BigUint,
5656
denominator: BigUint,
5757
}
5858

59+
impl<'de> Deserialize<'de> for Rational {
60+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
61+
where
62+
D: Deserializer<'de>,
63+
{
64+
#[derive(Deserialize)]
65+
struct RationalWire {
66+
sign: Sign,
67+
numerator: BigUint,
68+
denominator: BigUint,
69+
}
70+
71+
let wire = RationalWire::deserialize(deserializer)?;
72+
if wire.denominator.is_zero() {
73+
return Err(serde::de::Error::custom(
74+
"Rational denominator must be nonzero",
75+
));
76+
}
77+
Ok(Self::from_fraction_parts(wire.sign, wire.numerator, wire.denominator).reduce())
78+
}
79+
}
80+
5981
static ONE: LazyLock<BigUint> = LazyLock::new(BigUint::one);
6082
// Small positive constants use their narrow primitive source type; this keeps
6183
// construction direct and avoids the older `ToBigUint` conversion shim.
@@ -1613,11 +1635,11 @@ impl std::str::FromStr for Rational {
16131635
if numerator.is_zero() {
16141636
sign = NoSign;
16151637
}
1616-
Ok(Self {
1617-
sign,
1618-
numerator,
1619-
denominator: BigUint::parse_bytes(d.as_bytes(), 10).ok_or(Problem::BadFraction)?,
1620-
})
1638+
let denominator = BigUint::parse_bytes(d.as_bytes(), 10).ok_or(Problem::BadFraction)?;
1639+
if denominator.is_zero() {
1640+
return Err(Problem::DivideByZero);
1641+
}
1642+
Ok(Self::from_fraction_parts(sign, numerator, denominator).reduce())
16211643
} else if let Some((i, d)) = s.split_once('.') {
16221644
let numerator = BigUint::parse_bytes(i.as_bytes(), 10).ok_or(Problem::BadDecimal)?;
16231645
let whole = if numerator.is_zero() {
@@ -1963,6 +1985,27 @@ mod tests {
19631985
assert_eq!(answer, expected);
19641986
}
19651987

1988+
#[test]
1989+
fn parse_fraction_rejects_zero_denominator_and_reduces() {
1990+
assert_eq!("1/0".parse::<Rational>(), Err(Problem::DivideByZero));
1991+
assert_eq!("0/0".parse::<Rational>(), Err(Problem::DivideByZero));
1992+
1993+
let reduced: Rational = "9/18".parse().unwrap();
1994+
assert_eq!(reduced, Rational::fraction(1, 2).unwrap());
1995+
assert_eq!(format!("{reduced}"), "1/2");
1996+
}
1997+
1998+
#[test]
1999+
fn serde_rejects_invalid_or_uncanonical_rational_state() {
2000+
let bad = r#"{"sign":1,"numerator":[1],"denominator":[]}"#;
2001+
assert!(serde_json::from_str::<Rational>(bad).is_err());
2002+
2003+
let unreduced = r#"{"sign":1,"numerator":[9],"denominator":[18]}"#;
2004+
let decoded: Rational = serde_json::from_str(unreduced).unwrap();
2005+
assert_eq!(decoded, Rational::fraction(1, 2).unwrap());
2006+
assert_eq!(format!("{decoded}"), "1/2");
2007+
}
2008+
19662009
#[test]
19672010
fn square_reduced() {
19682011
let thirty_two = Rational::new(32);

src/serde.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use std::str::FromStr;
33
use crate::{Problem, Rational, real::Real};
44
use ciborium::{Value, de, ser};
55
use num::BigInt;
6+
use num::bigint::Sign as BigIntSign;
67

78
impl Real {
89
/// Serializes the Real to a JSON string.
@@ -68,11 +69,19 @@ impl TryFrom<&Value> for Real {
6869
}
6970
}
7071
// If we receive an array of length 2, we attempt to interpret it as the numerator and denominator of a Rational.
71-
let numerator = as_big_int(&x[0])?;
72+
let mut numerator = as_big_int(&x[0])?;
7273
let denominator = as_big_int(&x[1])?;
73-
Ok(Real::new(
74-
Rational::from_bigint(numerator) / Rational::from_bigint(denominator),
75-
))
74+
let (denominator_sign, denominator_magnitude) = denominator.into_parts();
75+
if denominator_sign == BigIntSign::NoSign {
76+
return Err(Problem::DivideByZero);
77+
}
78+
if denominator_sign == BigIntSign::Minus {
79+
numerator = -numerator;
80+
}
81+
Ok(Real::new(Rational::from_bigint_fraction(
82+
numerator,
83+
denominator_magnitude,
84+
)?))
7685
}
7786
_ => {
7887
// In this branch, we should be whatever type ciborium decided to use for `Real`.
@@ -262,4 +271,19 @@ mod tests {
262271
let y: Real = (&value).try_into().unwrap();
263272
assert_eq!(x, y);
264273
}
274+
275+
#[test]
276+
fn cbor_rational_pair_rejects_zero_denominator() {
277+
let value = Value::Array(vec![Value::Integer(1.into()), Value::Integer(0.into())]);
278+
assert_eq!(Real::try_from(&value), Err(Problem::DivideByZero));
279+
}
280+
281+
#[test]
282+
fn cbor_rational_pair_normalizes_negative_denominator() {
283+
let value = Value::Array(vec![Value::Integer(1.into()), Value::Integer((-2).into())]);
284+
assert_eq!(
285+
Real::try_from(&value).unwrap(),
286+
Real::new(Rational::fraction(-1, 2).unwrap())
287+
);
288+
}
265289
}

src/simple.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -681,7 +681,16 @@ impl std::str::FromStr for Simple {
681681

682682
fn from_str(s: &str) -> Result<Self, Self::Err> {
683683
let mut chars = s.chars().peekable();
684-
Simple::parse(&mut chars)
684+
let parsed = Simple::parse(&mut chars)?;
685+
while let Some(c) = chars.peek() {
686+
match c {
687+
' ' | '\t' => {
688+
chars.next();
689+
}
690+
_ => return Err("Trailing input after expression"),
691+
}
692+
}
693+
Ok(parsed)
685694
}
686695
}
687696

@@ -696,6 +705,15 @@ mod tests {
696705
assert_eq!(xpr, Err("Incomplete expression"))
697706
}
698707

708+
#[test]
709+
fn parse_rejects_trailing_input() {
710+
let xpr: Result<Simple, &str> = "(+ 1 2) junk".parse();
711+
assert_eq!(xpr, Err("Trailing input after expression"));
712+
713+
let xpr: Result<Simple, &str> = "(+ 1 2) \t".parse();
714+
assert!(xpr.is_ok());
715+
}
716+
699717
#[test]
700718
fn parse_named_operators() {
701719
let cases = [

0 commit comments

Comments
 (0)