Encode::JP::JIS7 のJIS X 0201片仮名対応

本来、iso-2022-jp という文字符号化方式には、JIS X 0201片仮名、所謂半角片仮名の対応は含まれていない。一方、perl の Encode モジュールで iso-2022-jp を担当する Encode::JP::JIS7 は、JIS X 0201片仮名も含めてエンコード・デコードを行うように作られている。恐らくこれは、JIS X 0201片仮名が根強く使用され続けている現状を考慮して書かれたのだと思う。
しかし、符号化方式が対応していないのに、どうやって符号化しているんだろう?
調べて行くと、JIS X 0201:1997 にその符号化方法が幾つか示されていて、それを無理やり当てはめているようだ。
そこで示されている方式は、

  1. ラテン文字・片仮名用7ビット符号表を呼び出して SO(0/14)/SI(0/15) で切り替えて用いる
  2. ラテン文字・片仮名用8ビット符号表を呼び出して用いる
  3. 片仮名用7ビット符号表を呼び出して用いる

で、無理やり当てはめた方式は、JIS X 0201 片仮名によると、

  1. ラテン文字用7ビット符号表を(ラテン文字・片仮名用7ビット符号表と仮定して)呼び出して SO(0/14)/SI(0/15) で切り替えて用いる
  2. ラテン文字7ビット符号表を(ラテン文字・片仮名用8ビット符号表と仮定して)呼び出して用いる
  3. 片仮名用7ビット符号表を呼び出して用いる

前2つが余りにも強引過ぎるように見えるけど、それは置いておいて本題。
Encode::JP::JIS7 はこれら3つのうち、最後の1つだけ対応している。つまり、最初の2つの方式が文字列に現れた場合、正しく処理することが出来ない。
困ったことに、IRCNet IRCネットワークにおいて、 JIS X 0201 片仮名符号は最初の2つで符号化されていることが多いので、perl で書かれた Web サービスとしての IRC クライアントや、IRC BOT はこれらを文字化けとして受け取ることになる。大体、規格としては対応していること自体がおかしいのは分かっているんだけど、keitairc で携帯端末から IRC に参加するときに、文字化けばかりで流れが読めないって言うのはなかなか辛い。
そこで、iso-2022-jp-ext 符号化方式と題して Encode::JP::JIS7 をちょっといじったモジュールを作ることで対処することにした。
Encode::JP::JIS7 の居場所は perl/lib/encode/JP。そこにある JIS7.pm をコピーして JISex.pm と名付けたファイルを作成した。これを iso-2022-jp-ext 専用モジュールに仕立て上げてていく。
まずは、先頭の符号化方式名の定義を iso-2022-jp-ext に書き換え。

for my $name ('iso-2022-jp-ext'){
    $Encode::Encoding{$name} =
        bless {
               Name      =>   $name,
               h2z       =>   0,
               jis0212   =>   1,
              } => __PACKAGE__;
}

h2z は JIS X 0201 片仮名をJIS X 0208 片仮名に振り替えるフラグだけど、そんなのには用はないので 0 としておいた。
次は符号化本体の部分。jis_euc 関数で$r_strをあれこれする部分を書き換え。

    # for 7bit kana (with si/so) encoding
    $$r_str =~ s/\x0e([\x21-\x7e]*?)\x0f/\x1B\(I$1\x1B\(J/og;
    $$r_str =~ s($re_scan_jis)
    {
	my ($esc_0212, $esc_asc, $esc_kana, $chunk) =
	   ($1, $2, $3, $4);
	if (!$esc_asc) {
	    $chunk =~ tr/\x21-\x7e/\xa1-\xfe/;
	    if ($esc_kana) {
		$chunk =~ s/([\xa1-\xdf])/\x8e$1/og;
	    }
	    elsif ($esc_0212) {
		$chunk =~ s/([\xa1-\xfe][\xa1-\xfe])/\x8f$1/og;
	    }
	} else {
		# for 8bit kana encoding
		$chunk =~ s/([\xa1-\xdf])/\x8e$1/og;
	}

コメントを付けてある部分が、それぞれの符号化方式に対する変換手続きに当たる。SI/SO の方は、片仮名用7ビット符号の形に置き換えて、後の変換コードに任せている。8ビットの方は任せようがないので、変換コードを新たに書き加えた。
仕上げは perl/lib/encode/Encode.pmperl/lib/encode/Config.pm (ActivePerlの場合) に iso-2022-jp-ext のエントリを追加。よく分からないので、iso-2022-jp-1のとなりに置いておいた。

...
	 'iso-2022-jp-1'      => 'Encode::JP',
	 'iso-2022-jp-ext'    => 'Encode::JP',
	 'jis0201-raw'        => 'Encode::JP',
...

これで終わり、っと。
実際にこれを keitairc2 に適用して暫く放置してるけど、なかなかいい感じ。多分実用に耐えるんじゃないかな?

ruby/ext/socket のバグ?

ruby 1.8.7-p72(mswin32)+WEBrick で、LAN 向けの nslookup Web サービスを構築していたところ、socket ライブラリの不思議な挙動に気付いた。
ホスト名からホストの情報を取得する関数である gethostbyname は IPアドレスを与えても逆引きを行わない。IPアドレスからの逆引きを行うには、代わりに gethostbyaddr や、getnameinfo を呼び出す必要がある。しかし、Winsock では、何故か、逆引きで得られるホスト名を返してくれるようなのだ。
アドレスとホスト名を区別せずに実装できる!と喜んで実装したのはいいものの、テストに掛けてみると、何故か、幾つかの IP アドレスが逆引きできない。
ええ、どういうことなの・・・?
逆引き出来なかった IP アドレスを並べて暫く考えていると、ある共通性に気付いた。どれも最下位オクテットが 224 になっている。えーっと、IP アドレスで 224 といえば・・・そう、マルチキャストアドレスだ。しかし、マルチキャストアドレスは最上位オクテットが 224 のアドレスであって最下位オクテットではない。
じゃあ、マルチキャストアドレスとこの現象に何の関係が・・・?
ブラックボックスの外からあれこれ考えても、これ以上何も出てきそうになかったので、ソースコードを訪ねてみた。
Socket のソースコードruby/ext/socket/ ・・・っと。追ってみると、getaddrinfo.c で興味深い記述を見つけた。

switch (afdl[i].a_af) {
case AF_INET:
        v4a = ((struct in_addr *)pton)->s_addr;
        if (IN_MULTICAST(v4a) || IN_EXPERIMENTAL(v4a))
                pai->ai_flags &= ~AI_CANONNAME;

v4a がマルチキャストアドレスなら何かフラグを倒す分岐がある。IN_MULTICAST が受け付けるアドレスはホストバイトオーダーらしいので、v4a がホストバイトオーダーじゃないと正しく分岐されない。
さて、その v4a は、このコードの前にある inet_pton なる関数によって得られた in_addr 構造体データから抽出されている。inet(3) FreeBSDドキュメントJManによると、inet_pton はネットワークバイトオーダーでアドレスを返すことになっているようだ。
ということは、ネットワークバイトオーダー≠ホストバイトオーダーの環境では、最下位オクテットが224のとき、このコードは間違って分岐してしまうことになる・・・?
ためしに、バイトオーダーを逆にして使ってみると、意図した通りに動作してくれた。
いや・・・そんな間違いがあるはずがない!きっと思い違いだ・・・1999年からこのコードだから、間違っていたら誰か修正しているだろうし・・・