| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
1 parent 2bda6db commit 00b687b
1 file changed
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,92 @@ | |||
| 1 | + 'use strict'; | ||
| 2 | + | ||
| 3 | + // Lifecycle-hardening regression tests for node:zlib ZipFile (the on-disk, | ||
| 4 | + // fd-backed reader/writer). Each asserts the secure behavior, so it fails on | ||
| 5 | + // the pre-fix code and passes once its fix lands. | ||
| 6 | + // | ||
| 7 | + // 1. A read in flight when close() is called must complete on a live | ||
| 8 | + // descriptor - close() must not pull the fd out from under it (which would | ||
| 9 | + // surface as EBADF, or worse read another file once the fd number is | ||
| 10 | + // reused). | ||
| 11 | + // 2. If the central-directory rewrite fails after an add() has written the | ||
| 12 | + // member bytes, the in-memory state and the on-disk archive must be rolled | ||
| 13 | + // back to exactly what they were before the call, not left half-updated. | ||
| 14 | + | ||
| 15 | + require('../common'); | ||
| 16 | + const assert = require('assert'); | ||
| 17 | + const { test } = require('node:test'); | ||
| 18 | + const zlib = require('zlib'); | ||
| 19 | + const fs = require('fs'); | ||
| 20 | + const tmpdir = require('../common/tmpdir'); | ||
| 21 | + | ||
| 22 | + const SIG_CENTRAL = 0x02014b50; | ||
| 23 | + const SIG_EOCD = 0x06054b50; | ||
| 24 | + | ||
| 25 | + function writeArchive(file, entries) { | ||
| 26 | + const chunks = []; | ||
| 27 | + for (const chunk of zlib.createZipArchiveSync(entries)) chunks.push(chunk); | ||
| 28 | + fs.writeFileSync(file, Buffer.concat(chunks)); | ||
| 29 | + } | ||
| 30 | + | ||
| 31 | + // 1. A read that is in flight when the ZipFile is closed still completes. | ||
| 32 | + test('close() waits for an in-flight read instead of closing under it', async () => { | ||
| 33 | + tmpdir.refresh(); | ||
| 34 | + const file = tmpdir.resolve('inflight.zip'); | ||
| 35 | + const payload = Buffer.alloc(8 * 1024 * 1024, 0x5a); | ||
| 36 | + writeArchive(file, [zlib.ZipEntry.createSync('big', payload, { method: 'store' })]); | ||
| 37 | + | ||
| 38 | + const zf = zlib.ZipFile.openSync(file); | ||
| 39 | + const entry = zf.getSync('big'); | ||
| 40 | + const reading = entry.content(); // In flight; do not await yet | ||
| 41 | + await zf.close(); // Must wait for the read, not close the fd under it | ||
| 42 | + const data = await reading; // Must resolve with correct bytes, not reject EBADF | ||
| 43 | + assert.strictEqual(data.length, payload.length); | ||
| 44 | + assert.ok(data.equals(payload)); | ||
| 45 | + }); | ||
| 46 | + | ||
| 47 | + // 2. A failed central-directory rewrite during add() is rolled back. | ||
| 48 | + test('a failed directory rewrite during addEntrySync is rolled back', () => { | ||
| 49 | + tmpdir.refresh(); | ||
| 50 | + const file = tmpdir.resolve('addfail.zip'); | ||
| 51 | + writeArchive(file, [zlib.ZipEntry.createSync('first.txt', Buffer.from('original'), | ||
| 52 | + { method: 'store' })]); | ||
| 53 | + | ||
| 54 | + const zf = zlib.ZipFile.openSync(file, { writable: true }); | ||
| 55 | + const toAdd = zlib.ZipEntry.createSync('second.txt', Buffer.from('added'), { method: 'store' }); | ||
| 56 | + | ||
| 57 | + // Fail only the first central-directory write (its buffer starts with the | ||
| 58 | + // central-header or EOCD signature); the member bytes start with the local | ||
| 59 | + // header signature and pass through, and the rollback rewrite that follows | ||
| 60 | + // succeeds so the on-disk archive is restored. | ||
| 61 | + const realWriteSync = fs.writeSync; | ||
| 62 | + let failNextDirectoryWrite = true; | ||
| 63 | + fs.writeSync = function(fd, buffer, offset, length, position) { | ||
| 64 | + if (failNextDirectoryWrite && Buffer.isBuffer(buffer) && buffer.length - offset >= 4) { | ||
| 65 | + const sig = buffer.readUInt32LE(offset); | ||
| 66 | + if (sig === SIG_CENTRAL || sig === SIG_EOCD) { | ||
| 67 | + failNextDirectoryWrite = false; | ||
| 68 | + const err = new Error('ENOSPC: simulated no space left on device'); | ||
| 69 | + err.code = 'ENOSPC'; | ||
| 70 | + throw err; | ||
| 71 | + } | ||
| 72 | + } | ||
| 73 | + return realWriteSync.call(fs, fd, buffer, offset, length, position); | ||
| 74 | + }; | ||
| 75 | + try { | ||
| 76 | + assert.throws(() => zf.addEntrySync(toAdd), { code: 'ENOSPC' }); | ||
| 77 | + } finally { | ||
| 78 | + fs.writeSync = realWriteSync; | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + // In-memory: the half-added entry is gone and the original is still readable. | ||
| 82 | + assert.ok(!zf.has('second.txt')); | ||
| 83 | + assert.strictEqual(zf.getSync('first.txt').contentSync().toString(), 'original'); | ||
| 84 | + zf.closeSync(); | ||
| 85 | + | ||
| 86 | + // On disk: reopening shows the original, uncorrupted archive. | ||
| 87 | + const reopened = zlib.ZipFile.openSync(file); | ||
| 88 | + assert.ok(reopened.has('first.txt')); | ||
| 89 | + assert.ok(!reopened.has('second.txt')); | ||
| 90 | + assert.strictEqual(reopened.getSync('first.txt').contentSync().toString(), 'original'); | ||
| 91 | + reopened.closeSync(); | ||
| 92 | + }); | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments