Skip to content
57 changes: 57 additions & 0 deletions src/bun_core/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3325,6 +3325,63 @@ fn escape_powershell_impl(str: &[u8], writer: &mut impl fmt::Write) -> fmt::Resu
write_bytes(writer, remain)
}

// ───────────────────────────────────────────────────────────────────────────
// escapeControlChars
// ───────────────────────────────────────────────────────────────────────────
Comment thread
robobun marked this conversation as resolved.

/// `Display` adapter that spells out C0 controls, DEL and C1 controls
/// (`\n`, `\x1b`, `\x7f`, `\u009b`, ...) so text authored by a dependency
/// cannot erase, repaint or forge lines of terminal output when printed.
Comment thread
robobun marked this conversation as resolved.
pub struct EscapeControlChars<T>(pub T);

/// [`EscapeControlChars`] over raw bytes; invalid UTF-8 renders as U+FFFD.
pub fn escape_control_chars(text: &[u8]) -> EscapeControlChars<&bstr::BStr> {
EscapeControlChars(bstr::BStr::new(text))
}

impl<T: Display> Display for EscapeControlChars<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut writer = EscapeControlCharsWriter(f);
write!(writer, "{}", self.0)
}
}

struct EscapeControlCharsWriter<'a, 'f>(&'a mut Formatter<'f>);

impl fmt::Write for EscapeControlCharsWriter<'_, '_> {
fn write_str(&mut self, s: &str) -> fmt::Result {
let bytes = s.as_bytes();
let mut start = 0;
let mut cursor = 0;
// `\` doubles as the quote char so the scan stops at nothing else extra.
while let Some(offset) =
strings::index_of_needs_escape_for_java_script_string(&bytes[cursor..], b'\\')
{
let i = cursor + offset as usize;
let (code_point, len) = match bytes[i] {
byte @ (0x00..=0x1F | 0x7F) => (byte as u32, 1),
0xC2 if matches!(bytes.get(i + 1), Some(0x80..=0x9F)) => (bytes[i + 1] as u32, 2),
byte => {
let char_len = strings::wtf8_byte_sequence_length(byte) as usize;
cursor = (i + char_len).min(bytes.len());
continue;
}
};
self.0.write_str(&s[start..i])?;
match code_point {
0x0A => self.0.write_str("\\n")?,
0x0D => self.0.write_str("\\r")?,
0x09 => self.0.write_str("\\t")?,
0x00..=0x7F => write!(self.0, "\\x{:02x}", code_point)?,
_ => write!(self.0, "\\u{:04x}", code_point)?,
}
start = i + len;
cursor = start;
}
self.0.write_str(&s[start..])
}
}

// js_bindings (fmtString for highlighter.test.ts) lives in src/jsc/fmt_jsc.rs
// alongside fmt_jsc.bind.ts; bun_core/ stays JSC-free.

Expand Down
4 changes: 3 additions & 1 deletion src/install/PackageInstaller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2057,7 +2057,9 @@ impl<'a> PackageInstaller<'a> {
"Blocked {} scripts for: {}@{}\n",
count,
bstr::BStr::new(alias.slice(string_buf!())),
resolution.fmt(string_buf!(), PathSep::Posix),
bun_core::fmt::EscapeControlChars(
resolution.fmt(string_buf!(), PathSep::Posix)
),
);
}
let entry = self
Expand Down
56 changes: 32 additions & 24 deletions src/install/PackageManager/PackageManagerEnqueue.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,13 @@ pub fn enqueue_dependency_list(
// `format_args!` borrows temporaries — bind the
// formatter first so it outlives the macro expansion.
let realname = dependency.realname();
let path_fmt = bun_fmt::fmt_path_u8(
let path_fmt = bun_fmt::EscapeControlChars(bun_fmt::fmt_path_u8(
this.lockfile.str(&realname),
bun_fmt::PathFormatOptions {
path_sep,
escape_backslashes: false,
},
);
));
let log = this.log_mut();
if dependency.behavior.is_optional() || dependency.behavior.is_peer() {
log.add_warning_with_note(
Expand Down Expand Up @@ -806,8 +806,8 @@ pub fn enqueue_dependency_with_main_and_success_fn(
bun_ast::Loc::EMPTY,
format_args!(
"Package \"{}\" with tag \"{}\" not found, but package exists",
bstr::BStr::new(this.lockfile.str(&name)),
bstr::BStr::new(
bun_fmt::escape_control_chars(this.lockfile.str(&name)),
bun_fmt::escape_control_chars(
this.lockfile.str(&version.dist_tag().tag)
),
),
Expand All @@ -825,8 +825,10 @@ pub fn enqueue_dependency_with_main_and_success_fn(
None,
bun_ast::Loc::EMPTY,
"No version matching \"{}\" found for specifier \"{}\"<r> <d>(but package exists)<r>",
bstr::BStr::new(this.lockfile.str(&version.literal)),
bstr::BStr::new(this.lockfile.str(&name)),
bun_fmt::escape_control_chars(
this.lockfile.str(&version.literal)
),
bun_fmt::escape_control_chars(this.lockfile.str(&name)),
);
}
}
Expand All @@ -844,8 +846,10 @@ pub fn enqueue_dependency_with_main_and_success_fn(
None,
bun_ast::Loc::EMPTY,
"Package \"{}\" with tag \"{}\" not found<r> <d>(all versions blocked by minimum-release-age: {} seconds)<r>",
bstr::BStr::new(this.lockfile.str(&name)),
bstr::BStr::new(
bun_fmt::escape_control_chars(
this.lockfile.str(&name)
),
bun_fmt::escape_control_chars(
this.lockfile.str(&version.dist_tag().tag)
),
age_gate_ms / MS_PER_S,
Expand All @@ -856,8 +860,10 @@ pub fn enqueue_dependency_with_main_and_success_fn(
None,
bun_ast::Loc::EMPTY,
"No version matching \"{}\" found for specifier \"{}\"<r> <d>(blocked by minimum-release-age: {} seconds)<r>",
bstr::BStr::new(this.lockfile.str(&name)),
bstr::BStr::new(
bun_fmt::escape_control_chars(
this.lockfile.str(&name)
),
bun_fmt::escape_control_chars(
this.lockfile.str(&version.literal)
),
Comment thread
robobun marked this conversation as resolved.
age_gate_ms / MS_PER_S,
Expand All @@ -877,8 +883,8 @@ pub fn enqueue_dependency_with_main_and_success_fn(
bun_ast::Loc::EMPTY,
format_args!(
"Could not find package.json for \"file:{}\" dependency \"{}\"",
bstr::BStr::new(this.lockfile.str(version.folder())),
bstr::BStr::new(this.lockfile.str(&name)),
bun_fmt::escape_control_chars(this.lockfile.str(version.folder())),
bun_fmt::escape_control_chars(this.lockfile.str(&name)),
),
);
} else {
Expand All @@ -887,7 +893,9 @@ pub fn enqueue_dependency_with_main_and_success_fn(
bun_ast::Loc::EMPTY,
format_args!(
"Could not find package.json for dependency \"{}\"",
bstr::BStr::new(this.lockfile.str(&name)),
bun_fmt::escape_control_chars(
this.lockfile.str(&name)
),
),
);
}
Comment thread
claude[bot] marked this conversation as resolved.
Expand All @@ -912,12 +920,12 @@ pub fn enqueue_dependency_with_main_and_success_fn(
bun_core::pretty_errorln!(
" -> \"{}\": \"{}\" -> {}@{}",
bstr::BStr::new(this.lockfile.str(&result.package.name)),
bstr::BStr::new(label),
bun_fmt::escape_control_chars(label),
bstr::BStr::new(this.lockfile.str(&result.package.name)),
result.package.resolution.fmt(
bun_fmt::EscapeControlChars(result.package.resolution.fmt(
this.lockfile.buffers.string_bytes.as_slice(),
bun_fmt::PathSep::Auto
),
)),
);
}
// Resolve dependencies first
Expand Down Expand Up @@ -1431,12 +1439,12 @@ pub fn enqueue_dependency_with_main_and_success_fn(
bun_core::pretty_errorln!(
" -> \"{}\": \"{}\" -> {}@{}",
bstr::BStr::new(this.lockfile.str(&result.package.name)),
bstr::BStr::new(label),
bun_fmt::escape_control_chars(label),
bstr::BStr::new(this.lockfile.str(&result.package.name)),
result.package.resolution.fmt(
bun_fmt::EscapeControlChars(result.package.resolution.fmt(
this.lockfile.buffers.string_bytes.as_slice(),
bun_fmt::PathSep::Auto
),
)),
);
}
// We shouldn't see any dependencies
Expand Down Expand Up @@ -2282,10 +2290,10 @@ fn get_or_put_resolved_package(
existing_package
.name
.fmt(this.lockfile.buffers.string_bytes.as_slice()),
existing_package.resolution.fmt(
bun_fmt::EscapeControlChars(existing_package.resolution.fmt(
this.lockfile.buffers.string_bytes.as_slice(),
bun_fmt::PathSep::Auto
),
)),
),
);
success_fn(this, dependency_id, existing_id);
Expand Down Expand Up @@ -2333,10 +2341,10 @@ fn get_or_put_resolved_package(
existing_package
.name
.fmt(this.lockfile.buffers.string_bytes.as_slice()),
existing_package.resolution.fmt(
bun_fmt::EscapeControlChars(existing_package.resolution.fmt(
this.lockfile.buffers.string_bytes.as_slice(),
bun_fmt::PathSep::Auto
),
)),
),
);
success_fn(this, dependency_id, list[0]);
Expand Down Expand Up @@ -2490,7 +2498,7 @@ fn get_or_put_resolved_package(
bun_core::pretty_errorln!(
"<d>[minimum-release-age]<r> <b>{}@{}<r> selected <green>{}<r> instead of <yellow>{}<r> due to {}-second filter",
bstr::BStr::new(package_name),
bstr::BStr::new(tag_str),
bun_fmt::escape_control_chars(tag_str),
result.version.fmt(manifest_buf),
newest.fmt(manifest_buf),
min_age_seconds,
Expand Down
12 changes: 9 additions & 3 deletions src/install/PackageManager/PackageManagerResolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -352,14 +352,20 @@ impl PackageManager {
{
Output::err_generic(
"<b>{}<r><d> failed to resolve<r>",
(failed_dep.version.literal.fmt(string_buf),),
(bun_core::fmt::escape_control_chars(
failed_dep.version.literal.slice(string_buf),
),),
);
} else {
Output::err_generic(
"<b>{}<r><d>@<b>{}<r><d> failed to resolve<r>",
(
bstr::BStr::new(failed_dep.name.slice(string_buf)),
failed_dep.version.literal.fmt(string_buf),
bun_core::fmt::escape_control_chars(
failed_dep.name.slice(string_buf),
),
bun_core::fmt::escape_control_chars(
failed_dep.version.literal.slice(string_buf),
),
),
);
}
Expand Down
4 changes: 2 additions & 2 deletions src/install/PackageManager/install_with_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1353,7 +1353,7 @@ fn add_dependency_error(manager: &mut PackageManager, dependency: &Dependency, e
// taking `&mut` on `manager.log`.
let realname = dependency.realname();
let path = manager.lockfile.str(&realname).to_vec();
let path_fmt = bun_core::fmt::fmt_path(
let path_fmt = bun_core::fmt::EscapeControlChars(bun_core::fmt::fmt_path(
&path,
bun_core::fmt::PathFormatOptions {
path_sep: match dependency.version.tag {
Expand All @@ -1362,7 +1362,7 @@ fn add_dependency_error(manager: &mut PackageManager, dependency: &Dependency, e
},
..Default::default()
},
);
));

let log = manager.log_mut();
if dependency.behavior.is_optional() || dependency.behavior.is_peer() {
Expand Down
2 changes: 1 addition & 1 deletion src/install/PackageManager/patchPackage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1396,7 +1396,7 @@ fn pkg_info_for_name_and_version(
bun_core::pretty_error!(
" {}@<blue>{}<r>\n",
bstr::BStr::new(pkg.name.slice(strbuf)),
pkg.resolution.fmt(strbuf, PathSep::Posix)
bun_fmt::EscapeControlChars(pkg.resolution.fmt(strbuf, PathSep::Posix))
);

if i + 1 < pairs.len() {
Expand Down
13 changes: 10 additions & 3 deletions src/install/PackageManager/processDependencyList.rs
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,12 @@ impl PackageManager {
Output::err(
err,
"failed to parse package.json for <b>{}<r>",
format_args!("{}", resolution.fmt_url(string_buf)),
format_args!(
"{}",
bun_core::fmt::EscapeControlChars(
resolution.fmt_url(string_buf)
)
),
);
}
Global::crash();
Expand Down Expand Up @@ -260,7 +265,7 @@ impl PackageManager {
let string_buf = self.lockfile.buffers.string_bytes.as_slice();
bun_core::pretty_errorln!(
"<r><red>error:<r> expected package.json in <b>{}<r> to be a JSON file: {}\n",
resolution.fmt_url(string_buf),
bun_core::fmt::EscapeControlChars(resolution.fmt_url(string_buf)),
err.name(),
);
}
Expand Down Expand Up @@ -312,7 +317,9 @@ impl PackageManager {
let string_buf = self.lockfile.buffers.string_bytes.as_slice();
bun_core::pretty_errorln!(
"<r><red>error:<r> expected package.json in <b>{}<r> to be a JSON file: {}\n",
resolution.fmt_url(string_buf),
bun_core::fmt::EscapeControlChars(
resolution.fmt_url(string_buf)
),
err.name(),
);
}
Expand Down
Loading
Loading