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:
mtlcl410
2026-11-07 08:26:11 +08:00
co-authored by Claude Opus 5.5
parent 0647a4e724
commit fe5fa9e908
4 changed files with 43 additions and 5 deletions
+3 -2
View File
@@ -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,
+2 -2
View File
File diff suppressed because one or more lines are too long
+11
View File
@@ -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();
+27 -1
View File
@@ -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.