Skip to main content

pdf_syntax/object/
number.rs

1//! Numbers.
2
3use crate::math::{powi_f64, trunc_f64};
4use crate::object::macros::object;
5use crate::object::{Object, ObjectLike};
6use crate::reader::Reader;
7use crate::reader::{Readable, ReaderContext, ReaderExt, Skippable};
8use crate::trivia::{is_regular_character, is_white_space_character};
9use core::fmt::Debug;
10use log::debug;
11
12#[rustfmt::skip]
13static POWERS_OF_10: [f64; 20] = [
14    1.0, 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9,
15    1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19,
16];
17
18/// A number.
19#[derive(Clone, Copy, Debug, PartialEq)]
20pub struct Number(pub(crate) InternalNumber);
21
22impl Number {
23    /// The number zero.
24    pub const ZERO: Self = Self::from_i32(0);
25    /// The number one.
26    pub const ONE: Self = Self::from_i32(1);
27
28    /// Returns the number as a f64.
29    pub fn as_f64(&self) -> f64 {
30        match self.0 {
31            InternalNumber::Real(r) => r,
32            InternalNumber::Integer(i) => i as f64,
33        }
34    }
35
36    /// Returns the number as a f32.
37    pub fn as_f32(&self) -> f32 {
38        match self.0 {
39            InternalNumber::Real(r) => r as f32,
40            InternalNumber::Integer(i) => i as f32,
41        }
42    }
43
44    /// Returns the number as an i64.
45    pub fn as_i64(&self) -> i64 {
46        match self.0 {
47            InternalNumber::Real(r) => {
48                let res = r as i64;
49
50                if !(trunc_f64(r) == r) {
51                    debug!("float {r} was truncated to {res}");
52                }
53
54                res
55            }
56            InternalNumber::Integer(i) => i,
57        }
58    }
59
60    /// Create a new `Number` from an f32 number.
61    pub const fn from_f32(num: f32) -> Self {
62        Self(InternalNumber::Real(num as f64))
63    }
64
65    /// Create a new `Number` from an i32 number.
66    pub const fn from_i32(num: i32) -> Self {
67        Self(InternalNumber::Integer(num as i64))
68    }
69
70    /// Returns true if this number was parsed as a real (floating-point) number.
71    ///
72    /// PDF distinguishes integers from reals: implementation limits (§6.1.12/§6.1.13)
73    /// only apply to real values, not to integers.
74    pub fn is_real(&self) -> bool {
75        matches!(self.0, InternalNumber::Real(_))
76    }
77}
78
79impl Skippable for Number {
80    fn skip(r: &mut Reader<'_>, _: bool) -> Option<()> {
81        let has_sign = r.forward_if(|b| b == b'+' || b == b'-').is_some();
82
83        // Some PDFs have weird trailing minuses, so try to accept those as well.
84        match r.peek_byte()? {
85            b'.' => {
86                r.read_byte()?;
87                // See PDFJS-9252 - treat a single . as 0.
88                r.forward_while(is_digit_or_minus);
89            }
90            b'0'..=b'9' | b'-' => {
91                r.forward_while_1(is_digit_or_minus)?;
92                if let Some(()) = r.forward_tag(b".") {
93                    r.forward_while(is_digit_or_minus);
94                }
95            }
96            // See PDFJS-bug1753983 - accept just + or - as a zero.
97            // ALso see PDFJS-bug1953099, where the sign is followed by a show
98            // text string operand, requiring us to allow '<' and '(' as well.
99            b if has_sign && (is_white_space_character(b) || matches!(b, b'(' | b'<')) => {}
100            _ => return None,
101        }
102
103        // See issue 994. Don't accept numbers that are followed by a regular character.
104        if r.peek_byte().is_some_and(is_regular_character) {
105            return None;
106        }
107
108        Some(())
109    }
110}
111
112impl Readable<'_> for Number {
113    #[inline]
114    fn read(r: &mut Reader<'_>, _: &ReaderContext<'_>) -> Option<Self> {
115        let old_offset = r.offset();
116        read_inner(r).or_else(|| {
117            r.jump(old_offset);
118            None
119        })
120    }
121}
122
123#[inline(always)]
124fn read_inner(r: &mut Reader<'_>) -> Option<Number> {
125    let start_offset = r.offset();
126    let mut overflowed = false;
127    let mut number_end = None;
128    let negative = match r.peek_byte()? {
129        b'-' => {
130            r.forward();
131            true
132        }
133        b'+' => {
134            r.forward();
135            false
136        }
137        _ => false,
138    };
139
140    let mut mantissa: u64 = 0;
141    let mut has_dot = false;
142    let mut decimal_shift: u32 = 0;
143    let mut has_digits = false;
144
145    loop {
146        match r.peek_byte() {
147            Some(b'0'..=b'9') => {
148                let d = r.read_byte().expect("peek_byte returned Some");
149                // Wrapping was here, with a note that such numbers should not
150                // appear. They do, and wrapping turns an out-of-range number
151                // into a small plausible one -- a wrong document that parses
152                // cleanly. Overflow is recorded and the span re-read below.
153                // Ported from LaurenzV/hayro#1342.
154                if !overflowed {
155                    match mantissa
156                        .checked_mul(10)
157                        .and_then(|v| v.checked_add(u64::from(d - b'0')))
158                    {
159                        Some(value) => mantissa = value,
160                        None => overflowed = true,
161                    }
162                }
163                number_end = Some(r.offset());
164                has_digits = true;
165                if has_dot {
166                    decimal_shift += 1;
167                }
168            }
169            Some(b'.') if !has_dot => {
170                r.forward();
171                has_dot = true;
172            }
173            // Some weird PDFs have trailing minus in the fraction of number.
174            Some(b'-') if has_digits => {
175                r.forward();
176                r.forward_while(is_digit_or_minus);
177                break;
178            }
179            _ => break,
180        }
181    }
182
183    if !has_digits {
184        if negative || has_dot {
185            // Treat numbers like just `-`, `+` or `-.` as zero.
186            return Some(Number(InternalNumber::Integer(0)));
187        }
188        return None;
189    }
190
191    // See issue 994. Don't accept numbers that are followed by a regular character
192    // without any white space in-between.
193    if r.peek_byte().is_some_and(is_regular_character) {
194        return None;
195    }
196
197    // `mantissa as i64` is a wrap of its own once the value passes i64::MAX,
198    // and i64::MIN is exactly the case that cannot be produced by negating a
199    // positive i64 -- which is what int_min_does_not_panic guards. Re-reading
200    // the digits and letting the standard library parse them handles both, and
201    // costs nothing on the ordinary path.
202    let integer_overflow = !has_dot && mantissa > i64::MAX as u64;
203    if overflowed || integer_overflow {
204        let end_offset = number_end.unwrap_or_else(|| r.offset());
205        let text = core::str::from_utf8(r.range(start_offset..end_offset)?).ok()?;
206
207        if !has_dot && let Ok(value) = text.parse::<i64>() {
208            return Some(Number(InternalNumber::Integer(value)));
209        }
210        let value = text.parse::<f64>().ok()?;
211        return Some(Number(InternalNumber::Real(value)));
212    }
213
214    if !has_dot {
215        let value = if negative {
216            // `-i64::MIN` has no representation; a document that literally
217            // writes -9223372036854775808 must not take the process with it.
218            // Ported from hayro upstream (LaurenzV/hayro#1194).
219            (mantissa as i64).wrapping_neg()
220        } else {
221            mantissa as i64
222        };
223        Some(Number(InternalNumber::Integer(value)))
224    } else {
225        let mut value = mantissa as f64;
226
227        if decimal_shift > 0 {
228            if decimal_shift < POWERS_OF_10.len() as u32 {
229                value /= POWERS_OF_10[decimal_shift as usize];
230            } else {
231                value /= powi_f64(10.0, decimal_shift);
232            }
233        }
234
235        if negative {
236            value = -value;
237        }
238
239        Some(Number(InternalNumber::Real(value)))
240    }
241}
242
243object!(Number, Number);
244
245#[derive(Clone, Copy, Debug, PartialEq)]
246pub(crate) enum InternalNumber {
247    Real(f64),
248    Integer(i64),
249}
250
251macro_rules! int_num {
252    ($i:ident) => {
253        impl Skippable for $i {
254            fn skip(r: &mut Reader<'_>, _: bool) -> Option<()> {
255                r.forward_if(|b| b == b'+' || b == b'-');
256                r.forward_while_1(is_digit)?;
257
258                // We have a float instead of an integer.
259                if r.peek_byte() == Some(b'.') {
260                    return None;
261                }
262
263                // See issue 994, and LaurenzV/hayro#1191 which applied it here.
264                // Number::skip and read_inner already refused a number butted up
265                // against a regular character; this third path did not, so
266                // `[8 0R]` parsed as an array of two integers instead of the
267                // malformed reference it is.
268                if r.peek_byte().is_some_and(is_regular_character) {
269                    return None;
270                }
271
272                Some(())
273            }
274        }
275
276        impl<'a> Readable<'a> for $i {
277            fn read(r: &mut Reader<'a>, ctx: &ReaderContext<'a>) -> Option<$i> {
278                r.read::<Number>(ctx)
279                    .map(|n| n.as_i64())
280                    .and_then(|n| n.try_into().ok())
281            }
282        }
283
284        impl TryFrom<Object<'_>> for $i {
285            type Error = ();
286
287            fn try_from(value: Object<'_>) -> core::result::Result<Self, Self::Error> {
288                match value {
289                    Object::Number(n) => n.as_i64().try_into().ok().ok_or(()),
290                    _ => Err(()),
291                }
292            }
293        }
294
295        impl<'a> ObjectLike<'a> for $i {}
296    };
297}
298
299int_num!(i32);
300int_num!(i64);
301int_num!(u32);
302int_num!(u16);
303int_num!(usize);
304int_num!(u8);
305
306impl Skippable for f32 {
307    fn skip(r: &mut Reader<'_>, is_content_stream: bool) -> Option<()> {
308        r.skip::<Number>(is_content_stream).map(|_| {})
309    }
310}
311
312impl Readable<'_> for f32 {
313    fn read(r: &mut Reader<'_>, _: &ReaderContext<'_>) -> Option<Self> {
314        r.read_without_context::<Number>()
315            .map(|n| n.as_f64() as Self)
316    }
317}
318
319impl TryFrom<Object<'_>> for f32 {
320    type Error = ();
321
322    fn try_from(value: Object<'_>) -> Result<Self, Self::Error> {
323        match value {
324            Object::Number(n) => Ok(n.as_f64() as Self),
325            _ => Err(()),
326        }
327    }
328}
329
330impl ObjectLike<'_> for f32 {}
331
332impl Skippable for f64 {
333    fn skip(r: &mut Reader<'_>, is_content_stream: bool) -> Option<()> {
334        r.skip::<Number>(is_content_stream).map(|_| {})
335    }
336}
337
338impl Readable<'_> for f64 {
339    fn read(r: &mut Reader<'_>, _: &ReaderContext<'_>) -> Option<Self> {
340        r.read_without_context::<Number>().map(|n| n.as_f64())
341    }
342}
343
344impl TryFrom<Object<'_>> for f64 {
345    type Error = ();
346
347    fn try_from(value: Object<'_>) -> Result<Self, Self::Error> {
348        match value {
349            Object::Number(n) => Ok(n.as_f64()),
350            _ => Err(()),
351        }
352    }
353}
354
355impl ObjectLike<'_> for f64 {}
356
357pub(crate) fn is_digit(byte: u8) -> bool {
358    byte.is_ascii_digit()
359}
360
361pub(crate) fn is_digit_or_minus(byte: u8) -> bool {
362    is_digit(byte) || byte == b'-'
363}
364
365#[cfg(test)]
366mod tests {
367    use crate::object::Number;
368    use crate::reader::Reader;
369    use crate::reader::ReaderExt;
370
371    #[test]
372    fn int_1() {
373        assert_eq!(
374            Reader::new("0".as_bytes())
375                .read_without_context::<i32>()
376                .unwrap(),
377            0
378        );
379    }
380
381    #[test]
382    fn int_3() {
383        assert_eq!(
384            Reader::new("+32".as_bytes())
385                .read_without_context::<i32>()
386                .unwrap(),
387            32
388        );
389    }
390
391    #[test]
392    fn int_4() {
393        assert_eq!(
394            Reader::new("-32".as_bytes())
395                .read_without_context::<i32>()
396                .unwrap(),
397            -32
398        );
399    }
400
401    #[test]
402    fn int_6() {
403        assert_eq!(
404            Reader::new("98349".as_bytes())
405                .read_without_context::<i32>()
406                .unwrap(),
407            98349
408        );
409    }
410
411    #[test]
412    fn int_7() {
413        assert_eq!(
414            Reader::new("003245".as_bytes())
415                .read_without_context::<i32>()
416                .unwrap(),
417            3245
418        );
419    }
420
421    /// Regression test for a fix ported from hayro upstream
422    /// (LaurenzV/hayro#1194): `-(mantissa as i64)` has no representation for
423    /// `i64::MIN`, so a document that literally writes the minimum took the
424    /// process down.
425    #[test]
426    fn int_min_does_not_panic() {
427        assert_eq!(
428            Reader::new("-9223372036854775808".as_bytes())
429                .read_without_context::<i64>()
430                .unwrap(),
431            i64::MIN
432        );
433    }
434
435    #[test]
436    fn real_1() {
437        assert_eq!(
438            Reader::new("3".as_bytes())
439                .read_without_context::<f32>()
440                .unwrap(),
441            3.0
442        );
443    }
444
445    #[test]
446    fn real_3() {
447        assert_eq!(
448            Reader::new("+32".as_bytes())
449                .read_without_context::<f32>()
450                .unwrap(),
451            32.0
452        );
453    }
454
455    #[test]
456    fn real_4() {
457        assert_eq!(
458            Reader::new("-32".as_bytes())
459                .read_without_context::<f32>()
460                .unwrap(),
461            -32.0
462        );
463    }
464
465    #[test]
466    fn real_5() {
467        assert_eq!(
468            Reader::new("-32.01".as_bytes())
469                .read_without_context::<f32>()
470                .unwrap(),
471            -32.01
472        );
473    }
474
475    #[test]
476    fn real_6() {
477        assert_eq!(
478            Reader::new("-.345".as_bytes())
479                .read_without_context::<f32>()
480                .unwrap(),
481            -0.345
482        );
483    }
484
485    #[test]
486    fn real_7() {
487        assert_eq!(
488            Reader::new("-.00143".as_bytes())
489                .read_without_context::<f32>()
490                .unwrap(),
491            -0.00143
492        );
493    }
494
495    #[test]
496    fn real_8() {
497        assert_eq!(
498            Reader::new("-12.0013".as_bytes())
499                .read_without_context::<f32>()
500                .unwrap(),
501            -12.0013
502        );
503    }
504
505    #[test]
506    fn real_9() {
507        assert_eq!(
508            Reader::new("98349.432534".as_bytes())
509                .read_without_context::<f32>()
510                .unwrap(),
511            98_349.43
512        );
513    }
514
515    #[test]
516    fn real_10() {
517        assert_eq!(
518            Reader::new("-34534656.34".as_bytes())
519                .read_without_context::<f32>()
520                .unwrap(),
521            -34534656.34
522        );
523    }
524
525    #[test]
526    fn real_failing() {
527        assert!(
528            Reader::new("+abc".as_bytes())
529                .read_without_context::<f32>()
530                .is_none()
531        );
532    }
533
534    #[test]
535    fn number_1() {
536        assert_eq!(
537            Reader::new("+32".as_bytes())
538                .read_without_context::<Number>()
539                .unwrap()
540                .as_f64() as f32,
541            32.0
542        );
543    }
544
545    #[test]
546    fn number_2() {
547        assert_eq!(
548            Reader::new("-32.01".as_bytes())
549                .read_without_context::<Number>()
550                .unwrap()
551                .as_f64() as f32,
552            -32.01
553        );
554    }
555
556    #[test]
557    fn number_3() {
558        assert_eq!(
559            Reader::new("-.345".as_bytes())
560                .read_without_context::<Number>()
561                .unwrap()
562                .as_f64() as f32,
563            -0.345
564        );
565    }
566
567    #[test]
568    fn large_number() {
569        assert_eq!(
570            Reader::new("38359922".as_bytes())
571                .read_without_context::<Number>()
572                .unwrap()
573                .as_i64(),
574            38359922
575        );
576    }
577
578    #[test]
579    fn large_number_2() {
580        assert_eq!(
581            Reader::new("4294966260".as_bytes())
582                .read_without_context::<u32>()
583                .unwrap(),
584            4294966260
585        );
586    }
587
588    /// Ported from LaurenzV/hayro#1342.
589    ///
590    /// A number too large for u64 used to wrap, turning an out-of-range value
591    /// into a small plausible one. A `/Length` or a coordinate silently became a
592    /// different number and the document parsed cleanly around it.
593    #[test]
594    fn a_number_past_u64_does_not_wrap_into_a_small_one() {
595        // 10^25: twenty-six digits, far past u64::MAX (about 1.8e19).
596        let n = Reader::new(b"10000000000000000000000000")
597            .read_without_context::<Number>()
598            .unwrap();
599        let value = n.as_f64();
600        assert!(
601            value > 9.9e24 && value < 1.01e25,
602            "expected about 1e25, got {value}"
603        );
604    }
605
606    /// i64::MIN cannot be produced by negating a positive i64, which is the
607    /// shape int_min_does_not_panic already guarded. It must still come back as
608    /// an integer rather than being widened to a float.
609    #[test]
610    fn int_min_stays_an_integer() {
611        let n = Reader::new(b"-9223372036854775808")
612            .read_without_context::<Number>()
613            .unwrap();
614        assert_eq!(n.as_i64(), i64::MIN);
615    }
616
617    /// And the ordinary path is untouched: no re-read, no float widening.
618    #[test]
619    fn an_ordinary_integer_is_still_an_integer() {
620        let n = Reader::new(b"12345")
621            .read_without_context::<Number>()
622            .unwrap();
623        assert_eq!(n.as_i64(), 12345);
624    }
625}