#!/usr/bin/perl
#
# $Id$
#
# This script is only of interest for developers who want to analyze
# USB snoopy dumps of canon camera communication made with USB snoopy 1.8
# (http://benoit.papillault.free.fr/usbsnoop/). This script will *not*
# work with output of earlier versions (e.g. 0.13 aka 1.3).
#
# NOTE: All "interesting" URB's generate a line ending with "***", so the
# command "urb-interpret foo.LOG | grep '\*\*\*$'" will generate a summary
# of the log.
#
use strict;
use warnings;
my %canon_cmd = (
"0x01 0x11 0x202" => "Get picture/thumbnail",
"0x01 0x12 0x201" => "Identify camera",
"0x03 0x12 0x201" => "Get time",
"0x03 0x11 0x403" => "Upload data block",
"0x04 0x12 0x201" => "Set time",
"0x05 0x11 0x201" => "Make directory",
"0x05 0x12 0x201" => "Change owner (old) or get owner (new)",
"0x06 0x11 0x201" => "Remove directory",
"0x06 0x12 0x201" => "Change owner (new)",
"0x09 0x11 0x201" => "Disk info request",
"0x0a 0x11 0x202" => "Flash device ID",
"0x0a 0x11 0x201" => "Delete file (new)",
"0x0a 0x12 0x201" => "Power supply status",
"0x0b 0x11 0x202" => "Get directory",
"0x0d 0x11 0x201" => "Delete file (old) or Disk info request (new)",
"0x0e 0x11 0x201" => "Set file attribute",
"0x0e 0x11 0x202" => "Flash Device ID (new)",
"0x0f 0x11 0x201" => "Set file time",
"0x13 0x12 0x201" => "Remote Camera control",
"0x17 0x12 0x202" => "Download captured image",
"0x18 0x12 0x202" => "Download captured preview",
"0x1a 0x12 0x201" => "Unknown EOS D30 command",
"0x1b 0x12 0x201" => "EOS lock keys",
"0x1c 0x12 0x201" => "EOS unlock keys",
"0x1d 0x12 0x201" => "EOS get body ID (old) or Get Custom Functions (new)",
"0x1f 0x12 0x201" => "Get camera abilities",
"0x20 0x12 0x201" => "Lock keys, turn off LCD",
"0x21 0x12 0x201" => "Unknown newer command",
"0x22 0x12 0x201" => "Unknown newer command",
"0x23 0x12 0x201" => "Get EOS Body ID (new)",
"0x24 0x12 0x201" => "Get camera abilities (new)",
"0x25 0x12 0x201" => "Remote Camera control (new)",
"0x26 0x12 0x202" => "Get captured image (new)",
"0x35 0x12 0x201" => "Unknown newer command",
"0x36 0x12 0x201" => "Unknown newer command"
);
my %control_subcmd = ( "0x00" => "Camera control init",
"0x01" => "Exit release control",
"0x02" => "Start viewfinder",
"0x03" => "Stop viewfinder",
"0x04" => "Release shutter",
"0x07" => "Set release params",
"0x09" => "Set transfer mode",
"0x0a" => "Get release params",
"0x0b" => "Get zoom position",
"0x0c" => "Set zoom position",
"0x0d" => "Get available shot",
"0x0e" => "Set custom func.",
"0x0f" => "Get custom func.",
"0x10" => "Get extended release params size",
"0x11" => "Get extended params version",
"0x12" => "Get extended release params",
"0x13" => "Set extended params",
"0x14" => "Select camera output",
"0x15" => "Do AE, AF, and AWB"
);
my %intcode = ( "00" => "unknown 00",
"01" => "unknown 01",
"02" => "unknown 02",
"05" => "unknown 05",
"08" => "Thumbnail descriptor",
"0a" => "Capture status",
"0c" => "Image descriptor",
"0e" => "EOS flash write success",
"0f" => "Capture complete",
"20" => "initialization"
);
my ($file, %urb, %urbdata, %urbpipe);
if ($#ARGV == -1) {
warn("Syntax: $0 file [file ...]\n");
exit(1);
}
foreach $file (@ARGV) {
open(FILE, "< $file") or die("Could not open file '$file' for reading: $!\n");
my $line;
my $byte_offset;
my $dumping_urbdata = 0;
my $dumping_setup = 0;
my $current_urb = -1;
my $current_function_type;
my $buffer = 0;
my $direction;
my $current_lines;
my $seq;
my $timestamp;
warn("FILE: $file\n");
while ($line = <FILE>) {
chomp($line);
$line =~ s/\r//; # Trim possible <CR> (from Windows)
if ($line =~ />>>\s+URB (\d+) going down/) {
$current_urb = $1;
#warn("URB $current_urb down...\n");
$direction = "down";
# There may be a previous URB under way that was truncated.
$dumping_urbdata = 0;
$dumping_setup = 0;
} elsif ($line =~ /<<<\s+URB (\d+) coming back/) {
$current_urb = $1;
#warn("URB $current_urb read\n");
$direction = "back";
# There may be a previous URB under way that was truncated.
$dumping_urbdata = 0;
$dumping_setup = 0;
} elsif ( $line =~ /Diag Driver: Receive/
|| $line =~ /isEchoRequest/
|| $line =~ /isWakeUpRequest/
|| $line =~ /filter/ ) {
next;
} elsif ( $line =~ /^\[\d+ ms\] UsbSnoop/ ) {
$dumping_urbdata = 0;
$dumping_setup = 0;
next;
} elsif ( $line =~ /^\d+\s+\d+\.\d+\s+UrbLink/ ) {
$dumping_urbdata = 0;
$dumping_setup = 0;
next;
} else {
if ($current_urb > 0) {
if ( $line =~ /^(\d+)\s+(\d\d:\d\d:\d\d [AP]M)\s+0000:\s+/
|| $line =~ /^(\d+)\s+(\d+\.\d+)\s+0000:\s+/
|| $line =~ /^\s+00000000:\s+/ ) {
if ( !($dumping_setup) ) {
#warn ( "dumping URB data\n" );
$dumping_urbdata = 1;
$current_lines = 0;
$byte_offset = "0000";
# start of urbdata, decide which buffer this is
if ($urbdata{$current_urb}{buffer0} ) {
$buffer = "buffer1";
} else {
$buffer = "buffer0";
}
#warn("URB $current_urb $buffer\n");
# mark that the buffer goes here
push(@{$urb{$current_urb}}, "[buffer $buffer]");
} else {
$current_lines = 0;
$byte_offset = "0000";
$buffer = "setup";
#warn("URB $current_urb setup info\n");
# mark that the buffer goes here
push(@{$urb{$current_urb}}, "[buffer setup]");
}
}
if ($dumping_urbdata) {
# Within data buffer
my $hex = "";
if ($line =~ /^(\d+)\s+(\d\d:\d\d:\d\d [AP]M)\s+([\da-f]+):((\s[\da-f][\da-f])+)\s+$/
|| $line =~ /^(\d+)\s+(\d+\.\d+)\s+([\da-f]+):((\s[\da-f][\da-f])+)\s+$/
|| $line =~ /^\s+([\da-f]+):((\s[\da-f][\da-f])+)\s+$/ ) {
$seq = $1;
$timestamp = $2;
$byte_offset = $3;
$hex = $4;
#warn ( "case 1: row $byte_offset of URB $current_urb: seq $seq time $timestamp data $hex\n" );
} elsif ( $line =~ /^\s+([\da-f]+):((\s[\da-f][\da-f])+)\s*$/ ) {
$byte_offset = $1;
$hex = $2;
#warn ( "case 2: row $byte_offset of URB $current_urb\n" );
} elsif ( $line =~ /^(\d)+\s+(\d+\.\d+)\s+[a-f]:\s+$/ ) {
$byte_offset = $3;
$hex = $4;
#warn ( "case 3: row $byte_offset of URB $current_urb\n" );
} else {
#warn ( "case 4: not in data URB $current_urb\n" );
if ($line =~ /^(\d+)\s+(\d\d:\d\d:\d\d [AP]M)\s+([\da-f]+):\s+$/
|| $line =~ /^(\d+)\s+(\d+\.\d+)\s+([\da-f]+):\s+$/ ) {
# every other line is this kind of line, data offset
$byte_offset = $3;
next;
} elsif ( $line =~ /^\s+([a-f0-9]+):(( [a-f0-9][a-f0-9])+)$/ ) {
$hex = $2;
} else {
if ( $line =~ /UrbLink/
|| $line =~ /^(\d+)\s+(\d+\.\d+)\s*$/ ) {
# this is not a urbdata line. reset.
$dumping_urbdata = 0;
next; # unimportant line, just go to next
} else {
warn("$file:$. Unknown data '$line' when dumping URB data\n");
next;
}
}
}
if ($hex) {
if ( $urbdata{$current_urb}{op} eq "WRITE" ) {
# If this is an OUT transaction, try to decode
# Canon command codes. These will be in two lines
# of the buffer dump: the first, starting at offset
# 0000:; and the fifth, starting at offset 0040:
if (hex($byte_offset) == 0x00) {
my @h = split(" ", $hex);
if ( $#h < 8 ) {
warn ( "URB $current_urb has digits missing from cmd3: probable truncated packet\n" );
$urbdata{$current_urb}{cmd3} = "BAD";
}
else {
$urbdata{$current_urb}{cmd3} = sprintf("0x%02x", hex("$h[7]$h[6]$h[5]$h[4]"));
}
} elsif (hex($byte_offset) == 0x40) {
my @h = split(" ", $hex);
if ( defined($h[4]) ) {
$urbdata{$current_urb}{cmd1} = "0x$h[4]";
}
if ( defined($h[7]) ) {
$urbdata{$current_urb}{cmd2} = "0x$h[7]";
}
} elsif (hex($byte_offset) == 0x50) {
if ( defined ( $urbdata{$current_urb}{cmd1} )
&& defined ( $urbdata{$current_urb}{cmd2} ) ) {
if ( ( $urbdata{$current_urb}{cmd1} eq "0x13"
&& $urbdata{$current_urb}{cmd2} eq "0x12")
|| ( $urbdata{$current_urb}{cmd1} eq "0x25"
&& $urbdata{$current_urb}{cmd2} eq "0x12") ) {
my @h = split(" ", $hex);
$urbdata{$current_urb}{subcmd} = "0x$h[0]";
}
}
}
} elsif ($urbdata{$current_urb}{pipe} eq "2" && hex($byte_offset) == 0x00 ) {
# An interrupt read; let's save the data for later decoding
$urbdata{$current_urb}{intdata} = $hex;
} elsif ($urbdata{$current_urb}{op} eq "READ" && hex($byte_offset) == 0x40 ) {
my @h = split(" ", $hex);
if ( defined($h[0x4]) && defined($h[0x5])
&& defined($h[0x6]) && defined($h[0x7]) ) {
$urbdata{$current_urb}{usbid} = "(0x$h[0x7]$h[0x6],0x$h[0x5]$h[0x4])";
$urbdata{$current_urb}{magic} = "$h[0x3]$h[0x2]$h[0x1]$h[0x0]";
}
}
if ( $current_lines < 65 ) {
push(@{ $urbdata{$current_urb}{ $buffer } }, hex_format($byte_offset, $hex));
}
$current_lines++;
$byte_offset = -1;
next;
}
} elsif ( $dumping_setup ) {
my $hex = "";
if ($line =~ /^(\d+)\s+(\d\d:\d\d:\d\d [AP]M)\s+([\da-f]+):((\s[\da-f][\da-f])+)\s+$/
|| $line =~ /^(\d+)\s+(\d+\.\d+)\s+([\da-f]+):((\s[\da-f][\da-f])+)\s+$/ ) {
$byte_offset = $3;
$hex = $4;
} elsif ( $line =~ /^\s+([\da-f]+):((\s[\da-f][\da-f])+)\s*$/ ) {
$byte_offset = $1;
$hex = $2;
} else {
warn("$file:$. Unknown data '$line' when dumping URB setup data\n");
next;
}
if ($hex) {
# Store this for later dumping.
if ( $current_lines < 65 ) {
push(@{ $urbdata{$current_urb}{setup} }, hex_format($byte_offset, $hex));
}
$current_lines++;
}
next;
} else {
# Not within data buffer; parse other info
if ($line =~ /\sInterface\[0\]: Pipes\[(\d+)\] : PipeHandle\s+= 0x([\da-f]+)/) {
# remember URB pipe
$urbpipe{$2} = $1;
warn("PIPE $2 = $1\n");
print ( "Pipe $2 = $1" );
if ( $1 == 0 ) {
print ( " (main)\n" );
} elsif ( $1 == 2 ) {
print ( " (interrupt)\n" );
} else {
print ( "\n" );
}
} elsif ($line =~ /\sPipeHandle\s+= ([\da-f]+)/) {
# check if we know this URB pipe
if ( defined ( $urbpipe{$1} ) ) {
$urbdata{$current_urb}{pipe} = $urbpipe{$1};
} else {
if ( $current_function_type =~ "CONTROL_TRANSFER" ) {
$urbpipe{control} = $1;
$urbdata{$current_urb}{pipe} = "<control>";
} else {
#warn ( "Unknown pipe $1 from line $line" );
$urbdata{$current_urb}{pipe} = "UNKNOWN";
}
}
#warn("URB $current_urb PIPE $1 = $urbdata{$current_urb}{pipe}\n");
} elsif ( $line =~ /SetupPacket\s+=\s*/ ) {
#warn ( "found setup packet for URB $current_urb" );
$dumping_urbdata = 0;
$dumping_setup = 1;
next;
}
if ( $line =~ /^--\s+URB_FUNCTION_([A-Z_]+):/
|| $line =~ /^\d+\s+\d+\.\d+\s+--\s+URB_FUNCTION_([A-Z_]+):/
|| $line =~ /^\d+\s+\d\d:\d\d:\d\d [AP]M\s+--\s+URB_FUNCTION_([A-Z_]+):/ ) {
# Informing us of transfer type. Seven kinds have been seen.
$current_function_type = $1;
#warn ( "URB $current_urb is of function type $current_function_type" );
if ( $current_function_type =~ "GET_DESCRIPTOR_FROM_DEVICE" ) {
# This is a read request, but lacks the "TransferFlags"
# keyword.
$urbdata{$current_urb}{op} = "READ";
}
}
elsif ($line =~ /.*\sTransferFlags.+USBD_TRANSFER_DIRECTION_(OUT|IN)/) {
# OP in "coming back" is most reliable, and that one
# is always after "going down" so we don't need to
# check previous value here, just overwrite
if ($1 eq "OUT") {
$urbdata{$current_urb}{op} = "WRITE";
} elsif ($1 eq "IN") {
$urbdata{$current_urb}{op} = "READ";
} else {
warn ( "Invalid transfer direction \"$1\"\n" );
}
}
if ( $line =~ /\s+TransferBufferLength\s+=\s+([\da-f]+)/ ) {
my $t = hex($1);
if ( !defined($urbdata{$current_urb}{op}) ) {
warn ( "URB $current_urb length=$t, but don't know type\n" );
}
elsif ($urbdata{$current_urb}{op} eq "READ") {
# ok, this is read operation. info is
# always good.
$urbdata{$current_urb}{bytes} = $t;
#warn ( "URB $current_urb read $t\n" );
} elsif ($urbdata{$current_urb}{op} eq "WRITE") {
if ($direction eq "down") {
$urbdata{$current_urb}{bytes} = $t;
#warn ( "URB $current_urb $urbdata{$current_urb}{op} $urbdata{$current_urb}{bytes}\n" );
}
# else, this information is unreliable
}
}
}
}
}
if ($line =~ /^(\d+) ([\w\d:\.]+?) (.+?)\s*$/) {
# we want to trim down the line numbers and time
my $lineno = $1;
my $time = $2;
my $rest = $3;
$line = sprintf("%.5d %.3f %s", $lineno, $time, $rest);
} elsif ( $line =~ /^(\d+)\s+(\d+\.\d+)\s+(.+?)\s*$/ ) {
# we want to trim down the line numbers and time
my $lineno = $1;
my $time = $2;
my $rest = $3;
$line = sprintf("%.5d %.3f %s", $lineno, $time, $rest);
} elsif ( $line =~ /^\[(\d+) ms\] (.+?)\s*$/ ) {
my $time = $1;
my $rest = $3;
} else {
# replace tabs with two spaces to make lines shorter
$line =~ s/ / /g;
}
if ($current_urb == -1) {
print("$line\n");
} else {
push(@{$urb{$current_urb}}, $line);
}
}
close(FILE);
warn("\noutputting formatted data\n");
my $n_skipped = 0;
foreach $current_urb (sort numerically keys %urb) {
# these are declared for readability
my $cmd1 = $urbdata{$current_urb}{cmd1};
my $cmd2 = $urbdata{$current_urb}{cmd2};
my $cmd3 = $urbdata{$current_urb}{cmd3};
my $pipe = $urbdata{$current_urb}{pipe};
my $op = $urbdata{$current_urb}{op};
my $length = $urbdata{$current_urb}{bytes};
my $keep_alive = 0;
if ( defined($op) ) {
if ( !defined($length) ) {
warn ( "Missing operation length in URB $current_urb\n" );
} elsif ( defined($pipe) ) {
if ( $pipe eq "2" && $length eq "0") {
$n_skipped++;
next;
}
}
if ( $n_skipped ) {
print ( "\n*** Skipped $n_skipped null interrupt reads ***\n" );
$n_skipped = 0;
}
if ($op eq "READ") {
if ( defined($length) ) {
if ( $pipe eq "2" ) {
my $hex = "";
if ( defined ( $urbdata{$current_urb}{intdata} ) ) {
$hex = $urbdata{$current_urb}{intdata};
my @h = split ( " ", $hex );
my $code = $h[4];
if ( defined ( $intcode{$code} ) ) {
if ( hex($code) == 0x0a ) {
# Capture status: there's a subcode that tells us more.
my $subcode = hex($h[0x0c]);
if ( $subcode == 0x1c ) {
printf ( "\n*** URB $current_urb interrupt read 0x%02x bytes: capture start ***\n\n", $length );
}
elsif ( $subcode == 0x1d ) {
printf ( "\n*** URB $current_urb interrupt read 0x%02x bytes: capture finish ***\n\n", $length );
}
elsif ( $subcode == 0x1b ) {
printf ( "\n*** URB $current_urb interrupt read 0x%02x bytes: flash wait ***\n\n", $length );
}
elsif ( $subcode == 0x0a ) {
printf ( "\n*** URB $current_urb interrupt read 0x%02x bytes: photographic failure ***\n\n", $length );
}
} else {
printf ( "\n*** URB $current_urb interrupt read 0x%02x bytes: $intcode{$code} ***\n\n", $length );
}
} else {
printf ( "\n*** URB $current_urb interrupt read 0x%02x bytes: unknown type $code ***\n\n", $length );
}
} elsif ($length == 0x40) {
print("\n\n\n\n*** URB $current_urb possible CANON RESPONSE 0x%x bytes ***\n", $length);
} elsif ($length == 0x01) {
print("\n\n\n\n*** URB $current_urb probably CAMERA INIT ***\n\n");
} elsif ($length == 0x58 && defined ($urbdata{$current_urb}{usbid}) ) {
print("\n\n\n\n*** URB $current_urb probably camera initial response, USB ID=$urbdata{$current_urb}{usbid}, magic=$urbdata{$current_urb}{magic} ***\n\n");
} else {
print ( "\n*** URB $current_urb interrupt read, but data are missing from log file ***\n\n" );
}
} elsif ($length == 0x58 && defined ($urbdata{$current_urb}{usbid}) ) {
if ( $urbdata{$current_urb}{magic} == 0x00000002 ) {
print("\n\n\n\n*** URB $current_urb possible CANON RESPONSE 0x%x bytes ***\n", $length);
} else {
print("\n\n\n\n*** URB $current_urb probably camera initial response, USB ID=$urbdata{$current_urb}{usbid}, magic=$urbdata{$current_urb}{magic} ***\n\n");
}
} elsif ($length == 0x01) {
print("\n\n\n\n*** URB $current_urb probably CAMERA INIT ***\n\n");
} elsif ($length >= 0x40) {
printf("\n\n\n\n*** URB $current_urb possible CANON RESPONSE 0x%x bytes ***\n", $length);
} else {
print("\n");
}
} else {
print("\n");
}
} elsif ($op eq "WRITE") {
if ( defined($length) ) {
my $cmd;
if ( defined($urbdata{$current_urb}{cmd1}) ) {
$cmd = "$urbdata{$current_urb}{cmd1}";
} else {
$cmd = "LOST";
}
if ( defined($urbdata{$current_urb}{cmd2}) ) {
$cmd = "$cmd $urbdata{$current_urb}{cmd2}";
} else {
$cmd = "$cmd LOST";
}
if ( defined($urbdata{$current_urb}{cmd3}) ) {
$cmd = "$cmd $urbdata{$current_urb}{cmd3}";
} else {
$cmd = "$cmd LOST";
}
if ( defined ( $length ) ) {
if ($length >= 0x50) {
if ( $cmd eq "LOST LOST LOST" ) {
warn ( "Command string is $cmd at URB $current_urb\n" );
}
elsif ($canon_cmd{$cmd}) {
if ( $cmd eq "0x13 0x12 0x201"
|| $cmd eq "0x25 0x12 0x201" ) {
if ( defined($urbdata{$current_urb}{subcmd}) ) {
printf("\n\n\n\n*** URB $current_urb CANON COMMAND '$canon_cmd{$cmd}' ($cmd) 0x%02x bytes", $length);
if ( $control_subcmd{$urbdata{$current_urb}{subcmd}} ) {
print ( ": $control_subcmd{$urbdata{$current_urb}{subcmd}} ***\n\n" );
} else {
print ( ". Unknown subcommand: $urbdata{$current_urb}{subcmd} ***\n\n" );
}
} else {
# Special case for newer (e.g. EOS 20D) protocol
printf("\n\n\n\n*** URB $current_urb CANON COMMAND 'Get Power Status (newer)' ($cmd) 0x%02x bytes ***\n\n", $length);
}
} else {
printf("\n\n\n\n*** URB $current_urb CANON COMMAND '$canon_cmd{$cmd}' ($cmd) 0x%02x bytes ***\n\n", $length);
}
} elsif ($cmd3 eq "0x201" or $cmd3 eq "0x202") {
printf("\n\n\n\n*** URB $current_urb PROBABLE CANON COMMAND ($cmd) 0x%02x bytes ***\n\n", $length);
} else {
printf("\n\n\n\n*** URB $current_urb possible CANON COMMAND ($cmd) 0x%02x bytes ***\n\n", $length);
}
} elsif ($cmd3 eq "0x201" or $cmd3 eq "0x202") {
if ($canon_cmd{$cmd}) {
printf("\n\n\n\n*** URB $current_urb CANON COMMAND '$canon_cmd{$cmd}' ($cmd) 0x%02x bytes ***\n\n", $length);
} else {
printf("\n\n\n\n*** URB $current_urb PROBABLE CANON COMMAND ($cmd) 0x%02x bytes ***\n\n", $length);
}
} else {
printf("\n\n\n\n*** URB $current_urb possible CANON COMMAND ($cmd) 0x%02x bytes ***\n\n", $length);
}
} elsif ($cmd3 eq "0x201" or $cmd3 eq "0x202") {
if ($canon_cmd{$cmd}) {
printf("\n\n\n\n*** URB $current_urb CANON COMMAND '$canon_cmd{$cmd}' ($cmd) 0x%02x bytes ***\n\n", $length);
} else {
printf("\n\n\n\n*** URB $current_urb PROBABLE CANON COMMAND ($cmd) 0x%02x bytes ***\n\n", $length);
}
}
} else {
warn ( "URB $current_urb write of unknown length" );
print ( "\n\n\n\n*** URB $current_urb write of unknown length ***\n\n" );
}
}
if ( defined($length) ) {
printf("%s 0x%x BYTES\n", $op, $length);
} else {
printf( "%s *unknown* BYTES\n", $op );
}
} else {
print ( "*** URB $current_urb has missing type info ***\n" );
}
if ($urbdata{$current_urb}{pipe}) {
print("*** URB $current_urb PIPE $urbdata{$current_urb}{pipe}\n");
} else {
print("*** URB $current_urb\n");
}
foreach $line (@{$urb{$current_urb}}) {
if ($line =~ /^\[buffer (buffer\d)\]$/) {
my $b = $1;
if ($b eq "buffer0" and $urbdata{$current_urb}{buffer1}) {
# this is buffer 0 but this URB has two buffers,
# this one is FOO (buffer contents before read typically)
# so we just skip it
print("[cut]\n");
next;
}
print("DATA:\n",
join("\n", @{ $urbdata{$current_urb}{$b} }),
"\n");
} elsif ( $line =~ /^\[buffer setup\]$/) {
print("SETUP:\n",
join("\n", @{ $urbdata{$current_urb}{setup} }),
"\n");
} else {
print("$line\n");
}
}
}
}
warn("\nDone\n\n");
exit(0);
# SUBROUTINES
# -----------
sub numerically { $a <=> $b };
sub hex_format
{
my $byte_offset = shift;
my $hex = shift;
$hex =~ s/^\s*(.+?)\s*$/$1/; # trim
my @chars = split(" ", $hex);
my $out_rad = "$byte_offset $hex ";
# if not a full line, add whitespace
if ($#chars < 15) {
$out_rad .= " " x ((15 - $#chars) * 3);
}
# add ASCII
my $ch;
foreach $ch (@chars) {
my $i = hex($ch);
if ($i >= 32 and $i < 127) {
$out_rad .= sprintf("%c", $i);
} else {
$out_rad .= ".";
}
}
return $out_rad;
}