Repository navigation
fix(digest): hashes of text with a lone surrogate return a digest instead of raising UnicodeEncodeError - #761
Open
antonwolfy wants to merge 10 commits into
Open
antonwolfy wants to merge 10 commits into
antonwolfy wants to merge 10 commits into
Conversation
…cache_path (intel#713) cache_path hashed the URL with url.encode("utf-8"), which raises UnicodeEncodeError for a surrogate-escaped argv value such as https://example.com/n\udcff. It now hashes os.fsencode(url): the clone directory of every UTF-8 URL is unchanged on POSIX, and two different surrogate-escaped URLs keep two directories. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
…cter no longer stops _safe_id (intel#714) _safe_id appended hashlib.sha256(value.encode()) when the id held one of the characters Windows refuses, so a surrogate-escaped id such as n\udcff? from a --command template raised UnicodeEncodeError. It now hashes os.fsencode(value): ids that encode as UTF-8 keep their directory name on POSIX, and two different surrogate-escaped ids keep two names. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
…on-UTF-8 byte no longer raise (intel#715) token in repos/reach.py hashed f"{salt}\0{path}\0{position}".encode() with a file name from the tree, and token in repos/skill_reach.py did the same with the case id, so a surrogate-escaped name raised UnicodeEncodeError. Both now hash os.fsencode of the same string: the token of every UTF-8 path or case id is unchanged on POSIX. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
…cache key or the topics cache (intel#716) URLClient._cache_key hashed self.url.encode(), and the question step of EvaluationPipeline hashed self.doc_source.encode("utf-8") for the topics cache directory, so a surrogate-escaped --doc-source raised UnicodeEncodeError. Both now hash os.fsencode of the value, as commands/questions.py already does for the same doc source: the key and the directory of a UTF-8 source are unchanged on POSIX. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
…he plugin-delta pairing (intel#717) _paired_plugin_deltas hashed f"{baseline_name}\0{plugin_name}".encode("utf-8") for the delta artifact's name once a pair compared, and a JSON escape such as "n\udcff" or "n\ud800" in the matrix config loads as a lone surrogate, so it raised UnicodeEncodeError. It now encodes with "surrogatepass": the suffix of every name that encodes as UTF-8 is unchanged, and different lone surrogates keep different suffixes. os.fsencode would still raise for "n\ud800", which is not a surrogate-escaped byte. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
…tel#718) task_id hashed "\n".join(c.symbols).encode(), so a Candidate whose symbol holds a surrogate-escaped byte raised UnicodeEncodeError. It now hashes os.fsencode of the joined symbols: the id of every UTF-8 symbol set is unchanged on POSIX. The SCIP reader decodes a symbol as strict UTF-8 and refuses the index otherwise (code_index/scip.py _text), so a symbol read from an index holds no surrogate of any kind and os.fsencode is enough here. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
…easured_digest (intel#719) Bundle.measured_digest joined f"{item.path}:{item.sha256}" and encoded it with .encode("utf-8"); item.path is a file name from the skill directory, so a surrogate-escaped name raised UnicodeEncodeError. It now hashes os.fsencode(rows): the digest of a UTF-8 path is unchanged on POSIX. A file name read from a directory can hold only a surrogate-escaped byte (U+DC80..U+DCFF), which os.fsencode encodes. _digest_text, the other hash in the module, takes text read as strict UTF-8 and is left as it is. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
… stops the digest of the data (intel#720) item_digest and assignment_digest (eval/leakgate.py), graph_digest (eval_coverage.py), expectations_digest (repos/conformance.py), response_cache_key (caching.py) and _content_key (harnesses/harbor_export.py) dump parsed data with ensure_ascii=False and encode it as UTF-8, so a "\udcff" or "\ud800" escape in the input file loads as a lone surrogate and raised UnicodeEncodeError. They now encode with "surrogatepass": every digest that encoded before is unchanged, and different surrogates keep different digests. errors="replace" would give two inputs one digest, os.fsencode raises for "\ud800", and refusing the escape in each loader would change what a loaded file may hold at many call sites. The Harbor export's content key is reached by a terminal-bench task.yaml whose metadata holds such an escape (harbor_view calls it with _task_toml); the export then writes task.toml as UTF-8, which still raises, and is left as it is. Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
This was referenced Nov 14, 2026
Open
compute_question_set_hash raises UnicodeEncodeError for a question with a lone-surrogate escape
#757
Open
|
✅ OpenCodeReview: Review complete: 0 finding(s) across 2 selected item(s). CI at Review details
|
exai-bot
reviewed
Nov 14, 2026
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #761 +/- ##
==========================================
+ Coverage 95.71% 95.72% +0.01%
==========================================
Files 413 415 +2
Lines 55236 55367 +131
Branches 9501 9516 +15
==========================================
+ Hits 52867 53002 +135
+ Misses 1466 1463 -3
+ Partials 903 902 -1
🚀 New features to boost your workflow:
|
…system encoding
Settles the review point that os.fsencode makes the hashes depend on the locale: with an ASCII file-system encoding (LC_ALL=C PYTHONUTF8=0) it raised UnicodeEncodeError for an ordinary non-ASCII URL, path or symbol.
The cache path of a remote source, the harness id, the reach and skill-reach tokens, the URL cache key, Bundle.measured_digest, the graph task id and the topics cache key now encode with ("utf-8", "surrogateescape"). The digest of every UTF-8 input is unchanged, a non-UTF-8 byte still gets a digest, and the result no longer depends on the locale.
Tests run each site with an ASCII os.fsencode and one URLClient case in a child process with an ASCII file-system encoding.
Signed-off-by: Anton Volkov <antonwolfy@gmail.com>
Contributor
Author
|
Review points answered at |
skill_reach.token raised UnicodeEncodeError for a case id holding a lone \ud800 escape, since surrogateescape maps only \udc80..\udcff. Fall back to surrogatepass; digests that encoded before are unchanged. Signed-off-by: vchamarthi <123653452+vchamarthi@users.noreply.github.com>
Contributor
|
Pushed 1 commit to
|
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Eight sites hash text that can hold a lone surrogate (a surrogate-escaped argv value or file name, or a
\udXXXescape in loaded JSON/YAML) with.encode()or.encode("utf-8"), which raisesUnicodeEncodeError: ... surrogates not allowed. Onmain:maincache_path("https://example.com/n\udcff", cache_dir)(whatRepoSource.parse("https://example.com/n\udcff@" + "a" * 40)gives)UnicodeEncodeError_safe_id("n\udcff?")UnicodeEncodeErrorreach.token("s", "n\udcff", "p")andskill_reach.token("s", "n\udcff", "p")UnicodeEncodeErrorURLClient("n\udcff")._cache_key(), and the topics cache key ofEvaluationPipeline._generate_questionsfordoc_source="url:https://example.com/n\udcff"UnicodeEncodeError_paired_plugin_deltaswith a matrix cell namedn\udcff(orn\ud800)UnicodeEncodeErrortask_id(c)withc.symbols == ("n\udcff",)UnicodeEncodeErrorBundle(..., files=[BundleFile(path="n\udcff", ...)]).measured_digest()UnicodeEncodeErroritem_digest,assignment_digest,graph_digest,expectations_digest,response_cache_keyand_content_keywithjson.loads('"n\\udcff"')(or"n\\ud800") in the dataUnicodeEncodeErrorNow each returns a digest, and the digest of every input that encoded before is unchanged (the tests pin the literals). #713 to #716, #718 and #719 hash
.encode("utf-8", "surrogateescape"), which keeps the value of every UTF-8 input, keeps two different surrogate-escaped values apart, and does not depend on the locale (os.fsencodewould raise for a non-ASCII value when the file-system encoding is ASCII, e.g.LC_ALL=C PYTHONUTF8=0). #717 and #720 use.encode("utf-8", "surrogatepass")instead: their text comes from a JSON/YAML escape, where"\ud800"is possible andos.fsencode("\ud800")raises (so #717 does not take thesurrogateescapeencoding the other sites use);errors="replace"would give two inputs one digest, and refusing the escape in each loader would change what a loaded file may hold at many call sites. #718 keepssurrogateescape: the SCIP reader decodes every symbol as strict UTF-8 and refuses the index otherwise, so a symbol read from an index holds no surrogate at all.For #720,
harbor_export._content_keyis reached by a terminal-benchtask.yamlwhose metadata holds such an escape (difficulty: "n\udcff"), so it is fixed and tested here; the export that follows still stops on writingtask.toml, which is tracked in #759.The same search (every
.encode(indokimi/dokimiandscripts) found sites with the same cause that no issue names, tracked in: #754 (run_reachsalt from the tree label), #755 (build_fixturewritesSKILL.mdfor a case name with a surrogate), #756 (Context7 and topic-extractor cache keys; this also stops alocal:doc source with a non-UTF-8 directory name after #716), #757 (compute_question_set_hash), #758 (Atomid and retention cache key), #760 (process.runinput).Closes #713
Closes #714
Closes #715
Closes #716
Closes #717
Closes #718
Closes #719
Closes #720
Changes
tests/test_lone_surrogate_digests.py.repos/source.pycache_path,harnesses/runner.py_safe_id,repos/reach.pyandrepos/skill_reach.pytoken,mcp/url_client.py_cache_key,orchestrator/pipeline.pytopics cache key,repos/graph_tasks.pytask_id,novelty/bundle.pymeasured_digest:.encode("utf-8", "surrogateescape")in place of.encode(), so the digest does not depend on the file-system encoding.os.fsencode(and oneURLClientcase in a child process withLC_ALL=C PYTHONUTF8=0 PYTHONCOERCECLOCALE=0) pin that a non-ASCII URL, path or symbol hashes to its UTF-8 digest at each of these sites.eval/matrix_rollup.pypair suffix,eval/leakgate.pyitem_digestandassignment_digest,eval_coverage.pygraph_digest,repos/conformance.pyexpectations_digest,caching.pyresponse_cache_key,harnesses/harbor_export.py_content_key:.encode("utf-8", "surrogatepass").novelty/bundle.py_digest_text(text read as strict UTF-8),repos/reach.pyandrepos/skill_reach.pysalts from the clock (the tree-label one inreach.pycan, see run_reach raises UnicodeEncodeError for a checkout whose directory name has a non-UTF-8 byte #754), and the other hashes the search read.Risk
None for an input that encoded before: the tests pin the digest, token, key or directory name of a UTF-8 input to the literal
maingives (ten of them pass onmain's code). A value with a lone surrogate used to stop the command with a traceback; it now gets a digest, so a cache directory or file name is made for it (for #713,n_-4d0dae2da255.gitunder the cache dir). On Windows a surrogate-escaped name is not a file-name form, and the tests that need one are skipped there.Testing
On a Linux review host, at
4860cdb(onmain1bb8978):make ci-local: 33 passed, 0 failed, 14 skipped. 20374 tests passed, 19 skipped; changed-line coverage 100% of 15 lines; mypy, ruff, the duplicate ratchet and the fragment check pass.main's code: 16 fail withUnicodeEncodeError(each test that feeds a surrogate) and the 10 that pin a UTF-8 digest pass, so the literals are whatmaingives. On this branch all 26 pass.errors="replace"(two surrogates collide),U+DCFEreplaced byU+DCFF(two names share a digest), a high surrogate folded into an escaped byte, the pre-fix.encode()back (the raise), the UTF-8 digest changed, and for eval/matrix_rollup.py pair suffix raises UnicodeEncodeError for a matrix cell name with a lone surrogate #717os.fsencodeinstead ofsurrogatepass(raises forn\ud800). Each fails the test of its site.surrogateescapecommit, where the sites usedos.fsencode: all 8 fail withUnicodeEncodeError, the 26 others pass.Revert
A plain revert restores the
UnicodeEncodeErrorfor these inputs. Nothing is stored: a cache directory or file made for a surrogate-escaped name while this was in is not read by the old code.Checklist
pytestpassesruff,mypy, and schema checks passAGENTS.md.github/.licenserc.yaml)