10 'PrepareCancellation',
14 [string]$Phase = 'Build',
16 [string]$SourceRoot = (Get-Location).Path,
24 [string]$FirmwareModifiedNote,
26 [bool]$UseMethod8 = $true,
28 [string[]]$ExtraFriiDumpArgs = @(),
33 Set-StrictMode -Version Latest
34 $ErrorActionPreference = 'Stop'
36 $ExpectedVersion = '0.5.3.16-pf1-candidate21'
37 $ExpectedCommit = $null
38 $ExpectedSchemaSha256 = 'd1e97cb3e88e0f5a94ccf9b9edf921554fc15278662e69420b8c1e564c600411'
40 $SourceRoot = (Resolve-Path -LiteralPath $SourceRoot).Path
41 $CandidateMetadataPath = Join-Path $SourceRoot 'CANDIDATE.json'
42 if (-not (Test-Path -LiteralPath $CandidateMetadataPath -PathType Leaf)) {
43 throw "Candidate metadata is missing: $CandidateMetadataPath"
45 $CandidateMetadata = Get-Content -Raw -LiteralPath $CandidateMetadataPath | ConvertFrom-Json
46 $ExpectedCommit = [string]$CandidateMetadata.candidate_implementation.commit
47 if ($ExpectedCommit -notmatch '^[0-9a-fA-F]{40}$') {
48 throw "Candidate implementation commit is invalid: $ExpectedCommit"
50 $ExpectedCommit = $ExpectedCommit.ToLowerInvariant()
52 if ([string]::IsNullOrWhiteSpace($ResultsRoot)) {
53 $ResultsRoot = Join-Path $SourceRoot '_native-report-live-validation'
55 $ResultsRoot = [System.IO.Path]::GetFullPath($ResultsRoot)
57 $Exe = Join-Path $SourceRoot 'friidump.exe'
58 $BuildScript = Join-Path $SourceRoot 'build_msvc32.cmd'
59 $FixtureBuildScript = Join-Path $SourceRoot 'build_msvc32_native_report_tests.cmd'
60 $Validator = Join-Path $SourceRoot 'tests\validate_native_reports.py'
61 $Schema = Join-Path $SourceRoot 'tests\contracts\friidump-test-result.v1.schema.json'
62 $SourceManifest = Join-Path $SourceRoot 'SOURCE_SHA256SUMS.txt'
63 $MainResponseFile = Join-Path $SourceRoot 'msvc32_friidump.rsp'
64 $FixtureResponseFile = Join-Path $SourceRoot 'msvc32_native_report_tests.rsp'
66 function Assert-True {
68 [Parameter(Mandatory)]
70 [Parameter(Mandatory)]
74 if (-not $Condition) {
79 function Assert-LastExitCode {
81 [Parameter(Mandatory)]
85 if ($LASTEXITCODE -ne 0) {
86 throw "$Operation failed with exit code $LASTEXITCODE."
91 param([Parameter(Mandatory)][string]$Path)
93 return (Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash.ToLowerInvariant()
96 function Test-SourceManifest {
97 if (-not (Test-Path -LiteralPath $SourceManifest -PathType Leaf)) {
98 Write-Host '[INFO] SOURCE_SHA256SUMS.txt is not present; package-manifest validation skipped.'
103 foreach ($line in Get-Content -LiteralPath $SourceManifest) {
104 if ([string]::IsNullOrWhiteSpace($line)) {
107 if ($line -notmatch '^([0-9a-fA-F]{64}) (.+)$') {
108 throw "Malformed source-manifest line: $line"
111 $expected = $Matches[1].ToLowerInvariant()
112 $relative = $Matches[2] -replace '/', '\'
113 $path = Join-Path $SourceRoot $relative
114 Assert-True (Test-Path -LiteralPath $path -PathType Leaf) "Manifest file is missing: $relative"
115 $actual = Get-Sha256 $path
116 Assert-True ($actual -eq $expected) "Manifest SHA-256 mismatch: $relative"
120 Write-Host "Source manifest: PASS ($checked files)"
123 function Test-MsvcSplitLinkContract {
125 [Parameter(Mandatory)][string]$ResponsePath,
126 [Parameter(Mandatory)][string]$BuildScriptPath,
127 [Parameter(Mandatory)][string]$ExpectedInvocation,
128 [Parameter(Mandatory)][string]$Label
131 Assert-True (Test-Path -LiteralPath $ResponsePath -PathType Leaf) "$Label response file is missing: $ResponsePath"
132 Assert-True (Test-Path -LiteralPath $BuildScriptPath -PathType Leaf) "$Label build script is missing: $BuildScriptPath"
135 Get-Content -LiteralPath $ResponsePath |
136 ForEach-Object { $_.Trim() } |
137 Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
140 Assert-True (-not ($lines -icontains '/link')) "$Label response file must not contain /link. The linker boundary must be on the cl.exe command line."
141 Assert-True (-not ($lines -icontains 'ole32.lib')) "$Label response file must not contain ole32.lib. The library must follow /link on the cl.exe command line."
143 $buildText = Get-Content -Raw -LiteralPath $BuildScriptPath
144 Assert-True ($buildText.Contains($ExpectedInvocation)) "$Label build script is missing the required split compile/link invocation: $ExpectedInvocation"
146 Write-Host "$Label MSVC command-line linker boundary: PASS"
149 function Test-PythonValidatorAvailable {
150 & py -3 -c 'import jsonschema' 2>$null
151 if ($LASTEXITCODE -ne 0) {
152 throw 'Python 3 with jsonschema is required. Install it with: py -3 -m pip install jsonschema'
156 function Invoke-ReportValidation {
157 param([Parameter(Mandatory)][string]$ReportRoot)
159 Assert-True (Test-Path -LiteralPath $Validator -PathType Leaf) "Validator is missing: $Validator"
160 Assert-True (Test-Path -LiteralPath $Schema -PathType Leaf) "Schema is missing: $Schema"
161 Assert-True ((Get-Sha256 $Schema) -eq $ExpectedSchemaSha256) 'Bundled schema SHA-256 mismatch.'
163 Test-PythonValidatorAvailable
168 '--fixtures', $ReportRoot,
173 if (-not [string]::IsNullOrWhiteSpace($PhpValidator)) {
174 Assert-True (Test-Path -LiteralPath $PhpValidator -PathType Leaf) "PHP validator is missing: $PhpValidator"
175 $arguments += @('--php-validator', $PhpValidator)
179 Assert-LastExitCode 'Strict native-report validation'
182 function Get-NewReport {
184 [Parameter(Mandatory)][string]$ReportDirectory,
185 [Parameter(Mandatory)][datetime]$Started
189 Get-ChildItem -LiteralPath $ReportDirectory -Recurse -Filter '*.friidump.json' -File |
190 Where-Object { $_.LastWriteTimeUtc -ge $Started.ToUniversalTime().AddSeconds(-2) } |
191 Sort-Object LastWriteTimeUtc
194 Assert-True ($reports.Count -eq 1) "Expected exactly one new report; found $($reports.Count)."
198 function Read-Report {
199 param([Parameter(Mandatory)][string]$Path)
201 return Get-Content -Raw -LiteralPath $Path | ConvertFrom-Json
204 function Test-CommonReportIdentity {
206 [Parameter(Mandatory)]$Report,
207 [Parameter(Mandatory)][string]$ReportPath
210 Assert-True ($Report.schema -eq 'friidump-test-result.v1') "${ReportPath}: schema mismatch."
211 Assert-True ($Report.generator.name -eq 'FriiDump') "${ReportPath}: generator name mismatch."
212 Assert-True ($Report.generator.version -eq $ExpectedVersion) "${ReportPath}: generator version mismatch."
213 Assert-True ($Report.generator.build_commit -eq $ExpectedCommit) "${ReportPath}: build commit mismatch."
214 Assert-True (-not [string]::IsNullOrWhiteSpace([string]$Report.run.run_id)) "${ReportPath}: run UUID missing."
215 Assert-True (-not [string]::IsNullOrWhiteSpace([string]$Report.drive.model)) "${ReportPath}: drive model missing."
216 Assert-True (-not [string]::IsNullOrWhiteSpace([string]$Report.drive.firmware_revision)) "${ReportPath}: firmware revision missing."
218 if ([string]::IsNullOrWhiteSpace($FirmwareModifiedNote)) {
219 Assert-True ($Report.drive.firmware_modified -eq $false) "${ReportPath}: omitted --firmware-modified must assume stock firmware."
220 Assert-True ($null -eq $Report.drive.modification_note) "${ReportPath}: stock firmware must not have a modification note."
223 Assert-True ($Report.drive.firmware_modified -eq $true) "${ReportPath}: modified-firmware flag was not recorded."
224 Assert-True ([string]$Report.drive.modification_note -eq $FirmwareModifiedNote) "${ReportPath}: modified-firmware explanation mismatch."
228 function Test-DumpOutputIdentity {
230 [Parameter(Mandatory)]$Report,
231 [Parameter(Mandatory)][string]$ExpectedOutput
234 $resolvedOutput = [System.IO.Path]::GetFullPath($ExpectedOutput)
235 Assert-True (Test-Path -LiteralPath $resolvedOutput -PathType Leaf) "Dump output is missing: $resolvedOutput"
237 $actualBytes = (Get-Item -LiteralPath $resolvedOutput).Length
238 Assert-True ([int64]$Report.dump.byte_count -eq $actualBytes) 'Report byte count does not match the output file.'
239 Assert-True ([string]$Report.dump.output_path -eq $resolvedOutput) 'Report output path does not match the requested path.'
241 $artifacts = @($Report.artifacts | Where-Object { $_.type -eq 'dump_output' })
242 Assert-True ($artifacts.Count -eq 1) 'Exactly one dump_output artifact is required.'
243 Assert-True ([string]$artifacts[0].path -eq $resolvedOutput) 'dump_output artifact path mismatch.'
244 Assert-True ([int64]$artifacts[0].bytes -eq $actualBytes) 'dump_output artifact byte count mismatch.'
246 if ($null -ne $Report.hashes.sha256) {
247 $actualSha = Get-Sha256 $resolvedOutput
248 Assert-True ([string]$Report.hashes.sha256 -eq $actualSha) 'Report SHA-256 does not match the output file.'
249 Assert-True ([string]$artifacts[0].sha256 -eq $actualSha) 'Artifact SHA-256 does not match the output file.'
253 function New-RunDirectory {
254 param([Parameter(Mandatory)][string]$Name)
256 $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ')
257 $path = Join-Path $ResultsRoot "$stamp-$Name"
258 New-Item -ItemType Directory -Force -Path $path | Out-Null
259 New-Item -ItemType Directory -Force -Path (Join-Path $path 'reports') | Out-Null
263 function Invoke-FriiDumpRun {
265 [Parameter(Mandatory)][string[]]$Arguments,
266 [Parameter(Mandatory)][string]$RunDirectory,
267 [Parameter(Mandatory)][bool]$ExpectSuccess
270 $started = (Get-Date).ToUniversalTime()
272 Write-Host "Working directory: $RunDirectory"
273 Write-Host "Command: $Exe $($Arguments -join ' ')"
274 Write-Host 'Console mode: direct live FriiDump output; FriiDump native .log is the evidence log.'
276 # Do not route an interactive FriiDump run through a PowerShell
277 # pipeline. Tee-Object buffers carriage-return progress updates, turns
278 # native stderr into PowerShell error records, and also contaminates a
279 # function's return stream with every line emitted by the child.
281 # Start FriiDump with inherited console handles instead. This preserves
282 # real-time progress output and leaves this function with exactly one
284 function ConvertTo-WindowsCommandLineArgument {
285 param([AllowEmptyString()][string]$Value)
287 if ($Value.Length -gt 0 -and $Value -notmatch '[\s"]') {
291 $builder = New-Object System.Text.StringBuilder
292 [void]$builder.Append('"')
295 foreach ($character in $Value.ToCharArray()) {
296 if ($character -eq '\') {
301 if ($character -eq '"') {
302 [void]$builder.Append(('\' * (($backslashes * 2) + 1)))
303 [void]$builder.Append('"')
308 if ($backslashes -gt 0) {
309 [void]$builder.Append(('\' * $backslashes))
313 [void]$builder.Append($character)
316 if ($backslashes -gt 0) {
317 [void]$builder.Append(('\' * ($backslashes * 2)))
320 [void]$builder.Append('"')
321 return $builder.ToString()
327 ConvertTo-WindowsCommandLineArgument ([string]$_)
331 $startInfo = New-Object System.Diagnostics.ProcessStartInfo
332 $startInfo.FileName = $Exe
333 $startInfo.Arguments = $argumentLine
334 $startInfo.WorkingDirectory = $RunDirectory
335 $startInfo.UseShellExecute = $false
336 $startInfo.CreateNoWindow = $false
337 $startInfo.RedirectStandardOutput = $false
338 $startInfo.RedirectStandardError = $false
340 $process = New-Object System.Diagnostics.Process
341 $process.StartInfo = $startInfo
344 [void]$process.Start()
345 $process.WaitForExit()
346 $exitCode = $process.ExitCode
352 Set-Content -LiteralPath (Join-Path $RunDirectory 'exit-code.txt') -Value $exitCode -Encoding ASCII
354 if ($ExpectSuccess) {
355 Assert-True ($exitCode -eq 0) "FriiDump was expected to succeed but exited with $exitCode."
358 return [pscustomobject]@{
360 ExitCode = [int]$exitCode
365 function Add-ModifiedFirmwareArgument {
366 param([string[]]$Arguments)
368 if (-not [string]::IsNullOrWhiteSpace($FirmwareModifiedNote)) {
369 return $Arguments + @('--firmware-modified', $FirmwareModifiedNote)
374 New-Item -ItemType Directory -Force -Path $ResultsRoot | Out-Null
378 Assert-True (Test-Path -LiteralPath $BuildScript -PathType Leaf) "Build script is missing: $BuildScript"
379 Assert-True (Test-Path -LiteralPath $FixtureBuildScript -PathType Leaf) "Fixture build script is missing: $FixtureBuildScript"
382 $XboxRegionHeader = Join-Path $SourceRoot 'libfriidump\xbox_region.h'
383 $XboxLegacyUtilsHeader = Join-Path $SourceRoot 'libfriidump\xbox_ref\utils.h'
384 $XboxRegionHeaderText = Get-Content -Raw -LiteralPath $XboxRegionHeader
385 $XboxLegacyUtilsText = Get-Content -Raw -LiteralPath $XboxLegacyUtilsHeader
387 Assert-True (-not $XboxRegionHeaderText.Contains('<stdbool.h>')) `
388 'xbox_region.h must not introduce stdbool.h into the copied Xbox header graph.'
389 Assert-True ($XboxRegionHeaderText.Contains('int xbox_region_format(')) `
390 'xbox_region.h must expose the MSVC-safe int return type.'
391 Assert-True ($XboxLegacyUtilsText.Contains('typedef int bool;')) `
392 'The copied Xbox legacy bool declaration changed unexpectedly.'
393 Write-Host 'Xbox region/MSVC bool isolation: PASS'
395 $XboxCancelContract = Join-Path $SourceRoot 'tests\test_xbox_cancel_contract.py'
396 Assert-True (Test-Path -LiteralPath $XboxCancelContract -PathType Leaf) `
397 'Xbox copied-dumper cancellation contract test is missing.'
398 & py -3 $XboxCancelContract --source-root $SourceRoot
399 Assert-LastExitCode 'Xbox copied-dumper cancellation contract'
401 $SeedDiagnosticContract = Join-Path $SourceRoot 'tests\test_seed_diagnostics_contract.py'
402 Assert-True (Test-Path -LiteralPath $SeedDiagnosticContract -PathType Leaf) `
403 'Seed diagnostics contract test is missing.'
404 & py -3 $SeedDiagnosticContract
405 Assert-LastExitCode 'Seed diagnostics contract'
407 $LinuxRawIoContract = Join-Path $SourceRoot 'tests\test_linux_rawio_contract.py'
408 Assert-True (Test-Path -LiteralPath $LinuxRawIoContract -PathType Leaf) `
409 'Linux raw-I/O source/documentation contract test is missing.'
410 & py -3 $LinuxRawIoContract
411 Assert-LastExitCode 'Linux raw-I/O source/documentation contract'
413 $XboxDvdStructureContract = Join-Path $SourceRoot 'tests\test_xbox_dvd_structure_contract.py'
414 Assert-True (Test-Path -LiteralPath $XboxDvdStructureContract -PathType Leaf) `
415 'Xbox READ DVD STRUCTURE CDB contract test is missing.'
416 & py -3 $XboxDvdStructureContract --source-root $SourceRoot
417 Assert-LastExitCode 'Xbox READ DVD STRUCTURE CDB contract'
419 Test-MsvcSplitLinkContract `
420 -ResponsePath $MainResponseFile `
421 -BuildScriptPath $BuildScript `
422 -ExpectedInvocation 'cl @"%EFFECTIVE_RSP%" /link ole32.lib' `
423 -Label 'FriiDump executable'
425 Test-MsvcSplitLinkContract `
426 -ResponsePath $FixtureResponseFile `
427 -BuildScriptPath $FixtureBuildScript `
428 -ExpectedInvocation 'cl @"%~dp0msvc32_native_report_tests.rsp" /link ole32.lib' `
429 -Label 'Native-report fixtures'
431 $env:FRIIDUMP_BUILD_COMMIT = $ExpectedCommit
434 Assert-LastExitCode 'MSVC clean'
437 Assert-LastExitCode 'MSVC candidate build'
439 & $FixtureBuildScript
440 Assert-LastExitCode 'MSVC native-report fixture validation'
442 Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'friidump.exe was not produced.'
444 # Windows PowerShell 5.1 converts native stderr records into
445 # NativeCommandError objects when $ErrorActionPreference is Stop.
446 # FriiDump intentionally writes part of its startup/help text to
447 # stderr, so capture both streams through ProcessStartInfo instead
448 # of invoking the executable directly through the PowerShell
450 $helpStartInfo = New-Object System.Diagnostics.ProcessStartInfo
451 $helpStartInfo.FileName = $Exe
452 $helpStartInfo.Arguments = '--help'
453 $helpStartInfo.UseShellExecute = $false
454 $helpStartInfo.RedirectStandardOutput = $true
455 $helpStartInfo.RedirectStandardError = $true
456 $helpStartInfo.CreateNoWindow = $true
458 $helpProcess = New-Object System.Diagnostics.Process
459 $helpProcess.StartInfo = $helpStartInfo
462 [void]$helpProcess.Start()
464 # Drain stdout and stderr concurrently. Reading either redirected
465 # stream synchronously before the other can deadlock when the
466 # second pipe fills while the process is still running.
467 $helpStdoutTask = $helpProcess.StandardOutput.ReadToEndAsync()
468 $helpStderrTask = $helpProcess.StandardError.ReadToEndAsync()
470 $helpProcess.WaitForExit()
472 $helpStdout = $helpStdoutTask.GetAwaiter().GetResult()
473 $helpStderr = $helpStderrTask.GetAwaiter().GetResult()
474 $helpExitCode = $helpProcess.ExitCode
477 $helpProcess.Dispose()
483 ) -join [Environment]::NewLine
485 Assert-True ($helpExitCode -eq 1) "Candidate help command returned unexpected exit code $helpExitCode; FriiDump historically returns 1 for --help."
486 Assert-True ($help.Contains("FriiDump $ExpectedVersion")) 'Candidate version is missing from help output.'
487 foreach ($option in @('--report-json', '--report-dir', '--firmware-modified')) {
488 Assert-True ($help.Contains($option)) "Candidate help is missing $option."
490 Assert-True ($help.Contains('Native .friidump.json reports are created by default')) 'Default native-report behavior is missing from help output.'
491 Assert-True ($help.Contains('beside the final log')) 'Default beside-log report placement is missing from help output.'
492 Assert-True ($help.Contains('(may be combined with --report-json)')) 'Combined --report-json/--report-dir behavior is missing from help output.'
493 Assert-True ($help.Contains('omission assumes stock')) 'Stock-firmware default is missing from help output.'
496 "Generated UTC: $((Get-Date).ToUniversalTime().ToString('o'))",
497 "Version: $ExpectedVersion",
498 "Commit: $ExpectedCommit",
500 "Executable SHA-256: $(Get-Sha256 $Exe)",
502 'MSVC native-report fixtures: PASS',
503 'Schema and semantic validation: PASS (13 reports)',
504 'Help/version/options/default-location smoke: PASS',
505 'Xbox copied-dumper cancellation, hashing, and summary contract: PASS',
506 'Seed diagnostics, transport evidence, and seed-failure STOP UNIT contract: PASS',
507 'Linux CAP_SYS_RAWIO preflight, documentation, helper, and build-warning contract: PASS'
509 $recordPath = Join-Path $ResultsRoot 'build-validation.txt'
510 Set-Content -LiteralPath $recordPath -Value $buildRecord -Encoding UTF8
513 Write-Host 'FRIIDUMP CANDIDATE21 WINDOWS BUILD: PASS'
514 Write-Host "Build record: $recordPath"
519 Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.'
520 Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.'
522 Read-Host "Remove all media from $Drive, wait for the drive to settle, then press Enter"
523 $runDir = New-RunDirectory 'no-media-default-report'
524 $output = Join-Path $runDir 'no-media-placeholder.iso'
525 $args = @('-d', $Drive, '-i', $output)
526 $args = Add-ModifiedFirmwareArgument $args
527 $args += $ExtraFriiDumpArgs
529 $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $false
530 $reportFile = Get-NewReport -ReportDirectory $runDir -Started $run.Started
531 Assert-True ($reportFile.Name -eq 'no-media-placeholder.iso.friidump.json') "Default report filename mismatch: $($reportFile.Name)"
532 Assert-True ($reportFile.DirectoryName -eq $runDir) "Default no-media report was not placed beside the final log/output path."
533 Invoke-ReportValidation $runDir
534 $report = Read-Report $reportFile.FullName
535 Test-CommonReportIdentity $report $reportFile.FullName
537 Assert-True ($report.run.test_type -eq 'diagnostic_no_media') 'No-media test_type mismatch.'
538 Assert-True ($report.run.result -eq 'not_applicable') 'No-media run result mismatch.'
539 Assert-True ($report.dump.attempted -eq $false) 'No-media report must not claim a dump.'
540 Assert-True (-not (Test-Path -LiteralPath $output)) 'No-media test unexpectedly created an output image.'
543 Write-Host 'NO-MEDIA DEFAULT NATIVE REPORT: PASS'
544 Write-Host "Report: $($reportFile.FullName)"
549 Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.'
550 Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.'
552 $runDir = New-RunDirectory 'gamecube-full-dump'
553 if ([string]::IsNullOrWhiteSpace($OutputName)) {
554 $OutputName = 'Sonic Mega Collection.iso'
556 $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName))
557 $args = @('-d', $Drive)
561 $args += @('-i', $output, '--report-dir', $reportDir)
562 $args = Add-ModifiedFirmwareArgument $args
563 $args += $ExtraFriiDumpArgs
565 $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $true
566 $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started
567 Invoke-ReportValidation $reportDir
568 $report = Read-Report $reportFile.FullName
569 Test-CommonReportIdentity $report $reportFile.FullName
570 Test-DumpOutputIdentity $report $output
572 Assert-True ($report.media.platform -eq 'gamecube') 'Expected a GameCube report.'
573 Assert-True ($report.run.test_type -eq 'full_dump' -and $report.run.result -eq 'pass') 'GameCube run outcome mismatch.'
574 Assert-True ($report.dump.result -eq 'pass') 'GameCube dump result mismatch.'
575 Assert-True (@($report.notes) -contains 'Measurement scope: full_optical_payload.') 'GameCube measurement scope mismatch.'
578 Write-Host 'GAMECUBE FULL-DUMP NATIVE REPORT: PASS'
579 Write-Host "Report: $($reportFile.FullName)"
584 Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.'
585 Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.'
587 $runDir = New-RunDirectory 'xbox-redump-iso'
588 $reportDir = Join-Path $runDir 'reports'
589 if ([string]::IsNullOrWhiteSpace($OutputName)) {
590 $OutputName = 'Red Faction II [TQ00501A].iso'
592 $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName))
593 $args = @('-d', $Drive, '-T', '4', '-i', $output, '--report-dir', $reportDir)
594 $args = Add-ModifiedFirmwareArgument $args
595 $args += $ExtraFriiDumpArgs
597 $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $true
598 $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started
599 Invoke-ReportValidation $reportDir
600 $report = Read-Report $reportFile.FullName
601 Test-CommonReportIdentity $report $reportFile.FullName
602 Test-DumpOutputIdentity $report $output
604 Assert-True ($report.media.platform -eq 'xbox') 'Expected an Xbox report.'
605 Assert-True ($report.media.region -eq 'North America') 'Expected the XBE GameRegion to report North America.'
606 Assert-True (@($report.notes) -contains 'Xbox XBE GameRegion mask: 0x00000001.') 'Expected the raw XBE GameRegion mask note.'
607 Assert-True ($report.run.test_type -eq 'full_dump' -and $report.run.result -eq 'pass') 'Xbox ISO run outcome mismatch.'
608 Assert-True (@($report.notes) -contains 'Measurement scope: assembled_output.') 'Xbox ISO measurement scope mismatch.'
611 Write-Host 'XBOX REDUMP-STYLE ISO NATIVE REPORT: PASS'
612 Write-Host "Report: $($reportFile.FullName)"
617 Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.'
618 Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.'
620 $runDir = New-RunDirectory 'xbox-xiso'
621 $reportDir = Join-Path $runDir 'reports'
622 if ([string]::IsNullOrWhiteSpace($OutputName)) {
623 $OutputName = 'Red Faction II [TQ00501A].xiso'
625 $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName))
626 $args = @('-d', $Drive, '-T', '4', '-X', $output, '--report-dir', $reportDir)
627 $args = Add-ModifiedFirmwareArgument $args
628 $args += $ExtraFriiDumpArgs
630 $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $true
631 $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started
632 Invoke-ReportValidation $reportDir
633 $report = Read-Report $reportFile.FullName
634 Test-CommonReportIdentity $report $reportFile.FullName
635 Test-DumpOutputIdentity $report $output
637 Assert-True ($report.media.platform -eq 'xbox') 'Expected an Xbox report.'
638 Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.media.region)) 'Expected an XBE-derived Xbox region.'
639 Assert-True ($report.run.test_type -eq 'full_dump' -and $report.run.result -eq 'pass') 'Xbox XISO run outcome mismatch.'
640 Assert-True (@($report.notes) -contains 'Measurement scope: assembled_output.') 'Xbox XISO measurement scope mismatch.'
643 Write-Host 'XBOX XISO NATIVE REPORT: PASS'
644 Write-Host "Report: $($reportFile.FullName)"
649 Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.'
650 Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.'
652 $runDir = New-RunDirectory 'xbox-controlled-cancellation'
653 $reportDir = Join-Path $runDir 'reports'
654 if ([string]::IsNullOrWhiteSpace($OutputName)) {
655 $OutputName = 'Red Faction II [TQ00501A].xiso'
657 $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName))
658 $args = @('-d', $Drive, '-T', '4', '-X', $output, '--report-dir', $reportDir)
659 $args = Add-ModifiedFirmwareArgument $args
660 $args += $ExtraFriiDumpArgs
663 Write-Host 'After Regions: North America appears and XISO progress begins, press Ctrl+C once.'
666 $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $false
667 $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started
668 Invoke-ReportValidation $reportDir
669 $report = Read-Report $reportFile.FullName
670 Test-CommonReportIdentity $report $reportFile.FullName
671 Test-DumpOutputIdentity $report $output
673 Assert-True ($report.media.platform -eq 'xbox') 'Expected an Xbox report.'
674 Assert-True ($report.media.region -eq 'North America') 'Expected the XBE GameRegion to report North America.'
675 Assert-True (@($report.notes) -contains 'Xbox XBE GameRegion mask: 0x00000001.') 'Expected the raw XBE GameRegion mask note.'
676 Assert-True ($report.run.test_type -eq 'partial_dump' -and $report.run.result -eq 'partial') 'Xbox cancellation run outcome mismatch.'
677 Assert-True ($report.dump.result -eq 'partial') 'Xbox cancellation dump result mismatch.'
678 Assert-True ($report.dump.failure_stage -eq 'user_cancelled') 'Xbox cancellation failure stage mismatch.'
679 Assert-True ([int64]$report.dump.sector_count -gt 32) 'Xbox cancellation did not record meaningful partial progress.'
680 Assert-True ([int64]$report.dump.byte_count -eq ([int64]$report.dump.sector_count * 2048)) 'Xbox cancellation byte/sector accounting mismatch.'
681 Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.crc32)) 'Partial Xbox CRC32 is missing.'
682 Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.md5)) 'Partial Xbox MD5 is missing.'
683 Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.sha1)) 'Partial Xbox SHA-1 is missing.'
684 Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.sha256)) 'Partial Xbox SHA-256 is missing.'
686 $nativeLog = "$output.log"
687 Assert-True (Test-Path -LiteralPath $nativeLog -PathType Leaf) "Xbox native log is missing: $nativeLog"
688 $logText = Get-Content -Raw -LiteralPath $nativeLog
689 foreach ($expectedText in @(
690 'Game/Media ID.....: TQ00501A',
691 'Title.............: Red Faction',
692 'Region............: North America',
693 'Seed read.........: N/A (Xbox reference auth path)',
694 'Source sectors....: 3431264',
695 'Expected output...: 1913920 sectors',
696 '[OK] Controlled partial Xbox output hashes captured.'
698 Assert-True ($logText.Contains($expectedText)) "Xbox summary/log is missing: $expectedText"
702 Write-Host 'XBOX REGION, SUMMARY, GEOMETRY, CANCELLATION, AND HASHING: PASS'
703 Write-Host "Report: $($reportFile.FullName)"
707 'PrepareCancellation' {
708 Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.'
709 Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.'
711 $runDir = New-RunDirectory 'controlled-cancellation'
712 if ([string]::IsNullOrWhiteSpace($OutputName)) {
713 $OutputName = 'controlled-cancellation.partial.iso'
715 $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName))
716 $wrapper = Join-Path $runDir 'run-controlled-cancellation.ps1'
718 $argLines = @("'-d'", "'$($Drive.Replace("'", "''"))'")
723 "'-i'", "'$($output.Replace("'", "''"))'"
725 if (-not [string]::IsNullOrWhiteSpace($FirmwareModifiedNote)) {
726 $argLines += @("'--firmware-modified'", "'$($FirmwareModifiedNote.Replace("'", "''"))'")
728 foreach ($arg in $ExtraFriiDumpArgs) {
729 $argLines += "'$($arg.Replace("'", "''"))'"
733 `$ErrorActionPreference = 'Stop'
734 `$exe = '$($Exe.Replace("'", "''"))'
736 $($argLines -join ",`r`n ")
738 Write-Host 'Allow data to be written, then press Ctrl+C once.'
742 Set-Content -LiteralPath $wrapper -Value $wrapperText -Encoding UTF8
745 Write-Host 'CONTROLLED CANCELLATION TEST PREPARED'
746 Write-Host 'Run this wrapper in a separate PowerShell window:'
747 Write-Host "powershell -NoProfile -ExecutionPolicy Bypass -File `"$wrapper`""
749 Write-Host 'After the dump has written data, press Ctrl+C once. Then validate all generated reports with:'
750 Write-Host "powershell -NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`" -Phase ValidateReports -SourceRoot `"$SourceRoot`" -ResultsRoot `"$ResultsRoot`""
752 Write-Host 'Expected cancellation report: beside the final .log, partial_dump / partial, failure_stage=user_cancelled, scope=partial_progress.'
753 Write-Host 'The copied GDR-8050L Windows reference-dumper path is not yet an approved cancellation target.'
758 $reports = @(Get-ChildItem -LiteralPath $ResultsRoot -Recurse -Filter '*.friidump.json' -File)
759 Assert-True ($reports.Count -gt 0) "No native reports were found under $ResultsRoot."
760 Invoke-ReportValidation $ResultsRoot
763 foreach ($file in $reports) {
764 $report = Read-Report $file.FullName
765 Test-CommonReportIdentity $report $file.FullName
766 $runId = [string]$report.run.run_id
767 Assert-True (-not $runIds.ContainsKey($runId)) "Duplicate run UUID: $runId"
768 $runIds[$runId] = $true
770 if ($report.dump.attempted -and $null -ne $report.dump.output_path) {
771 Test-DumpOutputIdentity $report ([string]$report.dump.output_path)
776 $reports | Where-Object {
777 $document = Read-Report $_.FullName
778 $document.dump.failure_stage -eq 'user_cancelled'
781 foreach ($file in $cancelReports) {
782 $report = Read-Report $file.FullName
783 Assert-True ($report.run.test_type -eq 'partial_dump') "$($file.FullName): cancellation test_type mismatch."
784 Assert-True ($report.run.result -eq 'partial') "$($file.FullName): cancellation run result mismatch."
785 Assert-True ($report.dump.result -eq 'partial') "$($file.FullName): cancellation dump result mismatch."
786 Assert-True (@($report.notes) -contains 'Measurement scope: partial_progress.') "$($file.FullName): cancellation scope mismatch."
788 $outputPath = [System.IO.Path]::GetFullPath([string]$report.dump.output_path)
789 $finalLogPath = "$outputPath.log"
790 $reportParent = [System.IO.Path]::GetFullPath($file.DirectoryName).TrimEnd([char[]]"\/")
791 $logParent = [System.IO.Path]::GetFullPath((Split-Path -Parent $finalLogPath)).TrimEnd([char[]]"\/")
793 Assert-True ($reportParent -eq $logParent) "$($file.FullName): default report is not beside the final FriiDump log."
794 Assert-True (Test-Path -LiteralPath $finalLogPath -PathType Leaf) "$($file.FullName): final FriiDump log is missing: $finalLogPath"
796 $logText = Get-Content -Raw -LiteralPath $finalLogPath
797 Assert-True ($logText.Contains('Dump status.......: CANCELLED (PARTIAL)')) "${finalLogPath}: corrected cancellation status is missing."
798 Assert-True ($logText.Contains("Cancelled at......: sector $($report.dump.failure_sector)")) "${finalLogPath}: corrected cancellation-sector line is missing."
799 Assert-True ($logText.Contains("Completed sectors.: $($report.dump.sector_count)")) "${finalLogPath}: completed-sector line is missing or inconsistent."
800 Assert-True ($logText.Contains("Completed bytes...: $($report.dump.byte_count)")) "${finalLogPath}: completed-byte line is missing or inconsistent."
801 Assert-True (-not $logText.Contains('MiB ISO payload')) "${finalLogPath}: obsolete full-payload throughput wording remains."
803 $averageMatch = [regex]::Match(
805 'Observed average\.\.: ([0-9]+(?:\.[0-9]+)?) MiB/h over ([0-9]+(?:\.[0-9]+)?) MiB completed output'
807 Assert-True ($averageMatch.Success) "${finalLogPath}: corrected completed-output throughput line is missing."
809 $culture = [System.Globalization.CultureInfo]::InvariantCulture
810 $observedRate = [double]::Parse($averageMatch.Groups[1].Value, $culture)
811 $observedMiB = [double]::Parse($averageMatch.Groups[2].Value, $culture)
812 $expectedMiB = [double]$report.dump.byte_count / 1MB
813 $expectedRate = $expectedMiB / [double]$report.dump.duration_seconds * 3600.0
815 Assert-True ([Math]::Abs($observedMiB - $expectedMiB) -lt 0.01) "${finalLogPath}: completed MiB does not match the report byte count."
816 Assert-True ([Math]::Abs($observedRate - $expectedRate) -lt 0.05) "${finalLogPath}: observed average does not use completed bytes and report duration."
818 # Verify that the native report and human-readable summary agree
819 # with the authoritative Nintendo platform magic in sector 0.
820 # This catches HLDS drives that accept out-of-range dummy reads
821 # and therefore defeat the legacy capacity-only classifier.
822 $header = New-Object byte[] 32
823 $stream = [System.IO.File]::OpenRead($outputPath)
826 $headerBytes = $stream.Read($header, 0, $header.Length)
832 if ($headerBytes -ge 32) {
834 ([uint32]$header[0x18] -shl 24) -bor
835 ([uint32]$header[0x19] -shl 16) -bor
836 ([uint32]$header[0x1a] -shl 8) -bor
837 [uint32]$header[0x1b]
841 ([uint32]$header[0x1c] -shl 24) -bor
842 ([uint32]$header[0x1d] -shl 16) -bor
843 ([uint32]$header[0x1e] -shl 8) -bor
844 [uint32]$header[0x1f]
847 if ($wiiMagic -eq [uint32]0x5d1c9ea3) {
849 $report.media.platform -eq 'wii'
850 ) "${finalLogPath}: Wii header magic does not match the native-report platform."
852 elseif ($gameCubeMagic -eq [uint32]0xc2339f3d) {
854 $report.media.platform -eq 'gamecube'
855 ) "${finalLogPath}: GameCube header magic does not match the native-report platform."
858 $logText.Contains('Disc type..........: GameCube')
859 ) "${finalLogPath}: GameCube header magic does not match the human-readable disc type."
862 $logText.Contains('Expected sectors..: 712880')
863 ) "${finalLogPath}: GameCube header magic does not match the expected-sector geometry."
869 Write-Host "LIVE NATIVE REPORT VALIDATION: PASS ($($reports.Count) reports)"
870 Write-Host "Controlled cancellation reports: $($cancelReports.Count)"
875 Assert-True (Test-Path -LiteralPath $ResultsRoot -PathType Container) "Results directory is missing: $ResultsRoot"
876 $destination = "$ResultsRoot-results.zip"
877 if (Test-Path -LiteralPath $destination) {
878 Remove-Item -LiteralPath $destination -Force
880 Compress-Archive -Path (Join-Path $ResultsRoot '*') -DestinationPath $destination -CompressionLevel Optimal
881 Write-Host 'VALIDATION RESULT COLLECTION: PASS'
882 Write-Host "Archive: $destination"
883 Write-Host "Bytes: $((Get-Item -LiteralPath $destination).Length)"
884 Write-Host "SHA-256: $(Get-Sha256 $destination)"