#!/usr/bin/perl

# -------------------------------------
# Parse a usb config file copied from
# the output of UsbProber, a macintosh
# utility program.
# dvb08
# -------------------------------------

use Cwd;
use strict;


# -------------------------------------
# mp_new_opts(@ARGV)
#
# Given a list of arguments, return
# a hash where the keys and values
# are taken from those arguments of
# the form "--key=value". The hyphens
# disappear from the key name.
#
# A command line switch of "--key"
# is equivalent to "--key=_".
#
# a special key named _argc contains
# a count of non-dash-dash arguments,
# and they are in the hash as {0}, {1},
# and so on.
#
# |     in: array of arguments (such as @ARGV)
# |
# | return: reference to be accesed by mp_get_opt
# |
# | (no dispose routine needed)
# |

sub newOpts
  {
  my $arg;
  my $argVal;
  my $argc;
  my %hash;

  $argc = 0;


  while($arg = shift)
    {
    if($arg =~ /^--/)
      {
      if($arg =~ /^--(.*)\=(.*)$/)
        {
        $arg = $1;
        $argVal = $2;
        }
      elsif($arg=~ /^--(.*)$/)
        {
        $arg = $1;
        $argVal = "_";   # a true-but-zero value
        }

      $hash{$arg} = $argVal;
      }
    else
      {
      $hash{$argc++} = $arg;
      }
    }

  $hash{_argc} = $argc;

  return \%hash;
  }




# -------------------------------
# mp_get_opt(opts_ref, switch_name, default_value [, must_be_number])
#
# Look at a hash as returned by parseArgs, and
# give the value of the switch, or the default_value
# if it was not specified in the command line.
#
# |     in: opts_ref       -- reference from mp_new_opts
# |         switch_name    -- name of --option to check
# |         default_value  -- value to return if it wasnt in the args
# |         must_be_number -- optional boolean to force numeric interpretation
# |

sub getOpt
  {
  my $opts_ref = shift;
  my $switch_name = shift;
  my $default_value = shift;
  my $must_be_number = shift;

  my $switch_value;

  $switch_value = $$opts_ref{$switch_name};
  $switch_value = $default_value if ($switch_value eq "");
  $switch_value *= 1 if ($must_be_number);

  return $switch_value;
  }


# +-------------------------
# | mp_read_file(filename[,error_ref])
# |
# | returns the complete file contents
# |
# |
# |     in: filename -- name (path) of file to read
# |
# | return: one long string with the whole file, or
# |         empty string if no such file or empty file.
# |
# |         (use Perl's -e if you really really care whether
# |         the file was there...)
# |

sub readFile
  {
  my ($filename,$error_out) = (@_);
  my $bunch;
  my $file_contents;
  my $did;

  # (return error if ref provided)

  if($error_out)
    {
    $$error_out = (-f $filename) ? 0 : -1;
    }

  # (read the whole file, if present. else "")

  if(open(FILE,$filename))
    {
    binmode FILE;    # Bite me, Windows! --dvb
    while(read(FILE,$bunch,32000))
      {
      $file_contents .= $bunch;
      }
    close FILE;
    }

  return $file_contents;
  }

# -----------------------
# mp_write_file(filename,contents)
#
# creates new file and writes entire
# file contents. Return "ok" if so,
# or "" if not.
#
# |
# |     in: filename -- name of file to create (or replace)
# |         contents -- string of all the bytes to put into file
# |
# | return: 0 for success, <0 for error
# |

sub writeFile($$)
  {
  my ($filename,$contents) = (@_);

  my $did;


  #
  # If filename is "", print it to stdout
  # and that is all.
  #

  if($filename eq "")
    {
    print $contents;
    return "";
    }

  #
  # Delete existing file, if any.
  #

  unlink ($filename) if(-e $filename);

  $did = open(FILE,">$filename");
  if($did)
    {
    binmode FILE;    # Bite me, Windows! --dvb
    $did = print FILE $contents;
    close FILE;
    return $did ? 0 : -1;
    }

  return -1;
  }

sub dateTime
{
    my ($sec,$min,$hour,$mday,$mon,$year,$wday,$yday,$isdet) = localtime(time);
    $mon++;
    $year += 1900;
    my $d = sprintf("%04d.%02d.%02d",$year,$mon,$mday);
    my $t = sprintf("%02d:%02d:%02d",$hour,$min,$sec);

    return "$d $t";
}

sub E(@)
{
    print dateTime()," ",@_,"\n";
}

sub doSystem($)
{
    my ($cmd) = (@_);
    E("doing: $cmd");
    # danger and such.
    my $code = system($cmd);
    if($code != 0)
    {
        print "---------------------\n";
        print "ERROR: $code\n";
        print "doing:\n";
        print "  $cmd\n";
        print "---------------------\n";
    }
}

sub readBytes($)
{
    my @bytes;
    my ($filePath) = (@_);
    my $contents = readFile($filePath);
    E($contents);
    my @lines = split(/[\n\r]+/,$contents);
    for my $line (@lines)
    {
        if($line =~ /.*?: *(.*)/)
        {
            $line = $1;
        }
        my @numbers = split(/ +/,$line);
        for my $number (@numbers)
        {
            my $value = hex($number);
            push(@bytes,$value);
        }
    }
    return @bytes;
}

my $gClSuPr = ""; # GLOBAL most-recent class/subclass/protocol
my $gEp = ""; # GLOBAL current endpoint

sub print1FormattedDescriptor($$\@)
{
    my ($title,$valsRef,$bytesRef) = (@_);
    $title = uc($title);
    my $x = "";
    $x = "($gClSuPr) " if $gClSuPr;
    print "+------------------------------\n";
    print "| $x$title $gEp\n";

    print "|";
    foreach my $b (@$bytesRef)
    {
        printf(" %02x",$b);
    }
    print"\n";

    my $valsCount = scalar(@$valsRef);
    if($valsCount)
    {
        print "| ";
        for(my $i = 0; $i < $valsCount; $i += 2)
        {
            my $name = $$valsRef[$i];
            my $value = $$valsRef[$i + 1];
            my $padL = " " x (8 - length($name));
            my $padR = " " x (4 - length($value));
            print "$padL$name : $value$padR";
        }
        print "\n";
    }


    #print "+------------------------------\n";

}

sub printConfigurationDescriptor($$@)
{
    my ($length,$type,@bytes) = (@_);
    my $totalLength = $bytes[2] + $bytes[3] * 256;
    my @vals;
    push(@vals,"allLen",$totalLength,"interfs",$bytes[4],"id",$bytes[5],"str",$bytes[6]);
    push(@vals,"pwr",$bytes[8] * 2 . "mA");
    #printf ("configuration  total:%3d  interfaces:%3d  id:%3d  str:%3d\n",
        #$totalLength,
        #$bytes[4],
        #$bytes[5],
        #$bytes[6]);
    print1FormattedDescriptor("config",\@vals,@bytes);
}
sub printInterfaceDescriptor($$@)
{
    $gClSuPr = "";# reset class/subclass
    $gEp = "";
    my ($length,$type,@bytes) = (@_);
    my $interfaceNumber = $bytes[2];
    my $alt = $bytes[3];
    my $endpoints = $bytes[4];
    my $iClass = $bytes[5];
    my $iSubclass = $bytes[6];
    my $iProtocol = $bytes[7];
    my $str = $bytes[8];

    my @vals;
    push(@vals,
        "interfNum",$interfaceNumber,
        "alt",$alt,
        "endpoints",$endpoints,
        "type","$iClass/$iSubclass/$iProtocol",
        "str",$str);

    #printf("interface number:%3d  alt:%3d  endpoints:%3d  type:%d/%d/%d  str:%d\n",
            #$interfaceNumber,
            #$alt,
            #$endpoints,
            #$iClass, $iSubclass, $iProtocol,
            #$str);
    print1FormattedDescriptor("interface",\@vals,@bytes);

    $gClSuPr = sprintf("%d/%d/%d",$iClass,$iSubclass,$iProtocol);
}

sub printEndpointDescriptor($$@)
{
    $gEp = "";
    my ($length,$type,@bytes) = (@_);
    my $endpoint = $bytes[2];
    my $direction = $endpoint & 0x80 ? "IN " : "OUT";
    $endpoint = $endpoint & 0x0f;


    my $attrs = $bytes[3];
    my $type = ("CONTROL","ISOCHRONOUS","BULK","INTERRUPT")[$attrs & 0x03];
    my $sync = ("NONE","ASYNCH","ADAPTIVE","SYNCH")[($attrs >> 2) & 0x03];
    my $usage = ("DATA","FEEDBACK","IMPLICIT","?")[($attrs >> 4) & 0x03];

    my $maxPacketSize = $bytes[4] + $bytes[5] * 256;
    my $interval = $bytes[6];

    my $vals = [
            "endpoint","$endpoint $direction",
            "maxsize",$maxPacketSize,
            "interval",$interval,
            "settings","$type $sync $usage"
    ];
    print1FormattedDescriptor("endpoint",$vals,@bytes);
    $gEp = "$endpoint $direction";
}

sub printMSHeader($$@)
{
    my ($length,$type,@bytes) = (@_);
    my @vals;
    push (@vals,
        "version","$bytes[4].$bytes[3]",
        "length", $bytes[5] + $bytes[6] * 256
    );
    #printf("ms header version %d.%d length %d\n",
            #$bytes[4],$bytes[3],
            #$bytes[5] + $bytes[6] * 256);
    print1FormattedDescriptor("midi streaming header",\@vals,@bytes);
}

sub printMSMidiOutJack($$@)
{
    my ($length,$type,@bytes) = (@_);
    #printf("ms MIDI Out Jack type:%d jackid:%d pins:%d str:%d\n",
            #$bytes[3],
            #$bytes[4],
            #$bytes[5],
            #$bytes[$length - 1]);
    my @vals;
    push(@vals,
        "type",("unknown","embedded","external")[$bytes[3]],
        #"type",$bytes[3],
        "id",$bytes[4],
        "pins",$bytes[5]);

    my $w = 6;
    while($w < $length - 1)
    {
        push(@vals,"jackId",$bytes[$w],"1stPin",$bytes[$w + 1]);
       #printf("    entity id:%d first pin:%d\n",
            #$bytes[$w],
            #$bytes[$w + 1]);
       $w += 2;
    }
    push(@vals,"str",$bytes[$length - 1]);
    print1FormattedDescriptor("midi streaming out jack",\@vals,@bytes);

}

sub printMSMidiInJack($$@)
{
    my ($length,$type,@bytes) = (@_);
    #printf("ms MIDI In Jack type:%d jackid:%d str:%d \n",
            #$bytes[3],
            #$bytes[4],
            #$bytes[5]);
    my @vals;
    push(@vals,
        "type",("unknown","embedded","external")[$bytes[3]],
        "id",$bytes[4],
        "str",$bytes[5]);
    print1FormattedDescriptor("midi streaming in jack",\@vals,@bytes);
}

sub printACInterfaceHeader(@)
{
    my (@bytes) = (@_);
    my @vals;
    push(@vals,
        "version","$bytes[4].$bytes[3]",
        "allLen",$bytes[5] + $bytes[6] * 256,
        "interfs",$bytes[7],
        "list",join(",",@bytes[8 .. $#bytes])
    );
    print1FormattedDescriptor("audio control interface",\@vals,@bytes);
}

sub printCSInterfaceDescriptor($$@)
{
    my ($length,$type,@bytes) = (@_);
    my $subtype = $bytes[2];
    #printf("ms specific interface subtype %d [%d] \n",$subtype,scalar(@bytes));
    if($gClSuPr eq "1/3/0")
    {
        if($subtype == 1)
        {
            printMSHeader($length,$type,@bytes);
        }
        elsif($subtype == 2)
        {
            printMSMidiInJack($length,$type,@bytes);
        }
        elsif($subtype == 3)
        {
            printMSMidiOutJack($length,$type,@bytes);
        }
        else
        {
            print1FormattedDescriptor("unknown",[],@bytes);
        }
    }
    elsif($gClSuPr eq "1/1/0")
    {
        if($subtype == 1)
        {
            printACInterfaceHeader(@bytes);
        }
        else
        {
            print1FormattedDescriptor("audio control interface unknown",[],@bytes);
        }
    }
    else
    {
            print1FormattedDescriptor("unknown",[],@bytes);
    }
}


sub printCSEndpointDescriptor($$@)
{
# 1st arg is class/subclass/protocol, in hex 010301, for example
    my ($length,$type,@bytes) = (@_);
    my $subtype = $bytes[2];
    if($gClSuPr eq "1/3/0")
    {
        if($subtype == 1)
        {
            # ms general
            my @vals;
            push(@vals,
                "jacks",$bytes[3]);
            push(@vals, "jackIdList",join(",",@bytes[4 .. $#bytes]));
            print1FormattedDescriptor("MIDI STREAMING JACKS ENDPOINT",\@vals,@bytes);
        }
    }
    else
    {
        print1FormattedDescriptor("unknown class-specific endpoint",[],@bytes);
    }
}


sub print1Descriptor($@)
{
    my $offset = shift;
    my (@bytes) = (@_);

    my $length = $bytes[$offset];

    my @descriptorBytes = @bytes[$offset .. $offset + $length - 1];
    my $type = $descriptorBytes[1];

    if($type == 2)
    {
        printConfigurationDescriptor($length,$type,@descriptorBytes);
    }
    elsif($type == 4)
    {
        printInterfaceDescriptor($length,$type,@descriptorBytes);
    }
    elsif($type == 5)
    {
        printEndpointDescriptor($length,$type,@descriptorBytes);
    }
    elsif($type == 0x24)
    {
        # note -- this could be ANY class-specific endpoint descriptor,
        # but I'm all about MIDI today, so there you have it.
        printCSInterfaceDescriptor($length,$type,@descriptorBytes);
    }
    elsif($type == 0x25)
    {
        # note -- this could be ANY class-specific endpoint descriptor,
        # but I'm all about MIDI today, so there you have it.
        printCSEndpointDescriptor($length,$type,@descriptorBytes);
    }
    else
    {
        print1FormattedDescriptor("unknown",[],@bytes);
    }

    return $offset + $length;
}

sub main(@)
{
    my ($filePath) = (@_);
    print "\n\n\n";
E("Displaying USB Descriptors From: $filePath");
    print "\n\n";
    my @bytes = readBytes($filePath);
    #print @bytes;

    my $byteCount = scalar(@bytes);
    my $offset = 0;
    while($offset < $byteCount)
    {
        $offset = print1Descriptor($offset,@bytes);
    }

}

main(@ARGV);


