バイナリエディタとは?

バイナリエディタとは、ファイルを 16 進数や ASCII 表記で直接編集できるツールである。通常のテキストエディタが「文字列」を編集対象とするのに対し、バイナリエディタはファイルの持つ「生のバイト列」にアクセスし、任意の位置のビットやバイトを直接変更できる。

ソフトウェア解析やデータ復旧、プロトコル調査などの場面で欠かせない存在であり、GUI アプリとしては HxDBinary Ninja のようなものが有名だ。しかし今回の目標は「ブラウザだけで動作するシングルファイルのバイナリエディタ」である。


実装方針

  • 完全クライアントサイド(外部送信なし)
  • FileReader API でローカルファイルを読み込み
  • 左側に 16 進数(Hex)、右側に ASCII を表示
  • クリックで編集モードに入り、直接値を書き換え可能
  • 編集した箇所は赤字でハイライト
  • HEX 編集 / ASCII 編集モードを切り替え可能
  • 編集結果は再度ファイルとしてダウンロード可能

実際に躓いた点と修正

1. 編集枠のずれ

初期実装では、編集対象セルに黄色枠を出すとわずかに中心がずれて不自然だった。これは CSS の line-heightflex 配置の影響で、表示文字と枠の基準点がずれていたために発生していた。特に monospace フォントを前提にしているが、ブラウザの描画処理はフォントごとにベースラインが異なる場合があり、結果的に見た目が上下どちらかに寄ってしまった。

vertical-align: middle を強制し、さらに display: inline-block にして高さを揃えることで解決できた。

.hex-cell.editing {
  outline: 2px solid yellow;
  vertical-align: middle;
}

2. ESC キー押下で文字が入力されてしまう

初期実装では、編集枠を出した状態で ESC を押すと「EE」などの文字がセルに入力されてしまった。

これは keydown イベントを拾っていないため、ブラウザが押されたキーの文字コードをそのままセル編集の入力処理に渡していた ことが原因だ。通常の入力処理は 0–9, A–F のみを想定していたが、Escape キーにもキーコードが割り振られており(昔の実装では keyCode=27、現在の仕様では key="Escape")、この入力が意図せず処理ルートに乗ってしまい "E" と解釈された。"EE" が書き込まれるという挙動になった。

解決策はシンプルで、keydown イベントで Escape を明示的に捕捉し、セルの編集をキャンセルする処理に置き換えることだった。

document.addEventListener("keydown", e => {
  if (e.key === "Escape") {
    cancelEdit();
    e.preventDefault(); // 本来の入力ルートに流さない
  }
});

これにより、ESC は「編集解除」としてのみ機能し、セルに文字が入力されることはなくなった。

3. 再読込時に赤字が残る

一度ファイルを読み込んで編集した後、別ファイルを読み込むと前回の赤色ハイライトが残ってしまうバグがあった。

原因は、編集済みセルを追跡する配列や状態がファイル読み込み時に初期化されていなかったことだ。表示領域だけをクリアしても内部の「変更フラグ」が残っていたため、新しいファイルでも赤字状態が引き継がれてしまった。

修正として、ファイル読込直後に clearModifiedState() を呼び出して変更フラグを完全にリセットするようにした。

4. HEX/ASCII モード切り替え

最初の実装では HEX と ASCII が同時編集可能で、結果的に「どちらが正しいのか」が分かりにくかった。特に 1 バイトを同時に 2 種類の入力フォームで扱うため、同一セルを別ルートで書き換える競合が発生していた。ASCII 入力をすると即座に HEX 表記が上書きされるが、途中入力の段階では一時的に不整合な状態が見えてしまうことがあった。

この混乱を防ぐため、モードを明示的にセレクトボックスで選択する方式に変更し、デフォルトを HEX 編集に統一した。これでユーザーは「今は HEX を入力している」と明確に理解でき、同期処理もシンプルになった。


コードの一部

実際の編集処理は下記のように実装した。

function applyEdit(offset, newValue) {
  if (mode === "hex") {
    buffer[offset] = parseInt(newValue, 16);
  } else {
    buffer[offset] = newValue.charCodeAt(0);
  }
  markModified(offset);
  render();
}

markModified(offset) は赤字ハイライト用で、元の値と異なる場合にクラスを付与するシンプルな実装である。


まとめ

今回のブラウザ版バイナリエディタは、オフラインでも動作し、セキュリティ的にも安全な「完全クライアントサイドツール」となった。

  • ちょっとしたバイナリ修正をしたいとき
  • 専用アプリをインストールできない環境での調査
  • 教育や学習用途で「バイト列」を直感的に理解したいとき

こうした場面で活用できる。