StringUtil に関するバグ (2010/10/27)

[flex]

(まずは基本)StringUtil#substitute() の使い方

StringUtil#substitute() は文字列の一部を別の値で置換するときに使います。

例1:

answer = 5 * 8;

message1 = "Answer is: {0}";
trace( StringUtil#substitute(message1, answer) ); //=> "Answer is: 40";

例2:

firstName = "Taro";
lastName = "Yamada";

message2 = "Hello, {0} {1}!";
trace( StringUtil#substitute(message2, firstName, lastName) );
//=> "Hello, Taro Yamada!"

これだけだと、あまりありがたみが分からないかもしれません。

そこで、例2のメッセージを翻訳して、日本ローカライズ版を作ることを考えます。

firstName = "Taro";
lastName = "Yamada";

message2 = "{1} {0} さん、こんにちは!";
trace( StringUtil#substitute(message2, firstName, lastName) );
//=> "Yamada Taro さん、こんにちは!"

注目したいのは、前のコードと変更があったのは message2 だけで、StringUtil#substitute() に渡す引数の順番などは変っていないことです。StringUtil#substitute() では、何番目の引数で置換するかを波括弧で囲む({0}や{1}など)ことで、引数の順番を任意に指定することができるのです。

メッセージを多言語対応する場合には、先の例で示したように、単にメッセージを翻訳するだけでは収まらず、語順(変数の順番)も変更しなければならないケースが多々あります。このような場合、通常の文字列置換や文字列連結では対応するのが難しくなります。

 

そこで威力を発揮するのが StringUtil#substitute() というわけです。

(本題)StringUtil#substitute() のバグ

と、いうわけで、多言語対応したいときは、メッセージの文字列置換に StringUtil#substitute() を使うことを考えましょう(もちろん、もっと根本的に考慮しておかなければならないことがいくつかありますが)。

 

めでたし、めでたし。。。とは行きません。

 

実は StringUtil#substitute() には、分かりにくいバグ(仕様?)があるのです。

(バグ1)波括弧文字にまつわるバグ

 

1つ目のバグはなんとな~く内部のプログラムが想像できれば分かるバグです。

 

引数の文字列に波括弧囲みの数字({1}など)を入れると、おかしな挙動をします。

firstName = "Ta{1}ro";
lastName = "Yamada";

message2 = "Hello, {0} {1}!";
trace( StringUtil#substitute(message2, firstName, lastName) );
//=> "Hello, TaYamadaro Yamada!"

({0}から順番に置換していってるんですね)

(バグ2)ドル記号($)にまつわるバグ

 

2つ目は、初見では理解し難いバグです。

 

引数の文字列にドル記号、それに続いて特定の記号(クォート、バッククォート、アンパサンド、ドル)を入れると、おかしな挙動をします。

firstName = "Taro$`";
lastName = "Yamada";

message2 = "Hello, {0} {1}!";
trace( StringUtil#substitute(message2, firstName, lastName) );
//=> "Hello, TaroHello, Yamada!"

上の例はドル($)の後にバッククォート(`)が続く場合です。何が起こっているか、分かりますか?

 

StringUtil#substitute() の中で正規表現(RegExp)が使われていることが分かれば、理解できるかもしれません。

 

解決策(StringUtil#substitute() の置き換え)

引数に {1} とか $` とか、そんな状況ってあまりないんじゃない?ただの揚げ足取りじゃない?

 

と思った方もいらっしゃるかもしれません。

 

でも、上の例のように文字列を別の値で置換するような場合、引数はユーザーからの入力など外部入力であることが多いのです。外部入力はいつ、いかなるデータが入ってくるか分かりません。

 

今回のバグは、幸いXSS等の悪質な攻撃に使われるおそれはありません。とはいえ、万が一このようなバグが露出したら、プログラマとしてあるいはサービサーとして恥ずかしいですよね。

 

というわけで、対策しましょう^-^)v

残念ながら両方のバグに対処するには、ロジックを根本から替えないといけません。なので、StringUtil#substitute() を別のメソッドを作って置換しちゃいます。

 

具体的なコードは以下のようになります。

// CustomStringUtil.as
package
{
public class CustomStringUtil
{
        public static function substitute(str:String, ... rest):String
        {
                if (str == null) return "";
                
                // Get the arguments.
                var args:Array;
                if (rest[0] is Array && rest.length == 1) args = rest[0] as Array;
                else args = rest;
                
                var pattern:RegExp = /\{(\d+)\}/g;
                var result:Array = [];
                var beginPos:int = 0;
                var matches:Object = pattern.exec(str);
                while (matches != null)
                {
                        // Save the string before the matched string.
                        result.push(str.substring(beginPos, matches.index));
                        
                        // Get the number. (if the matched string is "{2}",
                        // matches[0] is "{2}" and matches[1] is "2")
                        var num:int = parseInt(matches[1]);
                        if (args[num] != undefined) result.push(args[num]);
                        else result.push(matches[0]);
                        
                        beginPos = pattern.lastIndex;
                        matches = pattern.exec(str);
                }
                result.push(str.substring(beginPos, str.length));
                
                return result.join("");
        }
}
}

余談

ちなみにバグ2の方は、1年以上前にバグ報告されていますが、2010/10/27 現在の最新安定板である flex_sdk_4.1.0.16076 でも直っていません。

 

もしかしたら本当に仕様で通すつもりなのかもしれませんね。それならそれで ASDoc に明記しとくべきですが。