Files
NetMesh/electron/bridges/sftpBridge/archiveExtract.test.cjs

189 lines
7.7 KiB
JavaScript
Raw Normal View History

"use strict";
const assert = require("node:assert/strict");
const test = require("node:test");
const {
getArchiveKind,
isExtractableArchive,
posixParentDir,
stripCompressionSuffix,
computeExtractTimeoutMs,
buildExtractCommand,
buildLocalExtractPlan,
EXTRACT_MAX_TIMEOUT_MS,
} = require("./archiveExtract.cjs");
test("detects compound archive suffixes before single-file compression", () => {
assert.equal(getArchiveKind("backup.tar.gz"), "tar.gz");
assert.equal(getArchiveKind("/var/a.tgz"), "tar.gz");
assert.equal(getArchiveKind("logs.tar.bz2"), "tar.bz2");
assert.equal(getArchiveKind("src.tar.xz"), "tar.xz");
assert.equal(getArchiveKind("app.tar"), "tar");
assert.equal(getArchiveKind("payload.zip"), "zip");
assert.equal(getArchiveKind("notes.txt.gz"), "gz");
assert.equal(getArchiveKind("notes.txt"), null);
assert.equal(getArchiveKind(".gz"), null);
assert.equal(isExtractableArchive("bundle.tgz"), true);
assert.equal(isExtractableArchive("readme.md"), false);
});
test("posix parent and gzip output stay in the archive directory", () => {
assert.equal(posixParentDir("/home/app/a.tar.gz"), "/home/app");
assert.equal(posixParentDir("/a.zip"), "/");
assert.equal(stripCompressionSuffix("/home/app/notes.txt.gz", "gz"), "/home/app/notes.txt");
assert.equal(stripCompressionSuffix("/notes.txt.gz", "gz"), "/notes.txt");
});
test("extract commands quote spaces and single quotes", () => {
const tarCmd = buildExtractCommand("/tmp/my files/app's.tgz");
assert.match(tarCmd, /tar -xzf '/);
assert.match(tarCmd, /'\/tmp\/my files\/app'\\''s\.tgz'/);
assert.match(tarCmd, /-C '\/tmp\/my files'/);
const zipCmd = buildExtractCommand("/opt/build/out.zip");
assert.match(zipCmd, /unzip -qo '\/opt\/build\/out\.zip' -d '\/opt\/build' >\/dev\/null/);
assert.match(zipCmd, /tar -xf '\/opt\/build\/out\.zip' -C '\/opt\/build'/);
const gzCmd = buildExtractCommand("/var/log/syslog.gz");
assert.match(gzCmd, /out='\/var\/log\/syslog'/);
assert.match(gzCmd, /archive='\/var\/log\/syslog\.gz'/);
assert.match(gzCmd, /trap 'rm -f -- "\$stage"' EXIT/);
assert.match(gzCmd, /gzip -dc -- "\$archive" > "\$stage"/);
assert.match(gzCmd, /if \[ -d "\$out" \]/);
assert.match(gzCmd, /mv -f -- "\$stage" "\$out"/);
const spacedGz = buildExtractCommand("/tmp/my file.txt.gz");
assert.match(spacedGz, /out='\/tmp\/my file\.txt'/);
assert.match(spacedGz, /archive='\/tmp\/my file\.txt\.gz'/);
assert.doesNotMatch(spacedGz, /trap 'rm -f -- '/);
});
test("generated gzip extract script can install its EXIT trap with spaces in the path", () => {
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { spawnSync } = require("node:child_process");
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nc-trap-"));
const archive = path.join(dir, "my file.txt.gz");
fs.writeFileSync(archive, "x");
const cmd = buildExtractCommand(archive);
const prefix = [];
for (const line of cmd.split("\n")) {
prefix.push(line);
if (line.startsWith("trap ")) break;
}
prefix.push("trap - EXIT");
prefix.push("rm -f -- \"$stage\"");
const ran = spawnSync("sh", ["-c", prefix.join("\n")], { encoding: "utf8" });
assert.equal(ran.status, 0, ran.stderr);
fs.rmSync(dir, { recursive: true, force: true });
});
test("extract command rejects newlines and unknown types", () => {
assert.throws(() => buildExtractCommand("/tmp/bad\n.tar.gz"), /NUL or newlines/);
assert.throws(() => buildExtractCommand("/tmp/notes.txt"), /Unsupported archive type/);
});
test("local extract plan uses unzip with tar fallback off Windows", () => {
const unixZip = buildLocalExtractPlan("/tmp/a.zip", "linux");
assert.deepEqual(unixZip.command, "unzip");
assert.deepEqual(unixZip.args, ["-qo", "/tmp/a.zip", "-d", "/tmp"]);
assert.deepEqual(unixZip.fallback, { command: "tar", args: ["-xf", "/tmp/a.zip", "-C", "/tmp"] });
const winZip = buildLocalExtractPlan("C:\\tmp\\a.zip", "win32");
assert.equal(winZip.command, "tar");
assert.ok(winZip.args.includes("-xf"));
const gz = buildLocalExtractPlan("/tmp/notes.txt.gz", "win32");
assert.equal(gz.builtin, "gunzip");
assert.equal(gz.archivePath, "/tmp/notes.txt.gz");
assert.equal(gz.stdoutFile, "/tmp/notes.txt");
});
test("unknown archive size uses the maximum extract timeout", () => {
assert.equal(computeExtractTimeoutMs(undefined), EXTRACT_MAX_TIMEOUT_MS);
assert.ok(computeExtractTimeoutMs(1024) < EXTRACT_MAX_TIMEOUT_MS);
});
test("extractLocalArchiveFile unpacks a tar.gz next to the archive", async () => {
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { spawnSync } = require("node:child_process");
const { extractLocalArchiveFile } = require("./archiveExtract.cjs");
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nc-extract-"));
const source = path.join(dir, "hello.txt");
const archive = path.join(dir, "hello.tgz");
fs.writeFileSync(source, "hello-extract");
const packed = spawnSync("tar", ["-czf", archive, "-C", dir, "hello.txt"], { encoding: "utf8" });
assert.equal(packed.status, 0, packed.stderr);
fs.unlinkSync(source);
await extractLocalArchiveFile(archive);
assert.equal(fs.readFileSync(source, "utf8"), "hello-extract");
fs.rmSync(dir, { recursive: true, force: true });
});
test("failed gzip extract leaves an existing sibling file intact", async () => {
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const { extractLocalArchiveFile } = require("./archiveExtract.cjs");
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nc-extract-keep-"));
const dest = path.join(dir, "notes.txt");
const archive = path.join(dir, "notes.txt.gz");
fs.writeFileSync(dest, "keep-me");
fs.writeFileSync(archive, "this-is-not-gzip");
await assert.rejects(() => extractLocalArchiveFile(archive), /Local extraction failed/);
assert.equal(fs.readFileSync(dest, "utf8"), "keep-me");
assert.deepEqual(
fs.readdirSync(dir).filter((name) => name.includes(".netcatty-extract")),
[],
);
fs.rmSync(dir, { recursive: true, force: true });
});
test("extractLocalArchiveFile gunzips with Node zlib and keeps a sibling staging name", async () => {
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const zlib = require("node:zlib");
const { extractLocalArchiveFile } = require("./archiveExtract.cjs");
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nc-extract-gz-"));
const dest = path.join(dir, "notes.txt");
const archive = path.join(dir, "notes.txt.gz");
const sibling = `${dest}.netcatty-extract`;
fs.writeFileSync(sibling, "do-not-touch");
fs.writeFileSync(archive, zlib.gzipSync("hello-gzip"));
await extractLocalArchiveFile(archive);
assert.equal(fs.readFileSync(dest, "utf8"), "hello-gzip");
assert.equal(fs.readFileSync(sibling, "utf8"), "do-not-touch");
fs.rmSync(dir, { recursive: true, force: true });
});
test("failed gzip extract leaves a sibling directory target intact", async () => {
const fs = require("node:fs");
const os = require("node:os");
const path = require("node:path");
const zlib = require("node:zlib");
const { extractLocalArchiveFile } = require("./archiveExtract.cjs");
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nc-extract-dir-"));
const dest = path.join(dir, "notes.txt");
const archive = path.join(dir, "notes.txt.gz");
fs.mkdirSync(dest);
fs.writeFileSync(path.join(dest, "keep.txt"), "inside");
fs.writeFileSync(archive, zlib.gzipSync("hello-gzip"));
await assert.rejects(() => extractLocalArchiveFile(archive), /directory/);
assert.equal(fs.readFileSync(path.join(dest, "keep.txt"), "utf8"), "inside");
fs.rmSync(dir, { recursive: true, force: true });
});