バイナリファイルを解析する

Perlといえばテキスト処理や正規表現が得意で、バイナリを扱うような話についてはあまり聞かない印象があります。Perlが持つ関数pack/unpack等でもバイナリ処理は可能ですが、今回はData::ParseBinaryを使ってバイナリファイルを気軽に解析してみましょう。

基本

ファイルからストリームを作る

解析したいファイルをData::ParseBinaryで扱えるストリームに変換します。

use Data::ParseBinary;
my $stream = CreateStreamReader(File => $file_handle);

解析したい構造を定義する

Struct関数で解析したい構造を定義します。Struct以下には基本データ型やコンテナ型、ビット/バイトパディング型、制御構文型等を使用できます。各型に指定したラベルが解析結果として得られるハッシュのキーとなります。

my $your_data_structure = Struct('YOUR_DATA_STRUCTURE',
    UBInt8('length'),
    Array(sub { $_->ctx->{length}}, UBInt8('data')),
);

定義した構造を元にストリームを解析する

解析したい構造に対してストリームを渡して解析を開始します。解析結果はハッシュとして返されます。

my $data = $your_data_structure->parse($stream);
#  結果例
#  $data = {
#      'YOUT_DATA_STRUCTURE' => {
#          'length' => 10,
#          'data' => [
#                     1,
#                     2,
#                     ...,
#                    ],
#          },
#  }

例:FLVファイルを解析する

例として、Flashで扱う映像ファイルフォーマットであるFLVファイルを途中まで解析してみます。こちらの仕様書(PDF)を見ながらStruct中を眺めるとやってることがわかると思います。

長くなりそうなので途中から詳細はn byteデータチャンクとして扱い、その先を具体的に解析しないようにしています。興味を持たれたらコメントアウト部分を修正して続きを解析してみてください。

#!/usr/bin/env perl
use strict;
use warnings;

package FLV::Parser;
use Data::ParseBinary;

sub parse {
    my($self, $file) = @_;

    my $s =
    Struct('FLV',
        header(),
        body(),
    );
    open my $fh, '<', $file;
    binmode $fh;
    my $stream = CreateStreamReader(File => $fh);
    my $data = $s->parse($stream);
    close $fh;
    $data;
}

sub UBInt24 {
    my($name) = @_;
    Struct($name,
        UBInt8('_b1'),
        UBInt8('_b2'),
        UBInt8('_b3'),
        Value('value', sub { $_->ctx->{_b1} << 16 | $_->ctx->{_b2} << 8 | $_->ctx->{_b3} }),
    );
}

sub header {
    # 9 byte
    Struct('Header',
        Const(String('Signature', 3), 'FLV'),
        UBInt8('Version'),
        BitStruct('TypeFlags',
            Padding(5),
            Flag('Audio'),
            Padding(1),
            Flag('Video'),
        ),
        UBInt32('DataOffset')
    );
}

sub body {
    Struct('Body',
        UBInt32('PreviousTagSize0'),
        GreedyRange(Struct('tags',
            flvtag(),
            UBInt32('PreviousTagSizeN'),
        )),
    );
}

sub flvtag {
    Struct('FLVTAG',
        UBInt8('TagType'),
        UBInt24('DataSize'),
        UBInt24('Timestamp'),
        UBInt8('TimestampExtended'),
        UBInt24('StreamID'),
        Switch("Data", sub {$_->ctx->{TagType}}, {
            8 => Struct('AUDIODATA',
                BitStruct('info',
                    BitField('SoundFormat', 4),
                    BitField('SoundRate', 2),
                    BitField('SoundSize', 1),
                    BitField('SoundType', 1),
                ),
                Array(sub{$_->ctx(1)->{DataSize}->{value} - 1}, UBInt8('SoundData')),
            ),
            9 => Struct('VIDEODATA',
                BitStruct('info',
                    BitField('FrameType', 4),
                    BitField('CodecID', 4),
                ),
                Array(sub{$_->ctx(1)->{DataSize}->{value} - 1}, UBInt8('VideoData'))
            ),

#            8 => audiodata(),
#            9 => videodata(),
#            10 => scriptdataobject(),
        },
        default => Array(sub{$_->ctx(0)->{DataSize}->{value}}, UBInt8('VideoData'))
        )
    );
}


=head2 commentout

sub audiodata {
    Struct('AUDIODATA',
        BitStruct('info',
            BitField('SoundFormat', 4),
            BitField('SoundRate', 2),
            BitField('SoundSize', 1),
            BitField('SoundType', 1),
        ),
        Switch('SoundData', sub {$_->ctx->{info}->{SoundFormat}}, {
            10 => aacaudiodata(),
        })
    );
}
sub aacaudiodata {
    Struct(
        UBInt8('AACPacketType'),
        UBInt8('Data', n)
    );
}
sub videodata {
    Struct('VIDEODATA',
    );
}
sub scriptdataobject {
    Struct('SCRIPTDATAOBJECT',
    );
}

=cut

package main;
use Data::Dumper;

my $data = FLV::Parser->parse('foo.flv');
print Dumper $data;

C言語でバイナリ解析する場合に比べると、細かい構造体単位でstruct定義を分ける必要がなく、複雑な構造であっても見た目に理解しやすいのがいいところです。制御構文のように使える関数もあるので、「bit 31から28の値が0001なら構造A、0010なら構造Bとして次のN Byteを解析する」というような処理も書きやすいです。

処理速度は速くないのでリアルタイムで処理するような用途には向きませんが、バイナリエディタで入れ子になったデータ構造のオフセットを確認しながら手動で値を確認するよりわかりやすいでしょう。また仕様書から複雑なデータフォーマットを学ぶ時にも理解の助けとなると思います。

Back