[Safari] 一部のutf-8文字がBlobに書き込めない問題



Unicode正規化に関する問題で少し困ったので簡単な再現コードと解決をまとめておく


## 概要

macOS/Safariでのみ確認
具体的には以下のようなコードを実行したとき、Blob書き込み前と後で異なる文字が得られる

```javascript
const invalidChar = '\u0374';
const blob = new Blob([invalidChar], { type: 'text/plane' });
const reader = new FileReader();
reader.onload = function (event) {
  // Safari: false
  // others: true
  console.log(event.target.result === invalidChar);
  
  // in Safari... invalidChar === '\u02B9'
};
reader.readAsText(blob);
```

- 書き込み前：[\u0374](https://www.fileformat.info/info/unicode/char/0374/index.htm) `GREEK NUMERAL SIGN`
- 書き込み後：[\u02B9](http://www.fileformat.info/info/unicode/char/2B9/index.htm) `MODIFIER LETTER PRIME`


この２つの文字は「Unicodeの等価性」という原則に基づいて「同じ文字」であるらしい（詳しくないのでこの表現が正しいかどうか分からない）が、フロントエンドマンはこの２つが一緒だと困るのである


## 解決

結論から言うと、いい解決策は見つけられなかった

### エスケープすれば元の値のまま保持できる


```javascript
let invalidChar = '\u0374';
invalidChar = invalidChar.replace(/\u0374/, '\\u0374');
const blob = new Blob([invalidChar], { type: 'text/plane' });
const reader = new FileReader();
reader.onload = function (event) {
  console.info(event.target.result === invalidChar); // true!
};
reader.readAsText(blob);
```

しかし問題は残る

- `\u0374`だけでなく、多くのutf-8文字がこの現象を引き起こす
- 対象の文字が多すぎるとき、エスケープ後のファイルサイズがとても大きくなる
- `String.prototype.replace`のパフォーマンス的問題
- 「そのブラウザが問題を起こすかどうか」を簡単に見分ける方法が思いつかない
  - これについては後述

### 上手くいかなかった方法

- Blob初期化時に文字コードを指定する
- readAsTextに文字コードを指定する
- Blob初期化時にBOMをつける

コードはgistに上げてある

https://gist.github.com/teramotodaiki/07071d4b5fb3e665349c6b895aad0526

## 実験

> 以下のスクリプトはSafariの開発者ツールに付属しているコンソールでその行だけを実行して結果を確認しています

### アンエスケープしてエスケープしてみる

```javascript
escape(unescape('\u0374')) === '%u0374' // true
```

これは問題ない

### `\u0374`をコピーペーストしてエスケープしてみる

```javascript
// ʹ === \u0374
escape('ʹ') === '%u0374' // false
```

ここに原因があるらしい

### `\u0374`と`\u02B9`を直接比較する

```javascript
'ʹ' === 'ʹ' // true
// ※Safari以外のブラウザでは false
```

上記が最も単純な検証コードである。これを利用すれば、前述の「そのブラウザが問題を起こすかどうか」を判定できるかも知れない

ところで、上に挙げた３つの検証用コードをエディタ(Atom)を使ってファイルに書き込んでscriptタグから実行させた場合、問題は起こらない

## そもそもどこに責任があるのか？

- エディタで保存したとき…………上手くいく
- コンソールから実行したとき…………失敗する
- エディタで保存した文字列をもとにBlobを初期化したとき…………失敗する

ということから、問題は **読み取り時ではなく書き込み時に起きている**と予想される

SafariのコンソールにJavaScriptコードを入力したとき、おそらくSafariは評価用のVMにコードを渡すために何らかの方法で **コードをファイルに書き込む**。このときに`\u0374`を`\u02B9`として書き込んでしまうのだろう

そして、これと同じ現象がBlob初期化時にも発生していると思われる

…という線でググってみたものの、それらしいWebKitのバグは見当たらず…そもそもこの問題はバグとして扱われていないのかも？


## どうして問題になったのか

この問題に気付いた経緯はこんな感じである

- ある開発中のサービスで`babel-standalone`を使っていた
- 変換処理が長いとブラウザが固まってしまう
- そうだ、Workerを使おう!!
- 諸事情によりエントリーポイントは１ファイルにする必要がある
- そうだ、Blob URL Schemeを使おう!!
- ビルド済みの`babel-standalone`をconcat -> Blob URL
- とりあえずWorkerを走らせてみる
- Workerがエラーを吐く

> SyntaxError: Invalid regular expression: range out of order in character class

- 該当箇所

```javascript
var nonASCIIidentifierStartChars = "ªµºÀ-ÖØ-öø-ˁˆ-ˑˠ-ˤˬˮͰ-ʹͶͷͺ-ͽͿΆΈ-ΊΌΎ-ΡΣ-ϵϷ-ҁҊ-ԯԱ-Ֆՙա-ևא-תװ-ײؠ-يٮٯٱ-ۓەۥۦۮۯۺ-ۼۿܐܒ-ܯݍ-ޥޱߊ-ߪߴߵߺࠀ-ࠕࠚࠤࠨࡀ-ࡘࢠ-ࢴࢶ-ࢽऄ-हऽॐक़-ॡॱ-ঀঅ-ঌএঐও-নপ-রলশ-হঽৎড়ঢ়য়-ৡৰৱਅ-ਊਏਐਓ-ਨਪ-ਰਲਲ਼ਵਸ਼ਸਹਖ਼-ੜਫ਼ੲ-ੴઅ-ઍએ-ઑઓ-નપ-રલળવ-હઽૐૠૡૹଅ-ଌଏଐଓ-ନପ-ରଲଳଵ-ହଽଡ଼ଢ଼ୟ-ୡୱஃஅ-ஊஎ-ஐஒ-கஙசஜஞடணதந-பம-ஹௐఅ-ఌఎ-ఐఒ-నప-హఽౘ-ౚౠౡಀಅ-ಌಎ-ಐಒ-ನಪ-ಳವ-ಹಽೞೠೡೱೲഅ-ഌഎ-ഐഒ-ഺഽൎൔ-ൖൟ-ൡൺ-ൿඅ-ඖක-නඳ-රලව-ෆก-ะาำเ-ๆກຂຄງຈຊຍດ-ທນ-ຟມ-ຣລວສຫອ-ະາຳຽເ-ໄໆໜ-ໟༀཀ-ཇཉ-ཬྈ-ྌက-ဪဿၐ-ၕၚ-ၝၡၥၦၮ-ၰၵ-ႁႎႠ-ჅჇჍა-ჺჼ-ቈቊ-ቍቐ-ቖቘቚ-ቝበ-ኈኊ-ኍነ-ኰኲ-ኵኸ-ኾዀዂ-ዅወ-ዖዘ-ጐጒ-ጕጘ-ፚᎀ-ᎏᎠ-Ᏽᏸ-ᏽᐁ-ᙬᙯ-ᙿᚁ-ᚚᚠ-ᛪᛮ-ᛸᜀ-ᜌᜎ-ᜑᜠ-ᜱᝀ-ᝑᝠ-ᝬᝮ-ᝰក-ឳៗៜᠠ-ᡷᢀ-ᢨᢪᢰ-ᣵᤀ-ᤞᥐ-ᥭᥰ-ᥴᦀ-ᦫᦰ-ᧉᨀ-ᨖᨠ-ᩔᪧᬅ-ᬳᭅ-ᭋᮃ-ᮠᮮᮯᮺ-ᯥᰀ-ᰣᱍ-ᱏᱚ-ᱽᲀ-ᲈᳩ-ᳬᳮ-ᳱᳵᳶᴀ-ᶿḀ-ἕἘ-Ἕἠ-ὅὈ-Ὅὐ-ὗὙὛὝὟ-ώᾀ-ᾴᾶ-ᾼιῂ-ῄῆ-ῌῐ-ΐῖ-Ίῠ-Ῥῲ-ῴῶ-ῼⁱⁿₐ-ₜℂℇℊ-ℓℕ℘-ℝℤΩℨK-ℹℼ-ℿⅅ-ⅉⅎⅠ-ↈⰀ-Ⱞⰰ-ⱞⱠ-ⳤⳫ-ⳮⳲⳳⴀ-ⴥⴧⴭⴰ-ⵧⵯⶀ-ⶖⶠ-ⶦⶨ-ⶮⶰ-ⶶⶸ-ⶾⷀ-ⷆⷈ-ⷎⷐ-ⷖⷘ-ⷞ々-〇〡-〩〱-〵〸-〼ぁ-ゖ゛-ゟァ-ヺー-ヿㄅ-ㄭㄱ-ㆎㆠ-ㆺㇰ-ㇿ㐀-䶵一-鿕ꀀ-ꒌꓐ-ꓽꔀ-ꘌꘐ-ꘟꘪꘫꙀ-ꙮꙿ-ꚝꚠ-ꛯꜗ-ꜟꜢ-ꞈꞋ-ꞮꞰ-ꞷꟷ-ꠁꠃ-ꠅꠇ-ꠊꠌ-ꠢꡀ-ꡳꢂ-ꢳꣲ-ꣷꣻꣽꤊ-ꤥꤰ-ꥆꥠ-ꥼꦄ-ꦲꧏꧠ-ꧤꧦ-ꧯꧺ-ꧾꨀ-ꨨꩀ-ꩂꩄ-ꩋꩠ-ꩶꩺꩾ-ꪯꪱꪵꪶꪹ-ꪽꫀꫂꫛ-ꫝꫠ-ꫪꫲ-ꫴꬁ-ꬆꬉ-ꬎꬑ-ꬖꬠ-ꬦꬨ-ꬮꬰ-ꭚꭜ-ꭥꭰ-ꯢ가-힣ힰ-ퟆퟋ-ퟻ豈-舘並-龎ﬀ-ﬆﬓ-ﬗיִײַ-ﬨשׁ-זּטּ-לּמּנּסּףּפּצּ-ﮱﯓ-ﴽﵐ-ﶏﶒ-ﷇﷰ-ﷻﹰ-ﹴﹶ-ﻼＡ-Ｚａ-ｚｦ-ﾾￂ-ￇￊ-ￏￒ-ￗￚ-ￜ";
var nonASCIIidentifierStart = new RegExp("[" + nonASCIIidentifierStartChars + "]");	
```

- …今に至る

`/[x-y]/`みたいな正規表現があったとき`x`が`y`よりも大きい（文字コード的に順番が後である）とき`RegExp`のコンストラクタが「そんな範囲ありえないよ！」と出すのがこのエラーである

たくさんエラーが起きる箇所があると思うが、例として`[Ͱ-ʹ]`は本来`[\u0370-\u0374]`を表す正規表現だったのに対してBlobに書き込まれたあとは`[\u0370-\u02B9]`となってしまい、範囲が定まらなかった、というのがエラーの原因である


## 余談

逆戻りする例があったからまだ良かったものの、右のほうが大きいまま値だけ変わっていたらと思うとゾッとする（とはいえ３時間ほど費やした）

Unicodeの等価性というものは初めて知ったのでいい勉強になった


`babel-standalone`にissueを送ろうと思うが、「あなたのライブラリを文字列にしてconcatして実行時にBlobにした上でWorker上で走らせたら、なんかSafariだけ様子がオカシイ」と言われたら、 wontfix のラベルを付けたくなるだろうなと思うので、控えめにしておこうと思う