| 458 | } |
| 459 | |
| 460 | function extractZipOnWindows(archivePath, destPath, requiredNames, extractNames) { |
| 461 | // -EncodedCommand is a constant program. Paths travel only through the child |
| 462 | // environment and are consumed with -LiteralPath, so PowerShell never parses |
| 463 | // user/TEMP path bytes as source code or wildcard syntax. |
| 464 | const script = [ |
| 465 | "$ErrorActionPreference = 'Stop'", |
| 466 | 'Add-Type -AssemblyName System.IO.Compression.FileSystem', |
| 467 | '$zip = [System.IO.Compression.ZipFile]::OpenRead($env:CBM_NPM_ARCHIVE_PATH)', |
| 468 | "$seen = New-Object 'System.Collections.Generic.HashSet[string]' ([System.StringComparer]::OrdinalIgnoreCase)", |
| 469 | "$requiredNames = @($env:CBM_NPM_REQUIRED_NAMES.Split('|'))", |
| 470 | '$targetCounts = @{}', |
| 471 | 'foreach ($requiredName in $requiredNames) { $targetCounts[$requiredName] = 0 }', |
| 472 | 'try { foreach ($entry in $zip.Entries) { ' + |
| 473 | "$name = $entry.FullName.Replace('\\', '/'); " + |
| 474 | "$directory = $name.EndsWith('/'); " + |
| 475 | "$segmentsPath = if ($directory) { $name.TrimEnd('/') } else { $name }; " + |
| 476 | "$segments = @($segmentsPath.Split('/')); " + |
| 477 | "if ([string]::IsNullOrEmpty($segmentsPath) -or $name.StartsWith('/') -or " + |
| 478 | "$name.Contains(':') -or $segments -contains '' -or " + |
| 479 | "$segments -contains '.' -or $segments -contains '..' -or " + |
| 480 | "@($segments | Where-Object { $_.EndsWith('.') -or $_.EndsWith(' ') }).Count -gt 0) { " + |
| 481 | "throw \"unsafe zip entry path: $($entry.FullName)\" }; " + |
| 482 | 'if (-not $seen.Add($segmentsPath)) { ' + |
| 483 | "throw \"duplicate or case-conflicting zip entry: $($entry.FullName)\" }; " + |
| 484 | 'foreach ($requiredName in $requiredNames) { ' + |
| 485 | 'if ($name -ceq $requiredName) { ' + |
| 486 | '$targetCounts[$requiredName] = $targetCounts[$requiredName] + 1 } } ' + |
| 487 | '} } finally { $zip.Dispose() }', |
| 488 | 'foreach ($requiredName in $requiredNames) { ' + |
| 489 | 'if ($targetCounts[$requiredName] -ne 1) { ' + |
| 490 | 'throw "archive must contain exactly one $requiredName" } }', |
| 491 | 'if ($seen.Count -ne $requiredNames.Count) { ' + |
| 492 | 'throw "archive does not match the exact release root-file allowlist" }', |
| 493 | "$extractNames = @($env:CBM_NPM_EXTRACT_NAMES.Split('|'))", |
| 494 | '$extractZip = [System.IO.Compression.ZipFile]::OpenRead($env:CBM_NPM_ARCHIVE_PATH)', |
| 495 | 'try { foreach ($extractName in $extractNames) { ' + |
| 496 | '$entry = @($extractZip.Entries | Where-Object { $_.FullName -ceq $extractName })[0]; ' + |
| 497 | '[System.IO.Compression.ZipFileExtensions]::ExtractToFile(' + |
| 498 | '$entry, (Join-Path $env:CBM_NPM_DEST_PATH $extractName), $false) ' + |
| 499 | '} } finally { $extractZip.Dispose() }', |
| 500 | ].join('; '); |
| 501 | const encoded = Buffer.from(script, 'utf16le').toString('base64'); |
| 502 | execFileSync('powershell', [ |
| 503 | '-NoLogo', '-NoProfile', '-NonInteractive', '-EncodedCommand', encoded, |
| 504 | ], { |
| 505 | env: { |
| 506 | ...process.env, |
| 507 | CBM_NPM_ARCHIVE_PATH: archivePath, |
| 508 | CBM_NPM_DEST_PATH: destPath, |
| 509 | CBM_NPM_REQUIRED_NAMES: requiredNames.join('|'), |
| 510 | CBM_NPM_EXTRACT_NAMES: extractNames.join('|'), |
| 511 | }, |
| 512 | stdio: 'inherit', |
| 513 | windowsHide: true, |
| 514 | }); |
| 515 | } |
| 516 | |
| 517 | // Fetch checksums.txt and verify the archive hash. |