JS/TSは、バイナリデータをBASE64エンコードするビルトイン機能が無いため、大人しくライブラリを使うか自前でロジックを用意する必要がありました。
結局はライブラリを使用する場合が多いのですが、機密性の高いデータを扱う場面もあるため、ブラックボックスになるのも精神衛生的に良くないなぁと、ずっと思っていました。
とはいえ、アルファベットテーブルを拵えて6ビット区切になるようビットシフトして文字列加算して...を毎回用意するのも気が引けていました。
しかしある時「実はビルトイン機能を組み合わせるだけで簡単に出来るのでは?」と単純な思い付きで書いたコードがすんなり動いてしまったので、本当に大丈夫なのか有識者の意見も欲しく、こうして記事にしてみました。
本体コード
function base64encode(data:Uint8Array){
return btoa([...data].map(n => String.fromCharCode(n)).join(""));
}
function base64decode(data:string){
return new Uint8Array([...atob(data)].map(s => s.charCodeAt(0)));
}
エンコードは以下のような手順です。
- バイト配列を配列リテラルへ展開
- 1バイト(1要素)ずつASCII文字列(
\0
~ÿ
)へ変換 - 全要素を結合しBASE64エンコード
デコードは以下の手順です。
- BASE64デコードし配列リテラルへ展開
- 1文字(1要素)ずつASCII値(
0x00
~0xFF
)へ変換 - 配列リテラルをバイト配列へ変換
検証コード
const sample = new Uint8Array(new Float32Array(0x00400000).map(() => Math.random()).buffer);
async function hash(data:Uint8Array){
return [...new Uint8Array(await crypto.subtle.digest("SHA-256", data))].map(n => n.toString(16).toUpperCase().padStart(2, "0")).join("");
}
const result = base64decode(base64encode(sample));
console.log(hash(sample));
console.log(hash(result));
上記のように、何回かランダムデータを流し込んでハッシュを取りましたが、全て一致していたので、制御文字を含む文字列へ変換することによるデータ欠損...なんてことは無さそうでした。
また、個人的に気になった「atob()
/ btoa()
がそんな大量の入出力に耐えれるのかよ」問題も、とりあえず100MBくらいなら動作したので、実用には耐えれそうな感じでした。
ただ、これらは同期関数なので、大容量データを変換したいなら、本体関数はWebWorkerなどの別プロセスへ置いて、データはTransferableオブジェクトとして転送する方法が賢いかも知れません。
これは使えるのでは...!?
コメント
@sugoroku_y
0
@dojyorin
0
@jkr_2255
1
@Zuishin(編集済み) 1
@te2ji(編集済み)
0
@Zuishin(編集済み)
0
@te2ji(編集済み) 1
@Zuishin0
spread構文やUint8Array.fromを使うともう少し短く書ける
やってることは同じ
@sugoroku_y さん
コメントありがとうございます!
TypedArray.from
の第2引数にはmap機能が付いてたのですね。これは見落としてました。
エンコードに関しては、私の記憶ですとJavaScriptは関数の引数の個数には制限があった気がするので、
String.fromCharCode
のある程度のサイズに制限されてしまうのではないか...?と思いました。その制限が、言語仕様によるものか実行エンジンによるものかは失念してしまいましたが、割と低い値だったとは記憶しています。
そうですね、動作限界にならないにしても速度的に不利という面もありますので、
new TextDecoder('latin1')
を使って変換したほうがいい感じでした(なお、TextEncoder
はUTF-8専用なので役に立ちません)。これは BASE64 ではなく名前のないテキスト化の一種に見えますが、これで他の BASE64 デコーダーで元のデータが復元できるんでしょうか?
@Zuishin さん
私も勘違いしてましたが、よくある string to base64, base64 to string じゃなくて、素のバイナリ(?)を扱うためのもののようです。
uint8array 経由で正しく動きました。
「バイナリだよ」って記事に書いてある通りなんですけど、いつものように Base64 エンコードした文字列を base64decode() に突っ込んで勘違いしちゃいました^^;
どういうことかよく読み取れませんでしたが、RFC3548 の BASE64 とは別にこの記事のような BASE64 があるということでしょうか?
btoa(), atob() が Base64 の変換を行っているので RFC は満たしていると思います。
私は知らなかったのですが、
Uint8Array
ってのが JavaScript でバイナリを扱う時のお作法のようです。で、この記事は
Uint8Array
を Base64 であつかうコードです。簡易検証のため、「ほげ」を
Uint8Array
へ変換し base64encode() を通したところ「44G744GS」をえることができ、 他の string to base64 の結果と一致します。また、「44G744GS」を base64decode() 後
string
へ変換してやると「ほげ」をえることが出来たのでこちらも 他の base64 to string と一致することが確認できました。うまく伝わりますかね?
理解しました。
根本的に勘違いしていたようです。
ありがとうございました。