Repository: J-F-Liu/lopdf (language: rust)

Issue:
Panic `attempt to multiply with overflow` in `src/object.rs:731:22`
code
```
fn check_file(file_path: &str) {
    let Ok(content) = fs::read(file_path) else {
        return;
    };
    let cursor = Cursor::new(content);
    match Document::load_from(cursor) {
        Ok(mut document) => {
            let pages = document.get_pages();

            let mut doc_clone = document.clone();
            doc_clone.decompress();

            for (i, _) in pages.iter().enumerate() {
                let page_number = (i + 1) as u32;
                let _text = document.extract_text(&[page_number]);
            }

            let _ = document.save_to(&mut Cursor::new(Vec::new()));
        }
        Err(err) => {
            eprintln!("Error reading PDF contents: {}", err)
        }
    }
}

```


cause this
```
thread 'main' panicked at /home/runner/.cargo/git/checkouts/lopdf-0013854ec52a6141/0d34b64/src/object.rs:731:22:
attempt to multiply with overflow
stack backtrace:
   0: rust_begin_unwind
             at /rustc/12b26c13fba25c9e1bc2fdf05f3c2dbb851c83de/library/std/src/panicking.rs:665:5
   1: core::panicking::panic_fmt
             at /rustc/12b26c13fba25c9e1bc2fdf05f3c2dbb851c83de/library/core/src/panicking.rs:74:14
   2: core::panicking::panic_const::panic_const_mul_overflow
             at /rustc/12b26c13fba25c9e1bc2fdf05f3c2dbb851c83de/library/core/src/panicking.rs:181:21
   3: lopdf::object::Stream::decode_ascii85
             at /home/runner/.cargo/git/checkouts/lopdf-0013854ec52a6141/0d34b64/src/object.rs:731:22
   4: lopdf::object::Stream::decompressed_content
             at /home/runner/.cargo/git/checkouts/lopdf-0013854ec52a6141/0d34b64/src/object.rs:658:36
   5: lopdf::object::Stream::decompress
             at /home/runner/.cargo/git/checkouts/lopdf-0013854ec52a6141/0d34b64/src/object.rs:772:27
   6: lopdf::processor::<impl lopdf::document::Document>::decompress
             at /home/runner/.cargo/git/checkouts/lopdf-0013854ec52a6141/0d34b64/src/processor.rs:37:17
   7: lopdf::check_file
             at ./src/crates/lopdf/src/main.rs:37:13
   8: lopdf::main
             at ./src/crates/lopdf/src/main.rs:23:9
   9: core::ops::function::FnOnce::call_once
             at /rustc/12b26c13fba25c9e1bc2fdf05f3c2dbb851c83de/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

##### Automatic Fuzzer note, output status "None", output signal "Some(6)"
```
[compressed.zip](https://github.com/user-attachments/files/16930144/compressed.zip)



Discussion / hints:

Fix patch:
diff --git a/src/encryption.rs b/src/encryption.rs
index ca1a218..34626d7 100644
--- a/src/encryption.rs
+++ b/src/encryption.rs
@@ -1,12 +1,8 @@
 use crate::rc4::Rc4;
 use crate::{Document, Object, ObjectId};
+use aes::cipher::{block_padding::Pkcs7, BlockDecryptMut, KeyIvInit};
 use md5::{Digest as _, Md5};
 use std::fmt;
-use aes::cipher::{
-    block_padding::Pkcs7,
-    BlockDecryptMut,
-    KeyIvInit
-};
 
 #[derive(Debug)]
 pub enum DecryptionError {
@@ -228,11 +224,7 @@ where
     Key: AsRef<[u8]>,
 {
     let key = key.as_ref();
-    let len = if aes {
-        key.len() + 9
-    } else {
-        key.len() + 5
-    };
+    let len = if aes { key.len() + 9 } else { key.len() + 5 };
     let mut builder = Vec::<u8>::with_capacity(len);
     builder.extend_from_slice(key.as_ref());
 
@@ -242,10 +234,10 @@ where
     builder.extend_from_slice(&obj_id.1.to_le_bytes()[..2]);
 
     if aes {
-		builder.append(&mut vec![0x73, 0x41, 0x6C, 0x54]);
-	}
+        builder.append(&mut vec![0x73, 0x41, 0x6C, 0x54]);
+    }
 
-	// Now construct the rc4 key
+    // Now construct the rc4 key
     let key_len = std::cmp::min(key.len() + 5, 16);
     let rc4_key = &Md5::digest(builder)[..key_len];
 
diff --git a/src/object.rs b/src/object.rs
index e8ff0a3..0964d8f 100644
--- a/src/object.rs
+++ b/src/object.rs
@@ -657,7 +657,7 @@ impl Stream {
             output = match filter.as_str() {
                 "FlateDecode" => Self::decompress_zlib(input, params)?,
                 "LZWDecode" => Self::decompress_lzw(input, params)?,
-                "ASCII85Decode" => Self::decode_ascii85(input),
+                "ASCII85Decode" => Self::decode_ascii85(input)?,
                 _ => return Err(Error::Type),
             };
             input = &output;
@@ -712,16 +712,22 @@ impl Stream {
         Self::decompress_predictor(output, params)
     }
 
-    fn decode_ascii85(input: &[u8]) -> Vec<u8> {
+    fn decode_ascii85(input: &[u8]) -> Result<Vec<u8>> {
         let mut output = vec![];
         let mut buffer: u32 = 0;
         let mut count = 0;
-
-        for &ch in input {
-            if ch == b'z' && count == 0 {
+        // Check for EOD marker
+        assert_eq!(b"~>", &input[input.len() - 2..]);
+        for &ch in &input[..input.len() - 2] {
+            if ch == b'z' {
+                if count != 0 {
+                    // z character is not allowed in middle of a group
+                    return Err(Error::ContentDecode);
+                }
                 output.extend_from_slice(&[0, 0, 0, 0]);
                 continue;
             }
+
             if ch.is_ascii_whitespace() {
                 continue;
             }
@@ -729,8 +735,8 @@ impl Stream {
             if !(b'!'..=b'u').contains(&ch) {
                 break;
             }
-
-            buffer = buffer * 85 + ((ch - b'!') as u32);
+            buffer = buffer.checked_mul(85).ok_or(Error::ContentDecode)?;
+            buffer += (ch - b'!') as u32;
             count += 1;
 
             if count == 5 {
@@ -742,14 +748,15 @@ impl Stream {
 
         if count > 0 {
             for _ in count..5 {
-                buffer = buffer * 85 + 84;
+                buffer = buffer.checked_mul(85).ok_or(Error::ContentDecode)?;
+                buffer += 84;
             }
 
             let bytes = buffer.to_be_bytes();
             output.extend_from_slice(&bytes[..count - 1]);
         }
 
-        output
+        Ok(output)
     }
 
     fn decompress_predictor(mut data: Vec<u8>, params: Option<&Dictionary>) -> Result<Vec<u8>> {
@@ -782,3 +789,31 @@ impl Stream {
         self.dict.get(b"Filter").is_ok()
     }
 }
+
+#[cfg(test)]
+mod test {
+    use crate::Error;
+
+    use super::Stream;
+
+    #[test]
+    fn test_decode_ascii85() {
+        let input = r#"9jqo^BlbD-BleB1DJ+*+F(f,q/0JhKF<GL>Cj@.4Gp$d7F!,L7@<6@)/0JDEF<G%<+EV:2F!,O<
+            DJ+*.@<*K0@<6L(Df-\0Ec5e;DffZ(EZee.Bl.9pF"AGXBPCsi+DGm>@3BB/F*&OCAfu2/AKYi(
+            DIb:@FD,*)+C]U=@3BN#EcYf8ATD3s@q?d$AftVqCh[NqF<G:8+EV:.+Cf>-FD5W8ARlolDIal(
+            DId<j@<?3r@:F%a+D58'ATD4$Bl@l3De:,-DJs`8ARoFb/0JMK@qB4^F!,R<AKZ&-DfTqBG%G>u
+            D.RTpAKYo'+CT/5+Cei#DII?(E,9)oF*2M7/c~>"#;
+        let expected = "Man is distinguished, not only by his reason, but by this singular passion from other animals, which is a lust of the mind, that by a perseverance of delight in the continued and indefatigable generation of knowledge, exceeds the short vehemence of any carnal pleasure.";
+        let output = Stream::decode_ascii85(input.as_bytes()).unwrap();
+        println!("{}", String::from_utf8(output.clone()).unwrap());
+        assert_eq!(&output, expected.as_bytes());
+    }
+
+    #[test]
+    fn test_decode_ascii85_overflow() {
+        let input = b"uuuuu~>";
+        let output = Stream::decode_ascii85(input);
+        // let expected: Result<Vec<u8>, Error> = Err(Error::ContentDecode);
+        assert!(matches!(output, Err(Error::ContentDecode)));
+    }
+}


Test patch: