Ruby 2.3.0 と URI.decode

`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 で振る舞いが変わっているようです。

```ruby
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 での実行結果。

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

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

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

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

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

## git で変更を追ってみる

調査したい対象は[こちら](https://github.com/ruby/ruby/blob/v2_3_0/lib/uri/generic.rb#L1344)です。どのタイミングで変更されたかをみてみます。

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

[frozen string literal 対応された際](https://github.com/ruby/ruby/commit/f2b9563bb40f1620da3965cfdb13cb9a8aebba74#diff-66cc3d0ae5a8886f51f0d2e670324bf3L1342) に動きが変わったようです。

```ruby
-      str = ''
+      str = String.new
```

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

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

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

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

これは盲点でした。

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

`URI::Generic#to_s` の方を Ruby 2.2.4 と互換のあるエンコーディングの形で [Pull Request を出した](https://github.com/ruby/ruby/pull/1188) ところ、 [@narse さん](https://github.com/nurse) からお返事を頂いてなるほどとなりました。

> 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.decode` も `CGI.unescape` みたいに force_encoding をする形とかになるとよいのかな。。。 (ここよく分かっていない) 今後の Ruby へのコミットとか追って行きたいところです。

## puts と p

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

## URI.decode と CGI.unescape

そもそもが [`URI.decode` は非推奨](http://docs.ruby-lang.org/ja/2.3.0/class/URI.html#S_DECODE) ということで、`CGI.unescape` を使ってみたところ期待の UTF-8 でエンコーディングされた文字列が返ってきました。

```ruby
$ 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 経由でのファイル名を期待したエンコーディングで取得できない問題がありました。
[このコミット](https://github.com/carrierwaveuploader/carrierwave/commit/8ee0f879e656ee788a2df67b31e724e952399578)でなおったと思いますので、この現象に出会ったら見てみてください ([@thomasfedb](https://github.com/thomasfedb) 氏に感謝) 。

## 後記

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