【问题标题】:Convert Byte Array (from legacy program data file) to Powershell object将字节数组(从遗留程序数据文件)转换为 Powershell 对象
【发布时间】:2017-04-20 07:02:51
【问题描述】:

我有一个来自旧版(非 MS)操作系统上的旧版程序的“结构化”文件(逻辑固定长度记录)。我知道原始程序中记录的结构,但原始 O/S 将结构化数据处理为文件 I/O 的字节序列,因此十六进制转储不会向您显示记录长度以外的任何内容(最初用于生成文件的访问方法 API 会产生标记字节和其他记录开销)。

一旦我在 Powershell 变量中获得了字节序列,并且开销字节“被删除”,我如何将其转换为结构化对象?一些“字段”是 16 位整数,一些是 [s]data 形式的字符串(其中 [s] 是一个字节,给出了该字段中“真实”数据的长度),一些是 BCD 编码的固定 -点数,有些是 IEEE 浮点数。

(我没有具体说明结构,无论是在 Powershell 方面还是在旧版方面,因为我正在寻求或多或少的“通用”解决方案/技术,因为我实际上有几个不同的文件要处理的不同记录结构。)

【问题讨论】:

  • 哇,这很有趣,你能告诉我们更多关于输入数据的信息吗?具体来说,原始操作系统是如何确定记录的数据类型的?据推测,您可以创建一个解释器,它基本上遵循旧版操作系统解析数据的相同规则,因此您遍历字符串,找到第一个标记字节,读入数据直到“记录结束”标记字节,然后解析基于该数据类型规则的数据,最有可能使用[convert] 将二进制转换为可用文本。
  • 原来的操作系统没有确定记录的类型;它是应用程序,当它传递一个指向记录的系统指针作为写入记录 API 调用的数据缓冲区的指针时,API 调用将简单地将其视为内存中的原始字节,用记录开销,并将其写入文件。如果原始应用程序是用 Pascal 编写的,那么程序的源代码中就会有一个 TYPE something = RECORD... 声明。
  • 旧版操作系统和应用程序是什么?也许已经存在一个可以通过一些努力移植到 .Net / Powershell 的实现。
  • 好的,那么原始程序如何在读取时确定数据类型?更好的是,您能告诉我们操作系统和应用程序在谈论什么,以便我们进行一些外部研究
  • 对于它的价值,如果结果证明有一种很好的通用方法来处理这个问题,这样我就不需要手动解析传入的数据(即使我知道结构 在这种情况下),我也许可以把它变成一个可共享的 Powershell 模块来处理“基于记录”的文件。

标签: powershell


【解决方案1】:

最初,我尝试通过创建一个可以获取缓冲区并覆盖结构的类型来做到这一点,以便很好地填充所有字段。但是,出现了某些问题(关于结构布局、固定缓冲区以及混合固定和托管成员)并且我还意识到不能保证缓冲区中的数据会正确(甚至合法地)对齐。决定尝试更程序化的路径。

“手动”解析已经出来了,那么自动解析呢?您将需要在某些时候定义PSobject 的成员,为什么不以有助于以编程方式解析数据的方式进行定义。这种方法不需要缓冲区中的数据正确对齐甚至是连续的。您还可以让字段重叠以将原始联合分隔成单个成员(尽管通常只有一个包含“正确”值)。

第一步,建立一个哈希表来识别成员,缓冲区中的偏移量,它们的数据类型,如果是数组,元素的数量:

$struct = @{
 field1 = 0,[int],0;  # 0 means not an array
 field2 = 4,[byte],16; # a C string maybe
 field3 = 24,[char],32;  # wchar_t[32] ? note: skipped over bytes 20-23
 field4 = 56,[double],0
}
# the names field1/2/3/4 are arbitrary, any valid member name may be used (but not
# necessarily any valid hash key if you want a PSObject as the end result).
# also, the values could be hash tables instead of arrays. that would allow
# descriptive names for the values but doesn't affect the end result.

接下来,使用[BitConverter] 提取所需的数据。这里的问题是我们需要为所有不同的类型调用正确的方法。只需使用(大)switch 语句。大多数值的基本原理是相同的,从$struct 定义中获取类型指示符和初始偏移量,然后调用正确的[BitConverter] 方法并提供缓冲区和初始偏移量,将偏移量更新到数组的下一个元素的位置将是然后重复所需的尽可能多的数组元素。这里唯一的陷阱是缓冲区中的数据必须具有与[BitConverter] 所期望的格式相同的格式,因此对于[double] 示例,缓冲区中的字节必须符合IEEE-754 浮点格式(假设@987654329 @ 用来)。因此,例如,来自 Paradox 数据库的原始数据将需要一些调整,因为它会翻转高位以简化排序。

$struct.keys | foreach {
# key order is undefined but that won't affect the final object's members
   $hashobject = @{}
} {
   $fieldoffs = $struct[$_][0]
   $fieldtype = $struct[$_][1]
   if (($arraysize = $struct[$_][2]) -ne 0) { # yes, I'm a C programmer from way back
      $array = @()
   } else {
      $array = $null
   }
:w while ($arraysize-- -ge 0) {
      switch($fieldtype) {
         ([int]) {
            $value = [bitconverter]::toint32($buffer, $fieldoffs)
            $fieldoffs += 4
         }
         ([byte]) {
            $value = $buffer[$fieldoffs++]
         }
         ([char]) {
            $value = [bitconverter]::tochar($buffer, $fieldoffs)
            $fieldoffs += 2
         }
         ([string]) { # ANSI string, 1 byte per character
            $array = new-object string (,[char[]]$buffer[$fieldoffs..($fieldoffs+$arraysize)])
# $arraysize has already been decremented so don't need to subtract 1
            break w # "array size" was actually string length so don't loop
#
# description:
# first, get a slice of the buffer as a byte[] (assume single byte characters)
# next, convert each byte to a char in a char[]
# then, invoke the constructor String(Char[])
# finally, put the String into $array ready for insertion into $hashobject
#
# Note the convoluted syntax - New-Object expects the second argument to be
# an array of the constructor parameters but String(Char[]) requires only
# one argument that is itself an array. By itself,
#    [char[]]$buffer[$fieldoffs..($fieldoffs+$arraysize)]
# is treated by PowerShell as an argument list of individual chars, corrupting the
# constructor call. The normal trick is to prepend a single comma to create an array
# of one element which is itself an array
#    ,[char[]]$buffer[$fieldoffs..($fieldoffs+$arraysize)]
# but this won't work because of the way PowerShell parses the command line. The
# space before the comma is ignored so that instead of getting 2 arguments (a string
# "String" and the array of an array of char), there is only one argument, an array
# of 2 elements ("String" and array of array of char) thereby totally confusing
# New-Object. To make it work you need to ALSO isolate the single element array into
# its own expression. Hence the parentheses
#    (,[char[]]$buffer[$fieldoffs..($fieldoffs+$arraysize)])
#
         }
      }
      if ($null -ne $array) {
# must be in this order* to stop the -ne from enumerating $array to compare against
# $null. this would result in the condition being considered false if $array were
# empty ( (@() -ne $null) -> $null -> $false ) or contained only one element with
# the value 0 ( (@(0) -ne $null) -> (scalar) 0 -> $false ).
         $array += $value
# $array is not $null so must be an array to which $value is appended
      } else {
# $array is $null only if $arraysize -eq 0 before the loop (and is now -1)
         $array = $value
# so the loop won't repeat thus leaving this one scalar in $array
      }
   }
   $hashobject[$_] = $array
}

#*could have reversed it as
#   if ($array -eq $null) { scalar } else { collect array }
# since the condition will only be true if $array is actually $null or contains at
# least 2 $null elements (but no valid conversion will produce $null)

此时有一个哈希表$hashobject,其键等于字段名称和包含缓冲区中的字节的值,这些字节排列成单个(或数组)数字(包括字符/布尔)值或( ANSI)字符串。要创建(正确的)对象,只需调用 New-Object -TypeName PSObject -Property $hashobject 或使用 [PSCustomObject]$hashobject

当然,如果缓冲区实际上包含结构化数据,那么过程会更复杂,但基本过程是相同的。还要注意$struct 哈希表中使用的“类型”对对象成员的结果类型没有直接影响,它们只是switch 语句的方便选择器。它同样适用于字符串或数字。实际上,case 标签周围的括号是因为switch 将它们解析为与命令参数相同。如果没有括号,标签将被视为文字字符串。使用它们,标签被评估为类型对象。然后将标签和开关值都转换为字符串(这就是开关对脚本块或$null 以外的值所做的事情),但每种类型都有不同的字符串表示形式,因此案例标签仍将正确匹配。 (我认为不是很准确,但仍然很有趣。)

可以进行多种优化,但会稍微增加复杂性。例如

         ([byte]) { # already have a byte[] so why collect bytes one at a time
            if ($arraysize -ge 0) { # was originally -gt 0 so want a byte[]
               $array = [byte[]]$buffer[$fieldoffs..($fieldoffs+$arraysize)]
# slicing the byte array produces an object array (of bytes) so cast it back
            } else { # $arraysize was 0 so just a single byte
               $array = $buffer[$fieldoffs]
            }
            break w # $array ready for insertion into $hashobject, don't need to loop
         }

但如果我的字符串实际上是 Unicode 怎么办?你说。很简单,只需使用[Text.Encoding] 类中的现有方法,

         [string] { # Unicode string, 2 (LE) bytes per character
            $array = [text.encoding]::unicode.getstring([byte[]]$buffer[$fieldoffs..($fieldoffs+$arraysize*2+1)])
# $arraysize should be the string length so, initially, $arraysize*2 is the byte
# count and $arraysize*2-1 is the end index (relative to $fieldoffs) but $arraysize
# was decremented so the end index is now $arraysize*2+1, i.e. length*2-1 = (length-1)*2+1
            break w # got $array, no loop
         }

您还可以通过为 ANSI 字符串使用不同的类型指示符(可能是 [char[]])来同时拥有 ANSI 和 Unicode。请记住,类型指示符不会影响结果,它们必须是不同的(并且希望是有意义的)标识符。

我意识到这并不完全是 OPs 评论中提到的“只是将字节转储到联合或变体记录”解决方案,但 PowerShell 基于 .NET 并在很大程度上禁止此类事情的情况下使用托管对象(或正如我发现的那样,很难开始工作)。例如,假设您可以将原始字符(而不是字节)转储到StringLength 属性将如何更新?此方法还允许进行一些有用的预处理,例如如上所述拆分联合或将原始 bytechar 数组转换为它们所代表的 Strings。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-06-01
    • 2021-11-30
    • 1970-01-01
    • 2016-10-31
    • 2018-02-14
    • 1970-01-01
    • 2017-10-07
    • 1970-01-01
    相关资源
    最近更新 更多