Repository navigation
Conversation
…ery refusal; write_net writes UTF-8 (lanl#1653) pybind11 hands every name, expression and message to Python through its strict UTF-8 decoder. A Latin-1 species name (Aµ(), the byte 0xb5) loaded, and its name raised a bare UnicodeDecodeError naming neither the file nor the line; a refusal quoting such a byte lost its words the same way. - Each of the five block parsers refuses a line, its comment off, that is not UTF-8, quoting it with the byte shown as \xb5. A byte in a comment, a block passed over or an action line never reaches Python, and still loads. - parse_net_file_structure shows such a byte in any refusal as \xhh before the bare-CR cut, which then runs over UTF-8 text. - The table-function reader shows the table text it quotes the same way: it is read at build, outside the loader's wrapper, and again by build_model_from_parsed. - write_net writes UTF-8 whatever the locale: under Latin-1 or cp1252 it wrote a byte the loader refuses, and could not write AΩ() at all. The UTF-8 rule (include/bngsim/utf8.hpp) is Table 3-7's, the one Python's strict decoder takes. windows-tail runs the new tests; conftest declares their one skip, for a platform whose C locale is UTF-8.
This was referenced Nov 12, 2026
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.
Fixes #1653.
Important
A
.netfile holding a byte that is not UTF-8 failed with a bareUnicodeDecodeErrorthat named neither the file nor the line. A Latin-1 species name (Aµ(), the byte 0xb5) loaded, and its name broke on the way to Python. A refusal quoting such a byte lost its words the same way. The loader now refuses a line it reads that is not UTF-8, with theModelErrorandValueErrorit documents, quoting the line with the byte shown as\xb5. Every other refusal, the table-function reader's among them, shows such a byte the same way.write_netnow writes UTF-8 whatever the locale.The problem
pybind11 hands every name, expression and message the core makes to Python through Python's strict UTF-8 decoder, and the loader never checked its bytes against it. On main (
80b15ae4), with the issue's script and the neighbouring routes:Aµ()in the species blockUnicodeDecodeErrorfromfrom_netandparse_net_fileModelError/ValueError:species line '1 A\xb5() 3' holds the byte 0xb5, which is not UTF-81 1 2 k 7 µin Latin-1)\xb5endmarker\xb5tfuntable file's data row or headerUnicodeDecodeError, from both loaders andbuild_model_from_parsed\xb5write_netof a UTF-8Aµ()model under a Latin-1 localewrite_netofAΩ()under a Latin-1 localeUnicodeEncodeErrorThe referees on the Latin-1 file: run_network 3.0 reads it, to A(0.4) = 1.347987 (the closed form 3 e^-0.8). BNG2.pl 2.9.3's
readFilerefuses it ("Invalid Molecule name"), and refuses the UTF-8 one too, since its names are ASCII. So the refusal is stricter than run_network and agrees with BNG2.pl, and the UTF-8 file keeps loading here as it did.The fix
Important
The issue suggests a UTF-8 check of the whole file. That would refuse six kinds of file that load on main, and whose bytes never reach Python: a byte in the header comment, a whole-line comment, a reaction's or a species' trailing comment, a block passed over, or an action line. So the check is on the text the loader reads: each of the five block parsers checks its line with the comment off, the same text it then parses. A control pins each of the six.
include/bngsim/utf8.hpp(new, header-only):utf8_char_lengthreads the well-formed sequences of the Unicode Standard's Table 3-7, which are the ones Python's strict decoder takes: no overlong form, no surrogate, nothing past U+10FFFF.shown_as_utf8shows each byte outside them as\xhh, as Python'sbackslashreplacedoes, and leaves UTF-8 text unchanged.src/net_file_loader.cpp:require_utf8, called by each of the parameters, species, functions, reactions and groups parsers after its end-of-block test, refuses a line that is not UTF-8. It uses the parser's own label (species line,functions line, ...).parse_net_file_structure's wrapper shows any such byte in a refusal as\xhhbefore A .net file with CR line endings loads as an empty model: 0 species and 0 reactions for 2 and 2, with no error (run_network refuses it) #1511's bare-CR handling. Then the 300-character cut runs over UTF-8 text, where a continuation byte always belongs to a character, so it stays at most three bytes past the mark.src/table_function.cpp: the four refusals that quote the table file (a data line, either header column, a one-column header) show it as UTF-8. The table is read when the model is built, outside the loader's wrapper, andbuild_model_from_parsedreads it through no wrapper at all, so the reader is where it is fixed.python/bngsim/convert/_net_writer.py:write_netwrites UTF-8, which the loader reads, rather than the locale's encoding. Under a Latin-1 or cp1252 locale (Windows) it wroteAµ()as a byte the loader refuses, and could not writeAΩ()at all.Docs and CI:
Model.from_net'sRaisesand the loading guide name the refusal, and the guide says what still loads.windows-tail.ymlruns the new test file. That leg already fires onsrc/net_file_loader.cpp, and Windows, at cp1252, is wherewrite_net's half of this lives.conftest.pydeclares the new test's one skip, for a platform whose C locale is UTF-8, which none of CI's is.How this was checked
backslashreplaceshows it..netI have, 178 files (the repository's plus local copies, 36 of them holding bytes over 0x7f), on main and here, on the final tree.parse_net_filedictionary: species, reactions, parameters,frozen_paramsand a digest of the whole dictionary.Model.from_netonfceri_gamma.net(2.4 MB, 58,276 reactions), median of 7, two rounds each: 95.1 / 95.0 ms on main, 94.3 / 97.2 ms here, inside the rounds' own spread.test_net_not_utf8.py, 28 cases.UnicodeDecodeError, orwrite_net'sUnicodeEncodeErrorunder the ASCII locale:build_model_from_parsed;write_netforµandΩ.write_netin the locale's encoding (2).ctest, all binaries pass..net, on the final tree:BNGSIM_SKIP_AUDIT=strict: 13,928 passed, 84 skipped;BNGSIM_CODEGEN_THRESHOLD=1): 13,914 passed, 98 skipped;80b15ae4, on the final tree: 19,576 passed, 140 skipped, 1 xfailed, 0 failed.PYTEST_COLLECTED_FLOOR(19,451), inside the 500 slack.PYTEST_SKIPPED_CEILING(252). The new file's one skip fires on no CI platform.python:3.12,--platform linux/amd64, gcc 14, built from the final tree with the[antimony,hdf5]extras and scipy):ANSI_X3.4-1968(ASCII), so thewrite_nettest runs rather than skips.test_codegen_compile_reaping,test_codegen_toolchain_verdict,test_run_tests_sh,test_vendor_script_git_cwd), which fail the same way on main in it and on simulator: leave assignment-rule species at the value a run reports (#1510) #1637's to eigenvalues: isolate rows and columns before balancing, as dgebal does (#1529) #1645's runs; none touches the loader.--all-filespasses (13 hooks, clang-format, ruff and mypy among them).ci/changelog.py checkpasses, and localsphinx-build -Wexits 0.Which CI legs this PR runs
python-tests.yml(Ubuntu, macOS)native-tests.ymlsrc/**)ctest, locally on the final treewindows-tail.yml(MSVC)src/net_file_loader.cpp,_model.pyand its own filemir.yml.net(step 8)windows-klu.yml,windows-nfsim.ymlChecked against past review findings
Sibling sites (the top review finding):
.netloaders,Model.loadandnet_to_sbml, which go through them;write_net, the loader's twin.Each is pinned.
sbml_to_net's temporary.netis written in the locale's encoding too, but its text is ASCII, even from an SBML model with Unicode names (measured), so it is left alone.Refusing what main gets right (cvode: confirm a state switch's order with an event against a tighter run (#1025) #1146):
Read as the engine reads: the UTF-8 rule is Python's own, and 9,312 strings agree with its decoder.
A control must check the effect, not the report (nfsim: refuse a seed past 2**32 - 1, which NFsim ran as its low word (#1515) #1640): the controls check the loaded species and the trajectory, and the
write_nettest reloads what it wrote.Mutate the neighbours (net: refuse a species index that is not its line (#1512) #1639, nfsim: refuse a seed past 2**32 - 1, which NFsim ran as its low word (#1515) #1640): a loader checking the comment too, a parser left unchecked, the table reader left raw, and the writer back on the locale. Each is killed.
Exception types:
Model.from_netandparse_net_fileraise theModelErrorandValueErrorthey document, where they raisedUnicodeDecodeError;build_model_from_parsedraises theRuntimeErrorit raises for any bad table line;write_netno longer raisesUnicodeEncodeError.A new skip is declared (
_DECLARED_SKIPS), and the strict audit passes.Strict xfails and quarantines citing A .net file that is not UTF-8 fails with a bare UnicodeDecodeError: a Latin-1 species name
Aµ()that loads as UTF-8 raises "can't decode byte 0xb5" from Model.from_net, and a refusal quoting such a byte loses its words #1653: none.In-flight PRs: antimony: load the main module, not the last one defined (#1508) #1642 and steady_state: say that steady_state_batch starts every entry from the initial conditions (#1521) #1643 also change
_model.py, and antimony: load the main module, not the last one defined (#1508) #1642 the loading guide. Trial merges with all seven open PRs are clean.Claims are measured: each number here comes from a run above.
The diff (abridged)
The second-byte ranges are where the overlong forms, the surrogates and the code points past U+10FFFF are excluded.
@@ src/net_file_loader.cpp — each block parser checks the text it reads if (ends_block(stripped, "reactions")) { got_end = true; break; } + require_utf8(stripped, "reaction");The same line follows the end-of-block test in the parameters, species, functions and groups parsers.
strippedis the line with its comment off, so a comment's bytes are not checked.@@ src/net_file_loader.cpp — parse_net_file_structure's wrapper } catch (const std::exception &e) { + const std::string words = shown_as_utf8(e.what()); - if (!holds_bare_cr(path)) - throw; + if (!holds_bare_cr(path)) { + if (words == e.what()) + throw; + throw std::runtime_error(words); + } ... - const char *p = e.what(); + const char *p = words.c_str();A refusal with no such byte is rethrown as it was. The bare-CR cut now runs over UTF-8 text.
The header line and both header columns are shown the same way.
Tests
python/tests/test_net_not_utf8.py(new, 28 cases):Model.from_netandparse_net_file;.netreads:build_model_from_parsedreading the table again.write_netunder the C locale with coercion off (ASCII on Linux and macOS, cp1252 on Windows):Aµ()andAΩ()round-trip.python/tests/conftest.py: the new skip's declaration.