FileReader.readAsBinaryString は Binary を返さない

JDC にバグが見つかった。zip ファイルをサーバにアップロードすると、 ファイルが壊れるというものだ。テスト用インスタンスを立ち上げて 挙動を見てみると、サーバ側ではなくクライアント側でファイルが壊されているようだった。 具体的には、ファイルサイズが何故か 1.5 倍に増加しており、しかも、データが ランダムなバイト列ではなく UTF-8 にエンコードされているのであった。

ファイルのアップロードにはネットで拾ったパプリックドメインの JavaScript コードを 利用していて、これがファイルの読み込みとサーバへの送信を実行している。 読み込みから送信までの間に何かが起こっているのだろう、とはすぐに見当がついた。 処理を追いかけて調べた結果、FileReader.readAsBinaryString() が読み込んだ ファイル内容を破壊していることがわかった。 これを FileReader.readAsArrayBuffer() に変更することでバグ自体は 修正できたのだが、これについて調べた結果がやや興味深かったので記録しておく。

さてこの FileReader.readAsBinaryString() 、字面からは、バイナリファイルを 読み込んで中身のバイナリデータをメモリ上に保持する函数であるかのように見える。 ネット上の記事でもそのように解説されているものが多いようだ。 たとえば これ とか これ とか これ とか。

ところが、実はそうではない。 FileReader.readAsBinaryString() は仕様上 DOMString オブジェクトにデータを 格納することになっており、そして DOMString は UTF-8 しか受け付けない。 だから、少なくとも仕様上は、 FileReader.readAsBinaryString() は ランダムなバイト列を読み込むことはできないのである。 ( 参考

そのため、たとえば Safari のエンジンである WebKit ではどうなっているかと言うと、

Source/WebCore/fileapi/FileReaderLoader.cpp:295
    switch (m_readType) {
    case ReadAsArrayBuffer:
        // No conversion is needed.
        break;
    case ReadAsBinaryString:
        m_stringResult = String(static_cast<const char*>(m_rawData->data()), m_bytesLoaded);
        break;

ReadAsArrayBuffer ではデータに何も加工しないのに対して ReadAsBinaryString では String クラスに渡す。この String には コンストラクタが数種類定義されているが、この場合に呼ばれるのは、

Source/WTF/wtf/text/WTFString.h:84-
    class String {
    ..snip..
    // Construct a string with latin1 data.
    WTF_EXPORT_STRING_API String(const LChar* characters, unsigned length);
    WTF_EXPORT_STRING_API String(const char* characters, unsigned length);

の下の方である。コメントに書かれているように、入力データを latin1 として 強制的に解釈する。そして target.result に結果を格納する際に UTF-8 に変換する のであろう。

実際、サーバに送信された壊れたデータを取り出して、元のファイルと比較してみると、

iconv -f L1 -t UTF-8

と変換した場合にファイルサイズも内容も一致した。

この FileReader.readAsBinaryString() 、実は 2012年に W3C の仕様から 削除されている。 2011年の仕様 では、 代わりに readAsArrayBuffer() を使うようにと書かれている。 その理由は、 readAsBinaryString() が、readAsBinaryString という名称でありながら DOMString の制限のためにバイナリを返せないという中途半端な函数だったからだろう。

それにしてもよく解らないことがある。

まず、多くの人が、readAsBinaryString() はバイナリファイルの内容をそのまま 保持する函数だと思っていたことである。 ざっとググった限りでは、そうではないと書いてある日本語ページは ここ くらいである。

もしかしたら、以前のブラウザの実装は、W3Cの仕様に反して、内容を加工しない ようになっていたのかも知れない。何しろわたしも JDC の開発中にこのことに 気がつかなかった。しかし WebKit のリポジトリから 2011年頭頃のコードを 取り出してみたが、今現在と変わらないように見える。 他の webエンジンではどうなのだろうか。