最初,我尝试通过创建一个可以获取缓冲区并覆盖结构的类型来做到这一点,以便很好地填充所有字段。但是,出现了某些问题(关于结构布局、固定缓冲区以及混合固定和托管成员)并且我还意识到不能保证缓冲区中的数据会正确(甚至合法地)对齐。决定尝试更程序化的路径。
“手动”解析已经出来了,那么自动解析呢?您将需要在某些时候定义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 并在很大程度上禁止此类事情的情况下使用托管对象(或正如我发现的那样,很难开始工作)。例如,假设您可以将原始字符(而不是字节)转储到String,Length 属性将如何更新?此方法还允许进行一些有用的预处理,例如如上所述拆分联合或将原始 byte 或 char 数组转换为它们所代表的 Strings。