# Windows host disk writer. The default only inspects and prints a disk-specific
# plan. Never invoke -Write until a human has selected and authorized that disk.
[CmdletBinding()]
param(
[string]$Image,
[int]$DiskNumber = -1,
[string]$PlanPath,
[switch]$Write,
[string]$ConfirmToken,
[switch]$LoadFunctionsOnly
)
$ErrorActionPreference = 'Stop'
function Get-XfeToken([string]$Value) {
$hash = [Security.Cryptography.SHA256]::Create()
try { return ([BitConverter]::ToString($hash.ComputeHash([Text.Encoding]::UTF8.GetBytes($Value)))).Replace('-', '').ToLowerInvariant() }
finally { $hash.Dispose() }
}
function Open-XfeLockedImage([string]$Path,[string]$ExpectedHash) {
# Sharing read access only prevents concurrent writes, replacement or delete.
# Hash and the later disk copy use this very same handle, held until finally.
$stream=[IO.File]::Open($Path,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::Read)
$hash=[Security.Cryptography.SHA256]::Create()
try {
$actual=([BitConverter]::ToString($hash.ComputeHash($stream))).Replace('-','').ToLowerInvariant()
if($actual -cne $ExpectedHash){throw 'Image changed since the plan; refusing before any target mutation.'}
[void]$stream.Seek(0,[IO.SeekOrigin]::Begin)
return $stream
} catch {$stream.Dispose();throw} finally {$hash.Dispose()}
}
function Assert-XfeDisk($Disk, $Partitions, [uint64]$ImageBytes) {
if ($null -eq $Disk -or $Disk.Number -lt 0) { throw 'A concrete target disk is required.' }
if ($Disk.IsSystem -or $Disk.IsBoot) { throw 'Refusing the Windows system or boot disk.' }
if ([string]::IsNullOrWhiteSpace([string]$Disk.UniqueId)) { throw 'Disk UniqueId is unavailable; refusing ambiguous identity.' }
if ([uint64]$Disk.Size -lt $ImageBytes) { throw 'The selected disk is smaller than the image.' }
if ([uint64]$Disk.LogicalSectorSize -ne 512) { throw 'Only 512-byte logical sectors are supported by this candidate.' }
if ($Disk.IsReadOnly) { throw 'The selected disk is read-only.' }
if ([string]$Disk.BusType -in @('Virtual', 'File Backed Virtual', 'Spaces', 'iSCSI')) { throw 'Refusing virtual, Storage Spaces or remote disks in the physical writer.' }
foreach ($part in @($Partitions)) {
if ($part.IsSystem -or $part.IsBoot) { throw 'A partition on the selected disk is marked as boot or system.' }
if ($part.DriveLetter -and ([string]$part.DriveLetter + ':') -ieq $env:SystemDrive) { throw 'The selected disk contains the running Windows volume.' }
foreach ($access in @($part.AccessPaths)) {
if ($access -and $access.TrimEnd('\') -ieq $env:SystemDrive) { throw 'The selected disk contains the running Windows volume.' }
}
}
}
function Get-XfeCrc32([byte[]]$Bytes) {
[uint32]$crc = [uint32]::MaxValue
foreach ($b in $Bytes) {
$crc = $crc -bxor [uint32]$b
for ($i = 0; $i -lt 8; $i++) {
if ($crc -band 1) { $crc = [uint32](($crc -shr 1) -bxor [uint32]3988292384) }
else { $crc = $crc -shr 1 }
}
}
return $crc -bxor [uint32]::MaxValue
}
function Read-XfeBytes($Stream, [uint64]$Offset, [int]$Count) {
[void]$Stream.Seek([int64]$Offset, [IO.SeekOrigin]::Begin)
$data = New-Object byte[] $Count
$done = 0
while ($done -lt $Count) { $n = $Stream.Read($data, $done, $Count - $done); if ($n -eq 0) { throw 'Unexpected end of disk or image.' }; $done += $n }
return ,$data
}
function Write-XfeBytes($Stream, [uint64]$Offset, [byte[]]$Bytes) {
[void]$Stream.Seek([int64]$Offset, [IO.SeekOrigin]::Begin)
$Stream.Write($Bytes, 0, $Bytes.Length)
}
function Set-XfeU64([byte[]]$Bytes, [int]$Offset, [uint64]$Value) { [BitConverter]::GetBytes($Value).CopyTo($Bytes, $Offset) }
function Set-XfeU32([byte[]]$Bytes, [int]$Offset, [uint32]$Value) { [BitConverter]::GetBytes($Value).CopyTo($Bytes, $Offset) }
function Set-XfeHeaderCrc([byte[]]$Header) {
Set-XfeU32 $Header 16 0
Set-XfeU32 $Header 16 (Get-XfeCrc32 ([byte[]]$Header[0..91]))
}
function Assert-XfeEqual([byte[]]$Expected, [byte[]]$Actual) {
if ($Expected.Length -ne $Actual.Length) { throw 'Written disk verification length mismatch.' }
for ($i=0; $i -lt $Expected.Length; $i++) { if ($Expected[$i] -ne $Actual[$i]) { throw "Written disk verification mismatch at byte $i." } }
}
function Assert-XfeImage([string]$Path) {
$stream=[IO.File]::Open($Path,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::Read)
try {
if($stream.Length -ne 256MB){throw 'Only generated 256 MiB candidate images are accepted.'}
[uint64]$sectors=$stream.Length/512
[uint64]$last=$sectors-1
$mbr=Read-XfeBytes $stream 0 512
if($mbr[450] -ne 238 -or $mbr[510] -ne 85 -or $mbr[511] -ne 170 -or [BitConverter]::ToUInt32($mbr,454) -ne 1 -or [BitConverter]::ToUInt32($mbr,458) -ne $last){throw 'Invalid protective MBR.'}
if(@($mbr[462..509] | Where-Object {$_ -ne 0}).Count -ne 0){throw 'Hybrid MBR images are refused.'}
$primary=Read-XfeBytes $stream 512 512
$backup=Read-XfeBytes $stream ($last*512) 512
foreach($pair in @(@($primary,[uint64]1,$last,[uint64]2),@($backup,$last,[uint64]1,($last-32)))){
[byte[]]$h=$pair[0]
if([Text.Encoding]::ASCII.GetString($h,0,8) -cne 'EFI PART' -or [BitConverter]::ToUInt32($h,8) -ne 65536 -or [BitConverter]::ToUInt32($h,12) -ne 92 -or [BitConverter]::ToUInt32($h,20) -ne 0 -or [BitConverter]::ToUInt64($h,24) -ne $pair[1] -or [BitConverter]::ToUInt64($h,32) -ne $pair[2] -or [BitConverter]::ToUInt64($h,40) -ne 34 -or [BitConverter]::ToUInt64($h,48) -ne ($last-33) -or [BitConverter]::ToUInt64($h,72) -ne $pair[3] -or [BitConverter]::ToUInt32($h,80) -ne 128 -or [BitConverter]::ToUInt32($h,84) -ne 128){throw 'Unsupported GPT header or geometry.'}
[uint32]$crc=[BitConverter]::ToUInt32($h,16)
[byte[]]$header=$h[0..91]
Set-XfeU32 $header 16 0
if((Get-XfeCrc32 $header) -ne $crc){throw 'GPT header CRC mismatch.'}
}
Assert-XfeEqual ([byte[]]$primary[56..71]) ([byte[]]$backup[56..71])
Assert-XfeEqual ([byte[]]$primary[88..91]) ([byte[]]$backup[88..91])
$table=Read-XfeBytes $stream 1024 16384
Assert-XfeEqual $table (Read-XfeBytes $stream (($last-32)*512) 16384)
if((Get-XfeCrc32 $table) -ne [BitConverter]::ToUInt32($primary,88)){throw 'GPT table CRC mismatch.'}
$kinds=@('c12a7328-f81f-11d2-ba4b-00a0c93ec93b','58464544-4154-4100-8000-584645535953','21686148-6449-6e6f-744e-656564454649')
$starts=@([uint64]2048,[uint64]264192,[uint64]329728)
$counts=@([uint64]262144,[uint64]65536,[uint64]2048)
$identities=@()
for($i=0;$i -lt 3;$i++){
$at=$i*128
Assert-XfeEqual ([Guid]$kinds[$i]).ToByteArray() ([byte[]]$table[$at..($at+15)])
$id=([BitConverter]::ToString($table,$at+16,16))
if($id -eq '00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00' -or $identities -contains $id){throw 'Invalid or duplicate GPT partition identity.'}
$identities+=$id
if([BitConverter]::ToUInt64($table,$at+32) -ne $starts[$i] -or [BitConverter]::ToUInt64($table,$at+40) -ne ($starts[$i]+$counts[$i]-1) -or [BitConverter]::ToUInt64($table,$at+48) -ne 0){throw 'GPT partition bounds or attributes mismatch.'}
}
if(@($table[384..16383] | Where-Object {$_ -ne 0}).Count -ne 0){throw 'Unexpected additional GPT partition.'}
if([Text.Encoding]::Unicode.GetString($table,184,72).TrimEnd([char]0) -cne 'XFEDATA'){throw 'XFEDATA label mismatch.'}
$sb=Read-XfeBytes $stream (264192*512+1024) 1024
if([BitConverter]::ToUInt16($sb,56) -ne 61267 -or [BitConverter]::ToUInt32($sb,24) -ne 2 -or [BitConverter]::ToUInt16($sb,88) -ne 128 -or [BitConverter]::ToUInt32($sb,96) -ne 2){throw 'Unsupported XFEDATA ext2 format.'}
} finally {$stream.Dispose()}
}
if ($LoadFunctionsOnly) { return }
if (-not $Image -or $DiskNumber -lt 0) { throw 'Specify -Image FILE.img and -DiskNumber N. Without -Write this only produces a review plan.' }
if ($env:OS -ne 'Windows_NT') { throw 'This script requires Windows Storage cmdlets.' }
$imagePath = (Resolve-Path -LiteralPath $Image).ProviderPath
$imageInfo = Get-Item -LiteralPath $imagePath
if ($imageInfo.PSIsContainer -or $imageInfo.Length -ne 256MB) { throw 'Expected a generated 256 MiB physical-image file.' }
$manifestPath = $imagePath + '.json'
$manifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
$imageHash = (Get-FileHash -LiteralPath $imagePath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($manifest.schema -ne 1 -or $manifest.architecture -notin @('x86_64','i686') -or $manifest.image_bytes -ne $imageInfo.Length -or $manifest.image_sha256 -cne $imageHash -or $manifest.data_type_guid -ne '58464544-4154-4100-8000-584645535953') { throw 'Image manifest or SHA256 mismatch; regenerate using physical-image.' }
Assert-XfeImage $imagePath
$disk = Get-Disk -Number $DiskNumber -ErrorAction Stop
$parts = @(Get-Partition -DiskNumber $DiskNumber -ErrorAction Stop)
Assert-XfeDisk $disk $parts $imageInfo.Length
$identity = '{0}|{1}|{2}|{3}|{4}|{5}|{6}' -f $disk.Number, $disk.UniqueId, ([string]$disk.SerialNumber).Trim(), $disk.Size, $disk.BusType, $disk.LogicalSectorSize, $imageHash
$token = Get-XfeToken $identity
$plan = [ordered]@{
schema = 1
operation = 'ERASE partition table and replace disk contents with XFESystem candidate'
dry_run = -not [bool]$Write
disk_number = $disk.Number
disk_unique_id = [string]$disk.UniqueId
disk_serial = ([string]$disk.SerialNumber).Trim()
disk_friendly_name = [string]$disk.FriendlyName
disk_bus = [string]$disk.BusType
disk_bytes = [uint64]$disk.Size
logical_sector_bytes = $disk.LogicalSectorSize
existing_partitions = @($parts | Select-Object PartitionNumber, DriveLetter, Type, Size)
image = $imagePath
image_bytes = $imageInfo.Length
image_sha256 = $imageHash
gpt_backup_relocated_to_target_end = $true
final_disk_state = 'offline'
confirm_token = $token
}
$planJson = $plan | ConvertTo-Json -Depth 6
if ($PlanPath) {
$planFile = [IO.File]::Open([IO.Path]::GetFullPath($PlanPath), [IO.FileMode]::CreateNew, [IO.FileAccess]::Write)
try { $b = [Text.Encoding]::UTF8.GetBytes($planJson); $planFile.Write($b, 0, $b.Length) }
finally { $planFile.Dispose() }
}
$planJson
if (-not $Write) {
Write-Host 'DRY RUN ONLY: no disk was opened for writing or changed. Review disk number, identity, capacity and ALL existing partitions.'
Write-Host 'Only after the human explicitly authorizes erasing this exact disk, rerun with -Write -ConfirmToken <full token>.'
return
}
if ([string]::IsNullOrWhiteSpace($ConfirmToken) -or $ConfirmToken -cne $token) { throw 'The full confirmation token from the disk-specific plan is required. No write was performed.' }
$principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Writing a physical disk requires an elevated PowerShell window after explicit disk approval.' }
# Check again immediately before the mutation. An old plan cannot select a disk
# whose number was recycled after hotplug or whose size/identity changed.
$freshDisk = Get-Disk -Number $DiskNumber -ErrorAction Stop
$freshParts = @(Get-Partition -DiskNumber $DiskNumber -ErrorAction Stop)
Assert-XfeDisk $freshDisk $freshParts $imageInfo.Length
$freshIdentity = '{0}|{1}|{2}|{3}|{4}|{5}|{6}' -f $freshDisk.Number, $freshDisk.UniqueId, ([string]$freshDisk.SerialNumber).Trim(), $freshDisk.Size, $freshDisk.BusType, $freshDisk.LogicalSectorSize, $imageHash
if ($freshIdentity -cne $identity) { throw 'Target identity changed since the plan; refusing the write.' }
$rawPath = '\\.\PhysicalDrive' + $DiskNumber
$target = $null
$source = $null
try {
$source = Open-XfeLockedImage $imagePath $imageHash
# Recheck the target after hashing while the source is locked. No disk state
# changes until both exact identities and the locked source hash are valid.
$lockedDisk=Get-Disk -Number $DiskNumber -ErrorAction Stop
$lockedParts=@(Get-Partition -DiskNumber $DiskNumber -ErrorAction Stop)
Assert-XfeDisk $lockedDisk $lockedParts $imageInfo.Length
$lockedIdentity='{0}|{1}|{2}|{3}|{4}|{5}|{6}' -f $lockedDisk.Number,$lockedDisk.UniqueId,([string]$lockedDisk.SerialNumber).Trim(),$lockedDisk.Size,$lockedDisk.BusType,$lockedDisk.LogicalSectorSize,$imageHash
if($lockedIdentity -cne $identity){throw 'Target identity changed while locking the source; refusing before any target mutation.'}
Set-Disk -Number $DiskNumber -IsOffline $true -ErrorAction Stop
# A physical drive does not provide regular-file EOF semantics. The default
# 4096-byte FileStream buffer can read past the device when verifying its
# final 512-byte GPT sector. All raw requests are sector-aligned; keep the
# managed buffer at one sector so those reads bypass speculative buffering.
$target = [IO.FileStream]::new($rawPath, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite, [IO.FileShare]::ReadWrite, 512, [IO.FileOptions]::None)
$buffer = New-Object byte[] 1MB
[uint64]$copied = 0
while (($n = $source.Read($buffer, 0, $buffer.Length)) -gt 0) { $target.Write($buffer, 0, $n); $copied += $n }
if ($copied -ne $imageInfo.Length) { throw 'Incomplete source image read.' }
$target.Flush($true)
# Verify the complete copy before changing the GPT end-of-disk coordinates.
[void]$target.Seek(0, [IO.SeekOrigin]::Begin)
$hash = [Security.Cryptography.SHA256]::Create()
try {
[uint64]$remaining = $copied
while ($remaining -gt 0) { $want = [int][Math]::Min([uint64]$buffer.Length, $remaining); $got = $target.Read($buffer, 0, $want); if ($got -le 0) { throw 'Read-back verification reached end of disk.' }; [void]$hash.TransformBlock($buffer, 0, $got, $buffer, 0); $remaining -= $got }
[void]$hash.TransformFinalBlock([byte[]]@(), 0, 0)
$readHash = ([BitConverter]::ToString($hash.Hash)).Replace('-', '').ToLowerInvariant()
if ($readHash -cne $imageHash) { throw 'Full image read-back SHA256 mismatch.' }
} finally { $hash.Dispose() }
[uint64]$sectors = [uint64]$freshDisk.Size / 512
[uint64]$last = $sectors - 1
$primary = Read-XfeBytes $source 512 512
$backup = Read-XfeBytes $source ($imageInfo.Length - 512) 512
$table = Read-XfeBytes $source 1024 16384
Set-XfeU64 $primary 32 $last
Set-XfeU64 $primary 48 ($last - 33)
Set-XfeHeaderCrc $primary
Set-XfeU64 $backup 24 $last
Set-XfeU64 $backup 48 ($last - 33)
Set-XfeU64 $backup 72 ($last - 32)
Set-XfeHeaderCrc $backup
$mbr = Read-XfeBytes $source 0 512
Set-XfeU32 $mbr 458 ([uint32][Math]::Min($last, [uint64][uint32]::MaxValue))
Write-XfeBytes $target 0 $mbr
Write-XfeBytes $target 512 $primary
Write-XfeBytes $target (($last - 32) * 512) $table
Write-XfeBytes $target ($last * 512) $backup
$target.Flush($true)
Assert-XfeEqual $mbr (Read-XfeBytes $target 0 512)
Assert-XfeEqual $primary (Read-XfeBytes $target 512 512)
Assert-XfeEqual $table (Read-XfeBytes $target (($last - 32) * 512) 16384)
Assert-XfeEqual $backup (Read-XfeBytes $target ($last * 512) 512)
Write-Host "Written and read-back verified disk $DiskNumber. GPT backup relocated to the physical disk end. Disk remains OFFLINE; disconnect the USB safely or reboot to UEFI."
} finally {
if ($source) { $source.Dispose() }
if ($target) { $target.Dispose() }
}
# Windows host disk writer. The default only inspects and prints a disk-specific
# plan. Never invoke -Write until a human has selected and authorized that disk.
[CmdletBinding()]
param(
[string]$Image,
[int]$DiskNumber = -1,
[string]$PlanPath,
[switch]$Write,
[string]$ConfirmToken,
[switch]$LoadFunctionsOnly
)
$ErrorActionPreference = 'Stop'
function Get-XfeToken([string]$Value) {
$hash = [Security.Cryptography.SHA256]::Create()
try { return ([BitConverter]::ToString($hash.ComputeHash([Text.Encoding]::UTF8.GetBytes($Value)))).Replace('-', '').ToLowerInvariant() }
finally { $hash.Dispose() }
}
function Open-XfeLockedImage([string]$Path,[string]$ExpectedHash) {
# Sharing read access only prevents concurrent writes, replacement or delete.
# Hash and the later disk copy use this very same handle, held until finally.
$stream=[IO.File]::Open($Path,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::Read)
$hash=[Security.Cryptography.SHA256]::Create()
try {
$actual=([BitConverter]::ToString($hash.ComputeHash($stream))).Replace('-','').ToLowerInvariant()
if($actual -cne $ExpectedHash){throw 'Image changed since the plan; refusing before any target mutation.'}
[void]$stream.Seek(0,[IO.SeekOrigin]::Begin)
return $stream
} catch {$stream.Dispose();throw} finally {$hash.Dispose()}
}
function Assert-XfeDisk($Disk, $Partitions, [uint64]$ImageBytes) {
if ($null -eq $Disk -or $Disk.Number -lt 0) { throw 'A concrete target disk is required.' }
if ($Disk.IsSystem -or $Disk.IsBoot) { throw 'Refusing the Windows system or boot disk.' }
if ([string]::IsNullOrWhiteSpace([string]$Disk.UniqueId)) { throw 'Disk UniqueId is unavailable; refusing ambiguous identity.' }
if ([uint64]$Disk.Size -lt $ImageBytes) { throw 'The selected disk is smaller than the image.' }
if ([uint64]$Disk.LogicalSectorSize -ne 512) { throw 'Only 512-byte logical sectors are supported by this candidate.' }
if ($Disk.IsReadOnly) { throw 'The selected disk is read-only.' }
if ([string]$Disk.BusType -in @('Virtual', 'File Backed Virtual', 'Spaces', 'iSCSI')) { throw 'Refusing virtual, Storage Spaces or remote disks in the physical writer.' }
foreach ($part in @($Partitions)) {
if ($part.IsSystem -or $part.IsBoot) { throw 'A partition on the selected disk is marked as boot or system.' }
if ($part.DriveLetter -and ([string]$part.DriveLetter + ':') -ieq $env:SystemDrive) { throw 'The selected disk contains the running Windows volume.' }
foreach ($access in @($part.AccessPaths)) {
if ($access -and $access.TrimEnd('\') -ieq $env:SystemDrive) { throw 'The selected disk contains the running Windows volume.' }
}
}
}
function Get-XfeCrc32([byte[]]$Bytes) {
[uint32]$crc = [uint32]::MaxValue
foreach ($b in $Bytes) {
$crc = $crc -bxor [uint32]$b
for ($i = 0; $i -lt 8; $i++) {
if ($crc -band 1) { $crc = [uint32](($crc -shr 1) -bxor [uint32]3988292384) }
else { $crc = $crc -shr 1 }
}
}
return $crc -bxor [uint32]::MaxValue
}
function Read-XfeBytes($Stream, [uint64]$Offset, [int]$Count) {
[void]$Stream.Seek([int64]$Offset, [IO.SeekOrigin]::Begin)
$data = New-Object byte[] $Count
$done = 0
while ($done -lt $Count) { $n = $Stream.Read($data, $done, $Count - $done); if ($n -eq 0) { throw 'Unexpected end of disk or image.' }; $done += $n }
return ,$data
}
function Write-XfeBytes($Stream, [uint64]$Offset, [byte[]]$Bytes) {
[void]$Stream.Seek([int64]$Offset, [IO.SeekOrigin]::Begin)
$Stream.Write($Bytes, 0, $Bytes.Length)
}
function Set-XfeU64([byte[]]$Bytes, [int]$Offset, [uint64]$Value) { [BitConverter]::GetBytes($Value).CopyTo($Bytes, $Offset) }
function Set-XfeU32([byte[]]$Bytes, [int]$Offset, [uint32]$Value) { [BitConverter]::GetBytes($Value).CopyTo($Bytes, $Offset) }
function Set-XfeHeaderCrc([byte[]]$Header) {
Set-XfeU32 $Header 16 0
Set-XfeU32 $Header 16 (Get-XfeCrc32 ([byte[]]$Header[0..91]))
}
function Assert-XfeEqual([byte[]]$Expected, [byte[]]$Actual) {
if ($Expected.Length -ne $Actual.Length) { throw 'Written disk verification length mismatch.' }
for ($i=0; $i -lt $Expected.Length; $i++) { if ($Expected[$i] -ne $Actual[$i]) { throw "Written disk verification mismatch at byte $i." } }
}
function Assert-XfeImage([string]$Path) {
$stream=[IO.File]::Open($Path,[IO.FileMode]::Open,[IO.FileAccess]::Read,[IO.FileShare]::Read)
try {
if($stream.Length -ne 256MB){throw 'Only generated 256 MiB candidate images are accepted.'}
[uint64]$sectors=$stream.Length/512
[uint64]$last=$sectors-1
$mbr=Read-XfeBytes $stream 0 512
if($mbr[450] -ne 238 -or $mbr[510] -ne 85 -or $mbr[511] -ne 170 -or [BitConverter]::ToUInt32($mbr,454) -ne 1 -or [BitConverter]::ToUInt32($mbr,458) -ne $last){throw 'Invalid protective MBR.'}
if(@($mbr[462..509] | Where-Object {$_ -ne 0}).Count -ne 0){throw 'Hybrid MBR images are refused.'}
$primary=Read-XfeBytes $stream 512 512
$backup=Read-XfeBytes $stream ($last*512) 512
foreach($pair in @(@($primary,[uint64]1,$last,[uint64]2),@($backup,$last,[uint64]1,($last-32)))){
[byte[]]$h=$pair[0]
if([Text.Encoding]::ASCII.GetString($h,0,8) -cne 'EFI PART' -or [BitConverter]::ToUInt32($h,8) -ne 65536 -or [BitConverter]::ToUInt32($h,12) -ne 92 -or [BitConverter]::ToUInt32($h,20) -ne 0 -or [BitConverter]::ToUInt64($h,24) -ne $pair[1] -or [BitConverter]::ToUInt64($h,32) -ne $pair[2] -or [BitConverter]::ToUInt64($h,40) -ne 34 -or [BitConverter]::ToUInt64($h,48) -ne ($last-33) -or [BitConverter]::ToUInt64($h,72) -ne $pair[3] -or [BitConverter]::ToUInt32($h,80) -ne 128 -or [BitConverter]::ToUInt32($h,84) -ne 128){throw 'Unsupported GPT header or geometry.'}
[uint32]$crc=[BitConverter]::ToUInt32($h,16)
[byte[]]$header=$h[0..91]
Set-XfeU32 $header 16 0
if((Get-XfeCrc32 $header) -ne $crc){throw 'GPT header CRC mismatch.'}
}
Assert-XfeEqual ([byte[]]$primary[56..71]) ([byte[]]$backup[56..71])
Assert-XfeEqual ([byte[]]$primary[88..91]) ([byte[]]$backup[88..91])
$table=Read-XfeBytes $stream 1024 16384
Assert-XfeEqual $table (Read-XfeBytes $stream (($last-32)*512) 16384)
if((Get-XfeCrc32 $table) -ne [BitConverter]::ToUInt32($primary,88)){throw 'GPT table CRC mismatch.'}
$kinds=@('c12a7328-f81f-11d2-ba4b-00a0c93ec93b','58464544-4154-4100-8000-584645535953','21686148-6449-6e6f-744e-656564454649')
$starts=@([uint64]2048,[uint64]264192,[uint64]329728)
$counts=@([uint64]262144,[uint64]65536,[uint64]2048)
$identities=@()
for($i=0;$i -lt 3;$i++){
$at=$i*128
Assert-XfeEqual ([Guid]$kinds[$i]).ToByteArray() ([byte[]]$table[$at..($at+15)])
$id=([BitConverter]::ToString($table,$at+16,16))
if($id -eq '00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00' -or $identities -contains $id){throw 'Invalid or duplicate GPT partition identity.'}
$identities+=$id
if([BitConverter]::ToUInt64($table,$at+32) -ne $starts[$i] -or [BitConverter]::ToUInt64($table,$at+40) -ne ($starts[$i]+$counts[$i]-1) -or [BitConverter]::ToUInt64($table,$at+48) -ne 0){throw 'GPT partition bounds or attributes mismatch.'}
}
if(@($table[384..16383] | Where-Object {$_ -ne 0}).Count -ne 0){throw 'Unexpected additional GPT partition.'}
if([Text.Encoding]::Unicode.GetString($table,184,72).TrimEnd([char]0) -cne 'XFEDATA'){throw 'XFEDATA label mismatch.'}
$sb=Read-XfeBytes $stream (264192*512+1024) 1024
if([BitConverter]::ToUInt16($sb,56) -ne 61267 -or [BitConverter]::ToUInt32($sb,24) -ne 2 -or [BitConverter]::ToUInt16($sb,88) -ne 128 -or [BitConverter]::ToUInt32($sb,96) -ne 2){throw 'Unsupported XFEDATA ext2 format.'}
} finally {$stream.Dispose()}
}
if ($LoadFunctionsOnly) { return }
if (-not $Image -or $DiskNumber -lt 0) { throw 'Specify -Image FILE.img and -DiskNumber N. Without -Write this only produces a review plan.' }
if ($env:OS -ne 'Windows_NT') { throw 'This script requires Windows Storage cmdlets.' }
$imagePath = (Resolve-Path -LiteralPath $Image).ProviderPath
$imageInfo = Get-Item -LiteralPath $imagePath
if ($imageInfo.PSIsContainer -or $imageInfo.Length -ne 256MB) { throw 'Expected a generated 256 MiB physical-image file.' }
$manifestPath = $imagePath + '.json'
$manifest = Get-Content -LiteralPath $manifestPath -Raw -Encoding UTF8 | ConvertFrom-Json
$imageHash = (Get-FileHash -LiteralPath $imagePath -Algorithm SHA256).Hash.ToLowerInvariant()
if ($manifest.schema -ne 1 -or $manifest.architecture -notin @('x86_64','i686') -or $manifest.image_bytes -ne $imageInfo.Length -or $manifest.image_sha256 -cne $imageHash -or $manifest.data_type_guid -ne '58464544-4154-4100-8000-584645535953') { throw 'Image manifest or SHA256 mismatch; regenerate using physical-image.' }
Assert-XfeImage $imagePath
$disk = Get-Disk -Number $DiskNumber -ErrorAction Stop
$parts = @(Get-Partition -DiskNumber $DiskNumber -ErrorAction Stop)
Assert-XfeDisk $disk $parts $imageInfo.Length
$identity = '{0}|{1}|{2}|{3}|{4}|{5}|{6}' -f $disk.Number, $disk.UniqueId, ([string]$disk.SerialNumber).Trim(), $disk.Size, $disk.BusType, $disk.LogicalSectorSize, $imageHash
$token = Get-XfeToken $identity
$plan = [ordered]@{
schema = 1
operation = 'ERASE partition table and replace disk contents with XFESystem candidate'
dry_run = -not [bool]$Write
disk_number = $disk.Number
disk_unique_id = [string]$disk.UniqueId
disk_serial = ([string]$disk.SerialNumber).Trim()
disk_friendly_name = [string]$disk.FriendlyName
disk_bus = [string]$disk.BusType
disk_bytes = [uint64]$disk.Size
logical_sector_bytes = $disk.LogicalSectorSize
existing_partitions = @($parts | Select-Object PartitionNumber, DriveLetter, Type, Size)
image = $imagePath
image_bytes = $imageInfo.Length
image_sha256 = $imageHash
gpt_backup_relocated_to_target_end = $true
final_disk_state = 'offline'
confirm_token = $token
}
$planJson = $plan | ConvertTo-Json -Depth 6
if ($PlanPath) {
$planFile = [IO.File]::Open([IO.Path]::GetFullPath($PlanPath), [IO.FileMode]::CreateNew, [IO.FileAccess]::Write)
try { $b = [Text.Encoding]::UTF8.GetBytes($planJson); $planFile.Write($b, 0, $b.Length) }
finally { $planFile.Dispose() }
}
$planJson
if (-not $Write) {
Write-Host 'DRY RUN ONLY: no disk was opened for writing or changed. Review disk number, identity, capacity and ALL existing partitions.'
Write-Host 'Only after the human explicitly authorizes erasing this exact disk, rerun with -Write -ConfirmToken <full token>.'
return
}
if ([string]::IsNullOrWhiteSpace($ConfirmToken) -or $ConfirmToken -cne $token) { throw 'The full confirmation token from the disk-specific plan is required. No write was performed.' }
$principal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
if (-not $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Writing a physical disk requires an elevated PowerShell window after explicit disk approval.' }
# Check again immediately before the mutation. An old plan cannot select a disk
# whose number was recycled after hotplug or whose size/identity changed.
$freshDisk = Get-Disk -Number $DiskNumber -ErrorAction Stop
$freshParts = @(Get-Partition -DiskNumber $DiskNumber -ErrorAction Stop)
Assert-XfeDisk $freshDisk $freshParts $imageInfo.Length
$freshIdentity = '{0}|{1}|{2}|{3}|{4}|{5}|{6}' -f $freshDisk.Number, $freshDisk.UniqueId, ([string]$freshDisk.SerialNumber).Trim(), $freshDisk.Size, $freshDisk.BusType, $freshDisk.LogicalSectorSize, $imageHash
if ($freshIdentity -cne $identity) { throw 'Target identity changed since the plan; refusing the write.' }
$rawPath = '\\.\PhysicalDrive' + $DiskNumber
$target = $null
$source = $null
try {
$source = Open-XfeLockedImage $imagePath $imageHash
# Recheck the target after hashing while the source is locked. No disk state
# changes until both exact identities and the locked source hash are valid.
$lockedDisk=Get-Disk -Number $DiskNumber -ErrorAction Stop
$lockedParts=@(Get-Partition -DiskNumber $DiskNumber -ErrorAction Stop)
Assert-XfeDisk $lockedDisk $lockedParts $imageInfo.Length
$lockedIdentity='{0}|{1}|{2}|{3}|{4}|{5}|{6}' -f $lockedDisk.Number,$lockedDisk.UniqueId,([string]$lockedDisk.SerialNumber).Trim(),$lockedDisk.Size,$lockedDisk.BusType,$lockedDisk.LogicalSectorSize,$imageHash
if($lockedIdentity -cne $identity){throw 'Target identity changed while locking the source; refusing before any target mutation.'}
Set-Disk -Number $DiskNumber -IsOffline $true -ErrorAction Stop
# A physical drive does not provide regular-file EOF semantics. The default
# 4096-byte FileStream buffer can read past the device when verifying its
# final 512-byte GPT sector. All raw requests are sector-aligned; keep the
# managed buffer at one sector so those reads bypass speculative buffering.
$target = [IO.FileStream]::new($rawPath, [IO.FileMode]::Open, [IO.FileAccess]::ReadWrite, [IO.FileShare]::ReadWrite, 512, [IO.FileOptions]::None)
$buffer = New-Object byte[] 1MB
[uint64]$copied = 0
while (($n = $source.Read($buffer, 0, $buffer.Length)) -gt 0) { $target.Write($buffer, 0, $n); $copied += $n }
if ($copied -ne $imageInfo.Length) { throw 'Incomplete source image read.' }
$target.Flush($true)
# Verify the complete copy before changing the GPT end-of-disk coordinates.
[void]$target.Seek(0, [IO.SeekOrigin]::Begin)
$hash = [Security.Cryptography.SHA256]::Create()
try {
[uint64]$remaining = $copied
while ($remaining -gt 0) { $want = [int][Math]::Min([uint64]$buffer.Length, $remaining); $got = $target.Read($buffer, 0, $want); if ($got -le 0) { throw 'Read-back verification reached end of disk.' }; [void]$hash.TransformBlock($buffer, 0, $got, $buffer, 0); $remaining -= $got }
[void]$hash.TransformFinalBlock([byte[]]@(), 0, 0)
$readHash = ([BitConverter]::ToString($hash.Hash)).Replace('-', '').ToLowerInvariant()
if ($readHash -cne $imageHash) { throw 'Full image read-back SHA256 mismatch.' }
} finally { $hash.Dispose() }
[uint64]$sectors = [uint64]$freshDisk.Size / 512
[uint64]$last = $sectors - 1
$primary = Read-XfeBytes $source 512 512
$backup = Read-XfeBytes $source ($imageInfo.Length - 512) 512
$table = Read-XfeBytes $source 1024 16384
Set-XfeU64 $primary 32 $last
Set-XfeU64 $primary 48 ($last - 33)
Set-XfeHeaderCrc $primary
Set-XfeU64 $backup 24 $last
Set-XfeU64 $backup 48 ($last - 33)
Set-XfeU64 $backup 72 ($last - 32)
Set-XfeHeaderCrc $backup
$mbr = Read-XfeBytes $source 0 512
Set-XfeU32 $mbr 458 ([uint32][Math]::Min($last, [uint64][uint32]::MaxValue))
Write-XfeBytes $target 0 $mbr
Write-XfeBytes $target 512 $primary
Write-XfeBytes $target (($last - 32) * 512) $table
Write-XfeBytes $target ($last * 512) $backup
$target.Flush($true)
Assert-XfeEqual $mbr (Read-XfeBytes $target 0 512)
Assert-XfeEqual $primary (Read-XfeBytes $target 512 512)
Assert-XfeEqual $table (Read-XfeBytes $target (($last - 32) * 512) 16384)
Assert-XfeEqual $backup (Read-XfeBytes $target ($last * 512) 512)
Write-Host "Written and read-back verified disk $DiskNumber. GPT backup relocated to the physical disk end. Disk remains OFFLINE; disconnect the USB safely or reboot to UEFI."
} finally {
if ($source) { $source.Dispose() }
if ($target) { $target.Dispose() }
}