[Astro] #137 Bz-Style Binary Editor — ブラウザ完結型16進数バイナリエディタ & ワイルドカード検索の実装
はじめに
ローカル環境でファイルをサーバーへ送信することなく、ブラウザ完結でバイナリデータの閲覧・書き換え・解析を行えるWebアプリケーション
「Bz-Style Binary Editor (v1.0)」
を実装しました。
名作バイナリエディタ「Bz」の操作感と視認性を踏襲しつつ、モダンなWeb技術(Astro / HTML5 Canvas / Pure JS)で再構築しています。
窓の杜
「Binary Editor BZ」色分け表示のできる多機能バイナリーエディターのオープンソース版
forest.watch.impress.co.jp本記事では、Canvasを用いた高速描画と仮想スクロール、HEX/TEXT(UTF-8)双方に対応したニブル(半バイト)単位のワイルドカード検索エンジン、カーソル位置のリアルタイム数値構造解析(Data Inspector)、グラフィカルなビットマッププレビュー(Bitmap View)、およびキーボードショートカットの実装詳細について解説します。
スクリーンショット
動画(GIF)
Sample data
サンプルデータは、過去に録画したSteamゲーム「Subsistence」の録画データになります。
Subsistence (64-bit, DX9) 2022-05-03 14-39-46.mp4
| TYPE | DATA |
|---|---|
| File | Subsistence (64-bit, DX9) 2022-05-03 14-39-46.mp4 |
| Size | 1381.2 MB |
| Declared Ext | .mp4 |
| Detected Type | MP4 |
1. 全体アーキテクチャ
[ Astroページ (binary-editor.astro) ]
│
├── 左パネル (Panel Left)
│ ├─ ドラッグ&ドロップ ゾーン
│ ├─ ファイルメトリクス(ファイル名 / サイズ / 現在オフセット)
│ └─ 表示設定(Bytes / Row: 16/32/64、Endianness)
│
├── 中央エリア (Viewport & Toolbar)
│ ├─ ツールバー (Undo / Redo / Save / Search / Goto / Help)
│ └─ Hex Canvas Viewport(アドレス / Hexグリッド / ASCIIテキスト)
│
├── 右パネル (Panel Right)
│ ├─ Data Inspector (Cursor Position の各型リアルタイムデコード)
│ └─ Bitmap View (Stride幅 16〜512px 設定付きピクセル描画)
│
└── ヘルプモーダル (Help Modal)
├─ キーボードショートカット一覧
└─ Search Syntax Guide(HEX / TEXT ワイルドカード仕様)
設計のコア原則:
- 完全ローカル処理: ファイルデータはブラウザのメモリ(
Uint8Array/ArrayBuffer)上でのみ保持し、一切の外部通信を行いません。 - DOM非依存の高性能レンダリング: 何十万行にも及ぶバイナリデータ表示においてDOM生成コストを排除するため、HTML5
<canvas>による直描きを採用。 - 編集状態の履歴管理: バイナリの直接書き換え(
0-9,A-F)や貼り付け操作に対して、Undo/Redo スタックを保持。
2. Canvas による仮想スクロール描画エンジン
DOM要素で大量の16進数セルを展開するとメモリ消費とリフロー負荷が肥大化するため、単一の <canvas> に可視領域(ビューポート)のデータのみを動的レンダリングします。
可視行の計算と Canvas 描画
スクロールオフセットとキャンバスの高さから、描画すべき先頭行 startRow と最終行 endRow を求め、その範囲のバイトデータのみを抽出し、アドレス列・16進数値・ASCII文字として描画します。
function renderHexCanvas() {
var bytesPerRow = state.bytesPerRow; // 16, 32, 64
var lineHeight = 18;
var totalRows = Math.ceil(state.buffer.length / bytesPerRow);
ctx.clearRect(0, 0, canvas.width, canvas.height);
var startRow = Math.floor(scrollTop / lineHeight);
var visibleRows = Math.ceil(canvas.height / lineHeight);
var endRow = Math.min(totalRows, startRow + visibleRows + 1);
for (var r = startRow; r < endRow; r++) {
var y = (r - startRow + 1) * lineHeight;
var rowOffset = r * bytesPerRow;
// 1. アドレス(オフセット)描画 (例: 00000080)
ctx.fillStyle = '#666666';
ctx.fillText(rowOffset.toString(16).padStart(8, '0').toUpperCase(), 10, y);
// 2. Hex バイト列 & ASCII 描画
for (var i = 0; i < bytesPerRow; i++) {
var idx = rowOffset + i;
if (idx >= state.buffer.length) break;
var byteVal = state.buffer[idx];
var hexStr = byteVal.toString(16).padStart(2, '0').toUpperCase();
var charStr = (byteVal >= 32 && byteVal <= 126) ? String.fromCharCode(byteVal) : '.';
// 選択範囲・カーソル位置のハイライト処理
highlightAndDrawByte(idx, hexStr, charStr, i, y);
}
}
}
3. ニブル・1バイト対応ワイルドカード検索エンジン
バイナリ解析において、「特定のパターンを探したいが一部のニブル(半バイト)や1バイトが不特定のデータ」を検索するニーズに応えるため、柔軟なパターンマッチングエンジンを構築しました。
HEX モードの検索仕様
- 完全一致:
50 4E 47(スペース区切りの16進数) - ニブル単位ワイルドカード:
9?(上位4ビット指定)、?A(下位4ビット指定) - 1バイトワイルドカード:
??または?
function parseHexPattern(input) {
var tokens = input.trim().split(/\s+/);
var pattern = [];
for (var i = 0; i < tokens.length; i++) {
var tok = tokens[i].toUpperCase();
if (tok === '??' || tok === '?') {
pattern.push({ type: 'wildcard' });
} else if (tok.length === 2) {
var high = tok[0];
var low = tok[1];
pattern.push({
type: 'nibble',
highMask: high === '?' ? false : parseInt(high, 16),
lowMask: low === '?' ? false : parseInt(low, 16)
});
}
}
return pattern;
}
function matchHexPattern(buffer, offset, pattern) {
if (offset + pattern.length > buffer.length) return false;
for (var p = 0; p < pattern.length; p++) {
var b = buffer[offset + p];
var pat = pattern[p];
if (pat.type === 'wildcard') continue;
if (pat.type === 'nibble') {
var bHigh = (b >> 4) & 0x0F;
var bLow = b & 0x0F;
if (pat.highMask !== false && bHigh !== pat.highMask) return false;
if (pat.lowMask !== false && bLow !== pat.lowMask) return false;
}
}
return true;
}
TEXT モード (UTF-8) の検索仕様
- 文字列検索:
PNGなど - 1バイトワイルドカード:
?PNG/P?G - エスケープ処理:
\?で記号「?」自体を一致対象に指定可能
4. Data Inspector & Bitmap View
バイナリデータの構造解析を補助するため、右パネルにリアルタイムインスペクターと2Dビジュアル表示を配置しています。
リアルタイム Data Inspector
カーソルが指しているオフセットから各種データ型(Uint8 / Int8, Uint16 / Int16, Uint32 / Int32, Float32, Float64)および 8bit Binary を、現在選択されている Endianness(Little / Big)に従って一瞬でデコードします。
function updateDataInspector(cursorOffset) {
var view = new DataView(state.buffer.buffer, cursorOffset);
var isLittle = state.endianness === 'little';
document.getElementById('insp-bin').textContent =
state.buffer[cursorOffset].toString(2).padStart(8, '0');
document.getElementById('insp-i8').textContent =
view.getUint8(0) + ' / ' + view.getInt8(0);
document.getElementById('insp-i16').textContent =
view.getUint16(0, isLittle) + ' / ' + view.getInt16(0, isLittle);
document.getElementById('insp-i32').textContent =
view.getUint32(0, isLittle) + ' / ' + view.getInt32(0, isLittle);
document.getElementById('insp-f32').textContent =
view.getFloat32(0, isLittle).toFixed(4);
document.getElementById('insp-f64').textContent =
view.getFloat64(0, isLittle).toFixed(4);
}
2D Bitmap View
バイナリ値(0x00〜0xFF)をグレースケール輝度に変換し、指定された歩幅(Stride: 16px〜512px)で 2D 画像として視覚化します。グラフィックデータやパッキングされたリソースの構造パターンを直感的に把握できます。
function renderBitmapView() {
var stride = parseInt(document.getElementById('select-bitmap-stride').value, 10);
var canvas = document.getElementById('bitmap-canvas');
var ctx = canvas.getContext('2d');
var imgData = ctx.createImageData(canvas.width, canvas.height);
for (var y = 0; y < canvas.height; y++) {
for (var x = 0; x < canvas.width; x++) {
var byteIdx = y * stride + x;
var val = (byteIdx < state.buffer.length) ? state.buffer[byteIdx] : 0;
var pixelIdx = (y * canvas.width + x) * 4;
imgData.data[pixelIdx + 0] = val; // R
imgData.data[pixelIdx + 1] = val; // G
imgData.data[pixelIdx + 2] = val; // B
imgData.data[pixelIdx + 3] = 255; // A
}
}
ctx.putImageData(imgData, 0, 0);
}
5. UI スタイリング & ヘルプモーダル
ダークテーマ(#0d0d0d)およびサイバーパンク感溢れるネオングリーン(#00ff88)をアクセントカラーに採用し、高いコントラストと視認性を実現しています。
検索ガイドを含む HELP モーダルの実装
? Help ボタンまたはショートカットから呼び出せるモーダル内に、操作一覧と検索構文ガイド(Search Syntax Guide)をスタイリングして配置しています。
<!-- ヘルプモーダル構造例 -->
<div id="help-modal" class="modal-overlay" style="display: none;">
<div class="modal-content">
<div class="modal-header">
<h3>操作・ショートカットキー一覧</h3>
<button id="btn-close-help" class="modal-close">×</button>
</div>
<div class="modal-body">
<!-- ショートカットキー一覧テーブル -->
<table class="help-table"> ... </table>
<!-- Search Syntax Guide -->
<section class="help-section">
<h3>Search Syntax Guide</h3>
<h4>HEX Mode</h4>
<ul>
<li><code>50 4E 47</code> - 完全一致検索(スペース区切り)</li>
<li><code>9?</code> / <code>?A</code> - ニブル(半バイト)単位のワイルドカード</li>
<li><code>??</code> または <code>?</code> - 任意の1バイトにマッチ</li>
</ul>
<h4>TEXT Mode (UTF-8)</h4>
<ul>
<li><code>PNG</code> - 文字列の完全一致</li>
<li><code>?PNG</code> / <code>P?G</code> - 任意の1文字(1バイト)にマッチ</li>
<li><code>\?</code> - 記号「?」自体の検索(エスケープ)</li>
</ul>
</section>
</div>
</div>
</div>
6. まとめ
- DOMレス高速描画: HTML5 Canvas と仮想スクロールにより、バイナリサイズに依存しない応答性を実現。
- ニブル単位のワイルドカード検索: HEX / TEXT モードで任意の曖昧パターンを瞬時に特定可能。
- マルチ視点インスペクション: 数値構造を明かす Data Inspector と、2Dパターンを浮き彫りにする Bitmap View を搭載。
- 直感的なキーボード操作: キー入力をダイレクトに拾う16進数編集や Undo/Redo により、デスクトップアプリと同等の操作感を実現。
ブラウザ標準の Canvas API と TypedArray を活かすことで、外部ライブラリに一切依存しない強力なバイナリ編集・解析ツールを構築できました。