Parse::Binary::Nested: byte-terminated groups
[wormy.git] / Parse / Binary / Nested.pm
index 75c22fa897eacaa3335cccec230722f6db603d02..6b74f648d4f879e8eb09cb4732029e7506b3cc31 100644 (file)
@@ -22,31 +22,63 @@ sub new {
 sub template {
        my ($self, $format) = @_;
        # total (flattened) unpack template from nested format definitions
-       return join '', map {
+       my $template = '';
+       @$format or return $template;
+       for (reverse 0 .. ($#$format - 1) >> 1) {
                my $value = $format->[-($_ << 1) - 1];
                if (ref $value eq 'ARRAY') {
                        my $count = $value->[0];
-                       $value = $self->template($value);
-                       $value = $count =~ s/^([*\d]+)// ? "$count($value)$1"
-                               : $count."X[$count]$count/($value)";
+                       if ($count =~ /^\?/) {
+                               $template .= 'a*';
+                               last;
+                       }
+                       else {
+                               $value = $self->template($value);
+                               $value = $count =~ s/^([*\d]+)// ? "$count($value)$1"
+                                       : $count."X[$count]$count/($value)";
+                       }
                }
                else {
                        $value =~ s/=(?:\d+|.)//g;  # hardcoded values
                        $value =~ s{^C/(a)(\d+)}{$1 . ($2 + 1)}e;  # maximum length
                }
-               $value;
-       } reverse 0 .. ($#$format - 1) >> 1;
+               $template .= $value;
+       }
+       return $template;
 }
 
 sub convert {
        my ($self, $format, $data, $pos) = @_;
        # map flat results into a named and nested hash
        my %res;
-       $pos ||= \(my $_pos);
+       $pos ||= \(my $_pos = 0);
        for (my $i = 0; $i < $#$format; $i += 2) {
                my ($field, $template) = @$format[$i, $i+1];
                if (ref $template eq 'ARRAY') {
                        my ($count, @subformat) = @$template;
+
+                       if ($count =~ /^\?(\d+)/) {
+                               # character-terminated group
+                               my $endmark = chr $1;
+                               my $iterate = ref($self)->new(\@subformat);
+                               push @{ $iterate->[0] }, -pos => '=.';
+                               my $subpos = 0;
+                               while ($subpos < length $data->[0]) {
+                                       last if substr($data->[0], $subpos, 1) eq $endmark;
+                                       my $iterdata = $iterate->convert($iterate->[0], [
+                                               unpack $iterate->[1], substr($data->[0], $subpos)
+                                       ]) or last;
+                                       $subpos += delete $iterdata->{-pos};
+                                       push @{ $res{$field} }, $iterdata;
+                               }
+                               $$pos += $subpos + 1;
+                               @$data = unpack(
+                                       $self->template([ @$format[$i+2 .. $#$format] ]),
+                                       substr($data->[0], $subpos + 1)
+                               ) if $subpos < length $data->[0];
+                               next;
+                       }
+
                        $$pos++ if $count eq 'C';
                        my $max = $count =~ s/^(\d+)// ? $1 : 0;
                        $count = !$count ? $max