Project

General

Profile

Feature #6752

Updated by nobu (Nobuyoshi Nakada) almost 8 years ago

=begin 
 == 概要 
 Stringになんらかの理由で不正なバイト列が含まれている時に、それを置換文字で置き換えたい。 

 == ユースケース 
 実際に確認されているユースケースは以下の通りです。 
 * twitterのtitle 
 * IRCのログ 
 * ニコニコ動画の API 
  
 * Webクローリング 
 これらの不正なバイト列の生成過程は、おそらく、バイト単位で文字列を切り詰めた時に末尾が切れて、 
 末尾がおかしい不正な文字列が作られます。(前二者) 
 これをコンテナに入れたり結合することによって、途中にも混ざった文字列が作られます。(後二者) 

 * https://twitter.com/takahashim/status/18974040397 
 * https://twitter.com/n0kada/status/215674740705210368 
 * https://twitter.com/n0kada/status/215686490070585346 
 * https://twitter.com/hajimehoshi/status/215671146769682432 
 * http://po-ru.com/diary/fixing-invalid-utf-8-in-ruby-revisited/ 
 * http://stackoverflow.com/questions/2982677/ruby-1-9-invalid-byte-sequence-in-utf-8 

 == 必要な引数: 置換文字 
 省略可能、String。 
 デフォルトは、Unicode系ならU+FFFD、それ以外では「?」。 
 デフォルトが空文字でない理由は、削除してしまうことで、従来は存在しなかったトークンを作れてしまい、 
 上位のレイヤーの脆弱性に繋がるからです。 
 http://unicode.org/reports/tr36/#UTF-8_Exploit 

 == API 
 --- === str.encode(str.encoding, invalid: replace, [replace: "〓"]) 
 * CSI的じゃなくて気持ち悪い 
 * iconv でできるのは glibc iconv か GNU libiconv に //IGNORE つけた時で他はできない 
 * 実装上のメリットは後述の通り、直感に反してあまりない(と思う) 

 == 別メソッド 
 * 新しいメソッドである 
 * fix/repair invalid/illegal bytes/sequence あたりの名前か 

 == 実装 
 === 鬼車ベース 
 int ret = rb_enc_precise_mbclen(p, e, enc); して、 
 MBCLEN_INVALID_P(ret) が真な時、何バイト目が不正なのかわからないのが微妙。 
 ONIGENC_CONSTRUCT_MBCLEN_INVALID() がバイト数を取らないのが原因なので、 
 鬼車のエンコーディングモジュール全てに影響してしまうため、修正困難。 
 不正なバイトはほとんど存在しないと仮定して、効率を犠牲にすれば回避は可能。 

 === transcodeベース 
 UCS正規化なglibc iconv, GNU libiconv, Perl Encodeなどと違って、 
 CSIなtranscodeでは、自分自身に変換する場合、 
 エンコーディングごとに「何もしない」変換モジュールを用意しないといけない。 


 とりあえず鬼車ベースのコンセプト実装とテストを添付しておきます。 

  

 diff --git a/string.c b/string.c 
  
 index d038835..4808f15 100644 
  
 --- a/string.c 
  
 +++ b/string.c 
  
 @@ -7426,6 +7426,199 @@ rb_str_ellipsize(VALUE str, long len) 
       
      return ret; 
   
  } 
  
  
 
 +/* 
  
 + *    call-seq: 
  
 + *      str.fix_invalid -> new_str 
  
 + * 
  
 + *    If the string is well-formed, it returns self. 
  
 + *    If the string has invalid byte sequence, repair it with given replacement 
  
 + *    character. 
  
 + */ 
  
 +VALUE 
  
 +rb_str_fix_invalid(VALUE str) 
  
 +{ 
  
 +      int cr = ENC_CODERANGE(str); 
  
 +      rb_encoding *enc; 
  
 +      if (cr == ENC_CODERANGE_7BIT || cr == ENC_CODERANGE_VALID) 
  
 + 	 return rb_str_dup(str); 
  
 + 
  
 +      enc = STR_ENC_GET(str); 
  
 +      if (rb_enc_asciicompat(enc)) { 
  
 + 	 const char *p = RSTRING_PTR(str); 
  
 + 	 const char *e = RSTRING_END(str); 
  
 + 	 const char *p1 = p; 
  
 + 	 /* 10 should be enough for the usual use case, 
  
 + 	  * fixing a wrongly chopped character at the end of the string 
  
 + 	  */ 
  
 + 	 long room = 10; 
  
 + 	 VALUE buf = rb_str_buf_new(RSTRING_LEN(str) + room); 
  
 + 	 const char *rep; 
  
 + 	 if (enc == rb_utf8_encoding()) 
  
 + 	     rep = "\xEF\xBF\xBD"; 
  
 + 	 else 
  
 + 	     rep = "?"; 
  
 + 	 cr = ENC_CODERANGE_7BIT; 
  
 + 
  
 + 	 p = search_nonascii(p, e); 
  
 + 	 if (!p) { 
  
 + 	     p = e; 
  
 + 	 } 
  
 + 	 while (p < e) { 
  
 + 	     int ret = rb_enc_precise_mbclen(p, e, enc); 
  
 + 	     if (MBCLEN_CHARFOUND_P(ret)) { 
  
 + 		 if ((unsigned char)*p > 127) cr = ENC_CODERANGE_VALID; 
  
 + 		 p += MBCLEN_CHARFOUND_LEN(ret); 
  
 + 	     } 
  
 + 	     else if (MBCLEN_INVALID_P(ret)) { 
  
 + 		 const char *q; 
  
 + 		 long clen = rb_enc_mbmaxlen(enc); 
  
 + 		 if (p > p1) rb_str_buf_cat(buf, p1, p - p1); 
  
 + 		 q = RSTRING_END(buf); 
  
 + 
  
 + 		 if (e - p < clen) clen = e - p; 
  
 + 		 if (clen < 3) { 
  
 + 		     clen = 1; 
  
 + 		 } 
  
 + 		 else { 
  
 + 		     long len = RSTRING_LEN(buf); 
  
 + 		     clen--; 
  
 + 		     rb_str_buf_cat(buf, p, clen); 
  
 + 		     for (; clen > 1; clen--) { 
  
 + 			 ret = rb_enc_precise_mbclen(q, q + clen, enc); 
  
 + 			 if (MBCLEN_NEEDMORE_P(ret)) { 
  
 + 			     break; 
  
 + 			 } 
  
 + 			 else if (MBCLEN_INVALID_P(ret)) { 
  
 + 			     continue; 
  
 + 			 } 
  
 + 			 else { 
  
 + 			     rb_bug("shouldn't reach here '%s'", q); 
  
 + 			 } 
  
 + 		     } 
  
 + 		     rb_str_set_len(buf, len); 
  
 + 		 } 
  
 + 		 p += clen; 
  
 + 		 p1 = p; 
  
 + 		 rb_str_buf_cat2(buf, rep); 
  
 + 		 p = search_nonascii(p, e); 
  
 + 		 if (!p) { 
  
 + 		     p = e; 
  
 + 		     break; 
  
 + 		 } 
  
 + 	     } 
  
 + 	     else if (MBCLEN_NEEDMORE_P(ret)) { 
  
 + 		 break; 
  
 + 	     } 
  
 + 	     else { 
  
 + 		 rb_bug("shouldn't reach here"); 
  
 + 	     } 
  
 + 	 } 
  
 + 	 if (p1 < p) { 
  
 + 	     rb_str_buf_cat(buf, p1, p - p1); 
  
 + 	 } 
  
 + 	 if (p < e) { 
  
 + 	     rb_str_buf_cat2(buf, rep); 
  
 + 	     cr = ENC_CODERANGE_VALID; 
  
 + 	 } 
  
 + 	 ENCODING_CODERANGE_SET(buf, rb_enc_to_index(enc), cr); 
  
 + 	 return buf; 
  
 +      } 
  
 +      else if (rb_enc_dummy_p(enc)) { 
  
 + 	 return rb_str_dup(str); 
  
 +      } 
  
 +      else { 
  
 + 	 /* ASCII incompatible */ 
  
 + 	 const char *p = RSTRING_PTR(str); 
  
 + 	 const char *e = RSTRING_END(str); 
  
 + 	 const char *p1 = p; 
  
 + 	 /* 10 should be enough for the usual use case, 
  
 + 	  * fixing a wrongly chopped character at the end of the string 
  
 + 	  */ 
  
 + 	 long room = 10; 
  
 + 	 VALUE buf = rb_str_buf_new(RSTRING_LEN(str) + room); 
  
 + 	 const char *rep; 
  
 + 	 long mbminlen = rb_enc_mbminlen(enc); 
  
 + 	 static rb_encoding *utf16be; 
  
 + 	 static rb_encoding *utf16le; 
  
 + 	 static rb_encoding *utf32be; 
  
 + 	 static rb_encoding *utf32le; 
  
 + 	 if (!utf16be) { 
  
 + 	     utf16be = rb_enc_find("UTF-16BE"); 
  
 + 	     utf16le = rb_enc_find("UTF-16LE"); 
  
 + 	     utf32be = rb_enc_find("UTF-32BE"); 
  
 + 	     utf32le = rb_enc_find("UTF-32LE"); 
  
 + 	 } 
  
 + 	 if (enc == utf16be) { 
  
 + 	     rep = "\xFF\xFD"; 
  
 + 	 } 
  
 + 	 else if (enc == utf16le) { 
  
 + 	     rep = "\xFD\xFF"; 
  
 + 	 } 
  
 + 	 else if (enc == utf32be) { 
  
 + 	     rep = "\x00\x00\xFF\xFD"; 
  
 + 	 } 
  
 + 	 else if (enc == utf32le) { 
  
 + 	     rep = "\xFD\xFF\x00\x00"; 
  
 + 	 } 
  
 + 	 else { 
  
 + 	     rep = "?"; 
  
 + 	 } 
  
 + 
  
 + 	 while (p < e) { 
  
 + 	     int ret = rb_enc_precise_mbclen(p, e, enc); 
  
 + 	     if (MBCLEN_CHARFOUND_P(ret)) { 
  
 + 		 p += MBCLEN_CHARFOUND_LEN(ret); 
  
 + 	     } 
  
 + 	     else if (MBCLEN_INVALID_P(ret)) { 
  
 + 		 const char *q; 
  
 + 		 long clen = rb_enc_mbmaxlen(enc); 
  
 + 		 if (p > p1) rb_str_buf_cat(buf, p1, p - p1); 
  
 + 		 q = RSTRING_END(buf); 
  
 + 
  
 + 		 if (e - p < clen) clen = e - p; 
  
 + 		 if (clen < mbminlen * 3) { 
  
 + 		     clen = mbminlen; 
  
 + 		 } 
  
 + 		 else { 
  
 + 		     long len = RSTRING_LEN(buf); 
  
 + 		     clen -= mbminlen; 
  
 + 		     rb_str_buf_cat(buf, p, clen); 
  
 + 		     for (; clen > mbminlen; clen-=mbminlen) { 
  
 + 			 ret = rb_enc_precise_mbclen(q, q + clen, enc); 
  
 + 			 if (MBCLEN_NEEDMORE_P(ret)) { 
  
 + 			     break; 
  
 + 			 } 
  
 + 			 else if (MBCLEN_INVALID_P(ret)) { 
  
 + 			     continue; 
  
 + 			 } 
  
 + 			 else { 
  
 + 			     rb_bug("shouldn't reach here '%s'", q); 
  
 + 			 } 
  
 + 		     } 
  
 + 		     rb_str_set_len(buf, len); 
  
 + 		 } 
  
 + 		 p += clen; 
  
 + 		 p1 = p; 
  
 + 		 rb_str_buf_cat2(buf, rep); 
  
 + 	     } 
  
 + 	     else if (MBCLEN_NEEDMORE_P(ret)) { 
  
 + 		 break; 
  
 + 	     } 
  
 + 	     else { 
  
 + 		 rb_bug("shouldn't reach here"); 
  
 + 	     } 
  
 + 	 } 
  
 + 	 if (p1 < p) { 
  
 + 	     rb_str_buf_cat(buf, p1, p - p1); 
  
 + 	 } 
  
 + 	 if (p < e) { 
  
 + 	     rb_str_buf_cat2(buf, rep); 
  
 + 	 } 
  
 + 	 ENCODING_CODERANGE_SET(buf, rb_enc_to_index(enc), ENC_CODERANGE_VALID); 
  
 + 	 return buf; 
  
 +      } 
  
 +} 
 + 
  + /********************************************************************** 
   /********************************************************************** 
    * Document-class: Symbol 
    
   * 
  
 @@ -7882,6 +8075,7 @@ Init_String(void) 
       
      rb_define_method(rb_cString, "getbyte", rb_str_getbyte, 1); 
       
      rb_define_method(rb_cString, "setbyte", rb_str_setbyte, 2); 
       
      rb_define_method(rb_cString, "byteslice", rb_str_byteslice, -1); 
  
 +      rb_define_method(rb_cString, "fix_invalid", rb_str_fix_invalid, 0); 
  
       
 
      rb_define_method(rb_cString, "to_i", rb_str_to_i, -1); 
       
      rb_define_method(rb_cString, "to_f", rb_str_to_f, 0); 
  
 diff --git a/test/ruby/test_string.rb b/test/ruby/test_string.rb 
  
 index 47f349c..2b0cfeb 100644 
  
 --- a/test/ruby/test_string.rb 
  
 +++ b/test/ruby/test_string.rb 
  
 @@ -2031,6 +2031,29 @@ class TestString < Test::Unit::TestCase 
  
       
 
      assert_equal(u("\x82")+("\u3042"*9), ("\u3042"*10).byteslice(2, 28)) 
     
    end 
  
 + 
  
 +    def test_fix_invalid 
  
 +      assert_equal("\uFFFD\uFFFD\uFFFD", "\x80\x80\x80".fix_invalid) 
  
 +      assert_equal("\uFFFDA", "\xF4\x80\x80A".fix_invalid) 
  
 + 
  
 +      # exapmles in Unicode 6.1.0 D93b 
  
 +      assert_equal("\x41\uFFFD\uFFFD\x41\uFFFD\x41", 
  
 +                   "\x41\xC0\xAF\x41\xF4\x80\x80\x41".fix_invalid) 
  
 +      assert_equal("\x41\uFFFD\uFFFD\uFFFD\x41", 
  
 +                   "\x41\xE0\x9F\x80\x41".fix_invalid) 
  
 +      assert_equal("\u0061\uFFFD\uFFFD\uFFFD\u0062\uFFFD\u0063\uFFFD\uFFFD\u0064", 
  
 +                   "\x61\xF1\x80\x80\xE1\x80\xC2\x62\x80\x63\x80\xBF\x64".fix_invalid) 
  
 + 
  
 +      assert_equal("abcdefghijklmnopqrstuvwxyz\u0061\uFFFD\uFFFD\uFFFD\u0062\uFFFD\u0063\uFFFD\uFFFD\u0064", 
  
 +                   "abcdefghijklmnopqrstuvwxyz\x61\xF1\x80\x80\xE1\x80\xC2\x62\x80\x63\x80\xBF\x64".fix_invalid) 
  
 + 
  
 +      assert_equal("\uFFFD\u3042".encode("UTF-16BE"), 
  
 +                   "\xD8\x00\x30\x42".force_encoding(Encoding::UTF_16BE). 
  
 +                   fix_invalid) 
  
 +      assert_equal("\uFFFD\u3042".encode("UTF-16LE"), 
  
 +                   "\x00\xD8\x42\x30".force_encoding(Encoding::UTF_16LE). 
  
 +                   fix_invalid) 
  
 +    end 
   
  end 
  
   
 
  class TestString2 < TestString 
 =end 

Back