| #! /usr/pkg/bin/perl -w |
| |
| # $Id: tme-binary-struct.pl.in,v 1.2 2005/01/14 11:40:50 fredette Exp $ |
| |
| # tools/tme-binary-struct.pl.in - common framework for scripts that |
| # manipulate files containing binary structures: |
| # |
| |
| # Copyright (c) 2004 Matt Fredette |
| # All rights reserved. |
| # |
| # Redistribution and use in source and binary forms, with or without |
| # modification, are permitted provided that the following conditions |
| # are met: |
| # 1. Redistributions of source code must retain the above copyright |
| # notice, this list of conditions and the following disclaimer. |
| # 2. Redistributions in binary form must reproduce the above copyright |
| # notice, this list of conditions and the following disclaimer in the |
| # documentation and/or other materials provided with the distribution. |
| # 3. All advertising materials mentioning features or use of this software |
| # must display the following acknowledgement: |
| # This product includes software developed by Matt Fredette. |
| # 4. The name of the author may not be used to endorse or promote products |
| # derived from this software without specific prior written permission. |
| # |
| # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR |
| # IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| # DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, |
| # INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR |
| # SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) |
| # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, |
| # STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN |
| # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE |
| # POSSIBILITY OF SUCH DAMAGE. |
| |
| # silence perl -w: |
| # |
| undef($bad); |
| undef($packed); |
| undef(%name_to_values); |
| |
| # globals: |
| # |
| $0 =~ /^(.*\/)?([^\/]+)$/; $PROG = $2; |
| |
| # check our command line: |
| # |
| $usage = 0; |
| $verbose = 0; |
| $all = 0; |
| undef($format_input); |
| undef($format_output); |
| for (; @ARGV > 0 && $ARGV[0] =~ /^-/; ) { |
| $option = shift(@ARGV); |
| if ($option eq '--verbose') { |
| $verbose++; |
| } |
| elsif ($option eq '--all') { |
| $all = 1; |
| } |
| elsif ($option =~ /^--format-input=(\S+)$/) { |
| $format_input = $1; |
| } |
| elsif ($option =~ /^--format-output=(\S+)$/) { |
| $format_output = $1; |
| } |
| else { |
| if ($option ne "-h" |
| && $option ne "--help" |
| && $option ne "-?") { |
| print STDERR "$PROG error: unknown option `$option'\n"; |
| } |
| $usage = 1; |
| last; |
| } |
| } |
| if (defined($format_input) |
| && $format_input ne 'text' |
| && $format_input ne 'binary') { |
| print STDERR "$PROG error: unknown input format $format_input\n"; |
| $usage = 1; |
| } |
| if (defined($format_output) |
| && $format_output ne 'text' |
| && $format_output ne 'binary') { |
| print STDERR "$PROG error: unknown input format $format_output\n"; |
| $usage = 1; |
| } |
| if (@ARGV > 0) { |
| print STDERR "$PROG error: `$ARGV[0]' unexpected\n"; |
| $usage = 1; |
| } |
| if ($usage) { |
| print STDERR <<"EOF;"; |
| usage: $PROG [ OPTIONS ] |
| where OPTIONS are: |
| --verbose include comments in text output |
| --all display normally hidden fields in text output |
| --format-input=FORMAT set the input format to FORMAT, one of: text binary |
| --format-output=FORMAT set the output format to FORMAT, one of: text binary |
| EOF; |
| exit (1); |
| } |
| |
| # the set of related types: |
| # |
| %types_related = split(/[\r\n\s]+/, <<'EOF;'); |
| generic_char_hex generic_integral |
| generic_char_dec generic_integral |
| generic_shorteb_hex generic_integral |
| generic_shorteb_dec generic_integral |
| generic_shortel_hex generic_integral |
| generic_shortel_dec generic_integral |
| generic_longeb_hex generic_integral |
| generic_longeb_dec generic_integral |
| generic_longel_hex generic_integral |
| generic_longel_dec generic_integral |
| EOF; |
| |
| # get the structure definition: |
| # |
| $struct_definition = &binary_struct(); |
| |
| # process the structure definition and make the default input: |
| # |
| $input_default = ""; |
| @comments = (""); |
| $comments_new = 0; |
| for ($line_start = 0; |
| $line_start < length($struct_definition); ) { |
| |
| # get the offset of the next line separator: |
| # |
| $line_end = index($struct_definition, "\n", $line_start); |
| if ($line_end < 0) { |
| $line_end = length($struct_definition) + 1; |
| } |
| |
| # get the next line: |
| # |
| $_ = substr($struct_definition, $line_start, $line_end - $line_start); |
| $line_start = $line_end + 1; |
| |
| # ignore comments and blank lines: |
| # |
| if ($_ !~ /\S/ || /^\s*\#/) { |
| if ($comments_new) { |
| push(@comments, ""); |
| $comments_new = 0; |
| } |
| $comments[$#comments] .= $_."\n"; |
| next; |
| } |
| |
| # tokenize this line: |
| # |
| ($offset, $name, $type, $values) = split(' ', $_, 4); |
| |
| # make sure this name isn't multiply-defined: |
| # |
| if (defined($name_to_offset{$name})) { |
| print STDERR "$PROG internal error: $name multiply defined\n"; |
| exit (1); |
| } |
| |
| # convert the offset: |
| # |
| $offset = hex($offset); |
| |
| # canonicalize the type and count: |
| # |
| if ($type =~ /^(.*\D)(\d+)$/) { |
| ($type, $count) = ($1, $2); |
| } |
| else { |
| $count = 1; |
| } |
| |
| # make sure this type is known: |
| # |
| $func = $types_related{$type}; |
| if (!defined($func)) { |
| $func = $type; |
| } |
| unless (eval("defined(\&type_${func}_pack);")) { |
| print STDERR "$PROG internal error: unknown type $func\n"; |
| exit (1); |
| } |
| |
| # remember this name: |
| # |
| push (@names, $name); |
| $name_to_offset{$name} = $offset; |
| $name_to_type{$name} = $type; |
| $name_to_count{$name} = $count; |
| $name_to_values{$name} = $values; |
| $name_to_func{$name} = $func; |
| $name_to_comments{$name} = $#comments; |
| |
| # get the default value for this field: |
| # |
| eval("(\$value) = \&type_${func}_values(\$type, \$count, \$values);"); |
| |
| # if the default value has an alias, use the alias: |
| # |
| if ($value =~ s/=([^=]+)$//) { |
| $value = $1; |
| } |
| |
| # add this value to the default input: |
| # |
| $input_default .= "$name $value\n"; |
| |
| # the next comment starts a new comment: |
| # |
| $comments_new = 1; |
| } |
| |
| # if our standard input is a terminal: |
| # |
| if (-t STDIN) { |
| |
| # if the user specified the input format, and it's not text, that's an error: |
| # |
| if (defined($format_input) |
| && $format_input ne 'text') { |
| print STDERR "$PROG error: the input format can't be $format_input when standard input is a terminal\n"; |
| exit (1); |
| } |
| $format_input = 'text'; |
| |
| # there is no standard input: |
| # |
| $input = ""; |
| } |
| |
| # otherwise, our standard input is not a terminal: |
| # |
| else { |
| |
| # read in standard input: |
| # |
| $input = ""; |
| for (;;) { |
| undef($_); |
| $size = sysread(STDIN, $_, 1024); |
| if (!defined($size)) { |
| print STDERR "fatal: could not read stdin: $!\n"; |
| exit (1); |
| } |
| elsif ($size == 0) { |
| last; |
| } |
| $input .= $_; |
| } |
| |
| # if we don't know if the input format is text or binary, try to |
| # figure it out: |
| # |
| if (!defined($format_input)) { |
| $format_input = ($input =~ /[\000-\011\013-\036]/ ? 'binary' : 'text'); |
| print STDERR "$PROG notice: input format is $format_input\n"; |
| } |
| } |
| |
| # if we don't know the output format, it's the opposite of the input format: |
| # |
| if (!defined($format_output)) { |
| $format_output = ($format_input eq 'text' ? 'binary' : 'text'); |
| print STDERR "$PROG notice: output format is $format_output\n"; |
| } |
| |
| # if the output format is binary, --verbose and --all don't make sense: |
| # |
| if ($format_output eq 'binary' |
| && ($verbose |
| || $all)) { |
| print STDERR "$PROG error: --verbose and --all don't make sense for binary output\n"; |
| exit (1); |
| } |
| |
| # if our input is text: |
| # |
| if ($format_input eq 'text') { |
| |
| # prepend the default input to the input, to provide values for |
| # any names that the user doesn't provide: |
| # |
| $input = $input_default."\n".$input; |
| |
| # process the lines of the input: |
| # |
| for ($line_start = 0; |
| $line_start < length($input); ) { |
| |
| # get the offset of the next line separator: |
| # |
| $line_end = index($input, "\n", $line_start); |
| if ($line_end < 0) { |
| $line_end = length($input) + 1; |
| } |
| |
| # get the next line: |
| # |
| $_ = substr($input, $line_start, $line_end - $line_start); |
| $line_start = $line_end + 1; |
| |
| # ignore comments and blank lines: |
| # |
| if ($_ !~ /\S/ || /^\s*\#/) { |
| next; |
| } |
| |
| # tokenize this line: |
| # |
| ($name, $value) = split(' ', $_, 2); |
| |
| # if this name is unknown: |
| # |
| if (!defined($name_to_offset{$name})) { |
| print STDERR "$PROG error: unknown name `$name'\n"; |
| exit (1); |
| } |
| |
| # save this value: |
| # |
| $name_to_value{$name} = $value; |
| } |
| } |
| |
| # otherwise, if our input is binary: |
| # |
| elsif ($format_input eq 'binary') { |
| |
| # extract values from the image: |
| # |
| foreach $name (@names) { |
| |
| # get this name's type, function, count, and offset: |
| # |
| $type = $name_to_type{$name}; |
| $func = $name_to_func{$name}; |
| $count = $name_to_count{$name}; |
| $offset = $name_to_offset{$name}; |
| |
| # unpack this value: |
| # |
| eval("\$value = \&type_${func}_unpack(\$type, \$count, substr(\$input, \$offset));"); |
| |
| # save this value: |
| # |
| $name_to_value{$name} = $value; |
| } |
| } |
| |
| # loop over the names: |
| # |
| $image = ""; |
| foreach $name (@names) { |
| |
| # get everything about this name: |
| # |
| $type = $name_to_type{$name}; |
| $func = $name_to_func{$name}; |
| $count = $name_to_count{$name}; |
| $offset = $name_to_offset{$name}; |
| $value = $name_to_value{$name}; |
| eval("\@values = \&type_${func}_values(\$type, \$count, \$name_to_values{\$name});"); |
| |
| # pack the possibilities and get any aliases: |
| # |
| @aliases = (); |
| @packeds = (); |
| undef($wild_alias); |
| foreach $_ (@values) { |
| |
| # strip any alias: |
| # |
| if (/^(.*)=([^=]+)$/) { |
| $_ = $1; |
| push (@aliases, $2); |
| } |
| else { |
| push (@aliases, ''); |
| } |
| |
| # if this is the wildcard: |
| # |
| if ($_ eq '*' |
| && $aliases[$#aliases] ne '') { |
| $wild_alias = $aliases[$#aliases]; |
| push(@packeds, ''); |
| } |
| |
| # otherwise, this is not the wildcard: |
| # |
| else { |
| |
| # this value must pack: |
| # |
| eval("(\$bad, \$packed) = \&type_${func}_pack(\$type, \$count, \$_);"); |
| if (defined($bad) |
| || !defined($packed)) { |
| print STDERR "$PROG internal error: bad value for $name ($_)\n"; |
| exit (1); |
| } |
| push (@packeds, $packed); |
| } |
| } |
| |
| # try to pack this value: |
| # |
| eval("(\$value_packed_bad, \$value_packed) = \&type_${func}_pack(\$type, \$count, \$value);"); |
| |
| # see if this value is on the list of possibilities, and is an |
| # alias or has an alias: |
| # |
| $value_ok = 0; |
| $value_alias = ''; |
| for ($value_i = 0; $value_i < @values; $value_i++) { |
| |
| # if this possibility has an alias, and the given value matches |
| # the alias, stop now: |
| # |
| if ($aliases[$value_i] ne '' |
| && $value eq $aliases[$value_i]) { |
| $value_ok = 1; |
| $value_alias = $aliases[$value_i]; |
| $value_packed = $packeds[$value_i]; |
| last; |
| } |
| |
| # if this value packed, and it matches this packed |
| # possibility, remember that this value is on the list of |
| # possibilities, and any alias: |
| # |
| if (!defined($value_packed_bad) |
| && $value_packed eq $packeds[$value_i]) { |
| $value_ok = 1; |
| $value_alias = $aliases[$value_i]; |
| } |
| } |
| |
| # if there is a list of possible values: |
| # |
| if (@values > 1) { |
| |
| # if this value isn't one of them: |
| # |
| if (!$value_ok) { |
| |
| # if the wildcard is accepted: |
| # |
| if ($wild_alias ne '') { |
| $value_alias = $wild_alias; |
| } |
| |
| # otherwise, complain: |
| # |
| else { |
| print STDERR "$PROG error: bad value `$value' for $name, must be one of:"; |
| for ($value_i = 0; $value_i < @values; $value_i++) { |
| print STDERR ' '.($aliases[$value_i] ne '' ? $aliases[$value_i] : $values[$value_i]); |
| } |
| if (defined($value_packed_bad)) { |
| print STDERR " (bad $value_packed_bad)"; |
| } |
| print STDERR "\n"; |
| exit (1); |
| } |
| } |
| } |
| |
| # otherwise, there isn't a list of possible values. if this value |
| # failed to pack: |
| # |
| elsif (defined($value_packed_bad)) { |
| print STDERR "$PROG error: bad value `$value' for $name\n"; |
| exit (1); |
| } |
| |
| # if our output is text: |
| # |
| if ($format_output eq 'text') { |
| |
| # display this variable if it's not normally hidden, or if |
| # we're displaying all variables: |
| # |
| if ($name !~ /^\./ || $all) { |
| |
| # if we're being verbose, display this variable's comment: |
| # |
| if ($verbose) { |
| print $comments[$name_to_comments{$name}]; |
| $comments[$name_to_comments{$name}] = ''; |
| } |
| |
| # display the variable and its alias or value: |
| # |
| print "$name ".($value_alias ne '' ? $value_alias : $value)."\n"; |
| } |
| } |
| |
| # otherwise, if our output is binary: |
| # |
| else { |
| |
| # add this packed value to the image: |
| # |
| if (length($image) < ($offset + length($value_packed))) { |
| $image .= pack('C', 0) x ($offset + length($value_packed) - length($image)); |
| } |
| substr($image, $offset, length($value_packed)) = $value_packed; |
| } |
| } |
| |
| # if our output is binary, output the image: |
| # |
| if ($format_output eq 'binary') { |
| print $image; |
| } |
| |
| # done: |
| # |
| exit(0); |
| |
| # this parses a set of integral values: |
| # |
| sub type_generic_integral_values { |
| my ($type, $count, $values) = @_; |
| if (!defined($values)) { |
| (''); |
| } |
| else { |
| split(' ', $values); |
| } |
| } |
| |
| # this returns the Perl pack template character for an integral type: |
| # |
| sub type_generic_integral_template { |
| my ($type) = @_; |
| |
| if ($type =~ /^generic_char_/) { |
| $type = 'C'; |
| } |
| elsif ($type =~ /^generic_shorteb_/) { |
| $type = 'n'; |
| } |
| elsif ($type =~ /^generic_longeb_/) { |
| $type = 'N'; |
| } |
| else { |
| print STDERR "$PROG fatal: unknown integral type $type\n"; |
| exit (1); |
| } |
| $type; |
| } |
| |
| # this packs an integral value: |
| # |
| sub type_generic_integral_pack { |
| my ($type, $count, $value) = @_; |
| my ($template, $bad, @parts); |
| |
| @parts = split(/,/, $value); |
| for (; @parts < $count; ) { push(@parts, '0'); } |
| foreach (@parts) { |
| if (/^0x[0-9A-Fa-f]+$/) { |
| $_ = hex($_) + 0; |
| } |
| elsif (/^\'(.)\'$/) { |
| $_ = ord($_) + 0; |
| } |
| elsif (/^\d+$/) { |
| $_ += 0; |
| } |
| else { |
| $bad = $_; |
| $_ = 0; |
| } |
| } |
| $template = &type_generic_integral_template($type); |
| ($bad, pack("$template$count", @parts)); |
| } |
| |
| # this unpacks an integral value: |
| # |
| sub type_generic_integral_unpack { |
| my ($type, $count, $packed) = @_; |
| my ($template, @parts); |
| |
| $template = &type_generic_integral_template($type); |
| @parts = unpack("$template$count", $packed); |
| for (; @parts > ($count > 1 ? 0 : 1) && $parts[$#parts] == 0; ) { pop(@parts); } |
| if ($type =~ /_hex$/) { |
| foreach (@parts) { |
| $_ = sprintf("0x%0".(length(pack($template, 0)) * 2)."x", $_); |
| } |
| } |
| else { |
| foreach (@parts) { |
| $_ = "$_"; |
| } |
| } |
| join(',', @parts); |
| } |
| |
| # this parses a set of generic string buffer values: |
| # |
| sub type_generic_string_buffer_values { |
| if (!defined($values)) { |
| $values = ''; |
| } |
| ($values); |
| } |
| |
| # this packs a generic string buffer value: |
| # |
| sub type_generic_string_buffer_pack { |
| my ($type, $count, $value) = @_; |
| my ($bad); |
| if (length($value) < $count) { |
| $value .= pack('C', 0) x ($count - length($value)); |
| } |
| elsif (length($value) > $count) { |
| $bad = $value; |
| } |
| ($bad, $value); |
| } |
| |
| # this unpacks a generic string buffer value: |
| # |
| sub type_generic_string_buffer_unpack { |
| my ($type, $count, $packed) = @_; |
| $lc = index($packed, pack('C', 0)); |
| if ($lc >= 0) { |
| $packed = substr($packed, 0, $lc); |
| } |
| $packed; |
| } |