Compare commits

...

1 Commits

Author SHA1 Message Date
Claude Bot
5bdd990e18 fix: Bun.color ansi-16 producing malformed escape sequences
Fixes #22161

The issue was in the ansi_16 case where color indices (0-15) were being
directly inserted as raw bytes into the ANSI escape sequence string literal.
This caused indices like 9, 10, and 12 to be interpreted as control characters
(tab, newline, form feed) instead of the decimal strings "9", "10", "12".

The fix replaces the problematic string literal approach with proper formatting
using std.fmt.bufPrint, following the same pattern as the working ansi_256 case.

Before: [38;5;	m (with tab character for index 9)
After:  [38;5;9m (with proper decimal string "9")

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-08-26 22:11:05 +00:00
3 changed files with 65 additions and 7 deletions

View File

@@ -329,13 +329,17 @@ pub fn jsFunctionColor(globalThis: *jsc.JSGlobalObject, callFrame: *jsc.CallFram
.ansi_16 => {
const ansi_16_color = Ansi256.get16(rgba.red, rgba.green, rgba.blue);
// 16-color ansi, foreground text color
break :color bun.String.cloneLatin1(&[_]u8{
// 0x1b is the escape character
// 38 is the foreground color code
// 5 is the 16-color mode
// {d} is the color index
0x1b, '[', '3', '8', ';', '5', ';', ansi_16_color, 'm',
});
var buf: [16]u8 = undefined;
// 0x1b is the escape character
buf[0] = 0x1b;
buf[1] = '[';
buf[2] = '3';
buf[3] = '8';
buf[4] = ';';
buf[5] = '5';
buf[6] = ';';
const extra = std.fmt.bufPrint(buf[7..], "{d}m", .{ansi_16_color}) catch unreachable;
break :color bun.String.cloneLatin1(buf[0 .. 7 + extra.len]);
},
.ansi_16m => {
// true color ansi

View File

@@ -0,0 +1,54 @@
// Regression test for issue #22161: Bun.color ansi-16 output is incorrect
// https://github.com/oven-sh/bun/issues/22161
import { test, expect } from "bun:test";
import { color } from "bun";
test("Bun.color ansi-16 should not contain control characters", () => {
// Test colors that previously produced malformed escape sequences
const testColors = [
{ name: 'Red', value: 0xFF0000, description: 'should not contain tab character' },
{ name: 'Blue', value: 0x0000FF, description: 'should not contain form feed character' },
{ name: 'Green', value: 0x00FF00, description: 'should not contain newline character' },
{ name: 'White', value: 0xFFFFFF, description: 'should produce valid escape sequence' },
];
testColors.forEach(({ name, value, description }) => {
const result = color(value, 'ansi-16');
expect(result).toBeDefined();
expect(typeof result).toBe('string');
// The result should not contain control characters (ASCII 0-31 except escape at start)
// Check that no control characters (except escape char at position 0) are present
const hasInvalidControlChars = result!.split('').some((char, idx) => {
const code = char.charCodeAt(0);
// Allow escape character (27) at start, disallow other control chars (0-31)
return idx > 0 && code >= 0 && code <= 31 && code !== 27;
});
expect(hasInvalidControlChars).toBe(false);
// Should follow the pattern \x1b[38;5;<number>m
expect(result).toMatch(/^\x1b\[38;5;\d+m$/);
// Should specifically not contain problematic characters that were in the bug
expect(result).not.toContain('\t'); // tab (ASCII 9)
expect(result).not.toContain('\n'); // newline (ASCII 10)
expect(result).not.toContain('\f'); // form feed (ASCII 12)
expect(result).not.toContain('\r'); // carriage return (ASCII 13)
});
});
test("Bun.color ansi-16 produces expected format for specific values", () => {
// Test specific color mappings to ensure proper formatting
const cases = [
{ input: 0xFF0000, expectedPattern: /^\x1b\[38;5;9m$/ }, // Red -> bright red (index 9)
{ input: 0x0000FF, expectedPattern: /^\x1b\[38;5;12m$/ }, // Blue -> bright blue (index 12)
{ input: 0x00FF00, expectedPattern: /^\x1b\[38;5;10m$/ }, // Green -> bright green (index 10)
{ input: 0xFFFFFF, expectedPattern: /^\x1b\[38;5;15m$/ }, // White -> bright white (index 15)
];
cases.forEach(({ input, expectedPattern }) => {
const result = color(input, 'ansi-16');
expect(result).toMatch(expectedPattern);
});
});