3

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).

7
  • 1
    $bytes -isnot [System.Array] is not a condition that would be met in any situation in the context of your question. If you're assigning the output of a method to a variable no unrolling happens. I feel like there is something we're not seeing Commented Oct 1 at 19:50
  • 2
    Does the , operator help? (i.e., just ,[BitConverter]::GetBytes($data)) Commented Oct 1 at 19:53
  • @Bill_Stewart Maybe. I already have code that works, but I'm trying to understand the why, since as others have indicated this makes no sense. Commented Oct 1 at 22:37
  • 1
    @JoelCoehoorn It makes perfect sense with the updated example - it's because of the | between the expression and Where-Object. | unrolls the input array unless the command immediately upstream requests otherwise (see Santiago's answer for examples), and if Where-Object ultimately outputs less than 2 objects they won't be implicitly wrapped in an array. Hence no array. Commented Oct 2 at 9:51
  • 3
    @JoelCoehoorn 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 Commented Oct 2 at 15:01

1 Answer 1

6

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.

Sign up to request clarification or add additional context in comments.

6 Comments

I don't know what to tell you, but I was definitely getting errors. The next step in the original code was to reverse the array, and both .Reverse() and [Array].Reverse($b) were failing with with an error that there was no reverse method and the input argument is not an array, respectively. Adding that exact conditional expression fixed it.
Added an edit with more details.
Joel where would .Reverse() as an instance method of byte[] would come from? There is no such thing in PowerShell, would be valid in C# if using Enumerable.Reverse() as an extension method. I actually think this is your problem in your original function. If you did [System.Linq.Enumerable]::Reverse([System.BitConverter]::GetBytes($length)) then it'd all be good to go!
Hopefully that last edit helps solving the actual problem / understanding where the problem origintaes
"hopefully the last edit..." Maybe. You're on to something about the extension method. But I'm definitely still able to prove the code sometimes enters the conditional block, so that still happens. And it still doesn't explain why I see a single byte earlier in the process, when an integer input should always have a four-byte array.
Can you provide a way to make your Encode-Asn1 function fail using the [System.Linq.Enumerable]::Reverse approach shown here ? Otherwise we're just left to guessing, without a reproducible example.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.