Skip to content

vt_shell: command spans are character indices but sliced as bytes (plan.rs:185) #719

Description

@Akokk0

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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions