qpdf-compress 0.8.1 → 0.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,11 +6,19 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/), and this
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
- ## [0.8.1] - 2026-09-23
9
+ ## [0.8.2] - 2026-09-28
10
10
 
11
11
  ### Fixed
12
12
 
13
- - **The release workflow published releases without their prebuilt binaries.** `gh release create <tag> <assets>` publishes the release and then uploads, and GitHub's immutable releases reject an upload to a published release — with a 422 that `gh` does not surface, so the step went green with nothing attached. `install.mjs` reads the prebuilts off the release, so every consumer of such a version is sent to a source build. The release is now created as a draft with the tarballs attached and published afterwards, and a following step fails the job unless every artifact made it onto the release. **0.8.0 is affected and should be skipped** — it installs only where a C++ toolchain is present. It is deprecated on npm in favour of this release.
13
+ - **The content-stream minifier could hang on a form feed or vertical tab.** `minifyContentStreams` skipped only four whitespace bytes (space, tab, CR, LF) in its dispatcher, while its token loops stopped on `std::isspace`, which also counts form feed (`0x0C`) and vertical tab (`0x0B`) — both legal PDF whitespace. A form feed between two operators (a valid content stream such as `q\fQ`) therefore reached the token branch, which consumed nothing, so the cursor never advanced and the loop appended a space forever until the process ran out of memory. It runs on a libuv worker thread, so a caller's `try`/`catch` cannot interrupt it — a single crafted document is a denial of service. The dispatcher now skips whatever `std::isspace` recognises, and the token branch guarantees forward progress on any byte no branch consumes (a stray unbalanced `)` stalled it the same way), so no input can wedge the loop.
14
+
15
+ - **Correction to the 0.8.1 release notes.** 0.8.1 shipped with an entry claiming that 0.8.0 had been published without its prebuilt binaries and should be skipped. That was wrong. The REST `assets` array reads empty for releases flagged immutable, and an empty array was mistaken for missing files. Every 0.8.0 tarball is attached and downloadable, each one's SHA-256 matches the pin shipped inside the npm package, and `npm install qpdf-compress@0.8.0` installs the prebuilt binary without compiling anything. **0.8.0 is fine to use and is not deprecated.**
16
+
17
+ ## [0.8.1] - 2026-09-23
18
+
19
+ ### Changed
20
+
21
+ - The release workflow attaches the prebuilt tarballs to a draft release and publishes it afterwards, instead of publishing first and uploading after: immutable releases reject an upload to an already-published release, so the draft ordering keeps the upload clear of that edge. A following step then confirms every tarball is downloadable from the release before the job succeeds.
14
22
 
15
23
  ### Added
16
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "qpdf-compress",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "Native PDF compression for Node.js, powered by QPDF",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,11 +1,11 @@
1
1
  {
2
- "darwin-arm64": "6835d46768f1fc7366bbc60818dff396ca03c8ffb59b80e94454ca9dd5104fbf",
3
- "darwin-x64": "dc222533f2ece1d5295f40f354a7fdd0846c2141a94ece211151cee6ceb45f70",
4
- "linux-arm": "a0458de5782d4f900fba4de00cad8a678eb0a416d6cfbb153e82002446e941aa",
5
- "linux-arm64": "ee25a8cd3ec974e6055206ecdfa96a5e5d4bc218885640b5739b149b734fe022",
6
- "linux-musl-arm64": "f95653c36036947378834fa5715a55dd4ab8b0b6245a8bee95b91b280d3dda2b",
7
- "linux-musl-x64": "7fdc5b849c9d4c8c7518490c274a1814c612b999e5752521f56b9e2253a9f37f",
8
- "linux-x64": "a75e4213e1309ee49ed91d0c1e5aa591954db2c84fae0768fb1ec718a65b4a11",
9
- "win32-arm64": "30d895806eec45f194c89b3fc1b521de164b1a2dd91adb8a7a939254baa0100d",
10
- "win32-x64": "9b294de489115c6f8cb5733db260c12904f956e1ec30275776fc29cfcab600dc"
2
+ "darwin-arm64": "a2fda84bd3569abf5a39b7ce30a58652b8189751cedbaf943e391c9f0b45646c",
3
+ "darwin-x64": "a908f0fe57b78a10e7dfa461a395da050da5499715f5aae2cf793dfe7a982e5f",
4
+ "linux-arm": "39531172aa069e69e3b7db3ffc2462572fddf2a725d2f9ac9abca06513111184",
5
+ "linux-arm64": "1216bea0d7a82fe47bcf3f77f3219f6f3a1620480ed42b3f3a9d0d688f88dbd3",
6
+ "linux-musl-arm64": "d0fe61e513147dced9454a590fe4c17a8f39c0ed9ed858b3f9d44d7b33ec0e86",
7
+ "linux-musl-x64": "865748574c08bdd74decdb7c9acd91b7a0c7e4269e3cb48811d8a9b8c654ff17",
8
+ "linux-x64": "2e766fd34ee2ff86d0f4e8f02008b08507b4d59081bea68185dd78e1341be91f",
9
+ "win32-arm64": "c6793d96feb0a272ced9ba18ec97b29dce6075fcef9692f095defa699252f657",
10
+ "win32-x64": "fcc1c299080e87a5932d5dbe7fcb2ec5c849cf98a094eff55f8408ed1ccce524"
11
11
  }
package/src/content.cc CHANGED
@@ -90,8 +90,10 @@ void minifyContentStreams(QPDF &qpdf) {
90
90
  while (pos < raw.size()) {
91
91
  char ch = raw[pos];
92
92
 
93
- // skip whitespace
94
- if (ch == ' ' || ch == '\t' || ch == '\r' || ch == '\n') {
93
+ // skip whitespace — use the same predicate the token loops below use
94
+ // (std::isspace), so a form-feed or vertical-tab can never fall through
95
+ // to the token branch and stall the tokenizer.
96
+ if (std::isspace(static_cast<unsigned char>(ch))) {
95
97
  if (!minified.empty())
96
98
  needSpace = true;
97
99
  ++pos;
@@ -203,10 +205,6 @@ void minifyContentStreams(QPDF &qpdf) {
203
205
 
204
206
  // regular token (number, operator)
205
207
  {
206
- if (needSpace) {
207
- minified += ' ';
208
- needSpace = false;
209
- }
210
208
  size_t start = pos;
211
209
  while (pos < raw.size() &&
212
210
  !std::isspace(static_cast<unsigned char>(raw[pos])) &&
@@ -215,6 +213,19 @@ void minifyContentStreams(QPDF &qpdf) {
215
213
  raw[pos] != ')')
216
214
  ++pos;
217
215
 
216
+ // forward-progress guard: a byte that starts no token — a stray ')'
217
+ // or any whitespace the dispatcher above did not catch — leaves
218
+ // pos == start. Without this the outer loop spins forever, appending
219
+ // to `minified` without bound. Skip the byte so progress is guaranteed.
220
+ if (pos == start) {
221
+ ++pos;
222
+ continue;
223
+ }
224
+
225
+ if (needSpace) {
226
+ minified += ' ';
227
+ needSpace = false;
228
+ }
218
229
  std::string token(raw, start, pos - start);
219
230
 
220
231
  // trim numeric formatting