Skip to content
44 changes: 44 additions & 0 deletions src/bun_core/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3325,6 +3325,50 @@ 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 mut start = 0;
for (i, c) in s.char_indices() {
Comment thread
robobun marked this conversation as resolved.
Outdated
if !matches!(c, '\0'..='\x1f' | '\x7f' | '\u{80}'..='\u{9f}') {
continue;
}
self.0.write_str(&s[start..i])?;
match c {
'\n' => self.0.write_str("\\n")?,
'\r' => self.0.write_str("\\r")?,
'\t' => self.0.write_str("\\t")?,
c if c.is_ascii() => write!(self.0, "\\x{:02x}", c as u32)?,
c => write!(self.0, "\\u{:04x}", c as u32)?,
}
start = i + c.len_utf8();
}
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
52 changes: 30 additions & 22 deletions src/install/PackageManager/PackageManagerEnqueue.rs
Original file line number Diff line number Diff line change
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
36 changes: 21 additions & 15 deletions src/install/PackageManager/runTasks.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ use crate::isolated_install::store::{EntryColumns as _, NodeColumns as _};
use crate::lifecycle_script_runner::InstallCtx;
use crate::network_task::{Authorization, ForTarballError};
use crate::package_manifest_map::Value as ManifestEntry;
use bun_core::fmt::PathSep;
use bun_core::fmt::{EscapeControlChars, PathSep, escape_control_chars};
use bun_install::lockfile::Package;
use bun_install::package_manager_task as Task;
// Import the *module* under the `Options` name so `Options::LogLevel` resolves as a path
Expand Down Expand Up @@ -676,10 +676,12 @@ pub fn run_tasks<C: RunTasksCallbacks>(
bun_ast::Loc::EMPTY,
"<r><yellow>warn:<r> {} downloading tarball <b>{}@{}<r>. Retrying {}/{}...",
bstr::BStr::new(err.name().as_bytes()),
bstr::BStr::new(extract.name.slice()),
extract
.resolution
.fmt(&manager.lockfile.buffers.string_bytes, PathSep::Auto,),
escape_control_chars(extract.name.slice()),
EscapeControlChars(
extract
.resolution
.fmt(&manager.lockfile.buffers.string_bytes, PathSep::Auto),
),
task.retried,
manager.options.max_retry_count,
);
Expand Down Expand Up @@ -746,10 +748,12 @@ pub fn run_tasks<C: RunTasksCallbacks>(
bun_ast::Loc::EMPTY,
"{} downloading tarball <b>{}@{}<r>",
err.name(),
bstr::BStr::new(extract.name.slice()),
extract
.resolution
.fmt(&manager.lockfile.buffers.string_bytes, PathSep::Auto,),
escape_control_chars(extract.name.slice()),
EscapeControlChars(
extract
.resolution
.fmt(&manager.lockfile.buffers.string_bytes, PathSep::Auto),
),
Comment thread
robobun marked this conversation as resolved.
);
} else {
bun_ast::add_warning_pretty!(
Expand All @@ -758,10 +762,12 @@ pub fn run_tasks<C: RunTasksCallbacks>(
bun_ast::Loc::EMPTY,
"{} downloading tarball <b>{}@{}<r>",
err.name(),
bstr::BStr::new(extract.name.slice()),
extract
.resolution
.fmt(&manager.lockfile.buffers.string_bytes, PathSep::Auto,),
escape_control_chars(extract.name.slice()),
EscapeControlChars(
extract
.resolution
.fmt(&manager.lockfile.buffers.string_bytes, PathSep::Auto),
),
);
}
if manager.subcommand != Subcommand::Remove {
Expand Down Expand Up @@ -834,7 +840,7 @@ pub fn run_tasks<C: RunTasksCallbacks>(
None,
bun_ast::Loc::EMPTY,
"<r><red><b>GET<r><red> {}<d> - {}<r>",
bstr::BStr::new(metadata.url.slice()),
escape_control_chars(metadata.url.slice()),
response.status_code,
);
} else {
Expand All @@ -843,7 +849,7 @@ pub fn run_tasks<C: RunTasksCallbacks>(
None,
bun_ast::Loc::EMPTY,
"<r><yellow><b>GET<r><yellow> {}<d> - {}<r>",
bstr::BStr::new(metadata.url.slice()),
escape_control_chars(metadata.url.slice()),
response.status_code,
);
}
Expand Down
7 changes: 5 additions & 2 deletions src/install/dependency.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1455,7 +1455,7 @@ pub(crate) fn parse_with_tag(
bun_ast::Loc::EMPTY,
format_args!(
"invalid or unsupported dependency \"{}\"",
bstr::BStr::new(dependency)
bun_core::fmt::escape_control_chars(dependency)
),
);
}
Expand Down Expand Up @@ -1576,7 +1576,10 @@ pub(crate) fn parse_with_tag(
log.add_error_fmt(
None,
bun_ast::Loc::EMPTY,
format_args!("Unsupported protocol {}", bstr::BStr::new(dependency)),
format_args!(
"Unsupported protocol {}",
bun_core::fmt::escape_control_chars(dependency)
),
);
}
return None;
Expand Down
Loading