Skip to content

pm pack: print the bytes of a non-UTF-8 tarball path when stdout is not a terminal - #46987

Open
robobun wants to merge 3 commits into
mainfrom
robobun/dcbdfeeb/pm-pack-tarball-path-bytes
Open

robobun wants to merge 3 commits into
mainfrom
robobun/dcbdfeeb/pm-pack-tarball-path-bytes

Conversation

@robobun

@robobun robobun commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

  • Since 1.4.0, bun pm pack --destination and --filename print U+FFFD for each byte of the tarball path that is not UTF-8. bun pm pack --quiet --destination dest-\xFF writes dest-\xFF/p-1.0.0.tgz and prints dest-\xEF\xBF\xBD/p-1.0.0.tgz, which does not exist.
  • Cause: both path callers passed bstr::BStr to print_tarball_path(impl fmt::Display) (src/runtime/cli/pack_command.rs:109, :1851, :2132). 1.3.14 printed the bytes. No user reported it (fuzzing).

Fix

  • The path callers use a new print_tarball_abs_path(&[u8]). Valid UTF-8 paths, and all paths on a terminal, run the same statement as before.
  • Otherwise a name that is not UTF-8 goes out as bytes, with only its ASCII control bytes spelled out. A UTF-8 name still goes through EscapeControlChars.
  • Verified: test/cli/install/bun-pack.test.ts (14 new tests fail on main) and escape-control-characters.test.ts. Self-reviewed: 23 concerns raised, 23 addressed (Notes).

Background

  • bstr::BStr lets {} take &[u8]. Its Display writes U+FFFD for invalid bytes: right for a message, wrong for a path.
  • EscapeControlChars (install: fix regressions and older bugs found after the package manager consolidation #45307) spells out control and bidirectional characters (ESC prints as \x1b), so a name cannot alter terminal output. That PR made bun pm diff write raw bytes only off a terminal. This PR uses that rule.
  • Considered raw bytes on a terminal: WezTerm's parser takes a lone 0x9B for a control. Considered a split per UTF-8 chunk: in Shift-JIS つくる, C2 82 reads as U+0082.

Downsides

  • In a pipe, a name that is not UTF-8 can now carry the bytes of U+009B or U+202E.
  • On a terminal the line still holds U+FFFD.
  • Release .text grows 1,280 bytes.
Notes

Provenance and reach. Automated fuzzing of paths that are not UTF-8 found this. No issue exists, and no user reported it in 1.4.0 to 1.4.4. It needs --destination or --filename, and a byte that is not UTF-8 in the working directory or in the flag value (a Latin-1, Shift-JIS or GBK name). Linux accepts such names. The documented TARBALL=$(bun pm pack --quiet) prints the bare name and was never affected.

History. 1.3.14 printed the bytes of the path, but its quiet line began with a newline (#32751 removed that in 1.4.0). So test -f "$(bun pm pack --quiet --destination dir)" passed on no release for such a path. What 1.4.0 lost is the bytes of the line: in 1.3.14 | tail -n 1 gave the path.

Repro (Linux x64).

mkdir /tmp/pk && cd /tmp/pk && echo '{"name":"p","version":"1.0.0"}' > package.json
t="$(bun pm pack --quiet --destination "$(printf 'dest-\377')")"; test -f "$t" || echo MISSING
# main and 1.4.3-canary: MISSING (the line holds EF BF BD)      this PR: the file exists

The code. print_tarball_path now takes only a TarballNameFormatter, so a BStr no longer compiles there. The two path callers (--dry-run and real) pass abs_tarball_dest.as_bytes() to print_tarball_abs_path. Both entries share the leading newline of the default log level through begin_tarball_path.

The rule.

stdout path the line
any valid UTF-8 as on main: control and bidirectional characters are spelled out
terminal not UTF-8 as on main: U+FFFD for each invalid sequence
pipe or file not UTF-8 name by name (below)

Name by name: a name that is valid UTF-8 goes through EscapeControlChars as on main. In any other name, the ASCII bytes go through EscapeControlChars (so ESC, CR and LF are still spelled out and the line stays one line) and every other byte is printed unchanged. The name of the tarball comes from package.json and is always UTF-8, so it is always text.

Why name by name. The first version of this branch split the path with utf8_chunks() and sent each valid chunk through the escaper. That is wrong for a multi-byte legacy encoding. In Shift-JIS つくる is 82 C2 82 AD 82 E9. The second byte of つ and the first byte of く are C2 82, which is U+0082 in UTF-8, so the line was 82 5C 75 30 30 38 32 AD 82 E9 and named no file. GBK 聯合國 (C2 93 BA CF 87 F8) failed in the same way. A name is what the kernel stores, and one program wrote it in one encoding. So a name is text or it is bytes.

Why a terminal keeps U+FFFD.

  • The first version also wrote the bytes to a terminal. On a pty a lone 0x9B then reached the terminal (0 -> 1). WezTerm's parser table gives each byte 0x80 to 0x9F a C1 action, and 0x9B starts a CSI sequence (vtparse/src/transitions.rs, anywhere_or, at e2a98e1b). This is read from the source, not run in a WezTerm window.
  • Two places in the tree already decide this for a terminal. bun pm diff writes raw bytes only when stdout is not a terminal (src/runtime/cli/pm_diff_command.rs:1722-1730, from install: fix regressions and older bugs found after the package manager consolidation #45307). sanitize_for_terminal blanks every byte above 0x7F of text that is not UTF-8 (src/runtime/bake/dev_server/error_report_request.rs:525-538).
  • If raw bytes on a terminal are wanted, as in 1.3.14, remove the None if Output::is_stdout_tty() arm. Nothing else changes.

Open question, not blocking. When stdout is not a terminal, is the --quiet line a message or the result? Since #45307 a control character in a path that is valid UTF-8 is spelled out in a pipe too, so that line does not name the file. This PR keeps that. The valid UTF-8 half of the new test "control characters are still spelled out" records the behaviour of main, not a new rule. If the answer is "the result", the last arm becomes Output::print_bytes(path).

Still failing, as before.

  • bun add "$tarball" with such a path. For caf\xE9 it exits 1 (invalid dependency name). For dest-\xFF it exits 0 and writes U+FFFD into bun.lock.
  • A prepack or prepare script that runs JavaScript in a working directory that is not UTF-8: error: Module not found, before the line is printed.

Not changed.

Measurements (Linux x64, merge base 449877f against this PR).

  • U+FFFD sequences in the printed path for a path with one invalid byte, into a pipe: 1 -> 0. The printed path exists: no -> yes (od, test -f).
  • stdout, stderr and exit code for names and paths that are valid UTF-8: 0 of 72 runs differ ({no flag, --destination, --filename} x {real, --dry-run} x {--quiet, default} x names with {ASCII, ESC[2K, CR, BEL, U+009B, U+202E}).
  • On a pty the output is identical to the merge base in 6 of 6 runs (lone 0x9B, 0xFF, overlong ESC E0 80 9B, ESC, default level, ASCII). Lone 0x80 to 0x9F bytes on the terminal: 0 -> 0. In a pipe: 0 -> 1.
  • Inside the printer (gdb on the debug builds, from entry to return), valid path with --quiet: allocator calls 0 -> 0, entries into the stdout writer 1 -> 1, write_all calls 2 -> 2, UTF-8 passes 2 -> 2 (one moved from Utf8Chunks to simdutf), syscalls 0 -> 0. Default level: 2 -> 2 entries and 3 -> 3 write_all calls. Bare name: all counts equal.
  • The same for a path with 0xFF into a pipe: allocator calls 0 -> 0, writer entries 1 -> 12, write_all calls 4 -> 12, syscalls 0 -> 0.
  • write(1) calls per process (stdout as a datagram socket): 1 -> 1 with --quiet, 3 -> 3 at the default level.
  • Release .text: 82,164,617 -> 82,165,897 bytes (llvm-size -A). .rodata and .data are equal. The printer symbols: 274 -> 1,510 bytes (nm -S).
  • strace, perf, valgrind and bloaty are not installed here.

Tests (14 new, in test/cli/install/bun-pack.test.ts).

  • Linux: a real pack into a directory named with 0xFF, Latin-1 é, a lone 0x9B, a cut-off sequence, EUC-JP 日本, Shift-JIS テ and つくる, and GBK 聯合國. Each runs test -f on the captured line. One more packs from a working directory that holds the byte, and one packs to a --filename that holds it.
  • Linux and macOS, with --dry-run: --filename, --silent and the default level, control characters, and a terminal against a pipe.
  • 14 of 14 fail on a debug build and on a release build of the merge base. 14 of 14 pass with this PR in 6 of 6 runs. The macOS runs were not done locally. CI ran the file on macOS aarch64 (build 133128): 362 pass, 36 skip, 0 fail.
  • The whole file with this PR: 393 pass, 3 skip, 0 fail (379 pass and 14 fail on the merge base). test/cli/install/escape-control-characters.test.ts: 66 pass. The pty test of bun pm diff passes.

Self-review. 23 concerns raised, 23 addressed.

  • The terminal arm and the name-by-name rule above come from it. So do the longer test table, the test -f on each row, the pty test and the macOS runs.
  • It also corrected this text: the history of the capture form, the reach, and the two lists above.
  • Between the two shapes it offered for the bytes arm, this PR takes the rule per name. A rule per path would print U+009B in the package name raw when a directory name is not UTF-8.

Related.


no test proof · iteration 0 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/install/bun-pack.test.ts

…ot a terminal

`bun pm pack --destination` and `--filename` print the path of the
tarball. The printer formatted that path through `bstr::BStr`, so each
byte that is not UTF-8 became U+FFFD. The line then named a file that
does not exist.

The printer now takes the path as bytes. A path that is valid UTF-8
goes through the same statement as before. So does every path when
stdout is a terminal. For any other path, each name that is valid UTF-8
goes through `EscapeControlChars`. In each other name, the ASCII bytes
go through `EscapeControlChars` and the other bytes go to stdout
unchanged.
@coderabbitai

coderabbitai Bot commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 6c2c438 and 66ccf4d.

Files selected for processing: 1
  • src/runtime/cli/pack_command.rs

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

Walkthrough

Tarball path output uses byte-aware formatting for absolute destination paths. Piped output preserves invalid non-ASCII bytes, while terminal output escapes invalid paths. Tests cover byte-containing paths, control characters, and logging modes.

Changes

Tarball path output

Layer / File(s) Summary
Tarball path formatting
src/runtime/cli/pack_command.rs
print_tarball_path now accepts TarballNameFormatter. The new print_tarball_abs_path formats absolute paths based on UTF-8 validity and whether output is a terminal.
Pack output integration
src/runtime/cli/pack_command.rs, test/cli/install/bun-pack.test.ts
Dry-run and completed-pack output use print_tarball_abs_path. Tests cover byte-containing paths, control characters, logging modes, and terminal versus piped output.

Suggested reviewers: jarred-sumner

Priority: Normal

Merge Risk: Minimal · up to 66ccf

The pack output change appears ready to merge after normal checks.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title clearly and specifically describes the main change: preserving non-UTF-8 tarball path bytes when stdout is not a terminal.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Description check Passed The description follows the repository template. It has both required sections, "What does this PR do?" (as "Problem", "Fix", and "Background") and "How did you verify your code works?" (as "Verified"…
Full details: Description check

Explanation

The description follows the repository template. It has both required sections, "What does this PR do?" (as "Problem", "Fix", and "Background") and "How did you verify your code works?" (as "Verified" and "Tests"). It gives a concrete reproduction, a measured verification plan, and the downsides. Its verification covers Linux only. The macOS runs were not done locally, and CI on macOS aarch64 is cited. The reviewer should check the macOS evidence, which the bot flagged as platform-specific test evidence deferred to CI. The description is detailed and specific, so it does not fall into the vague category.

  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@robobun

robobun commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 9:38 AM PT - Oct 26th, 2026

✅ @robobun, your commit 66ccf4da44eed15dc72a322cf29e9bbef4c03cc7 passed in Build #133128! 🎉


🧪   To try this PR locally:

bunx bun-pr 46987

That installs a local version of the PR into your bun-46987 executable, so you can run:

bun-46987 --bun

@robobun

robobun commented Oct 26, 2026 •

Copy link
Copy Markdown
Collaborator Author

Status: ready for review. In CI build 133128 every lane that ran is green: Linux x64 and aarch64 (Debian, Ubuntu, Alpine), the ASAN lane, Windows x64 and aarch64, and macOS aarch64. The two darwin x64 - test-bun jobs expired because no agent took them, and their retries are still scheduled. The six tests that the build lists as failed passed on retry, and none of them is in this diff.

test/cli/install/bun-pack.test.ts on macOS aarch64: 362 pass, 36 skip, 0 fail. The --dry-run tests of this PR run there.

How it was reproduced (Linux x64, on 1.4.3-canary 367d939 and on a debug build of main 449877f):

mkdir /tmp/pk && cd /tmp/pk && echo '{"name":"p","version":"1.0.0"}' > package.json
t="$(bun pm pack --quiet --destination "$(printf 'dest-\377')")"; test -f "$t" || echo MISSING

Both builds print dest-\xEF\xBF\xBD/p-1.0.0.tgz and the check reports MISSING. The 1.3.14 release binary prints the byte \xFF. With this PR the captured path exists.

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nothing blocking. The comments below are optional suggestions. There is no need to push a fix for them before merging.

Beyond the inline findings, I also checked that the mixed pretty! / Output::print_bytes writes in the non-TTY arm cannot reorder — both go through the same Destination::Stdout writer — and that slice::chunk_by is not caught by clippy.toml's disallowed-methods list or test/internal/source-lints/byte-search.test.ts, so the new arm passes the byte-search lint.

Extended reasoning...

The change touches only src/runtime/cli/pack_command.rs (the tarball-path printer and its two callers) and the matching test file; it writes raw non-UTF-8 bytes to stdout when stdout is not a terminal, which is output-escaping territory but not auth, crypto or injection. Two inline findings remain (missing real-pack --filename non-UTF-8 coverage and the unchanged behaviour for valid-UTF-8 names holding control characters), so a human look is warranted.

Comment thread test/cli/install/bun-pack.test.ts Resolved
Comment thread src/runtime/cli/pack_command.rs Resolved
The `--filename` route had such a name only with `--dry-run`. This
test packs for real and runs `test -f` on the captured line.
Comment thread src/runtime/cli/pack_command.rs Resolved
Comment thread src/runtime/cli/pack_command.rs Resolved
Comment thread src/runtime/cli/pack_command.rs Resolved

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I re-reviewed after the latest pushes and found no bugs; the new real-pack --filename test covers the second call site I asked about earlier. Because the PR settles an output-policy question (a non-UTF-8 path prints raw bytes in a pipe, while a valid-UTF-8 path with control characters is still escaped there, and a terminal keeps U+FFFD), a human look at that rule is still worthwhile before merging.

What was reviewed:

  • print_tarball_abs_path: the split on SEP_STR is lossless (empty leading/duplicate fields are re-joined with the separator), chunk_by runs and escape_control_chars on ASCII runs leave every non-ASCII byte unchanged, and pretty!/print_bytes share the same stdout writer so ordering holds.
  • Both callers (dry-run at :1884, real pack at :2162) pass the same abs_tarball_dest; --filename is resolved against original_cwd first so the printed line is absolute.
  • Tests: test -f "$tarball" assertions on the captured line fail on main for every matrix entry; the pty test mirrors the existing bun pm diff terminal test and drains stdout/stderr concurrently.
Extended reasoning...

The change touches one function family in src/runtime/cli/pack_command.rs (about 40 lines) and adds 13 subprocess tests in test/cli/install/bun-pack.test.ts; no CODEOWNERS entry covers either file. The only security-relevant surface is terminal escape handling, and the code keeps the existing escaping whenever stdout is a TTY or the path is valid UTF-8, so no new control-sequence injection route reaches a terminal. The code is small and I traced it as correct, but the PR introduces a behavioral rule (raw bytes only for non-UTF-8 names in a pipe) that the author themselves marks as an open question, which is a product decision rather than a correctness check. My prior inline comment about the untested real --filename route was addressed by commit 6c2c438; the prior pre-existing note was explicitly kept as-is by design.

This branch has not been deployed

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant