Perl書くくせにpack/unpackの動作が全然わからなかったため休日に少し練習。 以下、チラシの裏程度のメモ書き。校正しておらず、自分が思いついた思考の順序で一発書きしたもの。 たぶん間違えた内容を含んでます。
packは、引数に配列、あるいは文字列を受け取って、それを規則に従ってバイナリに変換する。
入力: 配列 or 文字列 出力: バイナリ
CとかHとか、色々なテンプレートがある。 http://perldoc.jp/func/pack
例えば、10進数表記で "ABC(改行)"は
$ echo ABC | od -tu1c
0000000 65 66 67 10
A B C \n
0000004
つまり 65, 66, 67, 10 で表すことができる。 ということで
1文字目を取り出す。
$ perl -e 'print pack("C1",65,66,67,10)'
A
$ perl -e 'print pack("C2",65,66,67,10)'
AB
$ perl -e 'print pack("C3",65,66,67,10)'
ABC
$ perl -e 'print pack("C4",65,66,67,10)'
ABC(改行)
Cというのは unsigned char 値を表す。つまり、1-255の10進数の整数値。 C(数字)で、その数字分のオクテットを読んでくれる。 C4だと、4オクテット分解釈してくれる。 基本的に10進数なので、バイト列に変換しても8オクテットに満たない。なので0埋めする。 たとえば、ABC(改行)を2進数で表すと下記のようになる。
$ perl -e 'print pack("C4",65,66,67,10)' | xxd -b -c1
00000000: 01000001 A
00000001: 01000010 B
00000002: 01000011 C
00000003: 00001010 .
しかし実際に65,66,67,10を2進数で表すと、8ビットにはならない。なので、8ビットになるように0埋めしてくれる。それがCの動作。
$ echo 'obase=2;ibase=10;65;66;67;10' | bc
1000001
1000010
1000011
1010
255以上の数字を与えた時は、2進数にした後に最初の8ビットを採用するらしい。
$ perl -e 'print pack("C*",65666710)' | od -tx1c
0000000 96
226
0000001
!! 実際のバイト列は?
$ perl -e 'print pack("C*",65666710)' | xxd -b
00000000: 10010110 .
!! 数字を2進数にすると、最初の8バイトだけが残っている。
$ echo 'obase=2;ibase=10;65666710' | bc
11111010011111111010010110
なお、perlは0x<16進数数値>
という書き方ができるため、下記のようにできる。
$ perl -e 'print pack("C*",0x41,0x42,0x43,0x0a)' | awk 1
ABC
0x
という書き方は、表記は16進数だが、10進数の数値と同じ扱いを受ける。
$ perl -e 'print 0x43' | awk 1
67
そして、数値リテラルではなくても動く。 この時点で混乱してきた。
$ perl -e 'print pack("C4","65","66","67","10")'
ABC(改行)
packは常にリスト型を受け取ると思っていた。じゃあ入力したい情報が10進数なら良いけど、16進数を数値リテラルでそのまま表しつつ、文字列に変換したい場合どうするのか。 →その場合は文字列として16進数の数値を入力する。例えば
$ echo ABC | od -tx1c
0000000 41 42 43 0a
A B C \n
0000004
なので
ABC(改行)
は16進数だと"4142430a"という文字列。
$ perl -e 'print pack("H*","4142430a")'
ABC
になる。リストを受け取ったり、文字列を受け取ったり、packはややこしい。。。 ただ、10進数のときと違い、16進数であれば、かならず2文字で1オクテットなので、一つの文字列でも大丈夫なのかもしれない。
もう一つ例を。 "うんこ(改行)"という文字列を16進数で表すと
$ echo うんこ | od -tx1 -An | tr -dc 'a-z0-9' | awk 1
e38186e38293e381930a
なので、こうなる。
$ perl -e 'print pack ("H*","e38186e38293e381930a")'
うんこ
UTF-8で「う」は3オクテット必要。 Hを使う場合、H1で4ビット(16進数の文字列1文字分)。 つまりH2で1オクテットになる。 よってH6で3オクテットになり、最初の一文字が表示できる。
$ perl -e 'print pack ("H6","e38186e38293e381930a")' | awk 1
う
同様のことを10進数でやろうとすると
$ perl -e 'print pack("C3",227,129,134,227,130,147,227,129,147,10)' | awk 1
う
2進数で「abc」はこうなる。
$ echo abc | xxd -b -c 1 | awk '{print $2}' | tr -dc '01' | awk 1
01100001011000100110001100001010
なので、こうできる。
$ perl -e 'print pack("B*","01100001011000100110001100001010")'
abc
2指数の文字列を数値のリテラルにしたら、想定した動作はしない。
$ perl -e 'print pack("B*",01100001011000100110001100001010)'
Integer overflow in octal number at -e line 1.
B(数字)で表し、B1で1ビット。つまり、B8で1オクテットとなる。 下記は、一文字だけ取り出す例。
$ perl -e 'print pack("B8","01100001011000100110001100001010")' | awk 1
a
a
は素直に文字列らしい。
a1で1オクテット。
$ perl -e 'print pack("a1","abc")' | awk 1
a
A
は2進数で01000001
$ echo -n A | xxd -b -c 1
00000000: 01000001 A
B
は10進数で66
$ echo -n B | od -tu1c
0000000 66
B
0000001
C
は16進数で43
$ echo -n C | od -tx1c
0000000 43
C
0000001
\n(改行)
は10進数で10
ということで。
"01000001",66,"43",10
という配列をぶっこむ。
$ perl -e 'print pack ("B8C1H2C1","01000001",66,"43",10)'
ABC(改行)
違う種類の変換が入る時は、配列は別要素にする必要がある。
packとは逆の動作をする。 バイナリ → 文字列 or 配列
文字列を2進数の文字列に。
$ perl -e 'print unpack("B*","ABC")' | awk 1
010000010100001001000011
一文字だけ取り出す。
$ perl -e 'print unpack("H2","ABC")' | awk 1
41
1文字目は2進数、2文字目は16進数。3文字目はそのまま。
$ perl -e 'print unpack("H2B8A1","ABC")' | awk 1
4101000010C
unpackは文字列だけでなく、配列として結果を返す。
配列の1要素になる単位は、C
だと1オクテットごとになる。
$ perl -e 'print join ".", unpack("C*","ABC")' | awk 1
65.66.67
ほかのフォーマットだと、ならない。
$ perl -e 'print join ".", unpack("H*","ABC")' | awk 1
414243
$ perl -e 'print join ".", unpack("B*","ABC")' | awk 1
010000010100001001000011
unpackには区切りの単位のようなものがあるらしい。
命令に*
を使わず、2オクテットごとに命令を分ける。
$ perl -e 'print join ".", unpack("H2H2H2","ABC")' | awk 1
41.42.43
B8B2B8
とやると、B
だけ2ビットだけ取り出される。つまりB
1回に付き1オクテット読む。B8B2
とやったら、1オクテット目の8ビットと2オクテット目の2ビットを取り出すらしい。
$ perl -e 'print join ".", unpack("B8B2B8","ABC")' | awk 1
01000001.01.01000011
括弧を使うと区切りの単位を指定できるらしい。
$ perl -e 'print join ".", unpack("(H2)*","ABC")' | awk 1
41.42.43
$ perl -e 'print join ".", unpack("(H2)2","ABC")' | awk 1
41.42
1-32の数字をサブネットマスクに変換する。
$ echo 14 | perl -nle 'print join(".", unpack("C4",pack("B*", "1"x$_."0"x32)))'
255.252.0.0