# ferridriver-jsstd
Vendored subset of [awslabs/llrt](https://github.com/awslabs/llrt) (Apache
License 2.0), providing the WHATWG Streams implementation, the `node:os`
module, and the pieces they depend on for the ferridriver QuickJS runtime.
Upstream: `0.9.0-beta`, re-synced against `awslabs/llrt@0a10758` (main,
2026-09-06). Every module is taken from that one commit.
| `llrt_utils` | `utils` |
| `llrt_context` | `context` |
| `llrt_exceptions` | `exceptions` |
| `llrt_events` | `events` |
| `llrt_abort` | `abort` |
| `llrt_encoding` | `encoding` |
| `llrt_buffer` | `buffer` |
| `llrt_json` | `json` |
| `llrt_crypto` | `crypto` |
| `llrt_os` | `os` |
| `llrt_fs` | `fs` |
| `llrt_path` | `pathutil` |
| `llrt_stream_web` | `stream_web` |
| `llrt_zlib` | `zlib` |
| `llrt_compression` | `compression`|
| `llrt_string_decoder` | `string_decoder` |
| `llrt_perf_hooks` | `perf_hooks` |
| `llrt_tty` | `tty` |
| `llrt_navigator` | `navigator` |
| `llrt_url` | `url` |
| `llrt_util` | `text` |
| `llrt_test` | `test` (dev) |
`llrt_stream` is NOT vendored, and not because we chose against it: llrt has
no node `stream` module. `llrt_stream` registers no `ModuleDef` and answers
no specifier — it is an internal trait library (`Readable` / `Writable` /
`SteamEvents`) that llrt's own `fs`, `net` and `child_process` implement.
`llrt_stream_web` is the module, and it is `stream/web`. There is likewise
no `querystring` anywhere in llrt.
The rest of llrt — its hyper/fetch stack, timers, console — is deliberately
not vendored: ferridriver has its own, over `reqwest`. `fs` is vendored
because it IS Node's `fs`, which is what a suite expects; only the Rust
path helpers of `llrt_path` come with it (`pathutil`) — the `path` MODULE
stays ferridriver's. `os` is vendored
because ferridriver has nothing equivalent and the module is pure host
introspection with no overlap with the automation stack. From `llrt_util`
only the four text codecs are taken (`TextEncoder`, `TextDecoder` and their
stream forms); its `format` / `inherits` / `inspect` are `node::util`'s,
which are richer.
## What a host installs
Two entry points, and nothing else to remember:
- `jsstd::init(ctx)` — every global this crate provides: `DOMException`,
`Event` / `EventTarget`, `AbortController` / `AbortSignal`, the Streams
surface, `Buffer` / `Blob` / `File`, `crypto`, `TextEncoder` /
`TextDecoder` (+ their stream forms), `URL` / `URLSearchParams`, `atob` /
`btoa`, `structuredClone` and `performance`.
- `jsstd::modules::modules()` — every Node / web MODULE it serves, each
entry carrying its specifiers, the `ModuleDef` the ES loader declares,
and the object `require()` returns: `path`, `buffer`, `os`, `util`,
`events`, `assert` (+ `/strict`), `url`, `process`, `timers` (+
`/promises`), `crypto`, `zlib`, `string_decoder`, `perf_hooks`, `tty`,
`stream/web`. The host merges that list into its own loader,
its `require` table and its bundler's external list, so the three
cannot drift apart.
## `src/node/` — ferridriver-authored
Not everything Node exposes has a usable upstream in llrt. `llrt_util` is
`TextEncoder`/`TextDecoder` plus `format` and `inherits` (no `promisify`, no
`inspect`, no `types`), and `llrt_assert` is a single `ok`. Those modules are
written here instead, under `src/node/`, so the runtime still has exactly one
implementation of each surface:
| `node::inspect` | The `util.inspect` / `util.format` renderer, moved out of `ferridriver-script`'s `console` so `console.log`, `util.format` and `util.inspect` cannot drift apart |
| `node::deep_equal` | Structural equality for `util.isDeepStrictEqual` (and `assert.deepStrictEqual` when it lands) |
| `node::util` | The `util` module |
| `node::assert` | The `assert` module (upstream `llrt_assert` is a single `ok`) |
| `node::process` | A sandbox-safe `process` — inert identity and timing, with `env` and `cwd()` supplied by the host — and its module form |
| `node::timers` | The module form of `web::timers`, plus `timers/promises` |
| `node::path` | The `path` module, moved out of `ferridriver-script`'s `node_compat` |
| `node::bytes` | The one JS-value-to-`Vec<u8>` walk: `BufferSource`, `Buffer`, byte arrays, encoded strings. `crypto`, the compression streams, `Buffer.from` and `setInputFiles` all read through it — there were three separate walks before |
`src/node/` carries its own `rustfmt.toml` re-enabling formatting (the crate
disables it for the vendored subtree) and follows the repo's house style. It
is compiled under this crate's relaxed lints because pedantic's
`needless_pass_by_value` cannot be satisfied by an rquickjs callback, which
must take owned JS values.
## `src/web/` — ferridriver-authored
Web-platform globals llrt has no upstream for. Same formatting rules as
`src/node/`.
| `web` (`mod.rs`) | `atob` / `btoa` (the WHATWG forgiving-base64 algorithm, which `base64::STANDARD` does not implement), `structuredClone`, and `performance.now()` / `timeOrigin` over a monotonic base. `llrt_buffer`'s module form reads `atob` / `btoa` off the globals, so installing them here is what makes `require('buffer').atob` resolve |
| `web::form_data` | `FormData`. It holds entries and nothing else: a host serializes them with its own multipart writer and hands parsed bodies back through `from_entries` |
| `web::compression` | `CompressionStream` / `DecompressionStream` (gzip, deflate, deflate-raw) over the vendored `TransformStream` |
| `web::timers` | `setTimeout` / `setInterval` / `clearTimeout` / `clearInterval` / `setImmediate` / `queueMicrotask`. NOT installed by `init`: a host supplies a `CallbackPolicy` so ambient state (ferridriver carries an `allow.net` grant) survives from arming the timer to running the callback |
| `web::blob_bytes` | The bytes-and-type read of a `Blob` / `File` value |
| `web::js_iterator` | The live-iterator protocol object `FormData`'s `entries` / `keys` / `values` return |
## Keeping it re-syncable
Sources are kept byte-close to upstream, including upstream's 4-space
formatting, so a re-sync against a newer llrt stays a mechanical diff. The
crate therefore does **not** inherit the workspace lints (see its
`Cargo.toml`), and `cargo fmt` must not be pointed at it.
Re-sync recipe (from a checkout of llrt):
```sh
for m in utils:libs/llrt_utils context:libs/llrt_context \
zlib:modules/llrt_zlib string_decoder:modules/llrt_string_decoder \
perf_hooks:modules/llrt_perf_hooks tty:modules/llrt_tty \
navigator:modules/llrt_navigator compression:libs/llrt_compression \
encoding:libs/llrt_encoding exceptions:modules/llrt_exceptions \
events:modules/llrt_events abort:modules/llrt_abort \
os:modules/llrt_os buffer:modules/llrt_buffer \
fs:modules/llrt_fs \
json:libs/llrt_json crypto:modules/llrt_crypto \
url:modules/llrt_url \
stream_web:modules/llrt_stream_web; do
name="${m%%:*}"; path="${m##*:}"
cp -R "$LLRT/$path/src" "src/$name" && mv "src/$name/lib.rs" "src/$name/mod.rs"
done
# `text` takes only llrt_util's four codec files; its own mod.rs stays.
for f in text_encoder text_decoder text_encoder_stream text_decoder_stream; do
cp "$LLRT/modules/llrt_util/src/$f.rs" "src/text/$f.rs"
done
# per-module first, then the cross-crate rewrite (BSD sed has no \b — use perl)
for name in utils context encoding exceptions events abort os buffer json crypto url text stream_web \
zlib string_decoder perf_hooks tty navigator compression; do
find "src/$name" -name '*.rs' | while read -r f; do
perl -pi -e "s/\bcrate::/crate::${name}::/g" "$f"
done
done
find src -name '*.rs' | while read -r f; do
perl -pi -e 's/\bllrt_([a-z_]+)/crate::$1/g' "$f"
done
```
Then re-apply the local deltas below.
## Local deltas
Everything here is a fix or a visibility widening, never a behaviour change
for ferridriver's convenience. Upstream candidates.
0. **Upstream regressions we do NOT take.** Still true at the 2026-09-06 main sync:
upstream still ships the two transform-stream bugs listed in deltas 2
and 3 below — and has since changed
`transform_stream_error_writable_and_unblock_write` to take `_e` and
ignore it, moving further from the spec. A future re-sync must keep
OUR versions of `stream_web/transform/{controller,stream}.rs`,
`stream_web/writable/mod.rs` and the visibility widenings; taking
upstream wholesale reintroduces a hung `read()` and a `JS_FreeRuntime`
assertion at teardown.
1. **`abort/abort_signal.rs`** — the `sleep-tokio` arm imports
`CtxExtension` from `llrt_utils::ctx`, where it does not exist; it lives
in `llrt_context`. Repointed at `crate::context`. (Upstream only builds
the default `sleep-timers` arm, which is why this never surfaced there.)
2. **`stream_web/transform/controller.rs`** —
`TransformStreamDefaultControllerPerformTransform` was missing spec step
3: reacting to the transform promise's rejection by erroring the stream.
A `transform()` that threw left both sides live, so a pending
`reader.read()` never settled.
3. **`stream_web/transform/controller.rs`** —
`TransformStreamErrorWritableAndUnblockWrite` was missing
`WritableStreamDefaultControllerErrorIfNeeded`, so an errored transform
left its writable in the `"writable"` state with an unresolved write
request. Added `stream_web::writable::writable_stream_error_if_needed`
for it.
4. **Visibility widenings only** — `SizeAlgorithm` / `SizeValue` /
`SizeFunction` / `NativeSizeFunction` from `pub(super)` to `pub(crate)`,
and `writable_stream_default_controller_error` to `pub(crate)`. Upstream
these were crate-visible because each module was its own crate; nesting
them under one crate narrowed them below what their own public API needs.
5. **Feature gates** — `sleep-tokio` is on by default. `sleep-timers` is
deliberately *not* a Cargo feature (the timers module is not vendored, so
`--all-features` would otherwise enable an arm that cannot compile); it is
declared to rustc as a known-but-never-set cfg via `check-cfg` in
`Cargo.toml`, which keeps the upstream `cfg` arms compiling out silently.
7. **`rquickjs` `half` feature** — enabled because the synced
`utils/bytes.rs` and `stream_web/readable/byob_reader.rs` handle
`Float16Array`. Without it `PredefinedAtom::Float16Array` does not
exist and `f16` has no `TypedArrayItem` impl.
6. **Tests** — `abort::abort_signal::tests::test_abort_signal` is no longer
gated on `sleep-timers`, so it covers the `sleep-tokio` path we build.
Two regression tests were added to `stream_web/transform/tests.rs` for
deltas 2 and 3.
8. **`os/mod.rs` — no Windows arm.** `llrt_os`'s `windows.rs` is not
vendored: ferridriver targets macOS and Linux, and that arm needs four
Windows-only dependencies (`whoami`, `windows-registry`,
`windows-result`, `windows-version`).
9. **`os/unix.rs` — `getpwuid_r` instead of the `users` crate.** Upstream
read the login name and shell through `users` 0.11, which has been
unmaintained since 2021, and at the 2026-09-06 sync moved to `uzers`
0.12, the maintained fork. The replacement calls `getpwuid_r` directly —
the same call either crate makes — including its ERANGE grow-the-buffer
protocol, so the one-line upstream swap is not taken and neither crate
is a dependency here.
10. **`os/statistics.rs` — real CPU times.** Upstream returns
`times: { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 }` for every CPU,
with the comment "cannot be obtained at this time". sysinfo does not
expose them, but the kernel does: `/proc/stat` on Linux and
`host_processor_info` on macOS, which is where libuv reads them for
Node. Ticks are converted to milliseconds through `_SC_CLK_TCK`.
Darwin does not account interrupt time, so `irq` stays 0 there — as
it does in libuv.
11. **`os/mod.rs` — `fill()` / `os_object()` split.** Upstream fills the
module's default export inline inside `evaluate`. ferridriver serves
every native module twice, as an ES module and as a synchronous
`require()` namespace, and its loader requires both to read from one
place, so the body moved into a function.
12. **`os` feature gates.** Upstream's `system` / `statistics` / `network`
features are declared here too (all on by default) so the `#[cfg]`
arms stay exactly as upstream wrote them.
## Known gaps against Node
- `os.constants` (signal, errno, priority and dlopen tables) is not
implemented upstream and is not added here.
- `networkInterfaces()` marks link-local and multicast addresses
`internal: true`; Node marks only loopback interfaces internal.
13. **`buffer/blob.rs` and `buffer/file.rs` — three fixes.** Both files
ARE vendored and `init` defines both classes (an earlier version of
this note said neither was taken). `Blob::stream`
copies the bytes out before building the pull closure: upstream
captures the JS `ArrayBuffer` in a native closure, a cycle the
collector cannot see, which trips `JS_FreeRuntime`'s
`list_empty(&rt->gc_obj_list)` assertion at teardown.
`File::from_bytes` goes through `Blob::from_bytes` rather than
`into_js`, which made a JS array of numbers that `new Blob([...])`
stringified (`File.from_bytes(b"hi")` read back as `"104105"`).
And `buffer/mod.rs` chains `File.prototype` to `Blob.prototype` after
defining both classes, since rquickjs classes do not inherit and
upstream leaves `file instanceof Blob` false.
14. **`buffer/class.rs`** is upstream's `buffer.rs`, renamed. A `buffer`
module inside a `buffer` module trips `clippy::module_inception`,
which is on by default and the repo's gate runs `-D warnings`.
15. **`buffer/mod.rs` — `equals` and `toJSON`.** Node defines both on
`Buffer.prototype`; upstream defines neither, and the hand-written
class this vendoring replaced had both, so not adding them would be a
regression. Added after `set_prototype` rather than inside the
vendored file, so `class.rs` stays a mechanical diff. Upstream
candidates.
16. **`llrt_encoding`'s build script is not vendored.** It only calls
`llrt_build::set_nightly_cfg()`; this repo pins stable. As of the
2026-09-06 sync upstream has dropped its last `rust_nightly` arm
(`bytes_to_utf16_string` now uses the stable `as_chunks`), so no
vendored file reads either cfg; `rust_nightly` and `nightly` stay
declared as known-but-never-set cfgs in `Cargo.toml` so a future
upstream arm compiles out silently rather than warning.
## Known gaps against Node — `Buffer`
`Buffer` is a real `Uint8Array` subclass, so every typed-array method
works and index access reads bytes. Missing against Node: the
string-aware overrides of `includes` / `indexOf` / `lastIndexOf` / `fill`
(the `Uint8Array` versions are inherited, so they take byte values, not
strings), `swap16` / `swap32` / `swap64`, `compare`, and `Buffer.poolSize`.
17. **`crypto/provider/{ring,openssl,graviola}.rs` are not vendored.**
Only the pure-Rust provider (`crypto-rust`, upstream's own default) is
taken; the other three back-ends would each add a system dependency.
Their feature names are declared as known-but-unset cfgs. Upstream's
`_modern-webcrypto` marker (ML-DSA, ML-KEM, the hybrid KEMs,
ChaCha20-Poly1305, SHA-3 / cSHAKE / TurboSHAKE, `supports`,
`getPublicKey`, `encapsulate*` / `decapsulate*`) is declared and on:
every upstream provider enables it, and its back-ends (`ml-dsa`,
`ml-kem`, `chacha20poly1305`, `sha3`, `shake`, `cshake`, `keccak`,
`sponge-cursor`, `ctutils`) are all pure Rust. `provider/modern.rs`
is provider-independent upstream and is vendored as is.
18. **`crypto` / `json` macro imports.** `iterable_enum` and `str_enum` are
`#[macro_export]`ed, so they live at the crate root rather than under
`utils` — the import lines are repointed at `crate::`.
19. **Hash crates keep their `oid` feature.** `sha1` / `sha2` / `md-5` are
taken with `oid` (and `aes-gcm` with `hazmat`): PKCS#1 v1.5 signing
needs `AssociatedOid`, and WebCrypto allows 32- and 64-bit GCM tags,
which are gated behind those features in the 0.11 releases.
20. **`url/url_search_params.rs` — a non-string, non-object init.**
Upstream ignores it, so `new URLSearchParams(null)` and
`new URLSearchParams(42)` both build an EMPTY query. WebIDL's init
union is not nullable, so anything that is neither a sequence nor a
record converts to USVString: the queries are `null` and `42`, which
is what every browser engine produces. Only `undefined` (the argument
omitted) means empty.
23. **`encoding/mod.rs` — `windows-1252` / `latin1` / `ascii` are real.**
Upstream folds `Windows1252` into the UTF-8 arm in every direction,
so `new TextDecoder('windows-1252').decode([0xE9])` answered U+FFFD
and `Buffer.from('é', 'latin1')` produced the two UTF-8 bytes rather
than one. Worse, one label map served both consumers, which cannot
be right: Node's `latin1` is ISO-8859-1 and its `ascii` masks the
high bit, while the WHATWG Encoding Standard maps BOTH labels to
`windows-1252`. There are now two maps — `Encoder::from_str` for
Buffer, `Encoder::from_web_label` for `TextDecoder` — and three
single-byte variants (`Windows1252` with the real 0x80-0x9F index,
`Latin1`, `Ascii`) implemented in both directions.
22. **`url/mod.rs` — `fileURLToPath` decodes and validates.** Upstream
strips the `file://` prefix and hands the rest to `PathBuf`: the
scheme is never checked, a host is silently swallowed, a query or
fragment stays in the path, and percent-escapes are NOT decoded, so
`file:///tmp/a%20b.txt` names a file whose name literally contains
`%20`. Node checks the scheme, refuses a host it cannot address
locally, drops query and fragment, decodes the escapes and refuses
an ENCODED separator (which would otherwise change which file is
named).
21. **`url/url_class.rs` — `urlToHttpOptions` matches Node's shape.**
Upstream reports `port` as a STRING, omits `search` / `hash` when
they are empty, keeps the brackets on an IPv6 `hostname`, and joins
the raw percent-encoded credentials into `auth`. Node reports a
numeric port, always sets `search` and `hash`, hands over a bare IPv6
host (what a socket connect takes) and `decodeURIComponent`s the
credentials. `URL::inner_url` was upstream's only reader for the
omitted-hash branch and goes with it; the `percent-encoding` dep is
for the credential decode (`url` keeps that crate private).
24. **`fs/mod.rs` — `existsSync`.** Node has it and a large share of real
code calls it; upstream ships neither it nor a callback API, so
without it the only way to ask whether a file is there is to catch a
`stat` rejection.
25. **`fs/mod.rs` — one namespace per VM.** Upstream builds a fresh
exports object per module evaluation. Node answers the SAME object
for `require("fs")`, `require("node:fs")` and `fs.promises` vs
`require("fs/promises")`, so the namespaces are built once per
context and remembered; the `fs` global is that object too. Without
it, identity comparisons are false and a caller who patches a method
patches a copy nobody else sees.
26. **`zlib` — the codec back-ends taken.** Upstream's default is
`compression-c`, which links a system zlib-ng, brotli and zstd. This
crate takes the pure-Rust back-end wherever one exists
(`flate2/rust_backend`, which is also what `CompressionStream`
already used, and the `brotli` crate) and `zstd-c` for zstd, which
upstream's own manifest says has no pure-Rust implementation. The
`zstd` crate vendors and compiles the C rather than needing one
installed, so the build still has no system dependency — the same
posture as `rquickjs-sys` compiling QuickJS. All six upstream feature
names are declared, so its `#[cfg]` arms stay exactly as written.
27. **`compression/streaming.rs` is not vendored.** Its only consumer
upstream is `llrt_fetch`'s response decoder, and this crate does not
vendor `llrt_fetch` (ferridriver's `fetch` is its own, over
`reqwest`). Carrying it would be dead code that also trips
`clippy::large_enum_variant` — `StreamingDecoder`'s zstd variant is
~336 bytes larger than the next — and boxing a variant to satisfy a
lint in code nothing calls is worse than not taking the file. Same
reasoning as `buffer/blob.rs`, `crypto/provider/ring.rs` and
`os/windows.rs`.
28. **`zlib/codec.rs` and `string_decoder/decoder.rs`** are upstream's
`zlib.rs` and `string_decoder.rs`, renamed. A module with its
parent's name trips `clippy::module_inception`, which is on by
default and the repo's gate runs `-D warnings`. Same fix as delta 14.
29. **`zlib/{brotli,codec,zstd}.rs` — macro imports.** `define_sync_function`
and `define_cb_function` are `#[macro_export]`ed, so they live at the
crate root rather than under `zlib`. The `use super::{...}` lines are
split: the two macros come from `crate::`, the plain items still from
`super::`. Same cause as delta 18.
30. **`perf_hooks` — the module only, not the global.** Upstream's `init`
installs its own `Performance` class on `globalThis`, and taking it
would be a regression: `Performance::now` reads
`llrt_utils::time::now_nanos`, which is `SystemTime::now()` — the
WALL clock. High Resolution Time exists to give a monotonic reading,
so upstream's `now()` steps backwards whenever the system clock
does, and `saturating_sub` clamps that to `0` rather than surfacing
it. `web::init`'s reads `Instant::elapsed`, which is monotonic by
construction. Upstream also leaves `origin_nanos()` at `0` until a
host calls `time::init()`, and an unset origin makes `now()` return
the whole Unix epoch in milliseconds.
Upstream's MODULE body only reads `globalThis.performance` and
re-exports it, so dropping `init` (and `performance.rs` with it)
leaves `perf_hooks` serving ferridriver's own.
The two things upstream's class had over the old plain object —
`toJSON()` and being a real class instance — are in `web::performance`
now, along with the User Timing and Performance Timeline surface
neither side had. See "`performance`" below.
31. **`navigator` — this runtime's name.** Upstream hardcodes
`userAgent` to `llrt <version>`. Shipping that verbatim would answer
every user-agent sniffer with the wrong runtime. It reads
`ferridriver/<version>` here, which is the shape Node 21+ uses.
Worth knowing before relying on it: a `navigator` global is still how
some libraries decide they are in a browser, the mirror image of the
`process.versions.node` check. It is Node parity, not a browser
claim, but a package that misroutes on it is misrouting for this
reason.
32. **`stream/web` registered as a specifier.** The implementation was
vendored from the start but only ever installed as globals, so
`import { ReadableStream } from 'node:stream/web'` — Node's own name
for it, and how a library reaches it without assuming a browser —
resolved to nothing. `modules.rs` now serves it, reading the classes
back off the globals so there is still one implementation.
## `performance`
`web::performance` is ferridriver's, not vendored, and covers three
specs rather than the `now()` / `timeOrigin` pair it started as:
- **High Resolution Time** — `now()`, `timeOrigin`, `toJSON()`.
`now()` reads `Instant::elapsed`; the wall clock appears exactly once,
as `timeOrigin`, which is what the monotonic readings are relative to.
- **User Timing** — `mark()`, `measure()`, `clearMarks()`,
`clearMeasures()`, and the `PerformanceMark` / `PerformanceMeasure`
classes with their `detail`. `measure` implements all three overloads
(bare name, start-mark, options bag) and refuses the two combinations
the spec calls out: an options bag together with a trailing `endMark`,
and `start` + `end` + `duration` all at once.
- **Performance Timeline** — `getEntries()`, `getEntriesByName()`,
`getEntriesByType()`, sorted chronologically by `startTime` rather
than by insertion, because `mark(name, { startTime })` can backdate an
entry. The sort is stable, so entries sharing a `startTime` keep the
order they were recorded in.
`PerformanceMark` and `PerformanceMeasure` chain their prototype to
`PerformanceEntry`, so `mark instanceof PerformanceEntry` holds.
Constructibility follows the IDL: `PerformanceMark` takes
`(name, options)` and does NOT buffer (only `performance.mark()`
records), while `PerformanceEntry`, `PerformanceMeasure` and
`Performance` throw `Illegal constructor`.
`performance.now()` and `process.hrtime()` count from ONE base
(`performance::monotonic_base`), so the two are correlatable the way
Node's are — it derives both from a single libuv hrtime. Two separate
`Instant::now()` calls would put a constant, invisible skew between
them; a test asserts they agree within a millisecond.
Not implemented: `PerformanceObserver` (it needs a task-queue hook this
runtime has no equivalent of), the resource and navigation entry types
(no document), Node's `eventLoopUtilization` / `nodeTiming`, and a
buffer size limit — nothing evicts, so a program marking in a hot loop
grows the buffer until it calls `clearMarks`.
## `Intl` is absent, and llrt cannot fill it
QuickJS-ng as vendored by `rquickjs-sys` contains no ECMA-402 at all —
no `Intl` object, and no build flag that would add one. `react-intl` and
anything like it fails with `Intl is not defined`.
`llrt_intl` does not close this. It is `Intl.DateTimeFormat` plus
`supportedValuesOf` and `Date.prototype.toLocaleString`, aimed at
timezone support, and it carries a `jiff` dependency and ~3k lines of
bundled CLDR data. There is no `NumberFormat`, `PluralRules` or
`Collator` — which is the part a formatting library actually reaches
for.
Until that changes, a suite that needs `Intl` supplies a JS polyfill
through `[bundler.alias]`, which keeps the choice (and its weight) in
the extension that needs it.
33. **`crypto/subtle/digest.rs` — a synchronous validation failure is
kept.** Upstream validates the algorithm before returning the
future, so the exception is thrown while `subtle_digest` is still
returning `Ok(future)`; by the time the future runs, the pending
exception is gone and the promise rejects with an uninitialized
value (`typeof e === "unknown"`). The thrown value is now taken with
`ctx.catch()` at that point and re-thrown inside the future, where
the rejection is built. Upstream candidate.
34. **`fs/access.rs` and `fs/stats.rs` — preserve filesystem error identity.** Sync and
async access, stat, and lstat failures use `node::system_error` to retain `code`,
`errno`, `syscall`, and `path`. Previously every metadata failure
became a plain missing-file message, preventing callers from
distinguishing missing paths from permission or directory errors.
35. **`fs/read_file.rs` — preserve read and open errors.** Sync and
async reads retain `code`, `errno`, `syscall`, and `path` through
`node::system_error`, including `EISDIR` for reading a directory.
36. **`crypto/provider/rust/mod.rs` — RSA is ours, not upstream's `rsa`
crate.** Upstream's pure-Rust provider implements RSA with the `rsa`
crate, which carries RUSTSEC-2023-0071 (Marvin attack: key recovery
through a timing side channel in a non-constant-time implementation).
RustSec records `patched = []`, so there is no version to move to and
no consumer can resolve it by bumping. A re-sync diffs against
upstream and would take it back, so this entry exists to stop that.
Where RSA went is delta 37, along with the rest of the provider.
37. **The crypto provider is AWS-LC, not RustCrypto.** This is the most
important entry in this list: upstream's provider is a pile of
RustCrypto crates, so a re-sync diffs against that and would drag all
of them back. `crypto/provider/rust/mod.rs` keeps its upstream name
and its `crypto-rust` feature gate, so upstream's `#[cfg]` arms still
line up. The implementation underneath is `aws-lc-rs`.
That covers digests, HMAC, HKDF, PBKDF2, all of RSA, ECDSA, ECDH, EC
key generation and the SEC1 / SPKI / PKCS#8 / JWK conversions,
Ed25519, X25519, AES-CBC, and in `provider/modern.rs`
ChaCha20-Poly1305, SHA-3 and the traditional half of the hybrid KEMs.
A host that speaks TLS already links the same `aws-lc-sys` through
rustls, so this puts one C crypto library in the binary rather than a
second one beside it. That is the reason it is not the `openssl`
crate: an earlier version of this provider vendored OpenSSL, which
built a whole second library from source and wanted perl and make
that nothing else here needs. `openssl-sys` can be backed by
`aws-lc-sys` and would have kept more surface, but it pins that
dependency to 0.41 where rustls is on 0.45, and the differing link
names mean the binary would hold two copies of AWS-LC.
What stays on pure-Rust crates is what AWS-LC's safe API cannot
reach. They add no C build and cross-compile wherever this runtime
does. `md-5` is there because AWS-LC has no MD5, and `cshake`,
`keccak` and `sponge-cursor` sit behind CSHAKE and TurboSHAKE.
`shake` is there because AWS-LC implements SHA-3 but exposes no XOF,
and the hybrid KEM seed expansion needs SHAKE-256. `ml-kem` and
`ml-dsa` keep their private keys as seeds, where AWS-LC takes only
expanded ones. And `aes-gcm` carries the seven WebCrypto tag lengths
AWS-LC cannot express, its AEAD being fixed at sixteen bytes.
Three capabilities are refused rather than answered wrongly, and each
has a test pinning the refusal. RSA-PSS takes only a `saltLength`
equal to the digest length. RSA generates only at AWS-LC's four sizes
and only with `e = 65537`. ECDSA signs only under the hash its curve
is paired with, which is ES256, ES384 and ES512.
Four things do not survive a careless re-sync, and each has a test
that fails if it is lost. WebCrypto's AES-CTR `length` is the width
of the counter field and wraps inside it, while AWS-LC's CTR always
increments the full 128-bit block, so the keystream is built from
AES-ECB. AES-KW is RFC 3394 over the same ECB primitive, because
`aws_lc_rs::key_wrap` carries no 192-bit algorithm. EC coordinates
are left-padded to the field width, 66 bytes on P-521, and an ECDSA
signature is the fixed-width r and s. And a hybrid KEM private key is
a seed expanded with SHAKE-256 whose traditional scalar is the first
chunk AWS-LC accepts as a valid one.
On re-sync: keep OUR provider. Do not take upstream's RustCrypto one
back.