Public/Invoke-SrrReconstruct.ps1
|
function Invoke-SrrReconstruct { <# .SYNOPSIS Reconstruct RAR archive volumes from an SRR file and source files. .DESCRIPTION Reads SRR metadata and rebuilds the original RAR archive files by: 1. Parsing SRR for block structure 2. Writing RAR headers from SRR metadata 3. Copying file data from source files .PARAMETER SrrFile Path to the SRR file. .PARAMETER SourcePath Directory containing source files. .PARAMETER OutputPath Directory for output RAR files. .PARAMETER SkipValidation Skip source file size validation. .PARAMETER ExtractStoredFiles Also extract stored files (NFO, SFV, etc.) to output directory. .EXAMPLE Invoke-SrrReconstruct -SrrFile "release.srr" -SourcePath "." -OutputPath "./output" Reconstructs RAR archives from the SRR file using source files in the current directory. #> [CmdletBinding()] param( [Parameter(Mandatory)] [string]$SrrFile, [Parameter(Mandatory)] [string]$SourcePath, [Parameter(Mandatory)] [string]$OutputPath, [Parameter()] [switch]$SkipValidation, [Parameter()] [switch]$ExtractStoredFiles ) $SrrFile = (Resolve-Path -Path $SrrFile -ErrorAction Stop).Path $SourcePath = (Resolve-Path -Path $SourcePath -ErrorAction Stop).Path $OutputPath = (Resolve-Path -Path $OutputPath -ErrorAction Stop).Path Write-Host "Starting SRR reconstruction..." -ForegroundColor Cyan Write-Host " SRR file: $SrrFile" Write-Host " Source: $SourcePath" Write-Host " Output: $OutputPath" Write-Host "" $reader = [BlockReader]::new($SrrFile) $blocks = $reader.ReadAllBlocks() Write-Host "Parsed $($blocks.Count) blocks from SRR file" -ForegroundColor Green if ($ExtractStoredFiles) { $storedBlocks = $blocks | Where-Object { $_ -is [SrrStoredFileBlock] } if ($storedBlocks.Count -gt 0) { Write-Host "Extracting stored files..." -ForegroundColor Cyan $fs = [System.IO.File]::OpenRead($SrrFile) try { $br = [System.IO.BinaryReader]::new($fs) $currentPos = 0 foreach ($block in $blocks) { $blockSize = $block.HeadSize + $block.AddSize if ($block -is [SrrStoredFileBlock]) { $relativePath = $block.FileName.TrimStart([char]92, [char]47) $targetPath = Join-Path $OutputPath $relativePath $targetDir = Split-Path $targetPath -Parent if ($targetDir -and -not (Test-Path $targetDir)) { [System.IO.Directory]::CreateDirectory($targetDir) | Out-Null } $dataStart = $currentPos + $block.HeadSize $fs.Seek($dataStart, [System.IO.SeekOrigin]::Begin) | Out-Null $fileData = $br.ReadBytes([int]$block.FileSize) [System.IO.File]::WriteAllBytes($targetPath, $fileData) Write-Host " Extracted stored file: $($block.FileName) ($($block.FileSize) bytes)" -ForegroundColor Gray } $currentPos += $blockSize } } finally { $br.Dispose() $fs.Close() } } } $rarVolumes = @{} $currentVolume = $null foreach ($block in $blocks) { if ($block -is [SrrRarFileBlock]) { $currentVolume = $block.FileName $rarVolumes[$currentVolume] = @{ RarFileBlock = $block; Blocks = [System.Collections.Generic.List[Object]]::new() } } elseif ($currentVolume -and ($block -is [RarMarkerBlock] -or $block -is [RarVolumeHeaderBlock] -or $block -is [RarPackedFileBlock] -or $block -is [RarEndArchiveBlock] -or $block -is [RarNewSubBlock] -or $block -is [RarOldStyleBlock])) { $rarVolumes[$currentVolume].Blocks.Add($block) } } Write-Host "Found $($rarVolumes.Count) RAR volumes to reconstruct" -ForegroundColor Green if (-not (Test-Path $OutputPath)) { New-Item -Path $OutputPath -ItemType Directory | Out-Null } $sourceFile = $null $sourceFileHandle = $null $sourceFileOffset = [long]0 try { $sortedVolumes = $rarVolumes.Keys | Sort-Object { if ($_ -match '\.rar$') { 0 } elseif ($_ -match '\.r(\d+)$') { [int]$matches[1] + 1 } else { 999 } } foreach ($volumeName in $sortedVolumes) { $volumeData = $rarVolumes[$volumeName] $outputFile = Join-Path $OutputPath $volumeName # Ensure parent directory exists (for multi-disc releases like CD1/, CD2/) $outputDir = Split-Path $outputFile -Parent if ($outputDir -and -not (Test-Path $outputDir)) { [System.IO.Directory]::CreateDirectory($outputDir) | Out-Null } Write-Host "Reconstructing: $volumeName" -ForegroundColor Yellow $rarStream = [System.IO.FileStream]::new($outputFile, [System.IO.FileMode]::Create) try { foreach ($block in $volumeData.Blocks) { if ($block -is [RarMarkerBlock]) { $markerBytes = [byte[]]@(0x52, 0x61, 0x72, 0x21, 0x1A, 0x07, 0x00) $rarStream.Write($markerBytes, 0, $markerBytes.Length) } elseif ($block -is [RarVolumeHeaderBlock]) { $blockBytes = $block.GetBlockBytes() $rarStream.Write($blockBytes, 0, $blockBytes.Length) } elseif ($block -is [RarPackedFileBlock]) { if ($sourceFile -ne $block.FileName) { if ($sourceFileHandle) { $sourceFileHandle.Close(); $sourceFileHandle = $null } $sourceFile = $block.FileName $sourceFileOffset = 0 $sourcePath = Find-SourceFile -FileName $block.FileName -SearchPath $SourcePath -ExpectedSize $block.FullUnpackedSize if (-not $sourcePath) { throw "Source file not found: $($block.FileName)" } Write-Host " Using source: $sourcePath" -ForegroundColor Gray $sourceFileHandle = [System.IO.File]::OpenRead($sourcePath) if (-not $SkipValidation) { $fileInfo = Get-Item $sourcePath if ($fileInfo.Length -ne $block.FullUnpackedSize) { throw "Source file size mismatch: Expected $($block.FullUnpackedSize) bytes, found $($fileInfo.Length) bytes" } } } $blockBytes = $block.GetBlockBytes() $rarStream.Write($blockBytes, 0, $blockBytes.Length) if ($block.FullPackedSize -gt 0) { $sourceFileHandle.Seek($sourceFileOffset, [System.IO.SeekOrigin]::Begin) | Out-Null $buffer = New-Object byte[] 65536 $remaining = [long]$block.FullPackedSize while ($remaining -gt 0) { $toRead = [Math]::Min($remaining, $buffer.Length) $bytesRead = $sourceFileHandle.Read($buffer, 0, $toRead) if ($bytesRead -eq 0) { break } $rarStream.Write($buffer, 0, $bytesRead) $remaining -= $bytesRead $sourceFileOffset += $bytesRead } } } elseif ($block -is [RarNewSubBlock]) { # Write new-style subblock (recovery record, comments, etc.) $blockBytes = $block.GetBlockBytes() $rarStream.Write($blockBytes, 0, $blockBytes.Length) # Note: Subblock data (if any) is not stored in SRR files } elseif ($block -is [RarOldStyleBlock]) { # Write old-style block as-is $blockBytes = $block.GetBlockBytes() $rarStream.Write($blockBytes, 0, $blockBytes.Length) } elseif ($block -is [RarEndArchiveBlock]) { $blockBytes = $block.GetBlockBytes() $rarStream.Write($blockBytes, 0, $blockBytes.Length) } } Write-Host " Created: $outputFile ($($rarStream.Length) bytes)" -ForegroundColor Green } finally { $rarStream.Close() } } } finally { if ($sourceFileHandle) { $sourceFileHandle.Close() } } Write-Host "" Write-Host "Reconstruction complete!" -ForegroundColor Green if (-not $SkipValidation) { Test-ReconstructedRar -SrrFile $SrrFile -OutputPath $OutputPath } } |