Conversation
The name of a store entry under node_modules/.bun is built from lockfile strings: the package name and, for a folder, tarball, workspace or git dependency, its path or URL. Two formatters spelled those strings and both went through core::fmt. bun_semver's StorePathFormatter wrote each byte with write_char(byte as char), which re-encoded every byte above 0x7F as a Latin-1 code point, so a UTF-8 path like paquetes/añadir became paquetes/añadir on disk. bun_install's StorePathFormatter returned fmt::Error for a path that is not UTF-8, and its callers dropped the Result. Replace both with one byte-oriented writer, bun_semver::string:: write_store_path, over bun_core::io::Write, and make the whole chain that builds an entry name (Repository, Resolution, store key, entry path) write bytes the same way. Path gets append_with for a component that a byte producer writes, and append_fmt is built on it. The installer appends store paths through it and no longer discards the Result. Fixes the mojibake, and a path with bytes that are not UTF-8 (legal on Linux) now names its store entry with the same bytes.
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (12)
💤 Files with no reviewable changes (1)
Included review availability: Your plan provides up to 5 included reviews per hour; 0 remain after this review. WalkthroughChangesThe change replaces formatter-based store-key and store-path generation with direct writer APIs. Installation, hashing, pruning, path construction, and license lookup call sites now use these APIs. Tests cover UTF-8 and raw non-UTF-8 store names. Store-path writer migration
Suggested reviewers: Merge Risk: ⚪ Minimal · up to This localized change has no actionable merge-blocking risk remaining and is merge-ready after normal checks and review. 🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
Full details: Description checkExplanation The description clearly explains the problem, implementation, behavior, and verification. It does not use the exact template headings, but it provides the information required by both template sections. Comment |
|
Updated 12:55 AM PT - Aug 28th, 2026
❌ @robobun, your commit 12707ae has 2 failures in
🧪 To try this PR locally: bunx bun-pr 40719That installs a local version of the PR into your bun-40719 --bun |
|
CI status for 12707ae (build 107471): 179 of 181 jobs passed. The two red lanes fail on tests this change does not touch, and both also fail on main: debian 13 x64-asan on |
Problem
--linker=isolated, a dependency whose path holds non-ASCII bytes gets a mojibake store directory.file:./paquetes/añadiris installed intonode_modules/.bun/anadir@file+paquetes+añadir(c3 b1becomesc3 83 c2 b1). The cause isbun_semver'sStorePathFormatter(src/semver/lib.rs:706): it spells each byte withf.write_char(n as char), which treats the byte as a Latin-1 code point and re-encodes it.bun_install::StorePathFormatter(src/install/lib.rs:913), returnsfmt::Errorfor a path that is not UTF-8. Its callers inInstaller.rsdropped theResultwithlet _ = buf.append_fmt(..).Fix
bun_semver::string::write_store_pathoverbun_core::io::Write. It writes the bytes unchanged, apart from/,\,:,#and?, which become+as before. The formatters that compose an entry name (Repository,Resolution, the store key, the entry path, the global entry path) are nowwrite_*functions over the same sink, so no&strsits between the lockfile bytes and the directory name.Pathgetsappend_with(|writer| ..): the producer writes bytes into a pooled scratch buffer, and the result is appended with the same trimming and separator rules asappend.append_fmtis now built on it.Installer.rsappends store paths through it and keeps theResult(.assume_ok()on theASSUMEpath types, as the type documents).writeByteloop did. For a valid UTF-8 path the name is the UTF-8 spelling of the path, so macOS and Windows see a valid name. For a path that is not UTF-8 (legal on Linux) the name holds the same bytes as the source directory.test/cli/install/isolated-install.test.ts(newstore entry names with non-ASCII bytesblock: UTF-8 folder, Latin-1 folder on Linux, plus the multi-byte cut test now asserts the exact name). Stock bun fails all three. Also ran the whole file (87 tests),bun-prune,bun-pm-licenses,isolated-relink,bun-install-git-deps, clippy, and acargo checkforx86_64-pc-windows-msvc.Background
node_modules/.bun/<entry>/node_modules/<name>and symlinks dependents to it.<entry>is<name>@<resolution>[+<peer hash>], and the resolution part embeds a folder path, a tarball URL or a git URL for non-npm packages. The same name is computed in several places (install, prune, hashing for the global store,bun pm licenses), so they all have to spell it identically.core::fmtonly transports&str. ADisplayimpl cannot emit a byte that is not part of valid UTF-8, so anyfmt-based chain either re-encodes or errors on such input.bun_core::io::Writeis the crate's byte sink trait (write_all(&[u8])), already used by the lockfile writers for the same reason.bun_paths::Pathis a pooled, fixed-capacity path buffer.appendtrims leading and trailing separators from its input and inserts the platform separator. TheAuto*aliases skip the length check (CheckLength::ASSUME), so theirResultis alwaysOkand.assume_ok()is the documented way to consume it.Notes
fmt::Display, fixed only the UTF-8 case and made non-UTF-8 input afmt::Error.{"dependencies":{"anadir":"file:./paquetes/añadir"}}withbun install --linker=isolatedcreatesnode_modules/.bun/anadir@file+paquetes+añadir. With a Latin-1 byte in the path (caf\xe9, package.json written as raw bytes) stock bun createscaf\xc3\xa9. After this change:caf\xe9.labelandresolvedvalues only ever went through the/and\mapping of the deleted formatter, and both are validated ASCII (is_safe_resolved_tag). The existing exact-name tests for tarball and git entries pass unchanged.bun.lockwrites the resolution of such a dependency with a JSON escape for code point 0 in place of the invalid byte: the JSON string printer decodes an invalid byte as 0 (same as the Zig printer). Pre-existing, not touched here.Installer.rs(the package failure handler) unlinksnode_modules/<entry>to force a reinstall next time, but the entry lives atnode_modules/.bun/<entry>, so the cleanup has never run. This PR rewrites that block for the byte writer and keeps its target path. The path fix (prefix withNODE_MODULES_BUN, delete the tree instead ofunlink) needs a test that injects a failure mid-install and is handled as a separate change.isolated-install.test.ts(87 pass),bun-pm-licenses.test.ts,bun-prune.test.ts,isolated-relink.test.ts,bun-install-git-deps.test.ts(203 pass, 2 skip),test/internal/source-lints(165 pass),cargo clippy -p bun_semver -p bun_paths -p bun_install,cargo check -p bun_install -p bun_paths --target x86_64-pc-windows-msvc.no test proof · iteration 2 · platform-specific test(s) that do not run on this machine, deferring to CI, which covers all platforms: test/cli/install/isolated-install.test.ts