From c7c81c3351bf894999ad8ddec1e99265fd6f53d0 Mon Sep 17 00:00:00 2001 From: Hyper Hu Date: Mon, 5 Oct 2026 15:09:08 +0800 Subject: [PATCH] Escape NUL bytes in white diff admin text. NUL is valid UTF-8, so decoding with backslashreplace keeps it as-is. PostgreSQL rejects NUL in text columns, so writing the evaluation fails and EvaluationService retries it forever, leaving the testcase unscored. Escape it as \x00, consistent with how backslashreplace renders invalid bytes, so the admin can still see the difference. --- cms/grading/steps/whitediff.py | 8 ++++++-- cmstestsuite/unit_tests/grading/steps/whitediff_test.py | 7 +++++++ 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cms/grading/steps/whitediff.py b/cms/grading/steps/whitediff.py index f5b7d39e13..f887543578 100644 --- a/cms/grading/steps/whitediff.py +++ b/cms/grading/steps/whitediff.py @@ -120,8 +120,12 @@ def _white_diff(output: typing.BinaryIO, res: typing.BinaryIO) -> tuple[bool, st lout = lout[:LENGTH_LIMIT] + b"..." if len(lres) > LENGTH_LIMIT: lres = lres[:LENGTH_LIMIT] + b"..." - lout = lout.decode("utf-8", errors='backslashreplace') - lres = lres.decode("utf-8", errors='backslashreplace') + # NUL is valid UTF-8, so backslashreplace lets it through, + # but PostgreSQL cannot store it in a text column. + lout = lout.decode("utf-8", errors='backslashreplace') \ + .replace("\x00", "\\x00") + lres = lres.decode("utf-8", errors='backslashreplace') \ + .replace("\x00", "\\x00") return False, f"Expected `{lres}`, found `{lout}` on line {line}" diff --git a/cmstestsuite/unit_tests/grading/steps/whitediff_test.py b/cmstestsuite/unit_tests/grading/steps/whitediff_test.py index 22c54e6e97..66c566e425 100755 --- a/cmstestsuite/unit_tests/grading/steps/whitediff_test.py +++ b/cmstestsuite/unit_tests/grading/steps/whitediff_test.py @@ -72,6 +72,13 @@ def test_diff_wrong_long_line(self): line2 = line1 + "0" self.assertFalse(self._diff(line1, line2)) + def test_diff_nul_byte_escaped_in_admin_text(self): + correct, admin_text = _white_diff( + BytesIO(b"hello\x00world"), BytesIO(b"helloworld")) + self.assertFalse(correct) + self.assertNotIn("\x00", admin_text) + self.assertIn("hello\\x00world", admin_text) + if __name__ == "__main__": unittest.main()