LoginSignup
37
33

More than 5 years have passed since last update.

Ruby 2.3.0 と URI.decode

Last updated at Posted at 2016-01-14

URI.decode まわりを見ていて、いい勉強になったので記しておきます。

ざっくり言うと

  • Ruby 2.3.0 でマルチバイト文字を含んだ URL を URI.decode でうまくデコードできなかった場合は CGI.unescape などの利用を検討されると良いと思います
  • CarrierWave (執筆時 0.10.0 が最新) を Ruby 2.3.0 と組み合わせた際に、Fog 経由でのファイル名を期待したエンコーディングで取得できない問題について CarrierWave の HEAD ではなおったと思います
  • URI.decode (URI.unescape), URI.encode (URI.escape) は非推奨

URI::Generic#to_s によるエンコーディングの違い

URI::Generic#to_s について、Ruby 2.2.4 と Ruby 2.3.0 で振る舞いが変わっているようです。

require 'uri'

str = URI::Generic.build(scheme: 'http', host: 'localhost', path: '/test', query: 'language=日本語').to_s

p URI.decode(str)
p URI.decode(str).encoding

Ruby 2.2.4 での実行結果。

"http://localhost/test?language=日本語"
#<Encoding:UTF-8>

Ruby 2.3.0 での実行結果。デコードしても ASCII-8BIT のままなので、いわゆる文字化けが発生するようです。

"http://localhost/test?language=\xE6\x97\xA5\xE6\x9C\xAC\xE8\xAA\x9E"
#<Encoding:ASCII-8BIT>

いちおう force_encoding('UTF-8') すれば期待する文字列を手に入れることができます。

URI.decode(str).force_encoding('UTF-8')
=> "http://localhost/test?language=日本語"

git で変更を追ってみる

調査したい対象はこちらです。どのタイミングで変更されたかをみてみます。

$ git blame lib/uri/generic.rb
$ git show f2b9563b

frozen string literal 対応された際 に動きが変わったようです。

-      str = ''
+      str = String.new

frozen なリテラル文字列だと、<< のような破壊的な操作はエラーになるため対応がされておりました。

空文字列のエンコーディングの違い

この文字列の生成方法で何がどう変わったというのが最初分かりませんでした。
Ruby 2.2.4 でもそうなっていますが、 ''String.new でエンコーディングに違いがあるようです。

> ''.encoding
=> #<Encoding:UTF-8>
> String.new.encoding
=> #<Encoding:ASCII-8BIT>

これは盲点でした。

エスケープされた文字列は ASCII-8BIT、デコードした文字列は UTF-8

URI::Generic#to_s の方を Ruby 2.2.4 と互換のあるエンコーディングの形で Pull Request を出した ところ、 @narse さん からお返事を頂いてなるほどとなりました。

The encoding of escaped URI should be US-ASCII, and maybe URI.decode should set the resulted encoding as UTF-8.

そうなれば今回の URI::Generic#to_s については、ASCII-8BIT で困ることはなさそう。
URI.decodeCGI.unescape みたいに force_encoding をする形とかになるとよいのかな。。。 (ここよく分かっていない) 今後の Ruby へのコミットとか追って行きたいところです。

puts と p

再現テストを行う際にputs URI.decode(str) では、URI.decode で期待しているいわゆる文字化けが起こらずなんだろうと思ったら、 puts を使っていたことが起因のようでした。デバッグ用途なら p でしたね。なので、p URI.decode(str) が正解。
ここは @kunitoo にアドバイスしてもらいました。感謝。

URI.decode と CGI.unescape

そもそもが URI.decode は非推奨 ということで、CGI.unescape を使ってみたところ期待の UTF-8 でエンコーディングされた文字列が返ってきました。

$ pry
> require 'cgi'
> show-method CGI.unescape
(snip)
def unescape(string,encoding=@@accept_charset)
  str=string.tr('+', ' ').b.gsub(/((?:%[0-9a-fA-F]{2})+)/) do |m|
    [m.delete('%')].pack('H*')
  end.force_encoding(encoding)
  str.valid_encoding? ? str : str.force_encoding(string.encoding)
end

なるほど。force_encoding しているんですね。

Ruby 2.3.0 でマルチバイト文字を含んだ URL を URI.decode でうまくデコードできなかった場合は CGI.unescape などの利用を検討されると良いと思います。

この問題で困ったライブラリ

自分が困ったというよりは Facebook で知り合いが困っていたのがきっかけですが、CarrierWave (執筆時 0.10.0 が最新) を Ruby 2.3.0 と組み合わせた際に、Fog 経由でのファイル名を期待したエンコーディングで取得できない問題がありました。
このコミットでなおったと思いますので、この現象に出会ったら見てみてください (@thomasfedb 氏に感謝) 。

後記

ちょうど、この記事を書いていたところ URI::Generic#to_s の Ruby 2.2 への互換性について trunk に取り込んで頂けたので (ありがとうございます!) 、Ruby 2.3.1 (?) あたりでいくつかの検討課題と含めていい感じの動きになるかなと思っています。

37
33
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
37
33