Visitar URL original
fix: read caller-supplied files as utf-8-sig, so a UTF-8 BOM is not data by PerryLink · Pull Request #1398 · NandhaKishorM/laya · GitHub
Skip to content

fix: read caller-supplied files as utf-8-sig, so a UTF-8 BOM is not data - #1398

Merged
NandhaKishorM merged 2 commits into
NandhaKishorM:mainfrom
PerryLink:fix/cli-utf8-bom
Oct 28, 2026
Merged

NandhaKishorM merged 2 commits into
NandhaKishorM:mainfrom
PerryLink:fix/cli-utf8-bom

Conversation

@PerryLink

@PerryLink PerryLink commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

A UTF-8 BOM — what Notepad and Excel write — is either refused or silently corrupted by the two CLI readers and by the evidence reader.

What happens

$ laya --questions q.json "hi"        # q.json saved by Notepad, valid JSON
laya: --questions file q.json is not valid JSON: Unexpected UTF-8 BOM (decode using utf-8-sig): line 1 column 1 (char 0)

For a batch file the failure is silent: the BOM becomes U+FEFF at the front of the first request, so the model reads it as part of the text (the same for cat batch.txt | laya --batch -). laya-evals evidence --checkpoint DIR refuses a config or report saved the same way.

Why

load_questions, read_batch_lines (file and piped stdin) and _read_json_object opened their input as plain utf-8. The other file readers in this repo already use utf-8-sig for exactly this reason — docker/entrypoint.py ("a secret file written by a Windows editor"), laya/train.py ("UTF-8 BOM headers from Excel"), and scripts/http_inference_smoke.py. utf-8-sig reads plain UTF-8 unchanged when there is no BOM, so nothing that works today changes.

Change

Three CLI opens (questions file, batch file, --batch - stdin reconfigure) and one evidence open. CLI/evidence only; no library behaviour changes.

Tests

  • tests/test_cli.py: a BOM-prefixed questions file parses and answers (exit 0, questions unchanged); a BOM-prefixed batch file yields ["first ticket", "second ticket"] rather than "\ufefffirst ticket"; the same for a piped stream. Without the change 208 passed, 3 failed; with it 211 passed, 0 failed.
  • tests/test_evidence.py: a BOM-prefixed artifact parses. Without the change the check reports refused: … is not valid JSON: Unexpected UTF-8 BOM (decode using utf-8-sig); with it all 98 checks passed.

Run as PYTHONPATH=<deps>:<repo> python3 tests/test_cli.py / python3 tests/test_evidence.py (the CLI suite stubs the Router; no weights are loaded).

…OM is not data

A questions or batch file written by Notepad or Excel starts with a UTF-8 BOM. Both
readers opened the file as plain utf-8, so the BOM became U+FEFF: `json.load` refused a
valid questions file ("Unexpected UTF-8 BOM (decode using utf-8-sig)"), and a batch file
sent U+FEFF as the first character of the first request, which the model then read as part
of the text. A piped `--batch -` stream had the same leak.

The other file readers in this repo already open as utf-8-sig for this reason
(docker/entrypoint.py, laya/train.py, scripts/http_inference_smoke.py). utf-8-sig reads
plain UTF-8 unchanged when there is no BOM.
The same BOM one layer down: `_read_json_object` opened the artifact as plain utf-8, so a
checkpoint config or report written by a Windows editor was refused as "not valid JSON" for
a file that is valid JSON. `laya/train.py` and `docker/entrypoint.py` already open their
caller files as utf-8-sig for this reason; a file without a BOM reads identically.

`tests/test_evidence.py` gains one check (97 → 98).
@PerryLink PerryLink changed the title fix(cli): read the --questions and --batch files as utf-8-sig, so a BOM is not data fix: read caller-supplied files as utf-8-sig, so a UTF-8 BOM is not data Oct 27, 2026
@NandhaKishorM
NandhaKishorM merged commit 508ad2e into NandhaKishorM:main Oct 28, 2026
51 of 52 checks passed
NandhaKishorM added a commit that referenced this pull request Oct 28, 2026
24 PRs. Vietnamese kept off the English checkpoint (#1392), deciding-window
attribution preserved through end hooks (#1369), RemoteRouter identifying
itself with a configurable User-Agent (#1396), ten more inputs refused where
they enter (#1372, #1394, #1397, #1398, #1400, #1401, #1402, #1403, #1404,
#1405, #1406), per-language temperatures and per-call budgets in .NET and
Rust (#1370, #1371), .NET calibration files (#1384), the Go router lifecycle,
SHA-256 pins and routed inference (#1374, #1379, #1395), a C++ foundation
(#1399, #1407), and parity-lane sharding that replaces the budget headroom
added for 0.4.16 (#1373, #1375).

Verified before tagging: all 92 suites pass, 640 tests, up from 612. ruff
clean on both lists. laya-ts 1251 tests across 44 files, tsc --noEmit clean.
Every generated-artifact gate clean on CPython 3.12 / unicodedata 15.0.0:
Java 20 families match laya (9 unverified), UnicodeTables matches CPython
15.0.0, LanguageTables matches laya/lang.py, three Go tables_gen.go match,
both laya-cpp .inc files match. Version agrees across pyproject.toml,
laya.__version__ and nix/package.nix. README carries 0.4.17 only.

Security audit of the merge diff, 362766 added lines over 480 files: zero
obfuscation, zero secrets, zero boundary hits, zero binaries, no mode or
symlink changes, 20 of 20 actions SHA-pinned including the new cpp.yml, npm
install hooks unchanged, and no third-party host in any added line
(example.com 90, github.com 2, download.pytorch.org 1, docs.rs 1). The 12
dynamic-exec hits are the C++ test harness invoking its own checker through
sys.executable, the Go sample generator reading laya's own example by AST at
development time, and one comment in rust.yml.

Held: #1364 (unchanged, it moves a cross-port contract), #1381 (a #[doc(hidden)]
but pub Rust hook that would ship as public API able to replace the forward
pass), #1386 (route_batch switched from self.route to self._route, silently
bypassing subclass overrides the docstring promises), and nine stacked port
PRs that conflict with each other and need linearizing (#1377, #1378, #1380,
#1382, #1383, #1385, #1387, #1388, #1389, #1390).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@NandhaKishorM

Copy link
Copy Markdown
Owner

Merged. utf-8-sig for caller-supplied files is the right default: a BOM is an encoding marker, and letting it arrive as a character means the first key or the first field silently carries an invisible prefix that compares unequal to the same text typed by hand. Editors on Windows write it without asking, so this is a real input rather than a hypothetical one.

Reading it as data is exactly the failure worth preventing, because it does not raise. It produces a near miss that looks like a content mistake.

One note on the merge. This landed alongside #1397 and both add a block to the same part of tests/test_cli.py, which git saw as an add/add conflict. I kept both, their directory case first and your BOM case second, and the file reports 213 passed. If your branch looks short a hunk after pulling main, that is the resolution rather than a lost change.

Shipped in 0.4.17.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants