Skip to content

Commit 3f0d49d

Browse files
committed
Merge remote-tracking branch 'origin/main' into claude/add-only-failures-flag
2 parents 7940ccf + 680e668 commit 3f0d49d

23 files changed

+665
-75
lines changed

docs/runtime/bunfig.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,46 @@ junit = "test-results.xml"
284284

285285
This generates a JUnit XML report that can be consumed by CI systems and other tools.
286286

287+
### `test.randomize`
288+
289+
Run tests in random order. Default `false`.
290+
291+
```toml
292+
[test]
293+
randomize = true
294+
```
295+
296+
This helps catch bugs related to test interdependencies by running tests in a different order each time. When combined with `seed`, the random order becomes reproducible.
297+
298+
The `--randomize` CLI flag will override this setting when specified.
299+
300+
### `test.seed`
301+
302+
Set the random seed for test randomization. This option requires `randomize` to be `true`.
303+
304+
```toml
305+
[test]
306+
randomize = true
307+
seed = 2444615283
308+
```
309+
310+
Using a seed makes the randomized test order reproducible across runs, which is useful for debugging flaky tests. When you encounter a test failure with randomization enabled, you can use the same seed to reproduce the exact test order.
311+
312+
The `--seed` CLI flag will override this setting when specified.
313+
314+
### `test.rerunEach`
315+
316+
Re-run each test file a specified number of times. Default `0` (run once).
317+
318+
```toml
319+
[test]
320+
rerunEach = 3
321+
```
322+
323+
This is useful for catching flaky tests or non-deterministic behavior. Each test file will be executed the specified number of times.
324+
325+
The `--rerun-each` CLI flag will override this setting when specified.
326+
287327
## Package manager
288328

289329
Package management is a complex issue; to support a range of use cases, the behavior of `bun install` can be configured under the `[install]` section.

src/CLAUDE.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
## Zig
22

3+
Syntax reminders:
4+
5+
- Private fields are fully supported in Zig with the `#` prefix. `struct { #foo: u32 };` makes a struct with a private field named `#foo`.
6+
- Decl literals in Zig are recommended. `const decl: Decl = .{ .binding = 0, .value = 0 };`
7+
8+
Conventions:
9+
310
- Prefer `@import` at the **bottom** of the file.
411
- It's `@import("bun")` not `@import("root").bun`
512
- You must be patient with the build.

src/bun.js/api/BunObject.zig

Lines changed: 4 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1837,31 +1837,10 @@ pub const JSZstd = struct {
18371837
const input = buffer.slice();
18381838
const allocator = bun.default_allocator;
18391839

1840-
// Try to get the decompressed size
1841-
const decompressed_size = bun.zstd.getDecompressedSize(input);
1842-
1843-
if (decompressed_size == std.math.maxInt(c_ulonglong) - 1 or decompressed_size == std.math.maxInt(c_ulonglong) - 2) {
1844-
// If size is unknown, we'll need to decompress in chunks
1845-
return globalThis.ERR(.ZSTD, "Decompressed size is unknown. Either the input is not a valid zstd compressed buffer or the decompressed size is too large. If you run into this error with a valid input, please file an issue at https://github.com/oven-sh/bun/issues", .{}).throw();
1846-
}
1847-
1848-
// Allocate output buffer based on decompressed size
1849-
var output = try allocator.alloc(u8, decompressed_size);
1850-
1851-
// Perform decompression
1852-
const actual_size = switch (bun.zstd.decompress(output, input)) {
1853-
.success => |actual_size| actual_size,
1854-
.err => |err| {
1855-
allocator.free(output);
1856-
return globalThis.ERR(.ZSTD, "{s}", .{err}).throw();
1857-
},
1840+
const output = bun.zstd.decompressAlloc(allocator, input) catch |err| {
1841+
return globalThis.ERR(.ZSTD, "Decompression failed: {s}", .{@errorName(err)}).throw();
18581842
};
18591843

1860-
bun.debugAssert(actual_size <= output.len);
1861-
1862-
// mimalloc doesn't care about the self-reported size of the slice.
1863-
output.len = actual_size;
1864-
18651844
return jsc.JSValue.createBuffer(globalThis, output);
18661845
}
18671846

@@ -1918,34 +1897,10 @@ pub const JSZstd = struct {
19181897
};
19191898
} else {
19201899
// Decompression path
1921-
// Try to get the decompressed size
1922-
const decompressed_size = bun.zstd.getDecompressedSize(input);
1923-
1924-
if (decompressed_size == std.math.maxInt(c_ulonglong) - 1 or decompressed_size == std.math.maxInt(c_ulonglong) - 2) {
1925-
job.error_message = "Decompressed size is unknown. Either the input is not a valid zstd compressed buffer or the decompressed size is too large";
1926-
return;
1927-
}
1928-
1929-
// Allocate output buffer based on decompressed size
1930-
job.output = allocator.alloc(u8, decompressed_size) catch {
1931-
job.error_message = "Out of memory";
1900+
job.output = bun.zstd.decompressAlloc(allocator, input) catch {
1901+
job.error_message = "Decompression failed";
19321902
return;
19331903
};
1934-
1935-
// Perform decompression
1936-
switch (bun.zstd.decompress(job.output, input)) {
1937-
.success => |actual_size| {
1938-
if (actual_size < job.output.len) {
1939-
job.output.len = actual_size;
1940-
}
1941-
},
1942-
.err => |err| {
1943-
allocator.free(job.output);
1944-
job.output = &[_]u8{};
1945-
job.error_message = err;
1946-
return;
1947-
},
1948-
}
19491904
}
19501905
}
19511906

src/bun.js/bindings/NodeVM.cpp

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,8 @@ Structure* NodeVMGlobalObject::createStructure(JSC::VM& vm, JSC::JSValue prototy
736736
return JSC::Structure::create(vm, nullptr, prototype, JSC::TypeInfo(JSC::GlobalObjectType, StructureFlags & ~IsImmutablePrototypeExoticObject), info());
737737
}
738738

739+
void unsafeEvalNoop(JSGlobalObject*, const WTF::String&) {}
740+
739741
const JSC::GlobalObjectMethodTable& NodeVMGlobalObject::globalObjectMethodTable()
740742
{
741743
static const JSC::GlobalObjectMethodTable table {
@@ -753,7 +755,7 @@ const JSC::GlobalObjectMethodTable& NodeVMGlobalObject::globalObjectMethodTable(
753755
&reportUncaughtExceptionAtEventLoop,
754756
&currentScriptExecutionOwner,
755757
&scriptExecutionStatus,
756-
nullptr, // reportViolationForUnsafeEval
758+
&unsafeEvalNoop, // reportViolationForUnsafeEval
757759
nullptr, // defaultLanguage
758760
nullptr, // compileStreaming
759761
nullptr, // instantiateStreaming

src/bun.js/bindings/ZigGlobalObject.cpp

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,6 +1240,9 @@ JSC::ScriptExecutionStatus Zig::GlobalObject::scriptExecutionStatus(JSC::JSGloba
12401240
}
12411241
}
12421242
}
1243+
1244+
void unsafeEvalNoop(JSGlobalObject*, const WTF::String&) {}
1245+
12431246
const JSC::GlobalObjectMethodTable& GlobalObject::globalObjectMethodTable()
12441247
{
12451248
static const JSC::GlobalObjectMethodTable table = {
@@ -1257,7 +1260,7 @@ const JSC::GlobalObjectMethodTable& GlobalObject::globalObjectMethodTable()
12571260
&reportUncaughtExceptionAtEventLoop,
12581261
&currentScriptExecutionOwner,
12591262
&scriptExecutionStatus,
1260-
nullptr, // reportViolationForUnsafeEval
1263+
&unsafeEvalNoop, // reportViolationForUnsafeEval
12611264
nullptr, // defaultLanguage
12621265
&compileStreaming,
12631266
&instantiateStreaming,
@@ -1287,7 +1290,7 @@ const JSC::GlobalObjectMethodTable& EvalGlobalObject::globalObjectMethodTable()
12871290
&reportUncaughtExceptionAtEventLoop,
12881291
&currentScriptExecutionOwner,
12891292
&scriptExecutionStatus,
1290-
nullptr, // reportViolationForUnsafeEval
1293+
&unsafeEvalNoop, // reportViolationForUnsafeEval
12911294
nullptr, // defaultLanguage
12921295
&compileStreaming,
12931296
&instantiateStreaming,

src/bun.js/bindings/highway_strings.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -444,7 +444,7 @@ static size_t IndexOfNeedsEscapeForJavaScriptStringImpl(const uint8_t* HWY_RESTR
444444
// Set up SIMD constants
445445
const auto vec_backslash = hn::Set(d, uint8_t { '\\' });
446446
const auto vec_min_ascii = hn::Set(d, uint8_t { 0x20 });
447-
const auto vec_max_ascii = hn::Set(d, uint8_t { 127 });
447+
const auto vec_max_ascii = hn::Set(d, uint8_t { 0x7E });
448448
const auto vec_quote = hn::Set(d, quote_char);
449449

450450
const auto vec_dollar = hn::Set(d, uint8_t { '$' });
@@ -482,7 +482,7 @@ static size_t IndexOfNeedsEscapeForJavaScriptStringImpl(const uint8_t* HWY_RESTR
482482
// Scalar check for the remainder
483483
for (; i < text_len; ++i) {
484484
const uint8_t char_ = text[i];
485-
if (char_ >= 127 || (char_ < 0x20 && char_ != 0x09) || char_ == '\\' || char_ == quote_char || (is_backtick && char_ == '$')) {
485+
if (char_ >= 127 || char_ < 0x20 || char_ == '\\' || char_ == quote_char || (is_backtick && char_ == '$')) {
486486
return i;
487487
}
488488
}

src/bun.js/node/path.zig

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2767,35 +2767,55 @@ pub fn resolve(globalObject: *jsc.JSGlobalObject, isWindows: bool, args_ptr: [*]
27672767
var stack_fallback = std.heap.stackFallback(stack_fallback_size_large, arena.allocator());
27682768
const allocator = stack_fallback.get();
27692769

2770-
var paths = try allocator.alloc(string, args_len);
2771-
defer allocator.free(paths);
2772-
var path_count: usize = 0;
2770+
var paths_buf = try allocator.alloc(string, args_len);
2771+
defer allocator.free(paths_buf);
2772+
var paths_offset: usize = args_len;
2773+
var resolved_root = false;
27732774

2774-
for (0..args_len, args_ptr) |i, path_ptr| {
2775-
// Supress exeption in zig. It does globalThis.vm().throwError() in JS land.
2776-
try validateString(globalObject, path_ptr, "paths[{d}]", .{i});
2777-
const pathZStr = try path_ptr.getZigString(globalObject);
2778-
if (pathZStr.len > 0) {
2779-
paths[path_count] = pathZStr.toSlice(allocator).slice();
2780-
path_count += 1;
2775+
var i = args_len;
2776+
while (i > 0) {
2777+
i -= 1;
2778+
2779+
if (resolved_root) {
2780+
break;
2781+
}
2782+
2783+
const path = args_ptr[i];
2784+
try validateString(globalObject, path, "paths[{d}]", .{i});
2785+
const path_str = try path.toBunString(globalObject);
2786+
defer path_str.deref();
2787+
2788+
if (path_str.length() == 0) {
2789+
continue;
2790+
}
2791+
2792+
paths_offset -= 1;
2793+
paths_buf[paths_offset] = path_str.toSlice(allocator).slice();
2794+
2795+
if (!isWindows) {
2796+
if (path_str.charAt(0) == CHAR_FORWARD_SLASH) {
2797+
resolved_root = true;
2798+
}
27812799
}
27822800
}
27832801

2802+
const paths = paths_buf[paths_offset..];
2803+
27842804
if (comptime Environment.isPosix) {
27852805
if (!isWindows) {
27862806
// Micro-optimization #1: avoid creating a new string when passing no arguments or only empty strings.
2787-
if (path_count == 0) {
2807+
if (paths.len == 0) {
27882808
return Process__getCachedCwd(globalObject);
27892809
}
27902810

27912811
// Micro-optimization #2: path.resolve(".") and path.resolve("./") === process.cwd()
2792-
else if (path_count == 1 and (strings.eqlComptime(paths[0], ".") or strings.eqlComptime(paths[0], "./"))) {
2812+
else if (paths.len == 1 and (strings.eqlComptime(paths[0], ".") or strings.eqlComptime(paths[0], "./"))) {
27932813
return Process__getCachedCwd(globalObject);
27942814
}
27952815
}
27962816
}
27972817

2798-
return resolveJS_T(u8, globalObject, allocator, isWindows, paths[0..path_count]);
2818+
return resolveJS_T(u8, globalObject, allocator, isWindows, paths);
27992819
}
28002820

28012821
/// Based on Node v21.6.1 path.win32.toNamespacedPath:

src/bunfig.zig

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,33 @@ pub const Bunfig = struct {
334334
this.ctx.test_options.coverage.skip_test_files = expr.data.e_boolean.value;
335335
}
336336

337+
var randomize_from_config: ?bool = null;
338+
339+
if (test_.get("randomize")) |expr| {
340+
try this.expect(expr, .e_boolean);
341+
randomize_from_config = expr.data.e_boolean.value;
342+
this.ctx.test_options.randomize = expr.data.e_boolean.value;
343+
}
344+
345+
if (test_.get("seed")) |expr| {
346+
try this.expect(expr, .e_number);
347+
const seed_value = expr.data.e_number.toU32();
348+
349+
// Validate that randomize is true when seed is specified
350+
// Either randomize must be set to true in this config, or already enabled
351+
const has_randomize_true = (randomize_from_config orelse this.ctx.test_options.randomize);
352+
if (!has_randomize_true) {
353+
try this.addError(expr.loc, "\"seed\" can only be used when \"randomize\" is true");
354+
}
355+
356+
this.ctx.test_options.seed = seed_value;
357+
}
358+
359+
if (test_.get("rerunEach")) |expr| {
360+
try this.expect(expr, .e_number);
361+
this.ctx.test_options.repeat_count = expr.data.e_number.toU32();
362+
}
363+
337364
if (test_.get("concurrentTestGlob")) |expr| {
338365
switch (expr.data) {
339366
.e_string => |str| {

src/deps/zstd.zig

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,47 @@ pub fn decompress(dest: []u8, src: []const u8) Result {
3333
return .{ .success = result };
3434
}
3535

36+
/// Decompress data, automatically allocating the output buffer.
37+
/// Returns owned slice that must be freed by the caller.
38+
/// Handles both frames with known and unknown content sizes.
39+
/// For safety, if the reported decompressed size exceeds 16MB, streaming decompression is used instead.
40+
pub fn decompressAlloc(allocator: std.mem.Allocator, src: []const u8) ![]u8 {
41+
const size = getDecompressedSize(src);
42+
43+
const ZSTD_CONTENTSIZE_UNKNOWN = std.math.maxInt(c_ulonglong); // 0ULL - 1
44+
const ZSTD_CONTENTSIZE_ERROR = std.math.maxInt(c_ulonglong) - 1; // 0ULL - 2
45+
const MAX_PREALLOCATE_SIZE = 16 * 1024 * 1024; // 16MB safety limit
46+
47+
if (size == ZSTD_CONTENTSIZE_ERROR) {
48+
return error.InvalidZstdData;
49+
}
50+
51+
// Use streaming decompression if:
52+
// 1. Content size is unknown, OR
53+
// 2. Reported size exceeds safety limit (to prevent malicious inputs claiming huge sizes)
54+
if (size == ZSTD_CONTENTSIZE_UNKNOWN or size > MAX_PREALLOCATE_SIZE) {
55+
var list = std.ArrayListUnmanaged(u8){};
56+
const reader = try ZstdReaderArrayList.init(src, &list, allocator);
57+
defer reader.deinit();
58+
59+
try reader.readAll(true);
60+
return try list.toOwnedSlice(allocator);
61+
}
62+
63+
// Fast path: size is known and within reasonable limits
64+
const output = try allocator.alloc(u8, size);
65+
errdefer allocator.free(output);
66+
67+
const result = decompress(output, src);
68+
return switch (result) {
69+
.success => |actual_size| output[0..actual_size],
70+
.err => {
71+
allocator.free(output);
72+
return error.DecompressionFailed;
73+
},
74+
};
75+
}
76+
3677
pub fn getDecompressedSize(src: []const u8) usize {
3778
return ZSTD_findDecompressedSize(src.ptr, src.len);
3879
}

src/js/node/zlib.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -642,7 +642,7 @@ function Unzip(opts): void {
642642
}
643643
$toClass(Unzip, "Unzip", Zlib);
644644

645-
function createConvenienceMethod(ctor, sync, methodName) {
645+
function createConvenienceMethod(ctor, sync, methodName, isZstd) {
646646
if (sync) {
647647
const fn = function (buffer, opts) {
648648
return zlibBufferSync(new ctor(opts), buffer);
@@ -655,6 +655,25 @@ function createConvenienceMethod(ctor, sync, methodName) {
655655
callback = opts;
656656
opts = {};
657657
}
658+
// For zstd compression, we need to set pledgedSrcSize to the buffer size
659+
// so that the content size is included in the frame header
660+
if (isZstd) {
661+
// Calculate buffer size
662+
let bufferSize;
663+
if (typeof buffer === "string") {
664+
bufferSize = Buffer.byteLength(buffer);
665+
} else if (isArrayBufferView(buffer)) {
666+
bufferSize = buffer.byteLength;
667+
} else if (isAnyArrayBuffer(buffer)) {
668+
bufferSize = buffer.byteLength;
669+
} else {
670+
bufferSize = 0;
671+
}
672+
// Set pledgedSrcSize if not already set
673+
if (!opts.pledgedSrcSize && bufferSize > 0) {
674+
opts = { ...opts, pledgedSrcSize: bufferSize };
675+
}
676+
}
658677
return zlibBuffer(new ctor(opts), buffer, callback);
659678
};
660679
ObjectDefineProperty(fn, "name", { value: methodName });
@@ -813,7 +832,7 @@ const zlib = {
813832
brotliCompressSync: createConvenienceMethod(BrotliCompress, true, "brotliCompressSync"),
814833
brotliDecompress: createConvenienceMethod(BrotliDecompress, false, "brotliDecompress"),
815834
brotliDecompressSync: createConvenienceMethod(BrotliDecompress, true, "brotliDecompressSync"),
816-
zstdCompress: createConvenienceMethod(ZstdCompress, false, "zstdCompress"),
835+
zstdCompress: createConvenienceMethod(ZstdCompress, false, "zstdCompress", true),
817836
zstdCompressSync: createConvenienceMethod(ZstdCompress, true, "zstdCompressSync"),
818837
zstdDecompress: createConvenienceMethod(ZstdDecompress, false, "zstdDecompress"),
819838
zstdDecompressSync: createConvenienceMethod(ZstdDecompress, true, "zstdDecompressSync"),

0 commit comments

Comments
 (0)