Search code examples
perlpackunpack

How to use perl's pack function to reorder fields


I'm trying to reorder fields when building a string using pack, but I can't seem to get pack to do what I want. For example, I want to populate a string with abc at offset 12, defg at offset 8, and hi at offset 3 (and whatever, presumably space or \0, at offsets 0-2 and 5-7).

perl -e '
   use strict; use warnings;
   my $str = "...hi...defgabc";
   my $fmt = q{@12 a3 @8 a4 @3 a2};

   my @a = unpack $fmt, $str;
   print "<$_>\n" for @a;
   print "\n";

   print unpack("H*", pack($fmt, @a)), "\n";
'

This works fine for unpacking fields in any order out of a string. But for packing, it \0-fills and truncates as documented. Is there any way to stop it from \0-filling and truncating without reordering the pack template to produce the fields left-to-right?

This question comes up when reading a field specification from an external source. Of course it can be arranged for the pack template to be produced in left-to-right order and resulting list can be reordered to match the external field specification. But it would sure be handy to reposition the pack "cursor" dynamically without filling in intermediate positions or truncating.

In the above code, I would be happy if the return value of pack(...) was the same as $str with any byte for . (e.g. blank or \0).


Solution

  • Apparently there is no way for pack to do that directly. Here is one way of doing it, which avoids looping and using substr. However, compared with the easy comprehensibility of unpacking, it's not very satisfactory. I was hoping that I had misunderstood something in the pack documentation that would really allow pack to be the reverse of unpack for placement of fields within the packed string.

    use strict; use warnings;
    my $str = "...hi...defgabc";
    my @pos = (
       { pos => 12, len => 3 }, 
       { pos =>  8, len => 4 }, 
       { pos =>  3, len => 2 }, 
    );
    my $fmt = join " ", map { "\@$_->{pos} a$_->{len}" } @pos;
    # q{@12 a3 @8 a4 @3 a2};
    
    my @a = unpack $fmt, $str;
    print "<$_>\n" for @a;
    print "\n";
    
    my @sorted_idxes =
       sort { $pos[$a]{pos} <=> $pos[$b]{pos}
           or $pos[$a]{len} <=> $pos[$b]{len} }
       0..$#pos;
    
    my $sorted_fmt = join " ", 
       map { "\@$pos[$_]->{pos} a$pos[$_]->{len}" } @sorted_idxes;
    
    my $out = pack $sorted_fmt, @a[@sorted_idxes];
    $out =~ s/\0/./g;
    print "$out\n";