Skip to content

fix(runtime): decode the shebang probe leniently so a multi-byte char at byte 256 can't hide the interpreter - #150

Merged
navbytes merged 1 commit into
mainfrom
fix/shebang-multibyte-boundary
Aug 30, 2026
Merged

fix(runtime): decode the shebang probe leniently so a multi-byte char at byte 256 can't hide the interpreter#150
navbytes merged 1 commit into
mainfrom
fix/shebang-multibyte-boundary

Conversation

@navbytes

Copy link
Copy Markdown
Owner

Summary

A plugin whose header comments contain an em-dash could fail vee lint and
vee render with a spurious non-zero exit, while running the same file directly
(python3 plugin.py) worked and printed valid output. The failure was
positional, not "em-dash present" — plugins with em-dashes elsewhere were fine,
which made it look content-sensitive and led to plugins being rewritten with
ASCII hyphens as a workaround.

The cause is in the shebang probe. PluginExecutor.shebang(of:) reads the first
256 bytes of the file and decoded them with a strict
String(data:encoding:.utf8). An em-dash is three bytes (E2 80 94); when one
straddled the 256-byte boundary, the truncated sequence made the entire decode
return nil. The shebang was then reported absent and launchCommand fell back
to /bin/bash <path> — so a Python plugin was handed to bash, which died with a
syntax error on the first print(...). That produced the non-zero exit in
vee lint, vee render, and the app itself (PluginCoordinator goes through
the same launchCommand).

The fix is to decode leniently with String(decoding:as:UTF8.self), the same
policy PluginSource already documents for reading plugin text: a truncated
tail becomes U+FFFD, which can only land past the first line, and the shebang
parses exactly as written. This was the only truncated read paired with a strict
decode in Sources/.

Type of change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Plugin-format change (new line param / header tag / <vee.*> capability)
  • Showcase example plugin (examples/) or SDK example (plugins/)
  • Documentation only
  • Refactor / internal change (no behavior change)
  • Breaking change (fix or feature that changes existing behavior)

Testing done

  • swift build
  • swift test
  • swift run vee (manual check in the menu bar)
  • xcodegen generate + xcodebuild (if the app target is affected)
  • npm test in plugins/ (if the TypeScript SDK is affected)

Regression test: ShebangLaunchTests.testHonorsShebangWhenMultibyteCharacterStraddlesReadBoundary
builds the straddling file byte-precisely (with a setup assertion that byte 255
is 0xE2, so the test can't silently stop covering the boundary). It fails
before the fix and passes after.

End-to-end, with a Python plugin whose header em-dash starts at byte 255:

# before
$ vee lint emdash.1m.py
Lint findings:
  error: plugin exited with code 2; only the output it managed to print was linted
$ vee render emdash.1m.py
emdash.1m.py: line 3: syntax error near unexpected token `'{"vee":1,...}''

# after
$ vee lint emdash.1m.py
No lint findings.
$ vee render emdash.1m.py      # exit 0

swiftlint lint --strict also reports 0 violations across 286 files.

Checklist

  • Tests added/updated for the change (TDD — a bug fix has a regression test).
  • All tests pass locally.
  • No new third-party dependencies in the app or the SDKs.
  • Code matches the surrounding Swift style; public API has doc comments.
  • If plugin output/format changed, golden fixtures were regenerated and
    committed — n/a, no format change.
  • Documentation updated as needed — n/a, the fixed behavior is what the docs
    already describe.
  • Commits have clear, imperative subjects.

… at byte 256 can't hide the interpreter

The shebang parser reads the first 256 bytes and decoded them with a
strict UTF-8 String(data:encoding:). When a multi-byte character — an
em-dash in a header comment — straddled that boundary, the truncated
sequence made the whole decode return nil, the shebang was reported
absent, and the plugin fell back to /bin/bash. For a Python plugin that
means a bash syntax error: a spurious non-zero exit from vee lint,
vee render, and the app itself, while running the plugin directly worked.

Decode leniently instead (same policy PluginSource documents): the
truncated tail becomes U+FFFD, which can only land past the first line,
and the shebang parses exactly as written.
@navbytes
navbytes merged commit 7a5c3b1 into main Aug 30, 2026
7 checks passed
@navbytes
navbytes deleted the fix/shebang-multibyte-boundary branch August 30, 2026 04:56
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.

1 participant