Compare commits

...

24 Commits

Author SHA1 Message Date
Claude Bot
7c0e4fb1f0 test: use await using for RAII cleanup, assert stdout before exitCode
Address review feedback:
- Use `await using` for Bun.spawn to ensure process cleanup on failures
- Swap assertion order in FileSink test: stdout before exitCode per
  project convention (more useful error messages on failure)

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-18 13:45:19 +00:00
Claude Bot
0bdcdfd9ad fix(win): defer onCloseSource when pipe write is pending to prevent use-after-free (#27097)
On Windows, when close() was called on a pipe writer while an async
uv_write was still in-flight, onCloseSource() was called synchronously.
This notified the parent immediately, which could free the writer's
StreamBuffer resources. When the pending write callback later fired
via uv__process_pipe_write_req, it accessed the freed StreamBuffer
memory, causing a segfault at address 0xFFFFFFFFFFFFFFFF.

The fix defers onCloseSource() when there's a pending async write.
Instead of notifying the parent immediately, the notification happens
in onWriteComplete after the pending write callback completes safely.
This ensures the StreamBuffer memory remains valid throughout the
write callback's execution.

Affects both WindowsBufferedWriter and WindowsStreamingWriter in
BaseWindowsPipeWriter.close().

Co-Authored-By: Claude <noreply@anthropic.com>
2026-02-18 13:26:59 +00:00
Alan Stott
7848648e09 fix: clean up raw TCP socket on TLS upgrade close (#26766)
## Summary

Fixes #12117, #24118, #25948

When a TCP socket is upgraded to TLS via `tls.connect({ socket })`,
`upgradeTLS()` creates **two** `TLSSocket` structs — a TLS wrapper and a
raw TCP wrapper. Both are `markActive()`'d and `ref()`'d. On close, uws
fires `onClose` through the **TLS context only**, so the TLS wrapper is
properly cleaned up, but the raw TCP wrapper's `onClose` never fires.
Its `has_pending_activity` stays `true` forever and its `ref_count` is
never decremented, **leaking one raw `TLSSocket` per upgrade cycle**.

This affects any code using the `tls.connect({ socket })` "starttls"
pattern:
- **MongoDB Node.js driver** — SDAM heartbeat connections cycle TLS
upgrades every ~10s, causing unbounded memory growth in production
- **mysql2** TLS upgrade path
- Any custom starttls implementation

### The fix

Adds a `defer` block in `NewWrappedHandler(true).onClose` that cleans up
the raw TCP socket when the TLS socket closes:

```zig
defer {
    if (!this.tcp.socket.isDetached()) {
        this.tcp.socket.detach();
        this.tcp.has_pending_activity.store(false, .release);
        this.tcp.deref();
    }
}
```

- **`isDetached()` guard** — skips cleanup if the raw socket was already
closed through another code path (e.g., JS-side `handle.close()`)
- **`socket.detach()`** — marks `InternalSocket` as `.detached` so
`isClosed()` returns `true` safely (the underlying `us_socket_t` is
freed when uws closes the TLS context)
- **`has_pending_activity.store(false)`** — allows JSC GC to collect the
raw socket's JS wrapper
- **`deref()`** — balances the `ref()` from `upgradeTLS`; the remaining
ref is the implicit one from JSC (`ref_count.init() == 1`). When GC
later calls `finalize()` → `deref()`, ref_count hits 0 and `deinit()`
runs the full cleanup chain (markInactive, handlers, poll_ref,
socket_context)

`markInactive()` is intentionally **not** called in the defer — it must
run inside `deinit()` to avoid double-freeing the handlers struct.

### Why Node.js doesn't have this bug

Node.js implements TLS upgrades purely in JavaScript/C++ with OpenSSL,
where the TLS wrapper takes ownership of the underlying socket. There is
no separate "raw socket wrapper" that needs independent cleanup.

## Test Results

### Regression test
```
$ bun test test/js/node/tls/node-tls-upgrade-leak.test.ts
 1 pass, 0 fail
```
Creates 20 TCP→TLS upgrade cycles, closes all connections, runs GC,
asserts `TLSSocket` count stays below 10.

### Existing TLS test suite (all passing)
```
node-tls-upgrade.test.ts      1 pass
node-tls-connect.test.ts     24 pass, 1 skip
node-tls-server.test.ts      21 pass
node-tls-cert.test.ts        25 pass, 3 todo
renegotiation.test.ts          6 pass
```

### MongoDB TLS scenario (patched Bun, 4 minutes)
```
Baseline: RSS=282.4 MB | Heap Used=26.4 MB
Check #4:  RSS=166.7 MB | Heap Used=24.2 MB — No TLSSocket growth. RSS DECREASED.
```

## Test plan

- [x] New regression test passes (`node-tls-upgrade-leak.test.ts`)
- [x] All existing TLS tests pass (upgrade, connect, server, cert,
renegotiation)
- [x] MongoDB TLS scenario shows zero `TLSSocket` accumulation
- [x] Node.js control confirms leak is Bun-specific
- [ ] CI passes

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-16 23:42:57 -08:00
Jarred Sumner
379daff22d Fix test failure (#27073)
### What does this PR do?

### How did you verify your code works?

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-16 22:52:43 -08:00
robobun
5b0db0191e fix(bundler): copy non-JS/CSS files referenced as URL assets in HTML (#27039)
## Summary

- Fix `<link rel="manifest" href="./manifest.json" />` (and similar
non-JS/CSS URL assets) resulting in 404s when using `Bun.build` with
HTML entrypoints
- The HTML scanner correctly identifies these as `ImportKind.url`
imports, but the bundler was assigning the extension-based loader (e.g.
`.json`) which parses the file instead of copying it as a static asset
- Force the `.file` loader for `ImportKind.url` imports when the
resolved loader wouldn't `shouldCopyForBundling()` and isn't JS/CSS/HTML
(which have their own handling)

## Test plan

- [x] Added `html/manifest-json` test: verifies manifest.json is copied
as hashed asset and HTML href is rewritten
- [x] Added `html/xml-asset` test: verifies `.webmanifest` files are
also handled correctly
- [x] All 20 HTML bundler tests pass (`bun bd test
test/bundler/bundler_html.test.ts`)
- [x] New tests fail on system bun (`USE_SYSTEM_BUN=1`) confirming they
validate the fix

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

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-16 18:06:25 -08:00
robobun
9ef9ac1db1 fix(http): fix setHeaders throwing ERR_HTTP_HEADERS_SENT on new requests (#27050)
## Summary

- Fix `OutgoingMessage.setHeaders()` incorrectly throwing
`ERR_HTTP_HEADERS_SENT` on brand new `ClientRequest` instances
- The guard condition `this[headerStateSymbol] !==
NodeHTTPHeaderState.none` failed when `headerStateSymbol` was
`undefined` (since `ClientRequest` doesn't call the `OutgoingMessage`
constructor), and was also stricter than Node.js which only checks
`this._header`
- Align the check with the working `setHeader()` (singular) method: only
throw when `_header` is set or `headerStateSymbol` equals `sent`

Closes #27049

## Test plan
- [x] New regression test `test/regression/issue/27049.test.ts` covers:
  - `ClientRequest.setHeaders()` with `Headers` object
  - `ClientRequest.setHeaders()` with `Map`
  - `ServerResponse.setHeaders()` before headers are sent
- [x] Test fails with system bun (`USE_SYSTEM_BUN=1`)
- [x] Test passes with debug build (`bun bd test`)
- [x] Existing header-related tests in `node-http.test.ts` still pass

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

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-16 18:04:35 -08:00
robobun
f5d98191b7 fix(install): store and verify SHA-512 integrity hash for GitHub tarball dependencies (#27019)
## Summary

- Compute SHA-512 hash of GitHub tarball bytes during extraction and
store in `bun.lock`
- Verify the hash on subsequent installs when re-downloading, rejecting
tampered tarballs
- Automatically upgrade old lockfiles without integrity by computing and
persisting the hash
- Maintain backward compatibility with old lockfile format (no integrity
field)

Fixes GHSA-pfwx-36v6-832x

## Lockfile format change

```
Before: ["pkg@github:user/repo#ref", {}, "resolved-commit"]
After:  ["pkg@github:user/repo#ref", {}, "resolved-commit", "sha512-..."]
```

The integrity field is optional for backward compatibility. Old
lockfiles are automatically upgraded when the tarball is re-downloaded.

## Test plan

- [x] Fresh install stores SHA-512 integrity hash in lockfile
- [x] Re-install with matching hash succeeds
- [x] Re-install with mismatched hash rejects the tarball
- [x] Old lockfile without integrity is auto-upgraded with hash on
re-download
- [x] Cache hits still work without re-downloading
- [x] Existing GitHub dependency tests pass (10/10)
- [x] Existing git resolution snapshot test passes
- [x] Yarn migration snapshot tests pass

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

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-16 17:53:32 -08:00
robobun
83bca9bea8 docs: fix plugin API documentation to reflect onStart/onEnd support (#27068)
## Summary
- Fixes the esbuild migration guide (`docs/bundler/esbuild.mdx`) which
incorrectly stated that `onStart`, `onEnd`, `onDispose`, and `resolve`
were all unimplemented. `onStart` and `onEnd` **are** implemented — only
`onDispose` and `resolve` remain unimplemented.
- Adds missing `onEnd()` documentation section to both
`docs/bundler/plugins.mdx` and `docs/runtime/plugins.mdx`, including
type signature, description, and usage examples.
- Adds `onEnd` to the type reference overview and lifecycle hooks list
in both plugin docs.

Fixes #27083

## Test plan
- Documentation-only change — no code changes.
- Verified the `onEnd` implementation exists in
`src/js/builtins/BundlerPlugin.ts` and matches the documented API.
- Verified `onStart` implementation exists and is fully functional.

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

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-16 17:44:09 -08:00
robobun
7794cc866e fix(http): preserve explicit Content-Length header with streaming request body (#27062)
## Summary

- When `http.ClientRequest.write()` was called more than once (streaming
data in chunks), Bun was stripping the explicitly-set `Content-Length`
header and switching to `Transfer-Encoding: chunked`. Node.js preserves
`Content-Length` in all cases when it's explicitly set by the user.
- This caused real-world failures (e.g. Vercel CLI file uploads) where
large binary files streamed via multiple `write()` calls had their
Content-Length stripped, causing server-side "invalid file size" errors.
- The fix preserves the user's explicit `Content-Length` for streaming
request bodies and skips chunked transfer encoding framing when
`Content-Length` is set.

Closes #27061
Closes #26976

## Changes

- **`src/http.zig`**: When a streaming request body has an explicit
`Content-Length` header set by the user, use that instead of adding
`Transfer-Encoding: chunked`. Added
`is_streaming_request_body_with_content_length` flag to track this.
- **`src/bun.js/webcore/fetch/FetchTasklet.zig`**: Skip chunked transfer
encoding framing (`writeRequestData`) and the chunked terminator
(`writeEndRequest`) when the request has an explicit `Content-Length`.
- **`test/regression/issue/27061.test.ts`**: Regression test covering
multiple write patterns (2x write, write+end(data), 3x write) plus
validation that chunked encoding is still used when no `Content-Length`
is set.

## Test plan

- [x] New regression test passes with `bun bd test
test/regression/issue/27061.test.ts`
- [x] Test fails with `USE_SYSTEM_BUN=1` (confirms the bug exists in
current release)
- [x] Existing `test/js/node/http/` tests pass (no regressions)
- [x] Fetch file upload tests pass

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

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-16 17:43:23 -08:00
robobun
70b354aa04 fix(bundler): include CSS in all HTML entrypoints sharing a deduplicated CSS chunk (#27040) 2026-02-15 03:36:06 -08:00
Jarred Sumner
9d5a800c3d fix(napi,timers): callback scope (#27026) 2026-02-15 03:33:48 -08:00
Jarred Sumner
77ca318336 Reduce the number of closures in generated bundler code (#27022)
### Problem

The bundler's `__toESM` helper creates a new getter-wrapped proxy object
every time a CJS
module is imported. In a large app, a popular dependency like React can
be imported 600+
times — each creating a fresh object with ~44 getter properties. This
produces ~27K
unnecessary `GetterSetter` objects, ~25K closures, and ~25K
`JSLexicalEnvironment` scope
objects at startup.

Additionally, `__export` and `__exportValue` use `var`-scoped loop
variables captured by
setter closures, meaning all setters incorrectly reference the last
iterated key (a latent
  bug).

### Changes

1. **`__toESM`: add WeakMap cache** — deduplicate repeated wrappings of
the same CJS
module. Two caches (one per `isNodeMode` value) to handle both import
modes correctly.
2. **Replace closures with `.bind()`** — `() => obj[key]` becomes
`__accessProp.bind(obj,
key)`. BoundFunction is cheaper than Function + JSLexicalEnvironment,
and frees the for-in
  `JSPropertyNameEnumerator` from the closure scope.
3. **Fix var-scoping bug in `__export`/`__exportValue`** — setter
closures captured a
shared `var name` and would all modify the last iterated key. `.bind()`
eagerly captures
the correct key per iteration.
4. **`__toCommonJS`: `.map()` → `for..of`** — eliminates throwaway array
allocation.
5. **`__reExport`: single `getOwnPropertyNames` call** — was calling it
twice when
`secondTarget` was provided.

### Impact (measured on a ~23MB single-bundle app with 600+ React
imports)

| Metric | Before | After | Delta |
|--------|--------|-------|-------|
| **Total objects** | 745,985 | 664,001 | **-81,984 (-11%)** |
| **Heap size** | 115 MB | 111 MB | **-4 MB** |
| GetterSetter | 34,625 | 13,428 | -21,197 (-61%) |
| Function | 221,302 | 197,024 | -24,278 (-11%) |
| JSLexicalEnvironment | 70,101 | 44,633 | -25,468 (-36%) |
| Structure | 40,254 | 39,762 | -492 |
2026-02-15 00:36:57 -08:00
Jarred Sumner
337a9f7f2b fix(module): prevent crash when resolving bun:main before entry_po… (#27027)
…int.generate()

`ServerEntryPoint.source` defaults to `undefined`, and accessing its
`.contents` or `.path.text` fields before `generate()` has been called
causes a segfault. This happens when `bun:main` is resolved in contexts
where `entry_point.generate()` is skipped (HTML entry points) or never
called (test runner).

Add a `generated` flag to `ServerEntryPoint` and guard both access
sites:
- `getHardcodedModule()` in ModuleLoader.zig (returns null instead of
crashing)
- `_resolve()` in VirtualMachine.zig (falls through to normal
resolution)

### What does this PR do?

### How did you verify your code works?

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-14 12:11:41 -08:00
robobun
38f41dccdf fix(crypto): fix segfault in X509Certificate.issuerCertificate getter (#27025) 2026-02-14 05:58:41 -08:00
robobun
883e43c371 update(crypto): update root certificates to NSS 3.119; eliminate external HTTPS in tests (#27020) 2026-02-14 02:47:13 -08:00
Dylan Conway
cd17934207 fix(stripANSI): handle SIMD false positives for non-ANSI control chars (#27021)
The SIMD fast path in `findEscapeCharacter` matches bytes in `0x10-0x1F`
and `0x90-0x9F`, but `consumeANSI` only handles actual ANSI escape
introducers (`0x1b`, `0x9b`, `0x9d`, `0x90`, `0x98`, `0x9e`, `0x9f`).
For non-ANSI bytes like `0x16` (SYN), `consumeANSI` returns the same
pointer it received, causing an infinite loop in release builds and an
assertion failure in debug builds.

When a SIMD false positive is detected (`consumeANSI` returns the same
pointer), the byte is preserved in the output and scanning advances past
it.

Fixes #27014
2026-02-14 02:01:25 -08:00
robobun
f6d4ff6779 fix(http): validate statusMessage for CRLF to prevent HTTP response splitting (#26949)
## Summary

- Fixes HTTP response splitting vulnerability where `res.statusMessage`
could contain CRLF characters that were written directly to the socket,
allowing injection of arbitrary HTTP headers and response body
- Adds native-layer validation in `NodeHTTPResponse.zig` `writeHead()`
to reject status messages containing control characters (matching
Node.js's `checkInvalidHeaderChar` behavior)
- The `writeHead(code, msg)` API already validated via JS-side
`checkInvalidHeaderChar`, but direct property assignment
(`res.statusMessage = userInput`) followed by `res.end()` or
`res.write()` bypassed all validation

## Test plan

- [x] Verified vulnerability is reproducible: attacker can inject
`Set-Cookie` headers via `res.statusMessage = "OK\r\nSet-Cookie:
admin=true"`
- [x] Verified fix throws `ERR_INVALID_CHAR` TypeError when CRLF is
present in status message
- [x] Added 4 new tests covering: property assignment + `res.end()`,
property assignment + `res.write()`, explicit `writeHead()` rejection,
and valid status message passthrough
- [x] Tests fail with `USE_SYSTEM_BUN=1` (confirming they detect the
vulnerability) and pass with `bun bd test`
- [x] Existing Node.js compat test
`test-http-status-reason-invalid-chars.js` still passes
- [x] All 14 HTTP security tests pass
- [x] Full `node-http.test.ts` suite passes (77 pass, 1 pre-existing
skip, 1 pre-existing proxy failure)

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

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-14 00:25:07 -08:00
robobun
2c173529fa fix(test): eliminate external HTTPS fetches and renew expired TLS certs (#27013)
## Summary

- **HTMLRewriter test**: Replaced `fetch("https://www.example.com/")`
with a local HTTP content server, eliminating dependency on external
HTTPS and system CA certificates
- **HTTPS agent test**: Replaced `https.request("https://example.com/")`
with a local TLS server using self-signed certs from harness
- **Expired certs**: Regenerated self-signed certificates in
`test/js/bun/http/fixtures/` and `test/regression/fixtures/` (were
expired since Sep 2024, now valid until 2036)

## Root cause

Tests fetching external HTTPS URLs (`https://example.com`,
`https://www.example.com`) fail on CI environments (Alpine Linux,
Windows) that lack system CA certificate bundles, producing
`UNABLE_TO_GET_ISSUER_CERT_LOCALLY` errors. This affected:
- `test/js/workerd/html-rewriter.test.js` (HTMLRewriter: async
replacement using fetch + Bun.serve)
-
`test/js/bun/test/parallel/test-https-should-work-when-sending-request-with-agent-false.ts`

## Test plan

- [x] `bun bd test test/js/workerd/html-rewriter.test.js` - 41 pass, 0
fail
- [x] `bun bd test test/js/workerd/html-rewriter.test.js -t "async
replacement using fetch"` - 1 pass
- [x] `bun bd
test/js/bun/test/parallel/test-https-should-work-when-sending-request-with-agent-false.ts`
- exits 0
- [x] `bun bd test test/js/bun/http/serve-listen.test.ts` - 27 pass, 0
fail (uses renewed certs)

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

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-13 20:45:06 -08:00
Dylan Conway
243fa45bec fix(shell): prevent use-after-free in builtin OutputTask callbacks inside $() (#26987)
## Summary

- Fixes use-after-free (ASAN use-after-poison) when shell builtins
(`ls`, `touch`, `mkdir`, `cp`) run inside command substitution `$(...)`
and encounter errors (e.g., permission denied)
- The `output_waiting` and `output_done` counters in the builtin exec
state went out of sync because `output_waiting` was only incremented for
async IO operations, while `output_done` was always incremented
- In command substitution, stdout is `.pipe` (sync) and stderr is `.fd`
(async), so a single OutputTask completing both writes could satisfy the
done condition while another OutputTask's async stderr write was still
pending in the IOWriter

The fix moves `output_waiting += 1` before the `needsIO()` check in all
four affected builtins so it's always incremented, matching
`output_done`.

## Test plan

- [x] `echo $(ls /tmp/*)` — no ASAN errors (previously crashed with
use-after-poison)
- [x] `echo $(touch /root/a /root/b ...)` — no ASAN errors
- [x] `echo $(mkdir /root/a /root/b ...)` — no ASAN errors
- [x] `ls /nonexistent` — still reports error and exits with code 1
- [x] `echo $(ls /tmp)` — still captures output correctly
- [x] Existing shell test suite: 292 pass, 52 fail (pre-existing), 83
todo — no regressions

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:40:49 -08:00
Dylan Conway
c19dcb3181 fix(shell): reject non-finite seq args and handle empty condexpr args (#26993)
## Summary
- **`seq inf` / `seq nan` / `seq -inf` hang**: `std.fmt.parseFloat`
accepts non-finite float values like `inf`, `nan`, `-inf`, but the loop
`while (current <= this._end)` never terminates when `_end` is infinity.
Now rejects non-finite values after parsing.
- **`[[ -d "" ]]` out-of-bounds panic**: Empty string expansion produces
no args in the args list, but `doStat()` unconditionally accesses
`args.items[0]`. Now checks `args.items.len == 0` before calling
`doStat()` and returns exit code 1 (path doesn't exist).

## Test plan
- [x] `seq inf`, `seq nan`, `seq -inf` return exit code 1 with "invalid
argument" instead of hanging
- [x] `[[ -d "" ]]` and `[[ -f "" ]]` return exit code 1 instead of
panicking
- [x] `seq 3` still works normally (produces 1, 2, 3)
- [x] `[[ -d /tmp ]]`, `[[ -f /etc/hostname ]]` still work correctly
- [x] Tests pass with `bun bd test`, seq tests fail with
`USE_SYSTEM_BUN=1`

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

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-13 18:01:40 -08:00
Jarred Sumner
c57af9df38 Fix websocket proxy ping crash (#26995)
### What does this PR do?

The `sendPong` fix alone wasn't sufficient. The bug only manifests with
**wss:// through HTTP proxy** (not ws://), because only that path uses
`initWithTunnel` with a detached socket.

**Two bugs were found and fixed:**

1. **`sendPong`/`sendCloseWithBody` socket checks**
(`src/http/websocket_client.zig`): Replaced `socket.isClosed() or
socket.isShutdown()` with `!this.hasTCP()` as originally proposed. Also
guarded `shutdownRead()` against detached sockets.

2. **Spurious 1006 during clean close** (`src/http/websocket_client.zig`
+ `WebSocketProxyTunnel.zig`): When `sendCloseWithBody` calls
`clearData()`, it shuts down the proxy tunnel. The tunnel's `onClose`
callback was calling `ws.fail(ErrorCode.ended)` which dispatched a 1006
abrupt close, overriding the clean 1000 close already in progress. Fixed
by adding `tunnel.clearConnectedWebSocket()` before `tunnel.shutdown()`
so the callback is a no-op.

### How did you verify your code works?

- `USE_SYSTEM_BUN=1`: Fails with `Unexpected close code: 1006`
- `bun bd test`: Passes with clean 1000 close
- Full proxy test suite: 25 pass, 4 skip, 0 fail
- Related fragmented/close tests: all passing

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-13 14:49:40 -08:00
SUZUKI Sosuke
3debd0a2d2 perf(structuredClone): remove unnecessary zero-fill for Int32 and Double array fast paths (#26989)
## What

Replace two-step `Vector<uint8_t>` zero-initialization + `memcpy` with
direct `std::span` copy construction in the `structuredClone` Int32 and
Double array fast paths.

## Why

The previous code allocated a zero-filled buffer and then immediately
overwrote it with `memcpy`. By constructing the `Vector` directly from a
`std::span`, we eliminate the redundant zero-fill and reduce the
operation to a single copy.

### Before
```cpp
Vector<uint8_t> buffer(byteSize, 0);
memcpy(buffer.mutableSpan().data(), data, byteSize);
```

### After
```cpp
Vector<uint8_t> buffer(std::span<const uint8_t> { reinterpret_cast<const uint8_t*>(data), byteSize });
```

## Test

`bun bd test test/js/web/structured-clone-fastpath.test.ts` — 91/92 pass
(1 pre-existing flaky memory test unrelated to this change).

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-12 23:50:54 -08:00
robobun
7afead629c fix(linker): defer dynamic import() of unknown node: modules to runtime (#26981)
## Summary

- Defer resolution of dynamic `import()` of unknown `node:` modules
(like `node:sqlite`) to runtime instead of failing at transpile time
- Fix use-after-poison in `addResolveError` by always duping `line_text`
from the source so Location data outlives the arena

Fixes #25707

## Root cause

When a CJS file is `require()`d, Bun's linker eagerly resolves all
import records, including dynamic `import()` expressions. For unknown
`node:` prefixed modules, `whenModuleNotFound` was only deferring
`.require` and `.require_resolve` to runtime — `.dynamic` imports fell
through to the error path, causing the entire `require()` to fail.

This broke Next.js builds with turbopack + `cacheComponents: true` +
Better Auth, because Kysely's dialect detection code uses
`import("node:sqlite")` inside a try/catch that gracefully handles the
module not being available.

Additionally, when the resolve error was generated, the
`Location.line_text` was a slice into arena-allocated source contents.
The arena is reset before `processFetchLog` processes the error, causing
a use-after-poison when `Location.clone` tries to dupe the freed memory.

## Test plan

- [x] New regression test in `test/regression/issue/25707.test.ts` with
3 cases:
- CJS require of file with `import("node:sqlite")` inside try/catch
(turbopack pattern)
  - CJS require of file with bare `import("node:sqlite")` (no try/catch)
  - Runtime error produces correct `ERR_UNKNOWN_BUILTIN_MODULE` code
- [x] Test fails with `USE_SYSTEM_BUN=1` (system bun v1.3.9)
- [x] Test passes with `bun bd test`
- [x] No ASAN use-after-poison crash on debug build


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

---------

Co-authored-by: Claude Bot <claude-bot@bun.sh>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Jarred Sumner <jarred@jarredsumner.com>
2026-02-12 14:47:31 -08:00
Jarred Sumner
9a72bbfae2 Update CLAUDE.md 2026-02-12 14:25:05 -08:00
73 changed files with 2487 additions and 1013 deletions

View File

@@ -198,13 +198,16 @@ const myPlugin: BunPlugin = {
};
```
The builder object provides some methods for hooking into parts of the bundling process. Bun implements `onResolve` and `onLoad`; it does not yet implement the esbuild hooks `onStart`, `onEnd`, and `onDispose`, and `resolve` utilities. `initialOptions` is partially implemented, being read-only and only having a subset of esbuild's options; use `config` (same thing but with Bun's `BuildConfig` format) instead.
The builder object provides some methods for hooking into parts of the bundling process. Bun implements `onStart`, `onEnd`, `onResolve`, and `onLoad`. It does not yet implement the esbuild hooks `onDispose` and `resolve`. `initialOptions` is partially implemented, being read-only and only having a subset of esbuild's options; use `config` (same thing but with Bun's `BuildConfig` format) instead.
```ts title="myPlugin.ts" icon="/icons/typescript.svg"
import type { BunPlugin } from "bun";
const myPlugin: BunPlugin = {
name: "my-plugin",
setup(builder) {
builder.onStart(() => {
/* called when the bundle starts */
});
builder.onResolve(
{
/* onResolve.options */
@@ -225,6 +228,9 @@ const myPlugin: BunPlugin = {
};
},
);
builder.onEnd(result => {
/* called when the bundle is complete */
});
},
};
```

View File

@@ -15,6 +15,7 @@ Plugins can register callbacks to be run at various points in the lifecycle of a
- `onResolve()`: Run before a module is resolved
- `onLoad()`: Run before a module is loaded
- `onBeforeParse()`: Run zero-copy native addons in the parser thread before a file is parsed
- `onEnd()`: Run after the bundle is complete
## Reference
@@ -39,6 +40,7 @@ type PluginBuilder = {
exports?: Record<string, any>;
},
) => void;
onEnd(callback: (result: BuildOutput) => void | Promise<void>): void;
config: BuildConfig;
};
@@ -423,3 +425,53 @@ This lifecycle callback is run immediately before a file is parsed by Bun's bund
As input, it receives the file's contents and can optionally return new source code.
<Info>This callback can be called from any thread and so the napi module implementation must be thread-safe.</Info>
### onEnd
```ts
onEnd(callback: (result: BuildOutput) => void | Promise<void>): void;
```
Registers a callback to be run after the bundle is complete. The callback receives the [`BuildOutput`](/docs/bundler#outputs) object containing the build results, including output files and any build messages.
```ts title="index.ts" icon="/icons/typescript.svg"
const result = await Bun.build({
entrypoints: ["./app.ts"],
outdir: "./dist",
plugins: [
{
name: "onEnd example",
setup(build) {
build.onEnd(result => {
console.log(`Build completed with ${result.outputs.length} files`);
for (const log of result.logs) {
console.log(log);
}
});
},
},
],
});
```
The callback can return a `Promise`. The build output promise from `Bun.build()` will not resolve until all `onEnd()` callbacks have completed.
```ts title="index.ts" icon="/icons/typescript.svg"
const result = await Bun.build({
entrypoints: ["./app.ts"],
outdir: "./dist",
plugins: [
{
name: "Upload to S3",
setup(build) {
build.onEnd(async result => {
if (!result.success) return;
for (const output of result.outputs) {
await uploadToS3(output);
}
});
},
},
],
});
```

View File

@@ -23196,557 +23196,6 @@ CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_TRUSTED_DELEGATOR
CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE
#
# Certificate "CommScope Public Trust ECC Root-01"
#
# Issuer: CN=CommScope Public Trust ECC Root-01,O=CommScope,C=US
# Serial Number:43:70:82:77:cf:4d:5d:34:f1:ca:ae:32:2f:37:f7:f4:7f:75:a0:9e
# Subject: CN=CommScope Public Trust ECC Root-01,O=CommScope,C=US
# Not Valid Before: Wed Apr 28 17:35:43 2021
# Not Valid After : Sat Apr 28 17:35:42 2046
# Fingerprint (SHA-256): 11:43:7C:DA:7B:B4:5E:41:36:5F:45:B3:9A:38:98:6B:0D:E0:0D:EF:34:8E:0C:7B:B0:87:36:33:80:0B:C3:8B
# Fingerprint (SHA1): 07:86:C0:D8:DD:8E:C0:80:98:06:98:D0:58:7A:EF:DE:A6:CC:A2:5D
CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE
CKA_TOKEN CK_BBOOL CK_TRUE
CKA_PRIVATE CK_BBOOL CK_FALSE
CKA_MODIFIABLE CK_BBOOL CK_FALSE
CKA_LABEL UTF8 "CommScope Public Trust ECC Root-01"
CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509
CKA_SUBJECT MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\105\103\103\040\122\157\157\164\055\060\061
END
CKA_ID UTF8 "0"
CKA_ISSUER MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\105\103\103\040\122\157\157\164\055\060\061
END
CKA_SERIAL_NUMBER MULTILINE_OCTAL
\002\024\103\160\202\167\317\115\135\064\361\312\256\062\057\067
\367\364\177\165\240\236
END
CKA_VALUE MULTILINE_OCTAL
\060\202\002\035\060\202\001\243\240\003\002\001\002\002\024\103
\160\202\167\317\115\135\064\361\312\256\062\057\067\367\364\177
\165\240\236\060\012\006\010\052\206\110\316\075\004\003\003\060
\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061\022
\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143\157
\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157\155
\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124\162
\165\163\164\040\105\103\103\040\122\157\157\164\055\060\061\060
\036\027\015\062\061\060\064\062\070\061\067\063\065\064\063\132
\027\015\064\066\060\064\062\070\061\067\063\065\064\062\132\060
\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061\022
\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143\157
\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157\155
\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124\162
\165\163\164\040\105\103\103\040\122\157\157\164\055\060\061\060
\166\060\020\006\007\052\206\110\316\075\002\001\006\005\053\201
\004\000\042\003\142\000\004\113\066\351\256\127\136\250\160\327
\320\217\164\142\167\303\136\172\252\345\266\242\361\170\375\002
\176\127\335\221\171\234\154\271\122\210\124\274\057\004\276\270
\315\366\020\321\051\354\265\320\240\303\360\211\160\031\273\121
\145\305\103\234\303\233\143\235\040\203\076\006\013\246\102\104
\205\021\247\112\072\055\351\326\150\057\110\116\123\053\007\077
\115\275\271\254\167\071\127\243\102\060\100\060\017\006\003\125
\035\023\001\001\377\004\005\060\003\001\001\377\060\016\006\003
\125\035\017\001\001\377\004\004\003\002\001\006\060\035\006\003
\125\035\016\004\026\004\024\216\007\142\300\120\335\306\031\006
\000\106\164\004\367\363\256\175\165\115\060\060\012\006\010\052
\206\110\316\075\004\003\003\003\150\000\060\145\002\061\000\234
\063\337\101\343\043\250\102\066\046\227\065\134\173\353\333\113
\370\252\213\163\125\025\134\254\170\051\017\272\041\330\304\240
\330\321\003\335\155\321\071\075\304\223\140\322\343\162\262\002
\060\174\305\176\210\323\120\365\036\045\350\372\116\165\346\130
\226\244\065\137\033\145\352\141\232\160\043\265\015\243\233\222
\122\157\151\240\214\215\112\320\356\213\016\313\107\216\320\215
\021
END
CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE
CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE
CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE
# Trust for "CommScope Public Trust ECC Root-01"
# Issuer: CN=CommScope Public Trust ECC Root-01,O=CommScope,C=US
# Serial Number:43:70:82:77:cf:4d:5d:34:f1:ca:ae:32:2f:37:f7:f4:7f:75:a0:9e
# Subject: CN=CommScope Public Trust ECC Root-01,O=CommScope,C=US
# Not Valid Before: Wed Apr 28 17:35:43 2021
# Not Valid After : Sat Apr 28 17:35:42 2046
# Fingerprint (SHA-256): 11:43:7C:DA:7B:B4:5E:41:36:5F:45:B3:9A:38:98:6B:0D:E0:0D:EF:34:8E:0C:7B:B0:87:36:33:80:0B:C3:8B
# Fingerprint (SHA1): 07:86:C0:D8:DD:8E:C0:80:98:06:98:D0:58:7A:EF:DE:A6:CC:A2:5D
CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST
CKA_TOKEN CK_BBOOL CK_TRUE
CKA_PRIVATE CK_BBOOL CK_FALSE
CKA_MODIFIABLE CK_BBOOL CK_FALSE
CKA_LABEL UTF8 "CommScope Public Trust ECC Root-01"
CKA_CERT_SHA1_HASH MULTILINE_OCTAL
\007\206\300\330\335\216\300\200\230\006\230\320\130\172\357\336
\246\314\242\135
END
CKA_CERT_MD5_HASH MULTILINE_OCTAL
\072\100\247\374\003\214\234\070\171\057\072\242\154\266\012\026
END
CKA_ISSUER MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\105\103\103\040\122\157\157\164\055\060\061
END
CKA_SERIAL_NUMBER MULTILINE_OCTAL
\002\024\103\160\202\167\317\115\135\064\361\312\256\062\057\067
\367\364\177\165\240\236
END
CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR
CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE
#
# Certificate "CommScope Public Trust ECC Root-02"
#
# Issuer: CN=CommScope Public Trust ECC Root-02,O=CommScope,C=US
# Serial Number:28:fd:99:60:41:47:a6:01:3a:ca:14:7b:1f:ef:f9:68:08:83:5d:7d
# Subject: CN=CommScope Public Trust ECC Root-02,O=CommScope,C=US
# Not Valid Before: Wed Apr 28 17:44:54 2021
# Not Valid After : Sat Apr 28 17:44:53 2046
# Fingerprint (SHA-256): 2F:FB:7F:81:3B:BB:B3:C8:9A:B4:E8:16:2D:0F:16:D7:15:09:A8:30:CC:9D:73:C2:62:E5:14:08:75:D1:AD:4A
# Fingerprint (SHA1): 3C:3F:EF:57:0F:FE:65:93:86:9E:A0:FE:B0:F6:ED:8E:D1:13:C7:E5
CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE
CKA_TOKEN CK_BBOOL CK_TRUE
CKA_PRIVATE CK_BBOOL CK_FALSE
CKA_MODIFIABLE CK_BBOOL CK_FALSE
CKA_LABEL UTF8 "CommScope Public Trust ECC Root-02"
CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509
CKA_SUBJECT MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\105\103\103\040\122\157\157\164\055\060\062
END
CKA_ID UTF8 "0"
CKA_ISSUER MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\105\103\103\040\122\157\157\164\055\060\062
END
CKA_SERIAL_NUMBER MULTILINE_OCTAL
\002\024\050\375\231\140\101\107\246\001\072\312\024\173\037\357
\371\150\010\203\135\175
END
CKA_VALUE MULTILINE_OCTAL
\060\202\002\034\060\202\001\243\240\003\002\001\002\002\024\050
\375\231\140\101\107\246\001\072\312\024\173\037\357\371\150\010
\203\135\175\060\012\006\010\052\206\110\316\075\004\003\003\060
\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061\022
\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143\157
\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157\155
\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124\162
\165\163\164\040\105\103\103\040\122\157\157\164\055\060\062\060
\036\027\015\062\061\060\064\062\070\061\067\064\064\065\064\132
\027\015\064\066\060\064\062\070\061\067\064\064\065\063\132\060
\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061\022
\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143\157
\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157\155
\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124\162
\165\163\164\040\105\103\103\040\122\157\157\164\055\060\062\060
\166\060\020\006\007\052\206\110\316\075\002\001\006\005\053\201
\004\000\042\003\142\000\004\170\060\201\350\143\036\345\353\161
\121\017\367\007\007\312\071\231\174\116\325\017\314\060\060\013
\217\146\223\076\317\275\305\206\275\371\261\267\264\076\264\007
\310\363\226\061\363\355\244\117\370\243\116\215\051\025\130\270
\325\157\177\356\154\042\265\260\257\110\105\012\275\250\111\224
\277\204\103\260\333\204\112\003\043\031\147\152\157\301\156\274
\006\071\067\321\210\042\367\243\102\060\100\060\017\006\003\125
\035\023\001\001\377\004\005\060\003\001\001\377\060\016\006\003
\125\035\017\001\001\377\004\004\003\002\001\006\060\035\006\003
\125\035\016\004\026\004\024\346\030\165\377\357\140\336\204\244
\365\106\307\336\112\125\343\062\066\171\365\060\012\006\010\052
\206\110\316\075\004\003\003\003\147\000\060\144\002\060\046\163
\111\172\266\253\346\111\364\175\122\077\324\101\004\256\200\103
\203\145\165\271\205\200\070\073\326\157\344\223\206\253\217\347
\211\310\177\233\176\153\012\022\125\141\252\021\340\171\002\060
\167\350\061\161\254\074\161\003\326\204\046\036\024\270\363\073
\073\336\355\131\374\153\114\060\177\131\316\105\351\163\140\025
\232\114\360\346\136\045\042\025\155\302\207\131\320\262\216\152
END
CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE
CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE
CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE
# Trust for "CommScope Public Trust ECC Root-02"
# Issuer: CN=CommScope Public Trust ECC Root-02,O=CommScope,C=US
# Serial Number:28:fd:99:60:41:47:a6:01:3a:ca:14:7b:1f:ef:f9:68:08:83:5d:7d
# Subject: CN=CommScope Public Trust ECC Root-02,O=CommScope,C=US
# Not Valid Before: Wed Apr 28 17:44:54 2021
# Not Valid After : Sat Apr 28 17:44:53 2046
# Fingerprint (SHA-256): 2F:FB:7F:81:3B:BB:B3:C8:9A:B4:E8:16:2D:0F:16:D7:15:09:A8:30:CC:9D:73:C2:62:E5:14:08:75:D1:AD:4A
# Fingerprint (SHA1): 3C:3F:EF:57:0F:FE:65:93:86:9E:A0:FE:B0:F6:ED:8E:D1:13:C7:E5
CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST
CKA_TOKEN CK_BBOOL CK_TRUE
CKA_PRIVATE CK_BBOOL CK_FALSE
CKA_MODIFIABLE CK_BBOOL CK_FALSE
CKA_LABEL UTF8 "CommScope Public Trust ECC Root-02"
CKA_CERT_SHA1_HASH MULTILINE_OCTAL
\074\077\357\127\017\376\145\223\206\236\240\376\260\366\355\216
\321\023\307\345
END
CKA_CERT_MD5_HASH MULTILINE_OCTAL
\131\260\104\325\145\115\270\134\125\031\222\002\266\321\224\262
END
CKA_ISSUER MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\105\103\103\040\122\157\157\164\055\060\062
END
CKA_SERIAL_NUMBER MULTILINE_OCTAL
\002\024\050\375\231\140\101\107\246\001\072\312\024\173\037\357
\371\150\010\203\135\175
END
CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR
CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE
#
# Certificate "CommScope Public Trust RSA Root-01"
#
# Issuer: CN=CommScope Public Trust RSA Root-01,O=CommScope,C=US
# Serial Number:3e:03:49:81:75:16:74:31:8e:4c:ab:d5:c5:90:29:96:c5:39:10:dd
# Subject: CN=CommScope Public Trust RSA Root-01,O=CommScope,C=US
# Not Valid Before: Wed Apr 28 16:45:54 2021
# Not Valid After : Sat Apr 28 16:45:53 2046
# Fingerprint (SHA-256): 02:BD:F9:6E:2A:45:DD:9B:F1:8F:C7:E1:DB:DF:21:A0:37:9B:A3:C9:C2:61:03:44:CF:D8:D6:06:FE:C1:ED:81
# Fingerprint (SHA1): 6D:0A:5F:F7:B4:23:06:B4:85:B3:B7:97:64:FC:AC:75:F5:33:F2:93
CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE
CKA_TOKEN CK_BBOOL CK_TRUE
CKA_PRIVATE CK_BBOOL CK_FALSE
CKA_MODIFIABLE CK_BBOOL CK_FALSE
CKA_LABEL UTF8 "CommScope Public Trust RSA Root-01"
CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509
CKA_SUBJECT MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\122\123\101\040\122\157\157\164\055\060\061
END
CKA_ID UTF8 "0"
CKA_ISSUER MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\122\123\101\040\122\157\157\164\055\060\061
END
CKA_SERIAL_NUMBER MULTILINE_OCTAL
\002\024\076\003\111\201\165\026\164\061\216\114\253\325\305\220
\051\226\305\071\020\335
END
CKA_VALUE MULTILINE_OCTAL
\060\202\005\154\060\202\003\124\240\003\002\001\002\002\024\076
\003\111\201\165\026\164\061\216\114\253\325\305\220\051\226\305
\071\020\335\060\015\006\011\052\206\110\206\367\015\001\001\013
\005\000\060\116\061\013\060\011\006\003\125\004\006\023\002\125
\123\061\022\060\020\006\003\125\004\012\014\011\103\157\155\155
\123\143\157\160\145\061\053\060\051\006\003\125\004\003\014\042
\103\157\155\155\123\143\157\160\145\040\120\165\142\154\151\143
\040\124\162\165\163\164\040\122\123\101\040\122\157\157\164\055
\060\061\060\036\027\015\062\061\060\064\062\070\061\066\064\065
\065\064\132\027\015\064\066\060\064\062\070\061\066\064\065\065
\063\132\060\116\061\013\060\011\006\003\125\004\006\023\002\125
\123\061\022\060\020\006\003\125\004\012\014\011\103\157\155\155
\123\143\157\160\145\061\053\060\051\006\003\125\004\003\014\042
\103\157\155\155\123\143\157\160\145\040\120\165\142\154\151\143
\040\124\162\165\163\164\040\122\123\101\040\122\157\157\164\055
\060\061\060\202\002\042\060\015\006\011\052\206\110\206\367\015
\001\001\001\005\000\003\202\002\017\000\060\202\002\012\002\202
\002\001\000\260\110\145\243\015\035\102\343\221\155\235\204\244
\141\226\022\302\355\303\332\043\064\031\166\366\352\375\125\132
\366\125\001\123\017\362\314\214\227\117\271\120\313\263\001\104
\126\226\375\233\050\354\173\164\013\347\102\153\125\316\311\141
\262\350\255\100\074\272\271\101\012\005\117\033\046\205\217\103
\265\100\265\205\321\324\161\334\203\101\363\366\105\307\200\242
\204\120\227\106\316\240\014\304\140\126\004\035\007\133\106\245
\016\262\113\244\016\245\174\356\370\324\142\003\271\223\152\212
\024\270\160\370\056\202\106\070\043\016\164\307\153\101\267\320
\051\243\235\200\260\176\167\223\143\102\373\064\203\073\163\243
\132\041\066\353\107\372\030\027\331\272\146\302\223\244\217\374
\135\244\255\374\120\152\225\254\274\044\063\321\275\210\177\206
\365\365\262\163\052\217\174\257\010\362\032\230\077\251\201\145
\077\301\214\211\305\226\060\232\012\317\364\324\310\064\355\235
\057\274\215\070\206\123\356\227\237\251\262\143\224\027\215\017
\334\146\052\174\122\121\165\313\231\216\350\075\134\277\236\073
\050\215\203\002\017\251\237\162\342\054\053\263\334\146\227\000
\100\320\244\124\216\233\135\173\105\066\046\326\162\103\353\317
\300\352\015\334\316\022\346\175\070\237\005\047\250\227\076\351
\121\306\154\005\050\301\002\017\351\030\155\354\275\234\006\324
\247\111\364\124\005\153\154\060\361\353\003\325\352\075\152\166
\302\313\032\050\111\115\177\144\340\372\053\332\163\203\201\377
\221\003\275\224\273\344\270\216\234\062\143\315\237\273\150\201
\261\204\133\257\066\277\167\356\035\177\367\111\233\122\354\322
\167\132\175\221\235\115\302\071\055\344\272\202\370\157\362\116
\036\017\116\346\077\131\245\043\334\075\207\250\050\130\050\321
\361\033\066\333\117\304\377\341\214\133\162\214\307\046\003\047
\243\071\012\001\252\300\262\061\140\203\042\241\117\022\011\001
\021\257\064\324\317\327\256\142\323\005\007\264\061\165\340\015
\155\127\117\151\207\371\127\251\272\025\366\310\122\155\241\313
\234\037\345\374\170\250\065\232\237\101\024\316\245\264\316\224
\010\034\011\255\126\345\332\266\111\232\112\352\143\030\123\234
\054\056\303\002\003\001\000\001\243\102\060\100\060\017\006\003
\125\035\023\001\001\377\004\005\060\003\001\001\377\060\016\006
\003\125\035\017\001\001\377\004\004\003\002\001\006\060\035\006
\003\125\035\016\004\026\004\024\067\135\246\232\164\062\302\302
\371\307\246\025\020\131\270\344\375\345\270\155\060\015\006\011
\052\206\110\206\367\015\001\001\013\005\000\003\202\002\001\000
\257\247\317\336\377\340\275\102\215\115\345\042\226\337\150\352
\175\115\052\175\320\255\075\026\134\103\347\175\300\206\350\172
\065\143\361\314\201\310\306\013\350\056\122\065\244\246\111\220
\143\121\254\064\254\005\073\127\000\351\323\142\323\331\051\325
\124\276\034\020\221\234\262\155\376\131\375\171\367\352\126\320
\236\150\124\102\217\046\122\342\114\337\057\227\246\057\322\007
\230\250\363\140\135\113\232\130\127\210\357\202\345\372\257\154
\201\113\222\217\100\232\223\106\131\313\137\170\026\261\147\076
\102\013\337\050\331\260\255\230\040\276\103\174\321\136\032\011
\027\044\215\173\135\225\351\253\301\140\253\133\030\144\200\373
\255\340\006\175\035\312\131\270\363\170\051\147\306\126\035\257
\266\265\164\052\166\241\077\373\165\060\237\224\136\073\245\140
\363\313\134\014\342\016\311\140\370\311\037\026\212\046\335\347
\047\177\353\045\246\212\275\270\055\066\020\232\261\130\115\232
\150\117\140\124\345\366\106\023\216\210\254\274\041\102\022\255
\306\112\211\175\233\301\330\055\351\226\003\364\242\164\014\274
\000\035\277\326\067\045\147\264\162\213\257\205\275\352\052\003
\217\314\373\074\104\044\202\342\001\245\013\131\266\064\215\062
\013\022\015\353\047\302\375\101\327\100\074\162\106\051\300\214
\352\272\017\361\006\223\056\367\234\250\364\140\076\243\361\070
\136\216\023\301\263\072\227\207\077\222\312\170\251\034\257\320
\260\033\046\036\276\160\354\172\365\063\230\352\134\377\053\013
\004\116\103\335\143\176\016\247\116\170\003\225\076\324\055\060
\225\021\020\050\056\277\240\002\076\377\136\131\323\005\016\225
\137\123\105\357\153\207\325\110\315\026\246\226\203\341\337\263
\006\363\301\024\333\247\354\034\213\135\220\220\015\162\121\347
\141\371\024\312\257\203\217\277\257\261\012\131\135\334\134\327
\344\226\255\133\140\035\332\256\227\262\071\331\006\365\166\000
\023\370\150\114\041\260\065\304\334\125\262\311\301\101\132\034
\211\300\214\157\164\240\153\063\115\265\001\050\375\255\255\211
\027\073\246\232\204\274\353\214\352\304\161\044\250\272\051\371
\010\262\047\126\065\062\137\352\071\373\061\232\325\031\314\360
END
CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE
CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE
CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE
# Trust for "CommScope Public Trust RSA Root-01"
# Issuer: CN=CommScope Public Trust RSA Root-01,O=CommScope,C=US
# Serial Number:3e:03:49:81:75:16:74:31:8e:4c:ab:d5:c5:90:29:96:c5:39:10:dd
# Subject: CN=CommScope Public Trust RSA Root-01,O=CommScope,C=US
# Not Valid Before: Wed Apr 28 16:45:54 2021
# Not Valid After : Sat Apr 28 16:45:53 2046
# Fingerprint (SHA-256): 02:BD:F9:6E:2A:45:DD:9B:F1:8F:C7:E1:DB:DF:21:A0:37:9B:A3:C9:C2:61:03:44:CF:D8:D6:06:FE:C1:ED:81
# Fingerprint (SHA1): 6D:0A:5F:F7:B4:23:06:B4:85:B3:B7:97:64:FC:AC:75:F5:33:F2:93
CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST
CKA_TOKEN CK_BBOOL CK_TRUE
CKA_PRIVATE CK_BBOOL CK_FALSE
CKA_MODIFIABLE CK_BBOOL CK_FALSE
CKA_LABEL UTF8 "CommScope Public Trust RSA Root-01"
CKA_CERT_SHA1_HASH MULTILINE_OCTAL
\155\012\137\367\264\043\006\264\205\263\267\227\144\374\254\165
\365\063\362\223
END
CKA_CERT_MD5_HASH MULTILINE_OCTAL
\016\264\025\274\207\143\135\135\002\163\324\046\070\150\163\330
END
CKA_ISSUER MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\122\123\101\040\122\157\157\164\055\060\061
END
CKA_SERIAL_NUMBER MULTILINE_OCTAL
\002\024\076\003\111\201\165\026\164\061\216\114\253\325\305\220
\051\226\305\071\020\335
END
CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR
CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE
#
# Certificate "CommScope Public Trust RSA Root-02"
#
# Issuer: CN=CommScope Public Trust RSA Root-02,O=CommScope,C=US
# Serial Number:54:16:bf:3b:7e:39:95:71:8d:d1:aa:00:a5:86:0d:2b:8f:7a:05:4e
# Subject: CN=CommScope Public Trust RSA Root-02,O=CommScope,C=US
# Not Valid Before: Wed Apr 28 17:16:43 2021
# Not Valid After : Sat Apr 28 17:16:42 2046
# Fingerprint (SHA-256): FF:E9:43:D7:93:42:4B:4F:7C:44:0C:1C:3D:64:8D:53:63:F3:4B:82:DC:87:AA:7A:9F:11:8F:C5:DE:E1:01:F1
# Fingerprint (SHA1): EA:B0:E2:52:1B:89:93:4C:11:68:F2:D8:9A:AC:22:4C:A3:8A:57:AE
CKA_CLASS CK_OBJECT_CLASS CKO_CERTIFICATE
CKA_TOKEN CK_BBOOL CK_TRUE
CKA_PRIVATE CK_BBOOL CK_FALSE
CKA_MODIFIABLE CK_BBOOL CK_FALSE
CKA_LABEL UTF8 "CommScope Public Trust RSA Root-02"
CKA_CERTIFICATE_TYPE CK_CERTIFICATE_TYPE CKC_X_509
CKA_SUBJECT MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\122\123\101\040\122\157\157\164\055\060\062
END
CKA_ID UTF8 "0"
CKA_ISSUER MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\122\123\101\040\122\157\157\164\055\060\062
END
CKA_SERIAL_NUMBER MULTILINE_OCTAL
\002\024\124\026\277\073\176\071\225\161\215\321\252\000\245\206
\015\053\217\172\005\116
END
CKA_VALUE MULTILINE_OCTAL
\060\202\005\154\060\202\003\124\240\003\002\001\002\002\024\124
\026\277\073\176\071\225\161\215\321\252\000\245\206\015\053\217
\172\005\116\060\015\006\011\052\206\110\206\367\015\001\001\013
\005\000\060\116\061\013\060\011\006\003\125\004\006\023\002\125
\123\061\022\060\020\006\003\125\004\012\014\011\103\157\155\155
\123\143\157\160\145\061\053\060\051\006\003\125\004\003\014\042
\103\157\155\155\123\143\157\160\145\040\120\165\142\154\151\143
\040\124\162\165\163\164\040\122\123\101\040\122\157\157\164\055
\060\062\060\036\027\015\062\061\060\064\062\070\061\067\061\066
\064\063\132\027\015\064\066\060\064\062\070\061\067\061\066\064
\062\132\060\116\061\013\060\011\006\003\125\004\006\023\002\125
\123\061\022\060\020\006\003\125\004\012\014\011\103\157\155\155
\123\143\157\160\145\061\053\060\051\006\003\125\004\003\014\042
\103\157\155\155\123\143\157\160\145\040\120\165\142\154\151\143
\040\124\162\165\163\164\040\122\123\101\040\122\157\157\164\055
\060\062\060\202\002\042\060\015\006\011\052\206\110\206\367\015
\001\001\001\005\000\003\202\002\017\000\060\202\002\012\002\202
\002\001\000\341\372\016\373\150\000\022\310\115\325\254\042\304
\065\001\073\305\124\345\131\166\143\245\177\353\301\304\152\230
\275\062\215\027\200\353\135\272\321\142\075\045\043\031\065\024
\351\177\211\247\033\142\074\326\120\347\064\225\003\062\261\264
\223\042\075\247\342\261\355\346\173\116\056\207\233\015\063\165
\012\336\252\065\347\176\345\066\230\242\256\045\236\225\263\062
\226\244\053\130\036\357\077\376\142\064\110\121\321\264\215\102
\255\140\332\111\152\225\160\335\322\000\342\314\127\143\002\173
\226\335\111\227\133\222\116\225\323\371\313\051\037\030\112\370
\001\052\322\143\011\156\044\351\211\322\345\307\042\114\334\163
\206\107\000\252\015\210\216\256\205\175\112\351\273\063\117\016
\122\160\235\225\343\174\155\226\133\055\075\137\241\203\106\135
\266\343\045\270\174\247\031\200\034\352\145\103\334\221\171\066
\054\164\174\362\147\006\311\211\311\333\277\332\150\277\043\355
\334\153\255\050\203\171\057\354\070\245\015\067\001\147\047\232
\351\063\331\063\137\067\241\305\360\253\075\372\170\260\347\054
\237\366\076\237\140\340\357\110\351\220\105\036\005\121\170\032
\054\022\054\134\050\254\015\242\043\236\064\217\005\346\242\063
\316\021\167\023\324\016\244\036\102\037\206\315\160\376\331\056
\025\075\035\273\270\362\123\127\333\314\306\164\051\234\030\263
\066\165\070\056\017\124\241\370\222\037\211\226\117\273\324\356
\235\351\073\066\102\265\012\073\052\324\144\171\066\020\341\371
\221\003\053\173\040\124\315\015\031\032\310\101\062\064\321\260
\231\341\220\036\001\100\066\265\267\372\251\345\167\165\244\042
\201\135\260\213\344\047\022\017\124\210\306\333\205\164\346\267
\300\327\246\051\372\333\336\363\223\227\047\004\125\057\012\157
\067\305\075\023\257\012\000\251\054\213\034\201\050\327\357\206
\061\251\256\362\156\270\312\152\054\124\107\330\052\210\056\257
\301\007\020\170\254\021\242\057\102\360\067\305\362\270\126\335
\016\142\055\316\055\126\176\125\362\247\104\366\053\062\364\043
\250\107\350\324\052\001\170\317\152\303\067\250\236\145\322\054
\345\372\272\063\301\006\104\366\346\317\245\015\247\146\010\064
\212\054\363\002\003\001\000\001\243\102\060\100\060\017\006\003
\125\035\023\001\001\377\004\005\060\003\001\001\377\060\016\006
\003\125\035\017\001\001\377\004\004\003\002\001\006\060\035\006
\003\125\035\016\004\026\004\024\107\320\347\261\042\377\235\054
\365\331\127\140\263\261\261\160\225\357\141\172\060\015\006\011
\052\206\110\206\367\015\001\001\013\005\000\003\202\002\001\000
\206\151\261\115\057\351\237\117\042\223\150\216\344\041\231\243
\316\105\123\033\163\104\123\000\201\141\315\061\343\010\272\201
\050\050\172\222\271\266\250\310\103\236\307\023\046\115\302\330
\345\125\234\222\135\120\330\302\053\333\376\346\250\227\317\122
\072\044\303\145\144\134\107\061\243\145\065\023\303\223\271\367
\371\121\227\273\244\360\142\207\305\326\006\323\227\203\040\251
\176\273\266\041\302\245\015\204\000\341\362\047\020\203\272\335
\003\201\325\335\150\303\146\020\310\321\166\264\263\157\051\236
\000\371\302\051\365\261\223\031\122\151\032\054\114\240\213\340
\025\232\061\057\323\210\225\131\156\345\304\263\120\310\024\010
\112\233\213\023\203\261\244\162\262\073\166\063\101\334\334\252
\246\007\157\035\044\022\237\310\166\275\057\331\216\364\054\356
\267\322\070\020\044\066\121\057\343\134\135\201\041\247\332\273
\116\377\346\007\250\376\271\015\047\154\273\160\132\125\172\023
\351\361\052\111\151\307\137\207\127\114\103\171\155\072\145\351
\060\134\101\356\353\167\245\163\022\210\350\277\175\256\345\304
\250\037\015\216\034\155\120\002\117\046\030\103\336\217\125\205
\261\013\067\005\140\311\125\071\022\004\241\052\317\161\026\237
\066\121\111\277\160\073\236\147\234\373\173\171\311\071\034\170
\254\167\221\124\232\270\165\012\201\122\227\343\146\141\153\355
\076\070\036\226\141\125\341\221\124\214\355\214\044\037\201\311
\020\232\163\231\053\026\116\162\000\077\124\033\370\215\272\213
\347\024\326\266\105\117\140\354\226\256\303\057\002\116\135\235
\226\111\162\000\262\253\165\134\017\150\133\035\145\302\137\063
\017\036\017\360\073\206\365\260\116\273\234\367\352\045\005\334
\255\242\233\113\027\001\276\102\337\065\041\035\255\253\256\364
\277\256\037\033\323\342\073\374\263\162\163\034\233\050\220\211
\023\075\035\301\000\107\011\226\232\070\033\335\261\317\015\302
\264\104\363\226\225\316\062\072\217\064\234\340\027\307\136\316
\256\015\333\207\070\345\077\133\375\233\031\341\061\101\172\160
\252\043\153\001\341\105\114\315\224\316\073\236\055\347\210\002
\042\364\156\350\310\354\326\074\363\271\262\327\167\172\254\173
END
CKA_NSS_MOZILLA_CA_POLICY CK_BBOOL CK_TRUE
CKA_NSS_SERVER_DISTRUST_AFTER CK_BBOOL CK_FALSE
CKA_NSS_EMAIL_DISTRUST_AFTER CK_BBOOL CK_FALSE
# Trust for "CommScope Public Trust RSA Root-02"
# Issuer: CN=CommScope Public Trust RSA Root-02,O=CommScope,C=US
# Serial Number:54:16:bf:3b:7e:39:95:71:8d:d1:aa:00:a5:86:0d:2b:8f:7a:05:4e
# Subject: CN=CommScope Public Trust RSA Root-02,O=CommScope,C=US
# Not Valid Before: Wed Apr 28 17:16:43 2021
# Not Valid After : Sat Apr 28 17:16:42 2046
# Fingerprint (SHA-256): FF:E9:43:D7:93:42:4B:4F:7C:44:0C:1C:3D:64:8D:53:63:F3:4B:82:DC:87:AA:7A:9F:11:8F:C5:DE:E1:01:F1
# Fingerprint (SHA1): EA:B0:E2:52:1B:89:93:4C:11:68:F2:D8:9A:AC:22:4C:A3:8A:57:AE
CKA_CLASS CK_OBJECT_CLASS CKO_NSS_TRUST
CKA_TOKEN CK_BBOOL CK_TRUE
CKA_PRIVATE CK_BBOOL CK_FALSE
CKA_MODIFIABLE CK_BBOOL CK_FALSE
CKA_LABEL UTF8 "CommScope Public Trust RSA Root-02"
CKA_CERT_SHA1_HASH MULTILINE_OCTAL
\352\260\342\122\033\211\223\114\021\150\362\330\232\254\042\114
\243\212\127\256
END
CKA_CERT_MD5_HASH MULTILINE_OCTAL
\341\051\371\142\173\166\342\226\155\363\324\327\017\256\037\252
END
CKA_ISSUER MULTILINE_OCTAL
\060\116\061\013\060\011\006\003\125\004\006\023\002\125\123\061
\022\060\020\006\003\125\004\012\014\011\103\157\155\155\123\143
\157\160\145\061\053\060\051\006\003\125\004\003\014\042\103\157
\155\155\123\143\157\160\145\040\120\165\142\154\151\143\040\124
\162\165\163\164\040\122\123\101\040\122\157\157\164\055\060\062
END
CKA_SERIAL_NUMBER MULTILINE_OCTAL
\002\024\124\026\277\073\176\071\225\161\215\321\252\000\245\206
\015\053\217\172\005\116
END
CKA_TRUST_SERVER_AUTH CK_TRUST CKT_NSS_TRUSTED_DELEGATOR
CKA_TRUST_EMAIL_PROTECTION CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_CODE_SIGNING CK_TRUST CKT_NSS_MUST_VERIFY_TRUST
CKA_TRUST_STEP_UP_APPROVED CK_BBOOL CK_FALSE
#
# Certificate "D-Trust SBR Root CA 1 2022"
#

View File

@@ -3182,96 +3182,6 @@ static struct us_cert_string_t root_certs[] = {
"MvHVI5TWWA==\n"
"-----END CERTIFICATE-----",.len=869},
/* CommScope Public Trust ECC Root-01 */
{.str="-----BEGIN CERTIFICATE-----\n"
"MIICHTCCAaOgAwIBAgIUQ3CCd89NXTTxyq4yLzf39H91oJ4wCgYIKoZIzj0EAwMwTjELMAkG\n"
"A1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29tbVNjb3BlIFB1Ymxp\n"
"YyBUcnVzdCBFQ0MgUm9vdC0wMTAeFw0yMTA0MjgxNzM1NDNaFw00NjA0MjgxNzM1NDJaME4x\n"
"CzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21tU2NvcGUxKzApBgNVBAMMIkNvbW1TY29wZSBQ\n"
"dWJsaWMgVHJ1c3QgRUNDIFJvb3QtMDEwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARLNumuV16o\n"
"cNfQj3Rid8NeeqrltqLxeP0CflfdkXmcbLlSiFS8LwS+uM32ENEp7LXQoMPwiXAZu1FlxUOc\n"
"w5tjnSCDPgYLpkJEhRGnSjot6dZoL0hOUysHP029uax3OVejQjBAMA8GA1UdEwEB/wQFMAMB\n"
"Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSOB2LAUN3GGQYARnQE9/OufXVNMDAKBggq\n"
"hkjOPQQDAwNoADBlAjEAnDPfQeMjqEI2Jpc1XHvr20v4qotzVRVcrHgpD7oh2MSg2NED3W3R\n"
"OT3Ek2DS43KyAjB8xX6I01D1HiXo+k515liWpDVfG2XqYZpwI7UNo5uSUm9poIyNStDuiw7L\n"
"R47QjRE=\n"
"-----END CERTIFICATE-----",.len=792},
/* CommScope Public Trust ECC Root-02 */
{.str="-----BEGIN CERTIFICATE-----\n"
"MIICHDCCAaOgAwIBAgIUKP2ZYEFHpgE6yhR7H+/5aAiDXX0wCgYIKoZIzj0EAwMwTjELMAkG\n"
"A1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29tbVNjb3BlIFB1Ymxp\n"
"YyBUcnVzdCBFQ0MgUm9vdC0wMjAeFw0yMTA0MjgxNzQ0NTRaFw00NjA0MjgxNzQ0NTNaME4x\n"
"CzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21tU2NvcGUxKzApBgNVBAMMIkNvbW1TY29wZSBQ\n"
"dWJsaWMgVHJ1c3QgRUNDIFJvb3QtMDIwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAAR4MIHoYx7l\n"
"63FRD/cHB8o5mXxO1Q/MMDALj2aTPs+9xYa9+bG3tD60B8jzljHz7aRP+KNOjSkVWLjVb3/u\n"
"bCK1sK9IRQq9qEmUv4RDsNuESgMjGWdqb8FuvAY5N9GIIvejQjBAMA8GA1UdEwEB/wQFMAMB\n"
"Af8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBTmGHX/72DehKT1RsfeSlXjMjZ59TAKBggq\n"
"hkjOPQQDAwNnADBkAjAmc0l6tqvmSfR9Uj/UQQSugEODZXW5hYA4O9Zv5JOGq4/nich/m35r\n"
"ChJVYaoR4HkCMHfoMXGsPHED1oQmHhS48zs73u1Z/GtMMH9ZzkXpc2AVmkzw5l4lIhVtwodZ\n"
"0LKOag==\n"
"-----END CERTIFICATE-----",.len=792},
/* CommScope Public Trust RSA Root-01 */
{.str="-----BEGIN CERTIFICATE-----\n"
"MIIFbDCCA1SgAwIBAgIUPgNJgXUWdDGOTKvVxZAplsU5EN0wDQYJKoZIhvcNAQELBQAwTjEL\n"
"MAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29tbVNjb3BlIFB1\n"
"YmxpYyBUcnVzdCBSU0EgUm9vdC0wMTAeFw0yMTA0MjgxNjQ1NTRaFw00NjA0MjgxNjQ1NTNa\n"
"ME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21tU2NvcGUxKzApBgNVBAMMIkNvbW1TY29w\n"
"ZSBQdWJsaWMgVHJ1c3QgUlNBIFJvb3QtMDEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\n"
"AoICAQCwSGWjDR1C45FtnYSkYZYSwu3D2iM0GXb26v1VWvZVAVMP8syMl0+5UMuzAURWlv2b\n"
"KOx7dAvnQmtVzslhsuitQDy6uUEKBU8bJoWPQ7VAtYXR1HHcg0Hz9kXHgKKEUJdGzqAMxGBW\n"
"BB0HW0alDrJLpA6lfO741GIDuZNqihS4cPgugkY4Iw50x2tBt9Apo52AsH53k2NC+zSDO3Oj\n"
"WiE260f6GBfZumbCk6SP/F2krfxQapWsvCQz0b2If4b19bJzKo98rwjyGpg/qYFlP8GMicWW\n"
"MJoKz/TUyDTtnS+8jTiGU+6Xn6myY5QXjQ/cZip8UlF1y5mO6D1cv547KI2DAg+pn3LiLCuz\n"
"3GaXAEDQpFSOm117RTYm1nJD68/A6g3czhLmfTifBSeolz7pUcZsBSjBAg/pGG3svZwG1KdJ\n"
"9FQFa2ww8esD1eo9anbCyxooSU1/ZOD6K9pzg4H/kQO9lLvkuI6cMmPNn7togbGEW682v3fu\n"
"HX/3SZtS7NJ3Wn2RnU3COS3kuoL4b/JOHg9O5j9ZpSPcPYeoKFgo0fEbNttPxP/hjFtyjMcm\n"
"AyejOQoBqsCyMWCDIqFPEgkBEa801M/XrmLTBQe0MXXgDW1XT2mH+VepuhX2yFJtocucH+X8\n"
"eKg1mp9BFM6ltM6UCBwJrVbl2rZJmkrqYxhTnCwuwwIDAQABo0IwQDAPBgNVHRMBAf8EBTAD\n"
"AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUN12mmnQywsL5x6YVEFm45P3luG0wDQYJ\n"
"KoZIhvcNAQELBQADggIBAK+nz97/4L1CjU3lIpbfaOp9TSp90K09FlxD533Ahuh6NWPxzIHI\n"
"xgvoLlI1pKZJkGNRrDSsBTtXAOnTYtPZKdVUvhwQkZyybf5Z/Xn36lbQnmhUQo8mUuJM3y+X\n"
"pi/SB5io82BdS5pYV4jvguX6r2yBS5KPQJqTRlnLX3gWsWc+QgvfKNmwrZggvkN80V4aCRck\n"
"jXtdlemrwWCrWxhkgPut4AZ9HcpZuPN4KWfGVh2vtrV0KnahP/t1MJ+UXjulYPPLXAziDslg\n"
"+MkfFoom3ecnf+slpoq9uC02EJqxWE2aaE9gVOX2RhOOiKy8IUISrcZKiX2bwdgt6ZYD9KJ0\n"
"DLwAHb/WNyVntHKLr4W96ioDj8z7PEQkguIBpQtZtjSNMgsSDesnwv1B10A8ckYpwIzqug/x\n"
"BpMu95yo9GA+o/E4Xo4TwbM6l4c/ksp4qRyv0LAbJh6+cOx69TOY6lz/KwsETkPdY34Op054\n"
"A5U+1C0wlREQKC6/oAI+/15Z0wUOlV9TRe9rh9VIzRamloPh37MG88EU26fsHItdkJANclHn\n"
"YfkUyq+Dj7+vsQpZXdxc1+SWrVtgHdqul7I52Qb1dgAT+GhMIbA1xNxVssnBQVocicCMb3Sg\n"
"azNNtQEo/a2tiRc7ppqEvOuM6sRxJKi6KfkIsidWNTJf6jn7MZrVGczw\n"
"-----END CERTIFICATE-----",.len=1935},
/* CommScope Public Trust RSA Root-02 */
{.str="-----BEGIN CERTIFICATE-----\n"
"MIIFbDCCA1SgAwIBAgIUVBa/O345lXGN0aoApYYNK496BU4wDQYJKoZIhvcNAQELBQAwTjEL\n"
"MAkGA1UEBhMCVVMxEjAQBgNVBAoMCUNvbW1TY29wZTErMCkGA1UEAwwiQ29tbVNjb3BlIFB1\n"
"YmxpYyBUcnVzdCBSU0EgUm9vdC0wMjAeFw0yMTA0MjgxNzE2NDNaFw00NjA0MjgxNzE2NDJa\n"
"ME4xCzAJBgNVBAYTAlVTMRIwEAYDVQQKDAlDb21tU2NvcGUxKzApBgNVBAMMIkNvbW1TY29w\n"
"ZSBQdWJsaWMgVHJ1c3QgUlNBIFJvb3QtMDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK\n"
"AoICAQDh+g77aAASyE3VrCLENQE7xVTlWXZjpX/rwcRqmL0yjReA61260WI9JSMZNRTpf4mn\n"
"G2I81lDnNJUDMrG0kyI9p+Kx7eZ7Ti6Hmw0zdQreqjXnfuU2mKKuJZ6VszKWpCtYHu8//mI0\n"
"SFHRtI1CrWDaSWqVcN3SAOLMV2MCe5bdSZdbkk6V0/nLKR8YSvgBKtJjCW4k6YnS5cciTNxz\n"
"hkcAqg2Ijq6FfUrpuzNPDlJwnZXjfG2WWy09X6GDRl224yW4fKcZgBzqZUPckXk2LHR88mcG\n"
"yYnJ27/aaL8j7dxrrSiDeS/sOKUNNwFnJ5rpM9kzXzehxfCrPfp4sOcsn/Y+n2Dg70jpkEUe\n"
"BVF4GiwSLFworA2iI540jwXmojPOEXcT1A6kHkIfhs1w/tkuFT0du7jyU1fbzMZ0KZwYszZ1\n"
"OC4PVKH4kh+Jlk+71O6d6Ts2QrUKOyrUZHk2EOH5kQMreyBUzQ0ZGshBMjTRsJnhkB4BQDa1\n"
"t/qp5Xd1pCKBXbCL5CcSD1SIxtuFdOa3wNemKfrb3vOTlycEVS8KbzfFPROvCgCpLIscgSjX\n"
"74Yxqa7ybrjKaixUR9gqiC6vwQcQeKwRoi9C8DfF8rhW3Q5iLc4tVn5V8qdE9isy9COoR+jU\n"
"KgF4z2rDN6ieZdIs5fq6M8EGRPbmz6UNp2YINIos8wIDAQABo0IwQDAPBgNVHRMBAf8EBTAD\n"
"AQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUR9DnsSL/nSz12Vdgs7GxcJXvYXowDQYJ\n"
"KoZIhvcNAQELBQADggIBAIZpsU0v6Z9PIpNojuQhmaPORVMbc0RTAIFhzTHjCLqBKCh6krm2\n"
"qMhDnscTJk3C2OVVnJJdUNjCK9v+5qiXz1I6JMNlZFxHMaNlNRPDk7n3+VGXu6TwYofF1gbT\n"
"l4MgqX67tiHCpQ2EAOHyJxCDut0DgdXdaMNmEMjRdrSzbymeAPnCKfWxkxlSaRosTKCL4BWa\n"
"MS/TiJVZbuXEs1DIFAhKm4sTg7GkcrI7djNB3NyqpgdvHSQSn8h2vS/ZjvQs7rfSOBAkNlEv\n"
"41xdgSGn2rtO/+YHqP65DSdsu3BaVXoT6fEqSWnHX4dXTEN5bTpl6TBcQe7rd6VzEojov32u\n"
"5cSoHw2OHG1QAk8mGEPej1WFsQs3BWDJVTkSBKEqz3EWnzZRSb9wO55nnPt7eck5HHisd5FU\n"
"mrh1CoFSl+NmYWvtPjgelmFV4ZFUjO2MJB+ByRCac5krFk5yAD9UG/iNuovnFNa2RU9g7Jau\n"
"wy8CTl2dlklyALKrdVwPaFsdZcJfMw8eD/A7hvWwTruc9+olBdytoptLFwG+Qt81IR2tq670\n"
"v64fG9PiO/yzcnMcmyiQiRM9HcEARwmWmjgb3bHPDcK0RPOWlc4yOo80nOAXx17Org3bhzjl\n"
"P1v9mxnhMUF6cKojawHhRUzNlM47ni3niAIi9G7oyOzWPPO5std3eqx7\n"
"-----END CERTIFICATE-----",.len=1935},
/* Telekom Security TLS ECC Root 2020 */
{.str="-----BEGIN CERTIFICATE-----\n"
"MIICQjCCAcmgAwIBAgIQNjqWjMlcsljN0AFdxeVXADAKBggqhkjOPQQDAzBjMQswCQYDVQQG\n"

View File

@@ -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
```

View File

@@ -1140,14 +1140,14 @@ export fn Bun__runVirtualModule(globalObject: *JSGlobalObject, specifier_ptr: *c
fn getHardcodedModule(jsc_vm: *VirtualMachine, specifier: bun.String, hardcoded: HardcodedModule) ?ResolvedSource {
analytics.Features.builtin_modules.insert(hardcoded);
return switch (hardcoded) {
.@"bun:main" => .{
.@"bun:main" => if (jsc_vm.entry_point.generated) .{
.allocator = null,
.source_code = bun.String.cloneUTF8(jsc_vm.entry_point.source.contents),
.specifier = specifier,
.source_url = specifier,
.tag = .esm,
.source_code_needs_deref = true,
},
} else null,
.@"bun:internal-for-testing" => {
if (!Environment.isDebug) {
if (!is_allowed_to_use_internal_testing_apis)

View File

@@ -1616,7 +1616,7 @@ fn _resolve(
if (strings.eqlComptime(std.fs.path.basename(specifier), Runtime.Runtime.Imports.alt_name)) {
ret.path = Runtime.Runtime.Imports.Name;
return;
} else if (strings.eqlComptime(specifier, main_file_name)) {
} else if (strings.eqlComptime(specifier, main_file_name) and jsc_vm.entry_point.generated) {
ret.result = null;
ret.path = jsc_vm.entry_point.source.path.text;
return;

View File

@@ -4,7 +4,7 @@ const TimerObjectInternals = @This();
/// Identifier for this timer that is exposed to JavaScript (by `+timer`)
id: i32 = -1,
interval: u31 = 0,
strong_this: jsc.Strong.Optional = .empty,
this_value: jsc.JSRef = .empty(),
flags: Flags = .{},
/// Used by:
@@ -76,31 +76,41 @@ pub fn runImmediateTask(this: *TimerObjectInternals, vm: *VirtualMachine) bool {
// loop alive other than setImmediates
(!this.flags.is_keeping_event_loop_alive and !vm.isEventLoopAliveExcludingImmediates()))
{
this.setEnableKeepingEventLoopAlive(vm, false);
this.this_value.downgrade();
this.deref();
return false;
}
const timer = this.strong_this.get() orelse {
const timer = this.this_value.tryGet() orelse {
if (Environment.isDebug) {
@panic("TimerObjectInternals.runImmediateTask: this_object is null");
}
this.setEnableKeepingEventLoopAlive(vm, false);
this.deref();
return false;
};
const globalThis = vm.global;
this.strong_this.deinit();
this.this_value.downgrade();
this.eventLoopTimer().state = .FIRED;
this.setEnableKeepingEventLoopAlive(vm, false);
timer.ensureStillAlive();
vm.eventLoop().enter();
const callback = ImmediateObject.js.callbackGetCached(timer).?;
const arguments = ImmediateObject.js.argumentsGetCached(timer).?;
this.ref();
const exception_thrown = this.run(globalThis, timer, callback, arguments, this.asyncID(), vm);
this.deref();
if (this.eventLoopTimer().state == .FIRED) {
this.deref();
}
const exception_thrown = brk: {
this.ref();
defer {
if (this.eventLoopTimer().state == .FIRED) {
this.deref();
}
this.deref();
}
break :brk this.run(globalThis, timer, callback, arguments, this.asyncID(), vm);
};
// --- after this point, the timer is no longer guaranteed to be alive ---
vm.eventLoop().exitMaybeDrainMicrotasks(!exception_thrown) catch return true;
@@ -120,7 +130,13 @@ pub fn fire(this: *TimerObjectInternals, _: *const timespec, vm: *jsc.VirtualMac
this.eventLoopTimer().state = .FIRED;
const globalThis = vm.global;
const this_object = this.strong_this.get().?;
const this_object = this.this_value.tryGet() orelse {
this.setEnableKeepingEventLoopAlive(vm, false);
this.flags.has_cleared_timer = true;
this.this_value.downgrade();
this.deref();
return;
};
const callback: JSValue, const arguments: JSValue, var idle_timeout: JSValue, var repeat: JSValue = switch (kind) {
.setImmediate => .{
@@ -143,7 +159,7 @@ pub fn fire(this: *TimerObjectInternals, _: *const timespec, vm: *jsc.VirtualMac
}
this.setEnableKeepingEventLoopAlive(vm, false);
this.flags.has_cleared_timer = true;
this.strong_this.deinit();
this.this_value.downgrade();
this.deref();
return;
@@ -152,7 +168,7 @@ pub fn fire(this: *TimerObjectInternals, _: *const timespec, vm: *jsc.VirtualMac
var time_before_call: timespec = undefined;
if (kind != .setInterval) {
this.strong_this.clearWithoutDeallocation();
this.this_value.downgrade();
} else {
time_before_call = timespec.msFromNow(.allow_mocked_time, this.interval);
}
@@ -239,7 +255,7 @@ fn convertToInterval(this: *TimerObjectInternals, global: *JSGlobalObject, timer
// https://github.com/nodejs/node/blob/a7cbb904745591c9a9d047a364c2c188e5470047/lib/internal/timers.js#L613
TimeoutObject.js.idleTimeoutSetCached(timer, global, repeat);
this.strong_this.set(global, timer);
this.this_value.setStrong(timer, global);
this.flags.kind = .setInterval;
this.interval = new_interval;
this.reschedule(timer, vm, global);
@@ -297,7 +313,7 @@ pub fn init(
this.reschedule(timer, vm, global);
}
this.strong_this.set(global, timer);
this.this_value.setStrong(timer, global);
}
pub fn doRef(this: *TimerObjectInternals, _: *jsc.JSGlobalObject, this_value: JSValue) JSValue {
@@ -327,7 +343,7 @@ pub fn doRefresh(this: *TimerObjectInternals, globalObject: *jsc.JSGlobalObject,
return this_value;
}
this.strong_this.set(globalObject, this_value);
this.this_value.setStrong(this_value, globalObject);
this.reschedule(this_value, VirtualMachine.get(), globalObject);
return this_value;
@@ -350,12 +366,18 @@ pub fn cancel(this: *TimerObjectInternals, vm: *VirtualMachine) void {
this.setEnableKeepingEventLoopAlive(vm, false);
this.flags.has_cleared_timer = true;
if (this.flags.kind == .setImmediate) return;
if (this.flags.kind == .setImmediate) {
// Release the strong reference so the GC can collect the JS object.
// The immediate task is still in the event loop queue and will be skipped
// by runImmediateTask when it sees has_cleared_timer == true.
this.this_value.downgrade();
return;
}
const was_active = this.eventLoopTimer().state == .ACTIVE;
this.eventLoopTimer().state = .CANCELLED;
this.strong_this.deinit();
this.this_value.downgrade();
if (was_active) {
vm.timer.remove(this.eventLoopTimer());
@@ -442,12 +464,12 @@ pub fn getDestroyed(this: *TimerObjectInternals) bool {
}
pub fn finalize(this: *TimerObjectInternals) void {
this.strong_this.deinit();
this.this_value.finalize();
this.deref();
}
pub fn deinit(this: *TimerObjectInternals) void {
this.strong_this.deinit();
this.this_value.deinit();
const vm = VirtualMachine.get();
const kind = this.flags.kind;

View File

@@ -1707,6 +1707,15 @@ pub fn NewWrappedHandler(comptime tls: bool) type {
pub fn onClose(this: WrappedSocket, socket: Socket, err: c_int, data: ?*anyopaque) bun.JSError!void {
if (comptime tls) {
// Clean up the raw TCP socket from upgradeTLS() — its onClose
// never fires because uws closes through the TLS context only.
defer {
if (!this.tcp.socket.isDetached()) {
this.tcp.socket.detach();
this.tcp.has_pending_activity.store(false, .release);
this.tcp.deref();
}
}
try TLSSocket.onClose(this.tls, socket, err, data);
} else {
try TLSSocket.onClose(this.tcp, socket, err, data);

View File

@@ -468,6 +468,17 @@ pub fn writeHead(this: *NodeHTTPResponse, globalObject: *jsc.JSGlobalObject, cal
return globalObject.ERR(.HTTP_HEADERS_SENT, "Stream already started", .{}).throw();
}
// Validate status message does not contain invalid characters (defense-in-depth
// against HTTP response splitting). Matches Node.js checkInvalidHeaderChar:
// rejects any char not in [\t\x20-\x7e\x80-\xff].
if (status_message_slice.len > 0) {
for (status_message_slice.slice()) |c| {
if (c != '\t' and (c < 0x20 or c == 0x7f)) {
return globalObject.ERR(.INVALID_CHAR, "Invalid character in statusMessage", .{}).throw();
}
}
}
do_it: {
if (status_message_slice.len == 0) {
if (HTTPStatusText.get(@intCast(status_code))) |status_message| {

View File

@@ -560,6 +560,8 @@ JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_infoAccess, (JSGlobalObject * g
return JSValue::encode(jsUndefined());
BUF_MEM* bptr = bio;
if (!bptr)
return JSValue::encode(jsUndefined());
return JSValue::encode(undefinedIfEmpty(jsString(vm, String::fromUTF8(std::span(bptr->data, bptr->length)))));
}
@@ -602,20 +604,10 @@ JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_issuerCertificate, (JSGlobalObj
return {};
}
auto issuerCert = thisObject->view().getIssuer();
if (!issuerCert)
return JSValue::encode(jsUndefined());
auto bio = issuerCert.get();
BUF_MEM* bptr = nullptr;
BIO_get_mem_ptr(bio, &bptr);
std::span<const uint8_t> span(reinterpret_cast<const uint8_t*>(bptr->data), bptr->length);
auto* zigGlobalObject = defaultGlobalObject(globalObject);
auto* structure = zigGlobalObject->m_JSX509CertificateClassStructure.get(zigGlobalObject);
auto jsIssuerCert = JSX509Certificate::create(vm, structure, globalObject, span);
RETURN_IF_EXCEPTION(scope, {});
return JSValue::encode(jsIssuerCert);
// issuerCertificate is only available when the certificate was obtained from
// a TLS connection with a peer certificate chain. For certificates parsed
// directly from PEM/DER data, it is always undefined (matching Node.js behavior).
return JSValue::encode(jsUndefined());
}
JSC_DEFINE_CUSTOM_GETTER(jsX509CertificateGetter_publicKey, (JSGlobalObject * globalObject, EncodedJSValue thisValue, PropertyName))

View File

@@ -42,6 +42,13 @@ static std::optional<WTF::String> stripANSI(const std::span<const Char> input)
// Append everything before the escape sequence
result.append(std::span { start, escPos });
const auto newPos = ANSI::consumeANSI(escPos, end);
if (newPos == escPos) {
// No ANSI found
result.append(std::span { escPos, escPos + 1 });
start = escPos + 1;
continue;
}
ASSERT(newPos > start);
ASSERT(newPos <= end);
foundANSI = true;

View File

@@ -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);
}
}

View File

@@ -1154,6 +1154,14 @@ pub const FetchTasklet = struct {
}
}
/// Whether the request body should skip chunked transfer encoding framing.
/// True for upgraded connections (e.g. WebSocket) or when the user explicitly
/// set Content-Length without setting Transfer-Encoding.
fn skipChunkedFraming(this: *const FetchTasklet) bool {
return this.upgraded_connection or
(this.request_headers.get("content-length") != null and this.request_headers.get("transfer-encoding") == null);
}
pub fn writeRequestData(this: *FetchTasklet, data: []const u8) ResumableSinkBackpressure {
log("writeRequestData {}", .{data.len});
if (this.signal) |signal| {
@@ -1175,7 +1183,7 @@ pub const FetchTasklet = struct {
// dont have backpressure so we will schedule the data to be written
// if we have backpressure the onWritable will drain the buffer
needs_schedule = stream_buffer.isEmpty();
if (this.upgraded_connection) {
if (this.skipChunkedFraming()) {
bun.handleOom(stream_buffer.write(data));
} else {
//16 is the max size of a hex number size that represents 64 bits + 2 for the \r\n
@@ -1209,15 +1217,14 @@ pub const FetchTasklet = struct {
}
this.abortTask();
} else {
if (!this.upgraded_connection) {
// If is not upgraded we need to send the terminating chunk
if (!this.skipChunkedFraming()) {
// Using chunked transfer encoding, send the terminating chunk
const thread_safe_stream_buffer = this.request_body_streaming_buffer orelse return;
const stream_buffer = thread_safe_stream_buffer.acquire();
defer thread_safe_stream_buffer.release();
bun.handleOom(stream_buffer.write(http.end_of_chunked_http1_1_encoding_response_body));
}
if (this.http) |http_| {
// just tell to write the end of the chunked encoding aka 0\r\n\r\n
http.http_thread.scheduleRequestWrite(http_, .end);
}
}

View File

@@ -68,6 +68,16 @@ pub const Chunk = struct {
}
pub fn getCSSChunkForHTML(this: *const Chunk, chunks: []Chunk) ?*Chunk {
// Look up the CSS chunk via the JS chunk's css_chunks indices.
// This correctly handles deduplicated CSS chunks that are shared
// across multiple HTML entry points (see issue #23668).
if (this.getJSChunkForHTML(chunks)) |js_chunk| {
const css_chunk_indices = js_chunk.content.javascript.css_chunks;
if (css_chunk_indices.len > 0) {
return &chunks[css_chunk_indices[0]];
}
}
// Fallback: match by entry_point_id for cases without a JS chunk.
const entry_point_id = this.entry_point.entry_point_id;
for (chunks) |*other| {
if (other.content == .css) {

View File

@@ -3683,7 +3683,20 @@ pub const BundleV2 = struct {
}
}
const import_record_loader = import_record.loader orelse path.loader(&transpiler.options.loaders) orelse .file;
const import_record_loader = brk: {
const resolved_loader = import_record.loader orelse path.loader(&transpiler.options.loaders) orelse .file;
// When an HTML file references a URL asset (e.g. <link rel="manifest" href="./manifest.json" />),
// the file must be copied to the output directory as-is. If the resolved loader would
// parse/transform the file (e.g. .json, .toml) rather than copy it, force the .file loader
// so that `shouldCopyForBundling()` returns true and the asset is emitted.
// Only do this for HTML sources — CSS url() imports should retain their original behavior.
if (loader == .html and import_record.kind == .url and !resolved_loader.shouldCopyForBundling() and
!resolved_loader.isJavaScriptLike() and !resolved_loader.isCSS() and resolved_loader != .html)
{
break :brk Loader.file;
}
break :brk resolved_loader;
};
import_record.loader = import_record_loader;
const is_html_entrypoint = import_record_loader == .html and target.isServerSide() and this.transpiler.options.dev_server == null;

View File

@@ -150,6 +150,7 @@ pub const ClientEntryPoint = struct {
pub const ServerEntryPoint = struct {
source: logger.Source = undefined,
generated: bool = false,
pub fn generate(
entry: *ServerEntryPoint,
@@ -230,6 +231,7 @@ pub const ServerEntryPoint = struct {
entry.source = logger.Source.initPathString(name, code);
entry.source.path.text = name;
entry.source.path.namespace = "server-entry";
entry.generated = true;
}
};

View File

@@ -22,7 +22,6 @@ pub noinline fn computeChunks(
const entry_source_indices = this.graph.entry_points.items(.source_index);
const css_asts = this.graph.ast.items(.css);
const css_chunking = this.options.css_chunking;
var html_chunks = bun.StringArrayHashMap(Chunk).init(temp_allocator);
const loaders = this.parse_graph.input_files.items(.loader);
const ast_targets = this.graph.ast.items(.target);
@@ -148,10 +147,11 @@ pub noinline fn computeChunks(
if (css_source_indices.len > 0) {
const order = this.findImportedFilesInCSSOrder(temp_allocator, css_source_indices.slice());
const use_content_based_key = css_chunking or has_server_html_imports;
const hash_to_use = if (!use_content_based_key)
bun.hash(try temp_allocator.dupe(u8, entry_bits.bytes(this.graph.entry_points.len)))
else brk: {
// Always use content-based hashing for CSS chunk deduplication.
// This ensures that when multiple JS entry points import the
// same CSS files, they share a single CSS output chunk rather
// than producing duplicates that collide on hash-based naming.
const hash_to_use = brk: {
var hasher = std.hash.Wyhash.init(5);
bun.writeAnyToHasher(&hasher, order.len);
for (order.slice()) |x| x.hash(&hasher);
@@ -322,7 +322,10 @@ pub noinline fn computeChunks(
const remapped_css_indexes = try temp_allocator.alloc(u32, css_chunks.count());
const css_chunk_values = css_chunks.values();
for (sorted_css_keys, js_chunks.count()..) |key, sorted_index| {
// Use sorted_chunks.len as the starting index because HTML chunks
// may be interleaved with JS chunks, so js_chunks.count() would be
// incorrect when HTML entry points are present.
for (sorted_css_keys, sorted_chunks.len..) |key, sorted_index| {
const index = css_chunks.getIndex(key) orelse unreachable;
sorted_chunks.appendAssumeCapacity(css_chunk_values[index]);
remapped_css_indexes[index] = @intCast(sorted_index);

View File

@@ -719,7 +719,21 @@ pub fn buildRequest(this: *HTTPClient, body_len: usize) picohttp.Request {
if (body_len > 0 or this.method.hasRequestBody()) {
if (this.flags.is_streaming_request_body) {
if (add_transfer_encoding and this.flags.upgrade_state == .none) {
if (original_content_length) |content_length| {
if (add_transfer_encoding) {
// User explicitly set Content-Length and did not set Transfer-Encoding;
// preserve Content-Length instead of using chunked encoding.
// This matches Node.js behavior where an explicit Content-Length is always honored.
request_headers_buf[header_count] = .{
.name = content_length_header_name,
.value = content_length,
};
header_count += 1;
}
// If !add_transfer_encoding, the user explicitly set Transfer-Encoding,
// which was already added to request_headers_buf. We respect that and
// do not add Content-Length (they are mutually exclusive per HTTP/1.1).
} else if (add_transfer_encoding and this.flags.upgrade_state == .none) {
request_headers_buf[header_count] = chunked_encoded_header;
header_count += 1;
}

View File

@@ -138,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();
}
@@ -910,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;
}
@@ -942,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)));

View File

@@ -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});

View File

@@ -623,6 +623,17 @@ pub const PackageInstaller = struct {
// else => unreachable,
// };
// If a newly computed integrity hash is available (e.g. for a GitHub
// tarball) and the lockfile doesn't already have one, persist it so
// the lockfile gets re-saved with the hash.
if (data.integrity.tag.isSupported()) {
var pkg_metas = this.lockfile.packages.items(.meta);
if (!pkg_metas[package_id].integrity.tag.isSupported()) {
pkg_metas[package_id].integrity = data.integrity;
this.manager.options.enable.force_save_lockfile = true;
}
}
if (this.manager.task_queue.fetchRemove(task_id)) |removed| {
var callbacks = removed.value;
defer callbacks.deinit(this.manager.allocator);

View File

@@ -133,6 +133,12 @@ pub fn processExtractedTarballPackage(
break :package pkg;
};
// Store the tarball integrity hash so the lockfile can pin the
// exact content downloaded from the remote (GitHub) server.
if (data.integrity.tag.isSupported()) {
package.meta.integrity = data.integrity;
}
package = manager.lockfile.appendPackage(package) catch unreachable;
package_id.* = package.meta.id;

View File

@@ -23,7 +23,26 @@ pub inline fn run(this: *const ExtractTarball, log: *logger.Log, bytes: []const
return error.IntegrityCheckFailed;
}
}
return this.extract(log, bytes);
var result = try this.extract(log, bytes);
// Compute and store SHA-512 integrity hash for GitHub tarballs so the
// lockfile can pin the exact tarball content. On subsequent installs the
// hash stored in the lockfile is forwarded via this.integrity and verified
// above, preventing a compromised server from silently swapping the tarball.
if (this.resolution.tag == .github) {
if (this.integrity.tag.isSupported()) {
// Re-installing with an existing lockfile: integrity was already
// verified above, propagate the known value to ExtractData so that
// the lockfile keeps it on re-serialisation.
result.integrity = this.integrity;
} else {
// First install (no integrity in the lockfile yet): compute it.
result.integrity = .{ .tag = .sha512 };
Crypto.SHA512.hash(bytes, result.integrity.value[0..Crypto.SHA512.digest]);
}
}
return result;
}
pub fn buildURL(
@@ -547,6 +566,7 @@ const string = []const u8;
const Npm = @import("./npm.zig");
const std = @import("std");
const Crypto = @import("../sha.zig").Hashers;
const FileSystem = @import("../fs.zig").FileSystem;
const Integrity = @import("./integrity.zig").Integrity;
const Resolution = @import("./resolution.zig").Resolution;

View File

@@ -209,6 +209,7 @@ pub const ExtractData = struct {
path: string = "",
buf: []u8 = "",
} = null,
integrity: Integrity = .{},
};
pub const DependencyInstallContext = struct {
@@ -271,6 +272,7 @@ pub const VersionSlice = external.VersionSlice;
pub const Dependency = @import("./dependency.zig");
pub const Behavior = @import("./dependency.zig").Behavior;
pub const Integrity = @import("./integrity.zig").Integrity;
pub const Lockfile = @import("./lockfile.zig");
pub const PatchedDep = Lockfile.PatchedDep;

View File

@@ -644,9 +644,16 @@ pub const Stringifier = struct {
&path_buf,
);
try writer.print(", {f}]", .{
repo.resolved.fmtJson(buf, .{}),
});
if (pkg_meta.integrity.tag.isSupported()) {
try writer.print(", {f}, \"{f}\"]", .{
repo.resolved.fmtJson(buf, .{}),
pkg_meta.integrity,
});
} else {
try writer.print(", {f}]", .{
repo.resolved.fmtJson(buf, .{}),
});
}
},
else => unreachable,
}
@@ -1885,6 +1892,15 @@ pub fn parseIntoBinaryLockfile(
};
@field(res.value, @tagName(tag)).resolved = try string_buf.append(bun_tag_str);
// Optional integrity hash (added to pin tarball content)
if (i < pkg_info.len) {
const integrity_expr = pkg_info.at(i);
if (integrity_expr.asString(allocator)) |integrity_str| {
pkg.meta.integrity = Integrity.parse(integrity_str);
i += 1;
}
}
},
else => {},
}

View File

@@ -816,6 +816,12 @@ fn BaseWindowsPipeWriter(
pub fn close(this: *WindowsPipeWriter) void {
this.is_done = true;
if (this.source) |source| {
// Check if there's a pending async write before closing.
// If so, we must defer the onCloseSource notification to
// onWriteComplete, because the parent's onClose handler may
// free the writer's StreamBuffer resources, and the pending
// write callback would then access freed memory.
const has_pending_async_write = this.hasPendingAsyncWrite();
switch (source) {
.sync_file, .file => |file| {
// Use state machine to handle close after operation completes
@@ -837,7 +843,12 @@ fn BaseWindowsPipeWriter(
},
}
this.source = null;
this.onCloseSource();
if (!has_pending_async_write) {
this.onCloseSource();
}
// When has_pending_async_write is true, onCloseSource() will
// be called from onWriteComplete/onFsWriteComplete after the
// pending write callback completes.
}
}
@@ -989,7 +1000,22 @@ pub fn WindowsBufferedWriter(Parent: type, function_table: anytype) type {
return .success;
}
/// Returns true if there is an outstanding async write request
/// (uv_write or uv_fs_write) that hasn't completed yet.
pub fn hasPendingAsyncWrite(this: *const WindowsWriter) bool {
return this.pending_payload_size > 0;
}
fn onWriteComplete(this: *WindowsWriter, status: uv.ReturnCode) void {
// If the source was closed (e.g. close() was called while a write
// was in-flight), clean up and notify the parent via onCloseSource
// (which was deferred by close()).
if (this.source == null) {
this.pending_payload_size = 0;
this.onCloseSource();
return;
}
const written = this.pending_payload_size;
this.pending_payload_size = 0;
if (status.toError(.write)) |err| {
@@ -1292,12 +1318,28 @@ pub fn WindowsStreamingWriter(comptime Parent: type, function_table: anytype) ty
return (this.outgoing.isNotEmpty() or this.current_payload.isNotEmpty());
}
/// Returns true if there is an outstanding async write request
/// (uv_write or uv_fs_write) that hasn't completed yet.
pub fn hasPendingAsyncWrite(this: *const WindowsWriter) bool {
return this.current_payload.isNotEmpty();
}
fn isDone(this: *WindowsWriter) bool {
// done is flags andd no more data queued? so we are done!
return this.is_done and !this.hasPendingData();
}
fn onWriteComplete(this: *WindowsWriter, status: uv.ReturnCode) void {
// If the source was closed (e.g. close() was called while a write
// was in-flight), clean up buffers and notify the parent via
// onCloseSource (which was deferred by close()).
if (this.source == null) {
this.current_payload.reset();
this.outgoing.reset();
this.onCloseSource();
return;
}
if (status.toError(.write)) |err| {
this.last_write_result = .{ .err = err };
log("onWrite() = {s}", .{err.name()});

View File

@@ -51,6 +51,15 @@ function onError(msg, err, callback) {
process.nextTick(emitErrorNt, msg, err, callback);
}
function isHTTPHeaderStateSentOrAssigned(state) {
return state === NodeHTTPHeaderState.sent || state === NodeHTTPHeaderState.assigned;
}
function throwHeadersSentIfNecessary(self, action) {
if (self._header != null || isHTTPHeaderStateSentOrAssigned(self[headerStateSymbol])) {
throw $ERR_HTTP_HEADERS_SENT(action);
}
}
function write_(msg, chunk, encoding, callback, fromEnd) {
if (typeof callback !== "function") callback = nop;
@@ -252,18 +261,14 @@ const OutgoingMessagePrototype = {
removeHeader(name) {
validateString(name, "name");
if ((this._header !== undefined && this._header !== null) || this[headerStateSymbol] === NodeHTTPHeaderState.sent) {
throw $ERR_HTTP_HEADERS_SENT("remove");
}
throwHeadersSentIfNecessary(this, "remove");
const headers = this[headersSymbol];
if (!headers) return;
headers.delete(name);
},
setHeader(name, value) {
if ((this._header !== undefined && this._header !== null) || this[headerStateSymbol] == NodeHTTPHeaderState.sent) {
throw $ERR_HTTP_HEADERS_SENT("set");
}
throwHeadersSentIfNecessary(this, "set");
validateHeaderName(name);
validateHeaderValue(name, value);
const headers = (this[headersSymbol] ??= new Headers());
@@ -271,9 +276,7 @@ const OutgoingMessagePrototype = {
return this;
},
setHeaders(headers) {
if (this._header || this[headerStateSymbol] !== NodeHTTPHeaderState.none) {
throw $ERR_HTTP_HEADERS_SENT("set");
}
throwHeadersSentIfNecessary(this, "set");
if (!headers || $isArray(headers) || typeof headers.keys !== "function" || typeof headers.get !== "function") {
throw $ERR_INVALID_ARG_TYPE("headers", ["Headers", "Map"], headers);

View File

@@ -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;
}
}

View File

@@ -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(

View File

@@ -766,19 +766,13 @@ pub extern fn napi_type_tag_object(env: napi_env, _: napi_value, _: [*c]const na
pub extern fn napi_check_object_type_tag(env: napi_env, _: napi_value, _: [*c]const napi_type_tag, _: *bool) napi_status;
// do nothing for both of these
pub export fn napi_open_callback_scope(env_: napi_env, _: napi_value, _: *anyopaque, _: *anyopaque) napi_status {
pub export fn napi_open_callback_scope(_: napi_env, _: napi_value, _: *anyopaque, _: *anyopaque) napi_status {
log("napi_open_callback_scope", .{});
const env = env_ orelse {
return envIsNull();
};
return env.ok();
return @intFromEnum(NapiStatus.ok);
}
pub export fn napi_close_callback_scope(env_: napi_env, _: *anyopaque) napi_status {
pub export fn napi_close_callback_scope(_: napi_env, _: *anyopaque) napi_status {
log("napi_close_callback_scope", .{});
const env = env_ orelse {
return envIsNull();
};
return env.ok();
return @intFromEnum(NapiStatus.ok);
}
pub extern fn napi_throw(env: napi_env, @"error": napi_value) napi_status;
pub extern fn napi_throw_error(env: napi_env, code: [*c]const u8, msg: [*c]const u8) napi_status;

View File

@@ -12,24 +12,32 @@ var __getOwnPropNames = Object.getOwnPropertyNames;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __hasOwnProp = Object.prototype.hasOwnProperty;
// Shared getter/setter functions: .bind(obj, key) avoids creating a closure
// and JSLexicalEnvironment per property. BoundFunction is much cheaper.
// Must be regular functions (not arrows) so .bind() can set `this`.
function __accessProp(key) {
return this[key];
}
// This is used to implement "export * from" statements. It copies properties
// from the imported module to the current module's ESM export object. If the
// current module is an entry point and the target format is CommonJS, we
// also copy the properties to "module.exports" in addition to our module's
// internal ESM export object.
export var __reExport = (target, mod, secondTarget) => {
for (let key of __getOwnPropNames(mod))
var keys = __getOwnPropNames(mod);
for (let key of keys)
if (!__hasOwnProp.call(target, key) && key !== "default")
__defProp(target, key, {
get: () => mod[key],
get: __accessProp.bind(mod, key),
enumerable: true,
});
if (secondTarget) {
for (let key of __getOwnPropNames(mod))
for (let key of keys)
if (!__hasOwnProp.call(secondTarget, key) && key !== "default")
__defProp(secondTarget, key, {
get: () => mod[key],
get: __accessProp.bind(mod, key),
enumerable: true,
});
@@ -37,11 +45,22 @@ export var __reExport = (target, mod, secondTarget) => {
}
};
/*__PURE__*/
var __toESMCache_node;
/*__PURE__*/
var __toESMCache_esm;
// Converts the module from CommonJS to ESM. When in node mode (i.e. in an
// ".mjs" file, package.json has "type: module", or the "__esModule" export
// in the CommonJS file is falsy or missing), the "default" property is
// overridden to point to the original CommonJS exports object instead.
export var __toESM = (mod, isNodeMode, target) => {
var canCache = mod != null && typeof mod === "object";
if (canCache) {
var cache = isNodeMode ? (__toESMCache_node ??= new WeakMap()) : (__toESMCache_esm ??= new WeakMap());
var cached = cache.get(mod);
if (cached) return cached;
}
target = mod != null ? __create(__getProtoOf(mod)) : {};
const to =
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
@@ -53,34 +72,34 @@ export var __toESM = (mod, isNodeMode, target) => {
for (let key of __getOwnPropNames(mod))
if (!__hasOwnProp.call(to, key))
__defProp(to, key, {
get: () => mod[key],
get: __accessProp.bind(mod, key),
enumerable: true,
});
if (canCache) cache.set(mod, to);
return to;
};
// Converts the module from ESM to CommonJS. This clones the input module
// object with the addition of a non-enumerable "__esModule" property set
// to "true", which overwrites any existing export named "__esModule".
var __moduleCache = /* @__PURE__ */ new WeakMap();
export var __toCommonJS = /* @__PURE__ */ from => {
var entry = __moduleCache.get(from),
export var __toCommonJS = from => {
var entry = (__moduleCache ??= new WeakMap()).get(from),
desc;
if (entry) return entry;
entry = __defProp({}, "__esModule", { value: true });
if ((from && typeof from === "object") || typeof from === "function")
__getOwnPropNames(from).map(
key =>
!__hasOwnProp.call(entry, key) &&
for (var key of __getOwnPropNames(from))
if (!__hasOwnProp.call(entry, key))
__defProp(entry, key, {
get: () => from[key],
get: __accessProp.bind(from, key),
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable,
}),
);
});
__moduleCache.set(from, entry);
return entry;
};
/*__PURE__*/
var __moduleCache;
// When you do know the module is CJS
export var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
@@ -97,6 +116,10 @@ export var __name = (target, name) => {
// ESM export -> CJS export
// except, writable incase something re-exports
var __returnValue = v => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
export var __export = /* @__PURE__ */ (target, all) => {
for (var name in all)
@@ -104,15 +127,19 @@ export var __export = /* @__PURE__ */ (target, all) => {
get: all[name],
enumerable: true,
configurable: true,
set: newValue => (all[name] = () => newValue),
set: __exportSetter.bind(all, name),
});
};
function __exportValueSetter(name, newValue) {
this[name] = newValue;
}
export var __exportValue = (target, all) => {
for (var name in all) {
__defProp(target, name, {
get: () => all[name],
set: newValue => (all[name] = newValue),
get: __accessProp.bind(all, name),
set: __exportValueSetter.bind(all, name),
enumerable: true,
configurable: true,
});

View File

@@ -1165,7 +1165,7 @@ 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 std.mem.indexOfAny(u8, value, "\r\n") != null;
return strings.indexOfAny(value, "\r\n") != null;
}
const std = @import("std");

View File

@@ -266,8 +266,8 @@ pub const ShellCpOutputTask = OutputTask(Cp, .{
const ShellCpOutputTaskVTable = struct {
pub fn writeErr(this: *Cp, childptr: anytype, errbuf: []const u8) ?Yield {
this.state.exec.output_waiting += 1;
if (this.bltn().stderr.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
return this.bltn().stderr.enqueue(childptr, errbuf, safeguard);
}
_ = this.bltn().writeNoIO(.stderr, errbuf);
@@ -279,8 +279,8 @@ const ShellCpOutputTaskVTable = struct {
}
pub fn writeOut(this: *Cp, childptr: anytype, output: *OutputSrc) ?Yield {
this.state.exec.output_waiting += 1;
if (this.bltn().stdout.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
return this.bltn().stdout.enqueue(childptr, output.slice(), safeguard);
}
_ = this.bltn().writeNoIO(.stdout, output.slice());

View File

@@ -175,8 +175,8 @@ pub const ShellLsOutputTask = OutputTask(Ls, .{
const ShellLsOutputTaskVTable = struct {
pub fn writeErr(this: *Ls, childptr: anytype, errbuf: []const u8) ?Yield {
log("ShellLsOutputTaskVTable.writeErr(0x{x}, {s})", .{ @intFromPtr(this), errbuf });
this.state.exec.output_waiting += 1;
if (this.bltn().stderr.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
return this.bltn().stderr.enqueue(childptr, errbuf, safeguard);
}
_ = this.bltn().writeNoIO(.stderr, errbuf);
@@ -190,8 +190,8 @@ const ShellLsOutputTaskVTable = struct {
pub fn writeOut(this: *Ls, childptr: anytype, output: *OutputSrc) ?Yield {
log("ShellLsOutputTaskVTable.writeOut(0x{x}, {s})", .{ @intFromPtr(this), output.slice() });
this.state.exec.output_waiting += 1;
if (this.bltn().stdout.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
return this.bltn().stdout.enqueue(childptr, output.slice(), safeguard);
}
log("ShellLsOutputTaskVTable.writeOut(0x{x}, {s}) no IO", .{ @intFromPtr(this), output.slice() });

View File

@@ -129,8 +129,8 @@ pub const ShellMkdirOutputTask = OutputTask(Mkdir, .{
const ShellMkdirOutputTaskVTable = struct {
pub fn writeErr(this: *Mkdir, childptr: anytype, errbuf: []const u8) ?Yield {
this.state.exec.output_waiting += 1;
if (this.bltn().stderr.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
return this.bltn().stderr.enqueue(childptr, errbuf, safeguard);
}
_ = this.bltn().writeNoIO(.stderr, errbuf);
@@ -142,8 +142,8 @@ const ShellMkdirOutputTaskVTable = struct {
}
pub fn writeOut(this: *Mkdir, childptr: anytype, output: *OutputSrc) ?Yield {
this.state.exec.output_waiting += 1;
if (this.bltn().stdout.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
const slice = output.slice();
log("THE SLICE: {d} {s}", .{ slice.len, slice });
return this.bltn().stdout.enqueue(childptr, slice, safeguard);

View File

@@ -46,12 +46,14 @@ pub fn start(this: *@This()) Yield {
const maybe1 = iter.next().?;
const int1 = std.fmt.parseFloat(f32, bun.sliceTo(maybe1, 0)) catch return this.fail("seq: invalid argument\n");
if (!std.math.isFinite(int1)) return this.fail("seq: invalid argument\n");
this._end = int1;
if (this._start > this._end) this.increment = -1;
const maybe2 = iter.next();
if (maybe2 == null) return this.do();
const int2 = std.fmt.parseFloat(f32, bun.sliceTo(maybe2.?, 0)) catch return this.fail("seq: invalid argument\n");
if (!std.math.isFinite(int2)) return this.fail("seq: invalid argument\n");
this._start = int1;
this._end = int2;
if (this._start < this._end) this.increment = 1;
@@ -60,6 +62,7 @@ pub fn start(this: *@This()) Yield {
const maybe3 = iter.next();
if (maybe3 == null) return this.do();
const int3 = std.fmt.parseFloat(f32, bun.sliceTo(maybe3.?, 0)) catch return this.fail("seq: invalid argument\n");
if (!std.math.isFinite(int3)) return this.fail("seq: invalid argument\n");
this._start = int1;
this.increment = int2;
this._end = int3;

View File

@@ -132,8 +132,8 @@ pub const ShellTouchOutputTask = OutputTask(Touch, .{
const ShellTouchOutputTaskVTable = struct {
pub fn writeErr(this: *Touch, childptr: anytype, errbuf: []const u8) ?Yield {
this.state.exec.output_waiting += 1;
if (this.bltn().stderr.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
return this.bltn().stderr.enqueue(childptr, errbuf, safeguard);
}
_ = this.bltn().writeNoIO(.stderr, errbuf);
@@ -145,8 +145,8 @@ const ShellTouchOutputTaskVTable = struct {
}
pub fn writeOut(this: *Touch, childptr: anytype, output: *OutputSrc) ?Yield {
this.state.exec.output_waiting += 1;
if (this.bltn().stdout.needsIO()) |safeguard| {
this.state.exec.output_waiting += 1;
const slice = output.slice();
log("THE SLICE: {d} {s}", .{ slice.len, slice });
return this.bltn().stdout.enqueue(childptr, slice, safeguard);

View File

@@ -168,6 +168,8 @@ fn commandImplStart(this: *CondExpr) Yield {
.@"-d",
.@"-f",
=> {
// Empty string expansion produces no args; the path doesn't exist.
if (this.args.items.len == 0) return this.parent.childDone(this, 1);
this.state = .waiting_stat;
return this.doStat();
},

View File

@@ -2,13 +2,17 @@
exports[`Bun.build Bun.write(BuildArtifact) 1`] = `
"var __defProp = Object.defineProperty;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: (newValue) => all[name] = () => newValue
set: __exportSetter.bind(all, name)
});
};
@@ -31,13 +35,17 @@ NS.then(({ fn: fn2 }) => {
exports[`Bun.build outdir + reading out blobs works 1`] = `
"var __defProp = Object.defineProperty;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: (newValue) => all[name] = () => newValue
set: __exportSetter.bind(all, name)
});
};
@@ -58,23 +66,27 @@ NS.then(({ fn: fn2 }) => {
"
`;
exports[`Bun.build BuildArtifact properties: hash 1`] = `"d1c7nm6t"`;
exports[`Bun.build BuildArtifact properties: hash 1`] = `"est79qzq"`;
exports[`Bun.build BuildArtifact properties + entry.naming: hash 1`] = `"rm7e36cf"`;
exports[`Bun.build BuildArtifact properties + entry.naming: hash 1`] = `"7gfnt0h6"`;
exports[`Bun.build BuildArtifact properties sourcemap: hash index.js 1`] = `"d1c7nm6t"`;
exports[`Bun.build BuildArtifact properties sourcemap: hash index.js 1`] = `"est79qzq"`;
exports[`Bun.build BuildArtifact properties sourcemap: hash index.js.map 1`] = `"00000000"`;
exports[`Bun.build new Response(BuildArtifact) sets content type: response text 1`] = `
"var __defProp = Object.defineProperty;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: (newValue) => all[name] = () => newValue
set: __exportSetter.bind(all, name)
});
};

View File

@@ -416,7 +416,8 @@ describe("bundler", () => {
const db = new Database("test.db");
const query = db.query(\`select "Hello world" as message\`);
if (query.get().message !== "Hello world") throw "fail from sqlite";
const icon = await fetch("https://bun.sh/favicon.ico").then(x=>x.arrayBuffer())
const icon = new Uint8Array(256);
for (let i = 0; i < 256; i++) icon[i] = i;
if(icon.byteLength < 100) throw "fail from icon";
if (typeof getRandomSeed() !== 'number') throw "fail from bun:jsc";
const server = serve({

View File

@@ -1113,7 +1113,7 @@ describe("bundler", () => {
snapshotSourceMap: {
"entry.js.map": {
files: ["../node_modules/react/index.js", "../entry.js"],
mappingsExactMatch: "qYACA,WAAW,IAAQ,EAAE,ICDrB,eACA,QAAQ,IAAI,CAAK",
mappingsExactMatch: "miBACA,WAAW,IAAQ,EAAE,ICDrB,eACA,QAAQ,IAAI,CAAK",
},
},
});

View File

@@ -843,4 +843,131 @@ body {
api.expectFile("out/" + jsFile).toContain("sourceMappingURL");
},
});
// Test that multiple HTML entrypoints sharing the same CSS file both get
// the CSS link tag in production mode (css_chunking deduplication).
// Regression test for https://github.com/oven-sh/bun/issues/23668
itBundled("html/SharedCSSProductionMultipleEntries", {
outdir: "out/",
production: true,
files: {
"/entry1.html": `<!doctype html>
<html>
<head>
<link rel="stylesheet" href="./global.css" />
</head>
<body>
<div id="root"></div>
<script src="./main1.tsx"></script>
</body>
</html>`,
"/entry2.html": `<!doctype html>
<html>
<head>
<link rel="stylesheet" href="./global.css" />
</head>
<body>
<div id="root"></div>
<script src="./main2.tsx"></script>
</body>
</html>`,
"/global.css": `h1 { font-size: 24px; }`,
"/main1.tsx": `console.log("entry1");`,
"/main2.tsx": `console.log("entry2");`,
},
entryPoints: ["/entry1.html", "/entry2.html"],
onAfterBundle(api) {
const entry1Html = api.readFile("out/entry1.html");
const entry2Html = api.readFile("out/entry2.html");
// Both HTML files must contain a CSS link tag
const cssMatch1 = entry1Html.match(/href="(.*\.css)"/);
const cssMatch2 = entry2Html.match(/href="(.*\.css)"/);
expect(cssMatch1).not.toBeNull();
expect(cssMatch2).not.toBeNull();
// Both should reference the same deduplicated CSS chunk
expect(cssMatch1![1]).toBe(cssMatch2![1]);
// The CSS file should contain the shared styles
const cssContent = api.readFile("out/" + cssMatch1![1]);
expect(cssContent).toContain("font-size");
// Both HTML files should also have their respective JS bundles
expect(entry1Html).toMatch(/src=".*\.js"/);
expect(entry2Html).toMatch(/src=".*\.js"/);
},
});
// Test manifest.json is copied as an asset and link href is rewritten
itBundled("html/manifest-json", {
outdir: "out/",
files: {
"/index.html": `
<!DOCTYPE html>
<html>
<head>
<link rel="manifest" href="./manifest.json" />
</head>
<body>
<h1>App</h1>
<script src="./app.js"></script>
</body>
</html>`,
"/manifest.json": JSON.stringify({
name: "My App",
short_name: "App",
start_url: "/",
display: "standalone",
background_color: "#ffffff",
theme_color: "#000000",
}),
"/app.js": "console.log('hello')",
},
entryPoints: ["/index.html"],
onAfterBundle(api) {
const htmlContent = api.readFile("out/index.html");
// The original manifest.json reference should be rewritten to a hashed filename
expect(htmlContent).not.toContain('manifest.json"');
expect(htmlContent).toMatch(/href="(?:\.\/|\/)?manifest-[a-zA-Z0-9]+\.json"/);
// Extract the hashed manifest filename and verify its content
const manifestMatch = htmlContent.match(/href="(?:\.\/|\/)?(manifest-[a-zA-Z0-9]+\.json)"/);
expect(manifestMatch).not.toBeNull();
const manifestContent = api.readFile("out/" + manifestMatch![1]);
expect(manifestContent).toContain('"name"');
expect(manifestContent).toContain('"My App"');
},
});
// Test that other non-JS/CSS file types referenced via URL imports are copied as assets
itBundled("html/xml-asset", {
outdir: "out/",
files: {
"/index.html": `
<!DOCTYPE html>
<html>
<head>
<link rel="manifest" href="./site.webmanifest" />
</head>
<body>
<h1>App</h1>
</body>
</html>`,
"/site.webmanifest": JSON.stringify({
name: "My App",
icons: [{ src: "/icon.png", sizes: "192x192" }],
}),
},
entryPoints: ["/index.html"],
onAfterBundle(api) {
const htmlContent = api.readFile("out/index.html");
// The webmanifest reference should be rewritten to a hashed filename
expect(htmlContent).not.toContain("site.webmanifest");
expect(htmlContent).toMatch(/href=".*\.webmanifest"/);
},
});
});

View File

@@ -57,17 +57,17 @@ describe("bundler", () => {
"../entry.tsx",
],
mappings: [
["react.development.js:524:'getContextName'", "1:5412:Y1"],
["react.development.js:524:'getContextName'", "1:5567:Y1"],
["react.development.js:2495:'actScopeDepth'", "23:4082:GJ++"],
["react.development.js:696:''Component'", '1:7474:\'Component "%s"'],
["entry.tsx:6:'\"Content-Type\"'", '100:18809:"Content-Type"'],
["entry.tsx:11:'<html>'", "100:19063:void"],
["entry.tsx:23:'await'", "100:19163:await"],
["react.development.js:696:''Component'", '1:7629:\'Component "%s"'],
["entry.tsx:6:'\"Content-Type\"'", '100:18808:"Content-Type"'],
["entry.tsx:11:'<html>'", "100:19062:void"],
["entry.tsx:23:'await'", "100:19161:await"],
],
},
},
expectExactFilesize: {
"out/entry.js": 221720,
"out/entry.js": 221895,
},
run: {
stdout: "<!DOCTYPE html><html><body><h1>Hello World</h1><p>This is an example.</p></body></html>",

View File

@@ -76,13 +76,17 @@ describe("bundler", () => {
expect(bundled).toMatchInlineSnapshot(`
"var __defProp = Object.defineProperty;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: (newValue) => all[name] = () => newValue
set: __exportSetter.bind(all, name)
});
};
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -160,7 +164,7 @@ describe("bundler", () => {
var { AsyncEntryPoint: AsyncEntryPoint2 } = await Promise.resolve().then(() => exports_AsyncEntryPoint);
AsyncEntryPoint2();
//# debugId=5E85CC0956C6307964756E2164756E21
//# debugId=42062903F19477CF64756E2164756E21
//# sourceMappingURL=out.js.map
"
`);
@@ -337,13 +341,17 @@ describe("bundler", () => {
expect(bundled).toMatchInlineSnapshot(`
"var __defProp = Object.defineProperty;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: (newValue) => all[name] = () => newValue
set: __exportSetter.bind(all, name)
});
};
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -402,7 +410,7 @@ describe("bundler", () => {
var { AsyncEntryPoint: AsyncEntryPoint2 } = await Promise.resolve().then(() => exports_AsyncEntryPoint);
AsyncEntryPoint2();
//# debugId=C92CBF0103732ECC64756E2164756E21
//# debugId=BF876FBF618133C264756E2164756E21
//# sourceMappingURL=out.js.map
"
`);

View File

@@ -2150,10 +2150,7 @@ c {
toplevel-tilde.css: WARNING: CSS nesting syntax is not supported in the configured target environment (chrome10)
`, */
});
// TODO: Bun's bundler doesn't support multiple entry points generating CSS outputs
// with identical content hashes to the same output path. This test exposes that
// limitation. Skip until the bundler can deduplicate or handle this case.
itBundled.skip("css/MetafileCSSBundleTwoToOne", {
itBundled("css/MetafileCSSBundleTwoToOne", {
files: {
"/foo/entry.js": /* js */ `
import '../common.css'

View File

@@ -103,11 +103,11 @@ console.log(favicon);
"files": [
{
"input": "client.html",
"path": "./client-s249t5qg.js",
"path": "./client-b5m4ng86.js",
"loader": "js",
"isEntry": true,
"headers": {
"etag": "fxoJ6L-0X3o",
"etag": "Ax71YVYyZQc",
"content-type": "text/javascript;charset=utf-8"
}
},

View File

@@ -0,0 +1,255 @@
import { file } from "bun";
import { describe, expect, test } from "bun:test";
import { rm } from "fs/promises";
import { bunEnv, bunExe, tempDir } from "harness";
import { join } from "path";
// Each test uses its own BUN_INSTALL_CACHE_DIR inside the temp dir for full
// isolation. This avoids interfering with the global cache or other tests.
function envWithCache(dir: string) {
return { ...bunEnv, BUN_INSTALL_CACHE_DIR: join(String(dir), ".bun-cache") };
}
describe.concurrent("GitHub tarball integrity", () => {
test("should store integrity hash in lockfile for GitHub dependencies", async () => {
using dir = tempDir("github-integrity", {
"package.json": JSON.stringify({
name: "test-github-integrity",
dependencies: {
"is-number": "jonschlinkert/is-number#98e8ff1",
},
}),
});
const env = envWithCache(dir);
await using proc = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("Saved lockfile");
expect(exitCode).toBe(0);
const lockfileContent = await file(join(String(dir), "bun.lock")).text();
// The lockfile should contain a sha512 integrity hash for the GitHub dependency
expect(lockfileContent).toContain("sha512-");
// The resolved commit hash should be present
expect(lockfileContent).toContain("jonschlinkert-is-number-98e8ff1");
// Verify the format: the integrity appears after the resolved commit hash
expect(lockfileContent).toMatch(/"jonschlinkert-is-number-98e8ff1",\s*"sha512-/);
});
test("should verify integrity passes on re-install with matching hash", async () => {
using dir = tempDir("github-integrity-match", {
"package.json": JSON.stringify({
name: "test-github-integrity-match",
dependencies: {
"is-number": "jonschlinkert/is-number#98e8ff1",
},
}),
});
const env = envWithCache(dir);
// First install to generate lockfile with correct integrity
await using proc1 = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [stdout1, stderr1, exitCode1] = await Promise.all([proc1.stdout.text(), proc1.stderr.text(), proc1.exited]);
expect(stderr1).not.toContain("error:");
expect(exitCode1).toBe(0);
// Read the generated lockfile and extract the integrity hash adjacent to
// the GitHub resolved entry to avoid accidentally matching an npm hash.
const lockfileContent = await file(join(String(dir), "bun.lock")).text();
const integrityMatch = lockfileContent.match(/"jonschlinkert-is-number-98e8ff1",\s*"(sha512-[A-Za-z0-9+/]+=*)"/);
expect(integrityMatch).not.toBeNull();
const integrityHash = integrityMatch![1];
// Clear cache and node_modules, then re-install with the same lockfile
await rm(join(String(dir), ".bun-cache"), { recursive: true, force: true });
await rm(join(String(dir), "node_modules"), { recursive: true, force: true });
await using proc2 = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [stdout2, stderr2, exitCode2] = await Promise.all([proc2.stdout.text(), proc2.stderr.text(), proc2.exited]);
// Should succeed because the integrity matches
expect(stderr2).not.toContain("Integrity check failed");
expect(exitCode2).toBe(0);
// Lockfile should still contain the same integrity hash
const lockfileContent2 = await file(join(String(dir), "bun.lock")).text();
expect(lockfileContent2).toContain(integrityHash);
});
test("should reject GitHub tarball when integrity check fails", async () => {
using dir = tempDir("github-integrity-reject", {
"package.json": JSON.stringify({
name: "test-github-integrity-reject",
dependencies: {
"is-number": "jonschlinkert/is-number#98e8ff1",
},
}),
// Pre-create a lockfile with an invalid integrity hash (valid base64, 64 zero bytes)
"bun.lock": JSON.stringify({
lockfileVersion: 1,
configVersion: 1,
workspaces: {
"": {
name: "test-github-integrity-reject",
dependencies: {
"is-number": "jonschlinkert/is-number#98e8ff1",
},
},
},
packages: {
"is-number": [
"is-number@github:jonschlinkert/is-number#98e8ff1",
{},
"jonschlinkert-is-number-98e8ff1",
"sha512-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==",
],
},
}),
});
// Fresh per-test cache ensures the tarball must be downloaded from the network
const env = envWithCache(dir);
await using proc = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("Integrity check failed");
expect(exitCode).not.toBe(0);
});
test("should update lockfile with integrity when old format has none", async () => {
using dir = tempDir("github-integrity-upgrade", {
"package.json": JSON.stringify({
name: "test-github-integrity-upgrade",
dependencies: {
"is-number": "jonschlinkert/is-number#98e8ff1",
},
}),
// Pre-create a lockfile in the old format (no integrity hash)
"bun.lock": JSON.stringify({
lockfileVersion: 1,
configVersion: 1,
workspaces: {
"": {
name: "test-github-integrity-upgrade",
dependencies: {
"is-number": "jonschlinkert/is-number#98e8ff1",
},
},
},
packages: {
"is-number": ["is-number@github:jonschlinkert/is-number#98e8ff1", {}, "jonschlinkert-is-number-98e8ff1"],
},
}),
});
// Fresh per-test cache ensures the tarball must be downloaded
const env = envWithCache(dir);
await using proc = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
// Should succeed without errors
expect(stderr).not.toContain("Integrity check failed");
expect(stderr).not.toContain("error:");
// The lockfile should be re-saved with the new integrity hash
expect(stderr).toContain("Saved lockfile");
expect(exitCode).toBe(0);
// Verify the lockfile now contains the integrity hash
const lockfileContent = await file(join(String(dir), "bun.lock")).text();
expect(lockfileContent).toContain("sha512-");
expect(lockfileContent).toMatch(/"jonschlinkert-is-number-98e8ff1",\s*"sha512-/);
});
test("should accept GitHub dependency from cache without re-downloading", async () => {
// Use a shared cache dir for both installs so the second is a true cache hit
using dir = tempDir("github-integrity-cached", {
"package.json": JSON.stringify({
name: "test-github-integrity-cached",
dependencies: {
"is-number": "jonschlinkert/is-number#98e8ff1",
},
}),
});
const env = envWithCache(dir);
// First install warms the per-test cache
await using proc1 = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [stdout1, stderr1, exitCode1] = await Promise.all([proc1.stdout.text(), proc1.stderr.text(), proc1.exited]);
expect(stderr1).not.toContain("error:");
expect(exitCode1).toBe(0);
// Remove node_modules but keep the cache
await rm(join(String(dir), "node_modules"), { recursive: true, force: true });
// Strip the integrity from the lockfile to simulate an old-format lockfile
// that should still work when the cache already has the package
const lockfileContent = await file(join(String(dir), "bun.lock")).text();
const stripped = lockfileContent.replace(/,\s*"sha512-[^"]*"/, "");
await Bun.write(join(String(dir), "bun.lock"), stripped);
// Second install should hit the cache and succeed without re-downloading
await using proc2 = Bun.spawn({
cmd: [bunExe(), "install"],
cwd: String(dir),
env,
stdout: "pipe",
stderr: "pipe",
});
const [stdout2, stderr2, exitCode2] = await Promise.all([proc2.stdout.text(), proc2.stderr.text(), proc2.exited]);
// Should succeed without integrity errors (package served from cache)
expect(stderr2).not.toContain("Integrity check failed");
expect(stderr2).not.toContain("error:");
expect(exitCode2).toBe(0);
});
});

View File

@@ -634,3 +634,42 @@ test.concurrent("bun serve files with correct Content-Type headers", async () =>
// The process will be automatically cleaned up by 'await using'
}
});
test("importing bun:main from HTML entry preload does not crash", async () => {
const dir = tempDirWithFiles("html-entry-bun-main", {
"index.html": /*html*/ `
<!DOCTYPE html>
<html>
<head><title>Test</title></head>
<body><h1>Hello</h1></body>
</html>
`,
"preload.mjs": /*js*/ `
try {
await import("bun:main");
} catch {}
// Signal that preload ran successfully without crashing
console.log("PRELOAD_OK");
`,
});
await using proc = Bun.spawn({
cmd: [bunExe(), "--preload", "./preload.mjs", "index.html", "--port=0"],
env: bunEnv,
cwd: dir,
stdout: "pipe",
stderr: "pipe",
});
const decoder = new TextDecoder();
let text = "";
for await (const chunk of proc.stdout) {
text += decoder.decode(chunk, { stream: true });
if (text.includes("http://")) break;
}
expect(text).toContain("PRELOAD_OK");
proc.kill();
await proc.exited;
});

View File

@@ -1,28 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCIzOJskt6VkEJY
XKSJv/Gdil3XYkjk3NVc/+m+kzqnkTRbPtT9w+IGWgmJhuf9DJPLCwHFAEFarVwV
x16Q0PbU4ajXaLRHEYGhrH10oTMjQnJ24xVm26mxRXPQa5vaLpWJqNyIdNLIQLe+
UXUOzSGGsFTRMAjvYrkzjBe4ZUnaZV+aFY/ug0jfzeA1dJjzKZs6+yTJRbsuWUEb
8MsDmT4v+kBZDKdaDn7AFDWRVqx/38BnqsRzkM0CxpnyT2kRzw5zQajIE13gdTJo
1EHvYSUkkxrY5m30Rl9BuBBZBjhMzOHq0fYVVooHO+sf4XHPgvFTTxJum85u7J1J
oEUjrLKtAgMBAAECggEACInVNhaiqu4infZGVMy0rXMV8VwSlapM7O2SLtFsr0nK
XUmaLK6dvGzBPKK9dxdiYCFzPlMKQTkhzsAvYFWSmm3tRmikG+11TFyCRhXLpc8/
ark4vD9Io6ZkmKUmyKLwtXNjNGcqQtJ7RXc7Ga3nAkueN6JKZHqieZusXVeBGQ70
YH1LKyVNBeJggbj+g9rqaksPyNJQ8EWiNTJkTRQPazZ0o1VX/fzDFyr/a5npFtHl
4BHfafv9o1Xyr70Kie8CYYRJNViOCN+ylFs7Gd3XRaAkSkgMT/7DzrHdEM2zrrHK
yNg2gyDVX9UeEJG2X5UtU0o9BVW7WBshz/2hqIUHoQKBgQC8zsRFvC7u/rGr5vRR
mhZZG+Wvg03/xBSuIgOrzm+Qie6mAzOdVmfSL/pNV9EFitXt1yd2ROo31AbS7Evy
Bm/QVKr2mBlmLgov3B7O/e6ABteooOL7769qV/v+yo8VdEg0biHmsfGIIXDe3Lwl
OT0XwF9r/SeZLbw1zfkSsUVG/QKBgQC5fANM3Dc9LEek+6PHv5+eC1cKkyioEjUl
/y1VUD00aABI1TUcdLF3BtFN2t/S6HW0hrP3KwbcUfqC25k+GDLh1nM6ZK/gI3Yn
IGtCHxtE3S6jKhE9QcK/H+PzGVKWge9SezeYRP0GHJYDrTVTA8Kt9HgoZPPeReJl
+Ss9c8ThcQKBgECX6HQHFnNzNSufXtSQB7dCoQizvjqTRZPxVRoxDOABIGExVTYt
umUhPtu5AGyJ+/hblEeU+iBRbGg6qRzK8PPwE3E7xey8MYYAI5YjL7YjISKysBUL
AhM6uJ6Jg/wOBSnSx8xZ8kzlS+0izUda1rjKeprCSArSp8IsjlrDxPStAoGAEcPr
+P+altRX5Fhpvmb/Hb8OTif8G+TqjEIdkG9H/W38oP0ywg/3M2RGxcMx7txu8aR5
NjI7zPxZFxF7YvQkY3cLwEsGgVxEI8k6HLIoBXd90Qjlb82NnoqqZY1GWL4HMwo0
L/Rjm6M/Rwje852Hluu0WoIYzXA6F/Q+jPs6nzECgYAxx4IbDiGXuenkwSF1SUyj
NwJXhx4HDh7U6EO/FiPZE5BHE3BoTrFu3o1lzverNk7G3m+j+m1IguEAalHlukYl
rip9iUISlKYqbYZdLBoLwHAfHhszdrjqn8/v6oqbB5yR3HXjPFUWJo0WJ2pqJp56
ZshgmQQ/5Khoj6x0/dMPSg==
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCt7iqkEIco372h
v19q0zjaYbm6gzxEnR45UjpQYqgztq4QHicD80mqIkCBCYknFxhwxhNn+Y3g5RWQ
dReplpQbkneqRVp+qixMvu2FmOA4zRRoqObP7FyF1Yusvmroe0Y9SP2xTTmA9Zo7
3paywPUIuZ9eKGwIiFTtj1yQ1FdghLhzZgxcf3LHEHRkGnxgxxNITFxh4nd6fGIj
NqM5fQAY8z35lMXdeWjrhtaqgFYB+Z20YY0X7LJx39vYao0wqW8sZjX88TqHI1zX
WLpUk6UK9RqaNza5xc80wV+9/zjhr3dc1FRjBxI1DS/ufo33dUfvilxv9/LtWwUn
KfKLns9LAgMBAAECggEAAacPHM2G7GBIm/9rCr6tvihNgD8M685zOOZAqGYn9CqY
cYHC4gtF/L2U6CBj2pNAoCwo3LXUkD+6r7MYKXAgqQg3HTCM4rwFbhD1rU8FVHfh
OL0QwwZ2ut95DVdjoxTAlEN9ZcdSFc//llMJ1cF8lxoVvKFc4cv3uCI2mcaJk858
iABfJLl3yfdv1xtpAuOfXf66sXbAmn5NQfN0qTEg2iOdgb4BUee5Wb35MakDQb6+
/s7/bWB+ublZzYt12ChIh1jkBBHaGyQ8mFnPj99ZAJdFjAzi6ydoJ0a2rCVY7Ugs
bkhnzDUtAaHKxo9JXaqIwbUaVFkX8dDhbg82dJrWUQKBgQDb7hNR0bJFW845N19M
74p2PM+0dIiVzwxAg4E2dXDVe39awO/tw8Vu1o1+NPFhWAzGcidP7pAHmPEgRTVO
7LA2P3CDXpkAEx5E0QW6QWZGqHfSa3+P1AvetvAV+OxtlDphcNeLApY16TUVOKZg
SZlxW2e0dZylbHewgLBTIV9wUQKBgQDKdML+JD18WfenPeowsw8HzKdaw01iGiV1
fvTjEXu6YxPPynWFMuj5gjBQodXM2vv0EsQBAPKYfe0nzRFL2kNuYs7TLoaNxqkp
DNfJ2Ww5OSg7Mp76XgppeKKlsXLyUMYHHrDh6MRi5jvWtiHRpaNmV3cHMRs22c+B
cqKP5Zma2wKBgCPNnS2Lsrbh3C+qWQRgVq0q9zFMa1PgEgGKpwVjlwvaAACZOjX9
0e1aVkx+d/E98U55FPdJQf9Koa58NdJ0a7dZGor4YnYFpr7TPFh2/xxvnpoN0AVt
IsWOCIW7MVohcGOeiChkMmnyXibnQwaX1LgEhlx1bRvtDYsZWBsgarYRAoGAARvo
oYnDSHYZtDHToZapg2pslEOzndD02ZLrdn73BYtbZWz/fc5MlmlPKHHqgOfGL40W
w8akjY9LCEfIS3kTm3wxE9kSZZ5r+MyYNgPZ4upcPQ7G7iortm4xveSd85PbsdhK
McKbqMsIEuIGh2Z34ayi+0galQ9WYqglGdKxJ7cCgYEAuSPBHa+en0xaraZNRvMk
OfV9Su/wrpR3TXSeo0E1mZHLwq1JwulpfO1SjxTH5uOJtG0tusl122wfm0KjrXUO
vG5/It+X4u1Nv9oWj+z1+EV4fQrQ/Coqcc1r+5w1yzfURkKlHh74jbK5Yy/KfXrE
eqbbJD40tKhY8ho15D3iCSo=
-----END PRIVATE KEY-----

View File

@@ -1,23 +1,24 @@
-----BEGIN CERTIFICATE-----
MIID5jCCAs6gAwIBAgIUN7coIsdMcLo9amZfkwogu0YkeLEwDQYJKoZIhvcNAQEL
MIIEDDCCAvSgAwIBAgIUbddWE2woW5e96uC4S2fd2M0AsFAwDQYJKoZIhvcNAQEL
BQAwfjELMAkGA1UEBhMCU0UxDjAMBgNVBAgMBVN0YXRlMREwDwYDVQQHDAhMb2Nh
dGlvbjEaMBgGA1UECgwRT3JnYW5pemF0aW9uIE5hbWUxHDAaBgNVBAsME09yZ2Fu
aXphdGlvbmFsIFVuaXQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMzA5MjExNDE2
MjNaFw0yNDA5MjAxNDE2MjNaMH4xCzAJBgNVBAYTAlNFMQ4wDAYDVQQIDAVTdGF0
aXphdGlvbmFsIFVuaXQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yNjAyMTMyMzEx
MjlaFw0zNjAyMTEyMzExMjlaMH4xCzAJBgNVBAYTAlNFMQ4wDAYDVQQIDAVTdGF0
ZTERMA8GA1UEBwwITG9jYXRpb24xGjAYBgNVBAoMEU9yZ2FuaXphdGlvbiBOYW1l
MRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRIwEAYDVQQDDAlsb2NhbGhv
c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCIzOJskt6VkEJYXKSJ
v/Gdil3XYkjk3NVc/+m+kzqnkTRbPtT9w+IGWgmJhuf9DJPLCwHFAEFarVwVx16Q
0PbU4ajXaLRHEYGhrH10oTMjQnJ24xVm26mxRXPQa5vaLpWJqNyIdNLIQLe+UXUO
zSGGsFTRMAjvYrkzjBe4ZUnaZV+aFY/ug0jfzeA1dJjzKZs6+yTJRbsuWUEb8MsD
mT4v+kBZDKdaDn7AFDWRVqx/38BnqsRzkM0CxpnyT2kRzw5zQajIE13gdTJo1EHv
YSUkkxrY5m30Rl9BuBBZBjhMzOHq0fYVVooHO+sf4XHPgvFTTxJum85u7J1JoEUj
rLKtAgMBAAGjXDBaMA4GA1UdDwEB/wQEAwIDiDATBgNVHSUEDDAKBggrBgEFBQcD
ATAUBgNVHREEDTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFNzx4Rfs9m8XR5ML0WsI
sorKmB4PMA0GCSqGSIb3DQEBCwUAA4IBAQB87iQy8R0fiOky9WTcyzVeMaavS3MX
iTe1BRn1OCyDq+UiwwoNz7zdzZJFEmRtFBwPNFOe4HzLu6E+7yLFR552eYRHlqIi
/fiLb5JiZfPtokUHeqwELWBsoXtU8vKxViPiLZ09jkWOPZWo7b/xXd6QYykBfV91
usUXLzyTD2orMagpqNksLDGS3p3ggHEJBZtRZA8R7kPEw98xZHznOQpr26iv8kYz
ZWdLFoFdwgFBSfxePKax5rfo+FbwdrcTX0MhbORyiu2XsBAghf8s2vKDkHg2UQE8
haonxFYMFaASfaZ/5vWKYDTCJkJ67m/BtkpRafFEO+ad1i1S61OjfxH4
c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCt7iqkEIco372hv19q
0zjaYbm6gzxEnR45UjpQYqgztq4QHicD80mqIkCBCYknFxhwxhNn+Y3g5RWQdRep
lpQbkneqRVp+qixMvu2FmOA4zRRoqObP7FyF1Yusvmroe0Y9SP2xTTmA9Zo73pay
wPUIuZ9eKGwIiFTtj1yQ1FdghLhzZgxcf3LHEHRkGnxgxxNITFxh4nd6fGIjNqM5
fQAY8z35lMXdeWjrhtaqgFYB+Z20YY0X7LJx39vYao0wqW8sZjX88TqHI1zXWLpU
k6UK9RqaNza5xc80wV+9/zjhr3dc1FRjBxI1DS/ufo33dUfvilxv9/LtWwUnKfKL
ns9LAgMBAAGjgYEwfzAdBgNVHQ4EFgQUQCpSY7ODhdyD6pdZHvfHoWRXWsIwHwYD
VR0jBBgwFoAUQCpSY7ODhdyD6pdZHvfHoWRXWsIwDwYDVR0TAQH/BAUwAwEB/zAs
BgNVHREEJTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJ
KoZIhvcNAQELBQADggEBAGKTIzGQsOqfD0+x15F2cu7FKjIo1ua0OiILAhPqGX65
kGcetjC/dJip2bGnw1NjG9WxEJNZ4YcsGrwh9egfnXXmfHNL0wzx/LTo2oysbXsN
nEj+cmzw3Lwjn/ywJc+AC221/xrmDfm3m/hMzLqncnj23ZAHqkXTSp5UtSMs+UDQ
my0AJOvsDGPVKHQsAX3JDjKHaoVJn4YqpHcIGmpjrNcQSvwUocDHPcC0ywco6SgF
Ylzy2bwWWdPd9Cz9JkAMb95nWc7Rwf/nxAqCjJFzKEisvrx7VZ+QSVI0nqJzt8V1
pbtWYH5gMFVstU3ghWdSLbAk4XufGYrIWAlA5mqjQ4o=
-----END CERTIFICATE-----

View File

@@ -221,8 +221,14 @@ describe.concurrent(() => {
["''", "''"],
['""', '""'],
])("test proxy env, http_proxy=%s https_proxy=%s", async (http_proxy, https_proxy) => {
using localServer = Bun.serve({
port: 0,
fetch() {
return new Response("OK");
},
});
const { exited, stderr: stream } = Bun.spawn({
cmd: [bunExe(), "-e", 'await fetch("https://example.com")'],
cmd: [bunExe(), "-e", `await fetch("${localServer.url}")`],
env: {
...bunEnv,
http_proxy: http_proxy,

View File

@@ -0,0 +1,48 @@
import { describe, expect, test } from "bun:test";
import { bunEnv, bunExe, tempDir } from "harness";
// Regression test for use-after-poison in builtin OutputTask callbacks
// inside command substitution $().
//
// The bug: output_waiting was only incremented for async writes but
// output_done was always incremented, so when stdout is sync (.pipe
// in cmdsub) the counter check `output_done >= output_waiting` fires
// prematurely, calling done() and freeing the builtin while IOWriter
// callbacks are still pending.
//
// Repro requires many ls tasks with errors — listing many entries
// alongside non-existent paths reliably triggers the ASAN
// use-after-poison.
describe("builtins in command substitution with errors should not crash", () => {
test("ls with errors in command substitution", async () => {
// Create a temp directory with many files to produce output,
// and include non-existent paths to produce errors.
const files: Record<string, string> = {};
for (let i = 0; i < 50; i++) {
files[`file${i}.txt`] = `content${i}`;
}
using dir = tempDir("shell-cmdsub", files);
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`
import { $ } from "bun";
$.throws(false);
await $\`echo $(ls $TEST_DIR/* /nonexistent_path_1 /nonexistent_path_2)\`;
console.log("done");
`,
],
env: { ...bunEnv, TEST_DIR: String(dir) },
stderr: "pipe",
stdout: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stdout).toContain("done");
expect(exitCode).toBe(0);
});
});

View File

@@ -0,0 +1,85 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
test("seq inf does not hang", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`import { $ } from "bun"; $.throws(false); const r = await $\`seq inf\`; process.exit(r.exitCode)`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("invalid argument");
expect(exitCode).toBe(1);
}, 10_000);
test("seq nan does not hang", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`import { $ } from "bun"; $.throws(false); const r = await $\`seq nan\`; process.exit(r.exitCode)`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("invalid argument");
expect(exitCode).toBe(1);
}, 10_000);
test("seq -inf does not hang", async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`import { $ } from "bun"; $.throws(false); const r = await $\`seq -- -inf\`; process.exit(r.exitCode)`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(stderr).toContain("invalid argument");
expect(exitCode).toBe(1);
}, 10_000);
test('[[ -d "" ]] does not crash', async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`import { $ } from "bun"; $.throws(false); const r = await $\`[[ -d "" ]]\`; process.exit(r.exitCode)`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
}, 10_000);
test('[[ -f "" ]] does not crash', async () => {
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
`import { $ } from "bun"; $.throws(false); const r = await $\`[[ -f "" ]]\`; process.exit(r.exitCode)`,
],
env: bunEnv,
stderr: "pipe",
});
const [stdout, stderr, exitCode] = await Promise.all([proc.stdout.text(), proc.stderr.text(), proc.exited]);
expect(exitCode).toBe(1);
}, 10_000);

View File

@@ -1,7 +1,7 @@
import { spawnSync } from "bun";
import { constants, Database, SQLiteError } from "bun:sqlite";
import { describe, expect, it } from "bun:test";
import { existsSync, readdirSync, realpathSync, writeFileSync } from "fs";
import { readdirSync, realpathSync } from "fs";
import { bunEnv, bunExe, isMacOS, isMacOSVersionAtLeast, isWindows, tempDirWithFiles } from "harness";
import { tmpdir } from "os";
import path from "path";
@@ -846,13 +846,15 @@ it("db.transaction()", () => {
// this bug was fixed by ensuring FinalObject has no more than 64 properties
it("inlineCapacity #987", async () => {
const path = tmpbase + "bun-987.db";
if (!existsSync(path)) {
const arrayBuffer = await (await fetch("https://github.com/oven-sh/bun/files/9265429/logs.log")).arrayBuffer();
writeFileSync(path, arrayBuffer);
}
const db = new Database(path);
const db = new Database(":memory:");
// Create schema matching the original regression test (media + logs tables)
db.exec(`
CREATE TABLE media (id INTEGER PRIMARY KEY, mid TEXT, name TEXT, url TEXT, duration INTEGER);
CREATE TABLE logs (mid INTEGER, duration INTEGER, start INTEGER, did TEXT, vid TEXT);
INSERT INTO media VALUES (1, 'm1', 'Test Media', 'http://test', 120);
INSERT INTO logs VALUES (1, 60, 1654100000, 'd1', 'v1');
INSERT INTO logs VALUES (1, 45, 1654200000, 'd2', 'v2');
`);
const query = `SELECT
media.mid,

View File

@@ -1,7 +1,20 @@
import { tls } from "harness";
import https from "node:https";
using server = Bun.serve({
port: 0,
tls,
fetch() {
return new Response("OK");
},
});
const { promise, resolve, reject } = Promise.withResolvers();
const client = https.request("https://example.com/", { agent: false });
const client = https.request(`https://localhost:${server.port}/`, {
agent: false,
ca: tls.cert,
rejectUnauthorized: true,
});
client.on("error", reject);
client.on("close", resolve);
client.end();

View File

@@ -212,10 +212,16 @@ describe("async context passes through", () => {
expect(s.getStore()).toBe(undefined);
});
test("fetch", async () => {
using server = Bun.serve({
port: 0,
fetch() {
return new Response("OK");
},
});
const s = new AsyncLocalStorage<string>();
await s.run("value", async () => {
expect(s.getStore()).toBe("value");
const response = await fetch("https://bun.sh") //
const response = await fetch(server.url) //
.then(r => {
expect(s.getStore()).toBe("value");
return true;

View File

@@ -5,7 +5,7 @@
*
* A handful of older tests do not run in Node in this file. These tests should be updated to run in Node, or deleted.
*/
import { bunEnv, bunExe, exampleSite, randomPort } from "harness";
import { bunEnv, bunExe, exampleSite, randomPort, tls as tlsCert } from "harness";
import { createTest } from "node-harness";
import { EventEmitter, once } from "node:events";
import nodefs, { unlinkSync } from "node:fs";
@@ -1081,9 +1081,19 @@ describe("node:http", () => {
});
test("should not decompress gzip, issue#4397", async () => {
using server = Bun.serve({
port: 0,
tls: tlsCert,
fetch() {
const body = Bun.gzipSync(Buffer.from("<html>Hello</html>"));
return new Response(body, {
headers: { "content-encoding": "gzip" },
});
},
});
const { promise, resolve } = Promise.withResolvers();
https
.request("https://bun.sh/", { headers: { "accept-encoding": "gzip" } }, res => {
.request(server.url, { ca: tlsCert.cert, headers: { "accept-encoding": "gzip" } }, res => {
res.on("data", function cb(chunk) {
resolve(chunk);
res.off("data", cb);
@@ -1632,6 +1642,75 @@ describe("HTTP Server Security Tests - Advanced", () => {
});
});
describe("Response Splitting Protection", () => {
test("rejects CRLF in statusMessage set via property assignment followed by res.end()", async () => {
const { promise: errorPromise, resolve: resolveError } = Promise.withResolvers<Error>();
server.on("request", (req, res) => {
res.statusCode = 200;
res.statusMessage = "OK\r\nSet-Cookie: admin=true";
try {
res.end("body");
} catch (e: any) {
resolveError(e);
res.statusMessage = "OK";
res.end("safe");
}
});
const response = (await sendRequest("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")) as string;
const err = await errorPromise;
expect((err as any).code).toBe("ERR_INVALID_CHAR");
// The injected Set-Cookie header must NOT appear in the response
expect(response).not.toInclude("Set-Cookie: admin=true");
});
test("rejects CRLF in statusMessage set via property assignment followed by res.write()", async () => {
const { promise: errorPromise, resolve: resolveError } = Promise.withResolvers<Error>();
server.on("request", (req, res) => {
res.statusCode = 200;
res.statusMessage = "OK\r\nX-Injected: evil";
try {
res.write("chunk");
} catch (e: any) {
resolveError(e);
res.statusMessage = "OK";
res.end("safe");
}
});
const response = (await sendRequest("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")) as string;
const err = await errorPromise;
expect((err as any).code).toBe("ERR_INVALID_CHAR");
expect(response).not.toInclude("X-Injected: evil");
});
test("rejects CRLF in statusMessage passed to writeHead()", async () => {
server.on("request", (req, res) => {
expect(() => {
res.writeHead(200, "OK\r\nX-Injected: evil");
}).toThrow(/Invalid character in statusMessage/);
res.writeHead(200, "OK");
res.end("safe");
});
const response = (await sendRequest("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")) as string;
expect(response).not.toInclude("X-Injected");
expect(response).toInclude("safe");
});
test("allows valid statusMessage without control characters", async () => {
server.on("request", (req, res) => {
res.statusCode = 200;
res.statusMessage = "Everything Is Fine";
res.end("ok");
});
const response = (await sendRequest("GET / HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n")) as string;
expect(response).toInclude("200 Everything Is Fine");
expect(response).toInclude("ok");
});
});
test("Server should not crash in clientError is emitted when calling destroy", async () => {
await using server = http.createServer(async (req, res) => {
res.end("Hello World");

View File

@@ -821,60 +821,74 @@ for (const nodeExecutable of [nodeExe(), bunExe()]) {
expect(typeof settings.maxHeaderListSize).toBe("number");
expect(typeof settings.maxHeaderSize).toBe("number");
};
const { promise, resolve, reject } = Promise.withResolvers();
const client = http2.connect("https://www.example.com");
client.on("error", reject);
expect(client.connecting).toBeTrue();
expect(client.alpnProtocol).toBeUndefined();
expect(client.encrypted).toBeTrue();
expect(client.closed).toBeFalse();
expect(client.destroyed).toBeFalse();
expect(client.originSet.length).toBe(1);
expect(client.pendingSettingsAck).toBeTrue();
assertSettings(client.localSettings);
expect(client.remoteSettings).toBeNull();
const headers = { ":path": "/" };
const req = client.request(headers);
expect(req.closed).toBeFalse();
expect(req.destroyed).toBeFalse();
// we always asign a stream id to the request
expect(req.pending).toBeFalse();
expect(typeof req.id).toBe("number");
expect(req.session).toBeDefined();
expect(req.sentHeaders).toEqual({
":authority": "www.example.com",
":method": "GET",
":path": "/",
":scheme": "https",
const h2Server = http2.createSecureServer({ ...TLS_CERT, allowHTTP1: false });
h2Server.on("stream", (stream, headers) => {
stream.respond({ ":status": 200 });
stream.end("OK");
});
expect(req.sentTrailers).toBeUndefined();
expect(req.sentInfoHeaders.length).toBe(0);
expect(req.scheme).toBe("https");
let response_headers = null;
req.on("response", (headers, flags) => {
response_headers = headers;
});
req.resume();
req.on("end", () => {
resolve();
});
await promise;
expect(response_headers[":status"]).toBe(200);
const settings = client.remoteSettings;
const localSettings = client.localSettings;
assertSettings(settings);
assertSettings(localSettings);
expect(settings).toEqual(client.remoteSettings);
expect(localSettings).toEqual(client.localSettings);
client.destroy();
expect(client.connecting).toBeFalse();
expect(client.alpnProtocol).toBe("h2");
expect(client.pendingSettingsAck).toBeFalse();
expect(client.destroyed).toBeTrue();
expect(client.closed).toBeTrue();
expect(req.closed).toBeTrue();
expect(req.destroyed).toBeTrue();
expect(req.rstCode).toBe(http2.constants.NGHTTP2_NO_ERROR);
const { promise: listenPromise, resolve: listenResolve } = Promise.withResolvers();
h2Server.listen(0, () => listenResolve());
await listenPromise;
const serverAddress = h2Server.address();
const serverUrl = `https://localhost:${serverAddress.port}`;
try {
const { promise, resolve, reject } = Promise.withResolvers();
const client = http2.connect(serverUrl, TLS_OPTIONS);
client.on("error", reject);
expect(client.connecting).toBeTrue();
expect(client.alpnProtocol).toBeUndefined();
expect(client.encrypted).toBeTrue();
expect(client.closed).toBeFalse();
expect(client.destroyed).toBeFalse();
expect(client.originSet.length).toBe(1);
expect(client.pendingSettingsAck).toBeTrue();
assertSettings(client.localSettings);
expect(client.remoteSettings).toBeNull();
const headers = { ":path": "/" };
const req = client.request(headers);
expect(req.closed).toBeFalse();
expect(req.destroyed).toBeFalse();
// we always asign a stream id to the request
expect(req.pending).toBeFalse();
expect(typeof req.id).toBe("number");
expect(req.session).toBeDefined();
expect(req.sentHeaders).toEqual({
":authority": `localhost:${serverAddress.port}`,
":method": "GET",
":path": "/",
":scheme": "https",
});
expect(req.sentTrailers).toBeUndefined();
expect(req.sentInfoHeaders.length).toBe(0);
expect(req.scheme).toBe("https");
let response_headers = null;
req.on("response", (headers, flags) => {
response_headers = headers;
});
req.resume();
req.on("end", () => {
resolve();
});
await promise;
expect(response_headers[":status"]).toBe(200);
const settings = client.remoteSettings;
const localSettings = client.localSettings;
assertSettings(settings);
assertSettings(localSettings);
expect(settings).toEqual(client.remoteSettings);
expect(localSettings).toEqual(client.localSettings);
client.destroy();
expect(client.connecting).toBeFalse();
expect(client.alpnProtocol).toBe("h2");
expect(client.pendingSettingsAck).toBeFalse();
expect(client.destroyed).toBeTrue();
expect(client.closed).toBeTrue();
expect(req.closed).toBeTrue();
expect(req.destroyed).toBeTrue();
expect(req.rstCode).toBe(http2.constants.NGHTTP2_NO_ERROR);
} finally {
h2Server.close();
}
});
it("ping events should work", async () => {
await using server = await nodeEchoServer(paddingStrategy);

View File

@@ -577,19 +577,27 @@ describe("fetch", () => {
});
it.concurrent('redirect: "follow"', async () => {
using target = Bun.serve({
port: 0,
tls,
fetch() {
return new Response("redirected!");
},
});
using server = Bun.serve({
port: 0,
fetch(req) {
return new Response(null, {
status: 302,
headers: {
Location: "https://example.com",
Location: target.url.href,
},
});
},
});
const response = await fetch(`http://${server.hostname}:${server.port}`, {
redirect: "follow",
tls: { ca: tls.cert },
});
expect(response.status).toBe(200);
expect(response.headers.get("location")).toBe(null);
@@ -734,8 +742,15 @@ it.concurrent("simultaneous HTTPS fetch", async () => {
});
it.concurrent("website with tlsextname", async () => {
// irony
await fetch("https://bun.sh", { method: "HEAD" });
using server = Bun.serve({
port: 0,
tls,
fetch() {
return new Response("OK");
},
});
const resp = await fetch(server.url, { method: "HEAD", tls: { ca: tls.cert } });
expect(resp.status).toBe(200);
});
function testBlobInterface(blobbyConstructor: { (..._: any[]): any }, hasBlobFn?: boolean) {

View File

@@ -30,94 +30,110 @@ async function createServer(cert: TLSOptions, callback: (port: number) => Promis
describe.concurrent("fetch-tls", () => {
it("can handle multiple requests with non native checkServerIdentity", async () => {
async function request() {
let called = false;
const result = await fetch("https://www.example.com", {
keepalive: false,
tls: {
checkServerIdentity(hostname: string, cert: tls.PeerCertificate) {
called = true;
return tls.checkServerIdentity(hostname, cert);
},
},
}).then((res: Response) => res.blob());
expect(result?.size).toBeGreaterThan(0);
expect(called).toBe(true);
}
const promises = [];
for (let i = 0; i < 5; i++) {
promises.push(request());
}
await Promise.all(promises);
});
it("fetch with valid tls should not throw", async () => {
const promises = [`https://example.com`, `https://www.example.com`].map(async url => {
const result = await fetch(url, { keepalive: false }).then((res: Response) => res.blob());
expect(result?.size).toBeGreaterThan(0);
});
await Promise.all(promises);
});
it("fetch with valid tls and non-native checkServerIdentity should work", async () => {
for (const isBusy of [true, false]) {
let count = 0;
const promises = [`https://example.com`, `https://www.example.com`].map(async url => {
await fetch(url, {
await createServer(CERT_LOCALHOST_IP, async port => {
async function request() {
let called = false;
const result = await fetch(`https://localhost:${port}`, {
keepalive: false,
tls: {
ca: validTls.cert,
checkServerIdentity(hostname: string, cert: tls.PeerCertificate) {
count++;
expect(url).toContain(hostname);
called = true;
return tls.checkServerIdentity(hostname, cert);
},
},
}).then((res: Response) => res.blob());
});
if (isBusy) {
const start = performance.now();
while (performance.now() - start < 500) {}
expect(result?.size).toBeGreaterThan(0);
expect(called).toBe(true);
}
const promises = [];
for (let i = 0; i < 5; i++) {
promises.push(request());
}
await Promise.all(promises);
expect(count).toBe(2);
}
});
});
it("fetch with valid tls should not throw", async () => {
await createServer(CERT_LOCALHOST_IP, async port => {
const urls = [`https://localhost:${port}`, `https://127.0.0.1:${port}`];
const promises = urls.map(async url => {
const result = await fetch(url, { keepalive: false, tls: { ca: validTls.cert } }).then((res: Response) =>
res.blob(),
);
expect(result?.size).toBeGreaterThan(0);
});
await Promise.all(promises);
});
});
it("fetch with valid tls and non-native checkServerIdentity should work", async () => {
let count = 0;
const promises = [`https://example.com`, `https://www.example.com`].map(async url => {
await fetch(url, {
keepalive: false,
tls: {
checkServerIdentity(hostname: string, cert: tls.PeerCertificate) {
count++;
expect(url).toContain(hostname);
throw new Error("CustomError");
},
},
});
await createServer(CERT_LOCALHOST_IP, async port => {
for (const isBusy of [true, false]) {
let count = 0;
const urls = [`https://localhost:${port}`, `https://127.0.0.1:${port}`];
const promises = urls.map(async url => {
await fetch(url, {
keepalive: false,
tls: {
ca: validTls.cert,
checkServerIdentity(hostname: string, cert: tls.PeerCertificate) {
count++;
return tls.checkServerIdentity(hostname, cert);
},
},
}).then((res: Response) => res.blob());
});
if (isBusy) {
const start = performance.now();
while (performance.now() - start < 500) {}
}
await Promise.all(promises);
expect(count).toBe(2);
}
});
});
it("fetch with valid tls and non-native checkServerIdentity that throws should reject", async () => {
await createServer(CERT_LOCALHOST_IP, async port => {
let count = 0;
const urls = [`https://localhost:${port}`, `https://127.0.0.1:${port}`];
const promises = urls.map(async url => {
await fetch(url, {
keepalive: false,
tls: {
ca: validTls.cert,
checkServerIdentity(hostname: string, cert: tls.PeerCertificate) {
count++;
throw new Error("CustomError");
},
},
});
});
const start = performance.now();
while (performance.now() - start < 1000) {}
expect((await Promise.allSettled(promises)).every(p => p.status === "rejected")).toBe(true);
expect(count).toBe(2);
});
const start = performance.now();
while (performance.now() - start < 1000) {}
expect((await Promise.allSettled(promises)).every(p => p.status === "rejected")).toBe(true);
expect(count).toBe(2);
});
it("fetch with rejectUnauthorized: false should not call checkServerIdentity", async () => {
let count = 0;
await createServer(CERT_LOCALHOST_IP, async port => {
let count = 0;
await fetch("https://example.com", {
keepalive: false,
tls: {
rejectUnauthorized: false,
checkServerIdentity(hostname: string, cert: tls.PeerCertificate) {
count++;
return tls.checkServerIdentity(hostname, cert);
await fetch(`https://localhost:${port}`, {
keepalive: false,
tls: {
rejectUnauthorized: false,
checkServerIdentity(hostname: string, cert: tls.PeerCertificate) {
count++;
return tls.checkServerIdentity(hostname, cert);
},
},
},
}).then((res: Response) => res.blob());
expect(count).toBe(0);
}).then((res: Response) => res.blob());
expect(count).toBe(0);
});
});
it("fetch with self-sign tls should throw", async () => {
@@ -152,20 +168,23 @@ describe.concurrent("fetch-tls", () => {
});
it("fetch with checkServerIdentity failing should throw", async () => {
try {
await fetch(`https://example.com`, {
keepalive: false,
tls: {
checkServerIdentity() {
return new Error("CustomError");
await createServer(CERT_LOCALHOST_IP, async port => {
try {
await fetch(`https://localhost:${port}`, {
keepalive: false,
tls: {
ca: validTls.cert,
checkServerIdentity() {
return new Error("CustomError");
},
},
},
}).then((res: Response) => res.blob());
}).then((res: Response) => res.blob());
expect.unreachable();
} catch (e: any) {
expect(e.message).toBe("CustomError");
}
expect.unreachable();
} catch (e: any) {
expect(e.message).toBe("CustomError");
}
});
});
it("fetch with self-sign certificate tls + rejectUnauthorized: false should not throw", async () => {

View File

@@ -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)", () => {

View File

@@ -109,13 +109,22 @@ describe("HTMLRewriter", () => {
await gcTick();
let content;
{
using contentServer = Bun.serve({
port: 0,
fetch(req) {
return new Response("<h1>Hello from content server</h1>", {
headers: { "Content-Type": "text/html" },
});
},
});
using server = Bun.serve({
port: 0,
fetch(req) {
return new HTMLRewriter()
.on("div", {
async element(element) {
content = await fetch("https://www.example.com/").then(res => res.text());
content = await fetch(`http://localhost:${contentServer.port}/`).then(res => res.text());
element.setInnerContent(content, { html: true });
},
})

View File

@@ -1,28 +1,28 @@
-----BEGIN PRIVATE KEY-----
MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCIzOJskt6VkEJY
XKSJv/Gdil3XYkjk3NVc/+m+kzqnkTRbPtT9w+IGWgmJhuf9DJPLCwHFAEFarVwV
x16Q0PbU4ajXaLRHEYGhrH10oTMjQnJ24xVm26mxRXPQa5vaLpWJqNyIdNLIQLe+
UXUOzSGGsFTRMAjvYrkzjBe4ZUnaZV+aFY/ug0jfzeA1dJjzKZs6+yTJRbsuWUEb
8MsDmT4v+kBZDKdaDn7AFDWRVqx/38BnqsRzkM0CxpnyT2kRzw5zQajIE13gdTJo
1EHvYSUkkxrY5m30Rl9BuBBZBjhMzOHq0fYVVooHO+sf4XHPgvFTTxJum85u7J1J
oEUjrLKtAgMBAAECggEACInVNhaiqu4infZGVMy0rXMV8VwSlapM7O2SLtFsr0nK
XUmaLK6dvGzBPKK9dxdiYCFzPlMKQTkhzsAvYFWSmm3tRmikG+11TFyCRhXLpc8/
ark4vD9Io6ZkmKUmyKLwtXNjNGcqQtJ7RXc7Ga3nAkueN6JKZHqieZusXVeBGQ70
YH1LKyVNBeJggbj+g9rqaksPyNJQ8EWiNTJkTRQPazZ0o1VX/fzDFyr/a5npFtHl
4BHfafv9o1Xyr70Kie8CYYRJNViOCN+ylFs7Gd3XRaAkSkgMT/7DzrHdEM2zrrHK
yNg2gyDVX9UeEJG2X5UtU0o9BVW7WBshz/2hqIUHoQKBgQC8zsRFvC7u/rGr5vRR
mhZZG+Wvg03/xBSuIgOrzm+Qie6mAzOdVmfSL/pNV9EFitXt1yd2ROo31AbS7Evy
Bm/QVKr2mBlmLgov3B7O/e6ABteooOL7769qV/v+yo8VdEg0biHmsfGIIXDe3Lwl
OT0XwF9r/SeZLbw1zfkSsUVG/QKBgQC5fANM3Dc9LEek+6PHv5+eC1cKkyioEjUl
/y1VUD00aABI1TUcdLF3BtFN2t/S6HW0hrP3KwbcUfqC25k+GDLh1nM6ZK/gI3Yn
IGtCHxtE3S6jKhE9QcK/H+PzGVKWge9SezeYRP0GHJYDrTVTA8Kt9HgoZPPeReJl
+Ss9c8ThcQKBgECX6HQHFnNzNSufXtSQB7dCoQizvjqTRZPxVRoxDOABIGExVTYt
umUhPtu5AGyJ+/hblEeU+iBRbGg6qRzK8PPwE3E7xey8MYYAI5YjL7YjISKysBUL
AhM6uJ6Jg/wOBSnSx8xZ8kzlS+0izUda1rjKeprCSArSp8IsjlrDxPStAoGAEcPr
+P+altRX5Fhpvmb/Hb8OTif8G+TqjEIdkG9H/W38oP0ywg/3M2RGxcMx7txu8aR5
NjI7zPxZFxF7YvQkY3cLwEsGgVxEI8k6HLIoBXd90Qjlb82NnoqqZY1GWL4HMwo0
L/Rjm6M/Rwje852Hluu0WoIYzXA6F/Q+jPs6nzECgYAxx4IbDiGXuenkwSF1SUyj
NwJXhx4HDh7U6EO/FiPZE5BHE3BoTrFu3o1lzverNk7G3m+j+m1IguEAalHlukYl
rip9iUISlKYqbYZdLBoLwHAfHhszdrjqn8/v6oqbB5yR3HXjPFUWJo0WJ2pqJp56
ZshgmQQ/5Khoj6x0/dMPSg==
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCt7iqkEIco372h
v19q0zjaYbm6gzxEnR45UjpQYqgztq4QHicD80mqIkCBCYknFxhwxhNn+Y3g5RWQ
dReplpQbkneqRVp+qixMvu2FmOA4zRRoqObP7FyF1Yusvmroe0Y9SP2xTTmA9Zo7
3paywPUIuZ9eKGwIiFTtj1yQ1FdghLhzZgxcf3LHEHRkGnxgxxNITFxh4nd6fGIj
NqM5fQAY8z35lMXdeWjrhtaqgFYB+Z20YY0X7LJx39vYao0wqW8sZjX88TqHI1zX
WLpUk6UK9RqaNza5xc80wV+9/zjhr3dc1FRjBxI1DS/ufo33dUfvilxv9/LtWwUn
KfKLns9LAgMBAAECggEAAacPHM2G7GBIm/9rCr6tvihNgD8M685zOOZAqGYn9CqY
cYHC4gtF/L2U6CBj2pNAoCwo3LXUkD+6r7MYKXAgqQg3HTCM4rwFbhD1rU8FVHfh
OL0QwwZ2ut95DVdjoxTAlEN9ZcdSFc//llMJ1cF8lxoVvKFc4cv3uCI2mcaJk858
iABfJLl3yfdv1xtpAuOfXf66sXbAmn5NQfN0qTEg2iOdgb4BUee5Wb35MakDQb6+
/s7/bWB+ublZzYt12ChIh1jkBBHaGyQ8mFnPj99ZAJdFjAzi6ydoJ0a2rCVY7Ugs
bkhnzDUtAaHKxo9JXaqIwbUaVFkX8dDhbg82dJrWUQKBgQDb7hNR0bJFW845N19M
74p2PM+0dIiVzwxAg4E2dXDVe39awO/tw8Vu1o1+NPFhWAzGcidP7pAHmPEgRTVO
7LA2P3CDXpkAEx5E0QW6QWZGqHfSa3+P1AvetvAV+OxtlDphcNeLApY16TUVOKZg
SZlxW2e0dZylbHewgLBTIV9wUQKBgQDKdML+JD18WfenPeowsw8HzKdaw01iGiV1
fvTjEXu6YxPPynWFMuj5gjBQodXM2vv0EsQBAPKYfe0nzRFL2kNuYs7TLoaNxqkp
DNfJ2Ww5OSg7Mp76XgppeKKlsXLyUMYHHrDh6MRi5jvWtiHRpaNmV3cHMRs22c+B
cqKP5Zma2wKBgCPNnS2Lsrbh3C+qWQRgVq0q9zFMa1PgEgGKpwVjlwvaAACZOjX9
0e1aVkx+d/E98U55FPdJQf9Koa58NdJ0a7dZGor4YnYFpr7TPFh2/xxvnpoN0AVt
IsWOCIW7MVohcGOeiChkMmnyXibnQwaX1LgEhlx1bRvtDYsZWBsgarYRAoGAARvo
oYnDSHYZtDHToZapg2pslEOzndD02ZLrdn73BYtbZWz/fc5MlmlPKHHqgOfGL40W
w8akjY9LCEfIS3kTm3wxE9kSZZ5r+MyYNgPZ4upcPQ7G7iortm4xveSd85PbsdhK
McKbqMsIEuIGh2Z34ayi+0galQ9WYqglGdKxJ7cCgYEAuSPBHa+en0xaraZNRvMk
OfV9Su/wrpR3TXSeo0E1mZHLwq1JwulpfO1SjxTH5uOJtG0tusl122wfm0KjrXUO
vG5/It+X4u1Nv9oWj+z1+EV4fQrQ/Coqcc1r+5w1yzfURkKlHh74jbK5Yy/KfXrE
eqbbJD40tKhY8ho15D3iCSo=
-----END PRIVATE KEY-----

View File

@@ -1,23 +1,24 @@
-----BEGIN CERTIFICATE-----
MIID5jCCAs6gAwIBAgIUN7coIsdMcLo9amZfkwogu0YkeLEwDQYJKoZIhvcNAQEL
MIIEDDCCAvSgAwIBAgIUbddWE2woW5e96uC4S2fd2M0AsFAwDQYJKoZIhvcNAQEL
BQAwfjELMAkGA1UEBhMCU0UxDjAMBgNVBAgMBVN0YXRlMREwDwYDVQQHDAhMb2Nh
dGlvbjEaMBgGA1UECgwRT3JnYW5pemF0aW9uIE5hbWUxHDAaBgNVBAsME09yZ2Fu
aXphdGlvbmFsIFVuaXQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yMzA5MjExNDE2
MjNaFw0yNDA5MjAxNDE2MjNaMH4xCzAJBgNVBAYTAlNFMQ4wDAYDVQQIDAVTdGF0
aXphdGlvbmFsIFVuaXQxEjAQBgNVBAMMCWxvY2FsaG9zdDAeFw0yNjAyMTMyMzEx
MjlaFw0zNjAyMTEyMzExMjlaMH4xCzAJBgNVBAYTAlNFMQ4wDAYDVQQIDAVTdGF0
ZTERMA8GA1UEBwwITG9jYXRpb24xGjAYBgNVBAoMEU9yZ2FuaXphdGlvbiBOYW1l
MRwwGgYDVQQLDBNPcmdhbml6YXRpb25hbCBVbml0MRIwEAYDVQQDDAlsb2NhbGhv
c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCIzOJskt6VkEJYXKSJ
v/Gdil3XYkjk3NVc/+m+kzqnkTRbPtT9w+IGWgmJhuf9DJPLCwHFAEFarVwVx16Q
0PbU4ajXaLRHEYGhrH10oTMjQnJ24xVm26mxRXPQa5vaLpWJqNyIdNLIQLe+UXUO
zSGGsFTRMAjvYrkzjBe4ZUnaZV+aFY/ug0jfzeA1dJjzKZs6+yTJRbsuWUEb8MsD
mT4v+kBZDKdaDn7AFDWRVqx/38BnqsRzkM0CxpnyT2kRzw5zQajIE13gdTJo1EHv
YSUkkxrY5m30Rl9BuBBZBjhMzOHq0fYVVooHO+sf4XHPgvFTTxJum85u7J1JoEUj
rLKtAgMBAAGjXDBaMA4GA1UdDwEB/wQEAwIDiDATBgNVHSUEDDAKBggrBgEFBQcD
ATAUBgNVHREEDTALgglsb2NhbGhvc3QwHQYDVR0OBBYEFNzx4Rfs9m8XR5ML0WsI
sorKmB4PMA0GCSqGSIb3DQEBCwUAA4IBAQB87iQy8R0fiOky9WTcyzVeMaavS3MX
iTe1BRn1OCyDq+UiwwoNz7zdzZJFEmRtFBwPNFOe4HzLu6E+7yLFR552eYRHlqIi
/fiLb5JiZfPtokUHeqwELWBsoXtU8vKxViPiLZ09jkWOPZWo7b/xXd6QYykBfV91
usUXLzyTD2orMagpqNksLDGS3p3ggHEJBZtRZA8R7kPEw98xZHznOQpr26iv8kYz
ZWdLFoFdwgFBSfxePKax5rfo+FbwdrcTX0MhbORyiu2XsBAghf8s2vKDkHg2UQE8
haonxFYMFaASfaZ/5vWKYDTCJkJ67m/BtkpRafFEO+ad1i1S61OjfxH4
c3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCt7iqkEIco372hv19q
0zjaYbm6gzxEnR45UjpQYqgztq4QHicD80mqIkCBCYknFxhwxhNn+Y3g5RWQdRep
lpQbkneqRVp+qixMvu2FmOA4zRRoqObP7FyF1Yusvmroe0Y9SP2xTTmA9Zo73pay
wPUIuZ9eKGwIiFTtj1yQ1FdghLhzZgxcf3LHEHRkGnxgxxNITFxh4nd6fGIjNqM5
fQAY8z35lMXdeWjrhtaqgFYB+Z20YY0X7LJx39vYao0wqW8sZjX88TqHI1zXWLpU
k6UK9RqaNza5xc80wV+9/zjhr3dc1FRjBxI1DS/ufo33dUfvilxv9/LtWwUnKfKL
ns9LAgMBAAGjgYEwfzAdBgNVHQ4EFgQUQCpSY7ODhdyD6pdZHvfHoWRXWsIwHwYD
VR0jBBgwFoAUQCpSY7ODhdyD6pdZHvfHoWRXWsIwDwYDVR0TAQH/BAUwAwEB/zAs
BgNVHREEJTAjgglsb2NhbGhvc3SHBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAEwDQYJ
KoZIhvcNAQELBQADggEBAGKTIzGQsOqfD0+x15F2cu7FKjIo1ua0OiILAhPqGX65
kGcetjC/dJip2bGnw1NjG9WxEJNZ4YcsGrwh9egfnXXmfHNL0wzx/LTo2oysbXsN
nEj+cmzw3Lwjn/ywJc+AC221/xrmDfm3m/hMzLqncnj23ZAHqkXTSp5UtSMs+UDQ
my0AJOvsDGPVKHQsAX3JDjKHaoVJn4YqpHcIGmpjrNcQSvwUocDHPcC0ywco6SgF
Ylzy2bwWWdPd9Cz9JkAMb95nWc7Rwf/nxAqCjJFzKEisvrx7VZ+QSVI0nqJzt8V1
pbtWYH5gMFVstU3ghWdSLbAk4XufGYrIWAlA5mqjQ4o=
-----END CERTIFICATE-----

View File

@@ -0,0 +1,63 @@
// Regression test for TLS upgrade raw socket leak (#12117, #24118, #25948)
// When a TCP socket is upgraded to TLS via tls.connect({ socket }),
// both a TLS wrapper and a raw TCP wrapper are created in Zig.
// Previously, the raw socket's has_pending_activity was never set to
// false on close, causing it (and all its retained objects) to leak.
import { describe, expect, it } from "bun:test";
import { tls as COMMON_CERT, expectMaxObjectTypeCount } from "harness";
import { once } from "node:events";
import net from "node:net";
import tls from "node:tls";
describe("TLS upgrade", () => {
it("should not leak TLSSocket objects after close", async () => {
// Create a TLS server that echoes data and closes
const server = tls.createServer(
{
key: COMMON_CERT.key,
cert: COMMON_CERT.cert,
},
socket => {
socket.end("hello");
},
);
await once(server.listen(0, "127.0.0.1"), "listening");
const port = (server.address() as net.AddressInfo).port;
// Simulate the MongoDB driver pattern: create a plain TCP socket,
// then upgrade it to TLS via tls.connect({ socket }).
// Do this multiple times to accumulate leaked objects.
const iterations = 50;
try {
for (let i = 0; i < iterations; i++) {
const tcpSocket = net.createConnection({ host: "127.0.0.1", port });
await once(tcpSocket, "connect");
const tlsSocket = tls.connect({
socket: tcpSocket,
ca: COMMON_CERT.cert,
rejectUnauthorized: false,
});
await once(tlsSocket, "secureConnect");
// Read any data and destroy the TLS socket (simulates SDAM close)
tlsSocket.on("data", () => {});
tlsSocket.destroy();
await once(tlsSocket, "close");
}
} finally {
server.close();
await once(server, "close");
}
// After all connections are closed and GC runs, the TLSSocket count
// should be low. Before the fix, each iteration would leak 1 raw
// TLSSocket (the TCP wrapper from upgradeTLS), accumulating over time.
// Allow some slack for prototypes/structures (typically 2-3 baseline).
await expectMaxObjectTypeCount(expect, "TLSSocket", 10, 1000);
});
});

View 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);
});

View File

@@ -0,0 +1,8 @@
import { expect, test } from "bun:test";
// https://github.com/oven-sh/bun/issues/27014
test("Bun.stripANSI does not hang on non-ANSI control characters", () => {
const s = "\u0016zo\u00BAd\u0019\u00E8\u00E0\u0013?\u00C1+\u0014d\u00D3\u00E9";
const result = Bun.stripANSI(s);
expect(result).toBe(s);
});

View File

@@ -0,0 +1,31 @@
import { expect, test } from "bun:test";
import { X509Certificate } from "crypto";
import { readFileSync } from "fs";
import { join } from "path";
const certPem = readFileSync(join(import.meta.dir, "../../js/node/test/fixtures/keys/agent1-cert.pem"));
test("issuerCertificate should return undefined for directly-parsed certificates without crashing", () => {
const cert = new X509Certificate(certPem);
// issuerCertificate is only populated for certificates obtained from TLS
// connections with a peer certificate chain. For directly parsed certs,
// it should be undefined (matching Node.js behavior).
expect(cert.issuerCertificate).toBeUndefined();
});
test("X509Certificate properties should not crash on valid certificates", () => {
const cert = new X509Certificate(certPem);
// These should all work without segfaulting
expect(cert.subject).toBeDefined();
expect(cert.issuer).toBeDefined();
expect(cert.validFrom).toBeDefined();
expect(cert.validTo).toBeDefined();
expect(cert.fingerprint).toBeDefined();
expect(cert.fingerprint256).toBeDefined();
expect(cert.fingerprint512).toBeDefined();
expect(cert.serialNumber).toBeDefined();
expect(cert.raw).toBeInstanceOf(Uint8Array);
expect(cert.ca).toBe(false);
});

View File

@@ -0,0 +1,89 @@
import { expect, test } from "bun:test";
import http from "node:http";
test("ClientRequest.setHeaders should not throw ERR_HTTP_HEADERS_SENT on new request", async () => {
await using server = Bun.serve({
port: 0,
fetch(req) {
return new Response(req.headers.get("x-test") ?? "missing");
},
});
const { resolve, reject, promise } = Promise.withResolvers<string>();
const req = http.request(`http://localhost:${server.port}/test`, { method: "GET" }, res => {
let data = "";
res.on("data", (chunk: Buffer) => {
data += chunk.toString();
});
res.on("end", () => resolve(data));
});
req.on("error", reject);
// This should not throw - headers haven't been sent yet
req.setHeaders(new Headers({ "x-test": "value" }));
req.end();
const body = await promise;
expect(body).toBe("value");
});
test("ClientRequest.setHeaders works with Map", async () => {
await using server = Bun.serve({
port: 0,
fetch(req) {
return new Response(req.headers.get("x-map-test") ?? "missing");
},
});
const { resolve, reject, promise } = Promise.withResolvers<string>();
const req = http.request(`http://localhost:${server.port}/test`, { method: "GET" }, res => {
let data = "";
res.on("data", (chunk: Buffer) => {
data += chunk.toString();
});
res.on("end", () => resolve(data));
});
req.on("error", reject);
req.setHeaders(new Map([["x-map-test", "map-value"]]));
req.end();
const body = await promise;
expect(body).toBe("map-value");
});
test("ServerResponse.setHeaders should not throw before headers are sent", async () => {
const { resolve, reject, promise } = Promise.withResolvers<string>();
const server = http.createServer((req, res) => {
// This should not throw - headers haven't been sent yet
res.setHeaders(new Headers({ "x-custom": "server-value" }));
res.writeHead(200);
res.end("ok");
});
try {
server.listen(0, () => {
const port = (server.address() as any).port;
try {
const req = http.request(`http://localhost:${port}/test`, res => {
resolve(res.headers["x-custom"] as string);
});
req.on("error", reject);
req.end();
} catch (e) {
reject(e);
}
});
expect(await promise).toBe("server-value");
} finally {
server.close();
}
});

View File

@@ -0,0 +1,336 @@
import { describe, expect, test } from "bun:test";
import http from "node:http";
// Regression test for https://github.com/oven-sh/bun/issues/27061
// When http.ClientRequest.write() is called more than once (streaming data in chunks),
// Bun was stripping the explicitly-set Content-Length header and switching to
// Transfer-Encoding: chunked. Node.js preserves Content-Length in all cases.
describe("node:http ClientRequest preserves explicit Content-Length", () => {
test("with multiple req.write() calls", async () => {
const { promise, resolve, reject } = Promise.withResolvers<{
contentLength: string | undefined;
transferEncoding: string | undefined;
bodyLength: number;
}>();
const server = http.createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => {
resolve({
contentLength: req.headers["content-length"],
transferEncoding: req.headers["transfer-encoding"],
bodyLength: Buffer.concat(chunks).length,
});
res.writeHead(200);
res.end("ok");
});
});
await new Promise<void>(res => server.listen(0, "127.0.0.1", res));
const port = (server.address() as any).port;
try {
const chunk1 = Buffer.alloc(100, "a");
const chunk2 = Buffer.alloc(100, "b");
const totalLength = chunk1.length + chunk2.length;
const req = http.request({
hostname: "127.0.0.1",
port,
method: "POST",
headers: {
"Content-Length": totalLength.toString(),
},
});
await new Promise<void>((res, rej) => {
req.on("error", rej);
req.on("response", () => res());
req.write(chunk1);
req.write(chunk2);
req.end();
});
const result = await promise;
expect(result.contentLength).toBe("200");
expect(result.transferEncoding).toBeUndefined();
expect(result.bodyLength).toBe(200);
} finally {
server.close();
}
});
test("with req.write() + req.end(data)", async () => {
const { promise, resolve, reject } = Promise.withResolvers<{
contentLength: string | undefined;
transferEncoding: string | undefined;
bodyLength: number;
}>();
const server = http.createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => {
resolve({
contentLength: req.headers["content-length"],
transferEncoding: req.headers["transfer-encoding"],
bodyLength: Buffer.concat(chunks).length,
});
res.writeHead(200);
res.end("ok");
});
});
await new Promise<void>(res => server.listen(0, "127.0.0.1", res));
const port = (server.address() as any).port;
try {
const chunk1 = Buffer.alloc(100, "a");
const chunk2 = Buffer.alloc(100, "b");
const totalLength = chunk1.length + chunk2.length;
const req = http.request({
hostname: "127.0.0.1",
port,
method: "POST",
headers: {
"Content-Length": totalLength.toString(),
},
});
await new Promise<void>((res, rej) => {
req.on("error", rej);
req.on("response", () => res());
req.write(chunk1);
req.end(chunk2);
});
const result = await promise;
expect(result.contentLength).toBe("200");
expect(result.transferEncoding).toBeUndefined();
expect(result.bodyLength).toBe(200);
} finally {
server.close();
}
});
test("with three req.write() calls", async () => {
const { promise, resolve, reject } = Promise.withResolvers<{
contentLength: string | undefined;
transferEncoding: string | undefined;
bodyLength: number;
}>();
const server = http.createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => {
resolve({
contentLength: req.headers["content-length"],
transferEncoding: req.headers["transfer-encoding"],
bodyLength: Buffer.concat(chunks).length,
});
res.writeHead(200);
res.end("ok");
});
});
await new Promise<void>(res => server.listen(0, "127.0.0.1", res));
const port = (server.address() as any).port;
try {
const chunk1 = Buffer.alloc(100, "a");
const chunk2 = Buffer.alloc(100, "b");
const chunk3 = Buffer.alloc(100, "c");
const totalLength = chunk1.length + chunk2.length + chunk3.length;
const req = http.request({
hostname: "127.0.0.1",
port,
method: "POST",
headers: {
"Content-Length": totalLength.toString(),
},
});
await new Promise<void>((res, rej) => {
req.on("error", rej);
req.on("response", () => res());
req.write(chunk1);
req.write(chunk2);
req.write(chunk3);
req.end();
});
const result = await promise;
expect(result.contentLength).toBe("300");
expect(result.transferEncoding).toBeUndefined();
expect(result.bodyLength).toBe(300);
} finally {
server.close();
}
});
test("single req.write() still works", async () => {
const { promise, resolve, reject } = Promise.withResolvers<{
contentLength: string | undefined;
transferEncoding: string | undefined;
bodyLength: number;
}>();
const server = http.createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => {
resolve({
contentLength: req.headers["content-length"],
transferEncoding: req.headers["transfer-encoding"],
bodyLength: Buffer.concat(chunks).length,
});
res.writeHead(200);
res.end("ok");
});
});
await new Promise<void>(res => server.listen(0, "127.0.0.1", res));
const port = (server.address() as any).port;
try {
const data = Buffer.alloc(200, "x");
const req = http.request({
hostname: "127.0.0.1",
port,
method: "POST",
headers: {
"Content-Length": data.length.toString(),
},
});
await new Promise<void>((res, rej) => {
req.on("error", rej);
req.on("response", () => res());
req.write(data);
req.end();
});
const result = await promise;
expect(result.contentLength).toBe("200");
expect(result.transferEncoding).toBeUndefined();
expect(result.bodyLength).toBe(200);
} finally {
server.close();
}
});
test("without explicit Content-Length still uses chunked encoding", async () => {
const { promise, resolve, reject } = Promise.withResolvers<{
contentLength: string | undefined;
transferEncoding: string | undefined;
bodyLength: number;
}>();
const server = http.createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => {
resolve({
contentLength: req.headers["content-length"],
transferEncoding: req.headers["transfer-encoding"],
bodyLength: Buffer.concat(chunks).length,
});
res.writeHead(200);
res.end("ok");
});
});
await new Promise<void>(res => server.listen(0, "127.0.0.1", res));
const port = (server.address() as any).port;
try {
const chunk1 = Buffer.alloc(100, "a");
const chunk2 = Buffer.alloc(100, "b");
const req = http.request({
hostname: "127.0.0.1",
port,
method: "POST",
// No Content-Length header
});
await new Promise<void>((res, rej) => {
req.on("error", rej);
req.on("response", () => res());
req.write(chunk1);
req.write(chunk2);
req.end();
});
const result = await promise;
// Without explicit Content-Length, chunked encoding should be used
expect(result.transferEncoding).toBe("chunked");
expect(result.bodyLength).toBe(200);
} finally {
server.close();
}
});
test("explicit Transfer-Encoding takes precedence over Content-Length", async () => {
const { promise, resolve } = Promise.withResolvers<{
contentLength: string | undefined;
transferEncoding: string | undefined;
bodyLength: number;
}>();
const server = http.createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (chunk: Buffer) => chunks.push(chunk));
req.on("end", () => {
resolve({
contentLength: req.headers["content-length"],
transferEncoding: req.headers["transfer-encoding"],
bodyLength: Buffer.concat(chunks).length,
});
res.writeHead(200);
res.end("ok");
});
});
await new Promise<void>(res => server.listen(0, "127.0.0.1", res));
const port = (server.address() as any).port;
try {
const chunk1 = Buffer.alloc(100, "a");
const chunk2 = Buffer.alloc(100, "b");
const req = http.request({
hostname: "127.0.0.1",
port,
method: "POST",
headers: {
"Content-Length": "200",
"Transfer-Encoding": "chunked",
},
});
await new Promise<void>((res, rej) => {
req.on("error", rej);
req.on("response", () => res());
req.write(chunk1);
req.write(chunk2);
req.end();
});
const result = await promise;
// When user explicitly sets Transfer-Encoding, it should be used
// and Content-Length should not be added
expect(result.transferEncoding).toBe("chunked");
expect(result.contentLength).toBeUndefined();
expect(result.bodyLength).toBe(200);
} finally {
server.close();
}
});
});

View File

@@ -0,0 +1,89 @@
import { expect, test } from "bun:test";
import { bunEnv, bunExe } from "harness";
// https://github.com/oven-sh/bun/issues/27097
// Segfault when pipe write completion callback fires after close() freed StreamBuffer
// The bug: on Windows, close() called onCloseSource() synchronously, which could
// free the writer's StreamBuffer resources. If a uv_write was pending, its callback
// would later access the freed memory, causing a segfault at 0xFFFFFFFFFFFFFFFF.
test("closing spawn stdin while write is pending should not crash", async () => {
// Spawn a process that reads from stdin.
// Write data to stdin, then immediately close.
// This creates a scenario where a pipe write may be pending when close() is called.
for (let i = 0; i < 5; i++) {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", "process.stdin.resume(); process.stdin.on('close', () => process.exit(0));"],
env: bunEnv,
stdin: "pipe",
stdout: "ignore",
stderr: "ignore",
});
// Write a large amount of data to stdin - this makes it more likely that
// a write will be pending when we close
try {
proc.stdin.write("x".repeat(65536));
proc.stdin.flush();
} catch {
// Write may fail if process already exited - that's fine
}
// Close stdin while the write may still be pending
proc.stdin.end();
// Wait for the process to exit
await proc.exited;
}
}, 30_000);
test("rapid spawn and close cycles should not corrupt pipe state", async () => {
// Simulate the pattern from the bug report: many spawn operations over time.
// Each spawn creates pipes, writes some data, and tears down.
const iterations = 10;
for (let i = 0; i < iterations; i++) {
await using proc = Bun.spawn({
cmd: [bunExe(), "-e", "console.log('ok');"],
env: bunEnv,
stdout: "pipe",
stderr: "ignore",
});
const stdout = await new Response(proc.stdout).text();
const exitCode = await proc.exited;
expect(stdout.trim()).toBe("ok");
expect(exitCode).toBe(0);
}
}, 30_000);
test("FileSink write and close race should not crash", async () => {
// Test the FileSink (StreamingWriter) path by using spawn with a ReadableStream
// as stdin, which creates a FileSink internally.
for (let i = 0; i < 5; i++) {
const data = "hello ".repeat(1000);
const stream = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(data));
controller.close();
},
});
await using proc = Bun.spawn({
cmd: [
bunExe(),
"-e",
"let t=0; process.stdin.on('data',(c)=>{t+=c.length}); process.stdin.on('end',()=>{console.log(t)})",
],
env: bunEnv,
stdin: stream,
stdout: "pipe",
stderr: "ignore",
});
const stdout = await new Response(proc.stdout).text();
const exitCode = await proc.exited;
expect(Number(stdout.trim())).toBeGreaterThan(0);
expect(exitCode).toBe(0);
}
}, 30_000);

View File

@@ -92,13 +92,17 @@ test("cyclic imports with async dependencies should generate async wrappers", as
expect(bundled).toMatchInlineSnapshot(`
"var __defProp = Object.defineProperty;
var __returnValue = (v) => v;
function __exportSetter(name, newValue) {
this[name] = __returnValue.bind(null, newValue);
}
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, {
get: all[name],
enumerable: true,
configurable: true,
set: (newValue) => all[name] = () => newValue
set: __exportSetter.bind(all, name)
});
};
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
@@ -176,7 +180,7 @@ test("cyclic imports with async dependencies should generate async wrappers", as
var { AsyncEntryPoint: AsyncEntryPoint2 } = await Promise.resolve().then(() => exports_AsyncEntryPoint);
AsyncEntryPoint2();
//# debugId=986E7BD819E590FD64756E2164756E21
//# debugId=2020261114B67BB564756E2164756E21
//# sourceMappingURL=entryBuild.js.map
"
`);