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:
aiongg
2026-10-24 09:38:10 -04:00
committed by GitHub
parent b6b0bbab08
commit fc1372a64c
21 changed files with 1966 additions and 2 deletions
Generated
-2
View File
@@ -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"
+1
View File
@@ -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" }
+1
View File
@@ -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
View File
@@ -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"
+70
View File
@@ -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
View File
@@ -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
View File
@@ -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
+7
View File
@@ -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.
+30
View File
@@ -0,0 +1,30 @@
# hayro-postscript
[![Crates.io](https://img.shields.io/crates/v/hayro-postscript.svg)](https://crates.io/crates/hayro-postscript)
[![Documentation](https://docs.rs/hayro-postscript/badge.svg)](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
View File
@@ -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!("]");
}
}
}
+127
View File
@@ -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));
}
}
+30
View File
@@ -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 {}
+179
View File
@@ -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());
}
}
+191
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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')
}