I'm working on some code that still needs Powershell 5, and I'm making use of some .NET tools, including [System.BitConverter]::GetBytes(). This function always returns a byte array... it has to, as .NET uses strongly-typed functions. However, sometimes when I call it the resulting array only has one element. When I do this, Powershell seems to unroll the array and give me the bare first element as a simple byte value (no array).
I end up working around it with code like this:
$bytes = [System.BitConverter]::GetBytes($data)
if ($bytes -isnot [System.Array]) {
$bytes = @($bytes)
}
I know if I were looking at this from the other direction I could force an array in various ways, and there are several questions already here about that. But I didn't see anything helping me understand what is going on with this case, where I know I already had an array, and now have to do extra work to get it back.
So the question: is there a better work-around than the -isnot [System.Array] conditional expression, and what is actually going on here?
I expect this is a "feature" that is actually useful in pipeline situations, but since I'm calling the .NET function directly and not piping anything I wonder if there's a way to tell PS to skip that feature here.
The longer form of what I'm actually doing involves certificate automation. We have a pfx file with a private key from a windows certificate store, and we want to transform it into cert and key pairs suitable to use with linux/apache. This code is part of a piece to export the primary key. Part of the key involves encoding the data length.
The original code I found looked like this:
function Encode-Asn1 {
param($type, $data)
# Common Type-Length-Value encoding
$encoded = New-Object System.IO.MemoryStream
# Type
$encoded.WriteByte($type)
# Length
$length = $data.Length
if ($length -lt 0x80) { # if the length is less than 128, write the length part of the structure as a single byte
$encoded.WriteByte($length)
} else { # Otherwise we need the longer encoding
# Handle multi-byte length fields for large data
$lengthBytes = [System.BitConverter]::GetBytes($length).Reverse() | Where-Object { $_ -ne 0 }
$encoded.WriteByte(0x80 + $lengthBytes.Length)
$encoded.Write($lengthBytes, 0, $lengthBytes.Length)
}
# Value
$encoded.Write($data, 0, $data.Length)
return $encoded.ToArray()
}
(Longer version available on GitHub)
The above function DEFINITELY put up errors where there was an attempt to call Reverse() as a method from a single byte, instead of a byte array. I don't know how that is possible in the first place, since $length should be an integer that always creates a 4 byte array, but I watched it happen and pulled hair over it for a while.
The fix that actually works looks like this:
# Otherwise we need the longer encoding
# Handle multi-byte length fields for large data
$bytes = [System.BitConverter]::GetBytes($length)
if ($bytes -isnot [System.Array]) {
$bytes = @($bytes)
} # If the array is one item long, no need to reverse
[Array]::Reverse($bytes)
$lengthBytes = $bytes | Where-Object { $_ -ne 0 }
That is, taking the method result as a variable with a conditional check to make a corrective action if we didn't get what we expect actually fixed things.
I ended up with [Array].Reverse($bytes), but I believe $bytes = $bytes.Reverse() or just $bytes.Reverse() | Where-Object ... has the same positive result. The point is it's not that change that fixed things.
The above works in testing so far, where the original did not. In fact, it has created the key for one certificate I've moved on the production and now seen served from a public web server. I had a version of this where I added an else block and logging to prove the conditional expression sometimes succeeds, and sometimes does not, depending on the data.
But I hate that I don't know what's going on.
(This would be much easier with either Powershell 7 or openssl available, but I've been asked to attempt this without taking the additional dependency).
The condition $bytes -isnot [System.Array] should never be met in the context of your question; as the method returns byte[], even on single element arrays like:
$bytes = [System.BitConverter]::GetBytes($null)
$bytes -is [array] # True
Unrolling of IEnumerables happen as they're being outputted from a function or script block, or in a cmdlet when the enumerateCollection argument in WriteObject is set to true.
Following the previous example and using a script block to demo:
$bytes = & { [System.BitConverter]::GetBytes($null) }
$bytes -is [array] # False
Also should be noted that when the method outputs more than one element it might seem it is working as expected; however, unless using one of the workarounds mentioned below, PowerShell is enumerating and collecting each element in a backing array list and then converting it to an array once enumeration finishes. We can tell this is the case by checking how the type changes from the original byte[] to an object[]:
$bytes = & { [System.BitConverter]::GetBytes(10) }
$bytes.GetType() # object[]
To prevent the unrolling, which you're probably aware of, you can use one of your choice:
# unary comma
$bytes = & { , [System.BitConverter]::GetBytes($null) }
$bytes.GetType() # byte[]
# Write-Output -NoEnumerate
$bytes = & { Write-Output ([System.BitConverter]::GetBytes($null)) -NoEnumerate }
$bytes.GetType() # byte[]
# $PSCmdlet.WriteObject - only in advanced functions / cmdlets
$bytes = & {[CmdletBinding()] param()
# default value for `enumerateCollection` is `false`
$PSCmdlet.WriteObject([System.BitConverter]::GetBytes($null))
}
$bytes.GetType() # byte[]
Regarding the latest edit, the issue I'm seeing is the use of .Reverse() as an instance method when byte[] doesn't have such method. It would be valid in C# as an extension method with Enumerable.Reverse, and very likely the error in your function is coming from there.
So if you change the line:
$lengthBytes = [System.BitConverter]::GetBytes($length).Reverse() |
Where-Object { $_ -ne 0 }
To use LINQ it should be good to go:
$lengthBytes = [System.Linq.Enumerable]::Reverse([System.BitConverter]::GetBytes($length)) |
Where-Object { $_ -ne 0 }
Might as well use LINQ all the way to to preserve type fidelity:
$lengthBytes = [System.Linq.Enumerable]::Where(
[System.Linq.Enumerable]::Reverse([System.BitConverter]::GetBytes($length)),
[System.Func[byte, bool]] { param($e) $e -ne 0 }).ToArray()
As aside, be mindful of your function return, perhaps you also want , $encoded.ToArray() to avoid enumeration there too.
Mathias notes that, although not explicitly stated in the question, the error message suggests you might be working with a single byte rather than a byte[]:
InvalidOperation: Method invocation failed because
[System.Byte]does not contain a method named 'Reverse'.
In his helpful comment, he explains:
The version with
Reverse()fails because there's no resolvable[byte[]].Reverse()method in 5.1, so PowerShell applies member-access enumeration, hence the error relating to a scalar byte.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With