panic 'not a char boundary' in extract_json_object fallback path on CRLF input (same class as #2509)
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 81.1k
- Forks
- 5.1k
- Avg merge
- 4d 21h
- Merged PRs (30d)
- 35
Description
Summary
extract_json_object's no-marker fallback path computes line offsets on the assumption of LF line endings. On CRLF input the offset drifts one byte per preceding line, and input[start_pos..] can land in the middle of a multibyte character — panicking rtk instead of degrading to Tier 2/3.
This is the same not a char boundary class as #2509. That issue's fix (27f9739b) corrected the forward brace-balancing scan (char_indices() + len_utf8()), which closed the marker path. The fallback path's start_pos derivation was not touched and is still broken.
Root cause
src/parser/mod.rs:157-163:
found_start = Some(
input[..]
.lines()
.take(idx)
.map(|l| l.len() + 1) // <-- assumes '\n'
.sum::<usize>(),
);
str::lines() strips \r\n as well as \n, so l.len() excludes the \r while + 1 accounts for only the \n. Every preceding CRLF line makes start_pos one byte too small. When the drift lands inside a multibyte character, the slice at src/parser/mod.rs:175 panics.
Reproduction (end to end)
A vitest that dies before emitting its JSON summary — CRLF output, an accented path, no numTotalTests marker, which is what forces the fallback path:
cat > /tmp/shim/vitest <<'SH'
#!/usr/bin/env bash
printf 'vitest run --reporter=json\r\nfailed to load config\r\n at C:\\dev\\caf\xc3\xa9\r\n{"error": 1}\r\n'
exit 1
SH
chmod +x /tmp/shim/vitest
PATH=/tmp/shim:$PATH rtk vitest
Actual:
thread 'main' panicked at src/parser/mod.rs:175:35:
start byte index 66 is not a char boundary; it is inside 'é' (bytes 65..67 of string)
exit=101
Expected: Tier 2 or Tier 3 degradation, per the module's "RTK never returns false data silently" contract.
Minimal unit-level repro:
let input = "ligne un\r\nligne deux\r\nfin é\r\n{\"a\": 1}\r\n";
extract_json_object(input);
// panicked: start byte index 27 is not a char boundary; it is inside 'é' (bytes 26..28)
Even without multibyte the result is wrong, just not fatal — "prefix line one\r\nprefix line two\r\n{\"a\": 1}\r\n" returns Some("\r\n{\"a\": 1}"), a slice that starts before the { and breaks the starts_with('{') contract two existing tests assert.
Impact
extract_json_object is called from VitestParser::parse (src/cmds/js/vitest_cmd.rs:59) with raw captured stdout and no catch_unwind, so the panic aborts rtk. CRLF is the norm on Windows, and the fallback path is what runs precisely when a test run has already failed — so this fires on the error path, on the platform where it is most likely.
Suggested fix
Take the byte offset from the iteration itself rather than reconstructing it from l.len() + 1 — for example track a running offset that adds the real line terminator width, or locate the line via its byte position in input.
Suggested regression coverage
A table-driven cross-product over line ending (\n / \r\n) × multibyte position (before / inside / after the object) × marker present or absent × trailing content, asserting the invariant: a Some(s) result is a char-boundary substring of the input, starts with {, ends with }, and round-trips through serde_json. That closes the whole class instead of adding one example at a time, and needs no new dev-dependency.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in src/parser/mod.rs:157-175, then inspect how extract_json_object is called from src/cmds/js/vitest_cmd.rs:59. Run the provided CRLF reproduction and existing parser tests, then add regression coverage for line endings, multibyte positions, markers, and trailing content. Done means no panic, and any Some result starts with {, ends with }, remains on character boundaries, and parses as JSON.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- cli, testing-qa
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 84/100