Upgrade to Pro — share decks privately, control downloads, hide ads and more …

本当はこわいエンコーディングの話

 本当はこわいエンコーディングの話

東京Ruby会議10 で発表したスライド

とみたまさひろ

January 13, 2013
Tweet

More Decks by とみたまさひろ

Other Decks in Programming

Transcript

  1. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    本当はこわい
    エンコーディングの話
    とみたまさひろ
    東京Ruby会議10
    2013-01-13

    View full-size slide

  2. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    自己紹介
    とみた まさひろ
    http://tmtms.hatenablog.com
    https://twitter.com/tmtms
    好きなもの/環境
    Ruby, Rabbit, MySQL, Emacs, Git,
    Ubuntu, ThinkPad
    所属など
    長野県北部在住 / 某社プログラマー / 日本
    MySQLユーザ会 / 長野ソフトウェア技術者
    グループ(NSEG)

    View full-size slide

  3. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    エンコーディング

    View full-size slide

  4. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    エンコーディングとは
    文字符号化方式
    文字をどのようなバイト列で表現するか
    UTF-8 とか EUC-JP とか SHIFT_JIS と
    かそーゆー奴
    「charset」とか呼ばれたりする
    「文字コード」とか呼ばれたりする

    View full-size slide

  5. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    同じバイト列でも別の文字
    0xC2 0xA9 の2バイトは
    UTF-8 では「©」1文字
    EUC-JP では「息」1文字
    SHIFT_JIS では「ツゥ」2文字

    View full-size slide

  6. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    Ruby 1.8
    "\xC2\xA9" という文字列は Ruby 的に
    はただのバイト列
    エンコーディング情報を持たない
    "©"(UTF-8) として扱うか "息"(EUC-JP)
    として扱うかはプログラム次第
    正規表現にはエンコーディングあり
    /〜/n /〜/s /〜/u /〜/e

    View full-size slide

  7. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    Ruby 1.9
    文字列のエンコーディングは文字列自身が
    知っている
    "©"(UTF-8) と "息"(EUC-JP) は同じバイ
    ト列だけど異なる文字列
    "あ"(UTF-8) と "あ"(EUC-JP) は同じ文
    字を表してるけど等しくない
    同じプログラム中で複数のエンコーディ
    ングの文字列を同時に扱える(珍しいかも)
    正規表現にもエンコーディングあり

    View full-size slide

  8. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    エンコーディング一覧(1.9.3)
    Ruby 自身が持ってるので環境に依存しない
    ASCII-8BIT Big5 Big5-HKSCS Big5-UAO CP50220 CP50221 CP51932 CP850
    CP852 CP855 CP949 CP950 CP951 EUC-JP EUC-KR EUC-TW Emacs-Mule
    GB12345 GB18030 GB1988 GB2312 GBK IBM437 IBM737 IBM775 IBM852
    IBM855 IBM857 IBM860 IBM861 IBM862 IBM863 IBM864 IBM865 IBM866
    IBM869 ISO-2022-JP ISO-2022-JP-2 ISO-2022-JP-KDDI ISO-8859-1
    ISO-8859-10 ISO-8859-11 ISO-8859-13 ISO-8859-14 ISO-8859-15
    ISO-8859-16 ISO-8859-2 ISO-8859-3 ISO-8859-4 ISO-8859-5 ISO-8859-6
    ISO-8859-7 ISO-8859-8 ISO-8859-9 KOI8-R KOI8-U MacJapanese
    SJIS-DoCoMo SJIS-KDDI SJIS-SoftBank Shift_JIS TIS-620 US-ASCII
    UTF-16 UTF-16BE UTF-16LE UTF-32 UTF-32BE UTF-32LE UTF-7 UTF-8
    UTF8-DoCoMo UTF8-KDDI UTF8-MAC UTF8-SoftBank Windows-1250
    Windows-1251 Windows-1252 Windows-1253 Windows-1254 Windows-1255
    Windows-1256 Windows-1257 Windows-1258 Windows-31J Windows-874
    eucJP-ms macCentEuro macCroatian macCyrillic macGreek macIceland
    macRoman macRomania macThai macTurkish macUkraine
    stateless-ISO-2022-JP stateless-ISO-2022-JP-KDDI

    View full-size slide

  9. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    うれしいこと

    View full-size slide

  10. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    1.8ではバイト単位
    "あいう".size #=> 9
    "あいう".bytesize #=> 9
    "あいう".chars{|c| ... }
    #=> "\xE3","\x81","\x82", ...
    "あいう"[0] #=> 0xE3
    "あいう".reverse
    #=> "\x86\x81\xE3\x84\x81\xE3\x82\x81\xE3"

    View full-size slide

  11. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    1.9では文字単位
    "あいう".size #=> 3
    "あいう".bytesize #=> 9
    "あいう".chars{|c| ... }
    #=> "あ", "い", "う"
    "あいう"[0] #=> "あ"
    "あいう".reverse #=> "ういあ"

    View full-size slide

  12. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    エンコーディング変換
    # -*- coding: utf-8 -*-
    s = "あいう"
    #=> "\xE3\x81\x82\xE3\x81\x84\xE3\x81\x86"
    s.encoding #=> #
    s2 = s.encode("CP932")
    #=> "\x82\xA0\x82\xA2\x82\xA4"
    s2.encoding #=> #

    View full-size slide

  13. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    IOで変換してくれる
    File.open("cp932.txt", "r:CP932").read
    #=> CP932 文字列
    File.open("cp932.txt", "r:CP932:UTF-8").read
    #=> UTF-8 文字列
    File.open("cp932.txt").read
    #=> 環境依存

    View full-size slide

  14. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    うれしいことばかりじゃない

    View full-size slide

  15. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    変換先にない文字
    # -*- coding: utf-8 -*-
    "あ♥".encode("CP932")
    #=> Encoding::UndefinedConversionError

    View full-size slide

  16. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    変換元にない文字
    # -*- coding: utf-8 -*-
    "あ\xFF".encode("CP932")
    #=> Encoding::InvalidByteSequenceError

    View full-size slide

  17. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    エンコーディングがあっても
    変換できるとは限らない
    # -*- coding: utf-8 -*-
    "あいう".encode("UTF-7")
    #=> Encoding::ConverterNotFoundError

    View full-size slide

  18. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    エンコーディングの不一致
    utf8 = "あいう"
    cp932 = "あ".encode("CP932")
    utf8.start_with?(cp932)
    #=> Encoding::CompatibilityError

    View full-size slide

  19. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    文字列と正規表現の
    エンコーディングの不一致
    utf8 = "あいう"
    re = /./s
    utf8 =~ re
    #=> Encoding::CompatibilityError

    View full-size slide

  20. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    エンコーディングが同じでも
    不正な文字を含んでいる
    utf8 = "あ\xFF"
    utf8 =~ /./
    #=> invalid byte sequence in UTF-8
    # (ArgumentError)

    View full-size slide

  21. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    IO

    View full-size slide

  22. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    メソッドによって
    エンコーディングが異なる
    テキスト読み込み(エンコードあり)
    IO#gets
    IO#getc
    IO#lines
    IO#read 等
    バイナリ読み込み(ASCII-8BIT固定)
    IO#read(n)
    IO#sysread 等

    View full-size slide

  23. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    IO#read
    IO#read(size) は ASCII-8BIT
    IO#read() は外部エンコーディング依存
    引数の有無によって結果のエンコーディ
    ングが異なる!
    なにそれこわい

    View full-size slide

  24. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    外部エンコーディング
    ファイル自身は自分の内容のエンコーデ
    ィングを知らない
    ファイルから読み込んだ文字列の Ruby
    内でのエンコーディングは何らかの方法
    で指定する必要がある

    View full-size slide

  25. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    引数で指定
    File.open(filename, "r:UTF-8")
    File.read(filename, :encoding=>"UTF-8")

    View full-size slide

  26. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    環境変数
    引数で指定されてない場合は環境変数が参照
    される
    LC_ALL
    LC_CTYPE
    LANG

    View full-size slide

  27. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    環境変数による違い
    % cat utf-8.txt
    あいうえお
    % export LC_ALL=C
    % ruby -e 'p File.read("utf-8.txt").size'
    16
    % export LC_ALL=ja_JP.UTF-8
    % ruby -e 'p File.read("utf-8.txt").size'
    6
    環境変数によって動きが変わっちゃう!こわい

    View full-size slide

  28. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    入力時にはエラーにならない
    utf8 = File.read("utf8.txt", :encoding=>"UTF-8")
    # 実は UTF-8 として不正な文字が含まれていて
    〜〜〜〜〜〜
    # ずっと後で別のメソッドでエラーになったり
    utf8 =~ /./
    #=> invalid byte sequence in UTF-8 (ArgumentError)

    View full-size slide

  29. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    CGI
    require "cgi"
    cgi = CGI.new
    不正な文字のパラメータを渡すとエラー
    GET http://example.com/hoge.cgi?fuga=%FF
    #=> Accept-Charset encoding error (CGI::InvalidEncoding)

    View full-size slide

  30. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    Rails
    不正な文字のパラメータを渡すとエラー
    POST http://example.com/posts
    post[title]=%FF
    #=> ArgumentError (invalid byte sequence in UTF-8)

    View full-size slide

  31. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    エラーになりすぎこわい!

    View full-size slide

  32. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    対処

    View full-size slide

  33. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    変換先にない文字を置換
    "あ♥".encode("CP932")
    #=> Encoding::UndefinedConversionError
    "あ♥".encode("CP932", :undef=>:replace)
    #=> "あ?"

    View full-size slide

  34. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    変換元にない文字を置換
    "あ\xFF".encode("CP932")
    #=> Encoding::InvalidByteSequenceError
    "あ\xFF".encode("CP932", :invalid=>:replace)
    #=> "あ?"

    View full-size slide

  35. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    置換文字の指定
    "あ♥".encode("CP932", :undef=>:replace, :replace=>"〓")
    #=> CP932 で "あ〓"

    View full-size slide

  36. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    そもそも変換が必要になるような
    ことをしないのが吉

    View full-size slide

  37. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    UTF-8に統一すれば
    たいていは問題ない

    View full-size slide

  38. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    UTF-8に統一したつもりでも
    他のエンコーディングが現れるこ
    とも
    File.open(filename, "r:UTF-8").read
    #=> UTF-8 文字列
    File.open(filename).read
    #=> 環境依存

    View full-size slide

  39. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    いちいち引数で指定する?

    View full-size slide

  40. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    デフォルト値を指定する
    プログラムで使用するファイルのエンコーディ
    ングがすべて同一であれば
    Encoding.default_external = "UTF-8"
    File.read(filename) #=> UTF-8文字列

    View full-size slide

  41. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    これで問題ない?

    View full-size slide

  42. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    ASCII-8BIT

    View full-size slide

  43. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    メソッドによっては ASCII-8BIT
    f = File.open(filename, "r:UTF-8")
    f.gets #=> UTF-8
    f.read(10) #=> ASCII-8BIT

    View full-size slide

  44. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    ソケットは ASCII-8BIT
    require 'socket'
    Encoding.default_external = "UTF-8"
    TCPSocket.new('127.0.0.1', 25).gets
    #=> ASCII-8BIT

    View full-size slide

  45. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    気をつけるしかない

    View full-size slide

  46. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    不正な文字

    View full-size slide

  47. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    エンコーディングがUTF-8でも
    データがUTF-8とは限らない
    f = File.open("/dev/urandom", "r:UTF-8")
    str = f.gets
    str.encoding #=> #
    str =~ /./
    #=> invalid byte sequence in UTF-8 (ArgumentError)

    View full-size slide

  48. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    事前に判定
    String#valid_encoding?
    f = File.open("/dev/urandom", "r:UTF-8")
    str = f.gets
    str.valid_encoding? #=> false

    View full-size slide

  49. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    不正な文字を置換したい
    簡単な方法はない
    String#encode は変換元と変換先が同じ
    場合は何もしない
    # -*- coding: utf-8 -*-
    "あ\xFF".encode("CP932", :invalid=>:replace)
    #=> "あ?"
    "あ\xFF".encode("UTF-8", :invalid=>:replace)
    #=> "あ\xFF"

    View full-size slide

  50. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    Iconv ではできたけど…
    require "iconv"
    src = "あい\xFFうえ"
    dst = ""
    iconv = Iconv.new("utf-8", "utf-8")
    begin
    dst.concat iconv.iconv(src)
    rescue Iconv::IllegalSequence => e
    dst.concat e.success
    dst.concat "〓"
    src = e.failed[1..-1]
    retry
    end
    dst #=> "あい〓うえ"
    Iconv は 2.0 で廃止

    View full-size slide

  51. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    同じ文字群を持つ
    別のエンコーディングを経由
    "あ\xFF".
    encode("UTF-16", :invalid=>:replace).
    encode("UTF-8")
    #=> "あ�"

    View full-size slide

  52. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    2.0.xだとできるようになる?

    View full-size slide

  53. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    CGI や Rails

    View full-size slide

  54. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    パラメータが不正な文字で
    エラー
    放置でいいんじゃない?
    外部からの不正なデータは最前線でエラー
    になってくれた方がありがたかったり
    後の処理に渡されても扱いに困るだけ
    実害はログが鬱陶しいくらい?

    View full-size slide

  55. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    CGIで頑張るなら
    require 'cgi'
    # 一旦 ASCII-8BIT で受けて
    cgi = CGI.new :accept_charset=>"ASCII-8BIT"
    # パラメータ毎にどうにかする
    hoge = cgi['hoge'].encode("UTF-8", "UTF-8")
    unless hoge.valid_encoding?
    ...

    View full-size slide

  56. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    Railsは...
    よくわかりません (><)

    View full-size slide

  57. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    おまけ

    View full-size slide

  58. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    ASCII文字だけなら
    異なるエンコーディングでも
    エラーにならない
    # -*- coding: utf-8 -*-
    "あ" =~ /./s #=> Encoding::CompatibilityError
    "ABC" =~ /./s #=> エラーにならない
    ASCII文字だけでテストしてると本番でエラー
    になったりこわい

    View full-size slide

  59. 本当はこわいエンコーディングの話 Powered by Rabbit 2.0.6
    まとめ
    内部のエンコーディングは UTF-8 に統一
    しよう
    IO 読み込みで ASCII-8BIT になってるこ
    とがある
    外部からのデータは不正な文字が入ってる
    ことがある
    ちゃんと気をつければそんなに
    こわくない

    View full-size slide