开发浏览器端二进制编辑器的经历
什么是二进制编辑器?
二进制编辑器是一种可以直接以 16 进制或 ASCII 形式编辑文件的工具。与把“字符串”作为编辑对象的普通文本编辑器不同,二进制编辑器能够访问文件的“原始字节序列”,并在任意位置直接修改比特或字节。
在软件分析、数据恢复、协议调查等场景中,它是不可或缺的存在,GUI 应用里常见的有 HxD
和 Binary Ninja
等。不过这一次的目标是「仅依赖浏览器即可运行的单文件二进制编辑器」。
实现方针
- 完全在客户端运行(无外部传输)
- 使用
FileReader
API 读取本地文件 - 左侧显示十六进制(Hex),右侧显示 ASCII
- 点击进入编辑模式,允许直接改写数值
- 已编辑的单元以红色字体高亮
- 可以切换 HEX 编辑 / ASCII 编辑模式
- 编辑结果可以重新导出为文件下载
遇到的问题与修正
1. 编辑框的偏移
最初的实现中,当针对某个单元格显示黄色边框时,边框会微微偏离中心,看上去很别扭。这是因为 CSS 的 line-height
与 flex
布局产生了影响,显示字符与边框的基准点错位。虽然假设使用等宽字体,但浏览器的渲染过程可能让不同字体拥有不同的基线,结果就是上下偏移。
通过强制 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)
用于红色高亮,只要与原值不同就加上对应的类,是一个相当简单的实现。
总结
这款浏览器版二进制编辑器能够在离线环境下运行,同时作为「完全客户端工具」也满足安全需求。
- 想快速修改少量二进制时
- 在无法安装专用应用的环境里进行调查
- 教学或学习时,希望直观理解「字节序列」
在这些场景中都能派上用场。