From 90480cd17ef8db398ee2c516b70f10612f0fbbe8 Mon Sep 17 00:00:00 2001 From: "Kamat, Trivikram" <16024985+trivikr@users.noreply.github.com> Date: Thu, 13 Aug 2026 20:53:34 -0700 Subject: [PATCH] zlib: avoid waiting for paused ZIP iterators Track file-backed contentIterator() reads only while I/O is active. This lets ZipFile.close() finish when an iterator is paused after yielding a chunk, while still waiting for reads in flight. Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com> Assisted-by: codex:gpt-5.6-sol --- lib/internal/zip/entry.js | 15 +++++++++++---- test/parallel/test-zlib-zip-file-lifecycle.js | 15 +++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/lib/internal/zip/entry.js b/lib/internal/zip/entry.js index 566f5ba58a51..cfe54903a126 100644 --- a/lib/internal/zip/entry.js +++ b/lib/internal/zip/entry.js @@ -501,15 +501,14 @@ class ZipEntry { // The entry's raw compressed bytes as a bounded-memory chunk stream, read // straight from disk (file-backed entries only). Nothing is retained. async *#rawChunks() { - // Count the whole stream as one in-flight read so a concurrent close() - // waits for it (the finally runs when the consumer stops iterating); a - // stream begun after close() was requested is rejected up front. + // Count active reads, but not time paused at a yield. const handle = this.#fd; if (handle.closing) { throw new ERR_INVALID_STATE( 'cannot read a ZipEntry after its backing ZipFile has been closed'); } handle.reads++; + let reading = true; try { this.#liveDescriptor(); let pos = await this.#resolveContentOffset(); @@ -521,10 +520,18 @@ class ZipEntry { await readFdFully(this.#liveDescriptor(), chunk, pos); pos += take; remaining -= take; + reading = false; + endHandleRead(handle); yield chunk; + if (handle.closing) { + throw new ERR_INVALID_STATE( + 'cannot read a ZipEntry after its backing ZipFile has been closed'); + } + handle.reads++; + reading = true; } } finally { - endHandleRead(handle); + if (reading) endHandleRead(handle); } } // Sync counterpart of #rawChunks(). diff --git a/test/parallel/test-zlib-zip-file-lifecycle.js b/test/parallel/test-zlib-zip-file-lifecycle.js index 85a06f86b75e..ad5d4b6237da 100644 --- a/test/parallel/test-zlib-zip-file-lifecycle.js +++ b/test/parallel/test-zlib-zip-file-lifecycle.js @@ -44,6 +44,21 @@ test('close() waits for an in-flight read instead of closing under it', async () assert.ok(data.equals(payload)); }); +test('close() resolves with a paused contentIterator()', async () => { + tmpdir.refresh(); + const file = tmpdir.resolve('paused-iterator.zip'); + const payload = Buffer.alloc(8 * 1024 * 1024, 0x61); + writeArchive(file, [zlib.ZipEntry.createSync('big', payload, { method: 'store' })]); + + const zf = zlib.ZipFile.openSync(file); + const iterator = zf.getSync('big').contentIterator(); + const first = await iterator.next(); + assert.strictEqual(first.done, false); + + await zf.close(); + await assert.rejects(iterator.next(), { code: 'ERR_INVALID_STATE' }); +}); + // 2. A failed central-directory rewrite during add() is rolled back. test('a failed directory rewrite during addEntrySync is rolled back', () => { tmpdir.refresh();