日本語が正しく表示されません。(JavaでWEBアプリを作っています)
2006年02月19日20:22
今回は、プログラマさん系のネタで恐縮です。 それに、この話題はプログラマさんでも、もう聞き飽きているような内容です。 でも、そこをあえて語ってみたいと思います。
この問題がなぜ発生するのかは、今ではとても有名です。次のようなTIPSがあります。
http://www.ingrid.org/java/jserv/i18n/corruptedchar.html
一般的には次のような理由で問題が発生すると考えられています。 実は、JavaVMは内部でUnicode16で処理を行っており、Shift_JISなどの文字が入力されると内部で一旦変換されてから処理されます。そして、出力する際に再び元に戻してユーザーに渡ります。 ところがこの時変換の前提となるコードが狂ってしまうことがしばしば起こります。 それは、入力の文字コードと出力の文字コードを明示的に指定するメカニズムが根本的に欠落している事がそもそもの原因です。
入力/出力文字コードが JavaVMのデフォルト文字コードで固定されている実装のアプリケーションサーバーが多いからです。
この問題が発生するかしないかは、それぞれのアプリケーションサーバーの実装の仕方によるところが大きく、中身を見せてくれないブラックボックス的なアプリケーションサーバーでは、問題を予想したり解決したりするのは、非常に困難です。
だから、すっきりしないのですが、あちらこちらの設定をゴチャゴチャと変更してごまかしているのが現状ではないでしょうか。
ところで、しばらくゴチャゴチャいじっていると、偶然EUC-JPに変更するとうまくいくことを発見します。 Shift_JISで処理させようとすると、なかなかうまくいかないことが多く、結局安定して動作するEUC-JPに落ち着く、と言うのが開発者の正直な気持ちではないでしょうか。
このあたりは、例のUTF-8マッピングの問題と言われています。 ある文字エンコードの文章をUTF-8でエンコード変換して元に戻すと、くるってしまうという問題です。これは、|や-が Shift_JISを UTF-8に変換し、Shift_JISに戻すと、正しく元に戻すことが出来ないという問題だと考えられてきました。
--------------------------------------------------------
僕は今までアプリケーションサーバーと言うものを便利だと思ったことは一度もありません。 何故アプリケーションサーバーが必要なのかも、全く理解できません。
そういうと「J2EEでサーブレットを作る時に必要だ」と言われてしまいそうですが、僕は J2EEもなんで必要なのか理解できません。
僕にはプログラムのサイズが不要に大きすぎるように感じます。 大きすぎるプログラムというのは、それだけで存在価値を全て否定するほどの致命的な欠点です。
僕は、皆さんにこのことを気が付いてほしい。
大きすぎると、全体が見通せないので、些細な問題であっても、解決するのに驚くほどほど時間が掛かってしまうのです。問題の原因がわかったときの決まり文句は必ず「な~~んだ~~」です。 それは、あまりにも近く目の前にあり、至極当然な理由であることを知った時の驚嘆の声です。 しかし、その声が聞こえるまで1週間近く掛かったということはまれではありません。
だらかプログラムサイズを「人間の記憶できる範囲」内に収めることはとても重要だと考えています。
しかもアプリケーションサーバーは大きいだけでなく、ソースが公開されていないブラックボックスであることが多い。 こういうプログラムを正しく動くまで調整するのは、とてつもない手間が掛かるのです。
--------------------------------------------------------
以上が前フリでした。 お疲れ様です。
僕は、昨日 実は、JavaのURLDecoderに致命的なバグがあることを見つけました。
ご存知のとおり、Shift_JISでエンコードされたHTMLファイル上の フォームで 「いろは。」と入力して投稿すると、ほとんどのブラウザは 入力したデータを %82%B1%82%F1%82%C9%82%BF%82%CD%81B といわゆる「パーセントエンコーディング」を施したうえで、サーバーに渡します。
ところで、注目していただきたいのは 一番最後の「。」の変換結果である %81Bです。
日本人である僕は、%81Bは %81%42 とエンコードしてほしいと考えていますが、そうなっていません。 そう考える根拠は %81はダブルバイトの先頭なので 次の文字も同様に扱う必要があると考えるからです。ですが、アメリカ人的には %42は ASCIIの範囲に収まっているからエンコードの必要はないと考えているようです。
よく考えてみれば、もしこれを %81%42として扱うことを義務付けると、韓国語中国語その他もろもろの コードセットの数だけエンコード方法を個別に考える必要があり大変すぎるので、あくまでも抽象的にUS-ASCIIの範囲以外はエンコードとしたのかもしれません。
理由はともあれ現状 そういうことになっています。
(Firefox/IE6update2/NN4.7.3 で確認しました。)
次にJavaです。
これを URLDecoder で 変換すると ダブルバイトの場合、僕の考えと同じ様に 次の文字も必ずパーセントエンコードされていることが前提としてコーディングされており、%81Bは あくまでも2文字として扱われます。
だから 「。」を変換した結果である %81B をURLDecoderを利用して デコードすると "?B" と 誤って変換されてしまうことになります。
なんですとぉ~~~~~~~~~~! (((((;`Д´)≡●)`Д)、;'.・
!(ノ`Д´)ノ 彡┻━┻
つまり、URLDecoderを使っているアプリケーションサーバーは 絶対にShift_JISを正しくデコードできないということです。
キタ----------------------(。A。)-----------------------
どんなに苦労してもShift_JISが通らないはずです。なんでこんな初歩的なバグを誰も気が付いていないんだろう。
(J2SE5.0update5で確認)
僕は、これこそが、巨大なブラックボックス、アプリケーションサーバーがもたらした害の典型的な例だと考えています。
こんな目の前にある問題なのに、誰からも気づかれること無く10年近く潜み続けたのは、他でもない、正に、プログラムが巨大すぎて問題を見通すことが誰も出来なかった事が理由だと思います。
そして、巨大なアプリケーションサーバーに隠れながら人々を悩ませ続けていたんだと思います。
(そうでなければ、例えこの問題に気が付いても、この問題を国際センスの無いアメリカ人に誤解なく説明できる人がいないのでしょう。)
最後までこの文章を読んでくれてありがとうございます。 この問題を修正した URLDecoderMultibyte は 僕の手元にあります。もし必要でしたらおっしゃってください。
この問題がなぜ発生するのかは、今ではとても有名です。次のようなTIPSがあります。
http://www.ingrid.org/java/jserv/i18n/corruptedchar.html
一般的には次のような理由で問題が発生すると考えられています。 実は、JavaVMは内部でUnicode16で処理を行っており、Shift_JISなどの文字が入力されると内部で一旦変換されてから処理されます。そして、出力する際に再び元に戻してユーザーに渡ります。 ところがこの時変換の前提となるコードが狂ってしまうことがしばしば起こります。 それは、入力の文字コードと出力の文字コードを明示的に指定するメカニズムが根本的に欠落している事がそもそもの原因です。
入力/出力文字コードが JavaVMのデフォルト文字コードで固定されている実装のアプリケーションサーバーが多いからです。
この問題が発生するかしないかは、それぞれのアプリケーションサーバーの実装の仕方によるところが大きく、中身を見せてくれないブラックボックス的なアプリケーションサーバーでは、問題を予想したり解決したりするのは、非常に困難です。
だから、すっきりしないのですが、あちらこちらの設定をゴチャゴチャと変更してごまかしているのが現状ではないでしょうか。
ところで、しばらくゴチャゴチャいじっていると、偶然EUC-JPに変更するとうまくいくことを発見します。 Shift_JISで処理させようとすると、なかなかうまくいかないことが多く、結局安定して動作するEUC-JPに落ち着く、と言うのが開発者の正直な気持ちではないでしょうか。
このあたりは、例のUTF-8マッピングの問題と言われています。 ある文字エンコードの文章をUTF-8でエンコード変換して元に戻すと、くるってしまうという問題です。これは、|や-が Shift_JISを UTF-8に変換し、Shift_JISに戻すと、正しく元に戻すことが出来ないという問題だと考えられてきました。
--------------------------------------------------------
僕は今までアプリケーションサーバーと言うものを便利だと思ったことは一度もありません。 何故アプリケーションサーバーが必要なのかも、全く理解できません。
そういうと「J2EEでサーブレットを作る時に必要だ」と言われてしまいそうですが、僕は J2EEもなんで必要なのか理解できません。
僕にはプログラムのサイズが不要に大きすぎるように感じます。 大きすぎるプログラムというのは、それだけで存在価値を全て否定するほどの致命的な欠点です。
僕は、皆さんにこのことを気が付いてほしい。
大きすぎると、全体が見通せないので、些細な問題であっても、解決するのに驚くほどほど時間が掛かってしまうのです。問題の原因がわかったときの決まり文句は必ず「な~~んだ~~」です。 それは、あまりにも近く目の前にあり、至極当然な理由であることを知った時の驚嘆の声です。 しかし、その声が聞こえるまで1週間近く掛かったということはまれではありません。
だらかプログラムサイズを「人間の記憶できる範囲」内に収めることはとても重要だと考えています。
しかもアプリケーションサーバーは大きいだけでなく、ソースが公開されていないブラックボックスであることが多い。 こういうプログラムを正しく動くまで調整するのは、とてつもない手間が掛かるのです。
--------------------------------------------------------
以上が前フリでした。 お疲れ様です。
僕は、昨日 実は、JavaのURLDecoderに致命的なバグがあることを見つけました。
ご存知のとおり、Shift_JISでエンコードされたHTMLファイル上の フォームで 「いろは。」と入力して投稿すると、ほとんどのブラウザは 入力したデータを %82%B1%82%F1%82%C9%82%BF%82%CD%81B といわゆる「パーセントエンコーディング」を施したうえで、サーバーに渡します。
ところで、注目していただきたいのは 一番最後の「。」の変換結果である %81Bです。
日本人である僕は、%81Bは %81%42 とエンコードしてほしいと考えていますが、そうなっていません。 そう考える根拠は %81はダブルバイトの先頭なので 次の文字も同様に扱う必要があると考えるからです。ですが、アメリカ人的には %42は ASCIIの範囲に収まっているからエンコードの必要はないと考えているようです。
よく考えてみれば、もしこれを %81%42として扱うことを義務付けると、韓国語中国語その他もろもろの コードセットの数だけエンコード方法を個別に考える必要があり大変すぎるので、あくまでも抽象的にUS-ASCIIの範囲以外はエンコードとしたのかもしれません。
理由はともあれ現状 そういうことになっています。
(Firefox/IE6update2/NN4.7.3 で確認しました。)
次にJavaです。
これを URLDecoder で 変換すると ダブルバイトの場合、僕の考えと同じ様に 次の文字も必ずパーセントエンコードされていることが前提としてコーディングされており、%81Bは あくまでも2文字として扱われます。
だから 「。」を変換した結果である %81B をURLDecoderを利用して デコードすると "?B" と 誤って変換されてしまうことになります。
なんですとぉ~~~~~~~~~~! (((((;`Д´)≡●)`Д)、;'.・
!(ノ`Д´)ノ 彡┻━┻
つまり、URLDecoderを使っているアプリケーションサーバーは 絶対にShift_JISを正しくデコードできないということです。
キタ----------------------(。A。)-----------------------
どんなに苦労してもShift_JISが通らないはずです。なんでこんな初歩的なバグを誰も気が付いていないんだろう。
(J2SE5.0update5で確認)
僕は、これこそが、巨大なブラックボックス、アプリケーションサーバーがもたらした害の典型的な例だと考えています。
こんな目の前にある問題なのに、誰からも気づかれること無く10年近く潜み続けたのは、他でもない、正に、プログラムが巨大すぎて問題を見通すことが誰も出来なかった事が理由だと思います。
そして、巨大なアプリケーションサーバーに隠れながら人々を悩ませ続けていたんだと思います。
(そうでなければ、例えこの問題に気が付いても、この問題を国際センスの無いアメリカ人に誤解なく説明できる人がいないのでしょう。)
最後までこの文章を読んでくれてありがとうございます。 この問題を修正した URLDecoderMultibyte は 僕の手元にあります。もし必要でしたらおっしゃってください。
コメント一覧
おかあつ 2006年02月19日 20:27
以下ソースを貼り付けておきます。
(このソースをこのままプログラムに貼り付けるのは意外と難しいと思いますので宜しければお声をかけてください。)
/*
* URLDecoder has a critical bug of manipulating double byte character. fixed.
*/
class URLDecoderMultibyte extends URLDecoder {
@Override
public static String decode(String s, String enc) throws UnsupportedEncodingException {
boolean needToChange = false;
int numChars = s.length();
// FIXED HERE : StringBuffer sb = new StringBuffer(numChars > 500 ? numChars / 2 : numChars);
ByteArrayOutputStream sb = new ByteArrayOutputStream();
int i = 0;
if (enc.length() == 0) {
throw new UnsupportedEncodingException ("URLDecoder: empty string enc parameter");
}
char c;
byte[] bytes = null;
while (i < numChars) {
c = s.charAt(i);
switch (c) {
case '+':
// FIXED HERE : sb.append(' ');
sb.write(' ');
i++;
needToChange = true;
break;
case '%':
/*
* Starting with this instance of %, process all
* consecutive substrings of the form %xy. Each
* substring %xy will yield a byte. Convert all
* consecutive bytes obtained this way to whatever
* character(s) they represent in the provided
* encoding.
*/
try {
// (numChars-i)/3 is an upper bound for the number
// of remaining bytes
if (bytes == null)
bytes = new byte[(numChars-i)/3];
int pos = 0;
while ( ((i+2) < numChars) &&
(c=='%')) {
bytes[pos++] =
(byte)Integer.parseInt(s.substring(i+1,i+3),16);
i+= 3;
if (i < numChars)
c = s.charAt(i);
}
// A trailing, incomplete byte encoding such as
// "%x" will cause an exception to be thrown
if ((i < numChars) && (c=='%'))
throw new IllegalArgumentException(
"URLDecoder: Incomplete trailing escape (%) pattern");
sb.write(bytes, 0, pos );
// FIXED HERE : sb.append(new String(bytes, 0, pos, enc));
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"URLDecoder: Illegal hex characters in escape (%) pattern - "
+ e.getMessage());
}
needToChange = true;
break;
default:
// FIXED HERE : sb.append(c);
sb.write(c);
i++;
break;
}
}
// return (needToChange? sb.toString() : s);
return ( needToChange ? new String( sb.toByteArray() , enc ) : s );
}
}
(このソースをこのままプログラムに貼り付けるのは意外と難しいと思いますので宜しければお声をかけてください。)
/*
* URLDecoder has a critical bug of manipulating double byte character. fixed.
*/
class URLDecoderMultibyte extends URLDecoder {
@Override
public static String decode(String s, String enc) throws UnsupportedEncodingException {
boolean needToChange = false;
int numChars = s.length();
// FIXED HERE : StringBuffer sb = new StringBuffer(numChars > 500 ? numChars / 2 : numChars);
ByteArrayOutputStream sb = new ByteArrayOutputStream();
int i = 0;
if (enc.length() == 0) {
throw new UnsupportedEncodingException ("URLDecoder: empty string enc parameter");
}
char c;
byte[] bytes = null;
while (i < numChars) {
c = s.charAt(i);
switch (c) {
case '+':
// FIXED HERE : sb.append(' ');
sb.write(' ');
i++;
needToChange = true;
break;
case '%':
/*
* Starting with this instance of %, process all
* consecutive substrings of the form %xy. Each
* substring %xy will yield a byte. Convert all
* consecutive bytes obtained this way to whatever
* character(s) they represent in the provided
* encoding.
*/
try {
// (numChars-i)/3 is an upper bound for the number
// of remaining bytes
if (bytes == null)
bytes = new byte[(numChars-i)/3];
int pos = 0;
while ( ((i+2) < numChars) &&
(c=='%')) {
bytes[pos++] =
(byte)Integer.parseInt(s.substring(i+1,i+3),16);
i+= 3;
if (i < numChars)
c = s.charAt(i);
}
// A trailing, incomplete byte encoding such as
// "%x" will cause an exception to be thrown
if ((i < numChars) && (c=='%'))
throw new IllegalArgumentException(
"URLDecoder: Incomplete trailing escape (%) pattern");
sb.write(bytes, 0, pos );
// FIXED HERE : sb.append(new String(bytes, 0, pos, enc));
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"URLDecoder: Illegal hex characters in escape (%) pattern - "
+ e.getMessage());
}
needToChange = true;
break;
default:
// FIXED HERE : sb.append(c);
sb.write(c);
i++;
break;
}
}
// return (needToChange? sb.toString() : s);
return ( needToChange ? new String( sb.toByteArray() , enc ) : s );
}
}