Javaの正規表現における下位サロゲートの問題ある挙動

Java正規表現で下位サロゲートコードにマッチするパターンを書くと、完全なサロゲートペアの後半16ビットにマッチしてしまうことがあります。少なくとも、次のバージョンはそのような挙動を示します。

  • Oracle Hotspot 1.8.0_72
  • OpenJDK 1.8.0_66

詳細

次のパターンは、問題を起こす正規表現Javaの文字列リテラルで表したものです。それぞれ、単一の下位サロゲートU+DC00あるいは下位サロゲート全体の文字クラス (U+DC00-U+DFFF) を表しています。

  • "\\udc00"
  • "\\x{dc00}"
  • "[\\udc00-\\udfff]"
  • "[\\x{dc00}-\\x{dfff}]"
  • "[\\p{blk=Low Surrogates}]"

この挙動は、最後のパターンを使って、孤立した下位サロゲートを検出するプログラムを書いている時に発見しました。孤立した下位サロゲートとは、上位サロゲートコード (U+D800-U+DBFF) に後続していない下位サロゲートのことです。UTF-16エンコードにおいて、孤立したサロゲートコードは “ill-formed” と定義されていますが、Javaの文字列値はこのようなシーケンスを含む可能性があります。たとえば">>\udc00<<"のように。

上記のパターンは期待通りに孤立したサロゲートコードにマッチしましたが、完全なサロゲートペアの後半16ビットにもマッチしてしまいました。たとえば、コードポイントU+010000を表す"\ud800\udc00"の、2つめのcharにマッチしてしまいます。

Pattern regex = Pattern.compile("[\\p{blk=Low Surrogates}]");
Matcher matcher = regex.matcher("\ud800\udc00");  // U+010000
System.out.println(matcher.find());   // => true
System.out.println(matcher.start());  // => 1
System.out.println(matcher.end());    // => 2

この挙動は、Unicode Technical Standard #18 の “RL1.7 Supplementary Code Points”に違反しているように見えます。ここでは次のように規定されています。

UTF-16を使う場合、先行するサロゲートと後続するサロゲートのペアからなるシーケンスは、マッチにおいて単一のコードポイントとして扱わなければならない。

回避策

生のサロゲートコードをPattern#compileに渡して生成したパターンは、孤立したサロゲートコードのみにマッチし、完全なサロゲートペアの一部にはマッチしません。

Pattern p1 = Pattern.compile("\udc00");
System.out.println(p1.matcher("\ud800\udc00").find());
System.out.println(p1.matcher(">>\udc00<<").find());
// => false true

Pattern p2 = Pattern.compile("[\udc00-\udfff]");
System.out.println(p2.matcher("\ud800\udc00").find());
System.out.println(p2.matcher(">>\udc00<<").find());
// => false true

ステータス

2月7日にJava Bug Reportに報告しました。進展があることを願っています。

JDK-8149446として登録されていました。*1

*1:2月10日8:30追記。