5
\$\begingroup\$

I have a third party system that submits what I am assuming is a byte array data from OBDII port on our fleet vehicles.

I was getting values like:

  • AA== (bool)
  • AQ== (bool)
  • AAAaMA== (int)
  • AAA0IQ== (int)
  • Mjow (string)
  • UzozLTIwNA== (string)

I know what the type is for each field but the regular base64_decode() function wouldn't decode them and neither did the online Base64 decoders.

Here is what I came up with that works, I just curious if this is the right way to decode base64 encoded bytes.

It essentially takes the input down to binary and converts the binary to the type. It works as expected but I feel like I'm over complicating it.

class Decoder
{
    /**
     * Decodes Base64 ByteArray to Number
     *
     * @param string $encoded
     * @return number
     */
    public static function getNumber($encoded)
    {
        $decoded = base64_decode($encoded);

        $byteArray = array();

        foreach(str_split($decoded) as $byte)
        {
            $byteArray[] = sprintf("%08b", ord($byte));
        }

        $raw_bytes = implode(' ', $byteArray);

        return bindec($raw_bytes);
    }

    /**
     * Decodes Base64 ByteArray to ASCII String
     *
     * @param string $encoded
     * @return string
     */
    public static function getString($encoded)
    {
        $decoded = base64_decode($encoded);

        $raw = "";

        foreach(str_split($decoded) as $byte)
        {
            $raw .= chr(bindec(sprintf("%08b", ord($byte))));
        }

        return $raw;
    }

    /**
     * Decodes Base64 ByteArray to Boolean
     *
     * CHEATER FUNCTION
     *
     * Simply interprets Base64 value against known true/false values
     *
     * Value: "AQ==" will return true everything else will be false
     *
     * @param $encoded
     * @return bool
     */
    public static function getBool($encoded)
    {
        return ($encoded == "AQ==");
    }

    /**
     * Decodes Base64 ByteArray to Boolean
     *
     * Actually does decode and comparison on byte level.
     *
     * More expensive but is a true decode.
     *
     * @param $encoded
     * @return bool
     */
    public static function zDecodeBool($encoded)
    {
        $decoded = base64_decode($encoded);

        $byteArray = array();

        foreach(str_split($decoded) as $byte)
        {
            $byteArray[] = sprintf("%08b", ord($byte));
        }

        return $byteArray[0] == "00000001";
    }
}
\$\endgroup\$
6
\$\begingroup\$

Your code is generally clean, you have accurate comments & variable naming is mostly consistent.

$raw_bytes and $byteArray use two different types of naming. It's best if you choose one style, and stick with it.

With return ($encoded == "AQ==") and return $byteArray[0] == "00000001", you don't have a consistent use of brackets, it's best to stick to one.


Onto code, in getNumber(), you initialise an array, build to it, and then proceed to implode it.
However, in getString(), you initialise a string, build to it, and then return it.

In getNumber(), it'd be better if you just built to the string instead of an array.

$byteArray = array();
foreach(str_split($decoded) as $byte)
{
    $byteArray[] = sprintf("%08b", ord($byte));
}
$raw_bytes = implode(' ', $byteArray);
return bindec($raw_bytes);

into:

$rawBytes = "";
foreach(str_split($decoded) as $byte)
{
    $rawBytes .= ' ' . sprintf("%08b", ord($byte));
}
return bindec($rawBytes);

Also, zDecodeBool() (could have a better name), runs nearly the same code as getString(), you could consider a compromise between the two functions.


Other than that, your code looks nice and clean.

\$\endgroup\$
  • 1
    \$\begingroup\$ Thanks, I made the changes to the formatting you suggested \$\endgroup\$ – McGee.WIll Jun 11 '15 at 13:58
  • \$\begingroup\$ Thanks I made the changes you suggested for format. Being the only programmer where I work its tough to catch those little things. zDecodeBool was actually just an after thought and I removed it anyway as I thinks its unnecessary. \$\endgroup\$ – McGee.WIll Jun 11 '15 at 14:02
1
\$\begingroup\$

Here is a somewhat more compact version of the decoder:

function Decode($input, $strTest = true, $allow = [":","-"]){
    while(strlen($input) % 4) $input .= "="; // ensure correct input length
    $input =  strtr($input,'-_','+/'); // encure correct input types
    $input = unpack("C*",base64_decode($input)); // convert input into binary array
    if($strTest){
        $strTest = $input; $output = ''; 
        while($strTest){$output .= chr(array_pop($strTest));}
        if(ctype_alnum(str_replace($allow,'',$output))) return $output; } // output str
    $output = 0; while($input)
        {$output <<= 8; $output |= array_pop($input);} return $output; // output integer
}

Here is the breakdown of the code and my reasoning:


1. function Decode($input, $strTest = true, $allow = [":","-"]){

The function is set to accept one input with two optional parameters. The main input here is $input which accepts base64 encoded string, more on the other two later.

2. while(strlen($input) % 4) $input .= "=";
   $input =  strtr($input,'-_','+/');

This step will reverse the common base64 encoding tricks which would have made input string shorter and URL friendly. This can be skipped if dealing with pure / known good strings.

3. $input = unpack("C*",base64_decode($input));

There are two instructions here, base64_decode($input) will decode the input into a binary data stream; at the same time unpack("C*",...) will convert the binary data stream into an array of byte sized chunks.

4. if($strTest) {
    $strTest = $input; $output = ''; 
    while($strTest){$output .= chr(array_pop($strTest));}...}

This portion will check if string test is desired ($strTest = true within function input means that it will do this part by default / if omitted). The code than proceeds to initialize the variables (make the copy of input data: $strTest = $input; and initialize output string: $output = '';). Finally the code builds $output string ($output .= chr(array_pop($strTest));) as long as input data is available (while($strTest){...}).

5. if(ctype_alnum(str_replace($allow,'',$output))) return $output;

The actual test for a valid string consists of two parts: The first one will omit any acceptable (non-alpha numeric) characters specified by the optional $allow = [":","-"] function input. str_replace($allow ,'',$out) is the command that does the replacement; the output is fed into the ctype_alnum(...) test for alpha/numeric characters. If what is left is an alpha/numeric string, the unmodified $output string is than returned by the function.
Note: A more generic if(ctype_print($output)) return $output; test can be used in cases a wider (printable) character set is needed.

6. $output = 0; while($input)
    {$output<<= 8; $output|= array_pop($input);} return $output;

If we got to this last line, the input string has either failed the previous test or an input parameter $s as set to false. This last line than proceeds to assemble the $o input (made into an array of binary byte sized peaces earlier) into an integer. It starts by initializing $out=0; and than uses binary shift $output <<= 8; (this is the same as $output *= 256, but quicker) in order to make room for fresh data which is added to the $output variable: $output|= array_pop($input); (This is essentially same as $output += array_pop($input); but quicker due to binary operation instead of arithmetic). Finally $output is returned as a number suitable to represent either integer or binary value within further process(es).


Another version which can be forced to interpret as string (step 5 is simplified / omitted):

function DecodeType($in,$str = false){
    while(strlen($in) % 4) $in .= "="; $in =  strtr($in,'-_','+/'); // validate input
    $in = unpack("C*",base64_decode($in)); 
    if($str)
        {$out = ''; while($in){$out .= chr(array_pop($in));}
        return $out;} // output string in case $str was set to true
    $out = 0; while($in){$out <<= 8; $out |= array_pop($in);} return $out; // integer
}

Difference between the two functions (besides use of short-hand variable names within DecodeType) is that first function will test for strings:

Decode('AA==') returns 0
Decode('AQ==') returns 1
Decode('AAAaMA==') returns 807010304
Decode('AAA0IQ==') returns 557056000
Decode('Mjow') returns '0:2'
Decode('UzozLTIwNA==') returns '402-3:S'

while the second one can be forced:

DecodeType('AA==',false) returns 0
 vs DecodeType('AA==',true) returns ''
DecodeType('AQ==',false) returns 1
 vs DecodeType('AQ==',true) returns ''
DecodeType('AAAaMA==',false) returns 807010304
 vs DecodeType('AAAaMA==',true) returns '0'
DecodeType('AAA0IQ==',false) returns 557056000
 vs DecodeType('AAA0IQ==',true) returns '!4'
DecodeType('Mjow',false) returns 3160626
 vs DecodeType('Mjow',true) returns '0:2'
DecodeType('UzozLTIwNA==',false) returns 14689690853784147
 vs DecodeType('UzozLTIwNA==',true) returns '402-3:S'

Cheers

\$\endgroup\$
  • \$\begingroup\$ Welcome to Code Review! You have presented alternative solutions, but haven't reviewed the code. Please edit it to explain your reasoning (how your solution works and how it improves upon the original) so that everyone can learn from your thought process. \$\endgroup\$ – Toby Speight Feb 15 '18 at 16:45
  • \$\begingroup\$ This is brilliant. I would have preferred to see variable names that are more explicit than $s, $o, $a. \$\endgroup\$ – ryantxr Feb 15 '18 at 19:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.