mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-30 13:08:48 -06:00
Merge PR #2267: fix(filters): preserve output-limit errors in raw Flate fallback
ROADMAP.md: next's revision history plus this PR's row; the #2267 refresh progress line dropped docs/roadmap.md: next's revision history plus this PR's row; refresh wording dropped from the status line and the M1 row
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
**Stage: alpha** · next: beta, ~25% (ready for real work 50% → 75%) and ~400–700 h away
|
||||
|
||||
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M2/gap 12: the Annex D.2 alias codes read as the ink they draw in both tables; the text oracle scores reading order; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M1: raw-deflate fallback preserves decoding-limit errors; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-14 · **Change:** minor (M14: the Italian catalog has every entry the other catalogs had and the code looks up; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M3 #2080 candidate: explicit search jumps join per-tab history; rail and control-channel Back/Forward; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (M4 #1910: Organize Pages opens on the current page; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
@@ -168,6 +169,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
|
||||
- **2026-10-15 (M14, gap 24):** `misc.localization` and `misc.installers` move from planned to partial. Localization: 22 catalogs ship, but Chinese shows tofu in releases, Telugu has no release font and Arabic is not mirrored. Installers: macOS and Windows releases are signed, Linux packages are not. The Acrobat menu diff itself remains open. Estimates unchanged.
|
||||
- **2026-10-13 (G14, Vietnamese localization):** Added Vietnamese (`vi`, Tiếng Việt) as an interface and date language with a 2,591-entry catalog (`plural_none`) covering 100% of the current localizable key union (plus `theme`/`Appearance`). Locale tags `vi` / `vi-VN` / `vi_VN.UTF-8` resolve; calendar months use numeric `Tháng 1`…`12` / `tháng 1`…`12`. Native-speaker community review recorded. The twelve-language localization mean moves from ≈59% to ≈68%; weighted readiness ≈49% → ≈50%. Status stays `partial`: engine/OS details, document contents, filenames, CLI/MCP and external help stay outside the catalog by design.
|
||||
|
||||
- **2026-10-14 (M1, Flate fallback):** Raw deflate whose first bytes resemble a zlib header no longer loses a decoding-limit error during fallback. Strict and tolerant filter regressions and a COS stream regression cover refusal at the caller limit and successful decoding within it. No parity status or estimate changes.
|
||||
|
||||
- **2026-10-12 (M6, XML field names):** AcroForm XML data export constrains generated element names to XML 1.0 NCNames, so Latin-1 names such as `Area²` and `Aµ` produce readable data files. Original PDF field names travel through the existing `xfdf:original` attribute; previously valid tags, including Greek and CJK, keep their spelling. Saved-PDF regressions cover invalid prefixes, combining letters and normalized-name collisions. Estimates unchanged.
|
||||
- **2026-10-12 (M6, CSV carriage returns):** Form-data CSV export quotes carriage returns in field names and values, so a trailing carriage return cannot turn into a record separator and disappear on reimport. Saved-PDF round-trip regressions cover CR, LF, CRLF, commas and quotes; estimates unchanged.
|
||||
- **2026-10-13 (community, M14, #1553 review):** Relative Fontconfig includes resolve beside each including config, including nested configuration selected through `FONTCONFIG_PATH`. Directory includes accept only digit-prefixed `*.conf` in lexical order. Isolated regressions reproduce both review findings; configuration size/count/depth limits, cycles, symlinked config and valid reset ordering are covered. Estimates unchanged.
|
||||
@@ -496,6 +499,7 @@ Newest first: the date, what moved, and the new overall estimate.
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | M9: separate page validation overlays preserve signature artwork, and snapshot tabs reuse instead of multiplying; baked captions remain unchanged (#79 open); estimates unchanged |
|
||||
| 2026-10-14 | minor | M1: raw-deflate fallback preserves decoding-limit errors in strict and tolerant decoding; synthetic filter and COS stream regressions. Estimates unchanged. |
|
||||
| 2026-10-12 | minor | M6: valid AcroForm XML element names with original PDF field names preserved; saved-PDF collision and Unicode regressions. Estimates unchanged. |
|
||||
| 2026-10-12 | minor | M6: preserve carriage returns in exported CSV field names and values; saved-PDF round-trip coverage. Estimates unchanged. |
|
||||
| 2026-10-13 | minor | M4: Insert-from-file preserves source bookmarks; integration tests retained with current main. Estimates unchanged. |
|
||||
|
||||
@@ -482,6 +482,19 @@ mod tests {
|
||||
assert_eq!(stream.decoded_strict().unwrap(), b"abc");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_flate_fallback_streams_preserve_the_caller_limit() {
|
||||
// Raw deflate with a stored byte and a final empty block. 78 01 also
|
||||
// passes the zlib header check, so decoding must retry the whole input.
|
||||
let mut dict = Dict::new();
|
||||
dict.set(b"Filter".to_vec(), Object::name("FlateDecode"));
|
||||
let stream = Stream::from_raw(dict, vec![0x78, 0x01, 0x00, 0xfe, 0xff, b'A', 0x01, 0x00, 0x00, 0xff, 0xff]);
|
||||
let result = stream.decoded_within(0);
|
||||
assert!(matches!(&result, Err(CosError::Filter(detail)) if detail.contains("exceeds the limit of 0 bytes")), "{result:?}");
|
||||
assert_eq!(stream.decoded_within(1).unwrap(), b"A");
|
||||
assert_eq!(stream.decoded_strict().unwrap(), b"A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn first_filter_decodes_only_the_outer_wrapper() {
|
||||
let data = "0 0 m 100 100 l S\n".repeat(64).into_bytes();
|
||||
|
||||
@@ -24,7 +24,7 @@ encode_flate(data) -> Vec<u8> // zlib, level 6
|
||||
|
||||
- `chain` is applied first to last, as in a `/Filter` array.
|
||||
- Image codecs (DCT, JPX, JBIG2, CCITT), `Crypt` and unknown filters anywhere in a chain give `Unsupported`: callers keep such data encoded. Decryption belongs to `crypt`.
|
||||
- `max_output` bounds every intermediate buffer (including the pre-predictor buffer) and gives `LimitExceeded(max_output)`.
|
||||
- `max_output` bounds every intermediate buffer (including the pre-predictor buffer) and gives `LimitExceeded(max_output)`. This also holds when Flate retries raw deflate after a misleading zlib header, in both strict and tolerant decoding.
|
||||
- `decode_tolerant` turns `Corrupt` into "what was decoded so far" plus `partial = true`, then runs the rest of the chain on it (predictors are applied to partial output too). `LimitExceeded` and `Unsupported` are still errors.
|
||||
|
||||
## Behaviour
|
||||
|
||||
@@ -26,7 +26,12 @@ pub(crate) fn decode(data: &[u8], max: usize) -> Step {
|
||||
Some((_, flg)) if flg & 0x20 != 0 => Err(Failure::corrupt(NAME, "zlib preset dictionary is not allowed in PDF", Vec::new())),
|
||||
Some(_) => match inflate_raw(&data[2..], max) {
|
||||
// A header that only looked valid: retry the whole input as raw deflate.
|
||||
Err(f) if f.partial.is_empty() && matches!(f.error, crate::FilterError::Corrupt { .. }) => inflate_raw(data, max).map_err(|_| f),
|
||||
Err(f) if f.partial.is_empty() && matches!(f.error, crate::FilterError::Corrupt { .. }) => match inflate_raw(data, max) {
|
||||
// Keep the original corruption diagnostic only if the retry is corrupt
|
||||
// too. A limit error must remain fatal even for tolerant decoding.
|
||||
Err(retry) if matches!(retry.error, crate::FilterError::Corrupt { .. }) => Err(f),
|
||||
retry => retry,
|
||||
},
|
||||
r => r,
|
||||
},
|
||||
None => inflate_raw(data, max),
|
||||
|
||||
@@ -295,6 +295,26 @@ mod tests {
|
||||
assert_eq!(decode(&chain, &hex, 100).unwrap().len(), 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_flate_fallback_preserves_output_limits() {
|
||||
// RFC 1951 stored blocks: the first is nonfinal with LEN=257 and
|
||||
// NLEN=!257, followed by a final empty block. Its ignored alignment
|
||||
// bits make the first two bytes also look like a valid zlib header.
|
||||
let data = vec![b'A'; 257];
|
||||
let mut raw = vec![0x78, 0x01, 0x01, 0xfe, 0xfe];
|
||||
raw.extend_from_slice(&data);
|
||||
raw.extend_from_slice(&[0x01, 0x00, 0x00, 0xff, 0xff]);
|
||||
let chain = [(Filter::Flate, Params::default())];
|
||||
for max in [0, 1, 128, 256] {
|
||||
assert!(matches!(decode(&chain, &raw, max), Err(FilterError::LimitExceeded(m)) if m == max));
|
||||
assert!(matches!(decode_tolerant(&chain, &raw, max), Err(FilterError::LimitExceeded(m)) if m == max));
|
||||
}
|
||||
for max in [257, 258] {
|
||||
assert_eq!(decode(&chain, &raw, max).unwrap(), data);
|
||||
assert_eq!(decode_tolerant(&chain, &raw, max).unwrap(), (data.clone(), false));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tolerant_flate_returns_partial() {
|
||||
let data: Vec<u8> = (0..20_000u32).map(|i| (i % 97) as u8 ^ (i / 300) as u8).collect();
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Where PdfCraft falls short of Acrobat Pro
|
||||
|
||||
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (gap 12: reading order is scored, not just word F1) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M1: raw-deflate fallback preserves decoding-limit errors; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-13 · **Change:** minor (gap 14: Vietnamese catalog shipped; Hindi/Indonesian/Korean and Arabic RTL remain) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (gap 6: #1910 Organize Pages opens on the current page; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (gap 6: #1398 stamps read upright on rotated pages; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
@@ -140,6 +141,9 @@ update the numbers.
|
||||
(#1398). Another batch keeps a spaced XFA radio value in the datasets (#1319), replaces only the
|
||||
selected pages when they are not adjacent (#1305), and leaves unrelated page streams in place
|
||||
when a figure is marked decorative (#1324). The rest of the backlog is untriaged.
|
||||
- **Fix verified (2026-10-14):** raw-deflate fallback returns a decoding-limit error instead of
|
||||
accepting an empty partial stream when a misleading zlib header triggers a retry. Synthetic
|
||||
filter and COS stream regressions cover refusal at the limit and decoding within it.
|
||||
- **Impact:** every one is a user who lost trust. Crash reports outrank feature work
|
||||
(`AGENTS.md` §4).
|
||||
- **Estimate:** 45–80 h (≈ 0.2–0.4 h per issue). **Doc:** this file.
|
||||
@@ -379,6 +383,7 @@ update the numbers.
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-15 | minor | Gap 12: the text oracle scores reading order as well as word F1, which is a bag of words and cannot see order; the first measurement separated 18 improvements from 6 regressions in a change F1 called unchanged on 26 of 29 files. Estimates unchanged. |
|
||||
| 2026-10-14 | minor | M1: raw-deflate fallback preserves decoding-limit errors in strict and tolerant decoding; synthetic filter and COS stream regressions. Estimates unchanged. |
|
||||
| 2026-10-15 | patch | Gap 24: record localization and installers as partial (unusable Chinese, Telugu and Arabic; unsigned Linux packages) |
|
||||
| 2026-10-13 | minor | Gap 6: Organize Pages selects and reveals the current page on open, including after an earlier grid selection elsewhere (#1910). Estimates unchanged. |
|
||||
| 2026-10-12 | minor | Gap 6: rubber stamps, explicit image rectangles and PDF-page stamps read upright on rotated pages (#1398). Estimates unchanged. |
|
||||
|
||||
+3
-1
@@ -1,5 +1,6 @@
|
||||
# PdfCraft roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M1: raw-deflate fallback preserves decoding-limit errors; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-14 · **Change:** minor (M14: the Italian catalog has every entry the other catalogs had and the code looks up, 2,528 entries; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-13 · **Change:** minor (M2/M6: spot-colour images convert once per pixel value; widget placement and the Fields panel scale with the field count; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M3 candidate: coordinate control clicks on slow frames and coalesced text selection; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
@@ -97,7 +98,7 @@ total matches the parity estimate.
|
||||
| M | Milestone | Plan size (h) | Done | Remaining (h, 2026-10-10) | Notes |
|
||||
|---|---|---|---|---|---|
|
||||
| M0 | Skeleton: workspace, xtask gates, CI | 15–30 | 98% | 1–2 | GitHub workflow, `deny.toml` (licence audit of every dependency), parity checklist (826 features, `xtask parity`) done. Missing: remaining crate stubs, testkit/oracle crates |
|
||||
| M1 | COS: filters, crypt, parser, xref, writer | 120–200 | 80% | 20–40 | Done:<br>- filters and crypt: every standard-security revision R2–R6 (RC4, AES-128/256), SASLprep, permissions, creating encryption;<br>- cos: parse and repair (including invalid xref object-number ranges), decoded-stream limits including unfiltered data, decrypt on load, re-encrypt on save, incremental and full writing.<br>Corpus: open/edit/save passes on 958 files, and all 7 password-protected files open.<br>Full saves now pack objects into compressed object streams. An unchanged incremental save of a classic table writes a zero-length cross-reference subsection instead of a section with no subsections (#1339). A full rewrite keeps unknown trailer entries, including objects reachable only from them (#1338). Fuzzing runs nightly (`xtask fuzz`). Missing: ≥ 250 tests, own image codecs |
|
||||
| M1 | COS: filters, crypt, parser, xref, writer | 120–200 | 80% | 20–40 | Done:<br>- filters and crypt: every standard-security revision R2–R6 (RC4, AES-128/256), SASLprep, permissions, creating encryption;<br>- cos: parse and repair (including invalid xref object-number ranges), decoded-stream limits including unfiltered data, decrypt on load, re-encrypt on save, incremental and full writing.<br>Corpus: open/edit/save passes on 958 files, and all 7 password-protected files open.<br>Full saves now pack objects into compressed object streams. An unchanged incremental save of a classic table writes a zero-length cross-reference subsection instead of a section with no subsections (#1339). A full rewrite keeps unknown trailer entries, including objects reachable only from them (#1338). Fuzzing runs nightly (`xtask fuzz`). Raw-deflate fallback preserves decoding-limit errors, including through tolerant COS stream decoding. Missing: ≥ 250 tests, own image codecs |
|
||||
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. Page marks, added content and print output retain small page-unit transform coefficients when serialized. |
|
||||
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. The renderer keeps its one-point crop-size floor in physical points after UserUnit scaling, with regression coverage for crop origins, rotations and f32 reciprocal rounding. Existing unit policy and nearly-zero fallback are retained. |
|
||||
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings |
|
||||
@@ -180,6 +181,7 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-15 | minor | M9 #1895: generic appearance rewriting deferred; separate validation overlays preserve original artwork, exports and printing. Baked captions remain unchanged (#79 open). Estimates unchanged. |
|
||||
| 2026-10-14 | minor | M1: raw-deflate fallback preserves decoding-limit errors in strict and tolerant decoding; synthetic filter and COS stream regressions. Estimates unchanged. |
|
||||
| 2026-10-12 | minor | M6: valid AcroForm XML element names with original PDF field names preserved; saved-PDF collision and Unicode regressions. Estimates unchanged. |
|
||||
| 2026-10-12 | minor | M6: preserve carriage returns in exported CSV field names and values; saved-PDF round-trip coverage. Estimates unchanged. |
|
||||
| 2026-10-12 | minor | M7 local candidate: text drafts grow and save their width; single visual rows grow across style boundaries; both text tools retype added items and Add Text edits source text. Added-item textless previews are read-only and covered by UI/tool regressions. Estimates unchanged. |
|
||||
|
||||
@@ -118,8 +118,8 @@ milestone = "M1"
|
||||
status = "shipped"
|
||||
commands = []
|
||||
tools = []
|
||||
evidence = ["crates/filters/src/lib.rs::chain_three_filters_with_predictor", "crates/filters/src/lzw.rs::spec_example", "crates/filters/src/predictor.rs::png_hand_vectors"]
|
||||
notes = ""
|
||||
evidence = ["crates/filters/src/lib.rs::chain_three_filters_with_predictor", "crates/filters/src/lzw.rs::spec_example", "crates/filters/src/predictor.rs::png_hand_vectors", "crates/filters/src/lib.rs::raw_flate_fallback_preserves_output_limits", "crates/cos/src/object.rs::raw_flate_fallback_streams_preserve_the_caller_limit"]
|
||||
notes = "Raw-deflate fallback preserves output limits in strict and tolerant decoding, including misleading zlib headers. Image codecs remain outside this general-purpose filter implementation."
|
||||
|
||||
[[feature]]
|
||||
id = "core.image-filters"
|
||||
|
||||
Reference in New Issue
Block a user