From 7d5e0e58946ecc718ff0cb8560c6b91bac558986 Mon Sep 17 00:00:00 2001 From: naorpeled Date: Sun, 30 Aug 2026 00:03:36 +0300 Subject: [PATCH] fix(test): extract the smoke-test tarball with Node built-ins Replaces the external tar process with zlib.gunzipSync plus a minimal ustar reader, so the smoke test spawns nothing but Node and behaves identically on every OS and Node version in the matrix. npm tarballs are gzipped ustar archives of regular files only, which is the one shape the reader accepts; a path-escape guard rejects anything else. --- test/package/smoke.cjs | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/test/package/smoke.cjs b/test/package/smoke.cjs index 33a4dee..b80e031 100644 --- a/test/package/smoke.cjs +++ b/test/package/smoke.cjs @@ -18,6 +18,7 @@ var cp = require('child_process'); var fs = require('fs'); var os = require('os'); var path = require('path'); +var zlib = require('zlib'); var tarball = process.argv[2]; @@ -40,12 +41,42 @@ var tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'bowser-smoke-')); var modules = path.join(tmp, 'node_modules'); fs.mkdirSync(modules); -// `npm install ` would work too, but plain tar keeps this dependency -// free and identical across the whole Node matrix, and sidesteps the -// npm.cmd-needs-a-shell quirks of spawning npm on Windows. tar itself is on -// PATH everywhere this runs: Linux, macOS, and Windows 10 1803+ (bsdtar). +/** + * Extracts an npm tarball using only Node built-ins, so the test needs no + * external `tar`/`npm` process and runs identically on every OS and every + * Node version in the support matrix. + * + * npm tarballs are gzipped ustar archives of regular files (npm rejects + * anything else at publish time), which is the only shape handled here: + * pax/global extended headers and directory entries are skipped, and parent + * directories are created per file instead. + */ +function extractNpmTarball(tgz, dest) { + var buf = zlib.gunzipSync(fs.readFileSync(tgz)); + var offset = 0; + while (offset + 512 <= buf.length) { + var header = buf.slice(offset, offset + 512); + offset += 512; + if (header[0] === 0) break; // zero block: end of archive + var name = header.slice(0, 100).toString('utf8').replace(/\0[\s\S]*$/, ''); + var prefix = header.slice(345, 500).toString('utf8').replace(/\0[\s\S]*$/, ''); + if (prefix) name = prefix + '/' + name; + var size = parseInt(header.slice(124, 136).toString('utf8'), 8) || 0; + var type = header[156]; + if (type === 48 /* '0' */ || type === 0) { + var target = path.join(dest, name); + if (target.indexOf(dest + path.sep) !== 0) { + throw new Error('tarball entry escapes destination: ' + name); + } + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.writeFileSync(target, buf.slice(offset, offset + size)); + } + offset += Math.ceil(size / 512) * 512; + } +} + // npm tarballs always unpack to a single top-level `package/` directory. -cp.execFileSync('tar', ['-xzf', tarball, '-C', modules], { stdio: 'inherit' }); +extractNpmTarball(tarball, modules); fs.renameSync(path.join(modules, 'package'), path.join(modules, 'bowser')); var runner = path.join(tmp, 'assertions.cjs');