browser4-cli 4.14.0-rc.4 → 4.14.0-rc.6
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/README.md +10 -10
- package/bin/browser4-cli-darwin-arm64 +0 -0
- package/bin/browser4-cli-darwin-x64 +0 -0
- package/bin/browser4-cli-linux-arm64 +0 -0
- package/bin/browser4-cli-linux-musl-arm64 +0 -0
- package/bin/browser4-cli-linux-musl-x64 +0 -0
- package/bin/browser4-cli-linux-x64 +0 -0
- package/bin/browser4-cli-win32-x64.exe +0 -0
- package/package.json +1 -1
- package/scripts/README.md +20 -1
- package/scripts/install-browser4-cli.ps1 +174 -46
- package/scripts/install-browser4-cli.sh +98 -28
- package/scripts/smoke-test-runtime-bundle.sh +18 -3
- package/scripts/tests/install-browser4-cli.tests.ps1 +150 -15
- package/scripts/tests/install-browser4-cli.tests.sh +157 -10
- package/scripts/tests/wait-for-npm-version.tests.sh +219 -0
- package/scripts/wait-for-npm-version.sh +89 -0
|
@@ -55,11 +55,41 @@ SCRIPT_DIR=""
|
|
|
55
55
|
# ----------------------------------------------
|
|
56
56
|
|
|
57
57
|
say() { if [[ "$SILENT" != true ]]; then echo -e "$*"; fi; }
|
|
58
|
+
# Informational note for helpers whose stdout is captured by the caller --
|
|
59
|
+
# anything these functions print on stdout ends up inside the captured value.
|
|
60
|
+
note() { if [[ "$SILENT" != true ]]; then echo -e " [i] $*" >&2; fi; }
|
|
58
61
|
step() { say " -> $*"; }
|
|
59
62
|
ok() { say " [v] $*"; }
|
|
60
63
|
warn() { say " [!] $*" >&2; }
|
|
61
64
|
die() { echo "ERROR: $*" >&2; exit 1; }
|
|
62
65
|
|
|
66
|
+
# Validate the value that must follow an option. A missing value and a value
|
|
67
|
+
# that is really the next option are both rejected: `--version --dry-run` used
|
|
68
|
+
# to install the literal tag "--dry-run" (and `--install-dir --silent` created
|
|
69
|
+
# a directory called "--silent"), which fails much later with a confusing 404.
|
|
70
|
+
# Runs in the current shell so `die` really stops the installer -- a `die`
|
|
71
|
+
# inside `$( ... )` only kills the subshell and the install would continue.
|
|
72
|
+
validate_value() {
|
|
73
|
+
local flag="$1" value="${2:-}"
|
|
74
|
+
if [[ -z "$value" ]]; then
|
|
75
|
+
die "$flag requires a value"
|
|
76
|
+
fi
|
|
77
|
+
if [[ "$value" == -* ]]; then
|
|
78
|
+
die "$flag requires a value (got '$value', which looks like another option)"
|
|
79
|
+
fi
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
# Release tags on GitHub are prefixed with "v" (v4.13.18). Accepting a bare
|
|
83
|
+
# "4.13.18" and fixing it up front is friendlier than a 404 from the CDN.
|
|
84
|
+
normalize_tag() {
|
|
85
|
+
local tag="$1"
|
|
86
|
+
if [[ "$tag" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then
|
|
87
|
+
note "Using release tag 'v$tag' (release tags are prefixed with 'v')"
|
|
88
|
+
tag="v$tag"
|
|
89
|
+
fi
|
|
90
|
+
printf '%s' "$tag"
|
|
91
|
+
}
|
|
92
|
+
|
|
63
93
|
color_cyan='\033[0;36m'
|
|
64
94
|
color_green='\033[0;32m'
|
|
65
95
|
color_yellow='\033[0;33m'
|
|
@@ -157,13 +187,12 @@ EOF
|
|
|
157
187
|
while [[ $# -gt 0 ]]; do
|
|
158
188
|
case "$1" in
|
|
159
189
|
--version|-v)
|
|
160
|
-
shift;
|
|
161
|
-
VERSION="$1"; shift ;;
|
|
190
|
+
shift; validate_value "--version" "${1:-}"; VERSION=$(normalize_tag "$1"); shift ;;
|
|
162
191
|
--install-dir|-d)
|
|
163
|
-
shift;
|
|
164
|
-
INSTALL_DIR="$1"; shift ;;
|
|
192
|
+
shift; validate_value "--install-dir" "${1:-}"; INSTALL_DIR="$1"; shift ;;
|
|
165
193
|
--source)
|
|
166
|
-
shift
|
|
194
|
+
shift
|
|
195
|
+
validate_value "--source" "${1:-}"
|
|
167
196
|
if [[ "$1" != "github" && "$1" != "oss" ]]; then
|
|
168
197
|
die "--source must be 'github' or 'oss'"
|
|
169
198
|
fi
|
|
@@ -316,7 +345,7 @@ get_default_install_dir() {
|
|
|
316
345
|
|
|
317
346
|
check_commands() {
|
|
318
347
|
local missing=()
|
|
319
|
-
for cmd in curl mktemp stat awk grep ln mkdir chmod; do
|
|
348
|
+
for cmd in curl mktemp stat awk grep ln mkdir chmod od cp mv tail cat; do
|
|
320
349
|
if ! command -v "$cmd" >/dev/null 2>&1; then
|
|
321
350
|
missing+=("$cmd")
|
|
322
351
|
fi
|
|
@@ -326,6 +355,21 @@ check_commands() {
|
|
|
326
355
|
fi
|
|
327
356
|
}
|
|
328
357
|
|
|
358
|
+
# A downloaded file must really be a native executable, not an HTML error page
|
|
359
|
+
# or a wrong-architecture body that happens to be large. Magic bytes are the
|
|
360
|
+
# cheapest check that needs no network and no published checksum file.
|
|
361
|
+
verify_binary_magic() {
|
|
362
|
+
local path="$1"
|
|
363
|
+
local magic
|
|
364
|
+
magic=$(od -An -tx1 -N4 "$path" 2>/dev/null | tr -d ' \n')
|
|
365
|
+
case "$magic" in
|
|
366
|
+
7f454c46) return 0 ;; # ELF (linux glibc/musl)
|
|
367
|
+
4d5a*) return 0 ;; # PE / MZ (windows)
|
|
368
|
+
feedface|feedfacf|cefaedfe|cffaedfe|cafebabe) return 0 ;; # Mach-O, incl. fat
|
|
369
|
+
*) return 1 ;;
|
|
370
|
+
esac
|
|
371
|
+
}
|
|
372
|
+
|
|
329
373
|
get_download_urls() {
|
|
330
374
|
local binary_name="$1"
|
|
331
375
|
local version_tag="$2"
|
|
@@ -374,8 +418,15 @@ download_file() {
|
|
|
374
418
|
curl_stderr=$(mktemp)
|
|
375
419
|
local http_code curl_exit
|
|
376
420
|
|
|
421
|
+
# Only GitHub needs (and only GitHub may receive) the token: the same header
|
|
422
|
+
# must never be sent to the Aliyun OSS mirror.
|
|
423
|
+
local -a auth_args=()
|
|
424
|
+
if [[ -n "${GITHUB_TOKEN:-}" ]] && [[ "$url" == https://github.com/* ]]; then
|
|
425
|
+
auth_args=(-H "Authorization: Bearer $GITHUB_TOKEN")
|
|
426
|
+
fi
|
|
427
|
+
|
|
377
428
|
http_code=$(curl -sSfL -w "%{http_code}" -o "$dest" \
|
|
378
|
-
${
|
|
429
|
+
${auth_args[@]+"${auth_args[@]}"} \
|
|
379
430
|
"$url" 2>"$curl_stderr")
|
|
380
431
|
curl_exit=$?
|
|
381
432
|
|
|
@@ -398,6 +449,11 @@ download_file() {
|
|
|
398
449
|
|
|
399
450
|
# Sanity check: binary must be > 100 KB
|
|
400
451
|
if [[ "$size" -gt 102400 ]]; then
|
|
452
|
+
if ! verify_binary_magic "$dest"; then
|
|
453
|
+
warn "Downloaded file is not a native executable (bad magic bytes) -- refusing it"
|
|
454
|
+
rm -f "$dest"
|
|
455
|
+
return 1
|
|
456
|
+
fi
|
|
401
457
|
local size_mb
|
|
402
458
|
size_mb=$(awk "BEGIN { printf \"%.1f\", $size / 1048576 }")
|
|
403
459
|
ok "Downloaded ${size_mb} MB"
|
|
@@ -439,7 +495,11 @@ add_to_shell_rc() {
|
|
|
439
495
|
rc_file="$HOME/.profile"
|
|
440
496
|
fi
|
|
441
497
|
|
|
442
|
-
|
|
498
|
+
local path_line="export PATH=\"$dir:\$PATH\""
|
|
499
|
+
|
|
500
|
+
# Idempotency: only PATH assignment lines count, so an unrelated mention of the
|
|
501
|
+
# directory (a comment, another tool's variable) does not suppress the entry.
|
|
502
|
+
if grep -E '^[[:space:]]*(export[[:space:]]+)?PATH=' "$rc_file" 2>/dev/null | grep -qF "$dir"; then
|
|
443
503
|
ok "PATH entry already in $rc_file"
|
|
444
504
|
return
|
|
445
505
|
fi
|
|
@@ -454,23 +514,25 @@ add_to_shell_rc() {
|
|
|
454
514
|
fi
|
|
455
515
|
fi
|
|
456
516
|
|
|
457
|
-
#
|
|
517
|
+
# Serialize concurrent installs when the platform has flock (Linux). macOS
|
|
518
|
+
# ships without it, and waiting for a lock that can never be taken only made
|
|
519
|
+
# every install pause and print a bogus warning.
|
|
458
520
|
local lock_file="${rc_file}.browser4-install.lock"
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
521
|
+
if command -v flock >/dev/null 2>&1; then
|
|
522
|
+
local waited=0
|
|
523
|
+
while ! flock -n 9 2>/dev/null; do
|
|
524
|
+
if [[ $waited -ge 20 ]]; then
|
|
525
|
+
warn "Could not acquire lock on ${rc_file} after 10s; appending anyway"
|
|
526
|
+
break
|
|
527
|
+
fi
|
|
528
|
+
sleep 0.5
|
|
529
|
+
waited=$((waited + 1))
|
|
530
|
+
done 9>"$lock_file"
|
|
531
|
+
fi
|
|
470
532
|
|
|
471
533
|
{
|
|
472
534
|
echo "# browser4-cli"
|
|
473
|
-
echo "
|
|
535
|
+
echo "$path_line"
|
|
474
536
|
} >> "$rc_file"
|
|
475
537
|
|
|
476
538
|
# Clean up (flock auto-releases when fd 9 is closed)
|
|
@@ -511,7 +573,7 @@ create_symlinks() {
|
|
|
511
573
|
local b4_exists=false
|
|
512
574
|
local b4_is_ours=false
|
|
513
575
|
|
|
514
|
-
# Check in install dir first
|
|
576
|
+
# Check in install dir first - anything here is ours
|
|
515
577
|
if [[ -e "$short_path" ]] || [[ -L "$short_path" ]]; then
|
|
516
578
|
b4_exists=true
|
|
517
579
|
b4_is_ours=true
|
|
@@ -620,7 +682,6 @@ install_backend() {
|
|
|
620
682
|
# ----------------------------------------------
|
|
621
683
|
|
|
622
684
|
main() {
|
|
623
|
-
check_commands
|
|
624
685
|
header
|
|
625
686
|
|
|
626
687
|
# Locate ourselves on disk (only works when run as a file, not piped)
|
|
@@ -663,9 +724,9 @@ main() {
|
|
|
663
724
|
step "Local binary: not found alongside script"
|
|
664
725
|
fi
|
|
665
726
|
|
|
666
|
-
# Check for already-installed binary
|
|
727
|
+
# Check for already-installed binary (honour an explicit --install-dir)
|
|
667
728
|
local default_dir existing_path
|
|
668
|
-
default_dir
|
|
729
|
+
default_dir="${INSTALL_DIR:-$(get_default_install_dir)}"
|
|
669
730
|
existing_path="${default_dir}/${binary_name}"
|
|
670
731
|
if [[ -f "$existing_path" ]]; then
|
|
671
732
|
step "Already installed: $existing_path"
|
|
@@ -690,6 +751,10 @@ main() {
|
|
|
690
751
|
step "Platform: $platform_key"
|
|
691
752
|
step "Binary: $binary_name"
|
|
692
753
|
|
|
754
|
+
# Everything below downloads, unpacks or writes: check the tooling now, so
|
|
755
|
+
# `--locate` still works on a box that has none of it.
|
|
756
|
+
check_commands
|
|
757
|
+
|
|
693
758
|
# Install directory
|
|
694
759
|
if [[ -z "$INSTALL_DIR" ]]; then
|
|
695
760
|
INSTALL_DIR=$(get_default_install_dir)
|
|
@@ -699,10 +764,15 @@ main() {
|
|
|
699
764
|
|
|
700
765
|
# Ensure install directory exists
|
|
701
766
|
if [[ ! -d "$INSTALL_DIR" ]]; then
|
|
702
|
-
if [[ "$
|
|
703
|
-
|
|
767
|
+
if [[ -e "$INSTALL_DIR" ]]; then
|
|
768
|
+
die "Install path exists but is not a directory: $INSTALL_DIR"
|
|
769
|
+
fi
|
|
770
|
+
if [[ "$DRY_RUN" == true ]]; then
|
|
771
|
+
step "[DRY-RUN] Would create directory: $INSTALL_DIR"
|
|
772
|
+
else
|
|
773
|
+
mkdir -p "$INSTALL_DIR" || die "Could not create install directory: $INSTALL_DIR"
|
|
774
|
+
step "Created directory: $INSTALL_DIR"
|
|
704
775
|
fi
|
|
705
|
-
step "Created directory: $INSTALL_DIR"
|
|
706
776
|
fi
|
|
707
777
|
|
|
708
778
|
local binary_path="${INSTALL_DIR}/${binary_name}"
|
|
@@ -174,17 +174,32 @@ cat > "$TEST_PAGE" <<'HTML'
|
|
|
174
174
|
</body></html>
|
|
175
175
|
HTML
|
|
176
176
|
|
|
177
|
-
|
|
177
|
+
# Serve the fixture from $TEMP_DIR: without --directory, python http.server
|
|
178
|
+
# serves the *current working directory* (the CI workspace / repo checkout),
|
|
179
|
+
# so every run browsed Python's 404 "Error response" page instead of the
|
|
180
|
+
# fixture. Backends that tolerate type/click on a missing element let that
|
|
181
|
+
# pass silently; the loud "no element found for selector [#input1]" failure
|
|
182
|
+
# exposed the harness bug on all three platforms.
|
|
183
|
+
python3 -m http.server "$TEST_PORT" --bind 127.0.0.1 --directory "$TEMP_DIR" > /dev/null 2>&1 &
|
|
178
184
|
HTTP_PID=$!
|
|
179
185
|
TEST_URL="http://127.0.0.1:$TEST_PORT/test.html"
|
|
180
186
|
|
|
181
|
-
# Wait for HTTP server to
|
|
187
|
+
# Wait for the HTTP server to actually serve the fixture. curl -f fails on
|
|
188
|
+
# 4xx/5xx, so a server serving the wrong directory (or not yet ready) cannot
|
|
189
|
+
# silently hand the browser an error page — earlier runs typed into a 404
|
|
190
|
+
# page whenever the interactive commands tolerated a missing element.
|
|
191
|
+
SERVING_OK=""
|
|
182
192
|
for i in $(seq 1 10); do
|
|
183
|
-
if curl -
|
|
193
|
+
if curl -sf -o /dev/null "$TEST_URL" 2>/dev/null; then
|
|
194
|
+
SERVING_OK=1
|
|
184
195
|
break
|
|
185
196
|
fi
|
|
186
197
|
sleep 0.5
|
|
187
198
|
done
|
|
199
|
+
if [ -z "$SERVING_OK" ]; then
|
|
200
|
+
info "FATAL: test page not served at $TEST_URL (python http.server not serving $TEMP_DIR?)"
|
|
201
|
+
exit 1
|
|
202
|
+
fi
|
|
188
203
|
|
|
189
204
|
# ---------------------------------------------------------------------------
|
|
190
205
|
# step 3: determine server URL (let CLI auto-start the server)
|
|
@@ -37,6 +37,21 @@ function Test($name, [ScriptBlock]$block) {
|
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
# ── Harness self-test ──
|
|
41
|
+
# An assertion that throws MUST be counted as a failure. Without this check a
|
|
42
|
+
# harness that silently swallows failures looks perfectly green, which is
|
|
43
|
+
# exactly how this suite's PowerShell sibling (the bash one) stayed green while
|
|
44
|
+
# running no assertions at all.
|
|
45
|
+
$selfTestFail = $script:fail
|
|
46
|
+
Write-Host " (one deliberate FAIL follows -- the harness self-test)" -ForegroundColor DarkGray
|
|
47
|
+
Test "__harness self-test (must be reported as a failure)" { throw "expected failure" }
|
|
48
|
+
if ($script:fail -ne ($selfTestFail + 1)) {
|
|
49
|
+
Write-Host " FATAL the harness does not record failures -- aborting" -ForegroundColor Red
|
|
50
|
+
exit 1
|
|
51
|
+
}
|
|
52
|
+
$script:fail = $selfTestFail
|
|
53
|
+
Write-Host " OK harness self-test (a throwing assertion is counted as a failure)" -ForegroundColor DarkGray
|
|
54
|
+
|
|
40
55
|
function RunScript([string]$scriptArgs, [ref]$exitCode) {
|
|
41
56
|
# Use -Command to control stderr redirect (2> path must come before script args,
|
|
42
57
|
# which -File would pass as literal arguments to the script).
|
|
@@ -52,7 +67,15 @@ function RunScript([string]$scriptArgs, [ref]$exitCode) {
|
|
|
52
67
|
$out = $proc.StandardOutput.ReadToEnd()
|
|
53
68
|
$proc.WaitForExit()
|
|
54
69
|
$exitCode.Value = $proc.ExitCode
|
|
55
|
-
|
|
70
|
+
# Never leave $err as AutomationNull: `$null -eq $err` is true for it, but
|
|
71
|
+
# `$err -notmatch 'x'` returns an empty array, which is falsey -- an
|
|
72
|
+
# assertion written that way can never fail.
|
|
73
|
+
$err = ""
|
|
74
|
+
if (Test-Path $tmpErr) {
|
|
75
|
+
$err = Get-Content $tmpErr -Raw -ErrorAction SilentlyContinue
|
|
76
|
+
Remove-Item $tmpErr -Force -ErrorAction SilentlyContinue
|
|
77
|
+
}
|
|
78
|
+
if ($null -eq $err) { $err = "" }
|
|
56
79
|
return [PSCustomObject]@{ Output = $out; Error = $err; ExitCode = $proc.ExitCode }
|
|
57
80
|
}
|
|
58
81
|
|
|
@@ -217,10 +240,24 @@ Test "-SkipLocal flag accepted" {
|
|
|
217
240
|
if ($ec4 -ne 0) { throw "Exit code: $ec4, output: $($r4.Output)" }
|
|
218
241
|
}
|
|
219
242
|
|
|
220
|
-
Test "-Force
|
|
221
|
-
$
|
|
222
|
-
|
|
223
|
-
|
|
243
|
+
Test "-Force overrides -SkipIfInstalled, and is a live parameter" {
|
|
244
|
+
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) "b4-force-$([System.IO.Path]::GetRandomFileName())"
|
|
245
|
+
New-Item -ItemType Directory -Path $tmp -Force | Out-Null
|
|
246
|
+
foreach ($n in @('browser4-cli-win32-x64.exe', 'browser4-cli-linux-x64', 'browser4-cli-darwin-arm64')) {
|
|
247
|
+
Set-Content -Path (Join-Path $tmp $n) -Value "dummy" -Encoding Ascii
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
$ecA = 0; $rA = RunScript -scriptArgs "-SkipIfInstalled -DryRun -SkipBackend -AddToPath:`$false -InstallDir `"$tmp`"" -exitCode ([ref]$ecA)
|
|
251
|
+
if ($rA.Output -notmatch 'Binary already installed') {
|
|
252
|
+
throw "Expected the -SkipIfInstalled skip path, got: $($rA.Output)"
|
|
253
|
+
}
|
|
254
|
+
$ecB = 0; $rB = RunScript -scriptArgs "-SkipIfInstalled -Force -DryRun -SkipBackend -AddToPath:`$false -InstallDir `"$tmp`"" -exitCode ([ref]$ecB)
|
|
255
|
+
if ($ecB -ne 0) { throw "-Force must be accepted, exit code: $ecB" }
|
|
256
|
+
if ($rB.Output -match 'Binary already installed') {
|
|
257
|
+
throw "-Force must override -SkipIfInstalled, got: $($rB.Output)"
|
|
258
|
+
}
|
|
259
|
+
} finally {
|
|
260
|
+
Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue
|
|
224
261
|
}
|
|
225
262
|
}
|
|
226
263
|
|
|
@@ -236,8 +273,8 @@ Test "-Source github accepted" {
|
|
|
236
273
|
|
|
237
274
|
Test "-Source invalid rejected" {
|
|
238
275
|
$ec8 = 0; $r8 = RunScript -scriptArgs "-Source invalid -DryRun" -exitCode ([ref]$ec8)
|
|
239
|
-
if ($
|
|
240
|
-
throw "-Source invalid
|
|
276
|
+
if ($ec8 -eq 0) {
|
|
277
|
+
throw "-Source invalid must fail, output: $($r8.Output)"
|
|
241
278
|
}
|
|
242
279
|
}
|
|
243
280
|
|
|
@@ -246,9 +283,33 @@ Test "-Silent flag accepted" {
|
|
|
246
283
|
if ($ec9 -ne 0) { throw "Exit code: $ec9, output: $($r9.Output)" }
|
|
247
284
|
}
|
|
248
285
|
|
|
249
|
-
Test "-Version
|
|
250
|
-
$ec10 = 0; $r10 = RunScript -scriptArgs "-Version v4.11.0 -DryRun" -exitCode ([ref]$ec10)
|
|
286
|
+
Test "-Version reaches the download URLs" {
|
|
287
|
+
$ec10 = 0; $r10 = RunScript -scriptArgs "-Version v4.11.0 -DryRun -AddToPath:`$false" -exitCode ([ref]$ec10)
|
|
251
288
|
if ($ec10 -ne 0) { throw "Exit code: $ec10, output: $($r10.Output)" }
|
|
289
|
+
if ($r10.Output -notmatch 'releases/download/v4\.11\.0/') {
|
|
290
|
+
throw "The tag never reached the URLs: $($r10.Output)"
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
Test "-Version accepts a bare semver and prefixes it with v" {
|
|
295
|
+
$ec11 = 0; $r11 = RunScript -scriptArgs "-Version 4.11.0 -DryRun -AddToPath:`$false" -exitCode ([ref]$ec11)
|
|
296
|
+
if ($ec11 -ne 0) { throw "Exit code: $ec11, output: $($r11.Output)" }
|
|
297
|
+
if ($r11.Output -notmatch 'releases/download/v4\.11\.0/') {
|
|
298
|
+
throw "Bare semver was not normalised to v4.11.0: $($r11.Output)"
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
Test "-Version rejects a path-traversal tag" {
|
|
303
|
+
$ec12 = 0; $r12 = RunScript -scriptArgs "-Version ../../evil -DryRun" -exitCode ([ref]$ec12)
|
|
304
|
+
if ($ec12 -eq 0) { throw "Traversal tag must be rejected, output: $($r12.Output)" }
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
Test "-DryRun does not probe the installed CLI" {
|
|
308
|
+
$ec13 = 0; $r13 = RunScript -scriptArgs "-DryRun -AddToPath:`$false" -exitCode ([ref]$ec13)
|
|
309
|
+
if ($ec13 -ne 0) { throw "Exit code: $ec13, output: $($r13.Output)" }
|
|
310
|
+
if ($r13.Output -match 'Backend already installed') {
|
|
311
|
+
throw "-DryRun ran 'status' on the installed CLI: $($r13.Output)"
|
|
312
|
+
}
|
|
252
313
|
}
|
|
253
314
|
|
|
254
315
|
Write-Host ""
|
|
@@ -256,10 +317,25 @@ Write-Host ""
|
|
|
256
317
|
# ── Functions via dot-source ──
|
|
257
318
|
Write-Host "--- Functions ---" -ForegroundColor Cyan
|
|
258
319
|
|
|
259
|
-
# Strip trailing Main call and dot-source for function-level tests
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
320
|
+
# Strip the trailing `Main` call and dot-source for function-level tests.
|
|
321
|
+
# Strip it by AST extent, never by regex: if a line is ever appended after
|
|
322
|
+
# `Main`, a regex silently stops matching and this suite would perform a REAL
|
|
323
|
+
# install and edit the user PATH.
|
|
324
|
+
$installAst = [System.Management.Automation.Language.Parser]::ParseFile($installScript, [ref]$null, [ref]$null)
|
|
325
|
+
$mainCall = @($installAst.FindAll({
|
|
326
|
+
param($node)
|
|
327
|
+
$node -is [System.Management.Automation.Language.CommandAst] -and
|
|
328
|
+
$node.GetCommandName() -eq 'Main' -and
|
|
329
|
+
$node.Extent.Text.Trim() -eq 'Main'
|
|
330
|
+
}, $true)) | Select-Object -Last 1
|
|
331
|
+
if (-not $mainCall) {
|
|
332
|
+
throw "Could not locate the top-level 'Main' invocation in $installScript -- refusing to dot-source the installer"
|
|
333
|
+
}
|
|
334
|
+
$allLines = Get-Content $installScript
|
|
335
|
+
$scriptContent = ($allLines[0..($mainCall.Extent.StartLineNumber - 2)] -join "`n") -replace '\$ErrorActionPreference\s*=\s*"Stop"', ''
|
|
336
|
+
if ($scriptContent -match '(?m)^\s*Main\s*$') {
|
|
337
|
+
throw "The stripped copy still invokes Main -- refusing to dot-source the installer"
|
|
338
|
+
}
|
|
263
339
|
$sb = [ScriptBlock]::Create($scriptContent)
|
|
264
340
|
|
|
265
341
|
& {
|
|
@@ -313,6 +389,31 @@ $sb = [ScriptBlock]::Create($scriptContent)
|
|
|
313
389
|
if ($result -ne $null) { throw "Expected null, got: $result" }
|
|
314
390
|
}
|
|
315
391
|
|
|
392
|
+
Test "Find-LocalBinary finds a bundled binary and enforces the 100 KB gate" {
|
|
393
|
+
# The bundled-binary path is what a sideload install uses; it used to be
|
|
394
|
+
# untestable because the dot-sourced copy never had a usable ScriptDir.
|
|
395
|
+
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) "b4-local-$([System.IO.Path]::GetRandomFileName())"
|
|
396
|
+
New-Item -ItemType Directory -Path $tmp -Force | Out-Null
|
|
397
|
+
try {
|
|
398
|
+
$name = Get-BinaryName -PlatformKey (Get-PlatformKey)
|
|
399
|
+
[System.IO.File]::WriteAllBytes((Join-Path $tmp $name), (New-Object byte[] 204800))
|
|
400
|
+
$savedScriptDir = $ScriptDir
|
|
401
|
+
try {
|
|
402
|
+
$ScriptDir = $tmp
|
|
403
|
+
$found = Find-LocalBinary -BinaryName $name
|
|
404
|
+
if (-not $found) { throw "A 200 KB bundled binary was not found in $tmp" }
|
|
405
|
+
[System.IO.File]::WriteAllBytes((Join-Path $tmp 'browser4-cli-too-small'), (New-Object byte[] 10))
|
|
406
|
+
if (Find-LocalBinary -BinaryName 'browser4-cli-too-small') {
|
|
407
|
+
throw "A 10-byte file passed the 100 KB gate"
|
|
408
|
+
}
|
|
409
|
+
} finally {
|
|
410
|
+
$ScriptDir = $savedScriptDir
|
|
411
|
+
}
|
|
412
|
+
} finally {
|
|
413
|
+
Remove-Item $tmp -Recurse -Force -ErrorAction SilentlyContinue
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
316
417
|
Test "Test-LocalBinary returns false for empty string" {
|
|
317
418
|
if (Test-LocalBinary -Path "") { throw "Should be false" }
|
|
318
419
|
}
|
|
@@ -331,9 +432,43 @@ $sb = [ScriptBlock]::Create($scriptContent)
|
|
|
331
432
|
if ($action -ne "upgrade") { throw "Expected 'upgrade', got: $action" }
|
|
332
433
|
}
|
|
333
434
|
|
|
334
|
-
Test "Get-BackendAction defaults to
|
|
435
|
+
Test "Get-BackendAction defaults to install on empty status" {
|
|
436
|
+
# An empty status means "no CLI answer" (fresh machine, or `status`
|
|
437
|
+
# failed). Defaulting to upgrade would run `upgrade` on a machine that
|
|
438
|
+
# has nothing to upgrade.
|
|
335
439
|
$action = Get-BackendAction -StatusOutput ""
|
|
336
|
-
if ($action -ne "
|
|
440
|
+
if ($action -ne "install") { throw "Expected 'install', got: $action" }
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
Test "Test-BinaryMagic accepts ELF, PE and Mach-O" {
|
|
444
|
+
$tmp = [System.IO.Path]::GetTempFileName()
|
|
445
|
+
try {
|
|
446
|
+
$cases = @{
|
|
447
|
+
'elf' = [byte[]]@(0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01)
|
|
448
|
+
'pe' = [byte[]]@(0x4d, 0x5a, 0x90, 0x00, 0x03)
|
|
449
|
+
'macho' = [byte[]]@(0xcf, 0xfa, 0xed, 0xfe, 0x07)
|
|
450
|
+
}
|
|
451
|
+
foreach ($name in $cases.Keys) {
|
|
452
|
+
[System.IO.File]::WriteAllBytes($tmp, $cases[$name])
|
|
453
|
+
if (-not (Test-BinaryMagic -Path $tmp)) { throw "Rejected a valid $name header" }
|
|
454
|
+
}
|
|
455
|
+
} finally {
|
|
456
|
+
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
Test "Test-BinaryMagic rejects an HTML error page" {
|
|
461
|
+
$tmp = [System.IO.Path]::GetTempFileName()
|
|
462
|
+
try {
|
|
463
|
+
[System.IO.File]::WriteAllText($tmp, "<html><body>404 Not Found</body></html>")
|
|
464
|
+
if (Test-BinaryMagic -Path $tmp) { throw "An HTML body was accepted as a binary" }
|
|
465
|
+
} finally {
|
|
466
|
+
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
Test "Test-BinaryMagic rejects a missing file" {
|
|
471
|
+
if (Test-BinaryMagic -Path "/nonexistent/binary-xyz") { throw "A missing file was accepted" }
|
|
337
472
|
}
|
|
338
473
|
}
|
|
339
474
|
|