fix(launcher): harden Campaign LA11 gate evidence
This commit is contained in:
parent
134edabed2
commit
accd01a008
16 changed files with 1820 additions and 210 deletions
|
|
@ -37,6 +37,35 @@ if (-not [IO.Path]::IsPathFullyQualified($OutputDirectory)) {
|
|||
}
|
||||
$OutputDirectory = [IO.Path]::TrimEndingDirectorySeparator(
|
||||
[IO.Path]::GetFullPath($OutputDirectory))
|
||||
|
||||
function Assert-NoReparseAncestry([string]$Path, [string]$Description) {
|
||||
$cursor = [IO.Path]::TrimEndingDirectorySeparator([IO.Path]::GetFullPath($Path))
|
||||
while (-not (Test-Path -LiteralPath $cursor)) {
|
||||
$parent = [IO.Path]::GetDirectoryName($cursor)
|
||||
if ([string]::IsNullOrEmpty($parent) -or $parent -ceq $cursor) { break }
|
||||
$cursor = $parent
|
||||
}
|
||||
while (-not [string]::IsNullOrEmpty($cursor)) {
|
||||
$item = Get-Item -LiteralPath $cursor -Force
|
||||
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
||||
throw "$Description has a reparse point in its ancestry."
|
||||
}
|
||||
$parent = [IO.Directory]::GetParent($cursor)
|
||||
if ($null -eq $parent) { break }
|
||||
$cursor = $parent.FullName
|
||||
}
|
||||
}
|
||||
|
||||
function Test-SameOrDescendant([string]$Path, [string]$Ancestor) {
|
||||
$comparison = if ($IsWindows) {
|
||||
[StringComparison]::OrdinalIgnoreCase
|
||||
} else { [StringComparison]::Ordinal }
|
||||
if ([string]::Equals($Path, $Ancestor, $comparison)) { return $true }
|
||||
$prefix = $Ancestor + [IO.Path]::DirectorySeparatorChar
|
||||
return $Path.StartsWith($prefix, $comparison)
|
||||
}
|
||||
|
||||
Assert-NoReparseAncestry $OutputDirectory 'Output directory'
|
||||
if ($Port -lt 1024 -or $Port -gt 65535) { throw '-Port must be 1024..65535.' }
|
||||
$semver = '^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$'
|
||||
if ($VersionA -notmatch $semver -or $VersionB -notmatch $semver -or
|
||||
|
|
@ -73,6 +102,11 @@ foreach ($key in @($sources.Keys)) {
|
|||
if (-not $DryRun -and -not (Test-Path -LiteralPath $source -PathType Container)) {
|
||||
throw "Payload source '$key' does not exist: $source"
|
||||
}
|
||||
Assert-NoReparseAncestry $source "Payload source '$key'"
|
||||
if ((Test-SameOrDescendant $OutputDirectory $source) -or
|
||||
(Test-SameOrDescendant $source $OutputDirectory)) {
|
||||
throw "Output directory and payload source '$key' must not overlap."
|
||||
}
|
||||
}
|
||||
|
||||
function Require-PayloadFile([string]$Key, [string]$Name) {
|
||||
|
|
@ -98,6 +132,7 @@ if (Test-Path -LiteralPath $OutputDirectory) {
|
|||
}
|
||||
}
|
||||
else { $null = New-Item -ItemType Directory -Path $OutputDirectory }
|
||||
Assert-NoReparseAncestry $OutputDirectory 'Output directory'
|
||||
|
||||
if ($DryRun) {
|
||||
$plan = [ordered]@{
|
||||
|
|
@ -121,6 +156,73 @@ Add-Type -AssemblyName System.IO.Compression
|
|||
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
||||
$fixedTimestamp = [DateTimeOffset]::new(2000, 1, 1, 0, 0, 0, [TimeSpan]::Zero)
|
||||
|
||||
function Get-LittleEndianUInt16([byte[]]$Bytes, [int]$Offset) {
|
||||
return [int]$Bytes[$Offset] -bor ([int]$Bytes[$Offset + 1] -shl 8)
|
||||
}
|
||||
|
||||
function Get-LittleEndianUInt32([byte[]]$Bytes, [int]$Offset) {
|
||||
return [uint32]([uint32]$Bytes[$Offset] -bor
|
||||
([uint32]$Bytes[$Offset + 1] -shl 8) -bor
|
||||
([uint32]$Bytes[$Offset + 2] -shl 16) -bor
|
||||
([uint32]$Bytes[$Offset + 3] -shl 24))
|
||||
}
|
||||
|
||||
function Set-DeterministicZipHostPlatform([string]$Path) {
|
||||
[byte[]]$bytes = [IO.File]::ReadAllBytes($Path)
|
||||
$minimumEocdSize = 22
|
||||
if ($bytes.Length -lt $minimumEocdSize) {
|
||||
throw "Generated ZIP is too short: $Path"
|
||||
}
|
||||
|
||||
$eocd = -1
|
||||
$minimumOffset = [Math]::Max(0, $bytes.Length - 65557)
|
||||
for ($offset = $bytes.Length - $minimumEocdSize; $offset -ge $minimumOffset; $offset--) {
|
||||
if ((Get-LittleEndianUInt32 $bytes $offset) -eq 0x06054b50) {
|
||||
$commentLength = Get-LittleEndianUInt16 $bytes ($offset + 20)
|
||||
if ($offset + $minimumEocdSize + $commentLength -eq $bytes.Length) {
|
||||
$eocd = $offset
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if ($eocd -lt 0) { throw "Generated ZIP has no valid end record: $Path" }
|
||||
if ((Get-LittleEndianUInt16 $bytes ($eocd + 4)) -ne 0 -or
|
||||
(Get-LittleEndianUInt16 $bytes ($eocd + 6)) -ne 0) {
|
||||
throw "Generated ZIP unexpectedly spans multiple disks: $Path"
|
||||
}
|
||||
|
||||
$entriesOnDisk = Get-LittleEndianUInt16 $bytes ($eocd + 8)
|
||||
$entryCount = Get-LittleEndianUInt16 $bytes ($eocd + 10)
|
||||
if ($entriesOnDisk -ne $entryCount) {
|
||||
throw "Generated ZIP central-directory count is inconsistent: $Path"
|
||||
}
|
||||
$centralSize = Get-LittleEndianUInt32 $bytes ($eocd + 12)
|
||||
$centralOffset = Get-LittleEndianUInt32 $bytes ($eocd + 16)
|
||||
if ([uint64]$centralOffset + [uint64]$centralSize -ne [uint64]$eocd) {
|
||||
throw "Generated ZIP central-directory bounds are inconsistent: $Path"
|
||||
}
|
||||
|
||||
[uint64]$cursor = $centralOffset
|
||||
for ($index = 0; $index -lt $entryCount; $index++) {
|
||||
if ($cursor + 46 -gt $eocd -or
|
||||
(Get-LittleEndianUInt32 $bytes ([int]$cursor)) -ne 0x02014b50) {
|
||||
throw "Generated ZIP central-directory entry is invalid: $Path"
|
||||
}
|
||||
# ZipArchive intentionally stamps the creating host (FAT on Windows,
|
||||
# Unix on Linux) in the upper byte of "version made by". Normalize it
|
||||
# to FAT; permissions are already explicit in ExternalAttributes.
|
||||
$bytes[[int]$cursor + 5] = 0
|
||||
$nameLength = Get-LittleEndianUInt16 $bytes ([int]$cursor + 28)
|
||||
$extraLength = Get-LittleEndianUInt16 $bytes ([int]$cursor + 30)
|
||||
$commentLength = Get-LittleEndianUInt16 $bytes ([int]$cursor + 32)
|
||||
$cursor += 46 + $nameLength + $extraLength + $commentLength
|
||||
}
|
||||
if ($cursor -ne $eocd) {
|
||||
throw "Generated ZIP central-directory length is inconsistent: $Path"
|
||||
}
|
||||
[IO.File]::WriteAllBytes($Path, $bytes)
|
||||
}
|
||||
|
||||
function New-DeterministicZip(
|
||||
[string]$SourceDirectory,
|
||||
[string]$Destination,
|
||||
|
|
@ -141,17 +243,30 @@ function New-DeterministicZip(
|
|||
$true,
|
||||
[Text.Encoding]::UTF8)
|
||||
try {
|
||||
$files = @(Get-ChildItem -LiteralPath $SourceDirectory -File -Recurse |
|
||||
Sort-Object { [IO.Path]::GetRelativePath($SourceDirectory, $_.FullName).Replace('\', '/') })
|
||||
foreach ($file in $files) {
|
||||
if (($file.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
||||
throw "Payload contains a reparse point: $($file.FullName)"
|
||||
$allEntries = @(Get-ChildItem -LiteralPath $SourceDirectory -Force -Recurse)
|
||||
foreach ($item in $allEntries) {
|
||||
if (($item.Attributes -band [IO.FileAttributes]::ReparsePoint) -ne 0) {
|
||||
throw "Payload contains a reparse point: $($item.FullName)"
|
||||
}
|
||||
$relative = [IO.Path]::GetRelativePath($SourceDirectory, $file.FullName).Replace('\', '/')
|
||||
}
|
||||
[string[]]$files = @($allEntries |
|
||||
Where-Object { -not $_.PSIsContainer } |
|
||||
ForEach-Object {
|
||||
[IO.Path]::GetRelativePath(
|
||||
$SourceDirectory,
|
||||
$_.FullName).Replace('\', '/')
|
||||
})
|
||||
[Array]::Sort($files, [StringComparer]::Ordinal)
|
||||
$caseFolded = [Collections.Generic.HashSet[string]]::new(
|
||||
[StringComparer]::OrdinalIgnoreCase)
|
||||
foreach ($relative in $files) {
|
||||
if ($relative.StartsWith('../', [StringComparison]::Ordinal) -or
|
||||
[IO.Path]::IsPathRooted($relative)) {
|
||||
[IO.Path]::IsPathRooted($relative) -or
|
||||
-not $caseFolded.Add($relative)) {
|
||||
throw "Payload path escaped its root: $relative"
|
||||
}
|
||||
$file = Get-Item -LiteralPath (
|
||||
Join-Path $SourceDirectory $relative.Replace('/', [IO.Path]::DirectorySeparatorChar))
|
||||
$entry = $archive.CreateEntry($relative, [IO.Compression.CompressionLevel]::Optimal)
|
||||
$entry.LastWriteTime = $fixedTimestamp
|
||||
$executable = $relative -ceq 'AcDream.App' -or
|
||||
|
|
@ -183,6 +298,7 @@ function New-DeterministicZip(
|
|||
finally { $archive.Dispose() }
|
||||
}
|
||||
finally { $stream.Dispose() }
|
||||
Set-DeterministicZipHostPlatform $Destination
|
||||
}
|
||||
|
||||
function Get-Artifact([string]$Path, [string]$Url) {
|
||||
|
|
@ -232,11 +348,15 @@ foreach ($release in $releaseDefinitions) {
|
|||
"$baseUri/launcher-linux-x64.zip"
|
||||
}
|
||||
}
|
||||
$manifest | ConvertTo-Json -Depth 8 -Compress |
|
||||
Set-Content -LiteralPath (Join-Path $releaseRoot 'manifest.json') -Encoding utf8NoBOM
|
||||
[IO.File]::WriteAllText(
|
||||
(Join-Path $releaseRoot 'manifest.json'),
|
||||
($manifest | ConvertTo-Json -Depth 8 -Compress),
|
||||
[Text.UTF8Encoding]::new($false))
|
||||
}
|
||||
Set-Content -LiteralPath (Join-Path $OutputDirectory 'active-release.txt') `
|
||||
-Value 'A' -Encoding ascii -NoNewline
|
||||
[IO.File]::WriteAllText(
|
||||
(Join-Path $OutputDirectory 'active-release.txt'),
|
||||
'A',
|
||||
[Text.Encoding]::ASCII)
|
||||
|
||||
$server = @'
|
||||
[CmdletBinding()]
|
||||
|
|
@ -311,7 +431,10 @@ try {
|
|||
}
|
||||
finally { $listener.Close() }
|
||||
'@
|
||||
$server | Set-Content -LiteralPath (Join-Path $OutputDirectory 'serve-fixture.ps1') -Encoding utf8NoBOM
|
||||
[IO.File]::WriteAllText(
|
||||
(Join-Path $OutputDirectory 'serve-fixture.ps1'),
|
||||
$server.Replace("`r`n", "`n"),
|
||||
[Text.UTF8Encoding]::new($false))
|
||||
|
||||
$selector = @'
|
||||
[CmdletBinding()]
|
||||
|
|
@ -340,16 +463,24 @@ finally {
|
|||
}
|
||||
Write-Host "Campaign LA fixture active release: $Release"
|
||||
'@
|
||||
$selector | Set-Content -LiteralPath (Join-Path $OutputDirectory 'set-active-release.ps1') -Encoding utf8NoBOM
|
||||
[IO.File]::WriteAllText(
|
||||
(Join-Path $OutputDirectory 'set-active-release.ps1'),
|
||||
$selector.Replace("`r`n", "`n"),
|
||||
[Text.UTF8Encoding]::new($false))
|
||||
|
||||
$inventory = @(Get-ChildItem -LiteralPath $OutputDirectory -File -Recurse |
|
||||
$inventoryPaths = [string[]]@(Get-ChildItem -LiteralPath $OutputDirectory -File -Recurse |
|
||||
Where-Object { $_.Name -ne 'fixture-report.json' } |
|
||||
Sort-Object FullName |
|
||||
ForEach-Object {
|
||||
[IO.Path]::GetRelativePath($OutputDirectory, $_.FullName).Replace('\', '/')
|
||||
})
|
||||
[Array]::Sort($inventoryPaths, [StringComparer]::Ordinal)
|
||||
$inventory = @($inventoryPaths | ForEach-Object {
|
||||
$fullPath = Join-Path $OutputDirectory $_.Replace('/', [IO.Path]::DirectorySeparatorChar)
|
||||
$item = Get-Item -LiteralPath $fullPath
|
||||
[ordered]@{
|
||||
path = [IO.Path]::GetRelativePath($OutputDirectory, $_.FullName).Replace('\', '/')
|
||||
size = $_.Length
|
||||
sha256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
path = $_
|
||||
size = $item.Length
|
||||
sha256 = (Get-FileHash -LiteralPath $fullPath -Algorithm SHA256).Hash.ToLowerInvariant()
|
||||
}
|
||||
})
|
||||
$report = [ordered]@{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue