mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
fix(render): vendor hayro-postscript so a run of << in a CMap can't overflow the stack (#4363)
hayro-cmap reads CMap streams with hayro-postscript's token scanner, whose object::read skipped each `<<` and `>>` by calling itself again. A CMap holding a long run of `<` or `>` used one stack frame per pair, overflowed the stack and aborted the process. Vendor hayro-postscript 0.1.0 with read() looping over the delimiters instead (from storytold/craft-libs#263), patched in beside hayro-cmap. Four object::tests cover long `<<` and `>>` runs and the skipped delimiters, plain and nested.
This commit is contained in:
Generated
-2
@@ -2591,8 +2591,6 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "hayro-postscript"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "885c5ef0654933139a9b9546fc2c69e18d37f38aa2520f079092b1be18f1fcaa"
|
||||
|
||||
[[package]]
|
||||
name = "hayro-syntax"
|
||||
|
||||
@@ -115,6 +115,7 @@ hayro-interpret = { path = "vendor/hayro-interpret" }
|
||||
hayro = { path = "vendor/hayro" }
|
||||
hayro-syntax = { path = "vendor/hayro-syntax" }
|
||||
hayro-cmap = { path = "vendor/hayro-cmap" }
|
||||
hayro-postscript = { path = "vendor/hayro-postscript" }
|
||||
hayro-jbig2 = { path = "vendor/hayro-jbig2" }
|
||||
lopdf = { path = "vendor/lopdf" }
|
||||
winit = { path = "vendor/winit" }
|
||||
|
||||
Vendored
+1
@@ -26,6 +26,7 @@ Other resource categories retain their existing parent-chain lookup.
|
||||
| egui-wgpu | 0.36.2 | MIT OR Apache-2.0 | (1) `winit::surface_fit`: a window's surface is configured within the device's `max_texture_dimension_2d`, both sides shrunk by one factor that egui then draws at (`pixels_per_point × render_scale`), so the whole window is drawn into the surface (on DX12 it is stretched over the window and lines up with the pointer; wgpu's GL backend copies it unscaled into a corner); the longer side becomes exactly the limit; the depth and MSAA textures follow, and a recreated surface starts again from the window's own size. egui-wgpu configured the window's size as is, and `Surface::configure` panics beyond the limit: a window restored at another monitor's scale (opening an Outlook attachment on a mixed-DPI laptop asked an 8192-pixel device for 8600 × 3423, #577) or stretched across monitors closed PdfCraft, before its window appeared when it happened at start-up ([emilk/egui#8361](https://github.com/emilk/egui/issues/8361)). The published crate leaves out egui's licence files; `LICENSE-APACHE` and `LICENSE-MIT` are from the 0.36.2 tag; (2) `winit::catch_errors`: a new window's surface is configured inside wgpu error scopes (validation, out of memory, internal), and a failure is returned as `WgpuError::ConfigureSurface` (for the root window at start-up eframe returns it as `Error::Wgpu`, which PdfCraft retries with OpenGL); nothing is kept for the window. wgpu's default handler panicked instead, and eframe configures the root surface before the app is created, so a driver that can't configure one closed PdfCraft at launch (a 2015 Intel GPU through wgpu's GL backend timed out waiting for the GPU to go idle, `GpuWaitTimeout`, #519). Unchanged, so still a panic: an error in a later reconfigure (resize, a lost surface), a device lost during the first configure (wgpu reports it only to a callback), and a window shown at zero size (first configured on resize) | (1) `pdfcraft` `tests::a_window_larger_than_the_gpu_limit_gets_a_surface_within_it`, `tests::surface_fit_never_empties_or_overflows_a_surface`, and `tests::egui_wgpu_carries_the_surface_size_fit`, which keeps the patch in the build and in the paths that size a surface; (2) `pdfcraft` `tests::a_gpu_error_while_a_window_is_set_up_is_returned_not_a_panic` (a real device: an error inside `catch_errors` is returned, not a panic) and `tests::egui_wgpu_returns_a_surface_it_cannot_configure_as_an_error`, which keeps it around `add_surface`'s configure |
|
||||
| vello_common | 0.0.8 | Apache-2.0 OR MIT | (1) `clip::intersect` (the clip step every clipped path passes through) starts its row walk at the first row both paths have strips in, not the earlier of the two first rows. The walk visited every row from the top of the clip for each path (on the A1 sample's full-page canvas, about half its strip rows per stroke), and rows where one path has no strips produce no output, so the strips are unchanged (vello_common 0.1 starts at the later row too). Its cost grew with the page, and a full-page render of the A1 sample was about 30% faster; the output is byte-identical | (1) `clip::tests::intersect_walks_only_rows_both_paths_have_strips` counts the rows the walk visits (`cargo test --manifest-path vendor/vello_common/Cargo.toml`) and fails without the patch; the render crate's pixel tests run through the patched crate |
|
||||
| hayro-cmap | 0.1.0 | Apache-2.0 OR MIT | (1) malformed parts of a CMap (bad bfchar/bfrange entries, unresolvable `usecmap`, unparseable tail) are skipped instead of discarding the whole map, so search and redaction find text the edit layer can read (#3177); (2) a binary CMap's `usecmap` counts against the 16-level nesting limit as a text one does, where it passed its own depth and a self-referencing binary CMap recursed until the stack overflowed (from storytold/craft-libs#254) | (1) `hayro-cmap::tests::bad_bfchar_entry_is_skipped`, `unresolvable_usecmap_is_skipped`; (2) `tests::self_referencing_binary_usecmap_stops_at_the_nesting_limit` |
|
||||
| hayro-postscript | 0.1.0 | Apache-2.0 OR MIT | (1) the token scanner's `object::read` skips `<<` and `>>` in a loop; it called itself once per delimiter, so a CMap stream holding a long run of `<` or `>` overflowed the stack and aborted the process (from storytold/craft-libs#263) | (1) `hayro-postscript::object::tests::long_open_dict_run_does_not_overflow`, `long_close_dict_run_does_not_overflow`, `dict_delimiters_are_skipped`, `nested_dict_delimiters_are_skipped` |
|
||||
|
||||
Renderer startup errors: `egui-wgpu::RenderState::create` also wraps `Renderer::new` in
|
||||
`winit::catch_errors` when the winit feature is enabled. A rejected shader or render pipeline
|
||||
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "hayro-postscript"
|
||||
version = "0.1.0"
|
||||
Vendored
+70
@@ -0,0 +1,70 @@
|
||||
# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO
|
||||
#
|
||||
# When uploading crates to the registry Cargo will automatically
|
||||
# "normalize" Cargo.toml files for maximal compatibility
|
||||
# with all versions of Cargo and also rewrite `path` dependencies
|
||||
# to registry (e.g., crates.io) dependencies.
|
||||
#
|
||||
# If you are reading this file be aware that the original Cargo.toml
|
||||
# will likely look very different (and much more reasonable).
|
||||
# See Cargo.toml.orig for the original contents.
|
||||
|
||||
[package]
|
||||
edition = "2024"
|
||||
name = "hayro-postscript"
|
||||
version = "0.1.0"
|
||||
authors = ["Laurenz Stampfl <laurenz.stampfl@gmail.com>"]
|
||||
build = false
|
||||
autolib = false
|
||||
autobins = false
|
||||
autoexamples = false
|
||||
autotests = false
|
||||
autobenches = false
|
||||
description = "A lightweight PostScript scanner."
|
||||
readme = "README.md"
|
||||
license = "Apache-2.0 OR MIT"
|
||||
repository = "https://github.com/LaurenzV/hayro"
|
||||
resolver = "2"
|
||||
|
||||
[lib]
|
||||
name = "hayro_postscript"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[[example]]
|
||||
name = "tokenize"
|
||||
path = "examples/tokenize.rs"
|
||||
|
||||
[lints.clippy]
|
||||
collection_is_never_read = "warn"
|
||||
dbg_macro = "warn"
|
||||
debug_assert_with_mut_call = "warn"
|
||||
default_trait_access = "warn"
|
||||
doc_markdown = "warn"
|
||||
fn_to_numeric_cast_any = "warn"
|
||||
infinite_loop = "warn"
|
||||
large_stack_arrays = "warn"
|
||||
mismatching_type_param_order = "warn"
|
||||
negative_feature_names = "warn"
|
||||
redundant_feature_names = "warn"
|
||||
same_functions_in_if_condition = "warn"
|
||||
semicolon_if_nothing_returned = "warn"
|
||||
todo = "warn"
|
||||
too_many_arguments = "allow"
|
||||
unseparated_literal_suffix = "warn"
|
||||
use_self = "warn"
|
||||
wildcard_dependencies = "warn"
|
||||
|
||||
[lints.rust]
|
||||
elided_lifetimes_in_paths = "warn"
|
||||
keyword_idents_2024 = "forbid"
|
||||
missing_docs = "warn"
|
||||
non_ascii_idents = "forbid"
|
||||
non_local_definitions = "forbid"
|
||||
trivial_numeric_casts = "warn"
|
||||
unnameable_types = "warn"
|
||||
unreachable_pub = "warn"
|
||||
unsafe_op_in_unsafe_fn = "forbid"
|
||||
unused_import_braces = "warn"
|
||||
unused_lifetimes = "warn"
|
||||
unused_macro_rules = "warn"
|
||||
unused_qualifications = "warn"
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "hayro-postscript"
|
||||
version = "0.1.0"
|
||||
description = "A lightweight PostScript scanner."
|
||||
authors = { workspace = true }
|
||||
edition = { workspace = true }
|
||||
repository = { workspace = true }
|
||||
license = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
Vendored
+7
@@ -0,0 +1,7 @@
|
||||
Copyright (c) The Hayro Authors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
# hayro-postscript
|
||||
|
||||
[](https://crates.io/crates/hayro-postscript)
|
||||
[](https://docs.rs/hayro-postscript)
|
||||
|
||||
<!-- cargo-rdme start -->
|
||||
|
||||
A lightweight PostScript scanner.
|
||||
|
||||
This crate provides a scanner for tokenizing PostScript programs into typed objects.
|
||||
It currently only implements a very small subset of the PostScript language,
|
||||
with the main goal of being enough to parse CMAP files, but the scope _might_
|
||||
be expanded upon in the future.
|
||||
|
||||
The supported types include integers and real numbers, name objects, strings and arrays.
|
||||
Unsupported is anything else, including dictionaries, procedures, etc. An error
|
||||
will be returned in case any of these is encountered.
|
||||
|
||||
### Safety
|
||||
This crate forbids unsafe code via a crate-level attribute.
|
||||
|
||||
<!-- cargo-rdme end -->
|
||||
|
||||
## License
|
||||
Licensed under either of
|
||||
|
||||
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or <http://www.apache.org/licenses/LICENSE-2.0>)
|
||||
- MIT license ([LICENSE-MIT](LICENSE-MIT) or <http://opensource.org/licenses/MIT>)
|
||||
|
||||
at your option.
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
//! A small example that shows how to tokenize a PostScript program.
|
||||
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use hayro_postscript::{Number, Object, Scanner};
|
||||
use std::env;
|
||||
use std::fs;
|
||||
use std::process;
|
||||
|
||||
fn main() {
|
||||
let path = match env::args().nth(1) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
eprintln!("Usage: tokenize <file>");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let data = match fs::read(&path) {
|
||||
Ok(d) => d,
|
||||
Err(e) => {
|
||||
eprintln!("Error reading {path}: {e}");
|
||||
process::exit(1);
|
||||
}
|
||||
};
|
||||
|
||||
let mut scanner = Scanner::new(&data);
|
||||
|
||||
while !scanner.at_end() {
|
||||
match scanner.parse_object() {
|
||||
Ok(object) => {
|
||||
print_object(&object);
|
||||
println!();
|
||||
}
|
||||
Err(e) => eprintln!("Error: {e}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_object(object: &Object<'_>) {
|
||||
match object {
|
||||
Object::Number(Number::Integer(n)) => print!("Integer({n})"),
|
||||
Object::Number(Number::Real(n)) => print!("Real({n})"),
|
||||
Object::Name(name) => {
|
||||
let kind = if name.is_literal() {
|
||||
"literal"
|
||||
} else {
|
||||
"executable"
|
||||
};
|
||||
let text = name.as_str().unwrap_or("<non-ascii name>");
|
||||
print!("Name({text}, {kind})");
|
||||
}
|
||||
Object::String(s) => {
|
||||
let decoded = s.decode().unwrap_or_else(|_| Vec::new());
|
||||
print!("String({})", String::from_utf8_lossy(&decoded));
|
||||
}
|
||||
Object::Array(arr) => {
|
||||
print!("[");
|
||||
let mut inner = arr.objects();
|
||||
let mut first = true;
|
||||
while !inner.at_end() {
|
||||
if !first {
|
||||
print!(" ");
|
||||
}
|
||||
first = false;
|
||||
match inner.parse_object() {
|
||||
Ok(obj) => print_object(&obj),
|
||||
Err(e) => print!("Error({e})"),
|
||||
}
|
||||
}
|
||||
print!("]");
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+127
@@ -0,0 +1,127 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::object;
|
||||
use crate::reader::Reader;
|
||||
use crate::string;
|
||||
|
||||
/// A PostScript array object.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Array<'a> {
|
||||
data: &'a [u8],
|
||||
}
|
||||
|
||||
impl<'a> Array<'a> {
|
||||
pub(crate) fn new(data: &'a [u8]) -> Self {
|
||||
Self { data }
|
||||
}
|
||||
|
||||
/// Return a [`Scanner`](crate::Scanner) that iterates over the objects inside
|
||||
/// this array.
|
||||
pub fn objects(&self) -> crate::Scanner<'a> {
|
||||
crate::Scanner::new(self.data)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse<'a>(r: &mut Reader<'a>) -> Result<&'a [u8]> {
|
||||
r.forward_tag(b"[").ok_or(Error::SyntaxError)?;
|
||||
|
||||
let start = r.offset();
|
||||
skip_array(r)?;
|
||||
let end = r.offset() - 1;
|
||||
|
||||
r.range(start..end).ok_or(Error::SyntaxError)
|
||||
}
|
||||
|
||||
fn skip_array(r: &mut Reader<'_>) -> Result<()> {
|
||||
let mut depth = 1_u32;
|
||||
|
||||
while depth > 0 {
|
||||
match r.peek_byte().ok_or(Error::SyntaxError)? {
|
||||
b'[' => {
|
||||
r.forward();
|
||||
depth += 1;
|
||||
}
|
||||
b']' => {
|
||||
r.forward();
|
||||
depth -= 1;
|
||||
}
|
||||
b'(' => {
|
||||
let _ = string::parse_literal(r).ok_or(Error::SyntaxError)?;
|
||||
}
|
||||
b'<' => {
|
||||
if r.peek_bytes(2) == Some(b"<~") {
|
||||
let _ = string::parse_ascii85(r).ok_or(Error::SyntaxError)?;
|
||||
} else if r.peek_bytes(2) == Some(b"<<") {
|
||||
r.forward();
|
||||
r.forward();
|
||||
} else {
|
||||
let _ = string::parse_hex(r).ok_or(Error::SyntaxError)?;
|
||||
}
|
||||
}
|
||||
b'%' => object::skip_whitespace_and_comments(r),
|
||||
_ => {
|
||||
r.forward();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn parse_array(input: &[u8]) -> Result<&[u8]> {
|
||||
let mut r = Reader::new(input);
|
||||
parse(&mut r)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
assert_eq!(parse_array(b"[]").unwrap(), b"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simple() {
|
||||
assert_eq!(parse_array(b"[1 2 3]").unwrap(), b"1 2 3");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested() {
|
||||
assert_eq!(parse_array(b"[1 [2 3] 4]").unwrap(), b"1 [2 3] 4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_string() {
|
||||
// The ']' inside the string should not close the array.
|
||||
assert_eq!(parse_array(b"[1 (str]) 2]").unwrap(), b"1 (str]) 2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_hex_string() {
|
||||
assert_eq!(parse_array(b"[<48> /name]").unwrap(), b"<48> /name");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_ascii85_string() {
|
||||
assert_eq!(parse_array(b"[<~87cURDZ~> 1]").unwrap(), b"<~87cURDZ~> 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn with_comment() {
|
||||
assert_eq!(
|
||||
parse_array(b"[1 % comment with ]\n2]").unwrap(),
|
||||
b"1 % comment with ]\n2"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unterminated() {
|
||||
assert_eq!(parse_array(b"[1 2"), Err(Error::SyntaxError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn not_an_array() {
|
||||
assert_eq!(parse_array(b"1 2]"), Err(Error::SyntaxError));
|
||||
}
|
||||
}
|
||||
Vendored
+30
@@ -0,0 +1,30 @@
|
||||
//! Error types for the PostScript scanner.
|
||||
|
||||
use core::fmt;
|
||||
|
||||
/// A specialized [`Result`] type for PostScript scanner operations.
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
|
||||
/// An error encountered while scanning a PostScript token stream.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
/// A syntax error in the input.
|
||||
SyntaxError,
|
||||
/// A numeric value exceeded implementation limits.
|
||||
LimitCheck,
|
||||
/// An unsupported PostScript type was encountered (like dictionaries or
|
||||
/// procedures, which will be added in the future).
|
||||
UnsupportedType,
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::SyntaxError => f.write_str("syntaxerror"),
|
||||
Self::LimitCheck => f.write_str("limitcheck"),
|
||||
Self::UnsupportedType => f.write_str("unsupported type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::error::Error for Error {}
|
||||
Vendored
+179
@@ -0,0 +1,179 @@
|
||||
/*!
|
||||
A lightweight PostScript scanner.
|
||||
|
||||
This crate provides a scanner for tokenizing PostScript programs into typed objects.
|
||||
It currently only implements a very small subset of the PostScript language,
|
||||
with the main goal of being enough to parse CMAP files, but the scope _might_
|
||||
be expanded upon in the future.
|
||||
|
||||
The supported types include integers and real numbers, name objects, strings and arrays.
|
||||
Unsupported is anything else, including dictionaries, procedures, etc. An error
|
||||
will be returned in case any of these is encountered.
|
||||
|
||||
## Safety
|
||||
This crate forbids unsafe code via a crate-level attribute.
|
||||
*/
|
||||
|
||||
#![no_std]
|
||||
#![forbid(unsafe_code)]
|
||||
#![allow(missing_docs)]
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
mod array;
|
||||
mod error;
|
||||
mod name;
|
||||
mod number;
|
||||
mod object;
|
||||
mod reader;
|
||||
mod string;
|
||||
|
||||
pub use array::Array;
|
||||
pub use error::{Error, Result};
|
||||
pub use name::Name;
|
||||
pub use number::Number;
|
||||
pub use object::Object;
|
||||
pub use string::String;
|
||||
|
||||
use reader::Reader;
|
||||
|
||||
/// A PostScript scanner that parses [`Object`]s from a byte stream.
|
||||
pub struct Scanner<'a> {
|
||||
reader: Reader<'a>,
|
||||
}
|
||||
|
||||
impl<'a> Scanner<'a> {
|
||||
/// Create a new scanner over the given bytes of a PostScript program.
|
||||
pub fn new(data: &'a [u8]) -> Self {
|
||||
Self {
|
||||
reader: Reader::new(data),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` if there are no more objects to parse.
|
||||
pub fn at_end(&mut self) -> bool {
|
||||
object::at_end(&mut self.reader)
|
||||
}
|
||||
|
||||
/// Parse the next object.
|
||||
pub fn parse_object(&mut self) -> Result<Object<'a>> {
|
||||
object::read(&mut self.reader)
|
||||
}
|
||||
|
||||
/// Parse the next object as a [`Number`].
|
||||
pub fn parse_number(&mut self) -> Result<Number> {
|
||||
match self.parse_object()? {
|
||||
Object::Number(n) => Ok(n),
|
||||
_ => Err(Error::SyntaxError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the next object as a [`Name`].
|
||||
pub fn parse_name(&mut self) -> Result<Name<'a>> {
|
||||
match self.parse_object()? {
|
||||
Object::Name(n) => Ok(n),
|
||||
_ => Err(Error::SyntaxError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the next object as a [`String`].
|
||||
pub fn parse_string(&mut self) -> Result<String<'a>> {
|
||||
match self.parse_object()? {
|
||||
Object::String(s) => Ok(s),
|
||||
_ => Err(Error::SyntaxError),
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse the next object as an [`Array`].
|
||||
pub fn parse_array(&mut self) -> Result<Array<'a>> {
|
||||
match self.parse_object()? {
|
||||
Object::Array(a) => Ok(a),
|
||||
_ => Err(Error::SyntaxError),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn cmap_snippet() {
|
||||
let input = br#"/CIDInit /ProcSet findresource begin
|
||||
12 dict begin
|
||||
begincmap
|
||||
/CMapName /Test-H def
|
||||
1 begincodespacerange
|
||||
<00> <FF>
|
||||
endcodespacerange
|
||||
2 beginbfchar
|
||||
<03> <0041>
|
||||
<04> <0042>
|
||||
endbfchar
|
||||
endcmap"#;
|
||||
|
||||
let mut s = Scanner::new(input);
|
||||
|
||||
assert_eq!(s.parse_name().unwrap(), Name::new(b"CIDInit", true));
|
||||
assert_eq!(s.parse_name().unwrap(), Name::new(b"ProcSet", true));
|
||||
assert_eq!(s.parse_name().unwrap(), Name::new(b"findresource", false));
|
||||
assert_eq!(s.parse_name().unwrap(), Name::new(b"begin", false));
|
||||
assert_eq!(s.parse_number().unwrap(), Number::Integer(12));
|
||||
assert_eq!(s.parse_name().unwrap(), Name::new(b"dict", false));
|
||||
assert_eq!(s.parse_name().unwrap(), Name::new(b"begin", false));
|
||||
assert_eq!(s.parse_name().unwrap(), Name::new(b"begincmap", false));
|
||||
assert_eq!(s.parse_name().unwrap(), Name::new(b"CMapName", true));
|
||||
assert_eq!(s.parse_name().unwrap(), Name::new(b"Test-H", true));
|
||||
assert_eq!(s.parse_name().unwrap(), Name::new(b"def", false));
|
||||
assert_eq!(s.parse_number().unwrap(), Number::Integer(1));
|
||||
assert_eq!(
|
||||
s.parse_name().unwrap(),
|
||||
Name::new(b"begincodespacerange", false)
|
||||
);
|
||||
assert_eq!(s.parse_string().unwrap(), String::from_hex(b"00"));
|
||||
assert_eq!(s.parse_string().unwrap(), String::from_hex(b"FF"));
|
||||
assert_eq!(
|
||||
s.parse_name().unwrap(),
|
||||
Name::new(b"endcodespacerange", false)
|
||||
);
|
||||
assert_eq!(s.parse_number().unwrap(), Number::Integer(2));
|
||||
assert_eq!(s.parse_name().unwrap(), Name::new(b"beginbfchar", false));
|
||||
assert_eq!(s.parse_string().unwrap(), String::from_hex(b"03"));
|
||||
assert_eq!(s.parse_string().unwrap(), String::from_hex(b"0041"));
|
||||
assert_eq!(s.parse_string().unwrap(), String::from_hex(b"04"));
|
||||
assert_eq!(s.parse_string().unwrap(), String::from_hex(b"0042"));
|
||||
assert_eq!(s.parse_name().unwrap(), Name::new(b"endbfchar", false));
|
||||
assert_eq!(s.parse_name().unwrap(), Name::new(b"endcmap", false));
|
||||
assert!(s.at_end());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn array_round_trip() {
|
||||
let input = b"[123 /abc (xyz)]";
|
||||
let mut scanner = Scanner::new(input);
|
||||
let arr = scanner.parse_array().unwrap();
|
||||
assert!(scanner.at_end());
|
||||
|
||||
let mut inner = arr.objects();
|
||||
assert_eq!(inner.parse_number().unwrap(), Number::Integer(123));
|
||||
assert_eq!(inner.parse_name().unwrap(), Name::new(b"abc", true));
|
||||
assert_eq!(inner.parse_string().unwrap(), String::from_literal(b"xyz"));
|
||||
assert!(inner.at_end());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn comments_skipped() {
|
||||
let input = b"% comment\n42 % another\n/Name";
|
||||
let mut scanner = Scanner::new(input);
|
||||
|
||||
assert_eq!(scanner.parse_number().unwrap(), Number::Integer(42));
|
||||
assert_eq!(scanner.parse_name().unwrap(), Name::new(b"Name", true));
|
||||
assert!(scanner.at_end());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wrong_type_is_error() {
|
||||
let mut scanner = Scanner::new(b"42 ");
|
||||
assert!(scanner.parse_name().is_err());
|
||||
}
|
||||
}
|
||||
Vendored
+191
@@ -0,0 +1,191 @@
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::reader::{Reader, is_regular};
|
||||
use crate::string::ascii_hex::decode_hex_digit;
|
||||
|
||||
/// A PostScript name object.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Name<'a> {
|
||||
data: &'a [u8],
|
||||
literal: bool,
|
||||
}
|
||||
|
||||
impl<'a> Name<'a> {
|
||||
pub(crate) fn new(data: &'a [u8], literal: bool) -> Self {
|
||||
Self { data, literal }
|
||||
}
|
||||
|
||||
/// Returns `true` if this is a literal name.
|
||||
pub fn is_literal(&self) -> bool {
|
||||
self.literal
|
||||
}
|
||||
|
||||
/// Returns the name as a string if it is valid UTF-8, or
|
||||
/// `None` if it contains non-ASCII bytes.
|
||||
pub fn as_str(&self) -> Option<&'a str> {
|
||||
core::str::from_utf8(self.data).ok()
|
||||
}
|
||||
|
||||
/// Decode the name into `out`, replacing any previous contents.
|
||||
pub fn decode_into(&self, out: &mut Vec<u8>) -> Result<()> {
|
||||
out.clear();
|
||||
|
||||
// Fast path: no escape sequences.
|
||||
if !self.data.contains(&b'#') {
|
||||
out.extend_from_slice(self.data);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Slow path: Process escape sequences.
|
||||
let mut inner = Reader::new(self.data);
|
||||
|
||||
while let Some(b) = inner.read_byte() {
|
||||
if b == b'#' {
|
||||
let hex = inner.read_bytes(2).ok_or(Error::SyntaxError)?;
|
||||
let hi = decode_hex_digit(hex[0]).ok_or(Error::SyntaxError)?;
|
||||
let lo = decode_hex_digit(hex[1]).ok_or(Error::SyntaxError)?;
|
||||
out.push(hi << 4 | lo);
|
||||
} else {
|
||||
out.push(b);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Decode the name.
|
||||
pub fn decode(&self) -> Result<Vec<u8>> {
|
||||
let mut out = Vec::new();
|
||||
self.decode_into(&mut out)?;
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_literal<'a>(r: &mut Reader<'a>) -> Option<&'a [u8]> {
|
||||
r.forward_tag(b"/")?;
|
||||
let start = r.offset();
|
||||
while r.eat(is_regular).is_some() {}
|
||||
r.range(start..r.offset())
|
||||
}
|
||||
|
||||
pub(crate) fn parse_executable<'a>(r: &mut Reader<'a>) -> Option<&'a [u8]> {
|
||||
let start = r.offset();
|
||||
r.forward_while(is_regular);
|
||||
if r.offset() == start {
|
||||
return None;
|
||||
}
|
||||
r.range(start..r.offset())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn read_literal(input: &[u8]) -> Option<Name<'_>> {
|
||||
let mut r = Reader::new(input);
|
||||
parse_literal(&mut r).map(|d| Name::new(d, true))
|
||||
}
|
||||
|
||||
fn read_executable(input: &[u8]) -> Option<Name<'_>> {
|
||||
let mut r = Reader::new(input);
|
||||
parse_executable(&mut r).map(|d| Name::new(d, false))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_simple() {
|
||||
let n = read_literal(b"/Name1").unwrap();
|
||||
assert_eq!(n.as_str().unwrap(), "Name1");
|
||||
assert!(n.is_literal());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_empty_name() {
|
||||
let n = read_literal(b"/").unwrap();
|
||||
assert_eq!(n.as_str().unwrap(), "");
|
||||
assert!(n.is_literal());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_with_hex_escape() {
|
||||
let n = read_literal(b"/lime#20Green").unwrap();
|
||||
assert_eq!(n.as_str().unwrap(), "lime#20Green");
|
||||
assert_eq!(n.decode().unwrap(), b"lime Green");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_multiple_hex_escapes() {
|
||||
let n = read_literal(b"/paired#28#29parentheses").unwrap();
|
||||
assert_eq!(n.decode().unwrap(), b"paired()parentheses");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_special_chars() {
|
||||
let n = read_literal(b"/A;Name_With-Various***Characters?").unwrap();
|
||||
assert_eq!(n.as_str().unwrap(), "A;Name_With-Various***Characters?");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_stops_at_delimiter() {
|
||||
let mut r = Reader::new(b"/Name(rest");
|
||||
let data = parse_literal(&mut r).unwrap();
|
||||
assert_eq!(data, b"Name");
|
||||
assert_eq!(r.peek_byte(), Some(b'('));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_stops_at_whitespace() {
|
||||
let mut r = Reader::new(b"/Name rest");
|
||||
let data = parse_literal(&mut r).unwrap();
|
||||
assert_eq!(data, b"Name");
|
||||
assert_eq!(r.peek_byte(), Some(b' '));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_not_a_name() {
|
||||
assert!(read_literal(b"Name").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn executable_simple() {
|
||||
let n = read_executable(b"beginbfchar ").unwrap();
|
||||
assert_eq!(n.as_str().unwrap(), "beginbfchar");
|
||||
assert!(!n.is_literal());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn executable_stops_at_delimiter() {
|
||||
let mut r = Reader::new(b"def/name");
|
||||
let data = parse_executable(&mut r).unwrap();
|
||||
assert_eq!(data, b"def");
|
||||
assert_eq!(r.peek_byte(), Some(b'/'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn executable_at_eof() {
|
||||
let n = read_executable(b"endcmap").unwrap();
|
||||
assert_eq!(n.as_str().unwrap(), "endcmap");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn executable_empty() {
|
||||
assert!(read_executable(b"").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn executable_starts_at_delimiter() {
|
||||
assert!(read_executable(b"(foo)").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_no_escapes() {
|
||||
let n = Name::new(b"simple", true);
|
||||
assert_eq!(n.decode().unwrap(), b"simple");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_with_escapes() {
|
||||
let n = Name::new(b"lime#20Green", true);
|
||||
assert_eq!(n.decode().unwrap(), b"lime Green");
|
||||
}
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
use crate::error::{Error, Result};
|
||||
use crate::reader::{Reader, is_delimiter, is_whitespace};
|
||||
|
||||
/// A PostScript number object (integer or real).
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum Number {
|
||||
Integer(i32),
|
||||
Real(f32),
|
||||
}
|
||||
|
||||
impl Number {
|
||||
/// Return the value as an `i32`. Reals are truncated.
|
||||
pub fn as_i32(self) -> i32 {
|
||||
match self {
|
||||
Self::Integer(v) => v,
|
||||
Self::Real(v) => v as i32,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the value as an `f32`.
|
||||
pub fn as_f32(self) -> f32 {
|
||||
match self {
|
||||
Self::Integer(v) => v as f32,
|
||||
Self::Real(v) => v,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the value as an `f64`.
|
||||
pub fn as_f64(self) -> f64 {
|
||||
match self {
|
||||
Self::Integer(v) => v as f64,
|
||||
Self::Real(v) => v as f64,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_terminated(r: &Reader<'_>) -> bool {
|
||||
match r.peek_byte() {
|
||||
None => true,
|
||||
Some(b) => is_whitespace(b) || is_delimiter(b),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn read(r: &mut Reader<'_>) -> Result<Number> {
|
||||
let saved = r.offset();
|
||||
|
||||
// Optional sign.
|
||||
let first = r.peek_byte().ok_or(Error::SyntaxError)?;
|
||||
let has_sign = first == b'+' || first == b'-';
|
||||
|
||||
if has_sign {
|
||||
r.forward();
|
||||
}
|
||||
|
||||
// Consume leading digits.
|
||||
let digit_start = r.offset();
|
||||
r.forward_while(|b| b.is_ascii_digit());
|
||||
let has_digits = r.offset() > digit_start;
|
||||
|
||||
// Check if number is a radix number.
|
||||
if !has_sign && has_digits && r.peek_byte() == Some(b'#') {
|
||||
let base_bytes = r.range(digit_start..r.offset()).ok_or(Error::SyntaxError)?;
|
||||
let base_str = core::str::from_utf8(base_bytes).map_err(|_| Error::SyntaxError)?;
|
||||
let base = base_str.parse::<u32>().map_err(|_| Error::SyntaxError)?;
|
||||
|
||||
if !(2..=36).contains(&base) {
|
||||
return Err(Error::SyntaxError);
|
||||
}
|
||||
|
||||
// Skip `#`.
|
||||
r.forward();
|
||||
|
||||
let num_start = r.offset();
|
||||
r.forward_while(|b| b.is_ascii_alphanumeric());
|
||||
|
||||
if r.offset() == num_start || !is_terminated(r) {
|
||||
return Err(Error::SyntaxError);
|
||||
}
|
||||
|
||||
let num_bytes = r.range(num_start..r.offset()).ok_or(Error::SyntaxError)?;
|
||||
let num_str = core::str::from_utf8(num_bytes).map_err(|_| Error::SyntaxError)?;
|
||||
let value = i32::from_str_radix(num_str, base).map_err(|_| Error::SyntaxError)?;
|
||||
|
||||
return Ok(Number::Integer(value));
|
||||
}
|
||||
|
||||
// Check for real number indicators: `.` or `e`/`E`.
|
||||
let has_dot = r.peek_byte() == Some(b'.');
|
||||
|
||||
if has_dot {
|
||||
r.forward(); // skip '.'
|
||||
r.forward_while(|b| b.is_ascii_digit());
|
||||
}
|
||||
|
||||
// At this point we need at least some digits (before or after the dot).
|
||||
if !has_digits && !has_dot {
|
||||
return Err(Error::SyntaxError);
|
||||
}
|
||||
|
||||
let has_exponent = matches!(r.peek_byte(), Some(b'e' | b'E'));
|
||||
if has_exponent {
|
||||
r.forward();
|
||||
|
||||
// Optional exponent sign.
|
||||
if matches!(r.peek_byte(), Some(b'+' | b'-')) {
|
||||
r.forward();
|
||||
}
|
||||
|
||||
r.forward_while(|b| b.is_ascii_digit());
|
||||
}
|
||||
|
||||
if !is_terminated(r) {
|
||||
return Err(Error::SyntaxError);
|
||||
}
|
||||
|
||||
let token = r.range(saved..r.offset()).ok_or(Error::SyntaxError)?;
|
||||
let str = core::str::from_utf8(token).map_err(|_| Error::SyntaxError)?;
|
||||
|
||||
if has_dot || has_exponent {
|
||||
let value = str.parse::<f32>().map_err(|_| Error::SyntaxError)?;
|
||||
|
||||
Ok(Number::Real(value))
|
||||
} else {
|
||||
if !has_digits {
|
||||
return Err(Error::SyntaxError);
|
||||
}
|
||||
|
||||
let value = str.parse::<i32>().map_err(|_| Error::SyntaxError)?;
|
||||
|
||||
Ok(Number::Integer(value))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn read_num(input: &[u8]) -> Result<Number> {
|
||||
let mut r = Reader::new(input);
|
||||
read(&mut r)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn signed_integers() {
|
||||
assert_eq!(read_num(b"123 ").unwrap(), Number::Integer(123));
|
||||
assert_eq!(read_num(b"-98 ").unwrap(), Number::Integer(-98));
|
||||
assert_eq!(read_num(b"43445 ").unwrap(), Number::Integer(43445));
|
||||
assert_eq!(read_num(b"0 ").unwrap(), Number::Integer(0));
|
||||
assert_eq!(read_num(b"+17 ").unwrap(), Number::Integer(17));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn real_numbers() {
|
||||
assert_eq!(read_num(b"-.002 ").unwrap(), Number::Real(-0.002));
|
||||
assert_eq!(read_num(b"34.5 ").unwrap(), Number::Real(34.5));
|
||||
assert_eq!(read_num(b"-3.62 ").unwrap(), Number::Real(-3.62));
|
||||
assert_eq!(read_num(b"123.6e10 ").unwrap(), Number::Real(123.6e10));
|
||||
assert_eq!(read_num(b"1.0E-5 ").unwrap(), Number::Real(1.0E-5));
|
||||
assert_eq!(read_num(b"1E6 ").unwrap(), Number::Real(1E6));
|
||||
assert_eq!(read_num(b"-1. ").unwrap(), Number::Real(-1.0));
|
||||
assert_eq!(read_num(b"0.0 ").unwrap(), Number::Real(0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn radix_numbers() {
|
||||
assert_eq!(read_num(b"8#1777 ").unwrap(), Number::Integer(0o1777));
|
||||
assert_eq!(read_num(b"16#FFFE ").unwrap(), Number::Integer(0xFFFE));
|
||||
assert_eq!(read_num(b"2#1000 ").unwrap(), Number::Integer(0b1000));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid() {
|
||||
assert!(read_num(b"abc").is_err());
|
||||
assert!(read_num(b"+abc").is_err());
|
||||
assert!(read_num(b"1a").is_err());
|
||||
}
|
||||
}
|
||||
+226
@@ -0,0 +1,226 @@
|
||||
use crate::array::{self, Array};
|
||||
use crate::error::{Error, Result};
|
||||
use crate::name::{self, Name};
|
||||
use crate::number::{self, Number};
|
||||
use crate::reader::Reader;
|
||||
use crate::string::{self, String};
|
||||
|
||||
/// A PostScript object.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Object<'a> {
|
||||
/// A number object.
|
||||
Number(Number),
|
||||
/// A name object.
|
||||
Name(Name<'a>),
|
||||
/// A string object.
|
||||
String(String<'a>),
|
||||
/// An array object.
|
||||
Array(Array<'a>),
|
||||
}
|
||||
|
||||
pub(crate) fn read<'a>(r: &mut Reader<'a>) -> Result<Object<'a>> {
|
||||
// PdfCraft patch: `<<` and `>>` are skipped by looping instead of
|
||||
// recursing once per delimiter, so a long run cannot overflow the stack.
|
||||
loop {
|
||||
skip_whitespace_and_comments(r);
|
||||
|
||||
let b = r.peek_byte().ok_or(Error::SyntaxError)?;
|
||||
|
||||
return match b {
|
||||
b'(' => string::parse_literal(r)
|
||||
.map(|s| Object::String(String::from_literal(s)))
|
||||
.ok_or(Error::SyntaxError),
|
||||
b'<' => {
|
||||
if r.peek_bytes(2) == Some(b"<~") {
|
||||
string::parse_ascii85(r)
|
||||
.map(|s| Object::String(String::from_ascii85(s)))
|
||||
.ok_or(Error::SyntaxError)
|
||||
} else if r.peek_bytes(2) == Some(b"<<") {
|
||||
// TODO: Proper dict support. For now, skip `<<` and return
|
||||
// the next object so callers see the inner tokens.
|
||||
r.forward();
|
||||
r.forward();
|
||||
continue;
|
||||
} else {
|
||||
string::parse_hex(r)
|
||||
.map(|s| Object::String(String::from_hex(s)))
|
||||
.ok_or(Error::SyntaxError)
|
||||
}
|
||||
}
|
||||
b'/' => name::parse_literal(r)
|
||||
.map(|s| Object::Name(Name::new(s, true)))
|
||||
.ok_or(Error::SyntaxError),
|
||||
b'[' => array::parse(r).map(|d| Object::Array(Array::new(d))),
|
||||
b'>' => {
|
||||
if r.peek_bytes(2) == Some(b">>") {
|
||||
// TODO: Proper dict support. Skip `>>` closing delimiter.
|
||||
r.forward();
|
||||
r.forward();
|
||||
continue;
|
||||
} else {
|
||||
Err(Error::SyntaxError)
|
||||
}
|
||||
}
|
||||
b'{' => {
|
||||
r.forward();
|
||||
Err(Error::UnsupportedType)
|
||||
}
|
||||
b'.' | b'+' | b'-' | b'0'..=b'9' => number::read(r).map(Object::Number),
|
||||
_ => name::parse_executable(r)
|
||||
.map(|s| Object::Name(Name::new(s, false)))
|
||||
.ok_or(Error::SyntaxError),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn at_end(r: &mut Reader<'_>) -> bool {
|
||||
skip_whitespace_and_comments(r);
|
||||
r.peek_byte().is_none()
|
||||
}
|
||||
|
||||
pub(crate) fn skip_whitespace_and_comments(r: &mut Reader<'_>) {
|
||||
loop {
|
||||
match r.peek_byte() {
|
||||
Some(b) if crate::reader::is_whitespace(b) => {
|
||||
r.forward();
|
||||
}
|
||||
Some(b'%') => {
|
||||
r.forward();
|
||||
r.forward_while(|b| !crate::reader::is_eol(b));
|
||||
}
|
||||
_ => return,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn read_one(input: &[u8]) -> Result<Object<'_>> {
|
||||
let mut r = Reader::new(input);
|
||||
read(&mut r)
|
||||
}
|
||||
|
||||
fn read_ok(input: &[u8]) -> Object<'_> {
|
||||
read_one(input).unwrap()
|
||||
}
|
||||
|
||||
fn read_err(input: &[u8]) -> Error {
|
||||
read_one(input).unwrap_err()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn integer() {
|
||||
assert_eq!(read_ok(b"42 "), Object::Number(Number::Integer(42)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn negative_integer() {
|
||||
assert_eq!(read_ok(b"-7 "), Object::Number(Number::Integer(-7)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_name() {
|
||||
assert_eq!(
|
||||
read_ok(b"/CMapName "),
|
||||
Object::Name(Name::new(b"CMapName", true))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn executable_name() {
|
||||
let obj = read_ok(b"beginbfchar ");
|
||||
assert_eq!(obj, Object::Name(Name::new(b"beginbfchar", false)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_string() {
|
||||
assert_eq!(
|
||||
read_ok(b"(Hello)"),
|
||||
Object::String(String::from_literal(b"Hello"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_string() {
|
||||
assert_eq!(
|
||||
read_ok(b"<48656C6C6F>"),
|
||||
Object::String(String::from_hex(b"48656C6C6F"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ascii85_string() {
|
||||
assert_eq!(
|
||||
read_ok(b"<~87cURDZ~>"),
|
||||
Object::String(String::from_ascii85(b"87cURDZ"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn array_simple() {
|
||||
let obj = read_ok(b"[1 2 3]");
|
||||
assert_eq!(obj, Object::Array(Array::new(b"1 2 3")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stray_close_bracket() {
|
||||
assert_eq!(read_err(b"]"), Error::SyntaxError);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stray_gt() {
|
||||
assert_eq!(read_err(b">x"), Error::SyntaxError);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_whitespace() {
|
||||
assert_eq!(read_ok(b" \t\n 42 "), Object::Number(Number::Integer(42)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_comments() {
|
||||
assert_eq!(
|
||||
read_ok(b"% this is a comment\n42 "),
|
||||
Object::Number(Number::Integer(42))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn eof() {
|
||||
assert!(read_one(b"").is_err());
|
||||
assert!(read_one(b" ").is_err());
|
||||
assert!(read_one(b"% comment only\n").is_err());
|
||||
}
|
||||
|
||||
// PdfCraft patch: `<<` and `>>` runs are skipped without recursion.
|
||||
#[test]
|
||||
fn long_open_dict_run_does_not_overflow() {
|
||||
let input = alloc::vec![b'<'; 1_000_000];
|
||||
assert_eq!(read_err(&input), Error::SyntaxError);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long_close_dict_run_does_not_overflow() {
|
||||
let input = b">>".repeat(500_000);
|
||||
assert_eq!(read_err(&input), Error::SyntaxError);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dict_delimiters_are_skipped() {
|
||||
let mut r = Reader::new(b"<< /A 1 >>");
|
||||
assert_eq!(read(&mut r), Ok(Object::Name(Name::new(b"A", true))));
|
||||
assert_eq!(read(&mut r), Ok(Object::Number(Number::Integer(1))));
|
||||
assert_eq!(read(&mut r), Err(Error::SyntaxError));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_dict_delimiters_are_skipped() {
|
||||
let mut r = Reader::new(b"<<<< /X >>>> 7 <<\n% c\n<< >> 8");
|
||||
assert_eq!(read(&mut r), Ok(Object::Name(Name::new(b"X", true))));
|
||||
assert_eq!(read(&mut r), Ok(Object::Number(Number::Integer(7))));
|
||||
assert_eq!(read(&mut r), Ok(Object::Number(Number::Integer(8))));
|
||||
assert!(at_end(&mut r));
|
||||
}
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
use core::ops::Range;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct Reader<'a> {
|
||||
pub(crate) data: &'a [u8],
|
||||
pub(crate) offset: usize,
|
||||
}
|
||||
|
||||
impl<'a> Reader<'a> {
|
||||
#[inline]
|
||||
pub(crate) fn new(data: &'a [u8]) -> Self {
|
||||
Self { data, offset: 0 }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn jump(&mut self, offset: usize) {
|
||||
self.offset = offset;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn range(&self, range: Range<usize>) -> Option<&'a [u8]> {
|
||||
self.data.get(range)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn offset(&self) -> usize {
|
||||
self.offset
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn read_bytes(&mut self, len: usize) -> Option<&'a [u8]> {
|
||||
let v = self.peek_bytes(len)?;
|
||||
self.offset += len;
|
||||
Some(v)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn read_byte(&mut self) -> Option<u8> {
|
||||
let v = self.peek_byte()?;
|
||||
self.offset += 1;
|
||||
Some(v)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn peek_bytes(&self, len: usize) -> Option<&'a [u8]> {
|
||||
self.data.get(self.offset..self.offset + len)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn peek_byte(&self) -> Option<u8> {
|
||||
self.data.get(self.offset).copied()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn eat(&mut self, f: impl Fn(u8) -> bool) -> Option<u8> {
|
||||
let val = self.peek_byte()?;
|
||||
if f(val) {
|
||||
self.forward();
|
||||
Some(val)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn forward(&mut self) {
|
||||
self.offset += 1;
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn forward_while(&mut self, f: impl Fn(u8) -> bool) {
|
||||
while let Some(b) = self.peek_byte() {
|
||||
if f(b) {
|
||||
self.forward();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn forward_tag(&mut self, tag: &[u8]) -> Option<()> {
|
||||
self.peek_tag(tag)?;
|
||||
self.offset += tag.len();
|
||||
Some(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn peek_tag(&self, tag: &[u8]) -> Option<()> {
|
||||
let mut cloned = self.clone();
|
||||
|
||||
for b in tag.iter().copied() {
|
||||
if cloned.peek_byte() == Some(b) {
|
||||
cloned.forward();
|
||||
} else {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn skip_eol(&mut self) {
|
||||
while let Some(b) = self.peek_byte() {
|
||||
if is_eol(b) {
|
||||
self.forward();
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn is_whitespace(b: u8) -> bool {
|
||||
matches!(b, 0x00 | 0x09 | 0x0a | 0x0c | 0x0d | 0x20)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn is_delimiter(b: u8) -> bool {
|
||||
matches!(
|
||||
b,
|
||||
b'(' | b')' | b'<' | b'>' | b'[' | b']' | b'{' | b'}' | b'/' | b'%'
|
||||
)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn is_regular(b: u8) -> bool {
|
||||
!is_whitespace(b) && !is_delimiter(b)
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn is_eol(b: u8) -> bool {
|
||||
matches!(b, 0x0a | 0x0d)
|
||||
}
|
||||
+288
@@ -0,0 +1,288 @@
|
||||
mod ascii_85;
|
||||
pub(crate) mod ascii_hex;
|
||||
mod literal;
|
||||
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::error::{Error, Result};
|
||||
use crate::reader::Reader;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum StringInner<'a> {
|
||||
Literal(&'a [u8]),
|
||||
Hex(&'a [u8]),
|
||||
Ascii85(&'a [u8]),
|
||||
}
|
||||
|
||||
/// A PostScript string object.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct String<'a> {
|
||||
inner: StringInner<'a>,
|
||||
}
|
||||
|
||||
impl<'a> String<'a> {
|
||||
pub(crate) const fn from_literal(data: &'a [u8]) -> Self {
|
||||
Self {
|
||||
inner: StringInner::Literal(data),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn from_hex(data: &'a [u8]) -> Self {
|
||||
Self {
|
||||
inner: StringInner::Hex(data),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) const fn from_ascii85(data: &'a [u8]) -> Self {
|
||||
Self {
|
||||
inner: StringInner::Ascii85(data),
|
||||
}
|
||||
}
|
||||
|
||||
/// Decode the string content into `out`, replacing any previous contents.
|
||||
pub fn decode_into(&self, out: &mut Vec<u8>) -> Result<()> {
|
||||
out.clear();
|
||||
match self.inner {
|
||||
StringInner::Literal(data) => literal::decode_into(data, out),
|
||||
StringInner::Hex(data) => ascii_hex::decode_into(data, out),
|
||||
StringInner::Ascii85(data) => ascii_85::decode_into(data, out),
|
||||
}
|
||||
.ok_or(Error::SyntaxError)
|
||||
}
|
||||
|
||||
/// Decode the string content.
|
||||
pub fn decode(&self) -> Result<Vec<u8>> {
|
||||
let mut out = Vec::new();
|
||||
self.decode_into(&mut out)?;
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_literal<'a>(r: &mut Reader<'a>) -> Option<&'a [u8]> {
|
||||
let start = r.offset();
|
||||
skip_literal(r)?;
|
||||
let end = r.offset();
|
||||
// Exclude outer parentheses.
|
||||
r.range(start + 1..end - 1)
|
||||
}
|
||||
|
||||
pub(crate) fn parse_hex<'a>(r: &mut Reader<'a>) -> Option<&'a [u8]> {
|
||||
r.forward_tag(b"<")?;
|
||||
let start = r.offset();
|
||||
while let Some(b) = r.read_byte() {
|
||||
if b == b'>' {
|
||||
return r.range(start..r.offset() - 1);
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn parse_ascii85<'a>(r: &mut Reader<'a>) -> Option<&'a [u8]> {
|
||||
r.forward_tag(b"<~")?;
|
||||
let start = r.offset();
|
||||
loop {
|
||||
let b = r.read_byte()?;
|
||||
if b == b'~' {
|
||||
let end = r.offset() - 1;
|
||||
r.forward_tag(b">")?;
|
||||
return r.range(start..end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn skip_literal(r: &mut Reader<'_>) -> Option<()> {
|
||||
r.forward_tag(b"(")?;
|
||||
let mut depth = 1_u32;
|
||||
|
||||
while depth > 0 {
|
||||
let byte = r.read_byte()?;
|
||||
match byte {
|
||||
b'\\' => {
|
||||
let _ = r.read_byte()?;
|
||||
}
|
||||
b'(' => depth += 1,
|
||||
b')' => depth -= 1,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn decode_literal(input: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut r = Reader::new(input);
|
||||
let data = parse_literal(&mut r).ok_or(Error::SyntaxError)?;
|
||||
String::from_literal(data).decode()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_empty() {
|
||||
assert_eq!(decode_literal(b"()").unwrap(), b"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_simple() {
|
||||
assert_eq!(decode_literal(b"(Hello)").unwrap(), b"Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_nested_parens() {
|
||||
assert_eq!(
|
||||
decode_literal(b"(Hi (()) there)").unwrap(),
|
||||
b"Hi (()) there"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_escape_n() {
|
||||
assert_eq!(decode_literal(b"(a\\nb)").unwrap(), b"a\nb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_escape_r() {
|
||||
assert_eq!(decode_literal(b"(a\\rb)").unwrap(), b"a\rb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_escape_t() {
|
||||
assert_eq!(decode_literal(b"(a\\tb)").unwrap(), b"a\tb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_escape_b() {
|
||||
assert_eq!(decode_literal(b"(a\\bb)").unwrap(), &[b'a', 0x08, b'b']);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_escape_f() {
|
||||
assert_eq!(decode_literal(b"(a\\fb)").unwrap(), &[b'a', 0x0C, b'b']);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_escape_backslash() {
|
||||
assert_eq!(decode_literal(b"(a\\\\b)").unwrap(), b"a\\b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_escape_parens() {
|
||||
assert_eq!(decode_literal(b"(Hi \\()").unwrap(), b"Hi (");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_octal_three_digits() {
|
||||
assert_eq!(decode_literal(b"(\\053)").unwrap(), b"+");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_octal_two_digits() {
|
||||
assert_eq!(decode_literal(b"(\\36)").unwrap(), b"\x1e");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_octal_one_digit() {
|
||||
assert_eq!(decode_literal(b"(\\3)").unwrap(), b"\x03");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_line_continuation_lf() {
|
||||
assert_eq!(decode_literal(b"(Hi \\\nthere)").unwrap(), b"Hi there");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_line_continuation_cr() {
|
||||
assert_eq!(decode_literal(b"(Hi \\\rthere)").unwrap(), b"Hi there");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_line_continuation_crlf() {
|
||||
assert_eq!(decode_literal(b"(Hi \\\r\nthere)").unwrap(), b"Hi there");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_bare_eol_lf() {
|
||||
assert_eq!(decode_literal(b"(a\nb)").unwrap(), b"a\nb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_bare_eol_cr() {
|
||||
assert_eq!(decode_literal(b"(a\rb)").unwrap(), b"a\nb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn literal_bare_eol_crlf() {
|
||||
assert_eq!(decode_literal(b"(a\r\nb)").unwrap(), b"a\nb");
|
||||
}
|
||||
|
||||
fn decode_hex(input: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut r = Reader::new(input);
|
||||
let data = parse_hex(&mut r).ok_or(Error::SyntaxError)?;
|
||||
String::from_hex(data).decode()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_simple() {
|
||||
assert_eq!(decode_hex(b"<48656C6C6F>").unwrap(), b"Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_with_whitespace() {
|
||||
assert_eq!(decode_hex(b"<48 65 6C 6C 6F>").unwrap(), b"Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_odd_nibble() {
|
||||
assert_eq!(decode_hex(b"<ABC>").unwrap(), &[0xAB, 0xC0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_empty() {
|
||||
assert_eq!(decode_hex(b"<>").unwrap(), b"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_lowercase() {
|
||||
assert_eq!(decode_hex(b"<abcd>").unwrap(), &[0xAB, 0xCD]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_mixed_case() {
|
||||
assert_eq!(decode_hex(b"<aB3E>").unwrap(), &[0xAB, 0x3E]);
|
||||
}
|
||||
|
||||
fn decode_a85(input: &[u8]) -> Result<Vec<u8>> {
|
||||
let mut r = Reader::new(input);
|
||||
let data = parse_ascii85(&mut r).ok_or(Error::SyntaxError)?;
|
||||
String::from_ascii85(data).decode()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ascii85_simple() {
|
||||
// "Hello" in ASCII85 is "87cURDZ"
|
||||
assert_eq!(decode_a85(b"<~87cURDZ~>").unwrap(), b"Hello");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ascii85_empty() {
|
||||
assert_eq!(decode_a85(b"<~~>").unwrap(), b"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ascii85_z_shorthand() {
|
||||
assert_eq!(decode_a85(b"<~z~>").unwrap(), &[0, 0, 0, 0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ascii85_partial_group() {
|
||||
let result = decode_a85(b"<~87~>").unwrap();
|
||||
assert_eq!(result.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ascii85_with_whitespace() {
|
||||
assert_eq!(decode_a85(b"<~87cU RDZ~>").unwrap(), b"Hello");
|
||||
}
|
||||
}
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
// Keep in sync with `hayro-syntax/src/filter/ascii_85.rs`.
|
||||
|
||||
use crate::reader::{Reader, is_whitespace};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
pub(crate) fn decode_into(data: &[u8], out: &mut Vec<u8>) -> Option<()> {
|
||||
const POW_85: [u32; 5] = [52200625, 614125, 7225, 85, 1];
|
||||
|
||||
let mut reader = Reader::new(data);
|
||||
|
||||
let mut read_byte = || -> Option<u8> {
|
||||
loop {
|
||||
let b = reader.read_byte()?;
|
||||
|
||||
// White space characters should be ignored.
|
||||
if !is_whitespace(b) {
|
||||
return Some(b);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let flush_group = |group: &mut Vec<u8>, decoded: &mut Vec<u8>| -> Option<()> {
|
||||
let (digits, output_len): ([u32; 5], usize) = match group.len() {
|
||||
0 => return Some(()),
|
||||
1 => return None, // A single character is not valid.
|
||||
2 => (
|
||||
[group[0], group[1], b'u', b'u', b'u'].map(|b| (b - b'!') as u32),
|
||||
1,
|
||||
),
|
||||
3 => (
|
||||
[group[0], group[1], group[2], b'u', b'u'].map(|b| (b - b'!') as u32),
|
||||
2,
|
||||
),
|
||||
4 => (
|
||||
[group[0], group[1], group[2], group[3], b'u'].map(|b| (b - b'!') as u32),
|
||||
3,
|
||||
),
|
||||
5 => (
|
||||
[group[0], group[1], group[2], group[3], group[4]].map(|b| (b - b'!') as u32),
|
||||
4,
|
||||
),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let value = digits[0]
|
||||
.checked_mul(POW_85[0])?
|
||||
.checked_add(digits[1].checked_mul(POW_85[1])?)?
|
||||
.checked_add(digits[2].checked_mul(POW_85[2])?)?
|
||||
.checked_add(digits[3].checked_mul(POW_85[3])?)?
|
||||
.checked_add(digits[4])?;
|
||||
|
||||
decoded.extend_from_slice(&value.to_be_bytes()[..output_len]);
|
||||
group.clear();
|
||||
Some(())
|
||||
};
|
||||
|
||||
out.reserve(data.len() * 4 / 5);
|
||||
let mut group = Vec::with_capacity(5);
|
||||
|
||||
loop {
|
||||
let Some(b) = read_byte() else {
|
||||
// Be lenient and accept what we have (see PDFBOX-5910).
|
||||
flush_group(&mut group, out)?;
|
||||
|
||||
return Some(());
|
||||
};
|
||||
|
||||
match b {
|
||||
b'!'..=b'u' => {
|
||||
group.push(b);
|
||||
|
||||
if group.len() == 5 {
|
||||
flush_group(&mut group, out)?;
|
||||
}
|
||||
}
|
||||
b'z' => {
|
||||
flush_group(&mut group, out)?;
|
||||
out.extend_from_slice(&[0, 0, 0, 0]);
|
||||
}
|
||||
b'~' => {
|
||||
// Technically requires a '>', but there is a PDF where it isn't
|
||||
// appended and decodes fine in other viewers.
|
||||
flush_group(&mut group, out)?;
|
||||
|
||||
return Some(());
|
||||
}
|
||||
_ => return None, // Invalid character.
|
||||
}
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
// Keep in sync with `hayro-syntax/src/filter/ascii_hex.rs`.
|
||||
|
||||
use crate::reader::is_whitespace;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
pub(crate) fn decode_into(data: &[u8], out: &mut Vec<u8>) -> Option<()> {
|
||||
let has_whitespace = data.iter().any(|&b| is_whitespace(b));
|
||||
|
||||
out.reserve(data.len().div_ceil(2));
|
||||
|
||||
if !has_whitespace {
|
||||
// Fast path, don't need to worry about white spaces.
|
||||
let mut i = 0;
|
||||
while i + 1 < data.len() {
|
||||
out.push(decode_hex_digit(data[i])? << 4 | decode_hex_digit(data[i + 1])?);
|
||||
i += 2;
|
||||
}
|
||||
if i < data.len() {
|
||||
out.push(decode_hex_digit(data[i])? << 4);
|
||||
}
|
||||
} else {
|
||||
// Slow path, need to strip white spaces.
|
||||
let mut iter = data.iter().copied();
|
||||
let mut read_byte = || -> Option<u8> {
|
||||
loop {
|
||||
let b = iter.next()?;
|
||||
if !is_whitespace(b) {
|
||||
return Some(b);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
loop {
|
||||
match (read_byte(), read_byte()) {
|
||||
(Some(hi), Some(lo)) => {
|
||||
out.push(decode_hex_digit(hi)? << 4 | decode_hex_digit(lo)?);
|
||||
}
|
||||
(Some(hi), None) => {
|
||||
out.push(decode_hex_digit(hi)? << 4);
|
||||
|
||||
break;
|
||||
}
|
||||
(None, _) => break,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn decode_hex_digit(c: u8) -> Option<u8> {
|
||||
match c {
|
||||
b'0'..=b'9' => Some(c - b'0'),
|
||||
b'A'..=b'F' => Some(c - b'A' + 10),
|
||||
b'a'..=b'f' => Some(c - b'a' + 10),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
// Keep in sync with `hayro-syntax/src/object/string.rs` (`read_literal`).
|
||||
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::reader::Reader;
|
||||
|
||||
pub(crate) fn decode_into(data: &[u8], out: &mut Vec<u8>) -> Option<()> {
|
||||
let mut r = Reader::new(data);
|
||||
|
||||
while let Some(byte) = r.read_byte() {
|
||||
match byte {
|
||||
b'\\' => {
|
||||
let next = r.read_byte()?;
|
||||
|
||||
if is_octal_digit(next) {
|
||||
let second = r.read_byte();
|
||||
let third = r.read_byte();
|
||||
|
||||
let bytes = match (second, third) {
|
||||
(Some(n1), Some(n2)) => match (is_octal_digit(n1), is_octal_digit(n2)) {
|
||||
(true, true) => [next, n1, n2],
|
||||
(true, _) => {
|
||||
r.jump(r.offset() - 1);
|
||||
[b'0', next, n1]
|
||||
}
|
||||
_ => {
|
||||
r.jump(r.offset() - 2);
|
||||
[b'0', b'0', next]
|
||||
}
|
||||
},
|
||||
(Some(n1), None) => {
|
||||
if is_octal_digit(n1) {
|
||||
[b'0', next, n1]
|
||||
} else {
|
||||
r.jump(r.offset() - 1);
|
||||
[b'0', b'0', next]
|
||||
}
|
||||
}
|
||||
_ => [b'0', b'0', next],
|
||||
};
|
||||
|
||||
let str = core::str::from_utf8(&bytes).unwrap();
|
||||
|
||||
if let Ok(num) = u8::from_str_radix(str, 8) {
|
||||
out.push(num);
|
||||
}
|
||||
} else {
|
||||
match next {
|
||||
b'n' => out.push(0xA),
|
||||
b'r' => out.push(0xD),
|
||||
b't' => out.push(0x9),
|
||||
b'b' => out.push(0x8),
|
||||
b'f' => out.push(0xC),
|
||||
b'(' => out.push(b'('),
|
||||
b')' => out.push(b')'),
|
||||
b'\\' => out.push(b'\\'),
|
||||
b'\n' | b'\r' => {
|
||||
// "If the \ is followed immediately by a newline
|
||||
// (CR, LF, or CR-LF pair), the scanner ignores
|
||||
// both the initial \ and the newline; this breaks
|
||||
// a string into multiple lines without including
|
||||
// the newline character as part of the string."
|
||||
r.skip_eol();
|
||||
}
|
||||
_ => out.push(next),
|
||||
}
|
||||
}
|
||||
}
|
||||
b'(' | b')' => out.push(byte),
|
||||
// "But if a newline appears without a preceding \, the result is
|
||||
// equivalent to \n."
|
||||
b'\n' | b'\r' => {
|
||||
out.push(b'\n');
|
||||
r.skip_eol();
|
||||
}
|
||||
other => out.push(other),
|
||||
}
|
||||
}
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn is_octal_digit(byte: u8) -> bool {
|
||||
matches!(byte, b'0'..=b'7')
|
||||
}
|
||||
Reference in New Issue
Block a user