什么是二进制编辑器?

二进制编辑器是一种可以直接以 16 进制或 ASCII 形式编辑文件的工具。与把“字符串”作为编辑对象的普通文本编辑器不同,二进制编辑器能够访问文件的“原始字节序列”,并在任意位置直接修改比特或字节。

在软件分析、数据恢复、协议调查等场景中,它是不可或缺的存在,GUI 应用里常见的有 HxDBinary Ninja 等。不过这一次的目标是「仅依赖浏览器即可运行的单文件二进制编辑器」。


实现方针

  • 完全在客户端运行(无外部传输)
  • 使用 FileReader API 读取本地文件
  • 左侧显示十六进制(Hex),右侧显示 ASCII
  • 点击进入编辑模式,允许直接改写数值
  • 已编辑的单元以红色字体高亮
  • 可以切换 HEX 编辑 / ASCII 编辑模式
  • 编辑结果可以重新导出为文件下载

遇到的问题与修正

1. 编辑框的偏移

最初的实现中,当针对某个单元格显示黄色边框时,边框会微微偏离中心,看上去很别扭。这是因为 CSS 的 line-heightflex 布局产生了影响,显示字符与边框的基准点错位。虽然假设使用等宽字体,但浏览器的渲染过程可能让不同字体拥有不同的基线,结果就是上下偏移。

通过强制 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"

解决办法很直接,在 keydown 事件中显式捕捉 Escape,改成取消编辑的流程。

document.addEventListener("keydown", e => {
  if (e.key === "Escape") {
    cancelEdit();
    e.preventDefault(); // 阻断原本的输入流程
  }
});

这样一来,ESC 仅用于「取消编辑」,再不会在单元格里留下字符。

3. 重新载入时红字残留

读取文件并编辑后,再加载另一份文件时,先前的红色高亮会残留。

原因是 用于追踪已编辑单元的数组或状态,在读取新文件时没有初始化。即便清空了显示区域,内部的「变更标记」仍然保留,新文件就继承了红字状态。

修正方法是在文件读取完成后立即调用 clearModifiedState(),彻底重置变更标记。

4. HEX/ASCII 模式切换

初始实现允许同时编辑 HEX 和 ASCII,结果导致「哪一个才是正确值」难以判断。特别是一个字节同时被两个输入表单处理,同一单元从不同路径写入时会发生竞争。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) 用于红色高亮,只要与原值不同就加上对应的类,是一个相当简单的实现。


总结

这款浏览器版二进制编辑器能够在离线环境下运行,同时作为「完全客户端工具」也满足安全需求。

  • 想快速修改少量二进制时
  • 在无法安装专用应用的环境里进行调查
  • 教学或学习时,希望直观理解「字节序列」

在这些场景中都能派上用场。