# Exercise persistent-workspace updates on regular files only. No disk cmdlets.
[CmdletBinding()]
param([Parameter(Mandatory=$true)][string]$Image,[Parameter(Mandatory=$true)][string]$SavedUsbImage)
$ErrorActionPreference='Stop'
$candidateImage=$Image
. (Join-Path $PSScriptRoot 'physical-media.ps1') -LoadFunctionsOnly
$Image=$candidateImage
$manifest=Get-Content -LiteralPath ($Image+'.json') -Raw -Encoding UTF8 | ConvertFrom-Json
if($manifest.UsbWorkspace -ne $true){throw 'A USB workspace image is required.'}
$originalHash=(Get-FileHash -LiteralPath $Image -Algorithm SHA256).Hash
$savedHash=(Get-FileHash -LiteralPath $SavedUsbImage -Algorithm SHA256).Hash
$directory=Join-Path ([IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../build'))) ('UsbUpdateTest-'+[Guid]::NewGuid().ToString('N'))
[void][IO.Directory]::CreateDirectory($directory)
$oldPath=Join-Path $directory 'Saved.img';Copy-Item -LiteralPath $SavedUsbImage -Destination $oldPath
$targetPath=Join-Path $directory 'Updated.img';Copy-Item -LiteralPath $Image -Destination $targetPath
$source=$old=$target=$null
try {
$source=[IO.File]::OpenRead([IO.Path]::GetFullPath($Image))
$old=[IO.File]::Open($oldPath,[IO.FileMode]::Open,[IO.FileAccess]::ReadWrite,[IO.FileShare]::Read)
$target=[IO.File]::Open($targetPath,[IO.FileMode]::Open,[IO.FileAccess]::ReadWrite,[IO.FileShare]::Read)
# UsbHello is a standard managed PE stored outside the journal. Hash its
# actual FAT chain to guard against losing a same-name user replacement.
$header=Read-XfeBytes $old (2048*512) 512
$fatSectors=[BitConverter]::ToUInt32($header,36);$reserved=[BitConverter]::ToUInt16($header,14)
$fat=Read-XfeBytes $old ((2048+$reserved)*512) ($fatSectors*512)
$data=(2048+$reserved+2*$fatSectors)*512
$root=@(Get-XfeFatDirectory $old $data $fat 2 512)
$folder=@($root | Where-Object ShortName -CEQ 'USERFILE ')[0]
$import=@(Get-XfeFatDirectory $old $data $fat $folder.Cluster 512)[0]
$chunk=Read-XfeBytes $old ($data+($import.Cluster-2)*512) $import.Size
$hash=[Security.Cryptography.SHA256]::Create()
try {$importHash=([BitConverter]::ToString($hash.ComputeHash($chunk))).Replace('-','').ToLowerInvariant()}finally{$hash.Dispose()}
$preserved=Get-XfePreservedWorkspace $old $old.Length $directory $importHash
if(!$preserved -or $preserved.Bytes.Length -ne 64MB -or (Get-Item -LiteralPath $preserved.SnapshotPath).Length -ne 64MB){throw 'Workspace backup was incomplete.'}
$patch=[pscustomobject]@{Offset=[uint64]$manifest.WorkspaceFirstLba*512;Bytes=$preserved.Bytes}
$target.SetLength(300MB)
Write-XfeBytes $target $patch.Offset $patch.Bytes
Set-XfeAdjustedGpt $target (Get-XfeAdjustedGpt $source $target.Length)
$target.Flush($true)
$verified=Test-XfeAdjustedReadback $source $target $target.Length $patch
if(!$verified.FullImageVerified -or !$verified.ActualTailBackupGptVerified){throw 'Workspace-preserving full update verification failed.'}
Write-XfeBytes $target ($patch.Offset+1024) ([byte[]]@($patch.Bytes[1024] -bxor 1))
$refused=$false;try {Test-XfeAdjustedReadback $source $target $target.Length $patch|Out-Null}catch{$refused=$true}
if(!$refused){throw 'Changed workspace bytes escaped full readback.'}
Write-XfeBytes $old ($data+($import.Cluster-2)*512) ([byte[]]@(0))
$refused=$false;try {Get-XfePreservedWorkspace $old $old.Length $directory $importHash|Out-Null}catch{$refused=$true}
if(!$refused){throw 'A changed UserFiles/UsbHello.dll would be overwritten.'}
Write-XfeBytes $old ($data+($import.Cluster-2)*512) ([byte[]]@($chunk[0]))
Write-XfeBytes $old ($preserved.OriginalOffset+20) ([byte[]]@($preserved.Bytes[20] -bxor 1))
$refused=$false;try {Get-XfePreservedWorkspace $old $old.Length $directory $importHash|Out-Null}catch{$refused=$true}
if(!$refused){throw 'Damaged workspace descriptor was accepted.'}
Write-Host 'USB_WORKSPACE_UPDATE_GUARDS_OK: complete backup/restore, resized GPT, full data readback, changed import and descriptor refusal; regular files only.'
} finally {
foreach($stream in @($source,$old,$target)){if($stream){$stream.Dispose()}}
if((Get-FileHash -LiteralPath $Image -Algorithm SHA256).Hash -cne $originalHash -or
(Get-FileHash -LiteralPath $SavedUsbImage -Algorithm SHA256).Hash -cne $savedHash){throw 'An original evidence image changed.'}
}
# Exercise persistent-workspace updates on regular files only. No disk cmdlets.
[CmdletBinding()]
param([Parameter(Mandatory=$true)][string]$Image,[Parameter(Mandatory=$true)][string]$SavedUsbImage)
$ErrorActionPreference='Stop'
$candidateImage=$Image
. (Join-Path $PSScriptRoot 'physical-media.ps1') -LoadFunctionsOnly
$Image=$candidateImage
$manifest=Get-Content -LiteralPath ($Image+'.json') -Raw -Encoding UTF8 | ConvertFrom-Json
if($manifest.UsbWorkspace -ne $true){throw 'A USB workspace image is required.'}
$originalHash=(Get-FileHash -LiteralPath $Image -Algorithm SHA256).Hash
$savedHash=(Get-FileHash -LiteralPath $SavedUsbImage -Algorithm SHA256).Hash
$directory=Join-Path ([IO.Path]::GetFullPath((Join-Path $PSScriptRoot '../build'))) ('UsbUpdateTest-'+[Guid]::NewGuid().ToString('N'))
[void][IO.Directory]::CreateDirectory($directory)
$oldPath=Join-Path $directory 'Saved.img';Copy-Item -LiteralPath $SavedUsbImage -Destination $oldPath
$targetPath=Join-Path $directory 'Updated.img';Copy-Item -LiteralPath $Image -Destination $targetPath
$source=$old=$target=$null
try {
$source=[IO.File]::OpenRead([IO.Path]::GetFullPath($Image))
$old=[IO.File]::Open($oldPath,[IO.FileMode]::Open,[IO.FileAccess]::ReadWrite,[IO.FileShare]::Read)
$target=[IO.File]::Open($targetPath,[IO.FileMode]::Open,[IO.FileAccess]::ReadWrite,[IO.FileShare]::Read)
# UsbHello is a standard managed PE stored outside the journal. Hash its
# actual FAT chain to guard against losing a same-name user replacement.
$header=Read-XfeBytes $old (2048*512) 512
$fatSectors=[BitConverter]::ToUInt32($header,36);$reserved=[BitConverter]::ToUInt16($header,14)
$fat=Read-XfeBytes $old ((2048+$reserved)*512) ($fatSectors*512)
$data=(2048+$reserved+2*$fatSectors)*512
$root=@(Get-XfeFatDirectory $old $data $fat 2 512)
$folder=@($root | Where-Object ShortName -CEQ 'USERFILE ')[0]
$import=@(Get-XfeFatDirectory $old $data $fat $folder.Cluster 512)[0]
$chunk=Read-XfeBytes $old ($data+($import.Cluster-2)*512) $import.Size
$hash=[Security.Cryptography.SHA256]::Create()
try {$importHash=([BitConverter]::ToString($hash.ComputeHash($chunk))).Replace('-','').ToLowerInvariant()}finally{$hash.Dispose()}
$preserved=Get-XfePreservedWorkspace $old $old.Length $directory $importHash
if(!$preserved -or $preserved.Bytes.Length -ne 64MB -or (Get-Item -LiteralPath $preserved.SnapshotPath).Length -ne 64MB){throw 'Workspace backup was incomplete.'}
$patch=[pscustomobject]@{Offset=[uint64]$manifest.WorkspaceFirstLba*512;Bytes=$preserved.Bytes}
$target.SetLength(300MB)
Write-XfeBytes $target $patch.Offset $patch.Bytes
Set-XfeAdjustedGpt $target (Get-XfeAdjustedGpt $source $target.Length)
$target.Flush($true)
$verified=Test-XfeAdjustedReadback $source $target $target.Length $patch
if(!$verified.FullImageVerified -or !$verified.ActualTailBackupGptVerified){throw 'Workspace-preserving full update verification failed.'}
Write-XfeBytes $target ($patch.Offset+1024) ([byte[]]@($patch.Bytes[1024] -bxor 1))
$refused=$false;try {Test-XfeAdjustedReadback $source $target $target.Length $patch|Out-Null}catch{$refused=$true}
if(!$refused){throw 'Changed workspace bytes escaped full readback.'}
Write-XfeBytes $old ($data+($import.Cluster-2)*512) ([byte[]]@(0))
$refused=$false;try {Get-XfePreservedWorkspace $old $old.Length $directory $importHash|Out-Null}catch{$refused=$true}
if(!$refused){throw 'A changed UserFiles/UsbHello.dll would be overwritten.'}
Write-XfeBytes $old ($data+($import.Cluster-2)*512) ([byte[]]@($chunk[0]))
Write-XfeBytes $old ($preserved.OriginalOffset+20) ([byte[]]@($preserved.Bytes[20] -bxor 1))
$refused=$false;try {Get-XfePreservedWorkspace $old $old.Length $directory $importHash|Out-Null}catch{$refused=$true}
if(!$refused){throw 'Damaged workspace descriptor was accepted.'}
Write-Host 'USB_WORKSPACE_UPDATE_GUARDS_OK: complete backup/restore, resized GPT, full data readback, changed import and descriptor refusal; regular files only.'
} finally {
foreach($stream in @($source,$old,$target)){if($stream){$stream.Dispose()}}
if((Get-FileHash -LiteralPath $Image -Algorithm SHA256).Hash -cne $originalHash -or
(Get-FileHash -LiteralPath $SavedUsbImage -Algorithm SHA256).Hash -cne $savedHash){throw 'An original evidence image changed.'}
}