非単バイト文字列の解析の詳細

前回示した例のうち、

$ echo -n "((@@出鼻)を挫く" | iconv -t Shift_JIS | ./parser | iconv -f Shift_JIS
marked-string [出呆挫]
string [ュ]

が、文字は化けているもののmarked-stringとstringの連接という構造を正しく抽出していることが気になった。
解析そのものに失敗しても不思議はないからだ。
例えば、シフトJISでエンコードされたこの入力の2バイトコードを適当なアルファベットに置き換えてみると、

$ echo -n "((@@abc@)defghi" | iconv -t Shift_JIS | ./parser
marked-string [abcefghi]
syntax error

「鼻」の2バイト目は'@'なので「鼻」は「c@」で表している。
すると、「@)d」はマーク終了文字の変更シーケンスとして解釈されるため、
マークされた文字列の語頭として「abcefghi」が抽出された後、
マーク終了文字の'd'が現れる前に入力が尽きて構文エラーとなる。
なぜ、これと同じことが「((@@出鼻)を挫く」では起きなかったのだろう。

シフトJISで「((@@出鼻)を挫く」のバイト列は16進数2桁で表すと、

28 28 40 40 8f 6f 95 40 29 82 f0 8d c1 82 ad

の15バイト長になる。分解してみると、

28 28 40   ((@   マーク開始文字を'@'に設定
40         @     マーク付け開始
8f 6f 95         マーク付けされた文字列の一部
40 29 82   @)    マーク終了文字を'\x82'に設定
f0 8d c1         マーク付けされた文字列の一部
82               マーク付け終了
ad               マーク外の文字列

なんと、文字「く」の1バイト目が変更後のマーク終了文字'\x82'に一致していた。
この偶然が6バイト3文字長のmarked-stringと1バイト1文字長のstringの連接という解釈を生んだのだった。

ちなみに、マーク終了文字を変更した「()@(出鼻@を挫く」の場合は、

28 29 40 28 8f 6f 95 40 40 82 f0 8d c1 82 ad

中ほどのマーク終了文字の連続「40 40」で構文エラーが発生する。