[CmdletBinding()] param( [ValidateSet( 'Build', 'NoMedia', 'GameCube', 'XboxIso', 'XboxXiso', 'XboxCancel', 'PrepareCancellation', 'ValidateReports', 'Collect' )] [string]$Phase = 'Build', [string]$SourceRoot = (Get-Location).Path, [string]$Drive, [string]$ResultsRoot, [string]$OutputName, [string]$FirmwareModifiedNote, [bool]$UseMethod8 = $true, [string[]]$ExtraFriiDumpArgs = @(), [string]$PhpValidator ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $ExpectedVersion = '0.5.3.16-pf1-candidate10' $ExpectedCommit = 'b03b227673ee00aec3c29d34038b3c86ee2d8b9a' $ExpectedSchemaSha256 = 'd1e97cb3e88e0f5a94ccf9b9edf921554fc15278662e69420b8c1e564c600411' $SourceRoot = (Resolve-Path -LiteralPath $SourceRoot).Path if ([string]::IsNullOrWhiteSpace($ResultsRoot)) { $ResultsRoot = Join-Path $SourceRoot '_native-report-live-validation' } $ResultsRoot = [System.IO.Path]::GetFullPath($ResultsRoot) $Exe = Join-Path $SourceRoot 'friidump.exe' $BuildScript = Join-Path $SourceRoot 'build_msvc32.cmd' $FixtureBuildScript = Join-Path $SourceRoot 'build_msvc32_native_report_tests.cmd' $Validator = Join-Path $SourceRoot 'tests\validate_native_reports.py' $Schema = Join-Path $SourceRoot 'tests\contracts\friidump-test-result.v1.schema.json' $SourceManifest = Join-Path $SourceRoot 'SOURCE_SHA256SUMS.txt' $MainResponseFile = Join-Path $SourceRoot 'msvc32_friidump.rsp' $FixtureResponseFile = Join-Path $SourceRoot 'msvc32_native_report_tests.rsp' function Assert-True { param( [Parameter(Mandatory)] [bool]$Condition, [Parameter(Mandatory)] [string]$Message ) if (-not $Condition) { throw $Message } } function Assert-LastExitCode { param( [Parameter(Mandatory)] [string]$Operation ) if ($LASTEXITCODE -ne 0) { throw "$Operation failed with exit code $LASTEXITCODE." } } function Get-Sha256 { param([Parameter(Mandatory)][string]$Path) return (Get-FileHash -Algorithm SHA256 -LiteralPath $Path).Hash.ToLowerInvariant() } function Test-SourceManifest { if (-not (Test-Path -LiteralPath $SourceManifest -PathType Leaf)) { Write-Host '[INFO] SOURCE_SHA256SUMS.txt is not present; package-manifest validation skipped.' return } $checked = 0 foreach ($line in Get-Content -LiteralPath $SourceManifest) { if ([string]::IsNullOrWhiteSpace($line)) { continue } if ($line -notmatch '^([0-9a-fA-F]{64}) (.+)$') { throw "Malformed source-manifest line: $line" } $expected = $Matches[1].ToLowerInvariant() $relative = $Matches[2] -replace '/', '\' $path = Join-Path $SourceRoot $relative Assert-True (Test-Path -LiteralPath $path -PathType Leaf) "Manifest file is missing: $relative" $actual = Get-Sha256 $path Assert-True ($actual -eq $expected) "Manifest SHA-256 mismatch: $relative" $checked++ } Write-Host "Source manifest: PASS ($checked files)" } function Test-MsvcSplitLinkContract { param( [Parameter(Mandatory)][string]$ResponsePath, [Parameter(Mandatory)][string]$BuildScriptPath, [Parameter(Mandatory)][string]$ExpectedInvocation, [Parameter(Mandatory)][string]$Label ) Assert-True (Test-Path -LiteralPath $ResponsePath -PathType Leaf) "$Label response file is missing: $ResponsePath" Assert-True (Test-Path -LiteralPath $BuildScriptPath -PathType Leaf) "$Label build script is missing: $BuildScriptPath" $lines = @( Get-Content -LiteralPath $ResponsePath | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } ) Assert-True (-not ($lines -icontains '/link')) "$Label response file must not contain /link. The linker boundary must be on the cl.exe command line." 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." $buildText = Get-Content -Raw -LiteralPath $BuildScriptPath Assert-True ($buildText.Contains($ExpectedInvocation)) "$Label build script is missing the required split compile/link invocation: $ExpectedInvocation" Write-Host "$Label MSVC command-line linker boundary: PASS" } function Test-PythonValidatorAvailable { & py -3 -c 'import jsonschema' 2>$null if ($LASTEXITCODE -ne 0) { throw 'Python 3 with jsonschema is required. Install it with: py -3 -m pip install jsonschema' } } function Invoke-ReportValidation { param([Parameter(Mandatory)][string]$ReportRoot) Assert-True (Test-Path -LiteralPath $Validator -PathType Leaf) "Validator is missing: $Validator" Assert-True (Test-Path -LiteralPath $Schema -PathType Leaf) "Schema is missing: $Schema" Assert-True ((Get-Sha256 $Schema) -eq $ExpectedSchemaSha256) 'Bundled schema SHA-256 mismatch.' Test-PythonValidatorAvailable $arguments = @( '-3', $Validator, '--fixtures', $ReportRoot, '--schema', $Schema, '--allow-arbitrary' ) if (-not [string]::IsNullOrWhiteSpace($PhpValidator)) { Assert-True (Test-Path -LiteralPath $PhpValidator -PathType Leaf) "PHP validator is missing: $PhpValidator" $arguments += @('--php-validator', $PhpValidator) } & py @arguments Assert-LastExitCode 'Strict native-report validation' } function Get-NewReport { param( [Parameter(Mandatory)][string]$ReportDirectory, [Parameter(Mandatory)][datetime]$Started ) $reports = @( Get-ChildItem -LiteralPath $ReportDirectory -Recurse -Filter '*.friidump.json' -File | Where-Object { $_.LastWriteTimeUtc -ge $Started.ToUniversalTime().AddSeconds(-2) } | Sort-Object LastWriteTimeUtc ) Assert-True ($reports.Count -eq 1) "Expected exactly one new report; found $($reports.Count)." return $reports[0] } function Read-Report { param([Parameter(Mandatory)][string]$Path) return Get-Content -Raw -LiteralPath $Path | ConvertFrom-Json } function Test-CommonReportIdentity { param( [Parameter(Mandatory)]$Report, [Parameter(Mandatory)][string]$ReportPath ) Assert-True ($Report.schema -eq 'friidump-test-result.v1') "${ReportPath}: schema mismatch." Assert-True ($Report.generator.name -eq 'FriiDump') "${ReportPath}: generator name mismatch." Assert-True ($Report.generator.version -eq $ExpectedVersion) "${ReportPath}: generator version mismatch." Assert-True ($Report.generator.build_commit -eq $ExpectedCommit) "${ReportPath}: build commit mismatch." Assert-True (-not [string]::IsNullOrWhiteSpace([string]$Report.run.run_id)) "${ReportPath}: run UUID missing." Assert-True (-not [string]::IsNullOrWhiteSpace([string]$Report.drive.model)) "${ReportPath}: drive model missing." Assert-True (-not [string]::IsNullOrWhiteSpace([string]$Report.drive.firmware_revision)) "${ReportPath}: firmware revision missing." if ([string]::IsNullOrWhiteSpace($FirmwareModifiedNote)) { Assert-True ($Report.drive.firmware_modified -eq $false) "${ReportPath}: omitted --firmware-modified must assume stock firmware." Assert-True ($null -eq $Report.drive.modification_note) "${ReportPath}: stock firmware must not have a modification note." } else { Assert-True ($Report.drive.firmware_modified -eq $true) "${ReportPath}: modified-firmware flag was not recorded." Assert-True ([string]$Report.drive.modification_note -eq $FirmwareModifiedNote) "${ReportPath}: modified-firmware explanation mismatch." } } function Test-DumpOutputIdentity { param( [Parameter(Mandatory)]$Report, [Parameter(Mandatory)][string]$ExpectedOutput ) $resolvedOutput = [System.IO.Path]::GetFullPath($ExpectedOutput) Assert-True (Test-Path -LiteralPath $resolvedOutput -PathType Leaf) "Dump output is missing: $resolvedOutput" $actualBytes = (Get-Item -LiteralPath $resolvedOutput).Length Assert-True ([int64]$Report.dump.byte_count -eq $actualBytes) 'Report byte count does not match the output file.' Assert-True ([string]$Report.dump.output_path -eq $resolvedOutput) 'Report output path does not match the requested path.' $artifacts = @($Report.artifacts | Where-Object { $_.type -eq 'dump_output' }) Assert-True ($artifacts.Count -eq 1) 'Exactly one dump_output artifact is required.' Assert-True ([string]$artifacts[0].path -eq $resolvedOutput) 'dump_output artifact path mismatch.' Assert-True ([int64]$artifacts[0].bytes -eq $actualBytes) 'dump_output artifact byte count mismatch.' if ($null -ne $Report.hashes.sha256) { $actualSha = Get-Sha256 $resolvedOutput Assert-True ([string]$Report.hashes.sha256 -eq $actualSha) 'Report SHA-256 does not match the output file.' Assert-True ([string]$artifacts[0].sha256 -eq $actualSha) 'Artifact SHA-256 does not match the output file.' } } function New-RunDirectory { param([Parameter(Mandatory)][string]$Name) $stamp = (Get-Date).ToUniversalTime().ToString('yyyyMMddTHHmmssZ') $path = Join-Path $ResultsRoot "$stamp-$Name" New-Item -ItemType Directory -Force -Path $path | Out-Null New-Item -ItemType Directory -Force -Path (Join-Path $path 'reports') | Out-Null return $path } function Invoke-FriiDumpRun { param( [Parameter(Mandatory)][string[]]$Arguments, [Parameter(Mandatory)][string]$RunDirectory, [Parameter(Mandatory)][bool]$ExpectSuccess ) $started = (Get-Date).ToUniversalTime() Write-Host "Working directory: $RunDirectory" Write-Host "Command: $Exe $($Arguments -join ' ')" Write-Host 'Console mode: direct live FriiDump output; FriiDump native .log is the evidence log.' # Do not route an interactive FriiDump run through a PowerShell # pipeline. Tee-Object buffers carriage-return progress updates, turns # native stderr into PowerShell error records, and also contaminates a # function's return stream with every line emitted by the child. # # Start FriiDump with inherited console handles instead. This preserves # real-time progress output and leaves this function with exactly one # return object. function ConvertTo-WindowsCommandLineArgument { param([AllowEmptyString()][string]$Value) if ($Value.Length -gt 0 -and $Value -notmatch '[\s"]') { return $Value } $builder = New-Object System.Text.StringBuilder [void]$builder.Append('"') $backslashes = 0 foreach ($character in $Value.ToCharArray()) { if ($character -eq '\') { $backslashes++ continue } if ($character -eq '"') { [void]$builder.Append(('\' * (($backslashes * 2) + 1))) [void]$builder.Append('"') $backslashes = 0 continue } if ($backslashes -gt 0) { [void]$builder.Append(('\' * $backslashes)) $backslashes = 0 } [void]$builder.Append($character) } if ($backslashes -gt 0) { [void]$builder.Append(('\' * ($backslashes * 2))) } [void]$builder.Append('"') return $builder.ToString() } $argumentLine = ( $Arguments | ForEach-Object { ConvertTo-WindowsCommandLineArgument ([string]$_) } ) -join ' ' $startInfo = New-Object System.Diagnostics.ProcessStartInfo $startInfo.FileName = $Exe $startInfo.Arguments = $argumentLine $startInfo.WorkingDirectory = $RunDirectory $startInfo.UseShellExecute = $false $startInfo.CreateNoWindow = $false $startInfo.RedirectStandardOutput = $false $startInfo.RedirectStandardError = $false $process = New-Object System.Diagnostics.Process $process.StartInfo = $startInfo try { [void]$process.Start() $process.WaitForExit() $exitCode = $process.ExitCode } finally { $process.Dispose() } Set-Content -LiteralPath (Join-Path $RunDirectory 'exit-code.txt') -Value $exitCode -Encoding ASCII if ($ExpectSuccess) { Assert-True ($exitCode -eq 0) "FriiDump was expected to succeed but exited with $exitCode." } return [pscustomobject]@{ Started = $started ExitCode = [int]$exitCode Log = $null } } function Add-ModifiedFirmwareArgument { param([string[]]$Arguments) if (-not [string]::IsNullOrWhiteSpace($FirmwareModifiedNote)) { return $Arguments + @('--firmware-modified', $FirmwareModifiedNote) } return $Arguments } New-Item -ItemType Directory -Force -Path $ResultsRoot | Out-Null switch ($Phase) { 'Build' { Assert-True (Test-Path -LiteralPath $BuildScript -PathType Leaf) "Build script is missing: $BuildScript" Assert-True (Test-Path -LiteralPath $FixtureBuildScript -PathType Leaf) "Fixture build script is missing: $FixtureBuildScript" Test-SourceManifest $XboxRegionHeader = Join-Path $SourceRoot 'libfriidump\xbox_region.h' $XboxLegacyUtilsHeader = Join-Path $SourceRoot 'libfriidump\xbox_ref\utils.h' $XboxRegionHeaderText = Get-Content -Raw -LiteralPath $XboxRegionHeader $XboxLegacyUtilsText = Get-Content -Raw -LiteralPath $XboxLegacyUtilsHeader Assert-True (-not $XboxRegionHeaderText.Contains('')) ` 'xbox_region.h must not introduce stdbool.h into the copied Xbox header graph.' Assert-True ($XboxRegionHeaderText.Contains('int xbox_region_format(')) ` 'xbox_region.h must expose the MSVC-safe int return type.' Assert-True ($XboxLegacyUtilsText.Contains('typedef int bool;')) ` 'The copied Xbox legacy bool declaration changed unexpectedly.' Write-Host 'Xbox region/MSVC bool isolation: PASS' $XboxCancelContract = Join-Path $SourceRoot 'tests\test_xbox_cancel_contract.py' Assert-True (Test-Path -LiteralPath $XboxCancelContract -PathType Leaf) ` 'Xbox copied-dumper cancellation contract test is missing.' & py -3 $XboxCancelContract --source-root $SourceRoot Assert-LastExitCode 'Xbox copied-dumper cancellation contract' Test-MsvcSplitLinkContract ` -ResponsePath $MainResponseFile ` -BuildScriptPath $BuildScript ` -ExpectedInvocation 'cl @"%EFFECTIVE_RSP%" /link ole32.lib' ` -Label 'FriiDump executable' Test-MsvcSplitLinkContract ` -ResponsePath $FixtureResponseFile ` -BuildScriptPath $FixtureBuildScript ` -ExpectedInvocation 'cl @"%~dp0msvc32_native_report_tests.rsp" /link ole32.lib' ` -Label 'Native-report fixtures' $env:FRIIDUMP_BUILD_COMMIT = $ExpectedCommit & $BuildScript clean Assert-LastExitCode 'MSVC clean' & $BuildScript Assert-LastExitCode 'MSVC candidate build' & $FixtureBuildScript Assert-LastExitCode 'MSVC native-report fixture validation' Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'friidump.exe was not produced.' # Windows PowerShell 5.1 converts native stderr records into # NativeCommandError objects when $ErrorActionPreference is Stop. # FriiDump intentionally writes part of its startup/help text to # stderr, so capture both streams through ProcessStartInfo instead # of invoking the executable directly through the PowerShell # pipeline. $helpStartInfo = New-Object System.Diagnostics.ProcessStartInfo $helpStartInfo.FileName = $Exe $helpStartInfo.Arguments = '--help' $helpStartInfo.UseShellExecute = $false $helpStartInfo.RedirectStandardOutput = $true $helpStartInfo.RedirectStandardError = $true $helpStartInfo.CreateNoWindow = $true $helpProcess = New-Object System.Diagnostics.Process $helpProcess.StartInfo = $helpStartInfo try { [void]$helpProcess.Start() # Drain stdout and stderr concurrently. Reading either redirected # stream synchronously before the other can deadlock when the # second pipe fills while the process is still running. $helpStdoutTask = $helpProcess.StandardOutput.ReadToEndAsync() $helpStderrTask = $helpProcess.StandardError.ReadToEndAsync() $helpProcess.WaitForExit() $helpStdout = $helpStdoutTask.GetAwaiter().GetResult() $helpStderr = $helpStderrTask.GetAwaiter().GetResult() $helpExitCode = $helpProcess.ExitCode } finally { $helpProcess.Dispose() } $help = @( $helpStdout, $helpStderr ) -join [Environment]::NewLine Assert-True ($helpExitCode -eq 1) "Candidate help command returned unexpected exit code $helpExitCode; FriiDump historically returns 1 for --help." Assert-True ($help.Contains("FriiDump $ExpectedVersion")) 'Candidate version is missing from help output.' foreach ($option in @('--report-json', '--report-dir', '--firmware-modified')) { Assert-True ($help.Contains($option)) "Candidate help is missing $option." } Assert-True ($help.Contains('Native .friidump.json reports are created by default')) 'Default native-report behavior is missing from help output.' Assert-True ($help.Contains('beside the final log')) 'Default beside-log report placement is missing from help output.' Assert-True ($help.Contains('(may be combined with --report-json)')) 'Combined --report-json/--report-dir behavior is missing from help output.' Assert-True ($help.Contains('omission assumes stock')) 'Stock-firmware default is missing from help output.' $buildRecord = @( "Generated UTC: $((Get-Date).ToUniversalTime().ToString('o'))", "Version: $ExpectedVersion", "Commit: $ExpectedCommit", "Executable: $Exe", "Executable SHA-256: $(Get-Sha256 $Exe)", 'MSVC build: PASS', 'MSVC native-report fixtures: PASS', 'Schema and semantic validation: PASS (13 reports)', 'Help/version/options/default-location smoke: PASS', 'Xbox copied-dumper cancellation, hashing, and summary contract: PASS' ) $recordPath = Join-Path $ResultsRoot 'build-validation.txt' Set-Content -LiteralPath $recordPath -Value $buildRecord -Encoding UTF8 Write-Host '' Write-Host 'FRIIDUMP CANDIDATE WINDOWS BUILD: PASS' Write-Host "Build record: $recordPath" break } 'NoMedia' { Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.' Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.' Read-Host "Remove all media from $Drive, wait for the drive to settle, then press Enter" $runDir = New-RunDirectory 'no-media-default-report' $output = Join-Path $runDir 'no-media-placeholder.iso' $args = @('-d', $Drive, '-i', $output) $args = Add-ModifiedFirmwareArgument $args $args += $ExtraFriiDumpArgs $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $false $reportFile = Get-NewReport -ReportDirectory $runDir -Started $run.Started Assert-True ($reportFile.Name -eq 'no-media-placeholder.iso.friidump.json') "Default report filename mismatch: $($reportFile.Name)" Assert-True ($reportFile.DirectoryName -eq $runDir) "Default no-media report was not placed beside the final log/output path." Invoke-ReportValidation $runDir $report = Read-Report $reportFile.FullName Test-CommonReportIdentity $report $reportFile.FullName Assert-True ($report.run.test_type -eq 'diagnostic_no_media') 'No-media test_type mismatch.' Assert-True ($report.run.result -eq 'not_applicable') 'No-media run result mismatch.' Assert-True ($report.dump.attempted -eq $false) 'No-media report must not claim a dump.' Assert-True (-not (Test-Path -LiteralPath $output)) 'No-media test unexpectedly created an output image.' Write-Host '' Write-Host 'NO-MEDIA DEFAULT NATIVE REPORT: PASS' Write-Host "Report: $($reportFile.FullName)" break } 'GameCube' { Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.' Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.' $runDir = New-RunDirectory 'gamecube-full-dump' if ([string]::IsNullOrWhiteSpace($OutputName)) { $OutputName = 'Sonic Mega Collection.iso' } $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName)) $args = @('-d', $Drive) if ($UseMethod8) { $args += '-8' } $args += @('-i', $output, '--report-dir', $reportDir) $args = Add-ModifiedFirmwareArgument $args $args += $ExtraFriiDumpArgs $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $true $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started Invoke-ReportValidation $reportDir $report = Read-Report $reportFile.FullName Test-CommonReportIdentity $report $reportFile.FullName Test-DumpOutputIdentity $report $output Assert-True ($report.media.platform -eq 'gamecube') 'Expected a GameCube report.' Assert-True ($report.run.test_type -eq 'full_dump' -and $report.run.result -eq 'pass') 'GameCube run outcome mismatch.' Assert-True ($report.dump.result -eq 'pass') 'GameCube dump result mismatch.' Assert-True (@($report.notes) -contains 'Measurement scope: full_optical_payload.') 'GameCube measurement scope mismatch.' Write-Host '' Write-Host 'GAMECUBE FULL-DUMP NATIVE REPORT: PASS' Write-Host "Report: $($reportFile.FullName)" break } 'XboxIso' { Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.' Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.' $runDir = New-RunDirectory 'xbox-redump-iso' $reportDir = Join-Path $runDir 'reports' if ([string]::IsNullOrWhiteSpace($OutputName)) { $OutputName = 'Red Faction II [TQ00501A].iso' } $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName)) $args = @('-d', $Drive, '-T', '4', '-i', $output, '--report-dir', $reportDir) $args = Add-ModifiedFirmwareArgument $args $args += $ExtraFriiDumpArgs $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $true $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started Invoke-ReportValidation $reportDir $report = Read-Report $reportFile.FullName Test-CommonReportIdentity $report $reportFile.FullName Test-DumpOutputIdentity $report $output Assert-True ($report.media.platform -eq 'xbox') 'Expected an Xbox report.' Assert-True ($report.media.region -eq 'North America') 'Expected the XBE GameRegion to report North America.' Assert-True (@($report.notes) -contains 'Xbox XBE GameRegion mask: 0x00000001.') 'Expected the raw XBE GameRegion mask note.' Assert-True ($report.run.test_type -eq 'full_dump' -and $report.run.result -eq 'pass') 'Xbox ISO run outcome mismatch.' Assert-True (@($report.notes) -contains 'Measurement scope: assembled_output.') 'Xbox ISO measurement scope mismatch.' Write-Host '' Write-Host 'XBOX REDUMP-STYLE ISO NATIVE REPORT: PASS' Write-Host "Report: $($reportFile.FullName)" break } 'XboxXiso' { Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.' Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.' $runDir = New-RunDirectory 'xbox-xiso' $reportDir = Join-Path $runDir 'reports' if ([string]::IsNullOrWhiteSpace($OutputName)) { $OutputName = 'Red Faction II [TQ00501A].xiso' } $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName)) $args = @('-d', $Drive, '-T', '4', '-X', $output, '--report-dir', $reportDir) $args = Add-ModifiedFirmwareArgument $args $args += $ExtraFriiDumpArgs $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $true $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started Invoke-ReportValidation $reportDir $report = Read-Report $reportFile.FullName Test-CommonReportIdentity $report $reportFile.FullName Test-DumpOutputIdentity $report $output Assert-True ($report.media.platform -eq 'xbox') 'Expected an Xbox report.' Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.media.region)) 'Expected an XBE-derived Xbox region.' Assert-True ($report.run.test_type -eq 'full_dump' -and $report.run.result -eq 'pass') 'Xbox XISO run outcome mismatch.' Assert-True (@($report.notes) -contains 'Measurement scope: assembled_output.') 'Xbox XISO measurement scope mismatch.' Write-Host '' Write-Host 'XBOX XISO NATIVE REPORT: PASS' Write-Host "Report: $($reportFile.FullName)" break } 'XboxCancel' { Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.' Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.' $runDir = New-RunDirectory 'xbox-controlled-cancellation' $reportDir = Join-Path $runDir 'reports' if ([string]::IsNullOrWhiteSpace($OutputName)) { $OutputName = 'Red Faction II [TQ00501A].xiso' } $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName)) $args = @('-d', $Drive, '-T', '4', '-X', $output, '--report-dir', $reportDir) $args = Add-ModifiedFirmwareArgument $args $args += $ExtraFriiDumpArgs Write-Host '' Write-Host 'After Regions: North America appears and XISO progress begins, press Ctrl+C once.' Write-Host '' $run = Invoke-FriiDumpRun -Arguments $args -RunDirectory $runDir -ExpectSuccess $false $reportFile = Get-NewReport -ReportDirectory $reportDir -Started $run.Started Invoke-ReportValidation $reportDir $report = Read-Report $reportFile.FullName Test-CommonReportIdentity $report $reportFile.FullName Test-DumpOutputIdentity $report $output Assert-True ($report.media.platform -eq 'xbox') 'Expected an Xbox report.' Assert-True ($report.media.region -eq 'North America') 'Expected the XBE GameRegion to report North America.' Assert-True (@($report.notes) -contains 'Xbox XBE GameRegion mask: 0x00000001.') 'Expected the raw XBE GameRegion mask note.' Assert-True ($report.run.test_type -eq 'partial_dump' -and $report.run.result -eq 'partial') 'Xbox cancellation run outcome mismatch.' Assert-True ($report.dump.result -eq 'partial') 'Xbox cancellation dump result mismatch.' Assert-True ($report.dump.failure_stage -eq 'user_cancelled') 'Xbox cancellation failure stage mismatch.' Assert-True ([int64]$report.dump.sector_count -gt 32) 'Xbox cancellation did not record meaningful partial progress.' Assert-True ([int64]$report.dump.byte_count -eq ([int64]$report.dump.sector_count * 2048)) 'Xbox cancellation byte/sector accounting mismatch.' Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.crc32)) 'Partial Xbox CRC32 is missing.' Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.md5)) 'Partial Xbox MD5 is missing.' Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.sha1)) 'Partial Xbox SHA-1 is missing.' Assert-True (-not [string]::IsNullOrWhiteSpace([string]$report.hashes.sha256)) 'Partial Xbox SHA-256 is missing.' $nativeLog = "$output.log" Assert-True (Test-Path -LiteralPath $nativeLog -PathType Leaf) "Xbox native log is missing: $nativeLog" $logText = Get-Content -Raw -LiteralPath $nativeLog foreach ($expectedText in @( 'Game/Media ID.....: TQ00501A', 'Title.............: Red Faction', 'Region............: North America', 'Seed read.........: N/A (Xbox reference auth path)', 'Source sectors....: 3431264', 'Expected output...: 1913920 sectors', '[OK] Controlled partial Xbox output hashes captured.' )) { Assert-True ($logText.Contains($expectedText)) "Xbox summary/log is missing: $expectedText" } Write-Host '' Write-Host 'XBOX REGION, SUMMARY, GEOMETRY, CANCELLATION, AND HASHING: PASS' Write-Host "Report: $($reportFile.FullName)" break } 'PrepareCancellation' { Assert-True (Test-Path -LiteralPath $Exe -PathType Leaf) 'Build the candidate first with -Phase Build.' Assert-True (-not [string]::IsNullOrWhiteSpace($Drive)) '-Drive is required.' $runDir = New-RunDirectory 'controlled-cancellation' if ([string]::IsNullOrWhiteSpace($OutputName)) { $OutputName = 'controlled-cancellation.partial.iso' } $output = [System.IO.Path]::GetFullPath((Join-Path $runDir $OutputName)) $wrapper = Join-Path $runDir 'run-controlled-cancellation.ps1' $argLines = @("'-d'", "'$($Drive.Replace("'", "''"))'") if ($UseMethod8) { $argLines += "'-8'" } $argLines += @( "'-i'", "'$($output.Replace("'", "''"))'" ) if (-not [string]::IsNullOrWhiteSpace($FirmwareModifiedNote)) { $argLines += @("'--firmware-modified'", "'$($FirmwareModifiedNote.Replace("'", "''"))'") } foreach ($arg in $ExtraFriiDumpArgs) { $argLines += "'$($arg.Replace("'", "''"))'" } $wrapperText = @" `$ErrorActionPreference = 'Stop' `$exe = '$($Exe.Replace("'", "''"))' `$arguments = @( $($argLines -join ",`r`n ") ) Write-Host 'Allow data to be written, then press Ctrl+C once.' & `$exe @arguments exit `$LASTEXITCODE "@ Set-Content -LiteralPath $wrapper -Value $wrapperText -Encoding UTF8 Write-Host '' Write-Host 'CONTROLLED CANCELLATION TEST PREPARED' Write-Host 'Run this wrapper in a separate PowerShell window:' Write-Host "powershell -NoProfile -ExecutionPolicy Bypass -File `"$wrapper`"" Write-Host '' Write-Host 'After the dump has written data, press Ctrl+C once. Then validate all generated reports with:' Write-Host "powershell -NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`" -Phase ValidateReports -SourceRoot `"$SourceRoot`" -ResultsRoot `"$ResultsRoot`"" Write-Host '' Write-Host 'Expected cancellation report: beside the final .log, partial_dump / partial, failure_stage=user_cancelled, scope=partial_progress.' Write-Host 'The copied GDR-8050L Windows reference-dumper path is not yet an approved cancellation target.' break } 'ValidateReports' { $reports = @(Get-ChildItem -LiteralPath $ResultsRoot -Recurse -Filter '*.friidump.json' -File) Assert-True ($reports.Count -gt 0) "No native reports were found under $ResultsRoot." Invoke-ReportValidation $ResultsRoot $runIds = @{} foreach ($file in $reports) { $report = Read-Report $file.FullName Test-CommonReportIdentity $report $file.FullName $runId = [string]$report.run.run_id Assert-True (-not $runIds.ContainsKey($runId)) "Duplicate run UUID: $runId" $runIds[$runId] = $true if ($report.dump.attempted -and $null -ne $report.dump.output_path) { Test-DumpOutputIdentity $report ([string]$report.dump.output_path) } } $cancelReports = @( $reports | Where-Object { $document = Read-Report $_.FullName $document.dump.failure_stage -eq 'user_cancelled' } ) foreach ($file in $cancelReports) { $report = Read-Report $file.FullName Assert-True ($report.run.test_type -eq 'partial_dump') "$($file.FullName): cancellation test_type mismatch." Assert-True ($report.run.result -eq 'partial') "$($file.FullName): cancellation run result mismatch." Assert-True ($report.dump.result -eq 'partial') "$($file.FullName): cancellation dump result mismatch." Assert-True (@($report.notes) -contains 'Measurement scope: partial_progress.') "$($file.FullName): cancellation scope mismatch." $outputPath = [System.IO.Path]::GetFullPath([string]$report.dump.output_path) $finalLogPath = "$outputPath.log" $reportParent = [System.IO.Path]::GetFullPath($file.DirectoryName).TrimEnd([char[]]"\/") $logParent = [System.IO.Path]::GetFullPath((Split-Path -Parent $finalLogPath)).TrimEnd([char[]]"\/") Assert-True ($reportParent -eq $logParent) "$($file.FullName): default report is not beside the final FriiDump log." Assert-True (Test-Path -LiteralPath $finalLogPath -PathType Leaf) "$($file.FullName): final FriiDump log is missing: $finalLogPath" $logText = Get-Content -Raw -LiteralPath $finalLogPath Assert-True ($logText.Contains('Dump status.......: CANCELLED (PARTIAL)')) "${finalLogPath}: corrected cancellation status is missing." Assert-True ($logText.Contains("Cancelled at......: sector $($report.dump.failure_sector)")) "${finalLogPath}: corrected cancellation-sector line is missing." Assert-True ($logText.Contains("Completed sectors.: $($report.dump.sector_count)")) "${finalLogPath}: completed-sector line is missing or inconsistent." Assert-True ($logText.Contains("Completed bytes...: $($report.dump.byte_count)")) "${finalLogPath}: completed-byte line is missing or inconsistent." Assert-True (-not $logText.Contains('MiB ISO payload')) "${finalLogPath}: obsolete full-payload throughput wording remains." $averageMatch = [regex]::Match( $logText, 'Observed average\.\.: ([0-9]+(?:\.[0-9]+)?) MiB/h over ([0-9]+(?:\.[0-9]+)?) MiB completed output' ) Assert-True ($averageMatch.Success) "${finalLogPath}: corrected completed-output throughput line is missing." $culture = [System.Globalization.CultureInfo]::InvariantCulture $observedRate = [double]::Parse($averageMatch.Groups[1].Value, $culture) $observedMiB = [double]::Parse($averageMatch.Groups[2].Value, $culture) $expectedMiB = [double]$report.dump.byte_count / 1MB $expectedRate = $expectedMiB / [double]$report.dump.duration_seconds * 3600.0 Assert-True ([Math]::Abs($observedMiB - $expectedMiB) -lt 0.01) "${finalLogPath}: completed MiB does not match the report byte count." Assert-True ([Math]::Abs($observedRate - $expectedRate) -lt 0.05) "${finalLogPath}: observed average does not use completed bytes and report duration." # Verify that the native report and human-readable summary agree # with the authoritative Nintendo platform magic in sector 0. # This catches HLDS drives that accept out-of-range dummy reads # and therefore defeat the legacy capacity-only classifier. $header = New-Object byte[] 32 $stream = [System.IO.File]::OpenRead($outputPath) try { $headerBytes = $stream.Read($header, 0, $header.Length) } finally { $stream.Dispose() } if ($headerBytes -ge 32) { $wiiMagic = ( ([uint32]$header[0x18] -shl 24) -bor ([uint32]$header[0x19] -shl 16) -bor ([uint32]$header[0x1a] -shl 8) -bor [uint32]$header[0x1b] ) $gameCubeMagic = ( ([uint32]$header[0x1c] -shl 24) -bor ([uint32]$header[0x1d] -shl 16) -bor ([uint32]$header[0x1e] -shl 8) -bor [uint32]$header[0x1f] ) if ($wiiMagic -eq [uint32]0x5d1c9ea3) { Assert-True ( $report.media.platform -eq 'wii' ) "${finalLogPath}: Wii header magic does not match the native-report platform." } elseif ($gameCubeMagic -eq [uint32]0xc2339f3d) { Assert-True ( $report.media.platform -eq 'gamecube' ) "${finalLogPath}: GameCube header magic does not match the native-report platform." Assert-True ( $logText.Contains('Disc type..........: GameCube') ) "${finalLogPath}: GameCube header magic does not match the human-readable disc type." Assert-True ( $logText.Contains('Expected sectors..: 712880') ) "${finalLogPath}: GameCube header magic does not match the expected-sector geometry." } } } Write-Host '' Write-Host "LIVE NATIVE REPORT VALIDATION: PASS ($($reports.Count) reports)" Write-Host "Controlled cancellation reports: $($cancelReports.Count)" break } 'Collect' { Assert-True (Test-Path -LiteralPath $ResultsRoot -PathType Container) "Results directory is missing: $ResultsRoot" $destination = "$ResultsRoot-results.zip" if (Test-Path -LiteralPath $destination) { Remove-Item -LiteralPath $destination -Force } Compress-Archive -Path (Join-Path $ResultsRoot '*') -DestinationPath $destination -CompressionLevel Optimal Write-Host 'VALIDATION RESULT COLLECTION: PASS' Write-Host "Archive: $destination" Write-Host "Bytes: $((Get-Item -LiteralPath $destination).Length)" Write-Host "SHA-256: $(Get-Sha256 $destination)" break } }