From 2ddbc019ae670321205abef2ee229cbb777f3575 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Sun, 13 Aug 2023 10:51:47 +0200 Subject: [PATCH] Fix regression in decoding mail parts FETCHed from IMAP (#9096) --- CHANGELOG.md | 2 +- program/lib/Roundcube/rcube_imap_generic.php | 167 +++++++++++----- tests/Framework/ImapGeneric.php | 99 +++++++++- tests/src/test.base64 | 54 +++++ tests/src/test.pdf | 198 +++++++++++++++++++ tests/src/test.uuencode | 71 +++++++ 6 files changed, 535 insertions(+), 56 deletions(-) create mode 100644 tests/src/test.base64 create mode 100644 tests/src/test.pdf create mode 100644 tests/src/test.uuencode diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a66e4465..4e96f7105 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ - Fix regression where LDAP addressbook 'filter' option was ignored (#9061) - Fix wrong order of a multi-folder search result when sorting by size (#9065) - Fix so install/update scripts do not require PEAR (#9037) -- Fix handling of mail parts that are encoded with x-uuencode (#9096) +- Fix regression where some mail parts could have been decoded incorrectly, or not at all (#9096) ## Release 1.6.2 diff --git a/program/lib/Roundcube/rcube_imap_generic.php b/program/lib/Roundcube/rcube_imap_generic.php index 207ca9c54..737ea6676 100644 --- a/program/lib/Roundcube/rcube_imap_generic.php +++ b/program/lib/Roundcube/rcube_imap_generic.php @@ -2870,7 +2870,7 @@ class rcube_imap_generic $mode = 3; break; default: - $mode = 0; + $mode = $formatted ? 4 : 0; } // Use BINARY extension when possible (and safe) @@ -2932,15 +2932,7 @@ class rcube_imap_generic } if ($result !== false) { - if ($mode == 1) { - $result = base64_decode($result); - } - else if ($mode == 2) { - $result = quoted_printable_decode($result); - } - else if ($mode == 3) { - $result = convert_uudecode($result); - } + $result = $this->decodeContent($result, $mode, true); } } // response with string literal @@ -2954,67 +2946,37 @@ class rcube_imap_generic if (!$bytes) { $result = ''; } + // An optimal path for a case when we need the body as-is in a string + else if (!$mode && !$file && !$print) { + $result = $this->readBytes($bytes); + } else while ($bytes > 0) { - $line = $this->readBytes($bytes > $chunkSize ? $chunkSize : $bytes); + $chunk = $this->readBytes($bytes > $chunkSize ? $chunkSize : $bytes); - if ($line === '') { + if ($chunk === '') { break; } - $len = strlen($line); + $len = strlen($chunk); if ($len > $bytes) { - $line = substr($line, 0, $bytes); - $len = strlen($line); + $chunk = substr($chunk, 0, $bytes); + $len = strlen($chunk); } $bytes -= $len; - // BASE64 - if ($mode == 1) { - $line = preg_replace('|[^a-zA-Z0-9+=/]|', '', $line); - // create chunks with proper length for base64 decoding - $line = $prev.$line; - $length = strlen($line); - if ($length % 4) { - $length = floor($length / 4) * 4; - $prev = substr($line, $length); - $line = substr($line, 0, $length); - } - else { - $prev = ''; - } - $line = base64_decode($line); - } - // QUOTED-PRINTABLE - else if ($mode == 2) { - $line = rtrim($line, "\t\r\0\x0B"); - $line = quoted_printable_decode($line); - } - // UUENCODE - else if ($mode == 3) { - $line = preg_replace( - ['/\r?\n/', '/\nend$/', '/^begin\s+[0-7]{3}\s+[^\n]+\n/'], - ["\n", '', ''], - $line - ); - - $line = convert_uudecode($line); - } - // default - else if ($formatted) { - $line = rtrim($line, "\t\r\n\0\x0B") . "\n"; - } + $chunk = $this->decodeContent($chunk, $mode, $bytes <= 0, $prev); if ($file) { - if (fwrite($file, $line) === false) { + if (fwrite($file, $chunk) === false) { break; } } else if ($print) { - echo $line; + echo $chunk; } else { - $result .= $line; + $result .= $chunk; } } } @@ -3036,6 +2998,105 @@ class rcube_imap_generic return false; } + /** + * Decodes a chunk of a message part content from a FETCH response. + * + * @param string $chunk Content + * @param int $mode Encoding mode + * @param bool $is_last Whether it is a last chunk of data + * @param string $prev Extra content from the previous chunk + * + * @return string Encoded string + */ + protected static function decodeContent($chunk, $mode, $is_last = false, &$prev = '') + { + // BASE64 + if ($mode == 1) { + $chunk = $prev . preg_replace('|[^a-zA-Z0-9+=/]|', '', $chunk); + + // create chunks with proper length for base64 decoding + $length = strlen($chunk); + + if ($length % 4) { + $length = floor($length / 4) * 4; + $prev = substr($chunk, $length); + $chunk = substr($chunk, 0, $length); + } + else { + $prev = ''; + } + + return base64_decode($chunk); + } + + // QUOTED-PRINTABLE + if ($mode == 2) { + if (!self::decodeContentChunk($chunk, $prev, $is_last)) { + return ''; + } + + $chunk = preg_replace('/[\t\r\0\x0B]+\n/', "\n", $chunk); + + return quoted_printable_decode($chunk); + } + + // X-UUENCODE + if ($mode == 3) { + if (!self::decodeContentChunk($chunk, $prev, $is_last)) { + return ''; + } + + $chunk = preg_replace( + ['/\r?\n/', '/(^|\n)end$/', '/^begin\s+[0-7]{3}\s+[^\n]+\n/'], + ["\n", '', ''], + $chunk + ); + + if (!strlen($chunk)) { + return ''; + } + + return convert_uudecode($chunk); + } + + // Plain text formatted + // TODO: Formatting should be handled outside of this class + if ($mode == 4) { + if (!self::decodeContentChunk($chunk, $prev, $is_last)) { + return ''; + } + + if ($is_last) { + $chunk = rtrim($chunk, "\t\r\n\0\x0B"); + } + + return preg_replace('/[\t\r\0\x0B]+\n/', "\n", $chunk); + } + + return $chunk; + } + + /** + * A helper for a new-line aware parsing. See self::decodeContent(). + */ + private static function decodeContentChunk(&$chunk, &$prev, $is_last) + { + $chunk = $prev . $chunk; + $prev = ''; + + if (!$is_last) { + if (($pos = strrpos($chunk, "\n")) !== false) { + $prev = substr($chunk, $pos + 1); + $chunk = substr($chunk, 0, $pos + 1); + } else { + $prev = $chunk; + return false; + } + } + + return true; + } + /** * Handler for IMAP APPEND command * diff --git a/tests/Framework/ImapGeneric.php b/tests/Framework/ImapGeneric.php index d066d1262..82c8b9839 100644 --- a/tests/Framework/ImapGeneric.php +++ b/tests/Framework/ImapGeneric.php @@ -82,7 +82,7 @@ class Framework_ImapGeneric extends PHPUnit\Framework\TestCase } /** - * Test for uncompressMessageSet + * Test for uncompressMessageSet() */ function test_uncompressMessageSet() { @@ -100,7 +100,7 @@ class Framework_ImapGeneric extends PHPUnit\Framework\TestCase } /** - * Test for tokenizeResponse + * Test for tokenizeResponse() */ function test_tokenizeResponse() { @@ -121,4 +121,99 @@ class Framework_ImapGeneric extends PHPUnit\Framework\TestCase $result = rcube_imap_generic::tokenizeResponse($response, 1); $this->assertSame(['item1', 'item2'], $result); } + + /** + * Test for decodeContent() with no encoding + */ + function test_decode_content_plain() + { + $content = "test uuencode encoded content\ntest uuencode encoded content"; + + $this->runDecodeContent($content, $content, 0); + } + + /** + * Test for decodeContent() with base64 encoding + */ + function test_decode_content_base64() + { + $content = "test base64 encoded content\ntest base64 encoded content"; + $encoded = chunk_split(base64_encode($content), 10, "\r\n"); + + $this->runDecodeContent($content, $encoded, 1); + + // Test some real-life example + $content = file_get_contents(TESTS_DIR . "src/test.pdf"); + $encoded = file_get_contents(TESTS_DIR . "src/test.base64"); + + $this->runDecodeContent($content, $encoded, 1, 2000); + $this->runDecodeContent($content, $encoded, 1, 4000); + $this->runDecodeContent($content, $encoded, 1, 6000); + } + + /** + * Test for decodeContent() with quoted-printable encoding + */ + function test_decode_content_qp() + { + $content = "test quoted-printable\n\n żąśźć encoded content\ntest quoted-printable żąśźć encoded content"; + $encoded = Mail_mimePart::quotedPrintableEncode($content, 12); + + $this->runDecodeContent($content, $encoded, 2); + } + + /** + * Test for decodeContent() with x-uuencode encoding + */ + function test_decode_content_uuencode() + { + $content = "test uuencode encoded content\ntest uuencode encoded content"; + $encoded = "begin 664 test.txt\r\n" . convert_uuencode($content) . "end"; + + $this->runDecodeContent($content, $encoded, 3); + + // Test some real-life example + $content = file_get_contents(TESTS_DIR . "src/test.pdf"); + $encoded = file_get_contents(TESTS_DIR . "src/test.uuencode"); + + $this->runDecodeContent($content, $encoded, 3, 2000); + $this->runDecodeContent($content, $encoded, 3, 4000); + } + + /** + * Test for decodeContent() with no encoding, but formatted output + */ + function test_decode_content_formatted() + { + $content = "test \r\n plain text\tcontent\t\r\n test plain text content\t"; + $expected = "test \n plain text\tcontent\n test plain text content"; + + $this->runDecodeContent($expected, $content, 4); + } + + /** + * Helper to execute decodeCOntent() method in multiple variations of an input + * and assert with the expected output + */ + function runDecodeContent($expected, $encoded, $mode, $size = null) + { + $method = new ReflectionMethod('rcube_imap_generic', 'decodeContent'); + $method->setAccessible(true); + + // Make sure the method works with any chunk size + for ($x = 1; $x <= strlen($encoded); $x++) { + if ($size && $size != $x) { + continue; + } + + $decoded = $prev = ''; + $chunks = str_split($encoded, $x); + + foreach ($chunks as $idx => $chunk) { + $decoded .= $method->invokeArgs(null, [$chunk, $mode, $idx == count($chunks)-1, &$prev]); + } + + $this->assertSame($expected, $decoded, "Failed on chunk size of $x"); + } + } } diff --git a/tests/src/test.base64 b/tests/src/test.base64 new file mode 100644 index 000000000..41a67a835 --- /dev/null +++ b/tests/src/test.base64 @@ -0,0 +1,54 @@ +JVBERi0xLjMNCiXi48/TDQoNCjEgMCBvYmoNCjw8DQovVHlwZSAvQ2F0YWxvZw0KL091dGxpbmVz +IDIgMCBSDQovUGFnZXMgMyAwIFINCj4+DQplbmRvYmoNCg0KMiAwIG9iag0KPDwNCi9UeXBlIC9P +dXRsaW5lcw0KL0NvdW50IDANCj4+DQplbmRvYmoNCg0KMyAwIG9iag0KPDwNCi9UeXBlIC9QYWdl +cw0KL0NvdW50IDINCi9LaWRzIFsgNCAwIFIgNiAwIFIgXSANCj4+DQplbmRvYmoNCg0KNCAwIG9i +ag0KPDwNCi9UeXBlIC9QYWdlDQovUGFyZW50IDMgMCBSDQovUmVzb3VyY2VzIDw8DQovRm9udCA8 +PA0KL0YxIDkgMCBSIA0KPj4NCi9Qcm9jU2V0IDggMCBSDQo+Pg0KL01lZGlhQm94IFswIDAgNjEy +LjAwMDAgNzkyLjAwMDBdDQovQ29udGVudHMgNSAwIFINCj4+DQplbmRvYmoNCg0KNSAwIG9iag0K +PDwgL0xlbmd0aCAxMDc0ID4+DQpzdHJlYW0NCjIgSg0KQlQNCjAgMCAwIHJnDQovRjEgMDAyNyBU +Zg0KNTcuMzc1MCA3MjIuMjgwMCBUZA0KKCBBIFNpbXBsZSBQREYgRmlsZSApIFRqDQpFVA0KQlQN +Ci9GMSAwMDEwIFRmDQo2OS4yNTAwIDY4OC42MDgwIFRkDQooIFRoaXMgaXMgYSBzbWFsbCBkZW1v +bnN0cmF0aW9uIC5wZGYgZmlsZSAtICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAg +NjY0LjcwNDAgVGQNCigganVzdCBmb3IgdXNlIGluIHRoZSBWaXJ0dWFsIE1lY2hhbmljcyB0dXRv +cmlhbHMuIE1vcmUgdGV4dC4gQW5kIG1vcmUgKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjku +MjUwMCA2NTIuNzUyMCBUZA0KKCB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBB +bmQgbW9yZSB0ZXh0LiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDYyOC44NDgw +IFRkDQooIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuIEFuZCBt +b3JlIHRleHQuIEFuZCBtb3JlICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNjE2 +Ljg5NjAgVGQNCiggdGV4dC4gQW5kIG1vcmUgdGV4dC4gQm9yaW5nLCB6enp6ei4gQW5kIG1vcmUg +dGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kICkgVGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1 +MDAgNjA0Ljk0NDAgVGQNCiggbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0 +LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRm +DQo2OS4yNTAwIDU5Mi45OTIwIFRkDQooIEFuZCBtb3JlIHRleHQuIEFuZCBtb3JlIHRleHQuICkg +VGoNCkVUDQpCVA0KL0YxIDAwMTAgVGYNCjY5LjI1MDAgNTY5LjA4ODAgVGQNCiggQW5kIG1vcmUg +dGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1v +cmUgKSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA1NTcuMTM2MCBUZA0KKCB0ZXh0 +LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBFdmVuIG1vcmUuIENvbnRpbnVlZCBvbiBw +YWdlIDIgLi4uKSBUag0KRVQNCmVuZHN0cmVhbQ0KZW5kb2JqDQoNCjYgMCBvYmoNCjw8DQovVHlw +ZSAvUGFnZQ0KL1BhcmVudCAzIDAgUg0KL1Jlc291cmNlcyA8PA0KL0ZvbnQgPDwNCi9GMSA5IDAg +UiANCj4+DQovUHJvY1NldCA4IDAgUg0KPj4NCi9NZWRpYUJveCBbMCAwIDYxMi4wMDAwIDc5Mi4w +MDAwXQ0KL0NvbnRlbnRzIDcgMCBSDQo+Pg0KZW5kb2JqDQoNCjcgMCBvYmoNCjw8IC9MZW5ndGgg +Njc2ID4+DQpzdHJlYW0NCjIgSg0KQlQNCjAgMCAwIHJnDQovRjEgMDAyNyBUZg0KNTcuMzc1MCA3 +MjIuMjgwMCBUZA0KKCBTaW1wbGUgUERGIEZpbGUgMiApIFRqDQpFVA0KQlQNCi9GMSAwMDEwIFRm +DQo2OS4yNTAwIDY4OC42MDgwIFRkDQooIC4uLmNvbnRpbnVlZCBmcm9tIHBhZ2UgMS4gWWV0IG1v +cmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gKSBUag0KRVQNCkJUDQovRjEg +MDAxMCBUZg0KNjkuMjUwMCA2NzYuNjU2MCBUZA0KKCBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0 +ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSB0ZXh0LiBBbmQgbW9yZSApIFRqDQpFVA0KQlQN +Ci9GMSAwMDEwIFRmDQo2OS4yNTAwIDY2NC43MDQwIFRkDQooIHRleHQuIE9oLCBob3cgYm9yaW5n +IHR5cGluZyB0aGlzIHN0dWZmLiBCdXQgbm90IGFzIGJvcmluZyBhcyB3YXRjaGluZyApIFRqDQpF +VA0KQlQNCi9GMSAwMDEwIFRmDQo2OS4yNTAwIDY1Mi43NTIwIFRkDQooIHBhaW50IGRyeS4gQW5k +IG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4gQW5kIG1vcmUgdGV4dC4g +KSBUag0KRVQNCkJUDQovRjEgMDAxMCBUZg0KNjkuMjUwMCA2NDAuODAwMCBUZA0KKCBCb3Jpbmcu +ICBNb3JlLCBhIGxpdHRsZSBtb3JlIHRleHQuIFRoZSBlbmQsIGFuZCBqdXN0IGFzIHdlbGwuICkg +VGoNCkVUDQplbmRzdHJlYW0NCmVuZG9iag0KDQo4IDAgb2JqDQpbL1BERiAvVGV4dF0NCmVuZG9i +ag0KDQo5IDAgb2JqDQo8PA0KL1R5cGUgL0ZvbnQNCi9TdWJ0eXBlIC9UeXBlMQ0KL05hbWUgL0Yx +DQovQmFzZUZvbnQgL0hlbHZldGljYQ0KL0VuY29kaW5nIC9XaW5BbnNpRW5jb2RpbmcNCj4+DQpl +bmRvYmoNCg0KMTAgMCBvYmoNCjw8DQovQ3JlYXRvciAoUmF2ZSBcKGh0dHA6Ly93d3cubmV2cm9u +YS5jb20vcmF2ZVwpKQ0KL1Byb2R1Y2VyIChOZXZyb25hIERlc2lnbnMpDQovQ3JlYXRpb25EYXRl +IChEOjIwMDYwMzAxMDcyODI2KQ0KPj4NCmVuZG9iag0KDQp4cmVmDQowIDExDQowMDAwMDAwMDAw +IDY1NTM1IGYNCjAwMDAwMDAwMTkgMDAwMDAgbg0KMDAwMDAwMDA5MyAwMDAwMCBuDQowMDAwMDAw +MTQ3IDAwMDAwIG4NCjAwMDAwMDAyMjIgMDAwMDAgbg0KMDAwMDAwMDM5MCAwMDAwMCBuDQowMDAw +MDAxNTIyIDAwMDAwIG4NCjAwMDAwMDE2OTAgMDAwMDAgbg0KMDAwMDAwMjQyMyAwMDAwMCBuDQow +MDAwMDAyNDU2IDAwMDAwIG4NCjAwMDAwMDI1NzQgMDAwMDAgbg0KDQp0cmFpbGVyDQo8PA0KL1Np +emUgMTENCi9Sb290IDEgMCBSDQovSW5mbyAxMCAwIFINCj4+DQoNCnN0YXJ0eHJlZg0KMjcxNA0K +JSVFT0YNCg== diff --git a/tests/src/test.pdf b/tests/src/test.pdf new file mode 100644 index 000000000..dbf091df9 --- /dev/null +++ b/tests/src/test.pdf @@ -0,0 +1,198 @@ +%PDF-1.3 +%âãÏÓ + +1 0 obj +<< +/Type /Catalog +/Outlines 2 0 R +/Pages 3 0 R +>> +endobj + +2 0 obj +<< +/Type /Outlines +/Count 0 +>> +endobj + +3 0 obj +<< +/Type /Pages +/Count 2 +/Kids [ 4 0 R 6 0 R ] +>> +endobj + +4 0 obj +<< +/Type /Page +/Parent 3 0 R +/Resources << +/Font << +/F1 9 0 R +>> +/ProcSet 8 0 R +>> +/MediaBox [0 0 612.0000 792.0000] +/Contents 5 0 R +>> +endobj + +5 0 obj +<< /Length 1074 >> +stream +2 J +BT +0 0 0 rg +/F1 0027 Tf +57.3750 722.2800 Td +( A Simple PDF File ) Tj +ET +BT +/F1 0010 Tf +69.2500 688.6080 Td +( This is a small demonstration .pdf file - ) Tj +ET +BT +/F1 0010 Tf +69.2500 664.7040 Td +( just for use in the Virtual Mechanics tutorials. More text. And more ) Tj +ET +BT +/F1 0010 Tf +69.2500 652.7520 Td +( text. And more text. And more text. And more text. ) Tj +ET +BT +/F1 0010 Tf +69.2500 628.8480 Td +( And more text. And more text. And more text. And more text. And more ) Tj +ET +BT +/F1 0010 Tf +69.2500 616.8960 Td +( text. And more text. Boring, zzzzz. And more text. And more text. And ) Tj +ET +BT +/F1 0010 Tf +69.2500 604.9440 Td +( more text. And more text. And more text. And more text. And more text. ) Tj +ET +BT +/F1 0010 Tf +69.2500 592.9920 Td +( And more text. And more text. ) Tj +ET +BT +/F1 0010 Tf +69.2500 569.0880 Td +( And more text. And more text. And more text. And more text. And more ) Tj +ET +BT +/F1 0010 Tf +69.2500 557.1360 Td +( text. And more text. And more text. Even more. Continued on page 2 ...) Tj +ET +endstream +endobj + +6 0 obj +<< +/Type /Page +/Parent 3 0 R +/Resources << +/Font << +/F1 9 0 R +>> +/ProcSet 8 0 R +>> +/MediaBox [0 0 612.0000 792.0000] +/Contents 7 0 R +>> +endobj + +7 0 obj +<< /Length 676 >> +stream +2 J +BT +0 0 0 rg +/F1 0027 Tf +57.3750 722.2800 Td +( Simple PDF File 2 ) Tj +ET +BT +/F1 0010 Tf +69.2500 688.6080 Td +( ...continued from page 1. Yet more text. And more text. And more text. ) Tj +ET +BT +/F1 0010 Tf +69.2500 676.6560 Td +( And more text. And more text. And more text. And more text. And more ) Tj +ET +BT +/F1 0010 Tf +69.2500 664.7040 Td +( text. Oh, how boring typing this stuff. But not as boring as watching ) Tj +ET +BT +/F1 0010 Tf +69.2500 652.7520 Td +( paint dry. And more text. And more text. And more text. And more text. ) Tj +ET +BT +/F1 0010 Tf +69.2500 640.8000 Td +( Boring. More, a little more text. The end, and just as well. ) Tj +ET +endstream +endobj + +8 0 obj +[/PDF /Text] +endobj + +9 0 obj +<< +/Type /Font +/Subtype /Type1 +/Name /F1 +/BaseFont /Helvetica +/Encoding /WinAnsiEncoding +>> +endobj + +10 0 obj +<< +/Creator (Rave \(http://www.nevrona.com/rave\)) +/Producer (Nevrona Designs) +/CreationDate (D:20060301072826) +>> +endobj + +xref +0 11 +0000000000 65535 f +0000000019 00000 n +0000000093 00000 n +0000000147 00000 n +0000000222 00000 n +0000000390 00000 n +0000001522 00000 n +0000001690 00000 n +0000002423 00000 n +0000002456 00000 n +0000002574 00000 n + +trailer +<< +/Size 11 +/Root 1 0 R +/Info 10 0 R +>> + +startxref +2714 +%%EOF diff --git a/tests/src/test.uuencode b/tests/src/test.uuencode new file mode 100644 index 000000000..aa9cb44c2 --- /dev/null +++ b/tests/src/test.uuencode @@ -0,0 +1,71 @@ +begin 664 test.pdf +M)5!$1BTQ+C,-"B7BX\_3#0H-"C$@,"!O8FH-"CP\#0HO5'EP92`O0V%T86QO +M9PT*+T]U=&QI;F5S(#(@,"!2#0HO4&%G97,@,R`P(%(-"CX^#0IE;F1O8FH- +M"@T*,B`P(&]B:@T*/#P-"B]4>7!E("]/=71L:6YE7!E("]086=E7!E("]086=E#0HO4&%R96YT(#,@,"!2#0HO4F5S;W5R +M8V5S(#P\#0HO1F]N="`\/`T*+T8Q(#D@,"!2(`T*/CX-"B]0'0N($%N9"!M;W)E('1E>'0N($%N9"!M;W)E +M('1E>'0N($%N9"!M;W)E('1E>'0N($%N9"!M;W)E("D@5&H-"D54#0I"5`T* +M+T8Q(#`P,3`@5&8-"C8Y+C(U,#`@-C$V+C@Y-C`@5&0-"B@@=&5X="X@06YD +M(&UOGIZ>BX@06YD(&UO'0N($%N9"!M;W)E('1E>'0N("D@5&H-"D54 +M#0I"5`T*+T8Q(#`P,3`@5&8-"C8Y+C(U,#`@-38Y+C`X.#`@5&0-"B@@06YD +M(&UO"!;,"`P(#8Q,BXP,#`P(#'0N($]H+"!H;W<@8F]R:6YG('1Y<&EN +M9R!T:&ES('-T=69F+B!"=70@;F]T(&%S(&)O2X@06YD(&UO'0N(%1H92!E;F0L(&%N9"!J=7-T(&%S +M('=E;&PN("D@5&H-"D54#0IE;F1S=')E86T-"F5N9&]B:@T*#0HX(#`@;V)J +M#0I;+U!$1B`O5&5X=%T-"F5N9&]B:@T*#0HY(#`@;V)J#0H\/`T*+U1Y<&4@ +M+T9O;G0-"B]3=6)T>7!E("]4>7!E,0T*+TYA;64@+T8Q#0HO0F%S949O;G0@ +M+TAE;'9E=&EC80T*+T5N8V]D:6YG("]7:6Y!;G-I16YC;V1I;F<-"CX^#0IE +M;F1O8FH-"@T*,3`@,"!O8FH-"CP\#0HO0W)E871OF4@,3$- +M"B]2;V]T(#$@,"!2#0HO26YF;R`Q,"`P(%(-"CX^#0H-"G-T87)T>')E9@T* +-,C