mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 04:54:17 +00:00
fix(render): a number token the scanner can't read is skipped whole, so 1beginbfchar doesn't start mappings (hayro-postscript patch 2)
A number read stops at the first byte that can't be part of a number, which may be inside the token: `parse_object_or_skip` then read the rest (`beginbfchar` in `1beginbfchar`) as a token of its own, and the pairs after it became mappings. The rest of the token is now skipped with the failed number, one failure per token, as PostScript reads a run of regular characters as one token. The render test's run of unreadable tokens before the mappings is spaced: a run of `-` with nothing between them is one token now. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pq7HyDBXygTf7jcqdsjcfn
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0647a4e724
commit
fe5fa9e908
@@ -9648,8 +9648,9 @@ fn a_to_unicode_cmap_reads_its_mappings_past_a_token_it_cannot_parse() {
|
||||
let flood = format!("{body}{}", "-".repeat(1 << 16));
|
||||
assert!(CMap::parse(flood.as_bytes(), |_| None).is_some_and(|c| c.lookup_bf_string(0x2B).is_some()));
|
||||
// A run past the limit before the mappings ends the scan before them, and a CMap that read
|
||||
// no mappings, like one of unreadable tokens only, reports failure.
|
||||
let flood_first = format!("{}\n{body}", "-".repeat(1025));
|
||||
// no mappings, like one of unreadable tokens only, reports failure. (A run of `-` with
|
||||
// nothing between them is one token, as in PostScript, so these are spaced.)
|
||||
let flood_first = format!("{}\n{body}", "- ".repeat(1025));
|
||||
assert!(CMap::parse(flood_first.as_bytes(), |_| None).is_none());
|
||||
assert!(CMap::parse("-".repeat(1 << 16).as_bytes(), |_| None).is_none());
|
||||
// Unclosed strings after the mappings: the scan ends and keeps them. (That the first one,
|
||||
|
||||
Vendored
+2
-2
File diff suppressed because one or more lines are too long
Vendored
+11
@@ -1196,6 +1196,17 @@ endbfchar
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_number_run_into_an_operator_does_not_start_its_mappings() {
|
||||
// `1beginbfchar` is one token, not `1` and `beginbfchar`: the pairs after it are not
|
||||
// mappings, and the well-formed block after them still reads.
|
||||
let cmap = parse_with_preamble(
|
||||
b"1beginbfchar <41> <0058> endbfchar\n1 beginbfchar <42> <0059> endbfchar",
|
||||
);
|
||||
assert_eq!(cmap.lookup_bf_string(0x41), None);
|
||||
assert_eq!(cmap.lookup_bf_string(0x42), Some(BfString::Char('Y')));
|
||||
}
|
||||
|
||||
/// A CMap with `count` unreadable tokens, each after a space, before one `bfchar` mapping.
|
||||
fn mapping_after_unreadable_tokens(count: usize) -> Option<CMap> {
|
||||
let mut data = PREAMBLE.to_vec();
|
||||
|
||||
Vendored
+27
-1
@@ -66,13 +66,24 @@ impl<'a> Scanner<'a> {
|
||||
/// CMap name with spaces in it) instead of stopping there. Every failure moves forward, one
|
||||
/// failure per unreadable token, and what a failed read went through (an unclosed string,
|
||||
/// to the end of the data) is not read again, so a loop over this ends and reads the data
|
||||
/// about once.
|
||||
/// about once. A number that can't be read is skipped to the end of its token: its tail
|
||||
/// (`beginbfchar` in `1beginbfchar`) is not read as a token of its own.
|
||||
pub fn parse_object_or_skip(&mut self) -> Result<Object<'a>> {
|
||||
// The whitespace and comments before the token are not the token: skipped first, they
|
||||
// don't count as progress, so a token after a space fails once, not twice.
|
||||
object::skip_whitespace_and_comments(&mut self.reader);
|
||||
let start = self.reader.offset();
|
||||
// `object::read` reads a token starting with one of these bytes as a number.
|
||||
let number = matches!(
|
||||
self.reader.peek_byte(),
|
||||
Some(b'.' | b'+' | b'-' | b'0'..=b'9')
|
||||
);
|
||||
let read = object::read(&mut self.reader);
|
||||
// A number read stops at the first byte that can't be part of a number, which may be
|
||||
// inside the token; the rest of the token goes with it.
|
||||
if read.is_err() && number {
|
||||
self.reader.forward_while(reader::is_regular);
|
||||
}
|
||||
if read.is_err() && self.reader.offset() <= start {
|
||||
self.reader
|
||||
.jump(start.saturating_add(1).min(self.reader.data.len()));
|
||||
@@ -219,6 +230,21 @@ endcmap"#;
|
||||
assert!(s.at_end());
|
||||
}
|
||||
|
||||
/// PdfCraft patch (2) test: a number that can't be read fails once with the rest of its
|
||||
/// token, which is not read as a name of its own; the tokens after it still read.
|
||||
#[test]
|
||||
fn parse_object_or_skip_skips_the_rest_of_an_unreadable_number() {
|
||||
let mut s = Scanner::new(b"1beginbfchar 2.5.3x 16#zz! -x /C");
|
||||
for _ in 0..4 {
|
||||
assert!(s.parse_object_or_skip().is_err());
|
||||
}
|
||||
assert_eq!(
|
||||
s.parse_object_or_skip(),
|
||||
Ok(Object::Name(Name::new(b"C", true)))
|
||||
);
|
||||
assert!(s.at_end());
|
||||
}
|
||||
|
||||
/// PdfCraft patch (2) test: what a failed read went through is not read again. An unclosed
|
||||
/// string reads to the end of the data, so one failure leaves nothing to read; the nested
|
||||
/// `(` inside it are not each read to the end once more.
|
||||
|
||||
Reference in New Issue
Block a user