Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 17 additions & 1 deletion src/bun_core/fmt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1866,7 +1866,15 @@ impl Display for QuickAndDirtyJavaScriptSyntaxHighlighter<'_> {
text = &text[1..];
}

if !text.is_empty() && (text[0] == b'=' || text[0] == b':') {
// A redacted keyword followed by nothing but whitespace:
// the loop above consumed the rest of the input, so there
// is no value left to redact (`text[0]` below would be out
// of bounds).
if text.is_empty() {
return Ok(());
}

if text[0] == b'=' || text[0] == b':' {
writer.write_char(text[0] as char)?;
text = &text[1..];
while !text.is_empty() && text[0].is_ascii_whitespace() {
Expand Down Expand Up @@ -2021,6 +2029,14 @@ impl Display for QuickAndDirtyJavaScriptSyntaxHighlighter<'_> {
continue;
} else if self.opts.redact_sensitive_information {
'try_redact: {
// `i == 0` happens when a `${...}` interpolation ended
// exactly at the end of the input: the scan loop above
// resets `i` to 0 and exits with `text` empty, so there
// is no quoted content to inspect (`text[1..0]` would
// be out of range).
if i == 0 {
break 'try_redact;
}
Comment thread
robobun marked this conversation as resolved.
let mut inner = &text[1..i];
if !inner.is_empty() && inner[inner.len() - 1] == char_ {
inner = &inner[..inner.len() - 1];
Expand Down
14 changes: 13 additions & 1 deletion src/bun_core/fmt.zig
Original file line number Diff line number Diff line change
Expand Up @@ -944,7 +944,13 @@ pub const QuickAndDirtyJavaScriptSyntaxHighlighter = struct {
text = text[1..];
}

if (text.len > 0 and (text[0] == '=' or text[0] == ':')) {
// A redacted keyword followed by nothing but whitespace:
// the loop above consumed the rest of the input, so there
// is no value left to redact (`text[0]` below would be out
// of bounds).
if (text.len == 0) return;

if (text[0] == '=' or text[0] == ':') {
try writer.writeByte(text[0]);
text = text[1..];
while (text.len > 0 and std.ascii.isWhitespace(text[0])) {
Expand Down Expand Up @@ -1066,6 +1072,12 @@ pub const QuickAndDirtyJavaScriptSyntaxHighlighter = struct {
continue;
} else if (this.opts.redact_sensitive_information) {
try_redact: {
// `i == 0` happens when a `${...}` interpolation ended
// exactly at the end of the input: the scan loop above
// resets `i` to 0 and exits with `text` empty, so there
// is no quoted content to inspect (`text[1..0]` would
// be out of range).
if (i == 0) break :try_redact;
var inner = text[1..i];
if (inner.len > 0 and inner[inner.len - 1] == char) {
inner = inner[0 .. inner.len - 1];
Expand Down
1 change: 1 addition & 0 deletions src/js/internal-for-testing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
const fmtBinding = $bindgenFn("fmt_jsc.bind.ts", "fmtString");

export const highlightJavaScript = (code: string) => fmtBinding(code, "highlight-javascript");
export const highlightJavaScriptRedacted = (code: string) => fmtBinding(code, "highlight-javascript-redacted");
export const escapePowershell = (code: string) => fmtBinding(code, "escape-powershell");

export const canonicalizeIP = $newCppFunction("NodeTLS.cpp", "Bun__canonicalizeIP", 1);
Expand Down
2 changes: 1 addition & 1 deletion src/jsc/fmt_jsc.bind.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { fn, t } from "bindgen";

const implNamespace = "js_bindings";

export const Formatter = t.stringEnum("highlight-javascript", "escape-powershell");
export const Formatter = t.stringEnum("highlight-javascript", "highlight-javascript-redacted", "escape-powershell");

export const fmtString = fn({
implNamespace,
Expand Down
12 changes: 12 additions & 0 deletions src/jsc/fmt_jsc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub mod js_bindings {
pub enum Formatter {
EscapePowershell = 0,
HighlightJavascript = 1,
HighlightJavascriptRedacted = 2,
}

/// Internal function for testing in highlighter.test.ts
Expand All @@ -44,6 +45,17 @@ pub mod js_bindings {
);
write!(writer, "{}", formatter).map_err(|_| global.throw_out_of_memory())?;
}
Formatter::HighlightJavascriptRedacted => {
let formatter = fmt::fmt_javascript(
code,
fmt::HighlighterOptions {
enable_colors: true,
check_for_unhighlighted_write: false,
redact_sensitive_information: true,
},
);
write!(writer, "{}", formatter).map_err(|_| global.throw_out_of_memory())?;
}
Formatter::EscapePowershell => {
write!(writer, "{}", fmt::escape_powershell(code))
.map_err(|_| global.throw_out_of_memory())?;
Expand Down
58 changes: 57 additions & 1 deletion test/js/bun/util/highlighter.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { highlightJavaScript as highlighter } from "bun:internal-for-testing";
import * as internalForTesting from "bun:internal-for-testing";
import { expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";

const { highlightJavaScript: highlighter, highlightJavaScriptRedacted: highlighterRedacted } = internalForTesting;

test("highlighter", () => {
expect(highlighter("`can do ${123} ${'123'} ${`123`}`").length).toBeLessThan(150);
Expand All @@ -17,3 +20,56 @@ test.each([
])("highlighter does not read past end of input for %p", input => {
expect(typeof highlighter(input)).toBe("string");
});

// A `${...}` interpolation ending exactly at the end of the input exits the
// string scan with `i == 0` and `text` fully consumed; the redacting
// highlighter then sliced `text[1..0]` (range start index 1 out of range for
// slice of length 0).
test.each([
"`${}", // empty interpolation, nothing after
"`${0}", // interpolation with content, nothing after
"`a${bc}", // text before the interpolation
"`${x}${y}", // two interpolations back to back
])("redacting highlighter handles `${}` at end of input for %p", input => {
expect(typeof highlighterRedacted(input)).toBe("string");
});

// A redacted keyword followed by nothing but whitespace used to drain `text`
// in the whitespace-skip loop and then index `text[0]` on an empty slice
// (index out of bounds: the len is 0 but the index is 0).
test.each([
"token ", // redacted keyword, trailing space
"email\n", // redacted keyword, trailing newline
"_auth\t", // redacted keyword, trailing tab
"_password ", // redacted keyword, several trailing spaces
"x token = ", // value also drained after the separator
])("redacting highlighter handles redacted keyword at end of input for %p", input => {
expect(typeof highlighterRedacted(input)).toBe("string");
});

test("redacting highlighter still redacts values", () => {
const out = highlighterRedacted('_authToken = "npm_123456"');
expect(out).not.toContain("npm_123456");
expect(out).toContain("*");
});

// End-to-end: an error in bunfig.toml whose source line ends with an
// unterminated template interpolation is printed through the redacting syntax
// highlighter. This used to panic while printing the error message.
test("bunfig error on a line ending in `${}` does not crash", async () => {
using dir = tempDir("bunfig-highlighter", {
"bunfig.toml": "logLevel = 3 # `${}\n",
"index.js": `console.log("hi");`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "run", "index.js"],
env: { ...bunEnv, NO_COLOR: undefined, FORCE_COLOR: "1" },
cwd: String(dir),
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("expected string");
expect(stdout).toContain("hi");
expect(exitCode).toBe(0);
});
Loading