4
\$\begingroup\$

I was needing to provide the hex-dump of a code, but I needed to create my own. And, for fun, I decided to do it.

function hex_dump( $value )
{
    $start_time = microtime(true);

    switch( gettype( $value ) )
    {
        case 'string':

            $lines = array_map(
                    function( $line ){
                        return array_map(
                                function( $char ){
                                    return str_pad( dechex( ord( $char ) ), 2, 0, STR_PAD_LEFT );
                                },
                                str_split( $line )
                            );
                    },
                    str_split( $value, 16 )
                );
            break;

        case 'double':
        case 'integer':

            $lines = array(
                    array_map(
                        function( $digits ){
                            return str_pad( $digits, 2, 0, STR_PAD_LEFT );
                        },
                        str_split( dechex( $value ), 2 )
                    )
                );
            break;

        case 'array':

            $lines = 
                array_map(
                    function( $chunk ){
                        return array_map(
                                function( $item ){
                                    switch( gettype( $item ) )
                                    {
                                        case 'double':
                                        case 'integer':
                                            return str_pad( dechex( $item & 255 ), 2, 0, STR_PAD_LEFT );
                                        case 'string':
                                            return str_pad( dechex( ord( $item ) ), 2, 0, STR_PAD_LEFT );
                                        default:
                                            return '--';
                                    }
                                },
                                $chunk
                            );
                    },
                    array_chunk( $value, 16, false )
                );
            break;

        default:
            trigger_error( 'Invalid value type passed', E_USER_WARNING );
            return false;
    }

    $num_length = strlen( dechex( $line_count = count( $lines ) ) ) + 1;

    $header = str_repeat( ' ', $num_length = $num_length + ( $num_length % 2 ) ).
        ' |'.
        implode(
            '|',
            array_map(
                function( $number ){
                    return str_pad( strtoupper( dechex( $number ) ), 2, 0, STR_PAD_LEFT );
                },
                range( 0, 15 )
            )
        ).
        '|      TEXT      ';

    echo $header, PHP_EOL;

    $separator = str_repeat( '-', strlen( $header) );

    foreach( $lines as $current_line => &$line )
    {
        $line_lenth = count( $line );

        echo
            $separator,
            PHP_EOL,
            str_pad( strtoupper( dechex( $current_line ) ), $num_length - 1, 0, STR_PAD_LEFT ),
            '0 |',
            strtoupper(
                implode(
                    '|',
                    $line_lenth < 16
                        ?array_pad(
                            array_merge(
                                $line,
                                array_fill(0, 16 - $line_lenth, '  ')
                            ),
                            16,
                            null
                        )
                        :$line
                )
            ),
            '|',
            implode(
                '',
                array_map(
                    function( $value ){
                        if( $value == '--' )
                        {
                            return "\xBF";
                        }
                        else
                        {
                            $value =  hexdec( $value );

                            return $value < 32 || $value > 126 ? '.' : chr( $value );
                        }
                    },
                    $line
                )
            ),
            PHP_EOL;
    }

    $stats = array(
        'lines' => $line_count,
        'bytes' => $line_count ? ( $line_count * 16 ) - ( 16 - count( $lines[ $line_count - 1 ] ) ) : 0,
        'time' => microtime(true) - $start_time
    );

    echo 
        str_repeat( '=', strlen( $header) ),
        PHP_EOL,
        str_pad( 'Lines: ' . $stats['lines'], 15, ' '),
        '| ',
        str_pad( 'Bytes: ' . $stats['bytes'], 16, ' '),
        '| Time: ',
        $stats['time'],
        'ms',
        PHP_EOL,
        PHP_EOL;

    return $stats;
}

As you can see, it is a total and complete mess, even though it is logically splitted.

Since I use a lot of chained functions, I've tried to avoid to create long lines (100+ characters). But in the process, I made this mess!


It is really easy to use. Just pass a string or a number or an array of bytes, then it will make an ascii table with the dump.

For example, to dump a number:

hex_dump(12345);

And it outputs something like this:

   |00|01|02|03|04|05|06|07|08|09|0A|0B|0C|0D|0E|0F|      TEXT      
--------------------------------------------------------------------
00 |30|39|  |  |  |  |  |  |  |  |  |  |  |  |  |  |09
====================================================================
Lines: 1       | Bytes: 2        | Time: 5.2928924560547E-5ms

To dump a string:

hex_dump('A very cool string
that spans
across
multiple lines!!!');

Which outputs:

   |00|01|02|03|04|05|06|07|08|09|0A|0B|0C|0D|0E|0F|      TEXT      
--------------------------------------------------------------------
00 |41|20|76|65|72|79|20|63|6F|6F|6C|20|73|74|72|69|A very cool stri
--------------------------------------------------------------------
10 |6E|67|0D|0A|74|68|61|74|20|73|70|61|6E|73|0D|0A|ng..that spans..
--------------------------------------------------------------------
20 |61|63|72|6F|73|73|0D|0A|6D|75|6C|74|69|70|6C|65|across..multiple
--------------------------------------------------------------------
30 |20|6C|69|6E|65|73|21|21|21|  |  |  |  |  |  |  | lines!!!
====================================================================
Lines: 4       | Bytes: 57       | Time: 0.00011205673217773ms

* The newlineas are represented as \x0D\x0A, which is \r\n (Windows newlines).

And you can even mix both, in an array of bytes:

hex_dump(
    array(
        123,
        's',
        'v <-- only that will be dumped',
        0
    )
);

Which will produce:

   |00|01|02|03|04|05|06|07|08|09|0A|0B|0C|0D|0E|0F|      TEXT      
--------------------------------------------------------------------
00 |7B|73|76|00|  |  |  |  |  |  |  |  |  |  |  |  |{sv.
====================================================================
Lines: 1       | Bytes: 4        | Time: 5.9843063354492E-5ms

Considering the huge mess, how can I improve the readability of the code? Also, is this code DRY enough?

\$\endgroup\$

1 Answer 1

2
\$\begingroup\$

Converting to hex and outputting the hex dump are two separate concerns. You should have another function (named to_hex or something similar) that handles the actual converting. You might even want a third one for formatting if you want to truly separate concerns.


Once you have this to_hex function, the array case should call to_hex recursively with each element rather than repeating logic (you asked about DRY -- your current approach is very unDRY with regards to this). As a bonus, it will then automatically support nested arrays.


If you care (a lot) about performance, your string case is very non-optimal. str_split creates an array each time it's called which means that you're copying data for no real reason. If you're interested in performance, you should loop over the string in place (your array case has a similar flaw).

Really I doubt performance matters this much, but it seemed worth mentioning given the timing code you have :).


If unsupported types cause a false return, I would expect the array case to act similarly if it comes across an element of unhandled type.

Likewise, I would expect the array case to actually handle integers, not just the first byte of integers.


Don't be afraid to use intermediate variables. Some of your lines are just straight up beastly, especially the output parts. I also find it clearer to have multiple echo calls rather than stringing together super long echos:

echo ...;
echo ...;
echo ...;

vs

echo ..., ..., ...;

Functions should very rarely actually output. Instead, functions should return their values and the caller should output the return if desired. Imagine if you wanted to write one of these hex dumps to a file. Currently you'd have to do it with some nasty output buffering or stdout redirection (which then gets complicated if you want to have certain things actually echo out). It's a bit weird to think about it like this at first, but generating data and outputting data are two very different concerns and should thus not typically be handled by the same function. There are of course times when it's necessary or desirable for performance or usability reasons to directly output, but I don't think this is one of them.

\$\endgroup\$
5
  • \$\begingroup\$ You amde really good points, but there are a few points I need to say. 1) The oposite of DRY is WET (like when you spill water on your floor, it isn't dry anymore). 2) The array case is supposed to be only bytes. Afterall, it is a byte array that I'm expecting. Invalid values in an array are represented as -- and as "\xBF" in the text output. (I will add it to the question). 3) This wasn't thinking about performance, but it is a good point. 4) I hate using intermediate variables. It is just using memory for nothing. When that memory could be used for something else. (...) \$\endgroup\$ May 30, 2015 at 19:31
  • \$\begingroup\$ (...) 5) You are right, I should split the outputs and repeat it. That is an ugly micro-optimization that I'm used to do. 6) Function like var_dump() simply spit it out. That's exactly the point of that function. To spit the hex dump. It is a dump afterall. \$\endgroup\$ May 30, 2015 at 19:34
  • \$\begingroup\$ @IsmaelMiguel 1) I prefer unDRY or non-DRY to the forced, cutesy WET, but to each his own :). 2) A byte array of doubles seems really strange, so I assume it was meant to act as a recursive dump. This function could probably use some documentation to make expectations/behaviors like this clearer. 4) I would be shocked it makes more than a few tens of nanosecond difference (after all, the same temporaries are still being created, they're just not being given a name currently). Besides, the str_split is going to be far, far less efficient than a few variables if you're worried about performance \$\endgroup\$
    – Corbin
    May 30, 2015 at 19:37
  • \$\begingroup\$ @IsmaelMiguel 6) Good point. I wasn't thinking of it as a var_dump style tool. \$\endgroup\$
    – Corbin
    May 30, 2015 at 19:38
  • \$\begingroup\$ Actually, I'm not worried about performance. As I said, it is a micro-optimization I'm used to and I'm using it when I shouldn't. But I will improve it and make a new question with a follow-up, if needed. But what do you suggest instead of the split? \$\endgroup\$ May 30, 2015 at 19:40

Your Answer

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

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