LoginSignup
1

More than 5 years have passed since last update.

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

Posted at

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

概要

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

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 GREEK NUMERAL SIGN
  • 書き込み後:\u02B9 MODIFIER LETTER PRIME

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

解決

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

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

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に上げてある

実験

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

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

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

これは問題ない

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

// ʹ === \u0374
escape('ʹ') === '%u0374' // false

ここに原因があるらしい

\u0374\u02B9を直接比較する

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

余談

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

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

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1