3

この記事は最終更新日から1年以上が経過しています。

投稿日

更新日

JS/TSでバイナリ⇔BASE64はライブラリを使わなくても簡単に出来る

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. バイト配列を配列リテラルへ展開
  2. 1バイト(1要素)ずつASCII文字列(\0~ÿ)へ変換
  3. 全要素を結合しBASE64エンコード

デコードは以下の手順です。

  1. BASE64デコードし配列リテラルへ展開
  2. 1文字(1要素)ずつASCII値(0x00~0xFF)へ変換
  3. 配列リテラルをバイト配列へ変換

検証コード

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オブジェクトとして転送する方法が賢いかも知れません。

これは使えるのでは...!?

新規登録して、もっと便利にQiitaを使ってみよう

  1. あなたにマッチした記事をお届けします
  2. 便利な情報をあとで効率的に読み返せます
ログインすると使える機能について
dojyorin
好奇心駆動でお届けするJavaScriptとC++の日々。

コメント

spread構文やUint8Array.fromを使うともう少し短く書ける

function base64encode(data:Uint8Array){
    return btoa(String.fromCharCode(...data));
}

function base64decode(data:string){
    return Uint8Array.from(atob(data), s => s.charCodeAt(0));
}

やってることは同じ

0

@sugoroku_y さん
コメントありがとうございます!

TypedArray.fromの第2引数にはmap機能が付いてたのですね。
これは見落としてました。

エンコードに関しては、私の記憶ですとJavaScriptは関数の引数の個数には制限があった気がするので、String.fromCharCodeのある程度のサイズに制限されてしまうのではないか...?と思いました。

その制限が、言語仕様によるものか実行エンジンによるものかは失念してしまいましたが、割と低い値だったとは記憶しています。

0

エンコードに関しては、私の記憶ですとJavaScriptは関数の引数の個数には制限があった気がするので、String.fromCharCodeのある程度のサイズに制限されてしまうのではないか...?と思いました。

そうですね、動作限界にならないにしても速度的に不利という面もありますので、new TextDecoder('latin1')を使って変換したほうがいい感じでした(なお、TextEncoderはUTF-8専用なので役に立ちません)。

1
(編集済み)

これは BASE64 ではなく名前のないテキスト化の一種に見えますが、これで他の BASE64 デコーダーで元のデータが復元できるんでしょうか?

1
(編集済み)

@Zuishin さん
私も勘違いしてましたが、よくある string to base64, base64 to string じゃなくて、素のバイナリ(?)を扱うためのもののようです。
uint8array 経由で正しく動きました。

text = 'ほげ';
u8 = new TextEncoder().encode(text);
b64 = base64encode(u8);
console.log(b64);//44G744GS

b64 = '44G744GS';
u8 = base64decode(b64);
text = new TextDecoder().decode(u8);

console.log(text);//ほげ

「バイナリだよ」って記事に書いてある通りなんですけど、いつものように Base64 エンコードした文字列を base64decode() に突っ込んで勘違いしちゃいました^^;

0
(編集済み)

どういうことかよく読み取れませんでしたが、RFC3548 の BASE64 とは別にこの記事のような BASE64 があるということでしょうか?

0
(編集済み)

btoa(), atob() が Base64 の変換を行っているので RFC は満たしていると思います。

私は知らなかったのですが、Uint8Arrayってのが JavaScript でバイナリを扱う時のお作法のようです。
で、この記事はUint8Arrayを Base64 であつかうコードです。

簡易検証のため、「ほげ」をUint8Arrayへ変換し base64encode() を通したところ「44G744GS」をえることができ、 他の string to base64 の結果と一致します。
また、「44G744GS」を base64decode() 後stringへ変換してやると「ほげ」をえることが出来たのでこちらも 他の base64 to string と一致することが確認できました。

うまく伝わりますかね?

1

理解しました。
根本的に勘違いしていたようです。
ありがとうございました。

0
あなたもコメントしてみませんか :)
新規登録
すでにアカウントを持っている方はログイン
記事投稿キャンペーン開催中
【RubyKaigi 2023連動イベント】みんなでRubyの知見を共有しよう
~
最新技術についてUdemyで学んだことをシェアしよう!
~
3