Public/Invoke-OffsetInspect.ps1
|
function Invoke-OffsetInspect { <# .SYNOPSIS Maps one or more byte offsets to bounded binary and source context. .DESCRIPTION Opens each unique input file once, scans it once for the requested source-line context, and reads only the byte windows needed for each result. The command supports human-readable, object, JSON, CSV, and CSV-file output. .PARAMETER FilePaths One or more files to inspect. .PARAMETER OffsetInputs Decimal or hexadecimal offsets. Numeric-only values are decimal. Hexadecimal values may use 0x, an h suffix, or contain A-F. .PARAMETER PassThru Returns structured OffsetInspect.Result objects. .EXAMPLE Invoke-OffsetInspect .\sample.bin 0x2A -PassThru .EXAMPLE Invoke-OffsetInspect .\script.ps1 128,256 -ContextLines 4 -Json #> [CmdletBinding(DefaultParameterSetName = 'Human')] param( [Parameter(Mandatory = $true, Position = 0)] [ValidateNotNullOrEmpty()] [Alias('FilePath')] [string[]]$FilePaths, [Parameter(Mandatory = $true, Position = 1)] [ValidateNotNullOrEmpty()] [Alias('OffsetInput', 'Offsets')] [string[]]$OffsetInputs, [ValidateRange(0, 4096)] [int]$ByteWindow = 32, [ValidateRange(0, 100)] [int]$ContextLines = 3, [ValidateSet('Auto', 'Default', 'UTF8', 'UTF16LE', 'UTF16BE', 'ASCII')] [string]$Encoding = 'Auto', [string]$CompareFile, [ValidateRange(1024, 16777216)] [int]$MaxLineBytes = 1048576, [Parameter(Mandatory = $true, ParameterSetName = 'Object')] [switch]$PassThru, [Parameter(Mandatory = $true, ParameterSetName = 'Json')] [switch]$Json, [Parameter(Mandatory = $true, ParameterSetName = 'Csv')] [switch]$Csv, [Parameter(Mandatory = $true, ParameterSetName = 'CsvFile')] [ValidateNotNullOrEmpty()] [string]$CsvPath, [switch]$FailOnError ) $mode = if ($PassThru) { 'Object' } elseif ($Json) { 'Json' } elseif ($Csv) { 'Csv' } elseif ($PSCmdlet.ParameterSetName -eq 'CsvFile') { 'CsvFile' } else { 'Human' } $plan = @(New-OIInspectionPlan -Files $FilePaths -Offsets $OffsetInputs) $results = New-Object object[] $plan.Count $compareContext = $null $compareError = $null if ($CompareFile) { try { $compareContext = Open-OIFileContext -Path $CompareFile -EncodingName 'Auto' } catch { $compareError = "Unable to open compare file '$CompareFile': $($_.Exception.Message)" } } try { foreach ($group in ($plan | Group-Object -Property FilePath)) { $context = $null try { $context = Open-OIFileContext -Path $group.Name -EncodingName $Encoding } catch { foreach ($item in $group.Group) { $results[$item.Index] = New-OIErrorResult ` -File $item.FilePath ` -OffsetInput $item.OffsetInput ` -Offset $null ` -FileSize $null ` -EncodingRequested $Encoding ` -CompareFile $CompareFile ` -Message "Unable to open file '$($item.FilePath)': $($_.Exception.Message)" } continue } try { if ($context.Length -eq 0) { foreach ($item in $group.Group) { $results[$item.Index] = New-OIErrorResult ` -File $context.Path ` -OffsetInput $item.OffsetInput ` -Offset $null ` -FileSize $context.Length ` -EncodingRequested $Encoding ` -CompareFile $CompareFile ` -Message "File is empty: $($context.Path)" } continue } $validEntries = New-Object 'System.Collections.Generic.List[object]' $validOffsets = New-Object 'System.Collections.Generic.List[System.Int64]' foreach ($item in $group.Group) { try { $offset = ConvertTo-OIOffset -InputValue $item.OffsetInput } catch { $results[$item.Index] = New-OIErrorResult ` -File $context.Path ` -OffsetInput $item.OffsetInput ` -Offset $null ` -FileSize $context.Length ` -EncodingRequested $Encoding ` -CompareFile $CompareFile ` -Message $_.Exception.Message continue } if ($offset -lt 0 -or $offset -ge $context.Length) { $results[$item.Index] = New-OIErrorResult ` -File $context.Path ` -OffsetInput $item.OffsetInput ` -Offset $offset ` -FileSize $context.Length ` -EncodingRequested $Encoding ` -CompareFile $CompareFile ` -Message "Offset $offset is outside the valid range 0-$($context.Length - 1)." continue } $entry = [pscustomobject]@{ Index = $item.Index OffsetInput = $item.OffsetInput Offset = [int64]$offset } $validEntries.Add($entry) $validOffsets.Add([int64]$offset) } if ($validEntries.Count -eq 0) { continue } $lineRecords = Get-OILineRecords ` -Context $context ` -Offsets $validOffsets.ToArray() ` -ContextLines $ContextLines foreach ($entry in $validEntries) { $lineRecord = $lineRecords[[string]$entry.Offset] if ($null -eq $lineRecord -or $null -eq $lineRecord.TargetLine) { $results[$entry.Index] = New-OIErrorResult ` -File $context.Path ` -OffsetInput $entry.OffsetInput ` -Offset $entry.Offset ` -FileSize $context.Length ` -EncodingRequested $Encoding ` -CompareFile $CompareFile ` -Message 'Unable to map the offset to a source line.' continue } $results[$entry.Index] = Get-OIInspectionResult ` -Context $context ` -OffsetInput $entry.OffsetInput ` -Offset $entry.Offset ` -LineRecord $lineRecord ` -ByteWindow $ByteWindow ` -CompareContext $compareContext ` -CompareError $compareError ` -CompareRequestedPath $CompareFile ` -MaxLineBytes $MaxLineBytes } } finally { Close-OIFileContext -Context $context } } } finally { Close-OIFileContext -Context $compareContext } $output = Write-OIInspectionOutput -Results $results -Mode $mode -CsvPath $CsvPath if ($null -ne $output) { Write-Output $output } $failed = @($results | Where-Object { -not $_.Success }) if ($FailOnError -and $failed.Count -gt 0) { throw "OffsetInspect completed with $($failed.Count) failed result(s)." } } |