155 lines
7.1 KiB
PowerShell
155 lines
7.1 KiB
PowerShell
param(
|
|
[Parameter(Mandatory = $true)][string]$Root,
|
|
[Parameter(Mandatory = $true)][string]$OutputDirectory
|
|
)
|
|
|
|
$ErrorActionPreference = 'Stop'
|
|
$rootPath = (Resolve-Path -LiteralPath $Root).Path
|
|
$outputPath = [IO.Path]::GetFullPath($OutputDirectory)
|
|
New-Item -ItemType Directory -Force -Path $outputPath | Out-Null
|
|
|
|
Add-Type -AssemblyName System.IO.Compression
|
|
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
|
|
function Get-Sha256([byte[]]$Bytes) {
|
|
$sha = [Security.Cryptography.SHA256]::Create()
|
|
try { return ([BitConverter]::ToString($sha.ComputeHash($Bytes)) -replace '-', '').ToLowerInvariant() }
|
|
finally { $sha.Dispose() }
|
|
}
|
|
|
|
function Get-KeyMaterial([byte[]]$Salt, [string]$Purpose) {
|
|
$purposeBytes = [Text.Encoding]::UTF8.GetBytes("YMhut.Box.AssetPackage.v2|$Purpose")
|
|
$material = [byte[]]::new($purposeBytes.Length + $Salt.Length)
|
|
[Array]::Copy($purposeBytes, 0, $material, 0, $purposeBytes.Length)
|
|
[Array]::Copy($Salt, 0, $material, $purposeBytes.Length, $Salt.Length)
|
|
$sha = [Security.Cryptography.SHA512]::Create()
|
|
try { return $sha.ComputeHash($material) }
|
|
finally { $sha.Dispose() }
|
|
}
|
|
|
|
function Write-AuthenticatedPackage([byte[]]$Plain, [string]$Purpose, [string]$Path) {
|
|
$salt = [byte[]]::new(16)
|
|
$iv = [byte[]]::new(16)
|
|
$random = [Security.Cryptography.RandomNumberGenerator]::Create()
|
|
try {
|
|
$random.GetBytes($salt)
|
|
$random.GetBytes($iv)
|
|
}
|
|
finally { $random.Dispose() }
|
|
$keys = Get-KeyMaterial $salt $Purpose
|
|
$aes = [Security.Cryptography.Aes]::Create()
|
|
try {
|
|
$aes.Key = $keys[0..31]
|
|
$aes.IV = $iv
|
|
$aes.Mode = [Security.Cryptography.CipherMode]::CBC
|
|
$aes.Padding = [Security.Cryptography.PaddingMode]::PKCS7
|
|
$encryptor = $aes.CreateEncryptor()
|
|
try { $cipher = $encryptor.TransformFinalBlock($Plain, 0, $Plain.Length) }
|
|
finally { $encryptor.Dispose() }
|
|
}
|
|
finally { $aes.Dispose() }
|
|
|
|
$magic = [Text.Encoding]::ASCII.GetBytes('YMHUTAS2')
|
|
$header = [IO.MemoryStream]::new()
|
|
$writer = [IO.BinaryWriter]::new($header, [Text.Encoding]::UTF8, $true)
|
|
try {
|
|
$writer.Write($magic)
|
|
$writer.Write([byte]2)
|
|
$writer.Write($salt)
|
|
$writer.Write($iv)
|
|
$writer.Write([long]$Plain.Length)
|
|
$writer.Write([long]$cipher.Length)
|
|
$writer.Flush()
|
|
}
|
|
finally { $writer.Dispose() }
|
|
|
|
$signed = [byte[]]::new($header.Length + $cipher.Length)
|
|
[Array]::Copy($header.ToArray(), 0, $signed, 0, $header.Length)
|
|
[Array]::Copy($cipher, 0, $signed, $header.Length, $cipher.Length)
|
|
$hmac = [Security.Cryptography.HMACSHA256]::new($keys[32..63])
|
|
try { $tag = $hmac.ComputeHash($signed) }
|
|
finally { $hmac.Dispose() }
|
|
[IO.File]::WriteAllBytes($Path, $signed + $tag)
|
|
$header.Dispose()
|
|
}
|
|
|
|
function Add-SourceFile([hashtable]$Entries, [string]$Source, [string]$EntryName) {
|
|
if (-not (Test-Path -LiteralPath $Source -PathType Leaf)) { throw "Missing authenticated asset source: $Source" }
|
|
$normalized = $EntryName.Replace('\', '/').TrimStart('/')
|
|
if ($normalized -eq '' -or $normalized.Contains('../') -or $normalized.StartsWith('../')) { throw "Unsafe authenticated asset entry: $EntryName" }
|
|
if ($Entries.ContainsKey($normalized)) { throw "Duplicate authenticated asset entry: $normalized" }
|
|
$bytes = [IO.File]::ReadAllBytes($Source)
|
|
$Entries[$normalized] = [pscustomobject]@{ Path = $normalized; Bytes = $bytes; Sha256 = Get-Sha256 $bytes }
|
|
}
|
|
|
|
function Add-SourceTree([hashtable]$Entries, [string]$SourceRoot, [string]$Prefix, [scriptblock]$Include = $null) {
|
|
if (-not (Test-Path -LiteralPath $SourceRoot -PathType Container)) { throw "Missing authenticated asset directory: $SourceRoot" }
|
|
foreach ($file in Get-ChildItem -LiteralPath $SourceRoot -Recurse -File) {
|
|
$relative = $file.FullName.Substring($SourceRoot.TrimEnd('\', '/').Length).TrimStart('\', '/').Replace('\', '/')
|
|
if ($Include -and -not (& $Include $relative)) { continue }
|
|
Add-SourceFile $Entries $file.FullName (("$Prefix/$relative").TrimStart('/'))
|
|
}
|
|
}
|
|
|
|
function New-AssetPackage([hashtable]$Entries, [string]$BundleId, [string]$Purpose, [string]$Path) {
|
|
$memory = [IO.MemoryStream]::new()
|
|
$archive = [IO.Compression.ZipArchive]::new($memory, [IO.Compression.ZipArchiveMode]::Create, $true)
|
|
try {
|
|
foreach ($item in ($Entries.Values | Sort-Object Path)) {
|
|
$entry = $archive.CreateEntry($item.Path, [IO.Compression.CompressionLevel]::Optimal)
|
|
$stream = $entry.Open()
|
|
try { $stream.Write($item.Bytes, 0, $item.Bytes.Length) }
|
|
finally { $stream.Dispose() }
|
|
}
|
|
$manifest = [ordered]@{
|
|
schemaVersion = 2
|
|
bundleId = $BundleId
|
|
files = @($Entries.Values | Sort-Object Path | ForEach-Object {
|
|
[ordered]@{ path = $_.Path; length = $_.Bytes.Length; sha256 = $_.Sha256 }
|
|
})
|
|
}
|
|
$manifestBytes = [Text.Encoding]::UTF8.GetBytes(($manifest | ConvertTo-Json -Depth 5 -Compress))
|
|
$manifestEntry = $archive.CreateEntry('_manifest.json', [IO.Compression.CompressionLevel]::Optimal)
|
|
$manifestStream = $manifestEntry.Open()
|
|
try { $manifestStream.Write($manifestBytes, 0, $manifestBytes.Length) }
|
|
finally { $manifestStream.Dispose() }
|
|
}
|
|
finally { $archive.Dispose() }
|
|
$plain = $memory.ToArray()
|
|
$memory.Dispose()
|
|
Write-AuthenticatedPackage $plain $Purpose $Path
|
|
}
|
|
|
|
$runtimeEntries = @{}
|
|
$projectAssets = Join-Path $rootPath 'src\box-winUI\Assets'
|
|
$repoAssets = Join-Path $rootPath 'assets'
|
|
Add-SourceTree $runtimeEntries (Join-Path $projectAssets 'startup-splash') 'startup-splash'
|
|
Add-SourceTree $runtimeEntries (Join-Path $projectAssets 'home-globe') 'home-globe' {
|
|
param($relative)
|
|
return $relative -notlike 'textures/solar/source/*' -and
|
|
$relative -notlike 'textures/solar/optimized/*' -and
|
|
$relative -ne 'textures/solar/solar-texture-manifest.json'
|
|
}
|
|
Add-SourceTree $runtimeEntries (Join-Path $projectAssets 'tool-pages') 'tool-pages'
|
|
Add-SourceTree $runtimeEntries (Join-Path $projectAssets 'tool-results') 'tool-results'
|
|
Add-SourceTree $runtimeEntries (Join-Path $repoAssets 'icons') 'icons' { param($relative) return $relative -ne '.gitkeep' }
|
|
Add-SourceTree $runtimeEntries (Join-Path $repoAssets 'images') 'images' { param($relative) return $relative -ne 'loading.gif' }
|
|
Add-SourceFile $runtimeEntries (Join-Path $repoAssets 'download.svg') 'download.svg'
|
|
Add-SourceFile $runtimeEntries (Join-Path $repoAssets 'icons\app_icon.ico') 'app_icon.ico'
|
|
New-AssetPackage $runtimeEntries 'runtime-assets' 'runtime-assets' (Join-Path $outputPath 'runtime-assets.dat')
|
|
|
|
$dataEntries = @{}
|
|
$dataRoot = Join-Path $repoAssets 'data'
|
|
foreach ($file in Get-ChildItem -LiteralPath $dataRoot -Recurse -File) {
|
|
if ($file.Extension -ieq '.dat' -or $file.Name -ieq 'ymhut-data.ybin') { continue }
|
|
$relative = $file.FullName.Substring($repoAssets.TrimEnd('\', '/').Length).TrimStart('\', '/').Replace('\', '/')
|
|
Add-SourceFile $dataEntries $file.FullName $relative
|
|
}
|
|
$rankingRoot = Join-Path $rootPath 'server\unified-management\internal\reference\data'
|
|
if (Test-Path -LiteralPath $rankingRoot -PathType Container) {
|
|
foreach ($file in Get-ChildItem -LiteralPath $rankingRoot -Filter '*.json' -File) {
|
|
Add-SourceFile $dataEntries $file.FullName ("data/rankings/$($file.Name)")
|
|
}
|
|
}
|
|
New-AssetPackage $dataEntries 'reference-data' 'reference-data' (Join-Path $outputPath 'ymhut-data.ybin')
|