怎么修的 · znzryb
修的思路就一句话:**数据库收不下的字节,在进门之前就洗掉**。洗两道。

- **第一道,在评测机上。** 选手输出、checker 的说明、编译报错、运行结果,先按上限截断,再去掉 NUL 和不合法的字节。
- **第二道,在仲裁器的入口。** 仲裁器收到任何一份结果,在动数据库之前,把里面所有要存的文本再洗一遍,连存成 JSON 的那几列(样例结果、建包结果)也洗。
为什么两边都洗:家用评测机睡着的时候不会跟着更新程序,醒来之前它跑的还是旧版。仲裁器这一道就是给旧版评测机兜底的,不管谁交上来的结果,进库的都是干净的。
洗掉之后,那位同学会拿到「答案错误」,输出对比里显示 `123`。「共多少字节」这个数字照原样记,洗掉的字节不会让它变小。
## 以前为什么没洗

其实代码里早就有一个清洗函数,只是只用在了**测试点的输入和答案缩略**上。当初出事的是一个二进制测试点,修的时候就只修了那一处。选手的输出、checker 的说明、编译报错、运行结果全都还裸着 —— 而选手的输出恰恰是最不受我们控制的那一个。
这次把整张表都填满了,**正式提交也一样**:同样的笔误放在正式提交里,以前会让整条提交落不了库,现在会正常判成「答案错误」。
## 顺手修的另一处:截断不能切出半个字符

代码审查的时候发现了一个藏得更深的问题。汉字在 UTF-8 里占 3 个字节,如果截断正好落在一个字的中间,只剩下半个字,那也是不合法的文本,数据库同样会拒收。
旧的截断函数号称「不会切出半个字符」,实际上它会把半个字的第一个字节留在末尾,就像图上第二行。平时评测机自己截好的内容不会超长,所以一直没出事;但仲裁器在洗完之后还要按 64 KB 再截一刀,旧版评测机交上来一段超长的中文编译报错时,就会被这一刀重新截坏。现在凑不齐一个完整字符就整个丢掉,截出来的永远是合法文本。
## 还剩一件没改的事
上一节说的第二个问题 —— 写库失败一律回 503、评测机一律重试 —— 这次没有动。数据洗干净以后,「数据本身有问题」这条路已经走不到了,剩下的写库失败基本都是真正的临时故障,重试是对的。如果以后又出现别的「永久错误」,就该把它和临时故障分开回。
## 上线之后
修复 10-09 中午上线。用测试账号把同一份带 `'\0'` 的代码在 `lg-p1059` 上原样跑了一遍:「运行样例」4.3 秒、正式提交 4.6 秒,都判成了「答案错误」,输出里的 NUL 被去掉,系统事件里没有再出现「认领失败」,日志里也没有 0x00 的报错了。
修复在 [PR #288](https://github.com/znzryb/OJ-AC-ladder/pull/288),每一道清洗都有测试:带 NUL 的运行结果、提交结果、JSON 结果都能照常存进去,旧版评测机交来的 90 KB 中文编译报错也能截成合法的 64 KB。