Ruby1.9のCGI.unescapeHTMLにて実体参照の戻しを行う場合には変換元の文字列のエンコーディングを考慮する必要がある

Ruby1.9で遊び始めた。WebのAPI叩いていたら、実体参照が含まれる文字列をunescapeHTMLしようとしても上手くいかない。例えば、"&#xffff"のような形式の実体参照

Ruby1.8の場合には問題とならなかったケースだった。cgi.rbのソースコードを読んで、ようやく原因が分かった。

cgi/util.rb@1.9.2p180に含まれる、一節を抜粋する。

  def CGI::unescapeHTML(string)
    enc = string.encoding
#(中略)
    string.gsub(/&(amp|quot|gt|lt|\#[0-9]+|\#x[0-9A-Fa-f]+);/) do
      match = $1.dup
      case match
#(中略)
      when /\A#x([0-9a-f]+)\z/i
        n = $1.hex
        if enc == Encoding::UTF_8 or
          enc == Encoding::ISO_8859_1 && n < 256 or
          asciicompat && n < 128
          n.chr(enc)
        else
          "&#x#{$1};"
        end
      else

上記のwhen節は正規表現で"&#x0000;"形式を抜き出したあとの処理になる。encとは、引数stringのencodingを示し、この入ってくるstringのエンコーディングUTF-8か、それ以外で処理が異なる。

Ruby1.9ではそれぞれの文字列にそれぞれのエンコーディングが関連付けられる。よって引数としてのstringにASCIIがエンコディングとなっている文字列が入っているのではないか、と考えられる。

#!/usr/bin/env ruby
require 'cgi'

print "RUBY_VERSION ", RUBY_VERSION,"\n\n"
word = "&#x901A;&#x5E38;"
print "encoding: ", word.encoding, "\n"
print "word: ", CGI.unescapeHTML(word), "\n\n"

word_utf8 = word.encode("UTF-8")
print "encoding: ", word_utf8.encoding, "\n"
print "word: ", CGI.unescapeHTML(word_utf8), "\n\n"

上記に検証コードを示す。下記に実行結果を示す。

$ ruby unescape.rb
RUBY_VERSION 1.9.2

encoding: US-ASCII
word: &#x901A;&#x5E38;

encoding: UTF-8
word: 通常

推測のとおり、US-ASCIIを入れてしまっていたようだ。文字コードを利用して整数から文字に変換するためには、当然、変換先のエンコーディングが必要になる。US-ASCIIのようなエンコーディング指定では、無理に変換しようとせずに、無視される。

Ruby1.9ではプログラムの冒頭にてリテラル文字列のエンコーディングに何を使うかの指定が行える。そのため通常は問題にならなそうだ。

# -*- encoding: utf-8 -*-

今回のケースでは、WebのAPIを叩く過程のどこかでASCIIエンコーディングの文字列が返ってきたようだ。どこで入っていたのかについては確認をしていない。

あまりにもアホなミスなのか、ネット上にunescapeHTMLについての事例の情報がなかった。恥ずかしいが、書いておこうと思う。