PIB - Cygwin - Ruby-1.9.3p327 - win32/registry
LANG=ja_JP.SJIS の場合、問題は出ないようなのだが、
2013-02-26 現在 Cygwin 上の Ruby-1.9.3p327 で win32/registry を使う際、LANG=ja_JP.UTF-8 になっていると、以下のようなコードでエラーが出る。

regtest.rb

#!/usr/bin/env ruby
require 'win32/registry'
Win32::Registry::HKEY_LOCAL_MACHINE.open('SOFTWARE\Microsoft\Windows\CurrentVersion\Run') do |reg|
  reg.each_value do |name, type, data|
    puts name
  end
end

実行結果

$ ./regtest.rb
IME14 JPN Setup
Adobe ARM
LifeCam
SunJavaUpdateSched
/usr/lib/ruby/1.9.1/win32/registry.rb:173:in `tr': invalid byte sequence in UTF-8 (ArgumentError)
        from /usr/lib/ruby/1.9.1/win32/registry.rb:173:in `initialize'
        from /usr/lib/ruby/1.9.1/win32/registry.rb:231:in `exception'
        from /usr/lib/ruby/1.9.1/win32/registry.rb:231:in `raise'
        from /usr/lib/ruby/1.9.1/win32/registry.rb:231:in `check'
        from /usr/lib/ruby/1.9.1/win32/registry.rb:269:in `EnumValue'
        from /usr/lib/ruby/1.9.1/win32/registry.rb:524:in `each_value'
        from ./regtest.rb:4:in `block in <main>'
        from /usr/lib/ruby/1.9.1/win32/registry.rb:389:in `open'
        from /usr/lib/ruby/1.9.1/win32/registry.rb:496:in `open'
        from ./regtest.rb:3:in `<main>'
どうも、each_value する際 Win32::Registry::API::RegEnumValueA() の戻り値が 0 以外になったら、raise させた Win32::Registry::Error を rescue する事で each_value から break させているようなのだが、raise させた Win32::Registry::Error でエラーメッセージのエンコーディングを変換する部分で不具合が出ているらしい。
FormatMessageA() は、日本語環境だと CP932 で書式化したメッセージを生成するが、それを force_encoding(Encoding.find(Encoding.locale_charmap)) すると、LANG=ja_JP.UTF-8 だと Encoding.locale_charmap は UTF-8 なので CP932 の文字列を UTF-8 で .force_encoding() している状態になっており、その直後で .tr() した際、不正な UTF-8 文字列として検出され腐っているらしい。
根本的な原因は、cygwin の場合、LANG の設定でロケールが変わっちゃうので FormatMessageA() が返す文字列のエンコーディングと Encoding.locale_charmap が必ずしも一致しないことが原因のようだ。

と言うことで、問題のコードと改善方法をまとめると以下のような感じ。

FormatMessageTest.rb

#!/usr/bin/env ruby
require 'dl/import'

module Kernel32
  extend DL::Importer
  dlload "kernel32.dll"
end
FormatMessageA = Kernel32.extern "int FormatMessageA(int, void *, int, int, void *, int, void *)", :stdcall
FormatMessageW = Kernel32.extern "int FormatMessageW(int, void *, int, int, void *, int, void *)", :stdcall

code = 259 # = "データはこれ以上ありません。"
msgA = "\0".force_encoding(Encoding::ASCII_8BIT) * 1024
lenA = FormatMessageA.call(0x1200, 0, code, 0, msgA, 1024, 0)
msgW = "\0".force_encoding(Encoding::ASCII_8BIT) * (1024 / 2)
lenW = FormatMessageW.call(0x1200, 0, code, 0, msgW, 1024 / 2, 0) * 2

# Ruby-1.9.3p327
p msgA[0, lenA].force_encoding(Encoding.find(Encoding.locale_charmap))
p msgA[0, lenA].force_encoding(Encoding.find(Encoding.locale_charmap)).encoding
# proposal1
p msgA[0, lenA].force_encoding("CP932").encode(Encoding.find(Encoding.locale_charmap))
p msgA[0, lenA].force_encoding("CP932").encode(Encoding.find(Encoding.locale_charmap)).encoding
# proposal2
p msgW[0, lenW].force_encoding("UTF-16LE").encode(Encoding.find(Encoding.locale_charmap))
p msgW[0, lenW].force_encoding("UTF-16LE").encode(Encoding.find(Encoding.locale_charmap)).encoding

実行結果

$ echo $LANG
ja_JP.UTF-8

$ ruby -e "p __ENCODING__"
#<Encoding:UTF-8>

$ ./FormMessageTest.rb
"\x83f\x81[\x83^\x82?\xB1\x82\xEA\x88??\x82\xE8\x82?\xB9\x82\xF1\x81B\r\n"
#<Encoding:UTF-8>
"データはこれ以上ありません。\r\n"
#<Encoding:UTF-8>
"データはこれ以上ありません。\r\n"
#<Encoding:UTF-8>
使っている Windows が何国語版かによって FormatMessageA() が返す文字列のエンコーディングが変わっちゃいそう(CP932 である保証がなさそう)なので FormatMessageW() 使って UTF-16LE 決め打ちしたほうが良いんじゃないかと思う。

と言うことで、以下のようなモンキーパッチを当てとけば不具合は解消する模様。
(補足: 2014-03-18: 以前公開してたコードは文字列の確保が半分しかできていなかったので要注意)
begin
  Win32::Registry::Error.new(259)
rescue ArgumentError => e
  if e.message == "invalid byte sequence in UTF-8"
    class Win32::Registry::Error
      FormatMessageW = Kernel32.extern "int FormatMessageW(int, void *, int, int, void *, int, void *)", :stdcall
      def initialize(code)
        @code = code
        msg = "\0\0".force_encoding(Encoding::UTF_16LE) * 1024
        len = FormatMessageW.call(0x1200, 0, code, 0, msg, msg.size, 0)
        msg = msg[0, len].encode(Encoding.find(Encoding.locale_charmap))
        super msg.tr("\r".encode(msg.encoding), '').chomp
      end
    end
  else
    raise e
  end
end
モンキーパッチじゃなくて win32/registry.rb の 172 行目付近にパッチ当てたほうが建設的じゃないかって気もする。

追記: 2014-03-18:
Ruby 2.1.0-preview1 以降、この問題は解決されている。
但し、2014-02-24 にリリースされた旧版の最新版である 2.0.0-p451, 1.9.3-p545 では、未解決のままなので何らかの対応が必要。

関連

関連