Utils/od.ps1
|
Register-GnuUtil @{ Name = 'od' Category = 'Viewing file contents' Synopsis = 'Dump files in octal and other formats.' Usage = @('od [OPTION]... [FILE]...') Description = @( 'Write an unambiguous representation, octal bytes by default,', 'of FILE to standard output. With more than one FILE argument,', 'concatenate them in the listed order to form the input.', '', 'With no FILE, or when FILE is -, read standard input.' ) FileOperands = $true Options = @( @{ Short = 'A'; Long = 'address-radix'; Arg = 'RADIX'; Help = 'output format for file offsets; RADIX is one of [doxn], for Decimal, Octal, Hex or None' } @{ Long = 'endian'; Arg = '{big|little}'; Help = 'swap input bytes according the specified order' } @{ Short = 'j'; Long = 'skip-bytes'; Arg = 'BYTES'; Help = 'skip BYTES input bytes first' } @{ Short = 'N'; Long = 'read-bytes'; Arg = 'BYTES'; Help = 'limit dump to BYTES input bytes' } @{ Short = 't'; Long = 'format'; Key = 'format'; Arg = 'TYPE'; Multi = $true; Help = 'select output format or formats' } @{ Short = 'v'; Long = 'output-duplicates'; Help = 'do not use * to mark line suppression' } @{ Short = 'w'; Long = 'width'; Arg = 'BYTES'; OptionalArg = $true; Help = 'output BYTES bytes per output line; 32 is implied when BYTES is not specified' } @{ Short = 'a'; Key = 'format'; Value = 'a'; Multi = $true; Help = 'same as -t a, select named characters, ignoring high-order bit' } @{ Short = 'b'; Key = 'format'; Value = 'o1'; Multi = $true; Help = 'same as -t o1, select octal bytes' } @{ Short = 'c'; Key = 'format'; Value = 'c'; Multi = $true; Help = 'same as -t c, select printable characters or backslash escapes' } @{ Short = 'd'; Key = 'format'; Value = 'u2'; Multi = $true; Help = 'same as -t u2, select unsigned decimal 2-byte units' } @{ Short = 'f'; Key = 'format'; Value = 'fF'; Multi = $true; Help = 'same as -t fF, select floats' } @{ Short = 'i'; Key = 'format'; Value = 'dI'; Multi = $true; Help = 'same as -t dI, select decimal ints' } @{ Short = 'l'; Key = 'format'; Value = 'dL'; Multi = $true; Help = 'same as -t dL, select decimal longs' } @{ Short = 'o'; Key = 'format'; Value = 'o2'; Multi = $true; Help = 'same as -t o2, select octal 2-byte units' } @{ Short = 's'; Key = 'format'; Value = 'd2'; Multi = $true; Help = 'same as -t d2, select decimal 2-byte units' } @{ Short = 'x'; Key = 'format'; Value = 'x2'; Multi = $true; Help = 'same as -t x2, select hexadecimal 2-byte units' } ) Notes = @( 'TYPE is made up of one or more of these specifications:', ' a named character, ignoring high-order bit', ' c printable character or backslash escape', ' d[SIZE] signed decimal, SIZE bytes per integer', ' f[SIZE] floating point, SIZE bytes per float', ' o[SIZE] octal, SIZE bytes per integer', ' u[SIZE] unsigned decimal, SIZE bytes per integer', ' x[SIZE] hexadecimal, SIZE bytes per integer', '', 'SIZE is a number. For TYPE in [doux], SIZE may also be C for', 'sizeof(char), S for sizeof(short), I for sizeof(int) or L for', 'sizeof(long). If TYPE is f, SIZE may also be F for sizeof(float), D', 'for sizeof(double) or L for sizeof(long double).', '', 'Adding a z suffix to any type displays printable characters at the end', 'of each output line.', '', 'BYTES is hex with 0x or 0X prefix, and may have a multiplier suffix:', ' b 512', ' KB 1000', ' K 1024', ' MB 1000*1000', ' M 1024*1024', 'and so on for G, T, P, E, Z, Y, R, Q.', 'Binary prefixes can be used, too: KiB=K, MiB=M, and so on.', '', 'PowerShell note: long double (fL) is printed as double; -S (strings) and', '--traditional are not supported (use the strings program or Select-String).' ) } $script:GnuOdNames = @('nul', 'soh', 'stx', 'etx', 'eot', 'enq', 'ack', 'bel', 'bs', 'ht', 'nl', 'vt', 'ff', 'cr', 'so', 'si', 'dle', 'dc1', 'dc2', 'dc3', 'dc4', 'nak', 'syn', 'etb', 'can', 'em', 'sub', 'esc', 'fs', 'gs', 'rs', 'us', 'sp') # Printed width of one value: integer widths by kind and size, floats by size. $script:GnuOdWidths = @{ d1 = 4; o1 = 3; u1 = 3; x1 = 2; d2 = 6; o2 = 6; u2 = 5; x2 = 4 d4 = 11; o4 = 11; u4 = 10; x4 = 8; d8 = 20; o8 = 22; u8 = 20; x8 = 16 f4 = 15; f8 = 24; a1 = 3; c1 = 3 } function script:ConvertFrom-GnuOdType { <# .SYNOPSIS Parses a -t TYPE string into specs @{ Kind; Size; Width; Trailer }. Returns @{ Specs } or @{ Error }. #> param([Parameter(Mandatory)][string]$Text) $specs = New-Object 'System.Collections.Generic.List[hashtable]' $i = 0 while ($i -lt $Text.Length) { $kind = [string]$Text[$i] $i++ $size = 1 if ('doux'.Contains($kind)) { $size = 4 if ($i -lt $Text.Length -and 'CSIL'.Contains([string]$Text[$i])) { $size = @{ C = 1; S = 2; I = 4; L = 8 }[[string]$Text[$i]] $i++ } elseif ($i -lt $Text.Length -and [char]::IsDigit($Text[$i])) { $digits = [regex]::Match($Text.Substring($i), '^\d+').Value $i += $digits.Length $size = [int]$digits if (@(1, 2, 4, 8) -notcontains $size) { return @{ Error = "invalid type string '$Text';`nthis system doesn't provide a $size-byte integral type" } } } } elseif ($kind -ceq 'f') { $size = 8 if ($i -lt $Text.Length -and 'FDL'.Contains([string]$Text[$i])) { if ($Text[$i] -ceq 'F') { $size = 4 } $i++ } elseif ($i -lt $Text.Length -and [char]::IsDigit($Text[$i])) { $digits = [regex]::Match($Text.Substring($i), '^\d+').Value $i += $digits.Length $size = [int]$digits if (@(4, 8) -notcontains $size) { return @{ Error = "invalid type string '$Text';`nthis system doesn't provide a $size-byte floating point type" } } } } elseif ($kind -cne 'a' -and $kind -cne 'c') { return @{ Error = "invalid character '$kind' in type string '$Text'" } } $trailer = $false if ($i -lt $Text.Length -and $Text[$i] -ceq 'z') { $trailer = $true; $i++ } $specs.Add(@{ Kind = $kind; Size = $size; Width = $script:GnuOdWidths["$kind$size"]; Trailer = $trailer }) } @{ Specs = $specs } } function script:ConvertFrom-GnuOdCount { <# .SYNOPSIS A byte count for -j / -N (decimal, 0x hex, leading-0 octal, size suffixes), or $null. #> param([Parameter(Mandatory)][AllowEmptyString()][string]$Text) if ($Text -match '^0[xX]([0-9a-fA-F]+)$') { return [System.Convert]::ToInt64($Matches[1], 16) } if ($Text -match '^0([0-7]+)$') { return [System.Convert]::ToInt64($Matches[1], 8) } $value = ConvertFrom-GnuSize -Text $Text if ($null -eq $value) { return $null } ConvertTo-GnuInt64 -Value $value } function script:Format-GnuOdFloat { <# .SYNOPSIS The shortest %g text that reads back as the same float or double (like coreutils' ftoastr). #> param([Parameter(Mandatory)][double]$Value, [Parameter(Mandatory)][int]$Size) if ([double]::IsNaN($Value)) { return 'nan' } if ([double]::IsPositiveInfinity($Value)) { return 'inf' } if ([double]::IsNegativeInfinity($Value)) { return '-inf' } if ($Value -eq 0) { if ((1 / $Value) -lt 0) { return '-0' } return '0' } $digits = 15 $smallest = 2.2250738585072014E-308 if ($Size -eq 4) { $digits = 6; $smallest = 1.17549435E-38 } if ([Math]::Abs($Value) -lt $smallest) { $digits = 1 } # 'G<p>' gives up to 17 significant digits on every runtime (.NET Framework's 'F' and 'E' stop at 15); # 17 digits always read back exactly. $invariant = [System.Globalization.CultureInfo]::InvariantCulture for ($precision = $digits; $precision -lt 17; $precision++) { $text = $Value.ToString('G' + $precision, $invariant) $back = [double]::Parse($text, $invariant) if ($Size -eq 4) { if ([float]$back -eq [float]$Value) { return ConvertTo-GnuCStyleG -Text $text -Precision $precision } } elseif ($back -eq $Value) { return ConvertTo-GnuCStyleG -Text $text -Precision $precision } } ConvertTo-GnuCStyleG -Text $Value.ToString('G17', $invariant) -Precision 17 } function script:ConvertTo-GnuCStyleG { <# .SYNOPSIS Re-lays a .NET 'G<p>' number text out like C's %.<p>g (exponent form when exp < -4 or exp >= p). #> param([Parameter(Mandatory)][string]$Text, [Parameter(Mandatory)][int]$Precision) $sign = '' if ($Text.StartsWith('-', [System.StringComparison]::Ordinal)) { $sign = '-'; $Text = $Text.Substring(1) } $exponent = 0 $mantissa = $Text $at = $Text.IndexOf('E') if ($at -ge 0) { $mantissa = $Text.Substring(0, $at); $exponent = [int]$Text.Substring($at + 1) } $point = $mantissa.IndexOf('.') if ($point -lt 0) { $point = $mantissa.Length } $digits = $mantissa.Replace('.', '') $zeros = $digits.Length - $digits.TrimStart('0').Length $digits = $digits.Substring($zeros).TrimEnd('0') $scientific = $exponent + $point - 1 - $zeros if ($scientific -lt -4 -or $scientific -ge $Precision) { $text = $digits.Substring(0, 1) if ($digits.Length -gt 1) { $text += '.' + $digits.Substring(1) } $mark = '+' if ($scientific -lt 0) { $mark = '-' } return "$sign$($text)e$mark$([Math]::Abs($scientific).ToString('00'))" } if ($scientific -lt 0) { return "$($sign)0." + ('0' * (-$scientific - 1)) + $digits } if ($digits.Length -le $scientific + 1) { return $sign + $digits.PadRight($scientific + 1, '0') } $sign + $digits.Substring(0, $scientific + 1) + '.' + $digits.Substring($scientific + 1) } function script:Format-GnuOdValue { <# .SYNOPSIS One value of an od line (Bytes holds Size bytes in the requested byte order). #> param([Parameter(Mandatory)][hashtable]$Spec, [Parameter(Mandatory)][byte[]]$Bytes, [Parameter(Mandatory)][bool]$BigEndian) $byte = $Bytes[0] switch -CaseSensitive ($Spec.Kind) { 'a' { $code = $byte -band 127 if ($code -lt 33) { return $script:GnuOdNames[$code] } if ($code -eq 127) { return 'del' } return [string][char]$code } 'c' { $escapes = @{ 0 = '\0'; 7 = '\a'; 8 = '\b'; 9 = '\t'; 10 = '\n'; 11 = '\v'; 12 = '\f'; 13 = '\r' } if ($escapes.ContainsKey([int]$byte)) { return $escapes[[int]$byte] } if ($byte -ge 32 -and $byte -lt 127) { return [string][char]$byte } return [System.Convert]::ToString([int]$byte, 8).PadLeft(3, '0') } } $ordered = [byte[]]$Bytes.Clone() if ($BigEndian -eq [System.BitConverter]::IsLittleEndian) { [array]::Reverse($ordered) } if ($Spec.Kind -ceq 'f') { if ($Spec.Size -eq 4) { return Format-GnuOdFloat -Value ([double][System.BitConverter]::ToSingle($ordered, 0)) -Size 4 } return Format-GnuOdFloat -Value ([System.BitConverter]::ToDouble($ordered, 0)) -Size 8 } $unsigned = [uint64]0 switch ($Spec.Size) { 1 { $unsigned = [uint64]$ordered[0] } 2 { $unsigned = [uint64][System.BitConverter]::ToUInt16($ordered, 0) } 4 { $unsigned = [uint64][System.BitConverter]::ToUInt32($ordered, 0) } default { $unsigned = [System.BitConverter]::ToUInt64($ordered, 0) } } switch -CaseSensitive ($Spec.Kind) { 'd' { $signed = [System.Numerics.BigInteger]$unsigned $limit = [System.Numerics.BigInteger]::Pow(2, 8 * $Spec.Size) if ($signed -ge $limit / 2) { $signed -= $limit } return $signed.ToString([System.Globalization.CultureInfo]::InvariantCulture) } 'u' { return $unsigned.ToString([System.Globalization.CultureInfo]::InvariantCulture) } 'x' { return $unsigned.ToString('x' + (2 * $Spec.Size)) } } ConvertTo-GnuRadixString -Value ([System.Numerics.BigInteger]$unsigned) -Base 8 | ForEach-Object { $_.PadLeft($Spec.Width, '0') } } function script:Invoke-GnuOdDump { <# .SYNOPSIS Implementation of od. #> param([Parameter(Mandatory)][hashtable]$Context) $radix = [string](Get-GnuOptionValue -Context $Context -Key 'address-radix' -Default 'o') if ($radix.Length -ne 1 -or 'doxn'.IndexOf($radix, [System.StringComparison]::Ordinal) -lt 0) { Write-GnuError -Context $Context -Message "invalid output address radix '$radix'; it must be one character from [doxn]" return } $bigEndian = -not [System.BitConverter]::IsLittleEndian if (Test-GnuOption -Context $Context -Key 'endian') { $endian = [string](Get-GnuOptionValue -Context $Context -Key 'endian') if (@('big', 'little') -notcontains $endian) { Write-GnuUsageError -Context $Context -Message "invalid argument '$endian' for '--endian'`nValid arguments are:`n - 'big'`n - 'little'"; return } $bigEndian = $endian -eq 'big' } $specs = New-Object 'System.Collections.Generic.List[hashtable]' foreach ($type in @(Get-GnuOptionValue -Context $Context -Key 'format' -Default @('o2'))) { $parsed = ConvertFrom-GnuOdType -Text $type if ($parsed.ContainsKey('Error')) { Write-GnuError -Context $Context -Message $parsed.Error; return } foreach ($spec in $parsed.Specs) { $specs.Add($spec) } } $skip = [long]0 $limit = [long]-1 foreach ($entry in @(@('skip-bytes', 'skip'), @('read-bytes', 'limit'))) { if (-not (Test-GnuOption -Context $Context -Key $entry[0])) { continue } $text = [string](Get-GnuOptionValue -Context $Context -Key $entry[0]) $value = ConvertFrom-GnuOdCount -Text $text if ($null -eq $value) { Write-GnuError -Context $Context -Message "invalid $($entry[0].Replace('-', ' ')) argument '$text'"; return } if ($entry[1] -eq 'skip') { $skip = $value } else { $limit = $value } } $lcm = 1 foreach ($spec in $specs) { $a = $lcm; $b = $spec.Size while ($b -ne 0) { $t = $a % $b; $a = $b; $b = $t } $lcm = $lcm / $a * $spec.Size } $width = 16 # a multiple of every size (1, 2, 4, 8) if (Test-GnuOption -Context $Context -Key 'width') { $text = [string](Get-GnuOptionValue -Context $Context -Key 'width') $width = 32 if (-not [string]::IsNullOrEmpty($text)) { if ($text -notmatch '^\d+$' -or [int]$text -eq 0) { Write-GnuError -Context $Context -Message "invalid -w argument '$text'"; return } $width = [int]$text } if ($width % $lcm -ne 0) { Write-GnuWarning -Context $Context -Message "warning: invalid width $width; using $lcm instead" $width = $lcm } } # Read the input: the files concatenated (stopping once enough bytes are read). $operands = @($Context.Operands) if ($operands.Count -eq 0) { $operands = @('-') } $data = New-Object 'System.Collections.Generic.List[byte]' foreach ($operand in $operands) { if ($limit -ge 0 -and $data.Count -ge $skip + $limit) { break } $bytes = Get-GnuInputBytes -Context $Context -Operand $operand if ($null -ne $bytes) { $data.AddRange([byte[]]$bytes) } } if ($skip -gt $data.Count) { Write-GnuError -Context $Context -Message 'cannot skip past end of combined input'; return } $count = $data.Count - $skip if ($limit -ge 0 -and $limit -lt $count) { $count = $limit } $dump = New-Object byte[] $count if ($count -gt 0) { $data.CopyTo([int]$skip, $dump, 0, [int]$count) } # Column layout (GNU: every format gets the same width per line, extra padding is spread over the fields). $maxWidth = 0 foreach ($spec in $specs) { $maxWidth = [Math]::Max($maxWidth, ($spec.Width + 1) * ($width / $spec.Size)) } foreach ($spec in $specs) { $spec.Pad = $maxWidth - $spec.Width * ($width / $spec.Size) } $addressWidth = @{ o = 7; d = 7; x = 6; n = 0 }[$radix] $previous = $null $starred = $false for ($offset = 0; $offset -lt $dump.Length; $offset += $width) { $length = [Math]::Min($width, $dump.Length - $offset) $line = New-Object byte[] $width [array]::Copy($dump, $offset, $line, 0, $length) if ($length -eq $width -and -not (Test-GnuOption -Context $Context -Key 'output-duplicates') -and $null -ne $previous -and [System.Linq.Enumerable]::SequenceEqual([byte[]]$line, [byte[]]$previous)) { if (-not $starred) { '*'; $starred = $true } continue } $starred = $false $previous = $line $first = $true foreach ($spec in $specs) { $builder = New-Object System.Text.StringBuilder if ($first) { [void]$builder.Append((Format-GnuOdAddress -Offset ($skip + $offset) -Radix $radix)) } else { [void]$builder.Append(' ' * $addressWidth) } $first = $false $fields = $width / $spec.Size $used = [int][Math]::Ceiling($length / $spec.Size) $remaining = $spec.Pad for ($i = $fields; $i -gt $fields - $used; $i--) { $nextPad = [int][Math]::Floor($spec.Pad * ($i - 1) / $fields) $index = ($fields - $i) * $spec.Size $chunk = New-Object byte[] $spec.Size [array]::Copy($line, $index, $chunk, 0, $spec.Size) $value = Format-GnuOdValue -Spec $spec -Bytes $chunk -BigEndian $bigEndian [void]$builder.Append($value.PadLeft($remaining - $nextPad + $spec.Width)) $remaining = $nextPad } if ($spec.Trailer) { $blank = $fields - $used [void]$builder.Append(' ' * ($blank * $spec.Width + [int][Math]::Floor($spec.Pad * $blank / $fields))) [void]$builder.Append(' >') for ($k = 0; $k -lt $length; $k++) { $byte = $line[$k] if ($byte -ge 32 -and $byte -lt 127) { [void]$builder.Append([char]$byte) } else { [void]$builder.Append('.') } } [void]$builder.Append('<') } $builder.ToString() } } if ($radix -ne 'n') { Format-GnuOdAddress -Offset ($skip + $dump.Length) -Radix $radix } } function script:Format-GnuOdAddress { <# .SYNOPSIS An od address: 7 octal or decimal digits, 6 hex digits, or nothing. #> param([Parameter(Mandatory)][long]$Offset, [Parameter(Mandatory)][string]$Radix) switch ($Radix) { 'o' { return [System.Convert]::ToString($Offset, 8).PadLeft(7, '0') } 'd' { return ([string]$Offset).PadLeft(7, '0') } 'x' { return $Offset.ToString('x6') } } '' } |