# Synthetic restore test. Creates only a new lab folder beside this script. # No user-file input, no external-drive access, no deletion, no overwrite switch. Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' $labRoot = Join-Path $PSScriptRoot ('run-' + [guid]::NewGuid().ToString('N')) $source = Join-Path $labRoot 'source' New-Item -ItemType Directory -Path $source | Out-Null $utf8 = New-Object System.Text.UTF8Encoding($false) function Put-TestText([string]$relative, [string]$text) { $target = Join-Path $source $relative [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($target)) | Out-Null [IO.File]::WriteAllText($target, $text, $utf8) } Put-TestText 'empty.txt' '' Put-TestText '한글 이름.txt' "합성 복원 시험입니다.`n" Put-TestText 'same-a.txt' 'AAAA' Put-TestText 'same-b.txt' 'BBBB' Put-TestText 'notes.txt' "Synthetic fixture, not a user backup.`n" Put-TestText 'nested/level2/sample.txt' "nested payload`n" Put-TestText 'nested/other/sample.txt' "different relative path`n" [IO.File]::WriteAllBytes((Join-Path $source 'bytes.bin'), [byte[]](0..255)) function Manifest([string]$root) { $prefix = [IO.Path]::GetFullPath($root).TrimEnd('\','/') + [IO.Path]::DirectorySeparatorChar @(Get-ChildItem -LiteralPath $root -File -Recurse -Force | ForEach-Object { [pscustomobject]@{ RelativePath = $_.FullName.Substring($prefix.Length).Replace('\','/') Bytes = $_.Length SHA256 = (Get-FileHash -LiteralPath $_.FullName -Algorithm SHA256).Hash } } | Sort-Object RelativePath) } $baseline = @(Manifest $source) $zip = Join-Path $labRoot 'fixture.zip' Compress-Archive -LiteralPath $source -DestinationPath $zip $restoredParent = Join-Path $labRoot 'restored' Expand-Archive -LiteralPath $zip -DestinationPath $restoredParent $restored = Join-Path $restoredParent 'source' # Positive control 1: make an incomplete copy, without deleting anything. $missing = Join-Path $labRoot 'missing-copy' New-Item -ItemType Directory -Path $missing | Out-Null foreach ($item in $baseline) { if ($item.RelativePath -eq 'nested/level2/sample.txt') { continue } $target = Join-Path $missing $item.RelativePath [IO.Directory]::CreateDirectory([IO.Path]::GetDirectoryName($target)) | Out-Null Copy-Item -LiteralPath (Join-Path $restored $item.RelativePath) -Destination $target } # Positive control 2: alter four bytes in another fresh restored synthetic copy. $changedParent = Join-Path $labRoot 'changed' Expand-Archive -LiteralPath $zip -DestinationPath $changedParent $changed = Join-Path $changedParent 'source' [IO.File]::WriteAllText((Join-Path $changed 'same-a.txt'), 'ZZZZ', $utf8) $details = @() $summary = @() foreach ($case in @( @{Name='zip_restore'; Root=$restored; Expected='PASS'}, @{Name='missing_copy'; Root=$missing; Expected='FAIL'}, @{Name='same_size_change'; Root=$changed; Expected='FAIL'} )) { $actual = @(Manifest $case.Root) $index = @{} foreach ($item in $actual) { $index[$item.RelativePath] = $item } $caseRows = @() foreach ($expected in $baseline) { $found = $index[$expected.RelativePath] $status = if ($null -eq $found) { 'MISSING' } elseif ($expected.Bytes -ne $found.Bytes) { 'SIZE_MISMATCH' } elseif ($expected.SHA256 -ne $found.SHA256) { 'HASH_MISMATCH' } else { 'MATCH' } $caseRows += [pscustomobject]@{Case=$case.Name;RelativePath=$expected.RelativePath;ExpectedBytes=$expected.Bytes;ActualBytes=$(if($null -ne $found){$found.Bytes}else{$null});ExpectedSHA256=$expected.SHA256;ActualSHA256=$(if($null -ne $found){$found.SHA256}else{$null});Status=$status} $index.Remove($expected.RelativePath) } foreach ($extra in $index.Values) { $caseRows += [pscustomobject]@{Case=$case.Name;RelativePath=$extra.RelativePath;ExpectedBytes=$null;ActualBytes=$extra.Bytes;ExpectedSHA256=$null;ActualSHA256=$extra.SHA256;Status='UNEXPECTED'} } $failures = @($caseRows | Where-Object Status -ne 'MATCH') $verdict = if ($failures.Count -eq 0) {'PASS'} else {'FAIL'} $summary += [pscustomobject]@{Case=$case.Name;ExpectedFiles=$baseline.Count;ActualFiles=$actual.Count;Matched=@($caseRows | Where-Object Status -eq 'MATCH').Count;Issues=$failures.Count;Verdict=$verdict;ControlBehavedAsExpected=($verdict -eq $case.Expected)} $details += $caseRows } $baseline | Export-Csv -LiteralPath (Join-Path $labRoot 'manifest.csv') -NoTypeInformation -Encoding UTF8 $details | Export-Csv -LiteralPath (Join-Path $labRoot 'comparison.csv') -NoTypeInformation -Encoding UTF8 $summary | Export-Csv -LiteralPath (Join-Path $labRoot 'summary.csv') -NoTypeInformation -Encoding UTF8 $result = [ordered]@{ExecutedAt=[DateTimeOffset]::Now.ToString('o');PowerShell=$PSVersionTable.PSVersion.ToString();OS=[Environment]::OSVersion.VersionString;ArchiveModule=(Get-Module Microsoft.PowerShell.Archive).Version.ToString();FixtureFileCount=$baseline.Count;FixtureBytes=($baseline | Measure-Object Bytes -Sum).Sum;ArchiveBytes=(Get-Item -LiteralPath $zip).Length;TestScope='Synthetic local byte-content restore; no performance, hardware reliability, ACL, application-open, malware or encryption testing';SourceManifest=$baseline;Summary=$summary;Comparison=$details} $result | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $labRoot 'results.json') -Encoding UTF8 $summary | Format-Table -AutoSize Write-Output "Evidence folder: $labRoot" if (@($summary | Where-Object { -not $_.ControlBehavedAsExpected }).Count -gt 0) { throw 'Control result unexpected; inspect results.json.' }