Question

What does the "B" do in this pack statement from Perl code?

$hce_hash=pack('B*', $hce_hash);

Is there an equivalent function in PHP?

Was it helpful?

Solution

PHP’s pack doesn’t support a format of B*, but it does support H*. In Perl, you could emulate it with

sub pack_Bstar {
  my($bits) = @_;
  my $Hstar;

  my $nybble = 0;
  for (my $i = 0; $i < length $bits; ++$i) {
    $nybble *= 2;
    $nybble += int substr($bits, $i, 1);
    if ($i % 4 == 3) {
      $Hstar .= sprintf "%x", $nybble;
      $nybble = 0;
    }
  }

  my $pad = 4 - length($bits) % 4;
  if ($pad != 4) {
    $nybble = ($nybble << $pad);
    $Hstar .= sprintf "%x", $nybble;
  }

  pack "H*", $Hstar;
}

The code above is not idiomatic Perl, but translation to PHP should be straightforward.

The H* format wants a hex string with high nybble (4 bits) first. The code above chews off four bits at a time to compute each nybble value. For example, for a bit string of 1011, tracing the algorithm gives

  1. nybble = 0
  2. nybble = 2 * 0 + 1 = 1
  3. nybble = 2 * 1 + 0 = 2
  4. nybble = 2 * 2 + 1 = 5
  5. nybble = 2 * 5 + 1 = 11

10112 is indeed 1110, which is b16. If the last nybble is incomplete (between one and three bits), we left-shift the bit the appropriate number of places. This has the effect of zero-padding on the right.

Tests:

my @tests = (
  ["01001010011101010111001101110100"                               => "Just"],
  ["0110000101101110011011110111010001101000011001010111001"        => "another"],
  ["01010000010010000101000000101111010100000110010101110010011011" => "PHP/Perl"],
  ["01101000011000010110001101101011011001010111001000101100"       => "hacker,"],
);

for (@tests) {
  my($input,$expect) = @$_;
  my $got = pack_Bstar $input;
  print "$input: ", ($got eq $expect ? "PASS" : "FAIL"), " ($got)\n";
}

Output:

01001010011101010111001101110100: PASS (Just)
0110000101101110011011110111010001101000011001010111001: PASS (another)
01010000010010000101000000101111010100000110010101110010011011: PASS (PHP/Perl)
01101000011000010110001101101011011001010111001000101100: PASS (hacker,)

OTHER TIPS

pack 'B*', $s returns the bytes represented by the string of 0 and 1 characters that form up the string in $s. The value of $s is right-padded with zeros to a length divisible by 8 if necessary.

For example,

pack 'B*', '0100101000110101'

results in

chr(0b01001010) . chr(0b00110101);

As others have noted, PHP's pack() does not support the B template, which in Perl's pack() turns a bitstring, represented as a literal string of 0 and 1 characters, into a packed byte string with 8 bits per byte.

However, since PHP's pack() does support the H template, which does the same except for hexadecimal digits instead of bits, we can emulate Perl's B template in PHP by first using base_convert() to turn the bits into hex digits and then packing those:

function pack_B( $bits, $len = false ) {
    // truncate input to desired length, if given:
    if ( $len === false ) $len = strlen( $bits );
    else $bits = substr( $bits, 0, $len );

    // pad input with zeros to next multiple of 4 above $len:
    $hexlen = (int)( ($len + 3) / 4 );
    $bits = str_pad( $bits, 4*$hexlen, "0" );

    // split input into chunks of 4 bits, convert each to hex and pack them:
    $nibbles = str_split( $bits, 4 );
    foreach ( $nibbles as $i => $nibble ) {
        $nibbles[$i] = base_convert( $nibble, 2, 16 );
    }
    return pack( "H*", implode( "", $nibbles ) );
}

(The reason we can't just feed the whole input string to base_convert() is that it stores its intermediate result as a PHP float, and thus doesn't produce correct results for numbers too large to be accurately represented by a float. Doing it one hex digit at a time works fine, however.)

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top