mirror of
https://github.com/oven-sh/bun
synced 2026-02-28 20:40:59 +01:00
Compare commits
19 Commits
claude/upd
...
claude/fix
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32ee722ad4 | ||
|
|
c57af9df38 | ||
|
|
3debd0a2d2 | ||
|
|
7afead629c | ||
|
|
9a72bbfae2 | ||
|
|
7a801fcf93 | ||
|
|
44541eb574 | ||
|
|
993be3f931 | ||
|
|
a68393926b | ||
|
|
e8a5f23385 | ||
|
|
16b3e7cde7 | ||
|
|
4c32f15339 | ||
|
|
635034ee33 | ||
|
|
3e792d0d2e | ||
|
|
b7d505b6c1 | ||
|
|
50e478dcdc | ||
|
|
e8f73601c0 | ||
|
|
ba6e84fecd | ||
|
|
e29e830a25 |
18
bench/snippets/path-parse.mjs
Normal file
18
bench/snippets/path-parse.mjs
Normal file
@@ -0,0 +1,18 @@
|
||||
import { posix, win32 } from "path";
|
||||
import { bench, run } from "../runner.mjs";
|
||||
|
||||
const paths = ["/home/user/dir/file.txt", "/home/user/dir/", "file.txt", "/root", ""];
|
||||
|
||||
paths.forEach(p => {
|
||||
bench(`posix.parse(${JSON.stringify(p)})`, () => {
|
||||
globalThis.abc = posix.parse(p);
|
||||
});
|
||||
});
|
||||
|
||||
paths.forEach(p => {
|
||||
bench(`win32.parse(${JSON.stringify(p)})`, () => {
|
||||
globalThis.abc = win32.parse(p);
|
||||
});
|
||||
});
|
||||
|
||||
await run();
|
||||
@@ -810,11 +810,19 @@ ssize_t bsd_write2(LIBUS_SOCKET_DESCRIPTOR fd, const char *header, int header_le
|
||||
#else
|
||||
ssize_t bsd_write2(LIBUS_SOCKET_DESCRIPTOR fd, const char *header, int header_length, const char *payload, int payload_length) {
|
||||
ssize_t written = bsd_send(fd, header, header_length);
|
||||
if (written < 0) {
|
||||
return written;
|
||||
}
|
||||
if (written == header_length) {
|
||||
ssize_t second_write = bsd_send(fd, payload, payload_length);
|
||||
if (second_write > 0) {
|
||||
written += second_write;
|
||||
} else if (second_write < 0) {
|
||||
/* First write succeeded but second failed with error.
|
||||
* Return the header bytes written so the caller knows
|
||||
* partial progress was made. */
|
||||
}
|
||||
/* If second_write == 0 (would-block), also just return header_length */
|
||||
}
|
||||
return written;
|
||||
}
|
||||
|
||||
@@ -495,6 +495,12 @@ void us_poll_change(struct us_poll_t *p, struct us_loop_t *loop, int events) {
|
||||
}
|
||||
}
|
||||
|
||||
/* On epoll/kqueue, force is the same as regular change since the kernel
|
||||
* handles level/edge triggering correctly. */
|
||||
void us_poll_change_force(struct us_poll_t *p, struct us_loop_t *loop, int events) {
|
||||
us_poll_change(p, loop, events);
|
||||
}
|
||||
|
||||
void us_poll_stop(struct us_poll_t *p, struct us_loop_t *loop) {
|
||||
int old_events = us_poll_events(p);
|
||||
int new_events = 0;
|
||||
|
||||
@@ -115,6 +115,18 @@ void us_poll_change(struct us_poll_t *p, struct us_loop_t *loop, int events) {
|
||||
}
|
||||
}
|
||||
|
||||
/* Like us_poll_change, but always calls uv_poll_start even if events haven't changed.
|
||||
* This is needed on Windows where WSAPoll may not reliably re-trigger writable events
|
||||
* after a partial write without an explicit poll restart. */
|
||||
void us_poll_change_force(struct us_poll_t *p, struct us_loop_t *loop, int events) {
|
||||
if(!p->uv_p) return;
|
||||
p->poll_type =
|
||||
us_internal_poll_type(p) |
|
||||
((events & LIBUS_SOCKET_READABLE) ? POLL_TYPE_POLLING_IN : 0) |
|
||||
((events & LIBUS_SOCKET_WRITABLE) ? POLL_TYPE_POLLING_OUT : 0);
|
||||
uv_poll_start(p->uv_p, events, poll_cb);
|
||||
}
|
||||
|
||||
void us_poll_stop(struct us_poll_t *p, struct us_loop_t *loop) {
|
||||
if(!p->uv_p) return;
|
||||
uv_poll_stop(p->uv_p);
|
||||
|
||||
@@ -399,6 +399,9 @@ void us_poll_start(us_poll_r p, us_loop_r loop, int events) nonnull_fn_decl;
|
||||
/* Returns 0 if successful */
|
||||
int us_poll_start_rc(us_poll_r p, us_loop_r loop, int events) nonnull_fn_decl;
|
||||
void us_poll_change(us_poll_r p, us_loop_r loop, int events) nonnull_fn_decl;
|
||||
/* Like us_poll_change but unconditionally restarts the poll even if events match.
|
||||
* Needed on Windows where WSAPoll may miss writable events without explicit restart. */
|
||||
void us_poll_change_force(us_poll_r p, us_loop_r loop, int events) nonnull_fn_decl;
|
||||
void us_poll_stop(us_poll_r p, struct us_loop_t *loop) nonnull_fn_decl;
|
||||
|
||||
/* Return what events we are polling for */
|
||||
|
||||
@@ -418,9 +418,11 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in
|
||||
if (!s->flags.last_write_failed || us_socket_is_shut_down(0, s)) {
|
||||
us_poll_change(&s->p, loop, us_poll_events(&s->p) & LIBUS_SOCKET_READABLE);
|
||||
} else {
|
||||
#ifdef LIBUS_USE_KQUEUE
|
||||
/* Kqueue one-shot writable needs to be re-registered */
|
||||
us_poll_change(&s->p, loop, us_poll_events(&s->p) | LIBUS_SOCKET_WRITABLE);
|
||||
#if defined(LIBUS_USE_KQUEUE) || defined(LIBUS_USE_LIBUV)
|
||||
/* Kqueue: one-shot writable needs to be re-registered.
|
||||
* Libuv/Windows: WSAPoll may not reliably deliver writable events
|
||||
* after a partial write without re-registration. Force it. */
|
||||
us_poll_change_force(&s->p, loop, us_poll_events(&s->p) | LIBUS_SOCKET_WRITABLE);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
@@ -509,8 +511,8 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in
|
||||
if(s && s->flags.adopted && s->prev) {
|
||||
s = s->prev;
|
||||
}
|
||||
// loop->num_ready_polls isn't accessible on Windows.
|
||||
#ifndef WIN32
|
||||
// loop->num_ready_polls isn't accessible on Windows.
|
||||
// rare case: we're reading a lot of data, there's more to be read, and either:
|
||||
// - the socket has hung up, so we will never get more data from it (only applies to macOS, as macOS will send the event the same tick but Linux will not.)
|
||||
// - the event loop isn't very busy, so we can read multiple times in a row
|
||||
@@ -529,6 +531,20 @@ void us_internal_dispatch_ready_poll(struct us_poll_t *p, int error, int eof, in
|
||||
}
|
||||
}
|
||||
#undef LOOP_ISNT_VERY_BUSY_THRESHOLD
|
||||
#else
|
||||
// On Windows, we don't have num_ready_polls but we still need to
|
||||
// drain available data to avoid requiring a full event loop iteration
|
||||
// for each recv. This is critical for streaming performance.
|
||||
if (
|
||||
s && length >= (LIBUS_RECV_BUFFER_LENGTH - 24 * 1024) && length <= LIBUS_RECV_BUFFER_LENGTH &&
|
||||
!us_socket_is_closed(0, s)
|
||||
) {
|
||||
repeat_recv_count++;
|
||||
// Limit to 10 iterations to avoid starving other sockets
|
||||
if (repeat_recv_count <= 10) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
} else if (!length) {
|
||||
eof = 1; // lets handle EOF in the same place
|
||||
|
||||
292
src/CLAUDE.md
292
src/CLAUDE.md
@@ -10,3 +10,295 @@ Conventions:
|
||||
- Prefer `@import` at the **bottom** of the file, but the auto formatter will move them so you don't need to worry about it.
|
||||
- **Never** use `@import()` inline inside of functions. **Always** put them at the bottom of the file or containing struct. Imports in Zig are free of side-effects, so there's no such thing as a "dynamic" import.
|
||||
- You must be patient with the build.
|
||||
|
||||
## Prefer Bun APIs over `std`
|
||||
|
||||
**Always use `bun.*` APIs instead of `std.*`.** The `bun` namespace (`@import("bun")`) provides cross-platform wrappers that preserve OS error info and never use `unreachable`. Using `std.fs`, `std.posix`, or `std.os` directly is wrong in this codebase.
|
||||
|
||||
| Instead of | Use |
|
||||
| ------------------------------------------------------------ | ------------------------------------ |
|
||||
| `std.fs.File` | `bun.sys.File` |
|
||||
| `std.fs.cwd()` | `bun.FD.cwd()` |
|
||||
| `std.posix.open/read/write/stat/mkdir/unlink/rename/symlink` | `bun.sys.*` equivalents |
|
||||
| `std.fs.path.join/dirname/basename` | `bun.path.join/dirname/basename` |
|
||||
| `std.mem.eql/indexOf/startsWith` (for strings) | `bun.strings.eql/indexOf/startsWith` |
|
||||
| `std.posix.O` / `std.posix.mode_t` / `std.posix.fd_t` | `bun.O` / `bun.Mode` / `bun.FD` |
|
||||
| `std.process.Child` | `bun.spawnSync` |
|
||||
| `catch bun.outOfMemory()` | `bun.handleOom(...)` |
|
||||
|
||||
## `bun.sys` — System Calls (`src/sys.zig`)
|
||||
|
||||
All return `Maybe(T)` — a tagged union of `.result: T` or `.err: bun.sys.Error`:
|
||||
|
||||
```zig
|
||||
const fd = switch (bun.sys.open(path, bun.O.RDONLY, 0)) {
|
||||
.result => |fd| fd,
|
||||
.err => |err| return .{ .err = err },
|
||||
};
|
||||
// Or: const fd = try bun.sys.open(path, bun.O.RDONLY, 0).unwrap();
|
||||
```
|
||||
|
||||
Key functions (all take `bun.FileDescriptor`, not `std.posix.fd_t`):
|
||||
|
||||
- `open`, `openat`, `openA` (non-sentinel) → `Maybe(bun.FileDescriptor)`
|
||||
- `read`, `readAll`, `pread` → `Maybe(usize)`
|
||||
- `write`, `pwrite`, `writev` → `Maybe(usize)`
|
||||
- `stat`, `fstat`, `lstat` → `Maybe(bun.Stat)`
|
||||
- `mkdir`, `unlink`, `rename`, `symlink`, `chmod`, `fchmod`, `fchown` → `Maybe(void)`
|
||||
- `readlink`, `getFdPath`, `getcwd` → `Maybe` of path slice
|
||||
- `getFileSize`, `dup`, `sendfile`, `mmap`
|
||||
|
||||
Use `bun.O.RDONLY`, `bun.O.WRONLY | bun.O.CREAT | bun.O.TRUNC`, etc. for open flags.
|
||||
|
||||
### `bun.sys.File` (`src/sys/File.zig`)
|
||||
|
||||
Higher-level file handle wrapping `bun.FileDescriptor`:
|
||||
|
||||
```zig
|
||||
// One-shot read: open + read + close
|
||||
const bytes = switch (bun.sys.File.readFrom(bun.FD.cwd(), path, allocator)) {
|
||||
.result => |b| b,
|
||||
.err => |err| return .{ .err = err },
|
||||
};
|
||||
|
||||
// One-shot write: open + write + close
|
||||
switch (bun.sys.File.writeFile(bun.FD.cwd(), path, data)) {
|
||||
.result => {},
|
||||
.err => |err| return .{ .err = err },
|
||||
}
|
||||
```
|
||||
|
||||
Key methods:
|
||||
|
||||
- `File.open/openat/makeOpen` → `Maybe(File)` (`makeOpen` creates parent dirs)
|
||||
- `file.read/readAll/write/writeAll` — single or looped I/O
|
||||
- `file.readToEnd(allocator)` — read entire file into allocated buffer
|
||||
- `File.readFrom(dir_fd, path, allocator)` — open + read + close
|
||||
- `File.writeFile(dir_fd, path, data)` — open + write + close
|
||||
- `file.stat()`, `file.close()`, `file.writer()`, `file.reader()`
|
||||
|
||||
### `bun.FD` (`src/fd.zig`)
|
||||
|
||||
Cross-platform file descriptor. Use `bun.FD.cwd()` for cwd, `bun.invalid_fd` for sentinel, `fd.close()` to close.
|
||||
|
||||
### `bun.sys.Error` (`src/sys/Error.zig`)
|
||||
|
||||
Preserves errno, syscall tag, and file path. Convert to JS: `err.toSystemError().toErrorInstance(globalObject)`.
|
||||
|
||||
## `bun.strings` — String Utilities (`src/string/immutable.zig`)
|
||||
|
||||
SIMD-accelerated string operations. Use instead of `std.mem` for strings.
|
||||
|
||||
```zig
|
||||
// Searching
|
||||
strings.indexOf(haystack, needle) // ?usize
|
||||
strings.contains(haystack, needle) // bool
|
||||
strings.containsChar(haystack, char) // bool
|
||||
strings.indexOfChar(haystack, char) // ?u32
|
||||
strings.indexOfAny(str, comptime chars) // ?OptionalUsize (SIMD-accelerated)
|
||||
|
||||
// Comparison
|
||||
strings.eql(a, b) // bool
|
||||
strings.eqlComptime(str, comptime literal) // bool — optimized
|
||||
strings.eqlCaseInsensitiveASCII(a, b, comptime true) // 3rd arg = check_len
|
||||
|
||||
// Prefix/Suffix
|
||||
strings.startsWith(str, prefix) // bool
|
||||
strings.endsWith(str, suffix) // bool
|
||||
strings.hasPrefixComptime(str, comptime prefix) // bool — optimized
|
||||
strings.hasSuffixComptime(str, comptime suffix) // bool — optimized
|
||||
|
||||
// Trimming
|
||||
strings.trim(str, comptime chars) // strip from both ends
|
||||
strings.trimSpaces(str) // strip whitespace
|
||||
|
||||
// Encoding conversions
|
||||
strings.toUTF8Alloc(allocator, utf16) // ![]u8
|
||||
strings.toUTF16Alloc(allocator, utf8) // !?[]u16
|
||||
strings.toUTF8FromLatin1(allocator, latin1) // !?Managed(u8)
|
||||
strings.firstNonASCII(slice) // ?u32
|
||||
```
|
||||
|
||||
Bun handles UTF-8, Latin-1, and UTF-16/WTF-16 because JSC uses Latin-1 and UTF-16 internally. Latin-1 is NOT UTF-8 — bytes 128-255 are single chars in Latin-1 but invalid UTF-8.
|
||||
|
||||
### `bun.String` (`src/string.zig`)
|
||||
|
||||
Bridges Zig and JavaScriptCore. Prefer over `ZigString` in new code.
|
||||
|
||||
```zig
|
||||
const s = bun.String.cloneUTF8(utf8_slice); // copies into WTFStringImpl
|
||||
const s = bun.String.borrowUTF8(utf8_slice); // no copy, caller keeps alive
|
||||
const utf8 = s.toUTF8(allocator); // ZigString.Slice
|
||||
defer utf8.deinit();
|
||||
const js_value = s.toJS(globalObject);
|
||||
|
||||
// Create a JS string value directly from UTF-8 bytes:
|
||||
const js_str = try bun.String.createUTF8ForJS(globalObject, utf8_slice);
|
||||
```
|
||||
|
||||
## `bun.path` — Path Manipulation (`src/resolver/resolve_path.zig`)
|
||||
|
||||
Use instead of `std.fs.path`. Platform param: `.auto` (current platform), `.posix`, `.windows`, `.loose` (both separators).
|
||||
|
||||
```zig
|
||||
// Join paths — uses threadlocal buffer, result must be copied if it needs to persist
|
||||
bun.path.join(&.{ dir, filename }, .auto)
|
||||
bun.path.joinZ(&.{ dir, filename }, .auto) // null-terminated
|
||||
|
||||
// Join into a caller-provided buffer
|
||||
bun.path.joinStringBuf(&buf, &.{ a, b }, .auto)
|
||||
bun.path.joinStringBufZ(&buf, &.{ a, b }, .auto) // null-terminated
|
||||
|
||||
// Resolve against an absolute base (like Node.js path.resolve)
|
||||
bun.path.joinAbsString(cwd, &.{ relative_path }, .auto)
|
||||
bun.path.joinAbsStringBufZ(cwd, &buf, &.{ relative_path }, .auto)
|
||||
|
||||
// Path components
|
||||
bun.path.dirname(path, .auto)
|
||||
bun.path.basename(path)
|
||||
|
||||
// Relative path between two absolute paths
|
||||
bun.path.relative(from, to)
|
||||
bun.path.relativeAlloc(allocator, from, to)
|
||||
|
||||
// Normalize (resolve `.` and `..`)
|
||||
bun.path.normalizeBuf(path, &buf, .auto)
|
||||
|
||||
// Null-terminate a path into a buffer
|
||||
bun.path.z(path, &buf) // returns [:0]const u8
|
||||
```
|
||||
|
||||
Use `bun.PathBuffer` for path buffers: `var buf: bun.PathBuffer = undefined;`
|
||||
|
||||
For pooled path buffers (avoids 64KB stack allocations on Windows):
|
||||
|
||||
```zig
|
||||
const buf = bun.path_buffer_pool.get();
|
||||
defer bun.path_buffer_pool.put(buf);
|
||||
```
|
||||
|
||||
## URL Parsing
|
||||
|
||||
Prefer `bun.jsc.URL` (WHATWG-compliant, backed by WebKit C++) over `bun.URL.parse` (internal, doesn't properly handle errors or invalid URLs).
|
||||
|
||||
```zig
|
||||
// Parse a URL string (returns null if invalid)
|
||||
const url = bun.jsc.URL.fromUTF8(href_string) orelse return error.InvalidURL;
|
||||
defer url.deinit();
|
||||
|
||||
url.protocol() // bun.String
|
||||
url.pathname() // bun.String
|
||||
url.search() // bun.String
|
||||
url.hash() // bun.String (includes leading '#')
|
||||
url.port() // u32 (maxInt(u32) if not set, otherwise u16 range)
|
||||
|
||||
// NOTE: host/hostname are SWAPPED vs JS:
|
||||
url.host() // hostname WITHOUT port (opposite of JS!)
|
||||
url.hostname() // hostname WITH port (opposite of JS!)
|
||||
|
||||
// Normalize a URL string (percent-encode, punycode, etc.)
|
||||
const normalized = bun.jsc.URL.hrefFromString(bun.String.borrowUTF8(input));
|
||||
if (normalized.tag == .Dead) return error.InvalidURL;
|
||||
defer normalized.deref();
|
||||
|
||||
// Join base + relative URLs
|
||||
const joined = bun.jsc.URL.join(base_str, relative_str);
|
||||
defer joined.deref();
|
||||
|
||||
// Convert between file paths and file:// URLs
|
||||
const file_url = bun.jsc.URL.fileURLFromString(path_str); // path → file://
|
||||
const file_path = bun.jsc.URL.pathFromFileURL(url_str); // file:// → path
|
||||
```
|
||||
|
||||
## MIME Types (`src/http/MimeType.zig`)
|
||||
|
||||
```zig
|
||||
const MimeType = bun.http.MimeType;
|
||||
|
||||
// Look up by file extension (without leading dot)
|
||||
const mime = MimeType.byExtension("html"); // MimeType{ .value = "text/html", .category = .html }
|
||||
const mime = MimeType.byExtensionNoDefault("xyz"); // ?MimeType (null if unknown)
|
||||
|
||||
// Category checks
|
||||
mime.category // .javascript, .css, .html, .json, .image, .text, .wasm, .font, .video, .audio, ...
|
||||
mime.category.isCode()
|
||||
```
|
||||
|
||||
Common constants: `MimeType.javascript`, `MimeType.json`, `MimeType.html`, `MimeType.css`, `MimeType.text`, `MimeType.wasm`, `MimeType.ico`, `MimeType.other`.
|
||||
|
||||
## Memory & Allocators
|
||||
|
||||
**Use `bun.default_allocator` for almost everything.** It's backed by mimalloc.
|
||||
|
||||
`bun.handleOom(expr)` converts `error.OutOfMemory` into a crash without swallowing other errors:
|
||||
|
||||
```zig
|
||||
const buf = bun.handleOom(allocator.alloc(u8, size)); // correct
|
||||
// NOT: allocator.alloc(u8, size) catch bun.outOfMemory() — could swallow non-OOM errors
|
||||
```
|
||||
|
||||
## Environment Variables (`src/env_var.zig`)
|
||||
|
||||
Type-safe, cached environment variable accessors via `bun.env_var`:
|
||||
|
||||
```zig
|
||||
bun.env_var.HOME.get() // ?[]const u8
|
||||
bun.env_var.CI.get() // ?bool
|
||||
bun.env_var.BUN_CONFIG_DNS_TIME_TO_LIVE_SECONDS.get() // u64 (has default: 30)
|
||||
```
|
||||
|
||||
## Logging (`src/output.zig`)
|
||||
|
||||
```zig
|
||||
const log = bun.Output.scoped(.MY_FEATURE, .visible); // .hidden = opt-in via BUN_DEBUG_MY_FEATURE=1
|
||||
log("processing {d} items", .{count});
|
||||
|
||||
// Color output (convenience wrappers auto-detect TTY):
|
||||
bun.Output.pretty("<green>success<r>: {s}\n", .{msg});
|
||||
bun.Output.prettyErrorln("<red>error<r>: {s}", .{msg});
|
||||
```
|
||||
|
||||
## Spawning Subprocesses (`src/bun.js/api/bun/process.zig`)
|
||||
|
||||
Use `bun.spawnSync` instead of `std.process.Child`:
|
||||
|
||||
```zig
|
||||
switch (bun.spawnSync(&.{
|
||||
.argv = argv,
|
||||
.envp = null, // inherit parent env
|
||||
.cwd = cwd,
|
||||
.stdout = .buffer, // capture
|
||||
.stderr = .inherit, // pass through
|
||||
.stdin = .ignore,
|
||||
|
||||
.windows = if (bun.Environment.isWindows) .{
|
||||
.loop = bun.jsc.EventLoopHandle.init(bun.jsc.MiniEventLoop.initGlobal(env, null)),
|
||||
},
|
||||
}) catch return) {
|
||||
.err => |err| { /* bun.sys.Error */ },
|
||||
.result => |result| {
|
||||
defer result.deinit();
|
||||
const stdout = result.stdout.items;
|
||||
if (result.status.isOK()) { ... }
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Options: `argv: []const []const u8`, `envp: ?[*:null]?[*:0]const u8` (null = inherit), `argv0: ?[*:0]const u8`. Stdio: `.inherit`, `.ignore`, `.buffer`.
|
||||
|
||||
## Common Patterns
|
||||
|
||||
```zig
|
||||
// Read a file
|
||||
const contents = switch (bun.sys.File.readFrom(bun.FD.cwd(), path, allocator)) {
|
||||
.result => |bytes| bytes,
|
||||
.err => |err| { globalObject.throwValue(err.toSystemError().toErrorInstance(globalObject)); return .zero; },
|
||||
};
|
||||
|
||||
// Create directories recursively
|
||||
bun.makePath(dir.stdDir(), sub_path) catch |err| { ... };
|
||||
|
||||
// Hashing
|
||||
bun.hash(bytes) // u64 — wyhash
|
||||
bun.hash32(bytes) // u32
|
||||
```
|
||||
|
||||
@@ -935,7 +935,7 @@ pub const StandaloneModuleGraph = struct {
|
||||
|
||||
var remain = bytes;
|
||||
while (remain.len > 0) {
|
||||
switch (Syscall.write(cloned_executable_fd, bytes)) {
|
||||
switch (Syscall.write(cloned_executable_fd, remain)) {
|
||||
.result => |written| remain = remain[written..],
|
||||
.err => |err| {
|
||||
Output.prettyErrorln("<r><red>error<r><d>:<r> failed to write to temporary file\n{f}", .{err});
|
||||
|
||||
@@ -954,6 +954,7 @@ BUN_DEFINE_HOST_FUNCTION(jsFunctionBunPluginClear, (JSC::JSGlobalObject * global
|
||||
global->onResolvePlugins.namespaces.clear();
|
||||
|
||||
delete global->onLoadPlugins.virtualModules;
|
||||
global->onLoadPlugins.virtualModules = nullptr;
|
||||
|
||||
return JSC::JSValue::encode(JSC::jsUndefined());
|
||||
}
|
||||
|
||||
@@ -114,6 +114,25 @@ static JSC::JSObject* createPath(JSGlobalObject* globalThis, bool isWindows)
|
||||
|
||||
} // namespace Zig
|
||||
|
||||
extern "C" JSC::EncodedJSValue PathParsedObject__create(
|
||||
JSC::JSGlobalObject* globalObject,
|
||||
JSC::EncodedJSValue root,
|
||||
JSC::EncodedJSValue dir,
|
||||
JSC::EncodedJSValue base,
|
||||
JSC::EncodedJSValue ext,
|
||||
JSC::EncodedJSValue name)
|
||||
{
|
||||
auto* global = JSC::jsCast<Zig::GlobalObject*>(globalObject);
|
||||
auto& vm = JSC::getVM(globalObject);
|
||||
JSC::JSObject* result = JSC::constructEmptyObject(vm, global->pathParsedObjectStructure());
|
||||
result->putDirectOffset(vm, 0, JSC::JSValue::decode(root));
|
||||
result->putDirectOffset(vm, 1, JSC::JSValue::decode(dir));
|
||||
result->putDirectOffset(vm, 2, JSC::JSValue::decode(base));
|
||||
result->putDirectOffset(vm, 3, JSC::JSValue::decode(ext));
|
||||
result->putDirectOffset(vm, 4, JSC::JSValue::decode(name));
|
||||
return JSC::JSValue::encode(result);
|
||||
}
|
||||
|
||||
namespace Bun {
|
||||
|
||||
JSC::JSValue createNodePathBinding(Zig::GlobalObject* globalObject)
|
||||
|
||||
@@ -2067,6 +2067,30 @@ void GlobalObject::finishCreation(VM& vm)
|
||||
init.set(structure);
|
||||
});
|
||||
|
||||
this->m_pathParsedObjectStructure.initLater(
|
||||
[](const Initializer<Structure>& init) {
|
||||
// { root, dir, base, ext, name } — path.parse() result
|
||||
Structure* structure = init.owner->structureCache().emptyObjectStructureForPrototype(
|
||||
init.owner, init.owner->objectPrototype(), 5);
|
||||
PropertyOffset offset;
|
||||
structure = Structure::addPropertyTransition(init.vm, structure,
|
||||
Identifier::fromString(init.vm, "root"_s), 0, offset);
|
||||
RELEASE_ASSERT(offset == 0);
|
||||
structure = Structure::addPropertyTransition(init.vm, structure,
|
||||
Identifier::fromString(init.vm, "dir"_s), 0, offset);
|
||||
RELEASE_ASSERT(offset == 1);
|
||||
structure = Structure::addPropertyTransition(init.vm, structure,
|
||||
Identifier::fromString(init.vm, "base"_s), 0, offset);
|
||||
RELEASE_ASSERT(offset == 2);
|
||||
structure = Structure::addPropertyTransition(init.vm, structure,
|
||||
Identifier::fromString(init.vm, "ext"_s), 0, offset);
|
||||
RELEASE_ASSERT(offset == 3);
|
||||
structure = Structure::addPropertyTransition(init.vm, structure,
|
||||
init.vm.propertyNames->name, 0, offset);
|
||||
RELEASE_ASSERT(offset == 4);
|
||||
init.set(structure);
|
||||
});
|
||||
|
||||
this->m_pendingVirtualModuleResultStructure.initLater(
|
||||
[](const Initializer<Structure>& init) {
|
||||
init.set(Bun::PendingVirtualModuleResult::createStructure(init.vm, init.owner, init.owner->objectPrototype()));
|
||||
|
||||
@@ -567,6 +567,7 @@ public:
|
||||
V(public, LazyClassStructure, m_JSHTTPParserClassStructure) \
|
||||
\
|
||||
V(private, LazyPropertyOfGlobalObject<Structure>, m_jsonlParseResultStructure) \
|
||||
V(private, LazyPropertyOfGlobalObject<Structure>, m_pathParsedObjectStructure) \
|
||||
V(private, LazyPropertyOfGlobalObject<Structure>, m_pendingVirtualModuleResultStructure) \
|
||||
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_performMicrotaskFunction) \
|
||||
V(private, LazyPropertyOfGlobalObject<JSFunction>, m_nativeMicrotaskTrampoline) \
|
||||
@@ -702,6 +703,7 @@ public:
|
||||
void reload();
|
||||
|
||||
JSC::Structure* jsonlParseResultStructure() { return m_jsonlParseResultStructure.get(this); }
|
||||
JSC::Structure* pathParsedObjectStructure() { return m_pathParsedObjectStructure.get(this); }
|
||||
JSC::Structure* pendingVirtualModuleResultStructure() { return m_pendingVirtualModuleResultStructure.get(this); }
|
||||
|
||||
// We need to know if the napi module registered itself or we registered it.
|
||||
|
||||
@@ -5954,16 +5954,14 @@ ExceptionOr<Ref<SerializedScriptValue>> SerializedScriptValue::create(JSGlobalOb
|
||||
auto* data = array->butterfly()->contiguous().data();
|
||||
if (!containsHole(data, length)) {
|
||||
size_t byteSize = sizeof(JSValue) * length;
|
||||
Vector<uint8_t> buffer(byteSize, 0);
|
||||
memcpy(buffer.mutableSpan().data(), data, byteSize);
|
||||
Vector<uint8_t> buffer(std::span<const uint8_t> { reinterpret_cast<const uint8_t*>(data), byteSize });
|
||||
return SerializedScriptValue::createInt32ArrayFastPath(WTF::move(buffer), length);
|
||||
}
|
||||
} else {
|
||||
auto* data = array->butterfly()->contiguousDouble().data();
|
||||
if (!containsHole(data, length)) {
|
||||
size_t byteSize = sizeof(double) * length;
|
||||
Vector<uint8_t> buffer(byteSize, 0);
|
||||
memcpy(buffer.mutableSpan().data(), data, byteSize);
|
||||
Vector<uint8_t> buffer(std::span<const uint8_t> { reinterpret_cast<const uint8_t*>(data), byteSize });
|
||||
return SerializedScriptValue::createDoubleArrayFastPath(WTF::move(buffer), length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,13 +71,12 @@ fn PathParsed(comptime T: type) type {
|
||||
name: []const T = "",
|
||||
|
||||
pub fn toJSObject(this: @This(), globalObject: *jsc.JSGlobalObject) bun.JSError!jsc.JSValue {
|
||||
var jsObject = jsc.JSValue.createEmptyObject(globalObject, 5);
|
||||
jsObject.put(globalObject, jsc.ZigString.static("root"), try bun.String.createUTF8ForJS(globalObject, this.root));
|
||||
jsObject.put(globalObject, jsc.ZigString.static("dir"), try bun.String.createUTF8ForJS(globalObject, this.dir));
|
||||
jsObject.put(globalObject, jsc.ZigString.static("base"), try bun.String.createUTF8ForJS(globalObject, this.base));
|
||||
jsObject.put(globalObject, jsc.ZigString.static("ext"), try bun.String.createUTF8ForJS(globalObject, this.ext));
|
||||
jsObject.put(globalObject, jsc.ZigString.static("name"), try bun.String.createUTF8ForJS(globalObject, this.name));
|
||||
return jsObject;
|
||||
const root = try bun.String.createUTF8ForJS(globalObject, this.root);
|
||||
const dir = try bun.String.createUTF8ForJS(globalObject, this.dir);
|
||||
const base = try bun.String.createUTF8ForJS(globalObject, this.base);
|
||||
const ext = try bun.String.createUTF8ForJS(globalObject, this.ext);
|
||||
const name_val = try bun.String.createUTF8ForJS(globalObject, this.name);
|
||||
return PathParsedObject__create(globalObject, root, dir, base, ext, name_val);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -2748,6 +2747,14 @@ pub fn resolveJS_T(comptime T: type, globalObject: *jsc.JSGlobalObject, allocato
|
||||
}
|
||||
|
||||
extern "c" fn Process__getCachedCwd(*jsc.JSGlobalObject) jsc.JSValue;
|
||||
extern "c" fn PathParsedObject__create(
|
||||
*jsc.JSGlobalObject,
|
||||
jsc.JSValue,
|
||||
jsc.JSValue,
|
||||
jsc.JSValue,
|
||||
jsc.JSValue,
|
||||
jsc.JSValue,
|
||||
) jsc.JSValue;
|
||||
|
||||
pub fn resolve(globalObject: *jsc.JSGlobalObject, isWindows: bool, args_ptr: [*]jsc.JSValue, args_len: u16) bun.JSError!jsc.JSValue {
|
||||
var arena = bun.ArenaAllocator.init(bun.default_allocator);
|
||||
|
||||
@@ -948,6 +948,7 @@ pub const CommandLineReporter = struct {
|
||||
this.printSummary();
|
||||
Output.prettyError("\nBailed out after {d} failure{s}<r>\n", .{ this.jest.bail, if (this.jest.bail == 1) "" else "s" });
|
||||
Output.flush();
|
||||
this.writeJUnitReportIfNeeded();
|
||||
Global.exit(1);
|
||||
}
|
||||
},
|
||||
@@ -970,6 +971,20 @@ pub const CommandLineReporter = struct {
|
||||
Output.printStartEnd(bun.start_time, std.time.nanoTimestamp());
|
||||
}
|
||||
|
||||
/// Writes the JUnit reporter output file if a JUnit reporter is active and
|
||||
/// an outfile path was configured. This must be called before any early exit
|
||||
/// (e.g. bail) so that the report is not lost.
|
||||
pub fn writeJUnitReportIfNeeded(this: *CommandLineReporter) void {
|
||||
if (this.reporters.junit) |junit| {
|
||||
if (this.jest.test_options.reporter_outfile) |outfile| {
|
||||
if (junit.current_file.len > 0) {
|
||||
junit.endTestSuite() catch {};
|
||||
}
|
||||
junit.writeToFile(outfile) catch {};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn generateCodeCoverage(this: *CommandLineReporter, vm: *jsc.VirtualMachine, opts: *TestCommand.CodeCoverageOptions, comptime reporters: TestCommand.Reporters, comptime enable_ansi_colors: bool) !void {
|
||||
if (comptime !reporters.text and !reporters.lcov) {
|
||||
return;
|
||||
@@ -1772,12 +1787,7 @@ pub const TestCommand = struct {
|
||||
Output.prettyError("\n", .{});
|
||||
Output.flush();
|
||||
|
||||
if (reporter.reporters.junit) |junit| {
|
||||
if (junit.current_file.len > 0) {
|
||||
junit.endTestSuite() catch {};
|
||||
}
|
||||
junit.writeToFile(ctx.test_options.reporter_outfile.?) catch {};
|
||||
}
|
||||
reporter.writeJUnitReportIfNeeded();
|
||||
|
||||
if (vm.hot_reload == .watch) {
|
||||
vm.runWithAPILock(jsc.VirtualMachine, vm, runEventLoopForWatch);
|
||||
@@ -1920,6 +1930,7 @@ pub const TestCommand = struct {
|
||||
if (reporter.jest.bail == reporter.summary().fail) {
|
||||
reporter.printSummary();
|
||||
Output.prettyError("\nBailed out after {d} failure{s}<r>\n", .{ reporter.jest.bail, if (reporter.jest.bail == 1) "" else "s" });
|
||||
reporter.writeJUnitReportIfNeeded();
|
||||
|
||||
vm.exit_handler.exit_code = 1;
|
||||
vm.is_shutting_down = true;
|
||||
|
||||
@@ -27,6 +27,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type {
|
||||
ping_frame_bytes: [128 + 6]u8 = [_]u8{0} ** (128 + 6),
|
||||
ping_len: u8 = 0,
|
||||
ping_received: bool = false,
|
||||
pong_received: bool = false,
|
||||
close_received: bool = false,
|
||||
close_frame_buffering: bool = false,
|
||||
|
||||
@@ -120,6 +121,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type {
|
||||
this.clearReceiveBuffers(true);
|
||||
this.clearSendBuffers(true);
|
||||
this.ping_received = false;
|
||||
this.pong_received = false;
|
||||
this.ping_len = 0;
|
||||
this.close_frame_buffering = false;
|
||||
this.receive_pending_chunk_len = 0;
|
||||
@@ -136,6 +138,10 @@ pub fn NewWebSocketClient(comptime ssl: bool) type {
|
||||
// Set to null FIRST to prevent re-entrancy (shutdown can trigger callbacks)
|
||||
if (this.proxy_tunnel) |tunnel| {
|
||||
this.proxy_tunnel = null;
|
||||
// Detach the websocket from the tunnel before shutdown so the
|
||||
// tunnel's onClose callback doesn't dispatch a spurious 1006
|
||||
// after we've already handled a clean close.
|
||||
tunnel.clearConnectedWebSocket();
|
||||
tunnel.shutdown();
|
||||
tunnel.deref();
|
||||
}
|
||||
@@ -650,14 +656,38 @@ pub fn NewWebSocketClient(comptime ssl: bool) type {
|
||||
if (data.len == 0) break;
|
||||
},
|
||||
.pong => {
|
||||
const pong_len = @min(data.len, @min(receive_body_remain, this.ping_frame_bytes.len));
|
||||
if (!this.pong_received) {
|
||||
if (receive_body_remain > 125) {
|
||||
this.terminate(ErrorCode.invalid_control_frame);
|
||||
terminated = true;
|
||||
break;
|
||||
}
|
||||
this.ping_len = @truncate(receive_body_remain);
|
||||
receive_body_remain = 0;
|
||||
this.pong_received = true;
|
||||
}
|
||||
const pong_len = this.ping_len;
|
||||
|
||||
this.dispatchData(data[0..pong_len], .Pong);
|
||||
if (data.len > 0) {
|
||||
const total_received = @min(pong_len, receive_body_remain + data.len);
|
||||
const slice = this.ping_frame_bytes[6..][receive_body_remain..total_received];
|
||||
@memcpy(slice, data[0..slice.len]);
|
||||
receive_body_remain = total_received;
|
||||
data = data[slice.len..];
|
||||
}
|
||||
const pending_body = pong_len - receive_body_remain;
|
||||
if (pending_body > 0) {
|
||||
// wait for more data - pong payload is fragmented across TCP segments
|
||||
break;
|
||||
}
|
||||
|
||||
const pong_data = this.ping_frame_bytes[6..][0..pong_len];
|
||||
this.dispatchData(pong_data, .Pong);
|
||||
|
||||
data = data[pong_len..];
|
||||
receive_state = .need_header;
|
||||
receive_body_remain = 0;
|
||||
receiving_type = last_receive_data_type;
|
||||
this.pong_received = false;
|
||||
|
||||
if (data.len == 0) break;
|
||||
},
|
||||
@@ -884,7 +914,7 @@ pub fn NewWebSocketClient(comptime ssl: bool) type {
|
||||
}
|
||||
|
||||
fn sendPong(this: *WebSocket, socket: Socket) bool {
|
||||
if (socket.isClosed() or socket.isShutdown()) {
|
||||
if (!this.hasTCP()) {
|
||||
this.dispatchAbruptClose(ErrorCode.ended);
|
||||
return false;
|
||||
}
|
||||
@@ -916,14 +946,17 @@ pub fn NewWebSocketClient(comptime ssl: bool) type {
|
||||
body_len: usize,
|
||||
) void {
|
||||
log("Sending close with code {d}", .{code});
|
||||
if (socket.isClosed() or socket.isShutdown()) {
|
||||
if (!this.hasTCP()) {
|
||||
this.dispatchAbruptClose(ErrorCode.ended);
|
||||
this.clearData();
|
||||
return;
|
||||
}
|
||||
// we dont wanna shutdownRead when SSL, because SSL handshake can happen when writting
|
||||
// For tunnel mode, shutdownRead on the detached socket is a no-op; skip it.
|
||||
if (comptime !ssl) {
|
||||
socket.shutdownRead();
|
||||
if (this.proxy_tunnel == null) {
|
||||
socket.shutdownRead();
|
||||
}
|
||||
}
|
||||
var final_body_bytes: [128 + 8]u8 = undefined;
|
||||
var header = @as(WebsocketHeader, @bitCast(@as(u16, 0)));
|
||||
|
||||
@@ -253,6 +253,13 @@ pub fn setConnectedWebSocket(this: *WebSocketProxyTunnel, ws: *WebSocketClient)
|
||||
this.#upgrade_client = .{ .none = {} };
|
||||
}
|
||||
|
||||
/// Clear the connected WebSocket reference. Called before tunnel shutdown during
|
||||
/// a clean close so the tunnel's onClose callback doesn't dispatch a spurious
|
||||
/// abrupt close (1006) after the WebSocket has already sent a clean close frame.
|
||||
pub fn clearConnectedWebSocket(this: *WebSocketProxyTunnel) void {
|
||||
this.#connected_websocket = null;
|
||||
}
|
||||
|
||||
/// SSLWrapper callback: Called with encrypted data to send to network
|
||||
fn writeEncrypted(this: *WebSocketProxyTunnel, encrypted_data: []const u8) void {
|
||||
log("writeEncrypted: {} bytes", .{encrypted_data.len});
|
||||
|
||||
72
src/ini.zig
72
src/ini.zig
@@ -291,25 +291,32 @@ pub const Parser = struct {
|
||||
}
|
||||
},
|
||||
else => {
|
||||
try unesc.appendSlice(switch (bun.strings.utf8ByteSequenceLength(c)) {
|
||||
1 => brk: {
|
||||
break :brk &[_]u8{ '\\', c };
|
||||
switch (bun.strings.utf8ByteSequenceLength(c)) {
|
||||
0, 1 => try unesc.appendSlice(&[_]u8{ '\\', c }),
|
||||
2 => if (val.len - i >= 2) {
|
||||
try unesc.appendSlice(&[_]u8{ '\\', c, val[i + 1] });
|
||||
i += 1;
|
||||
} else {
|
||||
try unesc.appendSlice(&[_]u8{ '\\', c });
|
||||
},
|
||||
2 => brk: {
|
||||
defer i += 1;
|
||||
break :brk &[_]u8{ '\\', c, val[i + 1] };
|
||||
3 => if (val.len - i >= 3) {
|
||||
try unesc.appendSlice(&[_]u8{ '\\', c, val[i + 1], val[i + 2] });
|
||||
i += 2;
|
||||
} else {
|
||||
try unesc.append('\\');
|
||||
try unesc.appendSlice(val[i..val.len]);
|
||||
i = val.len - 1;
|
||||
},
|
||||
3 => brk: {
|
||||
defer i += 2;
|
||||
break :brk &[_]u8{ '\\', c, val[i + 1], val[i + 2] };
|
||||
4 => if (val.len - i >= 4) {
|
||||
try unesc.appendSlice(&[_]u8{ '\\', c, val[i + 1], val[i + 2], val[i + 3] });
|
||||
i += 3;
|
||||
} else {
|
||||
try unesc.append('\\');
|
||||
try unesc.appendSlice(val[i..val.len]);
|
||||
i = val.len - 1;
|
||||
},
|
||||
4 => brk: {
|
||||
defer i += 3;
|
||||
break :brk &[_]u8{ '\\', c, val[i + 1], val[i + 2], val[i + 3] };
|
||||
},
|
||||
// this means invalid utf8
|
||||
else => unreachable,
|
||||
});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
@@ -342,25 +349,30 @@ pub const Parser = struct {
|
||||
try unesc.append('.');
|
||||
}
|
||||
},
|
||||
else => try unesc.appendSlice(switch (bun.strings.utf8ByteSequenceLength(c)) {
|
||||
1 => brk: {
|
||||
break :brk &[_]u8{c};
|
||||
else => switch (bun.strings.utf8ByteSequenceLength(c)) {
|
||||
0, 1 => try unesc.append(c),
|
||||
2 => if (val.len - i >= 2) {
|
||||
try unesc.appendSlice(&[_]u8{ c, val[i + 1] });
|
||||
i += 1;
|
||||
} else {
|
||||
try unesc.append(c);
|
||||
},
|
||||
2 => brk: {
|
||||
defer i += 1;
|
||||
break :brk &[_]u8{ c, val[i + 1] };
|
||||
3 => if (val.len - i >= 3) {
|
||||
try unesc.appendSlice(&[_]u8{ c, val[i + 1], val[i + 2] });
|
||||
i += 2;
|
||||
} else {
|
||||
try unesc.appendSlice(val[i..val.len]);
|
||||
i = val.len - 1;
|
||||
},
|
||||
3 => brk: {
|
||||
defer i += 2;
|
||||
break :brk &[_]u8{ c, val[i + 1], val[i + 2] };
|
||||
4 => if (val.len - i >= 4) {
|
||||
try unesc.appendSlice(&[_]u8{ c, val[i + 1], val[i + 2], val[i + 3] });
|
||||
i += 3;
|
||||
} else {
|
||||
try unesc.appendSlice(val[i..val.len]);
|
||||
i = val.len - 1;
|
||||
},
|
||||
4 => brk: {
|
||||
defer i += 3;
|
||||
break :brk &[_]u8{ c, val[i + 1], val[i + 2], val[i + 3] };
|
||||
},
|
||||
// this means invalid utf8
|
||||
else => unreachable,
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -199,18 +199,18 @@ function Sign(algorithm, options): void {
|
||||
}
|
||||
$toClass(Sign, "Sign", Writable);
|
||||
|
||||
Sign.prototype._write = function _write(chunk, encoding, callback) {
|
||||
this.update(chunk, encoding);
|
||||
callback();
|
||||
};
|
||||
|
||||
Sign.prototype.update = function update(data, encoding) {
|
||||
return this[kHandle].update(this, data, encoding);
|
||||
};
|
||||
|
||||
Sign.prototype.sign = function sign(options, encoding) {
|
||||
return this[kHandle].sign(options, encoding);
|
||||
};
|
||||
Object.assign(Sign.prototype, {
|
||||
_write: function (chunk, encoding, callback) {
|
||||
this.update(chunk, encoding);
|
||||
callback();
|
||||
},
|
||||
update: function (data, encoding) {
|
||||
return this[kHandle].update(this, data, encoding);
|
||||
},
|
||||
sign: function (options, encoding) {
|
||||
return this[kHandle].sign(options, encoding);
|
||||
},
|
||||
});
|
||||
|
||||
crypto_exports.Sign = Sign;
|
||||
crypto_exports.sign = sign;
|
||||
@@ -237,9 +237,11 @@ $toClass(Verify, "Verify", Writable);
|
||||
Verify.prototype._write = Sign.prototype._write;
|
||||
Verify.prototype.update = Sign.prototype.update;
|
||||
|
||||
Verify.prototype.verify = function verify(options, signature, sigEncoding) {
|
||||
return this[kHandle].verify(options, signature, sigEncoding);
|
||||
};
|
||||
Object.assign(Verify.prototype, {
|
||||
verify: function (options, signature, sigEncoding) {
|
||||
return this[kHandle].verify(options, signature, sigEncoding);
|
||||
},
|
||||
});
|
||||
|
||||
crypto_exports.Verify = Verify;
|
||||
crypto_exports.verify = verify;
|
||||
@@ -250,82 +252,76 @@ function createVerify(algorithm, options?) {
|
||||
|
||||
crypto_exports.createVerify = createVerify;
|
||||
|
||||
{
|
||||
function Hash(algorithm, options?): void {
|
||||
if (!new.target) {
|
||||
return new Hash(algorithm, options);
|
||||
}
|
||||
|
||||
const handle = new _Hash(algorithm, options);
|
||||
this[kHandle] = handle;
|
||||
|
||||
LazyTransform.$apply(this, [options]);
|
||||
function Hash(algorithm, options?): void {
|
||||
if (!new.target) {
|
||||
return new Hash(algorithm, options);
|
||||
}
|
||||
$toClass(Hash, "Hash", LazyTransform);
|
||||
|
||||
Hash.prototype.copy = function copy(options) {
|
||||
const handle = new _Hash(algorithm, options);
|
||||
this[kHandle] = handle;
|
||||
|
||||
LazyTransform.$apply(this, [options]);
|
||||
}
|
||||
$toClass(Hash, "Hash", LazyTransform);
|
||||
|
||||
Object.assign(Hash.prototype, {
|
||||
copy: function (options) {
|
||||
return new Hash(this[kHandle], options);
|
||||
};
|
||||
|
||||
Hash.prototype._transform = function _transform(chunk, encoding, callback) {
|
||||
},
|
||||
_transform: function (chunk, encoding, callback) {
|
||||
this[kHandle].update(this, chunk, encoding);
|
||||
callback();
|
||||
};
|
||||
|
||||
Hash.prototype._flush = function _flush(callback) {
|
||||
},
|
||||
_flush: function (callback) {
|
||||
this.push(this[kHandle].digest(null, false));
|
||||
callback();
|
||||
};
|
||||
|
||||
Hash.prototype.update = function update(data, encoding) {
|
||||
},
|
||||
update: function (data, encoding) {
|
||||
return this[kHandle].update(this, data, encoding);
|
||||
};
|
||||
|
||||
Hash.prototype.digest = function digest(outputEncoding) {
|
||||
},
|
||||
digest: function (outputEncoding) {
|
||||
return this[kHandle].digest(outputEncoding);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
crypto_exports.Hash = Hash;
|
||||
crypto_exports.createHash = function createHash(algorithm, options) {
|
||||
return new Hash(algorithm, options);
|
||||
};
|
||||
}
|
||||
crypto_exports.Hash = Hash;
|
||||
crypto_exports.createHash = function createHash(algorithm, options) {
|
||||
return new Hash(algorithm, options);
|
||||
};
|
||||
|
||||
{
|
||||
function Hmac(hmac, key, options?): void {
|
||||
if (!new.target) {
|
||||
return new Hmac(hmac, key, options);
|
||||
}
|
||||
|
||||
const handle = new _Hmac(hmac, key, options);
|
||||
this[kHandle] = handle;
|
||||
|
||||
LazyTransform.$apply(this, [options]);
|
||||
function Hmac(hmac, key, options?): void {
|
||||
if (!new.target) {
|
||||
return new Hmac(hmac, key, options);
|
||||
}
|
||||
$toClass(Hmac, "Hmac", LazyTransform);
|
||||
|
||||
Hmac.prototype.update = function update(data, encoding) {
|
||||
const handle = new _Hmac(hmac, key, options);
|
||||
this[kHandle] = handle;
|
||||
|
||||
LazyTransform.$apply(this, [options]);
|
||||
}
|
||||
$toClass(Hmac, "Hmac", LazyTransform);
|
||||
|
||||
Object.assign(Hmac.prototype, {
|
||||
update: function (data, encoding) {
|
||||
return this[kHandle].update(this, data, encoding);
|
||||
};
|
||||
|
||||
Hmac.prototype.digest = function digest(outputEncoding) {
|
||||
},
|
||||
digest: function (outputEncoding) {
|
||||
return this[kHandle].digest(outputEncoding);
|
||||
};
|
||||
|
||||
Hmac.prototype._transform = function _transform(chunk, encoding, callback) {
|
||||
},
|
||||
_transform: function (chunk, encoding, callback) {
|
||||
this[kHandle].update(this, chunk, encoding);
|
||||
callback();
|
||||
};
|
||||
Hmac.prototype._flush = function _flush(callback) {
|
||||
},
|
||||
_flush: function (callback) {
|
||||
this.push(this[kHandle].digest());
|
||||
callback();
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
crypto_exports.Hmac = Hmac;
|
||||
crypto_exports.createHmac = function createHmac(hmac, key, options) {
|
||||
return new Hmac(hmac, key, options);
|
||||
};
|
||||
}
|
||||
crypto_exports.Hmac = Hmac;
|
||||
crypto_exports.createHmac = function createHmac(hmac, key, options) {
|
||||
return new Hmac(hmac, key, options);
|
||||
};
|
||||
|
||||
crypto_exports.getHashes = getHashes;
|
||||
|
||||
@@ -390,62 +386,6 @@ crypto_exports.createECDH = function createECDH(curve) {
|
||||
return decoder;
|
||||
}
|
||||
|
||||
function setAutoPadding(ap) {
|
||||
this[kHandle].setAutoPadding(ap);
|
||||
return this;
|
||||
}
|
||||
|
||||
function getAuthTag() {
|
||||
return this[kHandle].getAuthTag();
|
||||
}
|
||||
|
||||
function setAuthTag(tagbuf, encoding) {
|
||||
this[kHandle].setAuthTag(tagbuf, encoding);
|
||||
return this;
|
||||
}
|
||||
|
||||
function setAAD(aadbuf, options) {
|
||||
this[kHandle].setAAD(aadbuf, options);
|
||||
return this;
|
||||
}
|
||||
|
||||
function _transform(chunk, encoding, callback) {
|
||||
this.push(this[kHandle].update(chunk, encoding));
|
||||
callback();
|
||||
}
|
||||
|
||||
function _flush(callback) {
|
||||
try {
|
||||
this.push(this[kHandle].final());
|
||||
} catch (e) {
|
||||
callback(e);
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
}
|
||||
|
||||
function update(data, inputEncoding, outputEncoding) {
|
||||
const res = this[kHandle].update(data, inputEncoding);
|
||||
|
||||
if (outputEncoding && outputEncoding !== "buffer") {
|
||||
this._decoder = getDecoder(this._decoder, outputEncoding);
|
||||
return this._decoder.write(res);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function final(outputEncoding) {
|
||||
const res = this[kHandle].final();
|
||||
|
||||
if (outputEncoding && outputEncoding !== "buffer") {
|
||||
this._decoder = getDecoder(this._decoder, outputEncoding);
|
||||
return this._decoder.end(res);
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
function Cipheriv(cipher, key, iv, options): void {
|
||||
if (!new.target) {
|
||||
return new Cipheriv(cipher, key, iv, options);
|
||||
@@ -457,13 +397,52 @@ crypto_exports.createECDH = function createECDH(curve) {
|
||||
}
|
||||
$toClass(Cipheriv, "Cipheriv", LazyTransform);
|
||||
|
||||
Cipheriv.prototype.setAutoPadding = setAutoPadding;
|
||||
Cipheriv.prototype.getAuthTag = getAuthTag;
|
||||
Cipheriv.prototype.setAAD = setAAD;
|
||||
Cipheriv.prototype._transform = _transform;
|
||||
Cipheriv.prototype._flush = _flush;
|
||||
Cipheriv.prototype.update = update;
|
||||
Cipheriv.prototype.final = final;
|
||||
Object.assign(Cipheriv.prototype, {
|
||||
setAutoPadding: function (ap) {
|
||||
this[kHandle].setAutoPadding(ap);
|
||||
return this;
|
||||
},
|
||||
getAuthTag: function () {
|
||||
return this[kHandle].getAuthTag();
|
||||
},
|
||||
setAAD: function (aadbuf, options) {
|
||||
this[kHandle].setAAD(aadbuf, options);
|
||||
return this;
|
||||
},
|
||||
_transform: function (chunk, encoding, callback) {
|
||||
this.push(this[kHandle].update(chunk, encoding));
|
||||
callback();
|
||||
},
|
||||
_flush: function (callback) {
|
||||
try {
|
||||
this.push(this[kHandle].final());
|
||||
} catch (e) {
|
||||
callback(e);
|
||||
return;
|
||||
}
|
||||
callback();
|
||||
},
|
||||
update: function (data, inputEncoding, outputEncoding) {
|
||||
const res = this[kHandle].update(data, inputEncoding);
|
||||
|
||||
if (outputEncoding && outputEncoding !== "buffer") {
|
||||
this._decoder = getDecoder(this._decoder, outputEncoding);
|
||||
return this._decoder.write(res);
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
final: function (outputEncoding) {
|
||||
const res = this[kHandle].final();
|
||||
|
||||
if (outputEncoding && outputEncoding !== "buffer") {
|
||||
this._decoder = getDecoder(this._decoder, outputEncoding);
|
||||
return this._decoder.end(res);
|
||||
}
|
||||
|
||||
return res;
|
||||
},
|
||||
});
|
||||
|
||||
function Decipheriv(cipher, key, iv, options): void {
|
||||
if (!new.target) {
|
||||
@@ -476,13 +455,18 @@ crypto_exports.createECDH = function createECDH(curve) {
|
||||
}
|
||||
$toClass(Decipheriv, "Decipheriv", LazyTransform);
|
||||
|
||||
Decipheriv.prototype.setAutoPadding = setAutoPadding;
|
||||
Decipheriv.prototype.setAuthTag = setAuthTag;
|
||||
Decipheriv.prototype.setAAD = setAAD;
|
||||
Decipheriv.prototype._transform = _transform;
|
||||
Decipheriv.prototype._flush = _flush;
|
||||
Decipheriv.prototype.update = update;
|
||||
Decipheriv.prototype.final = final;
|
||||
Object.assign(Decipheriv.prototype, {
|
||||
setAutoPadding: Cipheriv.prototype.setAutoPadding,
|
||||
setAuthTag: function (tagbuf, encoding) {
|
||||
this[kHandle].setAuthTag(tagbuf, encoding);
|
||||
return this;
|
||||
},
|
||||
setAAD: Cipheriv.prototype.setAAD,
|
||||
_transform: Cipheriv.prototype._transform,
|
||||
_flush: Cipheriv.prototype._flush,
|
||||
update: Cipheriv.prototype.update,
|
||||
final: Cipheriv.prototype.final,
|
||||
});
|
||||
|
||||
crypto_exports.Cipheriv = Cipheriv;
|
||||
crypto_exports.Decipheriv = Decipheriv;
|
||||
|
||||
@@ -460,13 +460,13 @@ pub const Archiver = struct {
|
||||
if (comptime Environment.isWindows) {
|
||||
try bun.MakePath.makePath(u16, dir, path);
|
||||
} else {
|
||||
std.posix.mkdiratZ(dir_fd, pathname, @intCast(mode)) catch |err| {
|
||||
std.posix.mkdiratZ(dir_fd, path, @intCast(mode)) catch |err| {
|
||||
// It's possible for some tarballs to return a directory twice, with and
|
||||
// without `./` in the beginning. So if it already exists, continue to the
|
||||
// next entry.
|
||||
if (err == error.PathAlreadyExists or err == error.NotDir) continue;
|
||||
bun.makePath(dir, std.fs.path.dirname(path_slice) orelse return err) catch {};
|
||||
std.posix.mkdiratZ(dir_fd, pathname, 0o777) catch {};
|
||||
std.posix.mkdiratZ(dir_fd, path, 0o777) catch {};
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
@@ -222,7 +222,7 @@ pub const Linker = struct {
|
||||
|
||||
if (comptime is_bun) {
|
||||
// make these happen at runtime
|
||||
if (import_record.kind == .require or import_record.kind == .require_resolve) {
|
||||
if (import_record.kind == .require or import_record.kind == .require_resolve or import_record.kind == .dynamic) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -927,7 +927,9 @@ pub const Log = struct {
|
||||
err: anyerror,
|
||||
) OOM!void {
|
||||
@branchHint(.cold);
|
||||
return try addResolveErrorWithLevel(log, source, r, allocator, fmt, args, import_kind, false, .err, err);
|
||||
// Always dupe the line_text from the source to ensure the Location data
|
||||
// outlives the source's backing memory (which may be arena-allocated).
|
||||
return try addResolveErrorWithLevel(log, source, r, allocator, fmt, args, import_kind, true, .err, err);
|
||||
}
|
||||
|
||||
pub fn addResolveErrorWithTextDupe(
|
||||
|
||||
@@ -221,7 +221,11 @@ pub const S3Credentials = struct {
|
||||
defer str.deref();
|
||||
if (str.tag != .Empty and str.tag != .Dead) {
|
||||
new_credentials._contentDispositionSlice = str.toUTF8(bun.default_allocator);
|
||||
new_credentials.content_disposition = new_credentials._contentDispositionSlice.?.slice();
|
||||
const slice = new_credentials._contentDispositionSlice.?.slice();
|
||||
if (containsNewlineOrCR(slice)) {
|
||||
return globalObject.throwInvalidArguments("contentDisposition must not contain newline characters (CR/LF)", .{});
|
||||
}
|
||||
new_credentials.content_disposition = slice;
|
||||
}
|
||||
} else {
|
||||
return globalObject.throwInvalidArgumentTypeValue("contentDisposition", "string", js_value);
|
||||
@@ -236,7 +240,11 @@ pub const S3Credentials = struct {
|
||||
defer str.deref();
|
||||
if (str.tag != .Empty and str.tag != .Dead) {
|
||||
new_credentials._contentTypeSlice = str.toUTF8(bun.default_allocator);
|
||||
new_credentials.content_type = new_credentials._contentTypeSlice.?.slice();
|
||||
const slice = new_credentials._contentTypeSlice.?.slice();
|
||||
if (containsNewlineOrCR(slice)) {
|
||||
return globalObject.throwInvalidArguments("type must not contain newline characters (CR/LF)", .{});
|
||||
}
|
||||
new_credentials.content_type = slice;
|
||||
}
|
||||
} else {
|
||||
return globalObject.throwInvalidArgumentTypeValue("type", "string", js_value);
|
||||
@@ -251,7 +259,11 @@ pub const S3Credentials = struct {
|
||||
defer str.deref();
|
||||
if (str.tag != .Empty and str.tag != .Dead) {
|
||||
new_credentials._contentEncodingSlice = str.toUTF8(bun.default_allocator);
|
||||
new_credentials.content_encoding = new_credentials._contentEncodingSlice.?.slice();
|
||||
const slice = new_credentials._contentEncodingSlice.?.slice();
|
||||
if (containsNewlineOrCR(slice)) {
|
||||
return globalObject.throwInvalidArguments("contentEncoding must not contain newline characters (CR/LF)", .{});
|
||||
}
|
||||
new_credentials.content_encoding = slice;
|
||||
}
|
||||
} else {
|
||||
return globalObject.throwInvalidArgumentTypeValue("contentEncoding", "string", js_value);
|
||||
@@ -1150,6 +1162,12 @@ const CanonicalRequest = struct {
|
||||
}
|
||||
};
|
||||
|
||||
/// Returns true if the given slice contains any CR (\r) or LF (\n) characters,
|
||||
/// which would allow HTTP header injection if used in a header value.
|
||||
fn containsNewlineOrCR(value: []const u8) bool {
|
||||
return strings.indexOfAny(value, "\r\n") != null;
|
||||
}
|
||||
|
||||
const std = @import("std");
|
||||
const ACL = @import("./acl.zig").ACL;
|
||||
const MultiPartUploadOptions = @import("./multipart_options.zig").MultiPartUploadOptions;
|
||||
|
||||
@@ -1154,7 +1154,7 @@ pub const Interpreter = struct {
|
||||
_ = callframe; // autofix
|
||||
|
||||
if (this.setupIOBeforeRun().asErr()) |e| {
|
||||
defer this.#deinitFromExec();
|
||||
defer this.#derefRootShellAndIOIfNeeded(true);
|
||||
const shellerr = bun.shell.ShellErr.newSys(e);
|
||||
return try throwShellErr(&shellerr, .{ .js = globalThis.bunVM().event_loop });
|
||||
}
|
||||
|
||||
@@ -422,6 +422,19 @@ pub fn createInstance(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFra
|
||||
break :brk b.allocatedSlice();
|
||||
};
|
||||
|
||||
// Reject null bytes in connection parameters to prevent protocol injection
|
||||
// (null bytes act as field terminators in the MySQL wire protocol).
|
||||
inline for (.{ .{ username, "username" }, .{ password, "password" }, .{ database, "database" }, .{ path, "path" } }) |entry| {
|
||||
if (entry[0].len > 0 and std.mem.indexOfScalar(u8, entry[0], 0) != null) {
|
||||
bun.default_allocator.free(options_buf);
|
||||
tls_config.deinit();
|
||||
if (tls_ctx) |tls| {
|
||||
tls.deinit(true);
|
||||
}
|
||||
return globalObject.throwInvalidArguments(entry[1] ++ " must not contain null bytes", .{});
|
||||
}
|
||||
}
|
||||
|
||||
const on_connect = arguments[9];
|
||||
const on_close = arguments[10];
|
||||
const idle_timeout = arguments[11].toInt32();
|
||||
|
||||
@@ -680,6 +680,20 @@ pub fn call(globalObject: *jsc.JSGlobalObject, callframe: *jsc.CallFrame) bun.JS
|
||||
break :brk b.allocatedSlice();
|
||||
};
|
||||
|
||||
// Reject null bytes in connection parameters to prevent Postgres startup
|
||||
// message parameter injection (null bytes act as field terminators in the
|
||||
// wire protocol's key\0value\0 format).
|
||||
inline for (.{ .{ username, "username" }, .{ password, "password" }, .{ database, "database" }, .{ path, "path" } }) |entry| {
|
||||
if (entry[0].len > 0 and std.mem.indexOfScalar(u8, entry[0], 0) != null) {
|
||||
bun.default_allocator.free(options_buf);
|
||||
tls_config.deinit();
|
||||
if (tls_ctx) |tls| {
|
||||
tls.deinit(true);
|
||||
}
|
||||
return globalObject.throwInvalidArguments(entry[1] ++ " must not contain null bytes", .{});
|
||||
}
|
||||
}
|
||||
|
||||
const on_connect = arguments[9];
|
||||
const on_close = arguments[10];
|
||||
const idle_timeout = arguments[11].toInt32();
|
||||
@@ -1626,7 +1640,10 @@ pub fn on(this: *PostgresSQLConnection, comptime MessageType: @Type(.enum_litera
|
||||
// This will usually start with "v="
|
||||
const comparison_signature = final.data.slice();
|
||||
|
||||
if (comparison_signature.len < 2 or !bun.strings.eqlLong(server_signature, comparison_signature[2..], true)) {
|
||||
if (comparison_signature.len < 2 or
|
||||
server_signature.len != comparison_signature.len - 2 or
|
||||
BoringSSL.c.CRYPTO_memcmp(server_signature.ptr, comparison_signature[2..].ptr, server_signature.len) != 0)
|
||||
{
|
||||
debug("SASLFinal - SASL Server signature mismatch\nExpected: {s}\nActual: {s}", .{ server_signature, comparison_signature[2..] });
|
||||
this.fail("The server did not return the correct signature", error.SASL_SIGNATURE_MISMATCH);
|
||||
} else {
|
||||
|
||||
@@ -4092,6 +4092,12 @@ pub fn copyFileZSlowWithHandle(in_handle: bun.FileDescriptor, to_dir: bun.FileDe
|
||||
_ = std.os.linux.fallocate(out_handle.cast(), 0, 0, @intCast(stat_.size));
|
||||
}
|
||||
|
||||
// Seek input to beginning — the caller may have written to this fd,
|
||||
// leaving the file offset at EOF. copy_file_range / sendfile / read
|
||||
// all use the current offset when called with null offsets.
|
||||
// Ignore errors: the fd may be non-seekable (e.g. a pipe).
|
||||
_ = setFileOffset(in_handle, 0);
|
||||
|
||||
switch (bun.copyFile(in_handle, out_handle)) {
|
||||
.err => |e| return .{ .err = e },
|
||||
.result => {},
|
||||
|
||||
@@ -260,14 +260,35 @@ devTest("hmr handles rapid consecutive edits", {
|
||||
await Bun.sleep(1);
|
||||
}
|
||||
|
||||
// Wait event-driven for "render 10" to appear. Intermediate renders may
|
||||
// be skipped (watcher coalescing) and the final render may fire multiple
|
||||
// times (duplicate reloads), so we just listen for any occurrence.
|
||||
const finalRender = "render 10";
|
||||
while (true) {
|
||||
const message = await client.getStringMessage();
|
||||
if (message === finalRender) break;
|
||||
if (typeof message === "string" && message.includes("HMR_ERROR")) {
|
||||
throw new Error("Unexpected HMR error message: " + message);
|
||||
}
|
||||
}
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const check = () => {
|
||||
for (const msg of client.messages) {
|
||||
if (typeof msg === "string" && msg.includes("HMR_ERROR")) {
|
||||
cleanup();
|
||||
reject(new Error("Unexpected HMR error message: " + msg));
|
||||
return;
|
||||
}
|
||||
if (msg === finalRender) {
|
||||
cleanup();
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
const cleanup = () => {
|
||||
client.off("message", check);
|
||||
};
|
||||
client.on("message", check);
|
||||
// Check messages already buffered.
|
||||
check();
|
||||
});
|
||||
// Drain all buffered messages — intermediate renders and possible
|
||||
// duplicates of the final render are expected and harmless.
|
||||
client.messages.length = 0;
|
||||
|
||||
const hmrErrors = await client.js`return globalThis.__hmrErrors ? [...globalThis.__hmrErrors] : [];`;
|
||||
if (hmrErrors.length > 0) {
|
||||
|
||||
@@ -121,4 +121,71 @@ describe("Bun.build compile", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("compiled binary validity", () => {
|
||||
test("output binary has valid executable header", async () => {
|
||||
using dir = tempDir("build-compile-valid-header", {
|
||||
"app.js": `console.log("hello");`,
|
||||
});
|
||||
|
||||
const outfile = join(dir + "", "app-out");
|
||||
const result = await Bun.build({
|
||||
entrypoints: [join(dir + "", "app.js")],
|
||||
compile: {
|
||||
outfile,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
// Read the first 4 bytes and verify it's a valid executable magic number
|
||||
const file = Bun.file(result.outputs[0].path);
|
||||
const header = new Uint8Array(await file.slice(0, 4).arrayBuffer());
|
||||
|
||||
if (isMacOS) {
|
||||
// MachO magic: 0xCFFAEDFE (little-endian)
|
||||
expect(header[0]).toBe(0xcf);
|
||||
expect(header[1]).toBe(0xfa);
|
||||
expect(header[2]).toBe(0xed);
|
||||
expect(header[3]).toBe(0xfe);
|
||||
} else if (isLinux) {
|
||||
// ELF magic: 0x7F 'E' 'L' 'F'
|
||||
expect(header[0]).toBe(0x7f);
|
||||
expect(header[1]).toBe(0x45); // 'E'
|
||||
expect(header[2]).toBe(0x4c); // 'L'
|
||||
expect(header[3]).toBe(0x46); // 'F'
|
||||
} else if (isWindows) {
|
||||
// PE magic: 'M' 'Z'
|
||||
expect(header[0]).toBe(0x4d); // 'M'
|
||||
expect(header[1]).toBe(0x5a); // 'Z'
|
||||
}
|
||||
});
|
||||
|
||||
test("compiled binary runs and produces expected output", async () => {
|
||||
using dir = tempDir("build-compile-runs", {
|
||||
"app.js": `console.log("compile-test-output");`,
|
||||
});
|
||||
|
||||
const outfile = join(dir + "", "app-run");
|
||||
const result = await Bun.build({
|
||||
entrypoints: [join(dir + "", "app.js")],
|
||||
compile: {
|
||||
outfile,
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [result.outputs[0].path],
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout.trim()).toBe("compile-test-output");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
// file command test works well
|
||||
|
||||
@@ -611,6 +611,82 @@ describe("Bun.Archive", () => {
|
||||
// Very deep paths might fail on some systems - that's acceptable
|
||||
}
|
||||
});
|
||||
|
||||
test("directory entries with path traversal components cannot escape extraction root", async () => {
|
||||
// Manually craft a tar archive containing directory entries with "../" traversal
|
||||
// components in their pathnames. This tests that the extraction code uses the
|
||||
// normalized path (which strips "..") rather than the raw pathname from the tarball.
|
||||
function createTarHeader(
|
||||
name: string,
|
||||
size: number,
|
||||
type: "0" | "5", // 0=file, 5=directory
|
||||
): Uint8Array {
|
||||
const header = new Uint8Array(512);
|
||||
const enc = new TextEncoder();
|
||||
header.set(enc.encode(name).slice(0, 100), 0);
|
||||
header.set(enc.encode(type === "5" ? "0000755 " : "0000644 "), 100);
|
||||
header.set(enc.encode("0000000 "), 108);
|
||||
header.set(enc.encode("0000000 "), 116);
|
||||
header.set(enc.encode(size.toString(8).padStart(11, "0") + " "), 124);
|
||||
const mtime = Math.floor(Date.now() / 1000)
|
||||
.toString(8)
|
||||
.padStart(11, "0");
|
||||
header.set(enc.encode(mtime + " "), 136);
|
||||
header.set(enc.encode(" "), 148); // checksum placeholder
|
||||
header[156] = type.charCodeAt(0);
|
||||
header.set(enc.encode("ustar"), 257);
|
||||
header[262] = 0;
|
||||
header.set(enc.encode("00"), 263);
|
||||
let checksum = 0;
|
||||
for (let i = 0; i < 512; i++) checksum += header[i];
|
||||
header.set(enc.encode(checksum.toString(8).padStart(6, "0") + "\0 "), 148);
|
||||
return header;
|
||||
}
|
||||
|
||||
const blocks: Uint8Array[] = [];
|
||||
const enc = new TextEncoder();
|
||||
|
||||
// A legitimate directory
|
||||
blocks.push(createTarHeader("safe_dir/", 0, "5"));
|
||||
// A directory entry with traversal: "safe_dir/../../escaped_dir/"
|
||||
// After normalization this becomes "escaped_dir" (safe),
|
||||
// but the raw pathname resolves ".." via the kernel in mkdirat.
|
||||
blocks.push(createTarHeader("safe_dir/../../escaped_dir/", 0, "5"));
|
||||
// A normal file
|
||||
const content = enc.encode("hello");
|
||||
blocks.push(createTarHeader("safe_dir/file.txt", content.length, "0"));
|
||||
blocks.push(content);
|
||||
const pad = 512 - (content.length % 512);
|
||||
if (pad < 512) blocks.push(new Uint8Array(pad));
|
||||
// End-of-archive markers
|
||||
blocks.push(new Uint8Array(1024));
|
||||
|
||||
const totalLen = blocks.reduce((s, b) => s + b.length, 0);
|
||||
const tarball = new Uint8Array(totalLen);
|
||||
let offset = 0;
|
||||
for (const b of blocks) {
|
||||
tarball.set(b, offset);
|
||||
offset += b.length;
|
||||
}
|
||||
|
||||
// Create a parent directory so we can check if "escaped_dir" appears outside extractDir
|
||||
using parentDir = tempDir("archive-traversal-parent", {});
|
||||
const extractPath = join(String(parentDir), "extract");
|
||||
const { mkdirSync, existsSync } = require("fs");
|
||||
mkdirSync(extractPath, { recursive: true });
|
||||
|
||||
const archive = new Bun.Archive(tarball);
|
||||
await archive.extract(extractPath);
|
||||
|
||||
// The "escaped_dir" should NOT exist in the parent directory (outside extraction root)
|
||||
const escapedOutside = join(String(parentDir), "escaped_dir");
|
||||
expect(existsSync(escapedOutside)).toBe(false);
|
||||
|
||||
// The "safe_dir" should exist inside the extraction directory
|
||||
expect(existsSync(join(extractPath, "safe_dir"))).toBe(true);
|
||||
// The normalized "escaped_dir" may or may not exist inside extractPath
|
||||
// (depending on whether normalization keeps it), but it must NOT be outside
|
||||
});
|
||||
});
|
||||
|
||||
describe("Archive.write()", () => {
|
||||
|
||||
@@ -489,6 +489,61 @@ brr = 3
|
||||
"zr": ["deedee"],
|
||||
});
|
||||
});
|
||||
|
||||
describe("truncated/invalid utf-8", () => {
|
||||
test("bare continuation byte (0x80) should not crash", () => {
|
||||
// 0x80 is a continuation byte without a leading byte
|
||||
// utf8ByteSequenceLength returns 0, which must not hit unreachable
|
||||
const ini = Buffer.concat([Buffer.from("key = "), Buffer.from([0x80])]).toString("latin1");
|
||||
// Should not crash - just parse gracefully
|
||||
expect(() => parse(ini)).not.toThrow();
|
||||
});
|
||||
|
||||
test("truncated 2-byte sequence at end of value", () => {
|
||||
// 0xC0 is a 2-byte lead byte, but there's no continuation byte following
|
||||
const ini = Buffer.concat([Buffer.from("key = "), Buffer.from([0xc0])]).toString("latin1");
|
||||
expect(() => parse(ini)).not.toThrow();
|
||||
});
|
||||
|
||||
test("truncated 3-byte sequence at end of value", () => {
|
||||
// 0xE0 is a 3-byte lead byte, but only 0 continuation bytes follow
|
||||
const ini = Buffer.concat([Buffer.from("key = "), Buffer.from([0xe0])]).toString("latin1");
|
||||
expect(() => parse(ini)).not.toThrow();
|
||||
});
|
||||
|
||||
test("truncated 3-byte sequence with 1 continuation byte at end", () => {
|
||||
// 0xE0 is a 3-byte lead byte, but only 1 continuation byte follows
|
||||
const ini = Buffer.concat([Buffer.from("key = "), Buffer.from([0xe0, 0x80])]).toString("latin1");
|
||||
expect(() => parse(ini)).not.toThrow();
|
||||
});
|
||||
|
||||
test("truncated 4-byte sequence at end of value", () => {
|
||||
// 0xF0 is a 4-byte lead byte, but only 0 continuation bytes follow
|
||||
const ini = Buffer.concat([Buffer.from("key = "), Buffer.from([0xf0])]).toString("latin1");
|
||||
expect(() => parse(ini)).not.toThrow();
|
||||
});
|
||||
|
||||
test("truncated 4-byte sequence with 1 continuation byte at end", () => {
|
||||
const ini = Buffer.concat([Buffer.from("key = "), Buffer.from([0xf0, 0x80])]).toString("latin1");
|
||||
expect(() => parse(ini)).not.toThrow();
|
||||
});
|
||||
|
||||
test("truncated 4-byte sequence with 2 continuation bytes at end", () => {
|
||||
const ini = Buffer.concat([Buffer.from("key = "), Buffer.from([0xf0, 0x80, 0x80])]).toString("latin1");
|
||||
expect(() => parse(ini)).not.toThrow();
|
||||
});
|
||||
|
||||
test("truncated 2-byte sequence in escaped context", () => {
|
||||
// Backslash followed by a 2-byte lead byte at end of value
|
||||
const ini = Buffer.concat([Buffer.from("key = \\"), Buffer.from([0xc0])]).toString("latin1");
|
||||
expect(() => parse(ini)).not.toThrow();
|
||||
});
|
||||
|
||||
test("bare continuation byte in escaped context", () => {
|
||||
const ini = Buffer.concat([Buffer.from("key = \\"), Buffer.from([0x80])]).toString("latin1");
|
||||
expect(() => parse(ini)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
const wtf = {
|
||||
|
||||
@@ -504,6 +504,110 @@ describe("Hash", () => {
|
||||
expect(hash.update.name).toBe("update");
|
||||
expect(hash.digest.name).toBe("digest");
|
||||
expect(hash.copy.name).toBe("copy");
|
||||
expect(hash._transform.name).toBe("_transform");
|
||||
expect(hash._flush.name).toBe("_flush");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Hmac", () => {
|
||||
it("should have correct method names", () => {
|
||||
const hmac = crypto.createHmac("sha256", "key");
|
||||
expect(hmac.update.name).toBe("update");
|
||||
expect(hmac.digest.name).toBe("digest");
|
||||
expect(hmac._transform.name).toBe("_transform");
|
||||
expect(hmac._flush.name).toBe("_flush");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Sign", () => {
|
||||
it("should have correct method names", () => {
|
||||
const sign = crypto.createSign("sha256");
|
||||
expect(sign.update.name).toBe("update");
|
||||
expect(sign.sign.name).toBe("sign");
|
||||
expect(sign._write.name).toBe("_write");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Verify", () => {
|
||||
it("should have correct method names", () => {
|
||||
const verify = crypto.createVerify("sha256");
|
||||
expect(verify.update.name).toBe("update");
|
||||
expect(verify.verify.name).toBe("verify");
|
||||
expect(verify._write.name).toBe("_write");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Cipheriv", () => {
|
||||
it("should have correct method names", () => {
|
||||
const cipher = crypto.createCipheriv("aes-256-cbc", Buffer.alloc(32), Buffer.alloc(16));
|
||||
expect(cipher.update.name).toBe("update");
|
||||
expect(cipher.final.name).toBe("final");
|
||||
expect(cipher.setAutoPadding.name).toBe("setAutoPadding");
|
||||
expect(cipher.getAuthTag.name).toBe("getAuthTag");
|
||||
expect(cipher.setAAD.name).toBe("setAAD");
|
||||
expect(cipher._transform.name).toBe("_transform");
|
||||
expect(cipher._flush.name).toBe("_flush");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Decipheriv", () => {
|
||||
it("should have correct method names", () => {
|
||||
const decipher = crypto.createDecipheriv("aes-256-cbc", Buffer.alloc(32), Buffer.alloc(16));
|
||||
expect(decipher.update.name).toBe("update");
|
||||
expect(decipher.final.name).toBe("final");
|
||||
expect(decipher.setAutoPadding.name).toBe("setAutoPadding");
|
||||
expect(decipher.setAuthTag.name).toBe("setAuthTag");
|
||||
expect(decipher.setAAD.name).toBe("setAAD");
|
||||
expect(decipher._transform.name).toBe("_transform");
|
||||
expect(decipher._flush.name).toBe("_flush");
|
||||
});
|
||||
});
|
||||
|
||||
describe("DiffieHellman", () => {
|
||||
it("should have correct method names", () => {
|
||||
const dh = crypto.createDiffieHellman(512);
|
||||
expect(dh.generateKeys.name).toBe("generateKeys");
|
||||
expect(dh.computeSecret.name).toBe("computeSecret");
|
||||
expect(dh.getPrime.name).toBe("getPrime");
|
||||
expect(dh.getGenerator.name).toBe("getGenerator");
|
||||
expect(dh.getPublicKey.name).toBe("getPublicKey");
|
||||
expect(dh.getPrivateKey.name).toBe("getPrivateKey");
|
||||
expect(dh.setPublicKey.name).toBe("setPublicKey");
|
||||
expect(dh.setPrivateKey.name).toBe("setPrivateKey");
|
||||
});
|
||||
});
|
||||
|
||||
describe("ECDH", () => {
|
||||
it("should have correct method names", () => {
|
||||
const ecdh = crypto.createECDH("prime256v1");
|
||||
expect(ecdh.generateKeys.name).toBe("generateKeys");
|
||||
expect(ecdh.computeSecret.name).toBe("computeSecret");
|
||||
expect(ecdh.getPublicKey.name).toBe("getPublicKey");
|
||||
expect(ecdh.getPrivateKey.name).toBe("getPrivateKey");
|
||||
expect(ecdh.setPublicKey.name).toBe("setPublicKey");
|
||||
expect(ecdh.setPrivateKey.name).toBe("setPrivateKey");
|
||||
});
|
||||
});
|
||||
|
||||
describe("crypto module", () => {
|
||||
it("should have correct factory function names", () => {
|
||||
expect(crypto.createHash.name).toBe("createHash");
|
||||
expect(crypto.createHmac.name).toBe("createHmac");
|
||||
expect(crypto.createSign.name).toBe("createSign");
|
||||
expect(crypto.createVerify.name).toBe("createVerify");
|
||||
expect(crypto.createCipheriv.name).toBe("createCipheriv");
|
||||
expect(crypto.createDecipheriv.name).toBe("createDecipheriv");
|
||||
expect(crypto.createDiffieHellman.name).toBe("createDiffieHellman");
|
||||
expect(crypto.createECDH.name).toBe("createECDH");
|
||||
expect(crypto.hash.name).toBe("hash");
|
||||
expect(crypto.pbkdf2.name).toBe("pbkdf2");
|
||||
});
|
||||
|
||||
it("should have correct constructor names", () => {
|
||||
expect(crypto.Hash.name).toBe("Hash");
|
||||
expect(crypto.Hmac.name).toBe("Hmac");
|
||||
expect(crypto.Sign.name).toBe("Sign");
|
||||
expect(crypto.Verify.name).toBe("Verify");
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
222
test/js/web/websocket/websocket-pong-fragmented.test.ts
Normal file
222
test/js/web/websocket/websocket-pong-fragmented.test.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { TCPSocketListener } from "bun";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
const hostname = "127.0.0.1";
|
||||
const MAX_HEADER_SIZE = 16 * 1024;
|
||||
|
||||
function doHandshake(
|
||||
socket: any,
|
||||
handshakeBuffer: Uint8Array,
|
||||
data: Uint8Array,
|
||||
): { buffer: Uint8Array; done: boolean } {
|
||||
const newBuffer = new Uint8Array(handshakeBuffer.length + data.length);
|
||||
newBuffer.set(handshakeBuffer);
|
||||
newBuffer.set(data, handshakeBuffer.length);
|
||||
|
||||
if (newBuffer.length > MAX_HEADER_SIZE) {
|
||||
socket.end();
|
||||
throw new Error("Handshake headers too large");
|
||||
}
|
||||
|
||||
const dataStr = new TextDecoder("utf-8").decode(newBuffer);
|
||||
const endOfHeaders = dataStr.indexOf("\r\n\r\n");
|
||||
if (endOfHeaders === -1) {
|
||||
return { buffer: newBuffer, done: false };
|
||||
}
|
||||
|
||||
if (!dataStr.startsWith("GET")) {
|
||||
throw new Error("Invalid handshake");
|
||||
}
|
||||
|
||||
const magic = /Sec-WebSocket-Key:\s*(.*)\r\n/i.exec(dataStr);
|
||||
if (!magic) {
|
||||
throw new Error("Missing Sec-WebSocket-Key");
|
||||
}
|
||||
|
||||
const hasher = new Bun.CryptoHasher("sha1");
|
||||
hasher.update(magic[1].trim());
|
||||
hasher.update("258EAFA5-E914-47DA-95CA-C5AB0DC85B11");
|
||||
const accept = hasher.digest("base64");
|
||||
|
||||
socket.write(
|
||||
"HTTP/1.1 101 Switching Protocols\r\n" +
|
||||
"Upgrade: websocket\r\n" +
|
||||
"Connection: Upgrade\r\n" +
|
||||
`Sec-WebSocket-Accept: ${accept}\r\n` +
|
||||
"\r\n",
|
||||
);
|
||||
socket.flush();
|
||||
|
||||
return { buffer: newBuffer, done: true };
|
||||
}
|
||||
|
||||
function makeTextFrame(text: string): Uint8Array {
|
||||
const payload = new TextEncoder().encode(text);
|
||||
const len = payload.length;
|
||||
let header: Uint8Array;
|
||||
if (len < 126) {
|
||||
header = new Uint8Array([0x81, len]);
|
||||
} else if (len < 65536) {
|
||||
header = new Uint8Array([0x81, 126, (len >> 8) & 0xff, len & 0xff]);
|
||||
} else {
|
||||
throw new Error("Message too large for this test");
|
||||
}
|
||||
const frame = new Uint8Array(header.length + len);
|
||||
frame.set(header);
|
||||
frame.set(payload, header.length);
|
||||
return frame;
|
||||
}
|
||||
|
||||
describe("WebSocket", () => {
|
||||
test("fragmented pong frame does not cause frame desync", async () => {
|
||||
let server: TCPSocketListener | undefined;
|
||||
let client: WebSocket | undefined;
|
||||
let handshakeBuffer = new Uint8Array(0);
|
||||
let handshakeComplete = false;
|
||||
|
||||
try {
|
||||
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
||||
|
||||
server = Bun.listen({
|
||||
socket: {
|
||||
data(socket, data) {
|
||||
if (handshakeComplete) {
|
||||
// After handshake, we just receive client frames (like close) - ignore them
|
||||
return;
|
||||
}
|
||||
|
||||
const result = doHandshake(socket, handshakeBuffer, new Uint8Array(data));
|
||||
handshakeBuffer = result.buffer;
|
||||
if (!result.done) return;
|
||||
|
||||
handshakeComplete = true;
|
||||
|
||||
// Build a pong frame with a 50-byte payload, but deliver it in two parts.
|
||||
// Pong opcode = 0x8A, FIN=1
|
||||
const pongPayload = new Uint8Array(50);
|
||||
for (let i = 0; i < 50; i++) pongPayload[i] = 0x41 + (i % 26); // 'A'-'Z' repeated
|
||||
const pongFrame = new Uint8Array(2 + 50);
|
||||
pongFrame[0] = 0x8a; // FIN + Pong opcode
|
||||
pongFrame[1] = 50; // payload length
|
||||
pongFrame.set(pongPayload, 2);
|
||||
|
||||
// Part 1 of pong: header (2 bytes) + first 2 bytes of payload = 4 bytes
|
||||
// This leaves 48 bytes of pong payload undelivered.
|
||||
const pongPart1 = pongFrame.slice(0, 4);
|
||||
// Part 2: remaining 48 bytes of pong payload
|
||||
const pongPart2 = pongFrame.slice(4);
|
||||
|
||||
// A text message to send after the pong completes.
|
||||
const textFrame = makeTextFrame("hello after pong");
|
||||
|
||||
// Send part 1 of pong
|
||||
socket.write(pongPart1);
|
||||
socket.flush();
|
||||
|
||||
// After a delay, send part 2 of pong + the follow-up text message
|
||||
setTimeout(() => {
|
||||
// Concatenate part2 + text frame to simulate them arriving together
|
||||
const combined = new Uint8Array(pongPart2.length + textFrame.length);
|
||||
combined.set(pongPart2);
|
||||
combined.set(textFrame, pongPart2.length);
|
||||
socket.write(combined);
|
||||
socket.flush();
|
||||
}, 50);
|
||||
},
|
||||
},
|
||||
hostname,
|
||||
port: 0,
|
||||
});
|
||||
|
||||
const messages: string[] = [];
|
||||
|
||||
client = new WebSocket(`ws://${server.hostname}:${server.port}`);
|
||||
client.addEventListener("error", event => {
|
||||
reject(new Error("WebSocket error"));
|
||||
});
|
||||
client.addEventListener("close", event => {
|
||||
// If the connection closes unexpectedly due to frame desync, the test should fail
|
||||
reject(new Error(`WebSocket closed unexpectedly: code=${event.code} reason=${event.reason}`));
|
||||
});
|
||||
client.addEventListener("message", event => {
|
||||
messages.push(event.data as string);
|
||||
if (messages.length === 1) {
|
||||
// We got the text message after the fragmented pong
|
||||
try {
|
||||
expect(messages[0]).toBe("hello after pong");
|
||||
resolve();
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await promise;
|
||||
} finally {
|
||||
client?.close();
|
||||
server?.stop(true);
|
||||
}
|
||||
});
|
||||
|
||||
test("pong frame with payload > 125 bytes is rejected", async () => {
|
||||
let server: TCPSocketListener | undefined;
|
||||
let client: WebSocket | undefined;
|
||||
let handshakeBuffer = new Uint8Array(0);
|
||||
let handshakeComplete = false;
|
||||
|
||||
try {
|
||||
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
||||
|
||||
server = Bun.listen({
|
||||
socket: {
|
||||
data(socket, data) {
|
||||
if (handshakeComplete) return;
|
||||
|
||||
const result = doHandshake(socket, handshakeBuffer, new Uint8Array(data));
|
||||
handshakeBuffer = result.buffer;
|
||||
if (!result.done) return;
|
||||
|
||||
handshakeComplete = true;
|
||||
|
||||
// Send a pong frame with a 126-byte payload (invalid per RFC 6455 Section 5.5)
|
||||
// Control frames MUST have a payload length of 125 bytes or less.
|
||||
// Use 2-byte extended length encoding since 126 > 125.
|
||||
// But actually, the 7-bit length field in byte[1] can encode 0-125 directly.
|
||||
// For 126, the server must use the extended 16-bit length.
|
||||
// However, control frames with >125 payload are invalid regardless of encoding.
|
||||
const pongFrame = new Uint8Array(4 + 126);
|
||||
pongFrame[0] = 0x8a; // FIN + Pong
|
||||
pongFrame[1] = 126; // Signals 16-bit extended length follows
|
||||
pongFrame[2] = 0; // High byte of length
|
||||
pongFrame[3] = 126; // Low byte of length = 126
|
||||
// Fill payload with arbitrary data
|
||||
for (let i = 0; i < 126; i++) pongFrame[4 + i] = 0x42;
|
||||
|
||||
socket.write(pongFrame);
|
||||
socket.flush();
|
||||
},
|
||||
},
|
||||
hostname,
|
||||
port: 0,
|
||||
});
|
||||
|
||||
client = new WebSocket(`ws://${server.hostname}:${server.port}`);
|
||||
client.addEventListener("error", () => {
|
||||
// Expected - the connection should error due to invalid control frame
|
||||
resolve();
|
||||
});
|
||||
client.addEventListener("close", () => {
|
||||
// Also acceptable - connection closes due to protocol error
|
||||
resolve();
|
||||
});
|
||||
client.addEventListener("message", () => {
|
||||
reject(new Error("Should not receive a message from an invalid pong frame"));
|
||||
});
|
||||
|
||||
await promise;
|
||||
} finally {
|
||||
client?.close();
|
||||
server?.stop(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -398,6 +398,71 @@ describe("WebSocket wss:// through HTTP proxy (TLS tunnel)", () => {
|
||||
expect(messages).toContain("hello via tls tunnel");
|
||||
gc();
|
||||
});
|
||||
|
||||
test("server-initiated ping survives through TLS tunnel proxy", async () => {
|
||||
// Regression test: sendPong checked socket.isClosed() on the detached tcp
|
||||
// field instead of using hasTCP(). For wss:// through HTTP proxy, the
|
||||
// WebSocket uses initWithTunnel which sets tcp = detached (all I/O goes
|
||||
// through proxy_tunnel). Detached sockets return true for isClosed(), so
|
||||
// sendPong would immediately dispatch a 1006 close instead of sending the
|
||||
// pong through the tunnel.
|
||||
using pingServer = Bun.serve({
|
||||
port: 0,
|
||||
tls: {
|
||||
key: tlsCerts.key,
|
||||
cert: tlsCerts.cert,
|
||||
},
|
||||
fetch(req, server) {
|
||||
if (server.upgrade(req)) return;
|
||||
return new Response("Expected WebSocket", { status: 400 });
|
||||
},
|
||||
websocket: {
|
||||
message(ws, message) {
|
||||
if (String(message) === "ready") {
|
||||
// Send a ping after the client confirms it's connected.
|
||||
// On the buggy code path, this triggers sendPong on the detached
|
||||
// socket → dispatchAbruptClose → 1006.
|
||||
ws.ping();
|
||||
// Follow up with a text message. If the client receives this,
|
||||
// the connection survived the ping/pong exchange.
|
||||
ws.send("after-ping");
|
||||
}
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { promise, resolve, reject } = Promise.withResolvers<void>();
|
||||
|
||||
const ws = new WebSocket(`wss://127.0.0.1:${pingServer.port}`, {
|
||||
proxy: `http://127.0.0.1:${proxyPort}`,
|
||||
tls: { rejectUnauthorized: false },
|
||||
});
|
||||
|
||||
ws.onopen = () => {
|
||||
ws.send("ready");
|
||||
};
|
||||
|
||||
ws.onmessage = event => {
|
||||
if (String(event.data) === "after-ping") {
|
||||
ws.close(1000);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = event => {
|
||||
if (event.code === 1000) {
|
||||
resolve();
|
||||
} else {
|
||||
reject(new Error(`Unexpected close code: ${event.code}`));
|
||||
}
|
||||
};
|
||||
|
||||
ws.onerror = event => {
|
||||
reject(event);
|
||||
};
|
||||
|
||||
await promise;
|
||||
gc();
|
||||
});
|
||||
});
|
||||
|
||||
describe("WebSocket through HTTPS proxy (TLS proxy)", () => {
|
||||
|
||||
113
test/regression/issue/25707.test.ts
Normal file
113
test/regression/issue/25707.test.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDir } from "harness";
|
||||
|
||||
// https://github.com/oven-sh/bun/issues/25707
|
||||
// Dynamic import() of non-existent node: modules inside CJS files should not
|
||||
// fail at transpile/require time. They should be deferred to runtime so that
|
||||
// try/catch can handle the error gracefully.
|
||||
|
||||
test("require() of CJS file containing dynamic import of non-existent node: module does not fail at load time", async () => {
|
||||
using dir = tempDir("issue-25707", {
|
||||
// Simulates turbopack-generated chunks: a CJS module with a factory function
|
||||
// containing import("node:sqlite") inside a try/catch that is never called
|
||||
// during require().
|
||||
"chunk.js": `
|
||||
module.exports = [
|
||||
function factory(exports) {
|
||||
async function detect(e) {
|
||||
if ("createSession" in e) {
|
||||
let c;
|
||||
try {
|
||||
({DatabaseSync: c} = await import("node:sqlite"))
|
||||
} catch(a) {
|
||||
if (null !== a && "object" == typeof a && "code" in a && "ERR_UNKNOWN_BUILTIN_MODULE" !== a.code)
|
||||
throw a;
|
||||
}
|
||||
}
|
||||
}
|
||||
exports.detect = detect;
|
||||
}
|
||||
];
|
||||
`,
|
||||
"main.js": `
|
||||
// This require() should not fail even though chunk.js contains import("node:sqlite")
|
||||
const factories = require("./chunk.js");
|
||||
console.log("loaded " + factories.length + " factories");
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "main.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout.trim()).toBe("loaded 1 factories");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("require() of CJS file with bare dynamic import of non-existent node: module does not fail at load time", async () => {
|
||||
// The dynamic import is NOT inside a try/catch, but it's still a dynamic import
|
||||
// that should only be resolved at runtime, not at transpile time
|
||||
using dir = tempDir("issue-25707-bare", {
|
||||
"lib.js": `
|
||||
module.exports = async function() {
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
return DatabaseSync;
|
||||
};
|
||||
`,
|
||||
"main.js": `
|
||||
const fn = require("./lib.js");
|
||||
console.log("loaded");
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "main.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout.trim()).toBe("loaded");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
|
||||
test("dynamic import of non-existent node: module in CJS rejects at runtime with correct error", async () => {
|
||||
using dir = tempDir("issue-25707-runtime", {
|
||||
"lib.js": `
|
||||
module.exports = async function() {
|
||||
try {
|
||||
const { DatabaseSync } = await import("node:sqlite");
|
||||
return "resolved";
|
||||
} catch (e) {
|
||||
return "caught: " + e.code;
|
||||
}
|
||||
};
|
||||
`,
|
||||
"main.js": `
|
||||
const fn = require("./lib.js");
|
||||
fn().then(result => console.log(result));
|
||||
`,
|
||||
});
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "main.js"],
|
||||
cwd: String(dir),
|
||||
env: bunEnv,
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
|
||||
|
||||
expect(stdout.trim()).toBe("caught: ERR_UNKNOWN_BUILTIN_MODULE");
|
||||
expect(exitCode).toBe(0);
|
||||
});
|
||||
77
test/regression/issue/26851.test.ts
Normal file
77
test/regression/issue/26851.test.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { bunEnv, bunExe, tempDir } from "harness";
|
||||
import { join } from "path";
|
||||
|
||||
test("--bail writes JUnit reporter outfile", async () => {
|
||||
using dir = tempDir("bail-junit", {
|
||||
"fail.test.ts": `
|
||||
import { test, expect } from "bun:test";
|
||||
test("failing test", () => { expect(1).toBe(2); });
|
||||
`,
|
||||
});
|
||||
|
||||
const outfile = join(String(dir), "results.xml");
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test", "--bail", "--reporter=junit", `--reporter-outfile=${outfile}`, "fail.test.ts"],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const exitCode = await proc.exited;
|
||||
|
||||
// The test should fail and bail
|
||||
expect(exitCode).not.toBe(0);
|
||||
|
||||
// The JUnit report file should still be written despite bail
|
||||
const file = Bun.file(outfile);
|
||||
expect(await file.exists()).toBe(true);
|
||||
|
||||
const xml = await file.text();
|
||||
expect(xml).toContain("<?xml");
|
||||
expect(xml).toContain("<testsuites");
|
||||
expect(xml).toContain("</testsuites>");
|
||||
expect(xml).toContain("failing test");
|
||||
});
|
||||
|
||||
test("--bail writes JUnit reporter outfile with multiple files", async () => {
|
||||
using dir = tempDir("bail-junit-multi", {
|
||||
"a_pass.test.ts": `
|
||||
import { test, expect } from "bun:test";
|
||||
test("passing test", () => { expect(1).toBe(1); });
|
||||
`,
|
||||
"b_fail.test.ts": `
|
||||
import { test, expect } from "bun:test";
|
||||
test("another failing test", () => { expect(1).toBe(2); });
|
||||
`,
|
||||
});
|
||||
|
||||
const outfile = join(String(dir), "results.xml");
|
||||
|
||||
await using proc = Bun.spawn({
|
||||
cmd: [bunExe(), "test", "--bail", "--reporter=junit", `--reporter-outfile=${outfile}`],
|
||||
env: bunEnv,
|
||||
cwd: String(dir),
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
|
||||
const exitCode = await proc.exited;
|
||||
|
||||
// The test should fail and bail
|
||||
expect(exitCode).not.toBe(0);
|
||||
|
||||
// The JUnit report file should still be written despite bail
|
||||
const file = Bun.file(outfile);
|
||||
expect(await file.exists()).toBe(true);
|
||||
|
||||
const xml = await file.text();
|
||||
expect(xml).toContain("<?xml");
|
||||
expect(xml).toContain("<testsuites");
|
||||
expect(xml).toContain("</testsuites>");
|
||||
// Both the passing and failing tests should be recorded
|
||||
expect(xml).toContain("passing test");
|
||||
expect(xml).toContain("another failing test");
|
||||
});
|
||||
134
test/regression/issue/27010.test.ts
Normal file
134
test/regression/issue/27010.test.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import http from "node:http";
|
||||
|
||||
// Regression test for https://github.com/oven-sh/bun/issues/27010
|
||||
// HTTP requests hanging on Windows when making multiple concurrent large
|
||||
// streaming GET requests using the Node.js http module.
|
||||
|
||||
test("multiple concurrent streaming HTTP requests complete without hanging", async () => {
|
||||
const TWO_MIB = 2 * 1024 * 1024;
|
||||
const CHUNK_SIZE = 64 * 1024;
|
||||
const ZERO_CHUNK = new Uint8Array(CHUNK_SIZE);
|
||||
|
||||
// Start a streaming HTTP server
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req) {
|
||||
const url = new URL(req.url);
|
||||
if (url.pathname !== "/stream") {
|
||||
return new Response("OK");
|
||||
}
|
||||
let sent = 0;
|
||||
const stream = new ReadableStream({
|
||||
pull: async controller => {
|
||||
const remaining = TWO_MIB - sent;
|
||||
if (remaining <= 0) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
// Small delay to simulate realistic streaming
|
||||
await new Promise(resolve => setTimeout(resolve, 1));
|
||||
const n = Math.min(remaining, ZERO_CHUNK.byteLength);
|
||||
controller.enqueue(n === ZERO_CHUNK.byteLength ? ZERO_CHUNK : ZERO_CHUNK.subarray(0, n));
|
||||
sent += n;
|
||||
},
|
||||
});
|
||||
return new Response(stream, {
|
||||
headers: {
|
||||
"content-type": "application/octet-stream",
|
||||
"cache-control": "no-store",
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const url = `http://localhost:${server.port}/stream`;
|
||||
const NUM_WORKERS = 3;
|
||||
const NUM_ITERATIONS = 2;
|
||||
|
||||
function downloadOnce(): Promise<number> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const req = http.get(url, res => {
|
||||
let total = 0;
|
||||
res.on("data", (chunk: Buffer) => {
|
||||
total += chunk.length;
|
||||
});
|
||||
res.on("end", () => {
|
||||
resolve(total);
|
||||
});
|
||||
res.on("error", (err: Error) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
req.on("error", (err: Error) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function workerLoop(): Promise<void> {
|
||||
for (let iter = 0; iter < NUM_ITERATIONS; iter++) {
|
||||
const bytes = await downloadOnce();
|
||||
expect(bytes).toBe(TWO_MIB);
|
||||
}
|
||||
}
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
for (let w = 0; w < NUM_WORKERS; w++) {
|
||||
promises.push(workerLoop());
|
||||
}
|
||||
await Promise.all(promises);
|
||||
}, 30_000);
|
||||
|
||||
test("streaming HTTP response delivers all chunks via node:http", async () => {
|
||||
const TOTAL_SIZE = 512 * 1024; // 512KB
|
||||
const CHUNK_SIZE = 16 * 1024; // 16KB chunks
|
||||
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
fetch(req) {
|
||||
let sent = 0;
|
||||
const stream = new ReadableStream({
|
||||
pull: async controller => {
|
||||
if (sent >= TOTAL_SIZE) {
|
||||
controller.close();
|
||||
return;
|
||||
}
|
||||
// Create a chunk with incrementing byte pattern for verification
|
||||
const n = Math.min(TOTAL_SIZE - sent, CHUNK_SIZE);
|
||||
const chunk = new Uint8Array(n);
|
||||
chunk.fill((sent / CHUNK_SIZE) & 0xff);
|
||||
controller.enqueue(chunk);
|
||||
sent += n;
|
||||
},
|
||||
});
|
||||
return new Response(stream);
|
||||
},
|
||||
});
|
||||
|
||||
const url = `http://localhost:${server.port}/`;
|
||||
|
||||
const result = await new Promise<Buffer>((resolve, reject) => {
|
||||
const req = http.get(url, res => {
|
||||
const chunks: Buffer[] = [];
|
||||
res.on("data", (chunk: Buffer) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
res.on("end", () => {
|
||||
resolve(Buffer.concat(chunks));
|
||||
});
|
||||
res.on("error", reject);
|
||||
});
|
||||
req.on("error", reject);
|
||||
});
|
||||
|
||||
expect(result.length).toBe(TOTAL_SIZE);
|
||||
|
||||
// Verify chunk pattern integrity
|
||||
for (let i = 0; i < TOTAL_SIZE / CHUNK_SIZE; i++) {
|
||||
const offset = i * CHUNK_SIZE;
|
||||
const expectedByte = i & 0xff;
|
||||
expect(result[offset]).toBe(expectedByte);
|
||||
expect(result[offset + CHUNK_SIZE - 1]).toBe(expectedByte);
|
||||
}
|
||||
});
|
||||
187
test/regression/issue/postgres-null-byte-injection.test.ts
Normal file
187
test/regression/issue/postgres-null-byte-injection.test.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import { SQL } from "bun";
|
||||
import { expect, test } from "bun:test";
|
||||
import net from "net";
|
||||
|
||||
test("postgres connection rejects null bytes in username", async () => {
|
||||
let serverReceivedData = false;
|
||||
|
||||
const server = net.createServer(socket => {
|
||||
serverReceivedData = true;
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
await new Promise<void>(r => server.listen(0, "127.0.0.1", () => r()));
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
const sql = new SQL({
|
||||
hostname: "127.0.0.1",
|
||||
port,
|
||||
username: "alice\x00search_path\x00evil_schema,public",
|
||||
database: "testdb",
|
||||
max: 1,
|
||||
idleTimeout: 1,
|
||||
connectionTimeout: 2,
|
||||
});
|
||||
|
||||
await sql`SELECT 1`;
|
||||
expect.unreachable();
|
||||
} catch (e: any) {
|
||||
expect(e.message).toContain("null bytes");
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
|
||||
// The server should never have received any data because the null byte
|
||||
// should be rejected before the connection is established.
|
||||
expect(serverReceivedData).toBe(false);
|
||||
});
|
||||
|
||||
test("postgres connection rejects null bytes in database", async () => {
|
||||
let serverReceivedData = false;
|
||||
|
||||
const server = net.createServer(socket => {
|
||||
serverReceivedData = true;
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
await new Promise<void>(r => server.listen(0, "127.0.0.1", () => r()));
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
const sql = new SQL({
|
||||
hostname: "127.0.0.1",
|
||||
port,
|
||||
username: "alice",
|
||||
database: "testdb\x00search_path\x00evil_schema,public",
|
||||
max: 1,
|
||||
idleTimeout: 1,
|
||||
connectionTimeout: 2,
|
||||
});
|
||||
|
||||
await sql`SELECT 1`;
|
||||
expect.unreachable();
|
||||
} catch (e: any) {
|
||||
expect(e.message).toContain("null bytes");
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
|
||||
expect(serverReceivedData).toBe(false);
|
||||
});
|
||||
|
||||
test("postgres connection rejects null bytes in password", async () => {
|
||||
let serverReceivedData = false;
|
||||
|
||||
const server = net.createServer(socket => {
|
||||
serverReceivedData = true;
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
await new Promise<void>(r => server.listen(0, "127.0.0.1", () => r()));
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
const sql = new SQL({
|
||||
hostname: "127.0.0.1",
|
||||
port,
|
||||
username: "alice",
|
||||
password: "pass\x00search_path\x00evil_schema",
|
||||
database: "testdb",
|
||||
max: 1,
|
||||
idleTimeout: 1,
|
||||
connectionTimeout: 2,
|
||||
});
|
||||
|
||||
await sql`SELECT 1`;
|
||||
expect.unreachable();
|
||||
} catch (e: any) {
|
||||
expect(e.message).toContain("null bytes");
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
|
||||
expect(serverReceivedData).toBe(false);
|
||||
});
|
||||
|
||||
test("postgres connection does not use truncated path with null bytes", async () => {
|
||||
// The JS layer's fs.existsSync() rejects paths containing null bytes,
|
||||
// so the path is dropped before reaching the native layer. Verify that a
|
||||
// path with null bytes doesn't silently connect via a truncated path.
|
||||
let serverReceivedData = false;
|
||||
|
||||
const server = net.createServer(socket => {
|
||||
serverReceivedData = true;
|
||||
socket.destroy();
|
||||
});
|
||||
|
||||
await new Promise<void>(r => server.listen(0, "127.0.0.1", () => r()));
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
const sql = new SQL({
|
||||
hostname: "127.0.0.1",
|
||||
port,
|
||||
username: "alice",
|
||||
database: "testdb",
|
||||
path: "/tmp\x00injected",
|
||||
max: 1,
|
||||
idleTimeout: 1,
|
||||
connectionTimeout: 2,
|
||||
});
|
||||
|
||||
await sql`SELECT 1`;
|
||||
} catch {
|
||||
// Expected to fail
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
|
||||
// The path had null bytes so it should have been dropped by the JS layer,
|
||||
// falling back to TCP where it hits our mock server (not a truncated Unix socket).
|
||||
expect(serverReceivedData).toBe(true);
|
||||
});
|
||||
|
||||
test("postgres connection works with normal parameters (no null bytes)", async () => {
|
||||
// Verify that normal connections without null bytes still work.
|
||||
// Use a mock server that sends an auth error so we can verify the
|
||||
// startup message is sent correctly.
|
||||
let receivedData = false;
|
||||
|
||||
const server = net.createServer(socket => {
|
||||
socket.once("data", () => {
|
||||
receivedData = true;
|
||||
const errMsg = Buffer.from("SFATAL\0VFATAL\0C28000\0Mauthentication failed\0\0");
|
||||
const len = errMsg.length + 4;
|
||||
const header = Buffer.alloc(5);
|
||||
header.write("E", 0);
|
||||
header.writeInt32BE(len, 1);
|
||||
socket.write(Buffer.concat([header, errMsg]));
|
||||
socket.destroy();
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>(r => server.listen(0, "127.0.0.1", () => r()));
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
const sql = new SQL({
|
||||
hostname: "127.0.0.1",
|
||||
port,
|
||||
username: "alice",
|
||||
database: "testdb",
|
||||
max: 1,
|
||||
idleTimeout: 1,
|
||||
connectionTimeout: 2,
|
||||
});
|
||||
|
||||
await sql`SELECT 1`;
|
||||
} catch {
|
||||
// Expected - mock server sends auth error
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
|
||||
// Normal parameters should connect fine - the server should receive data
|
||||
expect(receivedData).toBe(true);
|
||||
});
|
||||
148
test/regression/issue/s3-header-injection.test.ts
Normal file
148
test/regression/issue/s3-header-injection.test.ts
Normal file
@@ -0,0 +1,148 @@
|
||||
import { S3Client } from "bun";
|
||||
import { describe, expect, test } from "bun:test";
|
||||
|
||||
// Test that CRLF characters in S3 options are rejected to prevent header injection.
|
||||
// See: HTTP Header Injection via S3 Content-Disposition Value
|
||||
|
||||
describe("S3 header injection prevention", () => {
|
||||
test("contentDisposition with CRLF should throw", () => {
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("OK", { status: 200 });
|
||||
},
|
||||
});
|
||||
|
||||
const client = new S3Client({
|
||||
accessKeyId: "test-key",
|
||||
secretAccessKey: "test-secret",
|
||||
endpoint: server.url.href,
|
||||
bucket: "test-bucket",
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
client.write("test-file.txt", "Hello", {
|
||||
contentDisposition: 'attachment; filename="evil"\r\nX-Injected: value',
|
||||
}),
|
||||
).toThrow(/CR\/LF/);
|
||||
});
|
||||
|
||||
test("contentEncoding with CRLF should throw", () => {
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("OK", { status: 200 });
|
||||
},
|
||||
});
|
||||
|
||||
const client = new S3Client({
|
||||
accessKeyId: "test-key",
|
||||
secretAccessKey: "test-secret",
|
||||
endpoint: server.url.href,
|
||||
bucket: "test-bucket",
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
client.write("test-file.txt", "Hello", {
|
||||
contentEncoding: "gzip\r\nX-Injected: value",
|
||||
}),
|
||||
).toThrow(/CR\/LF/);
|
||||
});
|
||||
|
||||
test("type (content-type) with CRLF should throw", () => {
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("OK", { status: 200 });
|
||||
},
|
||||
});
|
||||
|
||||
const client = new S3Client({
|
||||
accessKeyId: "test-key",
|
||||
secretAccessKey: "test-secret",
|
||||
endpoint: server.url.href,
|
||||
bucket: "test-bucket",
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
client.write("test-file.txt", "Hello", {
|
||||
type: "text/plain\r\nX-Injected: value",
|
||||
}),
|
||||
).toThrow(/CR\/LF/);
|
||||
});
|
||||
|
||||
test("contentDisposition with only CR should throw", () => {
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("OK", { status: 200 });
|
||||
},
|
||||
});
|
||||
|
||||
const client = new S3Client({
|
||||
accessKeyId: "test-key",
|
||||
secretAccessKey: "test-secret",
|
||||
endpoint: server.url.href,
|
||||
bucket: "test-bucket",
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
client.write("test-file.txt", "Hello", {
|
||||
contentDisposition: "attachment\rinjected",
|
||||
}),
|
||||
).toThrow(/CR\/LF/);
|
||||
});
|
||||
|
||||
test("contentDisposition with only LF should throw", () => {
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
fetch() {
|
||||
return new Response("OK", { status: 200 });
|
||||
},
|
||||
});
|
||||
|
||||
const client = new S3Client({
|
||||
accessKeyId: "test-key",
|
||||
secretAccessKey: "test-secret",
|
||||
endpoint: server.url.href,
|
||||
bucket: "test-bucket",
|
||||
});
|
||||
|
||||
expect(() =>
|
||||
client.write("test-file.txt", "Hello", {
|
||||
contentDisposition: "attachment\ninjected",
|
||||
}),
|
||||
).toThrow(/CR\/LF/);
|
||||
});
|
||||
|
||||
test("valid contentDisposition without CRLF should not throw", async () => {
|
||||
const { promise: requestReceived, resolve: onRequestReceived } = Promise.withResolvers<Headers>();
|
||||
|
||||
using server = Bun.serve({
|
||||
port: 0,
|
||||
async fetch(req) {
|
||||
onRequestReceived(req.headers);
|
||||
return new Response("OK", { status: 200 });
|
||||
},
|
||||
});
|
||||
|
||||
const client = new S3Client({
|
||||
accessKeyId: "test-key",
|
||||
secretAccessKey: "test-secret",
|
||||
endpoint: server.url.href,
|
||||
bucket: "test-bucket",
|
||||
});
|
||||
|
||||
// Valid content-disposition values should not throw synchronously.
|
||||
// The write may eventually fail because the mock server doesn't speak S3 protocol,
|
||||
// but the option parsing should succeed and a request should be initiated.
|
||||
expect(() =>
|
||||
client.write("test-file.txt", "Hello", {
|
||||
contentDisposition: 'attachment; filename="report.pdf"',
|
||||
}),
|
||||
).not.toThrow();
|
||||
|
||||
const receivedHeaders = await requestReceived;
|
||||
expect(receivedHeaders.get("content-disposition")).toBe('attachment; filename="report.pdf"');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user