ブラウザからmultipart/form-data送信したファイル名の濁点が全角二文字になる

Webアプリケーションを開発していて久しぶりにハマりました。こういう事があるとドキッとする半面ちょっとワクワクしますね。新しい知識が得られる予感がするので。

事象

まず事象について私が直面したケースに関してそのまま記述します。

掲題にあるとおりなのですがWebアプリケーションの開発において日本語をファイル名としたファイルをMIMEタイプを multipart/form-data として送信した際に、サーバ側で受信したファイルを確認するとファイル名の濁点が全角で一文字として認識されてしまうというものです。

具体例をあげると「アップロードテスト.jpg」という画像を送信したとすると「アッフ゜ロート゛テスト.jpg」として認識されるというような具合です。

ちなみにこの「認識される」という表現を詳細に伝えると、IDEのデバッガーで変数を参照するとそのように表示されて見える。というものです。

サーバ側である文字列とアップロードファイル名の突き合わせを行っていたのですがうまく一致しないという困った状況になっていました。

開発環境

ブラウザは Chrome最新版、フロントエンドは react / redux / redux-saga をベースのフレームワークとした実装としており、http通信部は superagent を利用、バックエンドは php / laravel を利用しておりました。

調査

はじめはフロントエンド部分かと思い送信処理をブラウザから Macクライアントアプリケーションへ切り替えて同様にファイルを送信してテストを行いました。しかし解決しません。バックエンドの原因かと思いましたがフレームワークが保持しているリクエスト変数ではなく php の提供する標準のスーパーグローバル変数 $_FILE を参照してもファイル名がおかしなことになっています。

これらのことから自分の開発環境における局所的な何かが原因となっているわけではなく、一般的な何かが原因となっていると当たりをつけ検索します。

原因

Wikipedia – Unicode正規化 を参考にします。

Unicode正規化が原因でこのような事象となっているようです。Unicodeは国際的な文字セットを定義した空間ですが、正規化の形式がいくつか存在するようです。以下引用ですが

  • NFD
    • 文字は正準等価性によって分解される
  • NFC
    • 文字は正準等価性によって分解され、再度合成される。結果として文字の並びが変換前と変わることもありうる。

何をおっしゃっているかよくわからないので調べます。

正準等価性とは

日本語の濁点やドイツ語のウムラウトなどがくっついた「合成文字」は分解すると「基底文字+結合文字」という文字列で構成される文字列で表現できるがこれを等価とみなしますよ、というものです。

たとえば「が」は「か」と「゛」に分解されるが「か」と「か゛」は正準等価であるとみなすことができます。

また

macOSのファイルシステムHFS+ではNFDの変種が用いられる

とのことですのでファイルシステム上でNFDで正規化された文字列は合成されない状態でそのまま符号化されてサーバ側へと送信されてしまったのではないかと予想できます。

対応

そうするとですね、その分解されてしまった文字を合成してから比較したりするような仕組みが必要になります。それを今回サーバ側で言語として選定しているphpを用いて実装してみます。

当初の不具合では以下のような具合で照合ができていませんでした

$composited = "が";
$separated = "か゛";
$result = $composited === $separated; // false

phpでこれらの符号化された文字列を照合可能にするにはいくつかの実装方法があるようです

Collatorクラスを用いる

Collatorクラスのマニュアルを利用すると文字列を合成してから照合してくれそうです。

しかしどうやらコンストラクタにロケールの指定をする必要があるようです。Localeについてもphpのマニュアルを見てみます。以下少し引用します

“ロケール” とは、言語や文化等の地域固有の内容を API で取得する際に使用する識別子のことです。PHP で使用しているロケールは、 ICU (そして他の多くの Unix 系 OS や Mac、Java など) が採用している CLDR ロケールと同じです。 ロケールは、RFC 4646 形式の言語タグ (アンダースコアではなくハイフンを使用したもの) を使用します。古くから用いられているアンダースコア形式の識別子も使用できます。 特にことわりがない限り、このクラスの関数では両方の形式の識別子を使用可能です。

またわからない用語がいっぱい出てくるので調べます

ICUとは

International Components for Unicode の略称です。国際的にUnicodeを利用するためのC/C++およびJavaのライブラリセットのことだそう。

CLDRロケールとは

Common Locale Data Repository の略称。ロケール情報をアプリケーションが利用可能なようにしたデータセット。

執筆時点でのWikipediaの情報を参照する限りこのCLDRは一部の組織のみで運営されている(AppleのmacOSなどに適用されている)ように受け取れる記載もあり、もしかしたら国際的な標準化というところではまだまだ普及しているようなものなのではないのかもしれない。

また別途POSIXローケルと呼ばれるロケールセットの定義も存在するようであり、やはりまだまだ黎明期なのかもしれない。

RFC4646とは

地球上に存在する言語に対して一意にタグ付けしちゃおう。というRFC。言わずもがな言語に対してタグがつけられることでいろいろな利点があります。

またタグはサブタグと呼ばれる概念が存在し、タグの中でさらに地域などの付加的な情報を与えることで情報を絞り込みます。サブタグはハイフンで区切られます。

RFC4646自体はCLDRのためだけに策定されているわけではないので、BNF記法を参照すると将来的な拡張の余地も含めてかなり緩めに定義されています。

Web領域などで一般的に使用されるケースにものすごく限定すると、タグは基本的には 言語コード – 文字体系コード – 地域コード の組み合わせで表現できそうです。言語コード以降は省略しても表現としては妥当ですので言語コードだけでもタグとしては妥当です。

 

話を戻します。日本のCLDRロケールにおける識別は ja-JP になります。日本語は地域は日本でしか使用されておりませんし、文字の種類も区別する必要がないので 言語-地域 がコードとなっているようです。

よって下記のような形にすることで対応できます。なおCollatorは引数にnullを指定するとデフォルトロケールが使用されるようなのでnullでも特に問題なさそうです。

$composited = "が";
$separated = "か゛";
$collator = new \Collator('ja-JP');
return $collator->compare($composited, $separated) === 0; // true

ちなみにですがこちらのコードをそのままコピペしても期待した結果は得られませんのでご注意ください。あくまで実装イメージです。テキストで「か゛」と基底文字と合成文字を別々に入力した場合はUnicodeとしては別々の文字が二文字入力された入力とみなされるためだと思います。

Normalizerクラスを用いる

Normalizerクラスのマニュアルを見てみると、Collatorよりもシンプルで特定の文字列を正規化方式を指定することで変換してくれるAPIを提供しています。

こちらの場合の実装イメージは下記のようになります。

$composited = "が";
$separated = "か゛";
$converted = Normalizer::normalize($separated);
return $composited === $converted; // true

 

まとめ

結構レアなケースだと思いますがまた一つ知見を深めることができました。検索するとmacでPDFからコピペするときに問題があるような記事が多く出てきます。Webファイルアップロードに限った話ではないですので原因がわかってよかったです。

参考サイト

Wikipedia – Unicode正規化

Wikipedia – CLDR

RFC4646

php – Collatorクラス

php – Localeクラス

php – Normalizerクラス

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください