vt_shell: command spans are character indices but sliced as bytes (plan.rs:185)
vp run <script> aborts with a slice panic before the script runs when the
script's command string contains a non-ASCII character. The Range<usize>
returned by try_parse_as_and_list counts characters, but every consumer
slices the command string by bytes, so the range can end in the middle of a
multi-byte character.
Reproduced with vp v0.3.0 (vite-task rev d05b1dc), macOS 26.6.2 arm64.
Filing here rather than on the vite-plus tracker the panic handler points at,
since the code lives in this repo — happy to move it if you prefer.
Panic
thread '<unnamed>' (28859855) panicked at crates/vt_plan/src/plan.rs:185:65:
end byte index 78 is not a char boundary; it is inside '置' (bytes 77..80 of string)
Reproduction
A package.json is the whole repro — no dependencies, no install:
{
"name": "pm-probe",
"version": "1.0.0",
"scripts": {
"whoami": "node -e \"console.log('agent =', process.env.npm_config_user_agent || '(未设置)')\""
}
}
$ vp run whoami
thread '<unnamed>' panicked at crates/vt_plan/src/plan.rs:185:65:
end byte index 78 is not a char boundary; it is inside '置' (bytes 77..80 of string)
The command string is 78 characters but 84 bytes, and its last multi-byte
character 置 occupies bytes 77..80 — so the character-based end index 78 lands
inside it. Position matters, not merely the presence of non-ASCII: the same
script with the Chinese text earlier in the line happens to survive, because the
byte index it produces is still a valid boundary (it just slices the wrong text).
Root cause
crates/vt_shell/src/lib.rs:95:
fn pipeline_to_command(pipeline: &Pipeline) -> Option<(TaskParsedCommand, Range<usize>)> {
let location = pipeline.location()?;
let range = location.start.index..location.end.index;
brush_parser::tokenizer::SourcePosition::index is documented as
The 0-based index of the character in the input stream.
while every consumer of that range slices a &str, i.e. by byte:
crates/vt_plan/src/plan.rs:185 — the panic site
crates/vt_plan/src/plan.rs:266
crates/vt_plan/src/plan.rs:281
(The same range is also stored through Context::push_stack_frame, though
nothing slices with it today.)
All three sites sit under the // Build execution display comment, so what
aborts the run is display bookkeeping, not the work itself.
vt_shell: command spans are character indices but sliced as bytes (
plan.rs:185)vp run <script>aborts with a slice panic before the script runs when thescript's command string contains a non-ASCII character. The
Range<usize>returned by
try_parse_as_and_listcounts characters, but every consumerslices the command string by bytes, so the range can end in the middle of a
multi-byte character.
Reproduced with
vpv0.3.0 (vite-task revd05b1dc), macOS 26.6.2 arm64.Filing here rather than on the vite-plus tracker the panic handler points at,
since the code lives in this repo — happy to move it if you prefer.
Panic
Reproduction
A
package.jsonis the whole repro — no dependencies, no install:{ "name": "pm-probe", "version": "1.0.0", "scripts": { "whoami": "node -e \"console.log('agent =', process.env.npm_config_user_agent || '(未设置)')\"" } }The command string is 78 characters but 84 bytes, and its last multi-byte
character
置occupies bytes 77..80 — so the character-based end index 78 landsinside it. Position matters, not merely the presence of non-ASCII: the same
script with the Chinese text earlier in the line happens to survive, because the
byte index it produces is still a valid boundary (it just slices the wrong text).
Root cause
crates/vt_shell/src/lib.rs:95:brush_parser::tokenizer::SourcePosition::indexis documented aswhile every consumer of that range slices a
&str, i.e. by byte:crates/vt_plan/src/plan.rs:185— the panic sitecrates/vt_plan/src/plan.rs:266crates/vt_plan/src/plan.rs:281(The same range is also stored through
Context::push_stack_frame, thoughnothing slices with it today.)
All three sites sit under the
// Build execution displaycomment, so whataborts the run is display bookkeeping, not the work itself.