mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
fix(cos): write whole reals past the 32-bit integer range as reals
write_real printed a whole real below 1e15 as an integer, and a larger one through Display, which has no decimal point either. So 4e9 or 1e30 was written as an integer token outside the range ISO 32000-2 Annex C gives integers (±2^31-1), and read back as Int (or, in readers with 32-bit integers, overflowed). Whole reals now use the integer form only within that range; past it they keep a trailing '.' (§7.3.3 allows "4."). Raised in the review of #2877. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
924c4da4ac
commit
4a7d556a32
@@ -49,8 +49,10 @@ fn write_real(r: f64, out: &mut Vec<u8>) {
|
||||
out.push(b'0');
|
||||
return;
|
||||
}
|
||||
if r.fract() == 0.0 && r.abs() < 1e15 {
|
||||
let _ = write!(out, "{}", r as i64);
|
||||
// A whole number is written as an integer only within the integer range Annex C gives
|
||||
// (±2³¹−1), so `4e9` or `1e30` stays a real token for readers with 32-bit integers.
|
||||
if r.fract() == 0.0 && r.abs() <= f64::from(i32::MAX) {
|
||||
let _ = write!(out, "{}", r as i32);
|
||||
return;
|
||||
}
|
||||
// Plain decimal notation (§7.3.3 has no exponents). Rust's `Display` for f64 prints the
|
||||
@@ -58,6 +60,10 @@ fn write_real(r: f64, out: &mut Vec<u8>) {
|
||||
// round-trip exactly (rounding to a fixed number of places visibly shifted shading colours).
|
||||
let s = format!("{r}");
|
||||
out.extend_from_slice(if s == "-0" { b"0" } else { s.as_bytes() });
|
||||
// A whole number past that range prints without a point; the trailing `.` keeps it a real.
|
||||
if !s.contains('.') && s != "-0" {
|
||||
out.push(b'.');
|
||||
}
|
||||
}
|
||||
|
||||
fn write_name(n: &[u8], out: &mut Vec<u8>) {
|
||||
@@ -662,14 +668,38 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn whole_reals_past_the_integer_limit_stay_reals() {
|
||||
for (r, text) in [
|
||||
(2_147_483_647.0, "2147483647"),
|
||||
(-2_147_483_647.0, "-2147483647"),
|
||||
(2_147_483_648.0, "2147483648."),
|
||||
(4e9, "4000000000."),
|
||||
(-4e9, "-4000000000."),
|
||||
(1e30, "1000000000000000000000000000000."),
|
||||
(595.0, "595"),
|
||||
(-0.0, "0"),
|
||||
] {
|
||||
let mut out = Vec::new();
|
||||
serialize(&Object::Real(r), &mut out);
|
||||
assert_eq!(String::from_utf8_lossy(&out), text, "{r}");
|
||||
let back = roundtrip(&Object::Real(r));
|
||||
if r.abs() > f64::from(i32::MAX) {
|
||||
assert_eq!(back, Object::Real(r), "{r} must read back as a real");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#[test]
|
||||
fn any_finite_real_round_trips(r in proptest::num::f64::NORMAL | proptest::num::f64::SUBNORMAL | proptest::num::f64::ZERO) {
|
||||
prop_assume!(r.abs() < 1e15 || r.fract() == 0.0);
|
||||
let back = roundtrip(&Object::Real(r));
|
||||
match back {
|
||||
Object::Real(b) => prop_assert_eq!(b, r),
|
||||
Object::Int(i) => prop_assert_eq!(i as f64, r),
|
||||
Object::Int(i) => {
|
||||
prop_assert!(i.unsigned_abs() <= i32::MAX as u64, "{r} came back as integer {i}");
|
||||
prop_assert_eq!(i as f64, r);
|
||||
}
|
||||
other => prop_assert!(false, "unexpected {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user