diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d00a208..008b779 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -86,6 +86,9 @@ jobs: - name: Ultracite check working-directory: npm run: npm run check + - name: Installer tests + working-directory: npm + run: npm test npm-package-smoke: runs-on: ubuntu-latest steps: diff --git a/npm/package.json b/npm/package.json index 4127f97..04de263 100644 --- a/npm/package.json +++ b/npm/package.json @@ -30,7 +30,8 @@ "scripts": { "check": "ultracite check", "fix": "ultracite fix", - "postinstall": "node scripts/install.js" + "postinstall": "node scripts/install.js", + "test": "node scripts/install.test.js" }, "devDependencies": { "@biomejs/biome": "2.5.6", diff --git a/npm/scripts/install.js b/npm/scripts/install.js index 7875c5c..c46c672 100644 --- a/npm/scripts/install.js +++ b/npm/scripts/install.js @@ -1,6 +1,6 @@ #!/usr/bin/env node -const { execSync } = require("node:child_process"); +const { execFileSync } = require("node:child_process"); const fs = require("node:fs"); const http = require("node:http"); const https = require("node:https"); @@ -79,7 +79,7 @@ function extractTarGz(buffer, destDir) { fs.writeFileSync(tmpFile, buffer); try { - execSync(`tar xzf "${tmpFile}" -C "${destDir}"`, { stdio: "pipe" }); + execFileSync("tar", ["xzf", tmpFile, "-C", destDir], { stdio: "pipe" }); } finally { try { fs.unlinkSync(tmpFile); @@ -94,7 +94,7 @@ function extractZip(buffer, destDir) { fs.writeFileSync(tmpFile, buffer); try { - execSync(`unzip -o "${tmpFile}" -d "${destDir}"`, { stdio: "pipe" }); + execFileSync("unzip", ["-o", tmpFile, "-d", destDir], { stdio: "pipe" }); } finally { try { fs.unlinkSync(tmpFile); @@ -166,7 +166,11 @@ async function install() { } } -install().catch((error) => { - console.error("Installation failed:", error.message); - process.exit(1); -}); +if (require.main === module) { + install().catch((error) => { + console.error("Installation failed:", error.message); + process.exit(1); + }); +} + +module.exports = { extractTarGz, extractZip }; diff --git a/npm/scripts/install.test.js b/npm/scripts/install.test.js new file mode 100644 index 0000000..dc6d401 --- /dev/null +++ b/npm/scripts/install.test.js @@ -0,0 +1,35 @@ +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const test = require("node:test"); +const { execFileSync } = require("node:child_process"); + +const { extractTarGz } = require("./install"); + +test("extractTarGz treats destination paths as literal arguments", (t) => { + const root = fs.mkdtempSync( + path.join(os.tmpdir(), "code-memory-install-test-") + ); + t.after(() => fs.rmSync(root, { force: true, recursive: true })); + + const source = path.join(root, "source"); + const destination = path.join(root, "$(touch injected)"); + const archive = path.join(root, "fixture.tar.gz"); + fs.mkdirSync(source); + fs.mkdirSync(destination); + fs.writeFileSync(path.join(source, "code-memory"), "fixture"); + execFileSync("tar", ["czf", archive, "-C", source, "code-memory"]); + + extractTarGz(fs.readFileSync(archive), destination); + + assert.equal( + fs.readFileSync(path.join(destination, "code-memory"), "utf8"), + "fixture" + ); + assert.equal( + fs.existsSync(path.join(process.cwd(), "injected")), + false, + "archive extraction must not execute shell syntax from paths" + ); +});