iterate-skill-installer 2.3.0 → 2.3.5
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/lib/installer.js +32 -4
- package/package.json +1 -1
- package/test/mode.test.js +21 -3
package/lib/installer.js
CHANGED
|
@@ -162,7 +162,11 @@ function parseChecksums(text) {
|
|
|
162
162
|
if (!trimmed) continue;
|
|
163
163
|
const parts = trimmed.split(/\s+/);
|
|
164
164
|
if (parts.length >= 2) {
|
|
165
|
-
|
|
165
|
+
// Strip the GNU tar binary-mode marker ('*') so that both
|
|
166
|
+
// "HASH filename" and "HASH *filename" formats match the target
|
|
167
|
+
// filename, consistent with scripts/install.py (name.lstrip('*')).
|
|
168
|
+
const name = parts[1].replace(/^\*/, '');
|
|
169
|
+
map.set(name, parts[0]);
|
|
166
170
|
}
|
|
167
171
|
}
|
|
168
172
|
return map;
|
|
@@ -187,9 +191,32 @@ function runCommand(bin, args, options = {}) {
|
|
|
187
191
|
}
|
|
188
192
|
|
|
189
193
|
async function extractTarball(tarballPath, destDir) {
|
|
190
|
-
// Use the system tar command (available on macOS, Linux, and Windows 10+)
|
|
191
|
-
//
|
|
192
|
-
|
|
194
|
+
// Use the system tar command (available on macOS, Linux, and Windows 10+),
|
|
195
|
+
// but defend against path-traversal tarballs: before extracting we list the
|
|
196
|
+
// archive contents and refuse to extract any entry whose resolved path
|
|
197
|
+
// escapes destDir (e.g. a "../" prefix). This keeps the installer
|
|
198
|
+
// dependency-free while closing the theoretical escape vector.
|
|
199
|
+
const absoluteDest = path.resolve(destDir);
|
|
200
|
+
fs.mkdirSync(absoluteDest, { recursive: true });
|
|
201
|
+
|
|
202
|
+
// 1. List archive entries (no extraction yet).
|
|
203
|
+
const listing = await runCommand('tar', ['-tzf', tarballPath]);
|
|
204
|
+
|
|
205
|
+
// 2. Validate every entry stays inside destDir after stripping the single
|
|
206
|
+
// top-level directory component (--strip-components=1).
|
|
207
|
+
const entries = listing.split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
208
|
+
for (const entry of entries) {
|
|
209
|
+
const stripped = entry.split('/').slice(1).join('/');
|
|
210
|
+
const resolved = path.resolve(absoluteDest, stripped || entry);
|
|
211
|
+
if (resolved !== absoluteDest && !resolved.startsWith(absoluteDest + path.sep)) {
|
|
212
|
+
throw new InstallerError(
|
|
213
|
+
`Refusing to extract tarball: entry "${entry}" escapes destination directory.`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// 3. Now extract safely — the content has been validated above.
|
|
219
|
+
await runCommand('tar', ['-xzf', tarballPath, '-C', absoluteDest, '--strip-components=1']);
|
|
193
220
|
}
|
|
194
221
|
|
|
195
222
|
function runPythonInstall(pythonBin, installScript, args, options = {}) {
|
|
@@ -487,4 +514,5 @@ module.exports = {
|
|
|
487
514
|
InstallerError,
|
|
488
515
|
resolveInstallMode,
|
|
489
516
|
askYesNo,
|
|
517
|
+
parseChecksums,
|
|
490
518
|
};
|
package/package.json
CHANGED
package/test/mode.test.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unit tests for resolveInstallMode() — the global vs project install-mode
|
|
3
|
-
* decision
|
|
4
|
-
* required; run via
|
|
3
|
+
* decision — and parseChecksums() — the SHA256SUMS.txt parser — in the npx
|
|
4
|
+
* installer. Uses node:assert so no extra test runner is required; run via
|
|
5
|
+
* `npm test`.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
const assert = require('node:assert');
|
|
8
9
|
const fs = require('node:fs');
|
|
9
10
|
const path = require('node:path');
|
|
10
11
|
const os = require('node:os');
|
|
11
|
-
const { resolveInstallMode } = require('../lib/installer');
|
|
12
|
+
const { resolveInstallMode, parseChecksums } = require('../lib/installer');
|
|
12
13
|
|
|
13
14
|
const originalCwd = process.cwd();
|
|
14
15
|
const originalHome = os.homedir;
|
|
@@ -79,6 +80,23 @@ async function run() {
|
|
|
79
80
|
});
|
|
80
81
|
|
|
81
82
|
console.log('mode.test.js: all resolveInstallMode tests passed');
|
|
83
|
+
|
|
84
|
+
// parseChecksums: must strip the GNU tar binary-mode marker ('*') so both
|
|
85
|
+
// "HASH name" and "HASH *name" formats resolve, matching the Python side.
|
|
86
|
+
const plain = parseChecksums('abc123 iterate-skill.tar.gz\n');
|
|
87
|
+
assert.strictEqual(plain.get('iterate-skill.tar.gz'), 'abc123', 'plain format should parse');
|
|
88
|
+
|
|
89
|
+
const starred = parseChecksums('def456 *iterate-skill.tar.gz\n');
|
|
90
|
+
assert.strictEqual(starred.get('iterate-skill.tar.gz'), 'def456', 'starred format should strip the * marker');
|
|
91
|
+
|
|
92
|
+
const crlf = parseChecksums('abc123 iterate-skill.tar.gz\r\n');
|
|
93
|
+
assert.strictEqual(crlf.get('iterate-skill.tar.gz'), 'abc123', 'CRLF line endings should parse');
|
|
94
|
+
|
|
95
|
+
const multi = parseChecksums('aa a.txt\nbb b.txt\n');
|
|
96
|
+
assert.strictEqual(multi.get('a.txt'), 'aa');
|
|
97
|
+
assert.strictEqual(multi.get('b.txt'), 'bb');
|
|
98
|
+
|
|
99
|
+
console.log('mode.test.js: all parseChecksums tests passed');
|
|
82
100
|
}
|
|
83
101
|
|
|
84
102
|
run().catch((err) => {
|