fix(install): make Windows zip extraction resilient (issue #603) (#713)

The Windows extraction step relied on `powershell -Command Expand-Archive`,
which fails when:
  - Microsoft.PowerShell.Archive (a script module) cannot be loaded due to
    PSModulePath shadowing (Store-installed pwsh injecting WindowsApps
    paths) or ExecutionPolicy Restricted (issue #603), or
  - the temp directory contains characters that corrupt PowerShell string
    parsing (e.g. a single quote in TEMP).

Switch to a two-tier extraction:
  1. Primary: Add-Type System.IO.Compression.FileSystem +
     [ZipFile]::ExtractToDirectory. Bypasses the PowerShell module system
     entirely. .NET 4.5+, available on Win 8 / Server 2012 by default and
     widely on Win 7 SP1.
  2. Fallback: Expand-Archive -LiteralPath, kept for the rare host without
     .NET 4.5 but with PS 5.0+ (e.g. Win 7 SP1 with WMF 5).

Both paths pass file paths through env vars ($env:LARK_CLI_ARCHIVE /
$env:LARK_CLI_DEST) so quoting / wildcard chars in the path can no longer
break command parsing. -LiteralPath ensures Expand-Archive treats the value
literally rather than as a wildcard pattern. $ErrorActionPreference='Stop'
makes non-terminating cmdlet errors propagate as non-zero exit codes.

Also drop `stdio: "ignore"` so the actual PowerShell error surfaces in the
postinstall log when both paths fail, instead of leaving users with
"Command failed: powershell ..." with no detail.

Verified on Windows 10 + PS 5.1:
  - Reproduced #603 with shadow Microsoft.PowerShell.Archive +
    Restricted ExecutionPolicy: original install.js fails, patched
    install.js succeeds.
  - Reproduced single-quote-in-TEMP path corruption: original fails,
    patched succeeds.
  - Fallback path verified end-to-end with primary forced to fail.
  - Normal-environment install: no regression.
This commit is contained in:
sang-neo03
2026-04-29 17:50:46 +08:00
committed by GitHub
parent 7752afab96
commit 418192507e

View File

@@ -125,6 +125,37 @@ function download(url, destPath) {
execFileSync("curl", args, { stdio: ["ignore", "ignore", "pipe"] });
}
function extractZipWindows(archivePath, destDir) {
const psOpts = ["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command"];
const psStdio = ["ignore", "inherit", "inherit"];
const psEnv = {
...process.env,
LARK_CLI_ARCHIVE: archivePath,
LARK_CLI_DEST: destDir,
};
try {
const dotnet =
"$ErrorActionPreference='Stop';" +
"Add-Type -AssemblyName System.IO.Compression.FileSystem;" +
"[System.IO.Compression.ZipFile]::ExtractToDirectory($env:LARK_CLI_ARCHIVE,$env:LARK_CLI_DEST)";
execFileSync("powershell.exe", [...psOpts, dotnet], { stdio: psStdio, env: psEnv });
} catch (primaryErr) {
try {
const cmdlet =
"$ErrorActionPreference='Stop';" +
"Expand-Archive -LiteralPath $env:LARK_CLI_ARCHIVE -DestinationPath $env:LARK_CLI_DEST -Force";
execFileSync("powershell.exe", [...psOpts, cmdlet], { stdio: psStdio, env: psEnv });
} catch (fallbackErr) {
throw new Error(
`Failed to extract ${archivePath}. ` +
`.NET ZipFile attempt: ${primaryErr.message}. ` +
`Expand-Archive fallback: ${fallbackErr.message}`
);
}
}
}
function install() {
const mirrorUrls = getMirrorUrls(process.env);
const downloadUrls = [GITHUB_URL, ...mirrorUrls];
@@ -156,10 +187,7 @@ function install() {
verifyChecksum(archivePath, expectedHash);
if (isWindows) {
execFileSync("powershell", [
"-Command",
`Expand-Archive -Path '${archivePath}' -DestinationPath '${tmpDir}'`,
], { stdio: "ignore" });
extractZipWindows(archivePath, tmpDir);
} else {
execFileSync("tar", ["-xzf", archivePath, "-C", tmpDir], {
stdio: "ignore",