Feature #6752

Replacing ill-formed subsequencce

Added by Yui NARUSE almost 2 years ago. Updated about 1 year ago.

[ruby-dev:45975]
Status:Closed
Priority:Normal
Assignee:Yukihiro Matsumoto
Category:core
Target version:next minor

Description

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

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

== 必要な引数: 置換文字
省略可能、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 = rbencprecisembclen(p, e, enc); して、
MBCLEN
INVALIDP(ret) が真な時、何バイト目が不正なのかわからないのが微妙。
ONIGENC
CONSTRUCTMBCLENINVALID() がバイト数を取らないのが原因なので、
鬼車のエンコーディングモジュール全てに影響してしまうため、修正困難。
不正なバイトはほとんど存在しないと仮定して、効率を犠牲にすれば回避は可能。

=== 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 @@ rbstrellipsize(VALUE str, long len)
return ret;
}

+/*
+ * call-seq:
+ * str.fixinvalid -> newstr
+ *
+ * If the string is well-formed, it returns self.
+ * If the string has invalid byte sequence, repair it with given replacement
+ * character.
+ /
+VALUE
+rbstrfixinvalid(VALUE str)
+{
+ int cr = ENC
CODERANGE(str);
+ rbencoding *enc;
+ if (cr == ENC
CODERANGE7BIT || cr == ENCCODERANGEVALID)
+ return rb
strdup(str);
+
+ enc = STR
ENCGET(str);
+ if (rb
encasciicompat(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 = rbstrbufnew(RSTRINGLEN(str) + room);
+ const char *rep;
+ if (enc == rbutf8encoding())
+ rep = "\xEF\xBF\xBD";
+ else
+ rep = "?";
+ cr = ENCCODERANGE7BIT;
+
+ p = searchnonascii(p, e);
+ if (!p) {
+ p = e;
+ }
+ while (p < e) {
+ int ret = rb
encprecisembclen(p, e, enc);
+ if (MBCLENCHARFOUNDP(ret)) {
+ if ((unsigned char)
p > 127) cr = ENCCODERANGEVALID;
+ p += MBCLENCHARFOUNDLEN(ret);
+ }
+ else if (MBCLENINVALIDP(ret)) {
+ const char q;
+ long clen = rbencmbmaxlen(enc);
+ if (p > p1) rbstrbufcat(buf, p1, p - p1);
+ q = RSTRING
END(buf);
+
+ if (e - p < clen) clen = e - p;
+ if (clen < 3) {
+ clen = 1;
+ }
+ else {
+ long len = RSTRINGLEN(buf);
+ clen--;
+ rb
strbufcat(buf, p, clen);
+ for (; clen > 1; clen--) {
+ ret = rbencprecisembclen(q, q + clen, enc);
+ if (MBCLEN
NEEDMOREP(ret)) {
+ break;
+ }
+ else if (MBCLEN
INVALIDP(ret)) {
+ continue;
+ }
+ else {
+ rb
bug("shouldn't reach here '%s'", q);
+ }
+ }
+ rbstrsetlen(buf, len);
+ }
+ p += clen;
+ p1 = p;
+ rb
strbufcat2(buf, rep);
+ p = searchnonascii(p, e);
+ if (!p) {
+ p = e;
+ break;
+ }
+ }
+ else if (MBCLEN
NEEDMOREP(ret)) {
+ break;
+ }
+ else {
+ rb
bug("shouldn't reach here");
+ }
+ }
+ if (p1 < p) {
+ rbstrbufcat(buf, p1, p - p1);
+ }
+ if (p < e) {
+ rb
strbufcat2(buf, rep);
+ cr = ENCCODERANGEVALID;
+ }
+ ENCODINGCODERANGESET(buf, rbenctoindex(enc), cr);
+ return buf;
+ }
+ else if (rb
encdummyp(enc)) {
+ return rbstrdup(str);
+ }
+ else {
+ /
ASCII incompatible /
+ const char *p = RSTRINGPTR(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 = rbstrbufnew(RSTRINGLEN(str) + room);
+ const char *rep;
+ long mbminlen = rbencmbminlen(enc);
+ static rbencoding *utf16be;
+ static rb
encoding *utf16le;
+ static rbencoding *utf32be;
+ static rb
encoding *utf32le;
+ if (!utf16be) {
+ utf16be = rbencfind("UTF-16BE");
+ utf16le = rbencfind("UTF-16LE");
+ utf32be = rbencfind("UTF-32BE");
+ utf32le = rbencfind("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 = rbencprecisembclen(p, e, enc);
+ if (MBCLEN
CHARFOUNDP(ret)) {
+ p += MBCLEN
CHARFOUNDLEN(ret);
+ }
+ else if (MBCLEN
INVALIDP(ret)) {
+ const char *q;
+ long clen = rb
encmbmaxlen(enc);
+ if (p > p1) rb
strbufcat(buf, p1, p - p1);
+ q = RSTRINGEND(buf);
+
+ if (e - p < clen) clen = e - p;
+ if (clen < mbminlen * 3) {
+ clen = mbminlen;
+ }
+ else {
+ long len = RSTRING
LEN(buf);
+ clen -= mbminlen;
+ rbstrbufcat(buf, p, clen);
+ for (; clen > mbminlen; clen-=mbminlen) {
+ ret = rb
encprecisembclen(q, q + clen, enc);
+ if (MBCLENNEEDMOREP(ret)) {
+ break;
+ }
+ else if (MBCLENINVALIDP(ret)) {
+ continue;
+ }
+ else {
+ rbbug("shouldn't reach here '%s'", q);
+ }
+ }
+ rb
strsetlen(buf, len);
+ }
+ p += clen;
+ p1 = p;
+ rbstrbufcat2(buf, rep);
+ }
+ else if (MBCLEN
NEEDMOREP(ret)) {
+ break;
+ }
+ else {
+ rb
bug("shouldn't reach here");
+ }
+ }
+ if (p1 < p) {
+ rbstrbufcat(buf, p1, p - p1);
+ }
+ if (p < e) {
+ rb
strbufcat2(buf, rep);
+ }
+ ENCODINGCODERANGESET(buf, rbenctoindex(enc), ENCCODERANGE_VALID);
+ return buf;
+ }
+}
+
/
*********************************************************************
* Document-class: Symbol
*
@@ -7882,6 +8075,7 @@ InitString(void)
rb
definemethod(rbcString, "getbyte", rbstrgetbyte, 1);
rbdefinemethod(rbcString, "setbyte", rbstrsetbyte, 2);
rb
definemethod(rbcString, "byteslice", rbstrbyteslice, -1);
+ rbdefinemethod(rbcString, "fixinvalid", rbstrfix_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/teststring.rb b/test/ruby/teststring.rb
index 47f349c..2b0cfeb 100644
--- a/test/ruby/teststring.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 testfixinvalid
+ assertequal("\uFFFD\uFFFD\uFFFD", "\x80\x80\x80".fixinvalid)
+ assertequal("\uFFFDA", "\xF4\x80\x80A".fixinvalid)
+
+ # exapmles in Unicode 6.1.0 D93b
+ assertequal("\x41\uFFFD\uFFFD\x41\uFFFD\x41",
+ "\x41\xC0\xAF\x41\xF4\x80\x80\x41".fix
invalid)
+ assertequal("\x41\uFFFD\uFFFD\uFFFD\x41",
+ "\x41\xE0\x9F\x80\x41".fix
invalid)
+ assertequal("\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)
+
+ assertequal("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)
+
+ assertequal("\uFFFD\u3042".encode("UTF-16BE"),
+ "\xD8\x00\x30\x42".force
encoding(Encoding::UTF16BE).
+ fix
invalid)
+ assertequal("\uFFFD\u3042".encode("UTF-16LE"),
+ "\x00\xD8\x42\x30".force
encoding(Encoding::UTF16LE).
+ fix
invalid)
+ end
end

class TestString2 < TestString
=end


Related issues

Related to ruby-trunk - Feature #6321: Find and repair bad bytes in encodings, without transcoding Closed 04/19/2012
Related to ruby-trunk - Bug #7967: String#encode invalid: :replace doesn't replace invalid c... Rejected 02/26/2013

Associated revisions

Revision 40391
Added by Yui NARUSE about 1 year ago

Add example for String#scrub

[Feature #6321] [Feature #6752] [Bug #7967]

Revision 40416
Added by Nobuyoshi Nakada about 1 year ago

string.c: suppress warnings

  • string.c (rbstrscrub): suppress maybe-uninitialized and empty body in an else-statement. [Feature #6752]

Revision 40417
Added by Nobuyoshi Nakada about 1 year ago

string.c: fix for UTF-32

  • string.c (rbstrscrub): fix for UTF-32. strlen() on strings contain NUL returns wrong result, use sizeof operator instead. [Feature #6752]

History

#1 Updated by Nobuyoshi Nakada over 1 year ago

  • Description updated (diff)

#2 Updated by Motohiro KOSAKI over 1 year ago

今日のなるせさん、中田さんとのTwitter上での議論をもとにいくつかリクエスト(というか備忘録)

・個人的にはencode()よりも専用メソッド推し。理由は頻度。入力の正当性チェックなんてそこら中に需要あると思う
・メソッド名は replaceinvalidcharacter みたいな思いっきり説明的な名前でいいような気がします。これをメソッドチェインしないでしょう。
あと、encode が invalid => replace なので用語合わせたほうがいい気がします。長すぎるなら replace_invalid で。
・オプショナル引数で置換文字(列)を変更できるようにしてほしい Unicode系でもU+FFFD がうれしくないケースは、ままありそう

#3 Updated by Martin Dürst over 1 year ago

  • Target version changed from 2.0.0 to next minor

I have thought about this a bit. Yui's patch to string treats this as a problem separat from transcoding. I think it is preferable to use the transcoding logic to implement this. The advantage is that exactly the same options and fallbacks can be used, and if we add a new option or fallback to transcode, it will be usable, too.

Some more notes: The checks for converting from one encoding to the same encoding are in str_transcode0. Anywhere else? We need some data to drive the conversion, but this should be easy to generate, and will be the same for many 8-bit encodings.

It will be easy to catch invalid byte sequences, but I'm not sure it's worth to check unassigned codepoints, at least not in Unicode.

I have changed the target version from 2.0.0 to next minor, because I don't think this will be ready for 2.0.0. But please change back if somebody can do it faster.

#4 Updated by Akinori MUSHA over 1 year ago

=begin
(('+1')) for the functionality.

What we currently have (Encoding::Converter) is not enough to solve this problem because 1) it is mandatory to pick a different encoding to convert to for nothing, and even if you have decided to pick one 2) #primitive_errinfo does not give you offset information that is necessary to locate where the found invalid bytes are.

In addition to this proposal, I'd like String#encode to accept a proc instead of a fixed string as a :replace option to get a callback for each invalid byte so you can dynamically compose a replace string for the given invalid byte. Perl's encode() and decode() have this feature and it's very handy to investigate and visualize how a string is garbled. (e.g. (({replace: ->(byte) { "\e[7m<%02X>\e[m" % byte }})))

I don't have a particular opinion about the API, but having self-transcoders perform validation as @duerst implies sounds great to me if it could be properly implemented.

My tentative solution that is known to be very slow is here: ((URL:https://gist.github.com/4491446))
=end

#5 Updated by Yui NARUSE about 1 year ago

duerst (Martin Dürst) wrote:

I have thought about this a bit. Yui's patch to string treats this as a problem separat from transcoding. I think it is preferable to use the transcoding logic to implement this. The advantage is that exactly the same options and fallbacks can be used, and if we add a new option or fallback to transcode, it will be usable, too.

This method doesn't need same options and fallbacks.
It need only invalid related, doesn't need undef related.
Moreover transcoder is usable only if Ruby has related transcoder of the target encoding.
But Ruby has some encodings which doesn't have transcoder for example emacs-mule.
Therefore this can't be built on transcoder.

Some more notes: The checks for converting from one encoding to the same encoding are in str_transcode0. Anywhere else? We need some data to drive the conversion, but this should be easy to generate, and will be the same for many 8-bit encodings.

Yeah, I came to str_transcode0 and it is correct place.

The date we need is problem.
transcode doesn't have all the data though tool/transcode-tblgen.rb has some base data.
The only one which has all the data we need is enc/*.

It will be easy to catch invalid byte sequences, but I'm not sure it's worth to check unassigned codepoints, at least not in Unicode.

If we need unassigned codepoints, we must define encodings more strictly.
Even if it is Unicode, it needs versions.
I don't think it's worth to check.

#6 Updated by Yui NARUSE about 1 year ago

I wrote a updated patch which include String#scrub and String#encode with extension.
String#scrub allows replacement as both argument or block.

diff --git a/string.c b/string.c
index 8b85739..bc973dc 100644
--- a/string.c
+++ b/string.c
@@ -7741,6 +7741,271 @@ rbstrellipsize(VALUE str, long len)
return ret;
}

+static VALUE
+strcompatandvalid(VALUE str, rbencoding enc)
+{
+ int cr;
+ str = StringValue(str);
+ cr = rbencstrcoderange(str);
+ if (cr == ENC
CODERANGEBROKEN) {
+ rb
raise(rbeArgError, "replacement must be valid byte sequence '%+"PRIsVALUE"'", str);
+ }
+ else if (cr == ENC
CODERANGE7BIT) {
+ rb
encoding *e = STRENCGET(str);
+ if (!rbencasciicompat(enc)) {
+ rbraise(rbeEncCompatError, "incompatible character encodings: %s and %s",
+ rbencname(enc), rbencname(e));
+ }
+ }
+ else { /
ENCCODERANGEVALID /
+ rbencoding *e = STRENCGET(str);
+ if (enc != e) {
+ rb
raise(rbeEncCompatError, "incompatible character encodings: %s and %s",
+ rb
encname(enc), rbenc_name(e));
+ }
+ }
+ return str;
+}
+
+/

+ * call-seq:
+ * str.scrub -> newstr
+ * str.scrub(repl) -> new
str
+ * str.scrub{|bytes|} -> newstr
+ *
+ * If the string is invalid byte sequence then replace invalid bytes with given replacement
+ * character, else returns self.
+ */
+VALUE
+rb
strscrub(int argc, VALUE *argv, VALUE str)
+{
+ int cr = ENC
CODERANGE(str);
+ rbencoding *enc;
+ VALUE repl;
+
+ if (cr == ENC
CODERANGE7BIT || cr == ENCCODERANGEVALID)
+ return rb
strdup(str);
+
+ enc = STR
ENCGET(str);
+ rb
scanargs(argc, argv, "01", &repl);
+ if (argc != 0) {
+ repl = str
compatandvalid(repl, enc);
+ }
+
+ if (rbencdummyp(enc)) {
+ return rb
strdup(str);
+ }
+
+ if (rb
encasciicompat(enc)) {
+ const char *p = RSTRING
PTR(str);
+ const char e = RSTRINGEND(str);
+ const char *p1 = p;
+ const char *rep;
+ long replen;
+ int rep7bit
p;
+ VALUE buf = rbstrbufnew(RSTRINGLEN(str));
+ if (rbblockgivenp()) {
+ rep = NULL;
+ }
+ else if (!NIL
P(repl)) {
+ rep = RSTRINGPTR(repl);
+ replen = RSTRING
LEN(repl);
+ rep7bitp = (ENCCODERANGE(repl) == ENCCODERANGE7BIT);
+ }
+ else if (enc == rbutf8encoding()) {
+ rep = "\xEF\xBF\xBD";
+ replen = strlen(rep);
+ rep7bitp = FALSE;
+ }
+ else {
+ rep = "?";
+ replen = strlen(rep);
+ rep7bit
p = TRUE;
+ }
+ cr = ENCCODERANGE7BIT;
+
+ p = searchnonascii(p, e);
+ if (!p) {
+ p = e;
+ }
+ while (p < e) {
+ int ret = rb
encprecisembclen(p, e, enc);
+ if (MBCLENNEEDMOREP(ret)) {
+ break;
+ }
+ else if (MBCLENCHARFOUNDP(ret)) {
+ cr = ENCCODERANGEVALID;
+ p += MBCLENCHARFOUNDLEN(ret);
+ }
+ else if (MBCLENINVALIDP(ret)) {
+ /

+ * p1~p: valid ascii/multibyte chars
+ * p ~e: invalid bytes + unknown bytes
+ /
+ long clen = rbencmbmaxlen(enc);
+ if (p > p1) {
+ rbstrbufcat(buf, p1, p - p1);
+ }
+
+ if (e - p < clen) clen = e - p;
+ if (clen <= 2) {
+ clen = 1;
+ }
+ else {
+ const char *q = p;
+ clen--;
+ for (; clen > 1; clen--) {
+ ret = rb
encprecisembclen(q, q + clen, enc);
+ if (MBCLENNEEDMOREP(ret)) break;
+ else if (MBCLENINVALIDP(ret)) continue;
+ else UNREACHABLE;
+ }
+ }
+ if (rep) {
+ rbstrbufcat(buf, rep, replen);
+ if (!rep7bit
p) cr = ENCCODERANGEVALID;
+ }
+ else {
+ repl = rbyield(rbencstrnew(p1, clen, enc));
+ repl = strcompatandvalid(repl, enc);
+ rb
strbufcat(buf, RSTRINGPTR(repl), RSTRINGLEN(repl));
+ if (ENCCODERANGE(repl) == ENCCODERANGEVALID)
+ cr = ENC
CODERANGEVALID;
+ }
+ p += clen;
+ p1 = p;
+ p = search
nonascii(p, e);
+ if (!p) {
+ p = e;
+ break;
+ }
+ }
+ else {
+ UNREACHABLE;
+ }
+ }
+ if (p1 < p) {
+ rbstrbufcat(buf, p1, p - p1);
+ }
+ if (p < e) {
+ if (rep) {
+ rb
strbufcat(buf, rep, replen);
+ if (!rep7bitp) cr = ENCCODERANGEVALID;
+ }
+ else {
+ repl = rb
yield(rbencstrnew(p, e-p, enc));
+ repl = str
compatandvalid(repl, enc);
+ rbstrbufcat(buf, RSTRINGPTR(repl), RSTRINGLEN(repl));
+ if (ENC
CODERANGE(repl) == ENCCODERANGEVALID)
+ cr = ENCCODERANGEVALID;
+ }
+ }
+ ENCODINGCODERANGESET(buf, rbencto_index(enc), cr);
+ return buf;
+ }
+ else {
+ /
ASCII incompatible /
+ const char *p = RSTRINGPTR(str);
+ const char *e = RSTRING
END(str);
+ const char *p1 = p;
+ VALUE buf = rbstrbufnew(RSTRINGLEN(str));
+ const char *rep;
+ long replen;
+ long mbminlen = rbencmbminlen(enc);
+ static rbencoding *utf16be;
+ static rb
encoding *utf16le;
+ static rbencoding *utf32be;
+ static rb
encoding *utf32le;
+ if (!utf16be) {
+ utf16be = rbencfind("UTF-16BE");
+ utf16le = rbencfind("UTF-16LE");
+ utf32be = rbencfind("UTF-32BE");
+ utf32le = rbencfind("UTF-32LE");
+ }
+ if (!NILP(repl)) {
+ rep = RSTRING
PTR(repl);
+ replen = RSTRINGLEN(repl);
+ }
+ else if (enc == utf16be) {
+ rep = "\xFF\xFD";
+ replen = strlen(rep);
+ }
+ else if (enc == utf16le) {
+ rep = "\xFD\xFF";
+ replen = strlen(rep);
+ }
+ else if (enc == utf32be) {
+ rep = "\x00\x00\xFF\xFD";
+ replen = strlen(rep);
+ }
+ else if (enc == utf32le) {
+ rep = "\xFD\xFF\x00\x00";
+ replen = strlen(rep);
+ }
+ else {
+ rep = "?";
+ replen = strlen(rep);
+ }
+
+ while (p < e) {
+ int ret = rb
encprecisembclen(p, e, enc);
+ if (MBCLENNEEDMOREP(ret)) {
+ break;
+ }
+ else if (MBCLENCHARFOUNDP(ret)) {
+ p += MBCLENCHARFOUNDLEN(ret);
+ }
+ else if (MBCLENINVALIDP(ret)) {
+ const char *q = p;
+ long clen = rbencmbmaxlen(enc);
+ if (p > p1) rbstrbufcat(buf, p1, p - p1);
+
+ if (e - p < clen) clen = e - p;
+ if (clen <= mbminlen * 2) {
+ clen = mbminlen;
+ }
+ else {
+ clen -= mbminlen;
+ for (; clen > mbminlen; clen-=mbminlen) {
+ ret = rb
encprecisembclen(q, q + clen, enc);
+ if (MBCLENNEEDMOREP(ret)) break;
+ else if (MBCLENINVALIDP(ret)) continue;
+ else UNREACHABLE;
+ }
+ }
+ if (rep) {
+ rbstrbufcat(buf, rep, replen);
+ }
+ else {
+ repl = rb
yield(rbencstrnew(p, e-p, enc));
+ repl = str
compatandvalid(repl, enc);
+ rbstrbufcat(buf, RSTRINGPTR(repl), RSTRINGLEN(repl));
+ }
+ p += clen;
+ p1 = p;
+ }
+ else {
+ UNREACHABLE;
+ }
+ }
+ if (p1 < p) {
+ rb
strbufcat(buf, p1, p - p1);
+ }
+ if (p < e) {
+ if (rep) {
+ rbstrbufcat(buf, rep, replen);
+ }
+ else {
+ repl = rb
yield(rbencstrnew(p, e-p, enc));
+ repl = str
compatandvalid(repl, enc);
+ rbstrbufcat(buf, RSTRINGPTR(repl), RSTRINGLEN(repl));
+ }
+ }
+ ENCODING
CODERANGESET(buf, rbenctoindex(enc), ENCCODERANGEVALID);
+ return buf;
+ }
+}
+
/
*********************************************************************
* Document-class: Symbol
*
@@ -8222,6 +8487,7 @@ InitString(void)
rb
definemethod(rbcString, "getbyte", rbstrgetbyte, 1);
rbdefinemethod(rbcString, "setbyte", rbstrsetbyte, 2);
rb
definemethod(rbcString, "byteslice", rbstrbyteslice, -1);
+ rbdefinemethod(rbcString, "scrub", rbstr_scrub, -1);

 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/dl/testcallback.rb b/test/dl/testcallback.rb
index 8ae652b..ef24235 100644
--- a/test/dl/testcallback.rb
+++ b/test/dl/test
callback.rb
@@ -61,9 +61,11 @@ module DL
addr = setcallback(TYPEVOID, 1) do |foo|
called = true
end
+ addrnum = addr.to_int

   func = CFunc.new(addr, TYPE_VOID, 'test')
   f = Function.new(func, [TYPE_VOIDP])
  •  assert_equal(addrnum, f.to_i)
    

    f.call(nil)

    assert called, 'function should be called'
    diff --git a/test/ruby/testm17n.rb b/test/ruby/testm17n.rb
    index a8d56a4..60834bb 100644
    --- a/test/ruby/testm17n.rb
    +++ b/test/ruby/test
    m17n.rb
    @@ -1489,4 +1489,38 @@ class TestM17N < Test::Unit::TestCase
    s.untrust
    assert_equal(true, s.b.untrusted?)
    end
    +

  • def test_scrub

  • assert_equal("\uFFFD\uFFFD\uFFFD", u("\x80\x80\x80").scrub)

  • assert_equal("\uFFFDA", u("\xF4\x80\x80A").scrub)
    +

  • # exapmles in Unicode 6.1.0 D93b

  • assert_equal("\x41\uFFFD\uFFFD\x41\uFFFD\x41",

  •             u("\x41\xC0\xAF\x41\xF4\x80\x80\x41").scrub)
    
  • assert_equal("\x41\uFFFD\uFFFD\uFFFD\x41",

  •             u("\x41\xE0\x9F\x80\x41").scrub)
    
  • assert_equal("\u0061\uFFFD\uFFFD\uFFFD\u0062\uFFFD\u0063\uFFFD\uFFFD\u0064",

  •             u("\x61\xF1\x80\x80\xE1\x80\xC2\x62\x80\x63\x80\xBF\x64").scrub)
    
  • assert_equal("abcdefghijklmnopqrstuvwxyz\u0061\uFFFD\uFFFD\uFFFD\u0062\uFFFD\u0063\uFFFD\uFFFD\u0064",

  •             u("abcdefghijklmnopqrstuvwxyz\x61\xF1\x80\x80\xE1\x80\xC2\x62\x80\x63\x80\xBF\x64").scrub)
    

    +

  • assert_equal("\u3042\u3013", u("\xE3\x81\x82\xE3\x81").scrub("\u3013"))

  • assert_raise(Encoding::CompatibilityError){ u("\xE3\x81\x82\xE3\x81").scrub(e("\xA4\xA2")) }

  • assert_raise(TypeError){ u("\xE3\x81\x82\xE3\x81").scrub(1) }

  • assert_raise(ArgumentError){ u("\xE3\x81\x82\xE3\x81\x82\xE3\x81").scrub(u("\x81")) }

  • assert_equal(e("\xA4\xA2\xA2\xAE"), e("\xA4\xA2\xA4").scrub(e("\xA2\xAE")))
    +

  • assert_equal("\u3042", u("\xE3\x81\x82\xE3\x81").scrub{|x|'<'+x.unpack('H*')[0]+'>'})

  • assert_raise(Encoding::CompatibilityError){ u("\xE3\x81\x82\xE3\x81").scrub{e("\xA4\xA2")} }

  • assert_raise(TypeError){ u("\xE3\x81\x82\xE3\x81").scrub{1} }

  • assert_raise(ArgumentError){ u("\xE3\x81\x82\xE3\x81\x82\xE3\x81").scrub{u("\x81")} }

  • assert_equal(e("\xA4\xA2\xA2\xAE"), e("\xA4\xA2\xA4").scrub{e("\xA2\xAE")})
    +

  • assert_equal("\uFFFD\u3042".encode("UTF-16BE"),

  •             "\xD8\x00\x30\x42".force_encoding(Encoding::UTF_16BE).
    
  •             scrub)
    
  • assert_equal("\uFFFD\u3042".encode("UTF-16LE"),

  •             "\x00\xD8\x42\x30".force_encoding(Encoding::UTF_16LE).
    
  •             scrub)
    
  • end
    end
    diff --git a/transcode.c b/transcode.c
    index de12c04..9c940ed 100644
    --- a/transcode.c
    +++ b/transcode.c
    @@ -2652,6 +2652,8 @@ strtranscodeenc_args(VALUE str, volatile VALUE *arg1, volatile VALUE *arg2,
    return dencidx;
    }

+VALUE rbstrscrub(int argc, VALUE *argv, VALUE str);
+
static int
strtranscode0(int argc, VALUE *argv, VALUE *self, int ecflags, VALUE ecopts)
{
@@ -2686,6 +2688,17 @@ str
transcode0(int argc, VALUE *argv, VALUE *self, int ecflags, VALUE ecopts)
ECONVXMLATTRCONTENTDECORATOR|
ECONVXMLATTRQUOTEDECORATOR)) == 0) {
if (senc && senc == denc) {
+ if (ecflags & ECONVINVALIDMASK) {
+ if (!NILP(ecopts)) {
+ VALUE rep = rb
hasharef(ecopts, symreplace);
+ dest = rbstrscrub(1, &rep, str);
+ }
+ else {
+ dest = rbstrscrub(0, NULL, str);
+ }
+ *self = dest;
+ return dencidx;
+ }
return NILP(arg2) ? -1 : dencidx;
}
if (senc && denc && rb
encasciicompat(senc) && rbenc_asciicompat(denc)) {

#7 Updated by Yui NARUSE about 1 year ago

といわけで、 の通り patch を更新し、提案を以下の通り更新しましたので、まつもとさんコメントいただけませんか。

= 不正なバイト列なStringを綺麗にするやつ

== ここまでのあらすじ
趣旨は認められつつも、String#encode を使う API 案と独自名の API 案が出てまとまらなかった。

== 本提案の概要
* API は String#encoode を用いるものと新メソッドの2つを提供する
* 新メソッドの名称は String#scrub とする

== API

API は String#encoode を用いるものと新しいメソッドである String#scrub の2つを提供する。

== String#encode

本 API が用意されるべき理由は、iconv からの連想でこの API を用いて本用途が実現されると
期待する人が多いからである。
一方で、String#encode は CSI な変換エンジンを持つ Ruby では引数が複雑になりがちである。
例えば、典型的な UTF-8 の文字列を修復したい場合、

str.encode("UTF-8", invalid: :replace)

置換文字列を指定したい場合には

str.encode("UTF-8", invalid: :replace, replace: "\u3013")

などとなります。
これにさらにブロックを加えてより複雑な fallback を実現したくなるわけが、
Ruby M17N では CSI な多段変換を用いているので、一つのブロックで fallback を
実現しようとすると複雑になってしまうため、従来よりそのような機能は見送ってきた。
具体的には、ブロックが呼ばれる際には、
* invalid か undef か
* from encoding (多段の途中では思いもよらぬエンコーディングのことがある)
* to encoding (多段の途中では思いもよらぬエンコーディングのことがある)
というようなパラメータが存在する。
そのため、1つのブロックに詰め込むには複雑にすぎると思われる。

== String#scrub

本用途に特化した、よりシンプルな API を提供すると同時に、
ブロックを用いた fallback 等の高度な操作をも可能とする新 API。
名前は zpool scrub より。
http://docs.oracle.com/cd/E19253-01/819-6260/gbbxi/

=== str.scrub

Unicode 系ならば U+FFFD (Replacement Character) を置換文字とし、
それ以外の場合は ? を置換文字とする。

=== str.scrub("**")

指定した文字列を置換文字とする。

=== str.scrub{|x| '<'+x.ord.to_s(16)+'>' }

ブロック引数として不正なバイト列を与え、引数を置換文字とする。

#8 Updated by Yukihiro Matsumoto about 1 year ago

OK, I agree with enhancement of String#encoding and String#scrub.

Matz.

#9 Updated by Yui NARUSE about 1 year ago

  • Status changed from Assigned to Closed
  • % Done changed from 0 to 100

This issue was solved with changeset r40391.
Yui, thank you for reporting this issue.
Your contribution to Ruby is greatly appreciated.
May Ruby be with you.


Add example for String#scrub

[Feature #6321] [Feature #6752] [Bug #7967]

Also available in: Atom PDF