browser4-cli 4.13.18 → 4.13.19
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/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/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}"
|
|
@@ -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
|
|
|
@@ -27,8 +27,8 @@ dim() { echo -e "\033[2m$*\033[0m"; }
|
|
|
27
27
|
|
|
28
28
|
test() {
|
|
29
29
|
local name="$1"
|
|
30
|
-
|
|
31
|
-
if "
|
|
30
|
+
shift
|
|
31
|
+
if "$@"; then
|
|
32
32
|
PASS=$((PASS + 1))
|
|
33
33
|
green " PASS $name"
|
|
34
34
|
else
|
|
@@ -109,26 +109,65 @@ test "--source invalid rejected" bash -c "
|
|
|
109
109
|
bash '$INSTALL_SCRIPT' --source invalid --dry-run 2>&1 | grep -qi 'github.*oss\|must be'
|
|
110
110
|
"
|
|
111
111
|
|
|
112
|
+
test "--version rejects a following option as its value" bash -c "
|
|
113
|
+
rc=0
|
|
114
|
+
out=\$(bash '$INSTALL_SCRIPT' --version --dry-run 2>&1) || rc=\$?
|
|
115
|
+
[[ \$rc -ne 0 ]] && echo \"\$out\" | grep -q 'requires a value'
|
|
116
|
+
"
|
|
117
|
+
|
|
118
|
+
test "--install-dir rejects a following option as its value" bash -c "
|
|
119
|
+
rc=0
|
|
120
|
+
out=\$(bash '$INSTALL_SCRIPT' --install-dir --silent 2>&1) || rc=\$?
|
|
121
|
+
[[ \$rc -ne 0 ]] && echo \"\$out\" | grep -q 'requires a value'
|
|
122
|
+
"
|
|
123
|
+
|
|
124
|
+
test "--version accepts a bare semver and prefixes it with v" bash -c "
|
|
125
|
+
bash '$INSTALL_SCRIPT' --locate --version 4.13.18 2>&1 | grep -q 'releases/download/v4\.13\.18/'
|
|
126
|
+
"
|
|
127
|
+
|
|
128
|
+
test "--version with the v prefix is left untouched" bash -c "
|
|
129
|
+
bash '$INSTALL_SCRIPT' --locate --version v4.13.18 2>&1 | grep -q 'releases/download/v4\.13\.18/'
|
|
130
|
+
"
|
|
131
|
+
|
|
112
132
|
test "--dry-run flag accepted" bash -c "
|
|
113
|
-
bash '$INSTALL_SCRIPT' --dry-run 2>&1 | grep -
|
|
133
|
+
bash '$INSTALL_SCRIPT' --dry-run 2>&1 | grep -q 'DRY-RUN'
|
|
114
134
|
"
|
|
115
135
|
|
|
116
|
-
test "--silent
|
|
117
|
-
bash '$INSTALL_SCRIPT' --silent --dry-run 2>&1
|
|
136
|
+
test "--silent suppresses progress output" bash -c "
|
|
137
|
+
out=\$(bash '$INSTALL_SCRIPT' --silent --dry-run 2>&1)
|
|
138
|
+
echo \"\$out\" | grep -q 'DRY-RUN' && ! echo \"\$out\" | grep -q 'Install:'
|
|
118
139
|
"
|
|
119
140
|
|
|
120
141
|
test "--skip-local flag accepted" bash -c "
|
|
121
|
-
bash '$INSTALL_SCRIPT' --skip-local --dry-run 2>&1 | grep -
|
|
142
|
+
bash '$INSTALL_SCRIPT' --skip-local --dry-run 2>&1 | grep -q 'Skipping local binary check'
|
|
122
143
|
"
|
|
123
144
|
|
|
124
145
|
test "--no-path flag accepted" bash -c "
|
|
125
|
-
bash '$INSTALL_SCRIPT' --no-path --dry-run 2>&1 | grep -
|
|
146
|
+
! bash '$INSTALL_SCRIPT' --no-path --dry-run 2>&1 | grep -q 'Added to PATH'
|
|
126
147
|
"
|
|
127
148
|
|
|
128
149
|
test "unknown arg rejected" bash -c "
|
|
129
150
|
bash '$INSTALL_SCRIPT' --bogus-flag 2>&1 | grep -q 'Unknown argument'
|
|
130
151
|
"
|
|
131
152
|
|
|
153
|
+
# A PATH that has everything the diagnostics need except the download tooling.
|
|
154
|
+
# `--locate` must still answer; a real install must refuse with a clear message
|
|
155
|
+
# instead of failing later with 'curl: command not found'.
|
|
156
|
+
minimal_path_locate_works() {
|
|
157
|
+
local dir; dir="$(mktemp -d)"
|
|
158
|
+
local bash_bin; bash_bin=$(command -v bash)
|
|
159
|
+
local tool tool_path
|
|
160
|
+
for tool in uname stat cat grep awk sed dirname basename tr od; do
|
|
161
|
+
tool_path=$(command -v "$tool") && ln -sf "$tool_path" "$dir/$tool"
|
|
162
|
+
done
|
|
163
|
+
local locate_rc=0 install_rc=0
|
|
164
|
+
PATH="$dir" "$bash_bin" "$INSTALL_SCRIPT" --locate >/dev/null 2>&1 || locate_rc=$?
|
|
165
|
+
PATH="$dir" "$bash_bin" "$INSTALL_SCRIPT" --dry-run --no-path >/dev/null 2>&1 || install_rc=$?
|
|
166
|
+
rm -rf "$dir"
|
|
167
|
+
[[ $locate_rc -eq 0 ]] && [[ $install_rc -ne 0 ]]
|
|
168
|
+
}
|
|
169
|
+
test "--locate needs no download tooling, installing does" minimal_path_locate_works
|
|
170
|
+
|
|
132
171
|
echo ""
|
|
133
172
|
|
|
134
173
|
# ── Locate mode ────────────────────────────────────────
|
|
@@ -191,6 +230,103 @@ test "OSS URL NOT using broken download/latest pattern" bash -c "
|
|
|
191
230
|
|
|
192
231
|
echo ""
|
|
193
232
|
|
|
233
|
+
# ── Download integrity (stubbed curl -- no network) ────
|
|
234
|
+
|
|
235
|
+
echo "--- Download integrity ---" | cyan
|
|
236
|
+
|
|
237
|
+
STUB_DIR="$(mktemp -d)"
|
|
238
|
+
mkdir -p "$STUB_DIR"
|
|
239
|
+
|
|
240
|
+
cat > "$STUB_DIR/curl" << 'STUBEOF'
|
|
241
|
+
#!/usr/bin/env bash
|
|
242
|
+
# curl stub for the installer tests. Honours -o <file>, takes the body from
|
|
243
|
+
# STUB_BODY (html = an error page, elf = a native binary), reports STUB_STATUS
|
|
244
|
+
# and records its own command line in STUB_ARGS_FILE so a test can assert on
|
|
245
|
+
# the headers the installer would have sent.
|
|
246
|
+
out=""
|
|
247
|
+
printf '%s\n' "$*" >> "${STUB_ARGS_FILE:-/dev/null}"
|
|
248
|
+
while [[ $# -gt 0 ]]; do
|
|
249
|
+
case "$1" in
|
|
250
|
+
-o) out="$2"; shift 2 ;;
|
|
251
|
+
*) shift ;;
|
|
252
|
+
esac
|
|
253
|
+
done
|
|
254
|
+
if [[ "${STUB_STATUS:-200}" == "200" ]]; then
|
|
255
|
+
if [[ "${STUB_BODY:-elf}" == "html" ]]; then
|
|
256
|
+
{ printf '<html>'; head -c 200000 /dev/zero | tr '\0' 'x'; printf '</html>'; } > "$out"
|
|
257
|
+
else
|
|
258
|
+
{ printf '\177ELF\002\001\001\000'; head -c 200000 /dev/zero; } > "$out"
|
|
259
|
+
fi
|
|
260
|
+
fi
|
|
261
|
+
echo "${STUB_STATUS:-200}"
|
|
262
|
+
exit "${STUB_EXIT:-0}"
|
|
263
|
+
STUBEOF
|
|
264
|
+
chmod +x "$STUB_DIR/curl"
|
|
265
|
+
|
|
266
|
+
stub_install() {
|
|
267
|
+
# $1 = body kind (html|elf), $2 = source, $3 = args file, $4 = extra env
|
|
268
|
+
local body="$1" source="$2" args_file="$3" dir rc=0
|
|
269
|
+
dir="$(mktemp -d)"
|
|
270
|
+
STUB_BODY="$body" STUB_ARGS_FILE="$args_file" \
|
|
271
|
+
PATH="$STUB_DIR:$PATH" bash "$INSTALL_SCRIPT" \
|
|
272
|
+
--source "$source" --no-path --skip-backend --skip-local \
|
|
273
|
+
--install-dir "$dir/inst" >"$dir/out" 2>&1 || rc=$?
|
|
274
|
+
printf '%s\n' "$rc" > "$dir/rc"
|
|
275
|
+
echo "$dir"
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
download_rejects_html() {
|
|
279
|
+
local args; args="$(mktemp)"
|
|
280
|
+
local dir; dir=$(stub_install html oss "$args")
|
|
281
|
+
local rc; rc=$(cat "$dir/rc")
|
|
282
|
+
local rejected=0
|
|
283
|
+
grep -q 'not a native executable' "$dir/out" && rejected=1
|
|
284
|
+
rm -rf "$dir" "$args"
|
|
285
|
+
[[ "$rc" -ne 0 ]] && [[ $rejected -eq 1 ]]
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
download_accepts_elf() {
|
|
289
|
+
local args; args="$(mktemp)"
|
|
290
|
+
local dir; dir=$(stub_install elf oss "$args")
|
|
291
|
+
local rc; rc=$(cat "$dir/rc")
|
|
292
|
+
local installed; installed=$(find "$dir/inst" -maxdepth 1 -type f -name 'browser4-cli-*' 2>/dev/null | head -1)
|
|
293
|
+
local ok=1
|
|
294
|
+
[[ "$rc" -eq 0 ]] && [[ -n "$installed" ]] && [[ -x "$installed" ]] && ok=0
|
|
295
|
+
rm -rf "$dir" "$args"
|
|
296
|
+
[[ $ok -eq 0 ]]
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
token_not_sent_to_oss() {
|
|
300
|
+
local args; args="$(mktemp)"
|
|
301
|
+
export GITHUB_TOKEN=stub-secret-token
|
|
302
|
+
local dir; dir=$(stub_install elf oss "$args")
|
|
303
|
+
unset GITHUB_TOKEN
|
|
304
|
+
local leaked=0
|
|
305
|
+
grep -q 'stub-secret-token' "$args" && leaked=1
|
|
306
|
+
rm -rf "$dir" "$args"
|
|
307
|
+
[[ $leaked -eq 0 ]]
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
token_sent_to_github() {
|
|
311
|
+
local args; args="$(mktemp)"
|
|
312
|
+
export GITHUB_TOKEN=stub-secret-token
|
|
313
|
+
local dir; dir=$(stub_install elf github "$args")
|
|
314
|
+
unset GITHUB_TOKEN
|
|
315
|
+
local sent=0
|
|
316
|
+
grep -q 'stub-secret-token' "$args" && sent=1
|
|
317
|
+
rm -rf "$dir" "$args"
|
|
318
|
+
[[ $sent -eq 1 ]]
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
test "download rejects an HTML error page (bad magic bytes)" download_rejects_html
|
|
322
|
+
test "download accepts a native binary body" download_accepts_elf
|
|
323
|
+
test "GITHUB_TOKEN is never sent to the OSS mirror" token_not_sent_to_oss
|
|
324
|
+
test "GITHUB_TOKEN is sent to GitHub" token_sent_to_github
|
|
325
|
+
|
|
326
|
+
rm -rf "$STUB_DIR"
|
|
327
|
+
|
|
328
|
+
echo ""
|
|
329
|
+
|
|
194
330
|
# ── Functions (extracted via copy without main call) ──
|
|
195
331
|
|
|
196
332
|
echo "--- Functions ---" | cyan
|
|
@@ -232,7 +368,9 @@ test "get_default_install_dir returns non-empty" bash -c "
|
|
|
232
368
|
|
|
233
369
|
test "detect_china_locale returns 0 or 1 (no crash)" bash -c "
|
|
234
370
|
source '$FUNC_TEST_SCRIPT' >/dev/null 2>&1
|
|
235
|
-
|
|
371
|
+
rc=0
|
|
372
|
+
detect_china_locale || rc=\$?
|
|
373
|
+
[[ \$rc -eq 0 || \$rc -eq 1 ]]
|
|
236
374
|
"
|
|
237
375
|
|
|
238
376
|
test "detect_os returns valid OS" bash -c "
|
|
@@ -323,7 +461,10 @@ test "header is ASCII (no box-drawing chars)" bash -c "
|
|
|
323
461
|
"
|
|
324
462
|
|
|
325
463
|
test "header does not contain Unicode box-drawing" bash -c "
|
|
326
|
-
|
|
464
|
+
# Portable (no grep -P): drop the ANSI colour escapes first, then any byte
|
|
465
|
+
# outside printable ASCII would be a non-ASCII character; the installer
|
|
466
|
+
# prints ASCII only.
|
|
467
|
+
! bash '$INSTALL_SCRIPT' --locate 2>&1 | tr -d '\033' | LC_ALL=C grep -q '[^ -~]'
|
|
327
468
|
"
|
|
328
469
|
|
|
329
470
|
echo ""
|
|
@@ -333,7 +474,13 @@ echo ""
|
|
|
333
474
|
echo "--- Edge cases ---" | cyan
|
|
334
475
|
|
|
335
476
|
test "double dash in --version handled (no operator parsing)" bash -c "
|
|
336
|
-
|
|
477
|
+
# '--version --dry-run' must not be read as the tag '--dry-run'; the value
|
|
478
|
+
# has to be a release tag, and a bare semver gets the 'v' prefix.
|
|
479
|
+
rc=0
|
|
480
|
+
out=\$(bash '$INSTALL_SCRIPT' --version --dry-run 2>&1) || rc=\$?
|
|
481
|
+
[[ \$rc -ne 0 ]] || exit 1
|
|
482
|
+
echo \"\$out\" | grep -q 'looks like another option' || exit 1
|
|
483
|
+
bash '$INSTALL_SCRIPT' --locate --version 4.13.18 2>&1 | grep -q 'releases/download/v4\.13\.18/'
|
|
337
484
|
"
|
|
338
485
|
|
|
339
486
|
test "empty --version with dry-run doesn't download" bash -c "
|