[Astro] #137 Bz-Style Binary Editor — ブラウザ完結型16進数バイナリエディタ & ワイルドカード検索の実装

[Astro] #137 Bz-Style Binary Editor — ブラウザ完結型16進数バイナリエディタ & ワイルドカード検索の実装

はじめに

ローカル環境でファイルをサーバーへ送信することなく、ブラウザ完結でバイナリデータの閲覧・書き換え・解析を行えるWebアプリケーション

「Bz-Style Binary Editor (v1.0)」

を実装しました。

名作バイナリエディタ「Bz」の操作感と視認性を踏襲しつつ、モダンなWeb技術(Astro / HTML5 Canvas / Pure JS)で再構築しています。

本記事では、Canvasを用いた高速描画と仮想スクロール、HEX/TEXT(UTF-8)双方に対応したニブル(半バイト)単位のワイルドカード検索エンジン、カーソル位置のリアルタイム数値構造解析(Data Inspector)、グラフィカルなビットマッププレビュー(Bitmap View)、およびキーボードショートカットの実装詳細について解説します。

スクリーンショット

[Astro] #137 Bz-Style Binary Editor — メインエディタ画面 [Astro] #137 Bz-Style Binary Editor — HELPモーダル画面

動画(GIF)

[Astro] #137 Bz-Style Binary Editor — 動作デモ

Sample data

サンプルデータは、過去に録画したSteamゲーム「Subsistence」の録画データになります。

Subsistence (64-bit, DX9) 2022-05-03 14-39-46.mp4

TYPEDATA
FileSubsistence (64-bit, DX9) 2022-05-03 14-39-46.mp4
Size1381.2 MB
Declared Ext.mp4
Detected TypeMP4

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

バイナリ値(0x000xFF)をグレースケール輝度に変換し、指定された歩幅(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">&times;</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 を活かすことで、外部ライブラリに一切依存しない強力なバイナリ編集・解析ツールを構築できました。