From 26de9d0138447a66efab00cfc926c6b334fb5531 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 7 Sep 2026 16:06:36 +0800 Subject: [PATCH 1/3] feat: add single-package quickjs-jit stdlib Co-authored-by: Codex --- .github/workflows/stdlib.yml | 46 + CHANGELOG.md | 2 + Cargo.toml | 1 + README.md | 5 + docs/superpowers/plans/2026-09-07-stdlib.md | 44 + scripts/check-stdlib-package.py | 39 + scripts/import-stdlib.py | 172 ++ stdlib/Cargo.toml | 71 + stdlib/LICENSE-APACHE | 202 ++ stdlib/NOTICE | 11 + stdlib/NOTICE-LLRT | 2 + stdlib/README.md | 67 + stdlib/UPSTREAM.json | 197 ++ stdlib/src/lib.rs | 113 + .../src/llrt/llrt_abort/abort_controller.rs | 62 + stdlib/src/llrt/llrt_abort/abort_signal.rs | 281 +++ stdlib/src/llrt/llrt_abort/lib.rs | 26 + .../llrt_async_hooks/finalization_registry.rs | 65 + stdlib/src/llrt/llrt_async_hooks/lib.rs | 335 +++ .../src/llrt/llrt_buffer/array_buffer_view.rs | 159 ++ stdlib/src/llrt/llrt_buffer/blob.rs | 436 ++++ stdlib/src/llrt/llrt_buffer/buffer.rs | 1039 ++++++++ stdlib/src/llrt/llrt_buffer/file.rs | 129 + stdlib/src/llrt/llrt_buffer/lib.rs | 102 + stdlib/src/llrt/llrt_compression/lib.rs | 99 + stdlib/src/llrt/llrt_compression/streaming.rs | 98 + stdlib/src/llrt/llrt_context/lib.rs | 93 + stdlib/src/llrt/llrt_crypto/crc32.rs | 72 + stdlib/src/llrt/llrt_crypto/hash.rs | 217 ++ stdlib/src/llrt/llrt_crypto/lib.rs | 385 +++ .../src/llrt/llrt_crypto/provider/graviola.rs | 571 +++++ stdlib/src/llrt/llrt_crypto/provider/mod.rs | 1268 ++++++++++ .../src/llrt/llrt_crypto/provider/openssl.rs | 1319 ++++++++++ stdlib/src/llrt/llrt_crypto/provider/ring.rs | 544 +++++ .../llrt_crypto/provider/rust/aes_variants.rs | 285 +++ .../src/llrt/llrt_crypto/provider/rust/mod.rs | 1654 +++++++++++++ .../src/llrt/llrt_crypto/subtle/crypto_key.rs | 165 ++ .../llrt_crypto/subtle/derive_algorithm.rs | 77 + .../llrt/llrt_crypto/subtle/derive_bits.rs | 184 ++ .../llrt/llrt_crypto/subtle/derive_keys.rs | 89 + stdlib/src/llrt/llrt_crypto/subtle/digest.rs | 59 + .../src/llrt/llrt_crypto/subtle/encryption.rs | 269 ++ .../subtle/encryption_algorithm.rs | 120 + .../src/llrt/llrt_crypto/subtle/export_key.rs | 229 ++ .../llrt/llrt_crypto/subtle/generate_key.rs | 137 ++ .../src/llrt/llrt_crypto/subtle/import_key.rs | 76 + .../llrt/llrt_crypto/subtle/key_algorithm.rs | 1609 ++++++++++++ stdlib/src/llrt/llrt_crypto/subtle/mod.rs | 183 ++ stdlib/src/llrt/llrt_crypto/subtle/sign.rs | 129 + .../llrt/llrt_crypto/subtle/sign_algorithm.rs | 58 + stdlib/src/llrt/llrt_crypto/subtle/stubs.rs | 64 + stdlib/src/llrt/llrt_crypto/subtle/util.rs | 87 + stdlib/src/llrt/llrt_crypto/subtle/verify.rs | 155 ++ .../src/llrt/llrt_crypto/subtle/wrapping.rs | 93 + stdlib/src/llrt/llrt_encoding/lib.rs | 254 ++ stdlib/src/llrt/llrt_events/custom_event.rs | 40 + stdlib/src/llrt/llrt_events/event.rs | 62 + stdlib/src/llrt/llrt_events/event_target.rs | 44 + stdlib/src/llrt/llrt_events/lib.rs | 580 +++++ stdlib/src/llrt/llrt_exceptions/lib.rs | 464 ++++ stdlib/src/llrt/llrt_hooking/lib.rs | 88 + stdlib/src/llrt/llrt_json/escape.rs | 341 +++ stdlib/src/llrt/llrt_json/lib.rs | 233 ++ stdlib/src/llrt/llrt_json/parse.rs | 105 + stdlib/src/llrt/llrt_json/stringify.rs | 552 +++++ stdlib/src/llrt/llrt_path/lib.rs | 906 +++++++ stdlib/src/llrt/llrt_stream_web/lib.rs | 181 ++ .../queuing_strategy/byte_length.rs | 36 + .../llrt_stream_web/queuing_strategy/count.rs | 36 + .../llrt_stream_web/queuing_strategy/mod.rs | 231 ++ .../llrt_stream_web/queuing_strategy/tests.rs | 159 ++ .../llrt_stream_web/readable/byob_reader.rs | 624 +++++ .../readable/byte_controller.rs | 2169 +++++++++++++++++ .../llrt_stream_web/readable/controller.rs | 200 ++ .../readable/default_controller.rs | 960 ++++++++ .../readable/default_reader.rs | 540 ++++ .../llrt/llrt_stream_web/readable/iterator.rs | 698 ++++++ .../src/llrt/llrt_stream_web/readable/mod.rs | 31 + .../llrt/llrt_stream_web/readable/objects.rs | 459 ++++ .../llrt/llrt_stream_web/readable/reader.rs | 404 +++ .../readable/stream/algorithms.rs | 281 +++ .../llrt_stream_web/readable/stream/mod.rs | 1117 +++++++++ .../llrt_stream_web/readable/stream/pipe.rs | 700 ++++++ .../llrt_stream_web/readable/stream/source.rs | 36 + .../llrt_stream_web/readable/stream/tee.rs | 1713 +++++++++++++ .../llrt_stream_web/readable_writable_pair.rs | 25 + .../llrt_stream_web/transform/controller.rs | 308 +++ .../src/llrt/llrt_stream_web/transform/mod.rs | 9 + .../llrt/llrt_stream_web/transform/stream.rs | 352 +++ .../llrt/llrt_stream_web/transform/tests.rs | 440 ++++ .../llrt_stream_web/transform/transformer.rs | 44 + stdlib/src/llrt/llrt_stream_web/utils/mod.rs | 58 + .../src/llrt/llrt_stream_web/utils/promise.rs | 260 ++ .../src/llrt/llrt_stream_web/utils/queue.rs | 103 + .../writable/default_controller.rs | 871 +++++++ .../writable/default_writer.rs | 497 ++++ .../src/llrt/llrt_stream_web/writable/mod.rs | 17 + .../llrt/llrt_stream_web/writable/objects.rs | 162 ++ .../llrt_stream_web/writable/stream/mod.rs | 772 ++++++ .../llrt_stream_web/writable/stream/sink.rs | 35 + .../llrt/llrt_stream_web/writable/writer.rs | 79 + stdlib/src/llrt/llrt_test/lib.rs | 149 ++ stdlib/src/llrt/llrt_timers/lib.rs | 557 +++++ stdlib/src/llrt/llrt_url/lib.rs | 356 +++ stdlib/src/llrt/llrt_url/url_class.rs | 347 +++ stdlib/src/llrt/llrt_url/url_search_params.rs | 1058 ++++++++ stdlib/src/llrt/llrt_utils/any_of.rs | 299 +++ stdlib/src/llrt/llrt_utils/array_buffer.rs | 122 + .../src/llrt/llrt_utils/bytearray_buffer.rs | 227 ++ stdlib/src/llrt/llrt_utils/bytes.rs | 679 ++++++ stdlib/src/llrt/llrt_utils/class.rs | 126 + stdlib/src/llrt/llrt_utils/clone.rs | 21 + stdlib/src/llrt/llrt_utils/ctx.rs | 18 + stdlib/src/llrt/llrt_utils/error.rs | 24 + stdlib/src/llrt/llrt_utils/error_messages.rs | 4 + stdlib/src/llrt/llrt_utils/fs.rs | 104 + stdlib/src/llrt/llrt_utils/hash.rs | 9 + stdlib/src/llrt/llrt_utils/io.rs | 31 + stdlib/src/llrt/llrt_utils/latch.rs | 31 + stdlib/src/llrt/llrt_utils/lib.rs | 37 + stdlib/src/llrt/llrt_utils/macros.rs | 56 + stdlib/src/llrt/llrt_utils/mc_oneshot.rs | 119 + stdlib/src/llrt/llrt_utils/module.rs | 30 + stdlib/src/llrt/llrt_utils/object.rs | 171 ++ stdlib/src/llrt/llrt_utils/option.rs | 102 + stdlib/src/llrt/llrt_utils/primordials.rs | 166 ++ stdlib/src/llrt/llrt_utils/provider.rs | 25 + stdlib/src/llrt/llrt_utils/result.rs | 105 + stdlib/src/llrt/llrt_utils/reuse_list.rs | 338 +++ stdlib/src/llrt/llrt_utils/signals.rs | 150 ++ stdlib/src/llrt/llrt_utils/string.rs | 27 + stdlib/src/llrt/llrt_utils/sysinfo.rs | 14 + stdlib/src/llrt/llrt_utils/time.rs | 48 + stdlib/src/llrt/llrt_zlib/brotli.rs | 50 + stdlib/src/llrt/llrt_zlib/lib.rs | 212 ++ stdlib/src/llrt/llrt_zlib/zlib.rs | 97 + stdlib/src/llrt/llrt_zlib/zstd.rs | 57 + stdlib/tests/modules.rs | 77 + 138 files changed, 38313 insertions(+) create mode 100644 .github/workflows/stdlib.yml create mode 100644 docs/superpowers/plans/2026-09-07-stdlib.md create mode 100644 scripts/check-stdlib-package.py create mode 100644 scripts/import-stdlib.py create mode 100644 stdlib/Cargo.toml create mode 100644 stdlib/LICENSE-APACHE create mode 100644 stdlib/NOTICE create mode 100644 stdlib/NOTICE-LLRT create mode 100644 stdlib/README.md create mode 100644 stdlib/UPSTREAM.json create mode 100644 stdlib/src/lib.rs create mode 100644 stdlib/src/llrt/llrt_abort/abort_controller.rs create mode 100644 stdlib/src/llrt/llrt_abort/abort_signal.rs create mode 100644 stdlib/src/llrt/llrt_abort/lib.rs create mode 100644 stdlib/src/llrt/llrt_async_hooks/finalization_registry.rs create mode 100644 stdlib/src/llrt/llrt_async_hooks/lib.rs create mode 100644 stdlib/src/llrt/llrt_buffer/array_buffer_view.rs create mode 100644 stdlib/src/llrt/llrt_buffer/blob.rs create mode 100644 stdlib/src/llrt/llrt_buffer/buffer.rs create mode 100644 stdlib/src/llrt/llrt_buffer/file.rs create mode 100644 stdlib/src/llrt/llrt_buffer/lib.rs create mode 100644 stdlib/src/llrt/llrt_compression/lib.rs create mode 100644 stdlib/src/llrt/llrt_compression/streaming.rs create mode 100644 stdlib/src/llrt/llrt_context/lib.rs create mode 100644 stdlib/src/llrt/llrt_crypto/crc32.rs create mode 100644 stdlib/src/llrt/llrt_crypto/hash.rs create mode 100644 stdlib/src/llrt/llrt_crypto/lib.rs create mode 100644 stdlib/src/llrt/llrt_crypto/provider/graviola.rs create mode 100644 stdlib/src/llrt/llrt_crypto/provider/mod.rs create mode 100644 stdlib/src/llrt/llrt_crypto/provider/openssl.rs create mode 100644 stdlib/src/llrt/llrt_crypto/provider/ring.rs create mode 100644 stdlib/src/llrt/llrt_crypto/provider/rust/aes_variants.rs create mode 100644 stdlib/src/llrt/llrt_crypto/provider/rust/mod.rs create mode 100644 stdlib/src/llrt/llrt_crypto/subtle/crypto_key.rs create mode 100644 stdlib/src/llrt/llrt_crypto/subtle/derive_algorithm.rs create mode 100644 stdlib/src/llrt/llrt_crypto/subtle/derive_bits.rs create mode 100644 stdlib/src/llrt/llrt_crypto/subtle/derive_keys.rs create mode 100644 stdlib/src/llrt/llrt_crypto/subtle/digest.rs create mode 100644 stdlib/src/llrt/llrt_crypto/subtle/encryption.rs create mode 100644 stdlib/src/llrt/llrt_crypto/subtle/encryption_algorithm.rs create mode 100644 stdlib/src/llrt/llrt_crypto/subtle/export_key.rs create mode 100644 stdlib/src/llrt/llrt_crypto/subtle/generate_key.rs create mode 100644 stdlib/src/llrt/llrt_crypto/subtle/import_key.rs create mode 100644 stdlib/src/llrt/llrt_crypto/subtle/key_algorithm.rs create mode 100644 stdlib/src/llrt/llrt_crypto/subtle/mod.rs create mode 100644 stdlib/src/llrt/llrt_crypto/subtle/sign.rs create mode 100644 stdlib/src/llrt/llrt_crypto/subtle/sign_algorithm.rs create mode 100644 stdlib/src/llrt/llrt_crypto/subtle/stubs.rs create mode 100644 stdlib/src/llrt/llrt_crypto/subtle/util.rs create mode 100644 stdlib/src/llrt/llrt_crypto/subtle/verify.rs create mode 100644 stdlib/src/llrt/llrt_crypto/subtle/wrapping.rs create mode 100644 stdlib/src/llrt/llrt_encoding/lib.rs create mode 100644 stdlib/src/llrt/llrt_events/custom_event.rs create mode 100644 stdlib/src/llrt/llrt_events/event.rs create mode 100644 stdlib/src/llrt/llrt_events/event_target.rs create mode 100644 stdlib/src/llrt/llrt_events/lib.rs create mode 100644 stdlib/src/llrt/llrt_exceptions/lib.rs create mode 100644 stdlib/src/llrt/llrt_hooking/lib.rs create mode 100644 stdlib/src/llrt/llrt_json/escape.rs create mode 100644 stdlib/src/llrt/llrt_json/lib.rs create mode 100644 stdlib/src/llrt/llrt_json/parse.rs create mode 100644 stdlib/src/llrt/llrt_json/stringify.rs create mode 100644 stdlib/src/llrt/llrt_path/lib.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/lib.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/queuing_strategy/byte_length.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/queuing_strategy/count.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/queuing_strategy/mod.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/queuing_strategy/tests.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/readable/byob_reader.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/readable/byte_controller.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/readable/controller.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/readable/default_controller.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/readable/default_reader.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/readable/iterator.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/readable/mod.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/readable/objects.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/readable/reader.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/readable/stream/algorithms.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/readable/stream/mod.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/readable/stream/pipe.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/readable/stream/source.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/readable/stream/tee.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/readable_writable_pair.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/transform/controller.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/transform/mod.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/transform/stream.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/transform/tests.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/transform/transformer.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/utils/mod.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/utils/promise.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/utils/queue.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/writable/default_controller.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/writable/default_writer.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/writable/mod.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/writable/objects.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/writable/stream/mod.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/writable/stream/sink.rs create mode 100644 stdlib/src/llrt/llrt_stream_web/writable/writer.rs create mode 100644 stdlib/src/llrt/llrt_test/lib.rs create mode 100644 stdlib/src/llrt/llrt_timers/lib.rs create mode 100644 stdlib/src/llrt/llrt_url/lib.rs create mode 100644 stdlib/src/llrt/llrt_url/url_class.rs create mode 100644 stdlib/src/llrt/llrt_url/url_search_params.rs create mode 100644 stdlib/src/llrt/llrt_utils/any_of.rs create mode 100644 stdlib/src/llrt/llrt_utils/array_buffer.rs create mode 100644 stdlib/src/llrt/llrt_utils/bytearray_buffer.rs create mode 100644 stdlib/src/llrt/llrt_utils/bytes.rs create mode 100644 stdlib/src/llrt/llrt_utils/class.rs create mode 100644 stdlib/src/llrt/llrt_utils/clone.rs create mode 100644 stdlib/src/llrt/llrt_utils/ctx.rs create mode 100644 stdlib/src/llrt/llrt_utils/error.rs create mode 100644 stdlib/src/llrt/llrt_utils/error_messages.rs create mode 100644 stdlib/src/llrt/llrt_utils/fs.rs create mode 100644 stdlib/src/llrt/llrt_utils/hash.rs create mode 100644 stdlib/src/llrt/llrt_utils/io.rs create mode 100644 stdlib/src/llrt/llrt_utils/latch.rs create mode 100644 stdlib/src/llrt/llrt_utils/lib.rs create mode 100644 stdlib/src/llrt/llrt_utils/macros.rs create mode 100644 stdlib/src/llrt/llrt_utils/mc_oneshot.rs create mode 100644 stdlib/src/llrt/llrt_utils/module.rs create mode 100644 stdlib/src/llrt/llrt_utils/object.rs create mode 100644 stdlib/src/llrt/llrt_utils/option.rs create mode 100644 stdlib/src/llrt/llrt_utils/primordials.rs create mode 100644 stdlib/src/llrt/llrt_utils/provider.rs create mode 100644 stdlib/src/llrt/llrt_utils/result.rs create mode 100644 stdlib/src/llrt/llrt_utils/reuse_list.rs create mode 100644 stdlib/src/llrt/llrt_utils/signals.rs create mode 100644 stdlib/src/llrt/llrt_utils/string.rs create mode 100644 stdlib/src/llrt/llrt_utils/sysinfo.rs create mode 100644 stdlib/src/llrt/llrt_utils/time.rs create mode 100644 stdlib/src/llrt/llrt_zlib/brotli.rs create mode 100644 stdlib/src/llrt/llrt_zlib/lib.rs create mode 100644 stdlib/src/llrt/llrt_zlib/zlib.rs create mode 100644 stdlib/src/llrt/llrt_zlib/zstd.rs create mode 100644 stdlib/tests/modules.rs diff --git a/.github/workflows/stdlib.yml b/.github/workflows/stdlib.yml new file mode 100644 index 00000000..4fc6d2f3 --- /dev/null +++ b/.github/workflows/stdlib.yml @@ -0,0 +1,46 @@ +name: Stdlib +on: + push: + branches: [main, master] + pull_request: + +jobs: + test: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v7 + with: + submodules: true + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + - run: cargo test -p quickjs-jit-stdlib + - run: cargo test -p quickjs-jit-stdlib --all-features + + package: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + submodules: true + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: stable + - name: Verify the redistributed package and local binding releases + run: cargo package -p quickjs-jit-sys -p quickjs-jit-core -p quickjs-jit-macro -p quickjs-jit -p quickjs-jit-stdlib + - name: Check published dependency boundary + run: python3 scripts/check-stdlib-package.py target/package/quickjs-jit-stdlib-*.crate + + msrv: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + submodules: true + - uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + toolchain: '1.89.0' + - run: cargo check -p quickjs-jit-stdlib diff --git a/CHANGELOG.md b/CHANGELOG.md index 52281ad5..82d64096 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- Add `quickjs-jit-stdlib`, a single-package redistribution of the LLRT standard modules used by GPUI Shell, with bundled sources, provenance, and no LLRT package dependencies. + - Add pre-generated bindings for `riscv64gc-unknown-linux-gnu` and `riscv64a23-unknown-linux-gnu` - JIT M2: Tier 1 and Tier 2 now support the remaining comparison, bitwise, shift, `%`, unary numeric, constant, stack-shuffle, tail-call, `null`, and empty-string opcodes; the JIT ABI diff --git a/Cargo.toml b/Cargo.toml index 75d4be6f..02041c21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ members = [ "jit", "benchmarks", "macro", + "stdlib", "examples/native-module", "examples/module-loader", "examples/import-attributes", diff --git a/README.md b/README.md index 0f169577..b4c5f6e3 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,11 @@ and use Cargo source patches from that revision for both `quickjs-jit-core` and `quickjs-jit-sys`. Do not combine the published 0.12.2 runtime with 0.12.3 core or sys crates. +The optional [`quickjs-jit-stdlib`](stdlib/README.md) package redistributes +LLRT-derived Buffer, Crypto, Path, URL and Zlib modules in one crate. Its LLRT +implementation dependencies are bundled as source modules; applications do not +need LLRT packages or a Cargo compatibility patch. Stdlib requires Rust 1.89+. + [![github](https://img.shields.io/badge/github-longbridge/rquickjs-8da0cb.svg?style=for-the-badge&logo=github)](https://github.com/longbridge/rquickjs) [![crates](https://img.shields.io/crates/v/quickjs-jit.svg?style=for-the-badge&color=fc8d62&logo=rust)](https://crates.io/crates/quickjs-jit) [![docs](https://img.shields.io/badge/docs.rs-quickjs--jit-66c2a5?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyByb2xlPSJpbWciIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgdmlld0JveD0iMCAwIDUxMiA1MTIiPjxwYXRoIGZpbGw9IiNmNWY1ZjUiIGQ9Ik00ODguNiAyNTAuMkwzOTIgMjE0VjEwNS41YzAtMTUtOS4zLTI4LjQtMjMuNC0zMy43bC0xMDAtMzcuNWMtOC4xLTMuMS0xNy4xLTMuMS0yNS4zIDBsLTEwMCAzNy41Yy0xNC4xIDUuMy0yMy40IDE4LjctMjMuNCAzMy43VjIxNGwtOTYuNiAzNi4yQzkuMyAyNTUuNSAwIDI2OC45IDAgMjgzLjlWMzk0YzAgMTMuNiA3LjcgMjYuMSAxOS45IDMyLjJsMTAwIDUwYzEwLjEgNS4xIDIyLjEgNS4xIDMyLjIgMGwxMDMuOS01MiAxMDMuOSA1MmMxMC4xIDUuMSAyMi4xIDUuMSAzMi4yIDBsMTAwLTUwYzEyLjItNi4xIDE5LjktMTguNiAxOS45LTMyLjJWMjgzLjljMC0xNS05LjMtMjguNC0yMy40LTMzLjd6TTM1OCAyMTQuOGwtODUgMzEuOXYtNjguMmw4NS0zN3Y3My4zek0xNTQgMTA0LjFsMTAyLTM4LjIgMTAyIDM4LjJ2LjZsLTEwMiA0MS40LTEwMi00MS40di0uNnptODQgMjkxLjFsLTg1IDQyLjV2LTc5LjFsODUtMzguOHY3NS40em0wLTExMmwtMTAyIDQxLjQtMTAyLTQxLjR2LS42bDEwMi0zOC4yIDEwMiAzOC4ydi42em0yNDAgMTEybC04NSA0Mi41di03OS4xbDg1LTM4Ljh2NzUuNHptMC0xMTJsLTEwMiA0MS40LTEwMi00MS40di0uNmwxMDItMzguMiAxMDIgMzguMnYuNnoiPjwvcGF0aD48L3N2Zz4K)](https://docs.rs/quickjs-jit) diff --git a/docs/superpowers/plans/2026-09-07-stdlib.md b/docs/superpowers/plans/2026-09-07-stdlib.md new file mode 100644 index 00000000..ebfa6fc2 --- /dev/null +++ b/docs/superpowers/plans/2026-09-07-stdlib.md @@ -0,0 +1,44 @@ +# Single-package LLRT redistribution + +The approved design is one publishable `quickjs-jit-stdlib` package containing +LLRT source modules, not a facade over separately published or Git LLRT crates. +Work is isolated in `.worktrees/stdlib`, branch `quickjs-jit-stdlib`, based on +v0.12.7; existing work in the main checkout and GPUI Kit remains untouched. + +Scope: GPUI Shell's buffer, crypto, path, URL and zlib modules, and their full +runtime dependency closure at LLRT 7b95c82a9b15e7ddfb2778eca4b5a63111e74f51. +Use its RustCrypto and compression-rust selections; no new JavaScript APIs or +backend selection system. All bindings use the distribution's quickjs-jit. +Expose the five module namespaces and module definitions from one Rust crate. +Retain copyright headers, Apache-2.0 license, provenance and a reproducible +importer. Rewrite former crate paths and exported macro paths into this crate. + +Implementation and validation: +- [x] Add consumer tests for modules, Buffer/URL globals, crypto hashing and + compression roundtrips; establish the missing-package failure. +- [x] Import runtime dependency closure as modules and merge active registry + dependencies, including target-specific settings. Retain upstream unit + tests with a local test helper where practical. +- [x] Implement registration/global initialization, document host-owned async + scheduling and the fact this is a subset of LLRT, not all Node APIs. +- [x] Run focused consumer and retained unit tests. Check features/type identity. +- [x] Inspect and extract the package, validate in an external consumer without + LLRT patches or Git dependencies. Any unpublished quickjs-jit release + prerequisite must be reported rather than publishing dependencies. +- [x] Document the GPUI Shell import migration and inspect final diffs. + +Acceptance: one distributable stdlib crate, no LLRT package dependency, no +rquickjs compatibility facade, functioning existing module behavior, clean +build from redistributed sources, no dependency on paths inside LLRT checkout. + + +Verification completed on Apple Silicon: +- 119 retained LLRT unit tests and 4 consumer integration tests pass with parallel enabled. +- README doctest passes; one upstream URLSearchParams doctest remains intentionally ignored. +- `cargo +1.89 check -p quickjs-jit-stdlib` passes. +- Cargo packages and verifies all four binding packages and stdlib together through its temporary registry. +- The final stdlib archive has only registry dependencies, no original rquickjs/LLRT packages, no nested Cargo manifests and all license/provenance files. +- Four tests pass in an external consumer assembled from extracted archives. Only the not-yet-published binding releases use temporary source substitutions. +- Importer idempotence, Rust formatting and whitespace checks pass. +- Linux/Windows and MSRV coverage are configured in `.github/workflows/stdlib.yml`; remote CI has not been run in this session. +- Main checkouts and Shell integration were left for their existing work; migration instructions are in stdlib/README.md. diff --git a/scripts/check-stdlib-package.py b/scripts/check-stdlib-package.py new file mode 100644 index 00000000..64e2dc67 --- /dev/null +++ b/scripts/check-stdlib-package.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Check that a Cargo archive is one self-contained LLRT redistribution.""" +import sys +import tarfile +if sys.version_info < (3, 11): + raise SystemExit("Python 3.11 or newer is required") +import tomllib + +with tarfile.open(sys.argv[1], 'r:gz') as archive: + names = archive.getnames() + root = names[0].split('/')[0] + def read(name): + return archive.extractfile(f'{root}/{name}').read() + manifest = tomllib.loads(read('Cargo.toml').decode()) + assert manifest['package']['name'] == 'quickjs-jit-stdlib' + assert not manifest.get('patch'), 'consumer patches are not a publication strategy' + sections = [manifest] + sections.extend(manifest.get('target', {}).values()) + for section in sections: + for kind in ['dependencies', 'dev-dependencies', 'build-dependencies']: + for name, spec in section.get(kind, {}).items(): + spec = {'version': spec} if isinstance(spec, str) else spec + package = spec.get('package', name) + assert not package.startswith('llrt_'), (kind, package) + assert not {'git', 'path', 'registry'} & spec.keys(), (kind, package, spec) + assert spec.get('version'), (kind, package) + assert manifest['dependencies']['rquickjs']['package'] == 'quickjs-jit' + for name in names: + assert not (name.endswith('/Cargo.toml') and name != f'{root}/Cargo.toml'), name + lock = tomllib.loads(read('Cargo.lock').decode()) + for package in lock['package']: + assert not package['name'].startswith('llrt_'), package + assert package['name'] not in {'rquickjs', 'rquickjs-core', 'rquickjs-sys', 'rquickjs-macro'}, package + assert not package.get('source', '').startswith('git+'), package + for required in ['LICENSE-APACHE', 'NOTICE', 'NOTICE-LLRT', 'UPSTREAM.json']: + assert read(required), required + for module in ['buffer', 'crypto', 'path', 'url', 'zlib']: + assert read(f'src/llrt/llrt_{module}/lib.rs'), module +print('PASS: one stdlib archive, registry-only dependencies, no LLRT packages or patches') diff --git a/scripts/import-stdlib.py b/scripts/import-stdlib.py new file mode 100644 index 00000000..c0399931 --- /dev/null +++ b/scripts/import-stdlib.py @@ -0,0 +1,172 @@ +#!/usr/bin/env python3 +"""Import the pinned LLRT runtime closure into one distributable Rust crate. + +Usage: python3 scripts/import-stdlib.py /path/to/llrt +The source checkout must match REVISION. This script never downloads code. +""" +import copy +import hashlib +import json +from pathlib import Path +import re +import shutil +import subprocess +import sys +if sys.version_info < (3, 11): + raise SystemExit("Python 3.11 or newer is required") +import tomllib + +REVISION = '7b95c82a9b15e7ddfb2778eca4b5a63111e74f51' +ROOTS = {'llrt_buffer': [], 'llrt_crypto': ['crypto-rust'], 'llrt_path': [], + 'llrt_url': [], 'llrt_zlib': ['compression-rust']} +root = Path(sys.argv[1]).resolve() +if subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip() != REVISION: + raise SystemExit('LLRT checkout must be at ' + REVISION) +subprocess.run(['git', '-C', str(root), 'diff', '--exit-code', 'HEAD', '--', 'libs', 'modules'], check=True, stdout=subprocess.DEVNULL) +dest = Path(__file__).resolve().parents[1] / 'stdlib' +crates = {} +for p in root.glob('*/*/Cargo.toml'): + data = tomllib.loads(p.read_text()) + if 'package' in data: + crates[data['package']['name']] = (p.parent, data) + +def dependencies(data): + result = [(None, k, v) for k, v in data.get('dependencies', {}).items()] + for target, section in data.get('target', {}).items(): + result += [(target, k, v) for k, v in section.get('dependencies', {}).items()] + return [(t, k, {'version': v} if isinstance(v, str) else v) for t, k, v in result] + +features = {name: set(fs) for name, fs in ROOTS.items()} +optional = {} +extra = {} +for name in ROOTS: + if name != 'llrt_zlib': features[name].add('default') +changed = True +while changed: + before = repr((features, optional, extra)) + for name, enabled in list(features.items()): + data = crates[name][1] + optional.setdefault(name, set()) + extra.setdefault(name, {}) + for feature in list(enabled): + for child in data.get('features', {}).get(feature, []): + if '/' in child: + dep, feat = child.split('/', 1) + conditional = dep.endswith('?') + dep = dep.rstrip('?') + if conditional and dep not in optional[name]: continue + optional[name].add(dep) + extra[name].setdefault(dep, set()).add(feat) + elif child.startswith('dep:'): + optional[name].add(child[4:]) + elif child in data.get('features', {}): + enabled.add(child) + else: + optional[name].add(child) + for _, dep, spec in dependencies(data): + if spec.get('optional') and dep not in optional[name]: continue + if dep.startswith('llrt_'): + fs = features.setdefault(dep, set()) + fs.update(spec.get('features', [])) + fs.update(extra[name].get(dep, set())) + if spec.get('default-features', True): fs.add('default') + changed = before != repr((features, optional, extra)) + +# Merge only selected external runtime dependencies. Optional backend selection +# is resolved during import: this package ships Shell's existing backend choices. +merged = {} +for name in features: + for target, dep, spec in dependencies(crates[name][1]): + if dep.startswith('llrt_'): continue + if spec.get('optional') and dep not in optional[name]: continue + spec = copy.deepcopy(spec) + spec.pop('optional', None) + spec.pop('path', None) + spec['features'] = sorted(set(spec.get('features', [])) | extra[name].get(dep, set())) + key = (target, dep) + if key in merged: + prior = merged[key] + if prior['version'] != spec['version'] and dep != 'rquickjs': + raise SystemExit(f'incompatible versions for {dep}: {prior} / {spec}') + prior['features'] = sorted(set(prior['features']) | set(spec['features'])) + prior['default-features'] = prior.get('default-features', True) or spec.get('default-features', True) + else: merged[key] = spec +binding = merged[(None, 'rquickjs')] +binding.update(package='quickjs-jit', version='=0.12.7', path='..') +binding['features'] = sorted(set(binding['features']) | {'std', 'loader'}) + +vendor = dest / 'src' / 'llrt' +if vendor.exists(): shutil.rmtree(vendor) +vendor.mkdir(parents=True) +provenance = {'repository': 'https://github.com/awslabs/llrt', 'revision': REVISION, + 'roots': ROOTS, 'features': {n: sorted(f) for n, f in sorted(features.items())}, 'files': {}} +# Keep the upstream unit tests and their helper in the same crate. +names = sorted(features) + ['llrt_test'] +macro_names = {} +for name in names: + src = crates[name][0] / 'src' + macro_names[name] = [] + for p in src.rglob('*.rs'): + text = p.read_text() + macro_names[name] += re.findall(r'#\[macro_export\]\s*macro_rules!\s+(\w+)', text) +for name in names: + src = crates[name][0] / 'src' + for p in src.rglob('*'): + if not p.is_file(): continue + output = vendor / name / p.relative_to(src) + output.parent.mkdir(parents=True, exist_ok=True) + raw = p.read_bytes() + provenance['files'][str(p.relative_to(root))] = hashlib.sha256(raw).hexdigest() + if p.suffix != '.rs': output.write_bytes(raw); continue + text = raw.decode() + text = text.replace('env!("CARGO_PKG_VERSION")', json.dumps(crates[name][1]['package']['version'])) + # Former crate-local paths are now local to the imported module. + text = re.sub(r'\bcrate::', f'crate::{name}::', text) + for other in names: + text = re.sub(r'(?() +})?; +# Ok::<(), rquickjs::Error>(()) +``` + +Hosts can compose their own loader with `buffer::BufferModule`, +`crypto::CryptoModule`, `path::PathModule`, `url::UrlModule` and +`zlib::ZlibModule`. Call `init` once on each fresh context to install globals +before evaluating scripts. Hosts still own Tokio execution and QuickJS job +polling for asynchronous operations. No filesystem, process, network or fetch +modules are installed by this package. + +This crate requires Rust 1.89 or newer because of the selected cryptography +dependencies. Bindings are `quickjs-jit` 0.12.7. Development uses a versioned local dependency; +publishing requires that binding release and its dependencies on crates.io +first. The `parallel` feature forwards the binding's parallel runtime support. +The imported configuration uses RustCrypto, Rust brotli/flate2, and zstd's native +backend, matching the current Shell integration. + +Upstream: LLRT revision `7b95c82a9b15e7ddfb2778eca4b5a63111e74f51`. +Sources retain their copyright headers. See `NOTICE`, `LICENSE-APACHE` and +`UPSTREAM.json`. To reproduce the import from a checkout at that revision, run +`python3 scripts/import-stdlib.py /path/to/llrt` from the repository (Python 3.11+). + + +## Migrating a GPUI Shell host + +Replace the five `llrt_*` dependency entries with `quickjs-jit-stdlib`. Change +`llrt_buffer::BufferModule` to `quickjs_jit_stdlib::buffer::BufferModule`, and +likewise for `crypto`, `path`, `url` and `zlib`. Replace the three global +initializers with `quickjs_jit_stdlib::init(ctx)` or retain individual module +initializers in the existing order. Remove the local `rquickjs` compatibility +crate and its `[patch.crates-io]` entry. Host-specific loaders and permission +checks stay in the host; the standard modules can be composed with them. + +## Validation and publication + +Run `cargo test -p quickjs-jit-stdlib --all-features`. To verify all unpublished +local binding packages together, Cargo can stage them into a temporary registry: + +```sh +cargo package --allow-dirty -p quickjs-jit-sys -p quickjs-jit-core \ + -p quickjs-jit-macro -p quickjs-jit -p quickjs-jit-stdlib +``` + +This builds the extracted packages without publishing anything. Once the +binding packages are available on crates.io, stdlib is the only additional +package to publish. Its archive contains the adapted LLRT modules, original +license and notice, and provenance; it does not contain nested LLRT manifests. diff --git a/stdlib/UPSTREAM.json b/stdlib/UPSTREAM.json new file mode 100644 index 00000000..5b90bfb9 --- /dev/null +++ b/stdlib/UPSTREAM.json @@ -0,0 +1,197 @@ +{ + "features": { + "llrt_abort": [ + "default", + "sleep-timers" + ], + "llrt_async_hooks": [ + "default" + ], + "llrt_buffer": [ + "default" + ], + "llrt_compression": [ + "brotli-rust", + "flate2-rust", + "zstd-rust" + ], + "llrt_context": [ + "default" + ], + "llrt_crypto": [ + "_rustcrypto", + "_subtle-full", + "crypto-rust", + "default" + ], + "llrt_encoding": [ + "default" + ], + "llrt_events": [ + "default" + ], + "llrt_exceptions": [ + "default" + ], + "llrt_hooking": [ + "default" + ], + "llrt_json": [ + "default" + ], + "llrt_path": [ + "default" + ], + "llrt_stream_web": [ + "default" + ], + "llrt_timers": [ + "default" + ], + "llrt_url": [ + "default" + ], + "llrt_utils": [], + "llrt_zlib": [ + "compression-rust" + ] + }, + "files": { + "libs/llrt_compression/src/lib.rs": "b1ab223de0b0d5f58de1eb8a9aae4fb2c3bb476773cba4984a82631ea62e7534", + "libs/llrt_compression/src/streaming.rs": "0081dee6787c1a055e2dbe518139de799ced8ba215bbf347c194db4ab698ba2d", + "libs/llrt_context/src/lib.rs": "378a1b41854247cbf058191013396100056f9a769d00ae03a5ee6f022bcadb16", + "libs/llrt_encoding/src/lib.rs": "f6b58ea2878ae0b9c191d86b3224db3989e7fc5c8cbd04b1f2d630325d1c97ec", + "libs/llrt_hooking/src/lib.rs": "71e438e92924ebef01bc979172ec4efe0ceef3caec96f083d350700a16f0bc84", + "libs/llrt_json/src/escape.rs": "6a9ccc1609ee270f29298a30041ac7d3224fda9c98ef1512e181d9bf8a3d89ee", + "libs/llrt_json/src/lib.rs": "f1e4eb2832c8316a2b857123c8d8fbf5c3d5b8049fa0792193a39e76debbc348", + "libs/llrt_json/src/parse.rs": "47d87e0544d11a82ed91d61c65ca67e6371cbf4523c061926da7f4846fb93fcf", + "libs/llrt_json/src/stringify.rs": "2229cf9fa359b63b1aca49fb2a26882d1333893dc24ef3cb0e2fc555fd752c0f", + "libs/llrt_test/src/lib.rs": "957e88cfa7716a9f8fff81b3bd82907681327f67786acd1e571102432a100c85", + "libs/llrt_utils/src/any_of.rs": "262075a7da167fb541036f116d2b31e4e45f2614063fc003d7a81499e7cd6c97", + "libs/llrt_utils/src/array_buffer.rs": "fb2fd5d4c80478189d0a70011239f3de4f4942f6e492b4e8ebebe013eb5b156c", + "libs/llrt_utils/src/bytearray_buffer.rs": "52290d386fa495c51c9dade2379586692350532c58a2e7e5571002791559aa52", + "libs/llrt_utils/src/bytes.rs": "da8e4c3bbd6caa604d393c6168a525e502ac0ee7aff325e76daff6fb35f671a2", + "libs/llrt_utils/src/class.rs": "10782bc386c2130ccc480c76e4ac03c39d1093216be441248995f91d83b07e33", + "libs/llrt_utils/src/clone.rs": "4588ca5d6809d8bc9c57fefa622efa2507ebd79f64a37115fa742bfaa116c322", + "libs/llrt_utils/src/ctx.rs": "8876ff8b7acc699baa29080f0089ee6efd1bd3cea23a8281bc8aef9a272806de", + "libs/llrt_utils/src/error.rs": "717d8eb504fc1e0a58b9cfbf70aceeb222a779d200df47438e688d89fbd10777", + "libs/llrt_utils/src/error_messages.rs": "b0be1dfc426cd07ced60cd60c77a9c633cf74899046204d73b64230a20aca66b", + "libs/llrt_utils/src/fs.rs": "994567b6b41773dd7e1ffbc5afec4fb56a73a15d9d20dfc791cec134c3ae705b", + "libs/llrt_utils/src/hash.rs": "8f67ad34d50aab82a6228ddf5afefdc7b1ce7f59ed94dc16d6d74274e3f757af", + "libs/llrt_utils/src/io.rs": "25f2a8fbfbd761efcf14000a08766d0def23d08ea3e065584bf0a365239a4d33", + "libs/llrt_utils/src/latch.rs": "379bb79d20955263d3bab52c2fde86e2f4aad9fa7f5cb09cd5464999d3882b51", + "libs/llrt_utils/src/lib.rs": "9d16b23e1eef07fd0cf76fd87ddc895be54c8bfc72219112989c8f9ea026a26d", + "libs/llrt_utils/src/macros.rs": "9b2683c35bc32fd5d2e2d15c01b807a7aad661bd9fd941eeba265fad4bd66f20", + "libs/llrt_utils/src/mc_oneshot.rs": "9b26ca47bc31d70714542dd4f544279112d5af0eb22ab4f723c1645cb62c82f2", + "libs/llrt_utils/src/module.rs": "849fd6a78613f51dd7f7c1c3d13490a0f83cb9aef6353092100e528bf0ab5f3c", + "libs/llrt_utils/src/object.rs": "7b6e334461d1d9474eb1e66d2735f445ec7b0866861a4ebfb8136421d5873c21", + "libs/llrt_utils/src/option.rs": "0641ba37bf512b4929dbdeeb1e5a13360df6748565d18c701d20c6fef52ddd9f", + "libs/llrt_utils/src/primordials.rs": "ebc757c7dcaf146c9d59948e749e9b097b565c6bce845ac1d0c2061cce137a19", + "libs/llrt_utils/src/provider.rs": "ffa04d08e605b3a3a133ea1ae8a9e470d092235274d27037ac727b0c4b1c566d", + "libs/llrt_utils/src/result.rs": "ff676fef4d64e8af3214467e9f600b529a0af508144dcc8db151fafb1dcc676e", + "libs/llrt_utils/src/reuse_list.rs": "e4bb420085fc2bce1f24d234ef7ddb29aac36aee40bb534339d624c978bf10b2", + "libs/llrt_utils/src/signals.rs": "5aadbf5a880f5d5eb09066faf20074d3ae8d8177d65b47935cff5ebc15dc84c9", + "libs/llrt_utils/src/string.rs": "b25e47b515448beca3239db5085e4bc4648296a7d6c24f544bda5d2ba9fa0311", + "libs/llrt_utils/src/sysinfo.rs": "b1912962fb6e3982f763e689c9e4db1e9de8f34ee0099f45b8e44eff8783dd04", + "libs/llrt_utils/src/time.rs": "1fea389eb2fac01d34092ffb7bc47d68149719034e40ba7ad48d19cdd2615f19", + "modules/llrt_abort/src/abort_controller.rs": "4c47c6b7e5f30d5e3da012bee73be6b6fb8b728a6167a2b58aad792a54d38bd2", + "modules/llrt_abort/src/abort_signal.rs": "d8b4acff4703e25275bb31655709ef65d1dabe5eaa20b09bc91271cfef5a7013", + "modules/llrt_abort/src/lib.rs": "c6cd0f4c108dcdb9a4e3ad9f8c7aff166de9cd16b743e901ad563b8ba62a5509", + "modules/llrt_async_hooks/src/finalization_registry.rs": "49a3303a899205abbcc3cd73498ae7b2b6dbc702165e3b149fde088fc82622a7", + "modules/llrt_async_hooks/src/lib.rs": "ed893a0c12a2ba2f6fe7253ab5ea8a7898cb91a895fafcabdf73a7030eb34988", + "modules/llrt_buffer/src/array_buffer_view.rs": "3765380e5fd4a0aa88f6ec8379e79cf16820f8bc47226958f72afbae4deaee1b", + "modules/llrt_buffer/src/blob.rs": "2241a76288260cc7256b0e83eaac0a86e5d60ca90ecee2b6d4211896e7a46c4d", + "modules/llrt_buffer/src/buffer.rs": "af69057974fce798449a5abe2fb5b2ad362a5427253fea2f75fe03ea6cfc5b99", + "modules/llrt_buffer/src/file.rs": "66af2d177fde3ec9cb7bf3490ce4ef99e4be75fb7f09f30670ebb9fa621555d4", + "modules/llrt_buffer/src/lib.rs": "b4fc7070e5d64ba83adef165db27bd084de4b8c17b68741931e5c753fdcdad06", + "modules/llrt_crypto/src/crc32.rs": "20ffd5f2ffe8da6928bb906b718e92342160d8e3f641ecbca5ad0c80456668eb", + "modules/llrt_crypto/src/hash.rs": "80ca6b27996d84179dca47253eb447d8aef1ad8b551d8731357ee162011e46a9", + "modules/llrt_crypto/src/lib.rs": "c8f1e8a68493a01aa88cb478599e6b46ae830d2c8b0192ea333125cb481fe2e1", + "modules/llrt_crypto/src/provider/graviola.rs": "731050853fd6625359a7281eb38b17739ae1c573c41af2a10003e688c4dd5361", + "modules/llrt_crypto/src/provider/mod.rs": "d3d7fc77ac421b5dc4b4e885d4a2452a3736f964d3fef5cb81578679f9a16d2b", + "modules/llrt_crypto/src/provider/openssl.rs": "1258c73175c608e5a3f37ee52c373a1c52181727aae31d551b9d9bfca36afc51", + "modules/llrt_crypto/src/provider/ring.rs": "64df7ff2d40e0e92ecba9dc66a88fee7881ee891133632bcd30165520f24affe", + "modules/llrt_crypto/src/provider/rust/aes_variants.rs": "81a6c7434ed235d0f4747a46802d9a8af3929c8fe1c4d9dc628f7e49e741bfc8", + "modules/llrt_crypto/src/provider/rust/mod.rs": "428ad9fd283f3384e6ba0bd0e826beb7656c843e9bc36c2c96022ae1dae60172", + "modules/llrt_crypto/src/subtle/crypto_key.rs": "710371a2e5166063894880676785aa34452fce4465b82ed428ef0656e715433a", + "modules/llrt_crypto/src/subtle/derive_algorithm.rs": "1c32532b9f34131dc23884e90f5092ff6740fa1a511530b3ae1f22df6dea8c08", + "modules/llrt_crypto/src/subtle/derive_bits.rs": "e91882fd0c312513964244e98a1ef53504ec74f8320ca9cbf82acfd5ea94afa2", + "modules/llrt_crypto/src/subtle/derive_keys.rs": "2c3e0cb4479577b24f60548afa8e956d83378d82862d1cf27b8450e280fb0cfa", + "modules/llrt_crypto/src/subtle/digest.rs": "eb2784bf5ec248d860aeb8ac85867f3503ac111dff65ed0ba9d406fc90ff97b7", + "modules/llrt_crypto/src/subtle/encryption.rs": "2baf77469ec5c2673a31353246cf6ab2225b09f8c1a9e8a01f156fa60c69a951", + "modules/llrt_crypto/src/subtle/encryption_algorithm.rs": "a01dcde97b7317e6a3795592f7f4faadd034ec3de8f13eb64c87f40ab3db7574", + "modules/llrt_crypto/src/subtle/export_key.rs": "c3d16f347ad264b14903675b2f4f68f8214b282cdbce0b9d3cfd956232fe3970", + "modules/llrt_crypto/src/subtle/generate_key.rs": "545ee866feec5a3987ce5959cb4ec8999ba2bbed64cc2830a0991cd390d2827c", + "modules/llrt_crypto/src/subtle/import_key.rs": "080a86eeb879b84a79ab572ea1dba5cb42fcd949a67a99f19b1b598a4826a67d", + "modules/llrt_crypto/src/subtle/key_algorithm.rs": "60b413c4581a6de8ad94f3782082810e33cccb4094e7e8e7638a153ce6445d6f", + "modules/llrt_crypto/src/subtle/mod.rs": "f4cc187891b18454e69526dbe1e7268d18f33978496093b26bd19c5699c9c269", + "modules/llrt_crypto/src/subtle/sign.rs": "b972432009c41cbde4f421b7401849cd3dddcd3ba12faa6457125af9250d8d23", + "modules/llrt_crypto/src/subtle/sign_algorithm.rs": "c5238409c833cf1950c6e27c47c7d2ff5da6943cdc5d7398c975a9f0672e6b3a", + "modules/llrt_crypto/src/subtle/stubs.rs": "258ad00d09084713e666b982abb0198f35175bb9465e8d35200281d3d251cd74", + "modules/llrt_crypto/src/subtle/util.rs": "09bbe7bd35dfae4f1e547b14558d51987c410b330022a27ff1f8639b2dbb3a70", + "modules/llrt_crypto/src/subtle/verify.rs": "50b94c3f7476085fa15dfee704c1ae86b45746a715ce31fee0a8a3568562e3a5", + "modules/llrt_crypto/src/subtle/wrapping.rs": "438a0b83ff2309e3c040bb3d4d875b8bdd847f338455323427eeb1d1b07b2db6", + "modules/llrt_events/src/custom_event.rs": "3fd2b93af9f049f5b422040d267b5db1a91dded37287cb90e94fc7ca69eb7dae", + "modules/llrt_events/src/event.rs": "702f6840703b355d025dce272385ab2c3d03776ca06863e9c2161102dca54517", + "modules/llrt_events/src/event_target.rs": "cfe75b0e524888916d4c8474cad58563e7793fdb59b64dc1da18f8a0f783871b", + "modules/llrt_events/src/lib.rs": "fc9856e7705c6d9a65ea6b32d3b0de5a46a8245c2b05a00714e08a4478f7f2a5", + "modules/llrt_exceptions/src/lib.rs": "fdeba55c9e1d6c063db6f89ef06f3b3d3debabd516254d1f574ccd53e43cfdbd", + "modules/llrt_path/src/lib.rs": "ce0acd413b638337d77ff9e2db8e462cdb063a29ae4784c93f93315761f023e7", + "modules/llrt_stream_web/src/lib.rs": "c4f509808b1f28b45870ccdfc3b4d6b62f07f34219916bca938b5d59108d316a", + "modules/llrt_stream_web/src/queuing_strategy/byte_length.rs": "7b7526dc31fd146d9fcf1743fa33cad598447cb08a068be1e7a09e22a8a4b7d3", + "modules/llrt_stream_web/src/queuing_strategy/count.rs": "1e14889970137784aa8e4e7696f31daa76dd84fe34561cc8cf9ac41888f6f217", + "modules/llrt_stream_web/src/queuing_strategy/mod.rs": "08c8f072d93944f2dd053b1ed7467ce0ab194c7355e033d6c03c9f7e09ec72b5", + "modules/llrt_stream_web/src/queuing_strategy/tests.rs": "7d99a2d6e15803a54d6a09bd5725d12d3a468a2649d52903d18fdc93a2e69e5f", + "modules/llrt_stream_web/src/readable/byob_reader.rs": "73d37b3bdeef3e69fae2f18d41c401f1a9a0e51a043cdff2469a364cd0fe354c", + "modules/llrt_stream_web/src/readable/byte_controller.rs": "34825b23e92952ab626c4523ef299c28b949c803d3bed6cbe9d13e752cd61408", + "modules/llrt_stream_web/src/readable/controller.rs": "1cded62c2b1f42cd86c2c28f1feda98170e38fcdc2dae16071cead7a5b83dea1", + "modules/llrt_stream_web/src/readable/default_controller.rs": "38a57f17ddd90dd60c74ac755fcc7c903ce6da26a8de7707e9511de32c02912e", + "modules/llrt_stream_web/src/readable/default_reader.rs": "ff4a966793df021ede2f425cc90f526eeb87774402dce8539b3c791c1ba91e75", + "modules/llrt_stream_web/src/readable/iterator.rs": "76fcc84083dc43775e1fe6edf66ebd7423c7c0055b7ed8326fdae0ada4a8b0ed", + "modules/llrt_stream_web/src/readable/mod.rs": "12580d913471a79125547ba9cfbb44cc1da2ba02db1ccb156dea8ab9e6a781ad", + "modules/llrt_stream_web/src/readable/objects.rs": "e12343220897d4cdd6317045be90427347102ad0840bedcd2262736e89253f01", + "modules/llrt_stream_web/src/readable/reader.rs": "e7458e756d6bae494e2327b5f2eb54d5a100695b1d695644d223c8cd2b11fe4f", + "modules/llrt_stream_web/src/readable/stream/algorithms.rs": "8e27781d599e6c04c9a8b94743d327deb47d90f99be4330299e4c9915cd7fe73", + "modules/llrt_stream_web/src/readable/stream/mod.rs": "22f338e88b4b9a7bf0af45de220c13a2b45ded9eb6f539b60e8eeeb5b3d80ba8", + "modules/llrt_stream_web/src/readable/stream/pipe.rs": "ccfa9c1c85290a0c5e97a5e8d373e6bdd6af80efe7eef3d86b785ed8a3e1083a", + "modules/llrt_stream_web/src/readable/stream/source.rs": "789e53bbb1d64b272110f3e6c20f92e5d15039d96abaefc86128bd454f401522", + "modules/llrt_stream_web/src/readable/stream/tee.rs": "d173d0a53f8c2590262e0ac40cde9ffb6ad5c05916713395c0fec771bf1a2871", + "modules/llrt_stream_web/src/readable_writable_pair.rs": "3f5ca0f7623aa997172df6a95ea5e36fe987c1dac32f8863cac207a111ba4e23", + "modules/llrt_stream_web/src/transform/controller.rs": "c02c1501d3463f088afff0b5fa7a0986c4f04df6b5d4e00cb00be3339238935a", + "modules/llrt_stream_web/src/transform/mod.rs": "10d7b0d55e4ef16806b80087a18931273a68ff35c1069ccd417b033d4fa4b5a9", + "modules/llrt_stream_web/src/transform/stream.rs": "0fa0351f5b1ddc5cd50e5f46d0a7243a3a1fa776b430bc4380d7495849097c40", + "modules/llrt_stream_web/src/transform/tests.rs": "732165628d15f5cfc76c4bb212deebbdddc52dc826f4b4e7ddd6fb03fc885834", + "modules/llrt_stream_web/src/transform/transformer.rs": "278f08b121ebd3156e68bcb35964e0ed780ed33e49ca9248d310057812e8311d", + "modules/llrt_stream_web/src/utils/mod.rs": "b92077176adf26fef7fd9c13e4bfd4b337fadd10dceb39223d09338cefead4fd", + "modules/llrt_stream_web/src/utils/promise.rs": "e9de2b34b2f580160734ae884b8c0e91c21c6247fe118fb6a5b399bb74818e28", + "modules/llrt_stream_web/src/utils/queue.rs": "dfa3a49a61c14b1f305a0eab6c1a49a20b7f06061bab58fcc108633846336819", + "modules/llrt_stream_web/src/writable/default_controller.rs": "cc57d48bc914b327f683be5779c3bd767d881a6f03495592fb56acf03b8f73d6", + "modules/llrt_stream_web/src/writable/default_writer.rs": "d9b6caa39d9b1bc645183b4eb4c555da69879a9a7c1521db1715d4826669d01a", + "modules/llrt_stream_web/src/writable/mod.rs": "2d272315e87daebea4f30d42613ee9b3fe1313761b8e6f086fe5f3597992a253", + "modules/llrt_stream_web/src/writable/objects.rs": "20eaf1c41136739d6ef56c0e853154f7f03533ef02811a04fdfb9dfc48dfbd5a", + "modules/llrt_stream_web/src/writable/stream/mod.rs": "4f0ae1e11bad363f8f0532f61b36b1c435ec34bc13c8d8e8d81d37c26fae96ba", + "modules/llrt_stream_web/src/writable/stream/sink.rs": "029c389756500e83f3f0f73f0618c3e36e9dfc5d3fb47d3c14110b45ee9bc707", + "modules/llrt_stream_web/src/writable/writer.rs": "ec7675027a0080bece5594b0becd2102d66c8e7bb4b0faad4e71bb2aef06b5b1", + "modules/llrt_timers/src/lib.rs": "dfb82e2671ab0854c46ec532f22a340fdb7ccd9744aefcad645748c9deacccff", + "modules/llrt_url/src/lib.rs": "2c2980426a1566c369cf75d3a63a793ecc67a2796db6b11ac31b63e21bf0edb4", + "modules/llrt_url/src/url_class.rs": "c4f9057df44a4ea2876a414d492d2fbbc85d2321da58fa1a1d8a74d952d9e7fa", + "modules/llrt_url/src/url_search_params.rs": "e48498686956e9492df7f6b82d480d2f64ee1b2d7a3477f98499daa47d86257d", + "modules/llrt_zlib/src/brotli.rs": "4a59a6186ed2525264dff303528cf10d6fc45c5100fec316d04538f293c0c738", + "modules/llrt_zlib/src/lib.rs": "a35873cdf3a3baab7233364c61d0cf45f03085211d8d5d70dc192c11a04c35fd", + "modules/llrt_zlib/src/zlib.rs": "6dd03456f7ccd0f279c38f7680ec4b4721d6449e524ee214206e843efa06750d", + "modules/llrt_zlib/src/zstd.rs": "7511caba3eeed9080c431c3d5c30855307e597b1d066566c07d3a5aae69393ae" + }, + "repository": "https://github.com/awslabs/llrt", + "revision": "7b95c82a9b15e7ddfb2778eca4b5a63111e74f51", + "roots": { + "llrt_buffer": [], + "llrt_crypto": [ + "crypto-rust" + ], + "llrt_path": [], + "llrt_url": [], + "llrt_zlib": [ + "compression-rust" + ] + } +} diff --git a/stdlib/src/lib.rs b/stdlib/src/lib.rs new file mode 100644 index 00000000..3dcf42ca --- /dev/null +++ b/stdlib/src/lib.rs @@ -0,0 +1,113 @@ +#![doc = include_str!("../README.md")] + +// BEGIN IMPORTED MODULES +#[allow(dead_code, unused_imports, private_interfaces, clippy::non_minimal_cfg)] +#[path = "llrt/llrt_abort/lib.rs"] +mod llrt_abort; +#[allow(dead_code, unused_imports, private_interfaces, clippy::non_minimal_cfg)] +#[path = "llrt/llrt_async_hooks/lib.rs"] +mod llrt_async_hooks; +#[allow(dead_code, unused_imports, private_interfaces, clippy::non_minimal_cfg)] +#[path = "llrt/llrt_buffer/lib.rs"] +mod llrt_buffer; +#[allow(dead_code, unused_imports, private_interfaces, clippy::non_minimal_cfg)] +#[path = "llrt/llrt_compression/lib.rs"] +mod llrt_compression; +#[allow(dead_code, unused_imports, private_interfaces, clippy::non_minimal_cfg)] +#[path = "llrt/llrt_context/lib.rs"] +mod llrt_context; +#[allow(dead_code, unused_imports, private_interfaces, clippy::non_minimal_cfg)] +#[path = "llrt/llrt_crypto/lib.rs"] +mod llrt_crypto; +#[allow(dead_code, unused_imports, private_interfaces, clippy::non_minimal_cfg)] +#[path = "llrt/llrt_encoding/lib.rs"] +mod llrt_encoding; +#[allow(dead_code, unused_imports, private_interfaces, clippy::non_minimal_cfg)] +#[path = "llrt/llrt_events/lib.rs"] +mod llrt_events; +#[allow(dead_code, unused_imports, private_interfaces, clippy::non_minimal_cfg)] +#[path = "llrt/llrt_exceptions/lib.rs"] +mod llrt_exceptions; +#[allow(dead_code, unused_imports, private_interfaces, clippy::non_minimal_cfg)] +#[path = "llrt/llrt_hooking/lib.rs"] +mod llrt_hooking; +#[allow(dead_code, unused_imports, private_interfaces, clippy::non_minimal_cfg)] +#[path = "llrt/llrt_json/lib.rs"] +mod llrt_json; +#[allow(dead_code, unused_imports, private_interfaces, clippy::non_minimal_cfg)] +#[path = "llrt/llrt_path/lib.rs"] +mod llrt_path; +#[allow(dead_code, unused_imports, private_interfaces, clippy::non_minimal_cfg)] +#[path = "llrt/llrt_stream_web/lib.rs"] +mod llrt_stream_web; +#[cfg(test)] +#[allow(dead_code)] +#[path = "llrt/llrt_test/lib.rs"] +mod llrt_test; +#[allow(dead_code, unused_imports, private_interfaces, clippy::non_minimal_cfg)] +#[path = "llrt/llrt_timers/lib.rs"] +mod llrt_timers; +#[allow(dead_code, unused_imports, private_interfaces, clippy::non_minimal_cfg)] +#[path = "llrt/llrt_url/lib.rs"] +mod llrt_url; +#[allow(dead_code, unused_imports, private_interfaces, clippy::non_minimal_cfg)] +#[path = "llrt/llrt_utils/lib.rs"] +mod llrt_utils; +#[allow(dead_code, unused_imports, private_interfaces, clippy::non_minimal_cfg)] +#[path = "llrt/llrt_zlib/lib.rs"] +mod llrt_zlib; +// END IMPORTED MODULES + +pub mod buffer { + pub use crate::llrt_buffer::*; +} +pub mod crypto { + pub use crate::llrt_crypto::*; +} +pub mod path { + pub use crate::llrt_path::*; +} +pub mod url { + pub use crate::llrt_url::*; +} +pub mod zlib { + pub use crate::llrt_zlib::*; +} + +use rquickjs::{ + loader::{BuiltinResolver, ModuleLoader}, + Ctx, Result, +}; + +/// Module names provided by this redistribution. +pub const MODULE_NAMES: &[&str] = &["buffer", "crypto", "path", "url", "zlib"]; + +/// A resolver for the five standard modules. Compose it with host resolvers. +pub fn resolver() -> BuiltinResolver { + MODULE_NAMES + .iter() + .fold(BuiltinResolver::default(), |resolver, name| { + resolver.with_module(*name) + }) +} + +/// A loader for the five standard modules. Compose it with host loaders. +pub fn loader() -> ModuleLoader { + ModuleLoader::default() + .with_module("buffer", buffer::BufferModule) + .with_module("crypto", crypto::CryptoModule) + .with_module("path", path::PathModule) + .with_module("url", url::UrlModule) + .with_module("zlib", zlib::ZlibModule) +} + +/// Install the Buffer, URL and Crypto globals into a fresh context, in order. +/// +/// Call once per context before evaluating a module using this library. +/// This does not install host I/O modules, start Tokio, or drive pending jobs. +pub fn init(ctx: &Ctx<'_>) -> Result<()> { + buffer::init(ctx)?; + url::init(ctx)?; + crypto::init(ctx)?; + Ok(()) +} diff --git a/stdlib/src/llrt/llrt_abort/abort_controller.rs b/stdlib/src/llrt/llrt_abort/abort_controller.rs new file mode 100644 index 00000000..86d0876a --- /dev/null +++ b/stdlib/src/llrt/llrt_abort/abort_controller.rs @@ -0,0 +1,62 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use rquickjs::{ + atom::PredefinedAtom, + prelude::{Opt, This}, + Class, Ctx, JsLifetime, Result, Value, +}; + +use super::AbortSignal; + +#[rquickjs::class] +#[derive(rquickjs::class::Trace)] +pub struct AbortController<'js> { + signal: Class<'js, AbortSignal<'js>>, +} + +unsafe impl<'js> JsLifetime<'js> for AbortController<'js> { + type Changed<'to> = AbortController<'to>; +} + +#[rquickjs::methods] +impl<'js> AbortController<'js> { + #[qjs(constructor)] + pub fn new(ctx: Ctx<'js>) -> Result { + let signal = AbortSignal::new(); + + let abort_controller = Self { + signal: Class::instance(ctx, signal)?, + }; + Ok(abort_controller) + } + + #[qjs(get)] + pub fn signal(&self) -> Class<'js, AbortSignal<'js>> { + self.signal.clone() + } + + #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] + pub fn to_string_tag() -> &'static str { + stringify!(AbortController) + } + + pub fn abort( + ctx: Ctx<'js>, + this: This>, + reason: Opt>, + ) -> Result<()> { + let instance = this.0.borrow(); + let signal = instance.signal.clone(); + let mut signal_borrow = signal.borrow_mut(); + if signal_borrow.aborted { + //only once + return Ok(()); + } + signal_borrow.set_reason(reason); + drop(signal_borrow); + AbortSignal::send_aborted(This(signal), ctx)?; + + Ok(()) + } +} diff --git a/stdlib/src/llrt/llrt_abort/abort_signal.rs b/stdlib/src/llrt/llrt_abort/abort_signal.rs new file mode 100644 index 00000000..bfce649c --- /dev/null +++ b/stdlib/src/llrt/llrt_abort/abort_signal.rs @@ -0,0 +1,281 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::sync::{Arc, RwLock}; + +use crate::llrt_events::{Emitter, EventEmitter, EventList}; +use crate::llrt_exceptions::{DOMException, DOMExceptionName}; +use crate::llrt_utils::mc_oneshot; +use rquickjs::{ + atom::PredefinedAtom, + class::{Trace, Tracer}, + function::OnceFn, + prelude::{Opt, This}, + Array, Class, Ctx, Error, Exception, Function, JsLifetime, Result, Undefined, Value, +}; + +#[derive(Clone)] +#[rquickjs::class] +pub struct AbortSignal<'js> { + emitter: EventEmitter<'js>, + pub aborted: bool, + reason: Option>, + pub sender: mc_oneshot::Sender>, +} + +unsafe impl<'js> JsLifetime<'js> for AbortSignal<'js> { + type Changed<'to> = AbortSignal<'to>; +} + +impl<'js> Trace<'js> for AbortSignal<'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + if let Some(reason) = &self.reason { + tracer.mark(reason); + } + self.emitter.trace(tracer); + self.sender.trace(tracer); + } +} + +impl<'js> Emitter<'js> for AbortSignal<'js> { + fn get_event_list(&self) -> Arc>> { + self.emitter.get_event_list() + } +} + +#[rquickjs::methods(rename_all = "camelCase")] +impl<'js> AbortSignal<'js> { + #[qjs(constructor)] + pub fn new() -> Self { + let (sender, _) = mc_oneshot::channel::>(); + Self { + emitter: EventEmitter::new(), + aborted: false, + reason: None, + sender, + } + } + + #[qjs(get, rename = "onabort")] + pub fn get_on_abort(&self) -> Option> { + Self::get_listeners_str(self, "abort").first().cloned() + } + + #[qjs(set, rename = "onabort")] + pub fn set_on_abort( + this: This>, + ctx: Ctx<'js>, + listener: Function<'js>, + ) -> Result<()> { + Self::add_event_listener_str(this.0, &ctx, "abort", listener, false, false)?; + Ok(()) + } + + pub fn remove_on_abort( + this: This>, + ctx: Ctx<'js>, + listener: Function<'js>, + ) -> Result<()> { + Self::remove_event_listener_str(this.0, &ctx, "abort", listener)?; + Ok(()) + } + + pub fn throw_if_aborted(&self, ctx: Ctx<'js>) -> Result<()> { + if self.aborted { + return Err(ctx.throw( + self.reason + .clone() + .unwrap_or_else(|| Undefined.into_value(ctx.clone())), + )); + } + Ok(()) + } + + #[qjs(static)] + pub fn any(ctx: Ctx<'js>, signals: Array<'js>) -> Result> { + let mut new_signal = AbortSignal::new(); + + let mut signal_instances = Vec::with_capacity(signals.len()); + + for signal in signals.iter() { + let signal: Value = signal?; + let signal: Class = Class::from_value(&signal) + .map_err(|_| Exception::throw_type(&ctx, "Value is not an AbortSignal instance"))?; + let signal_borrow = signal.borrow(); + if signal_borrow.aborted { + new_signal.aborted = true; + new_signal.reason.clone_from(&signal_borrow.reason); + let new_signal = Class::instance(ctx, new_signal)?; + return Ok(new_signal); + } else { + drop(signal_borrow); + signal_instances.push(signal); + } + } + + let new_signal_instance = Class::instance(ctx.clone(), new_signal)?; + for signal in signal_instances { + let signal_instance_2 = new_signal_instance.clone(); + Self::add_event_listener_str( + signal, + &ctx, + "abort", + Function::new( + ctx.clone(), + OnceFn::from(|ctx, signal| { + struct Args<'js>(Ctx<'js>, This>>); + let Args(ctx, signal) = Args(ctx, signal); + let mut borrow = signal_instance_2.borrow_mut(); + borrow.aborted = true; + borrow.reason.clone_from(&signal.borrow().reason); + drop(borrow); + Self::send_aborted(This(signal_instance_2), ctx) + }), + )?, + false, + true, + )?; + } + + Ok(new_signal_instance) + } + + #[qjs(get)] + pub fn aborted(&self) -> bool { + self.aborted + } + + #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] + pub fn to_string_tag() -> &'static str { + stringify!(AbortSignal) + } + + #[qjs(get)] + pub fn reason(&self) -> Option> { + self.reason.clone() + } + + #[qjs(set, rename = "reason")] + pub fn set_reason(&mut self, reason: Opt>) { + match reason.0 { + Some(new_reason) if !new_reason.is_undefined() => self.reason.replace(new_reason), + _ => self.reason.take(), + }; + } + + #[qjs(skip)] + pub fn send_aborted(this: This>, ctx: Ctx<'js>) -> Result<()> { + let mut borrow = this.borrow_mut(); + borrow.aborted = true; + let reason = get_reason_or_dom_exception( + &ctx, + borrow.reason.as_ref(), + DOMExceptionName::AbortError, + )?; + borrow.reason = Some(reason.clone()); + borrow.sender.send(reason); + drop(borrow); + Self::emit_str(this.0, &ctx, "abort", vec![], false)?; + Ok(()) + } + + #[qjs(static)] + pub fn abort(ctx: Ctx<'js>, reason: Opt>) -> Result> { + let mut signal = Self::new(); + signal.set_reason(reason); + let instance = Class::instance(ctx.clone(), signal)?; + Self::send_aborted(This(instance.clone()), ctx)?; + Ok(instance) + } + + #[qjs(static)] + pub fn timeout(ctx: Ctx<'js>, milliseconds: u64) -> Result> { + let timeout_error = + get_reason_or_dom_exception(&ctx, None, DOMExceptionName::TimeoutError)?; + + let signal = Self::new(); + let signal_instance = Class::instance(ctx.clone(), signal)?; + let signal_instance2 = signal_instance.clone(); + + let cb = Function::new( + ctx.clone(), + OnceFn::from(move |ctx| { + let mut borrow = signal_instance.borrow_mut(); + borrow.set_reason(Opt(Some(timeout_error))); + drop(borrow); + Self::send_aborted(This(signal_instance), ctx)?; + Ok::<_, Error>(()) + }), + )?; + + #[cfg(all())] + { + crate::llrt_timers::set_timeout_interval( + &ctx, + cb, + milliseconds, + crate::llrt_utils::provider::ProviderType::Timeout, + )?; + } + #[cfg(all(not(all()), any()))] + { + use crate::llrt_utils::ctx::CtxExtension; + ctx.clone().spawn_exit_simple(async move { + tokio::time::sleep(std::time::Duration::from_millis(milliseconds)).await; + cb.call::<_, ()>(())?; + Ok(()) + }); + } + #[cfg(all(not(any()), not(all())))] + { + compile_error!("Either the `sleep-tokio` or `sleep-timers` feature must be enabled") + } + + Ok(signal_instance2) + } +} + +fn get_reason_or_dom_exception<'js>( + ctx: &Ctx<'js>, + reason: Option<&Value<'js>>, + name: DOMExceptionName, +) -> Result> { + let reason = if let Some(reason) = reason { + reason.clone() + } else { + let ex = DOMException::new_with_name(ctx, name, String::new())?; + Class::instance(ctx.clone(), ex)?.into_value() + }; + Ok(reason) +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use crate::llrt_test::test_async_with; + + use super::*; + + #[cfg(all())] + #[tokio::test] + async fn test_abort_signal() { + test_async_with(|ctx| { + crate::llrt_abort::init(&ctx).unwrap(); + crate::llrt_timers::init(&ctx).unwrap(); + Box::pin(async move { + let signal = AbortSignal::timeout(ctx, 5).unwrap(); + + assert!(!signal.borrow().aborted()); + + tokio::time::sleep(Duration::from_millis(50)).await; + + assert!(signal.borrow().aborted()); + let reason = signal.borrow().reason().unwrap(); + let reason = Class::::from_value(&reason).unwrap(); + assert_eq!(reason.borrow().name(), "TimeoutError"); + }) + }) + .await; + } +} diff --git a/stdlib/src/llrt/llrt_abort/lib.rs b/stdlib/src/llrt/llrt_abort/lib.rs new file mode 100644 index 00000000..b379a331 --- /dev/null +++ b/stdlib/src/llrt/llrt_abort/lib.rs @@ -0,0 +1,26 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#![allow(clippy::new_without_default)] +use crate::llrt_events::Emitter; +use crate::llrt_utils::primordials::{BasePrimordials, Primordial}; +use rquickjs::{Class, Ctx, Result}; + +pub use self::{abort_controller::AbortController, abort_signal::AbortSignal}; + +mod abort_controller; +mod abort_signal; + +pub fn init(ctx: &Ctx<'_>) -> Result<()> { + let globals = ctx.globals(); + + BasePrimordials::init(ctx)?; + + Class::::define(&globals)?; + Class::::define(&globals)?; + + AbortSignal::add_event_emitter_prototype(ctx)?; + AbortSignal::add_event_target_prototype(ctx)?; + + Ok(()) +} diff --git a/stdlib/src/llrt/llrt_async_hooks/finalization_registry.rs b/stdlib/src/llrt/llrt_async_hooks/finalization_registry.rs new file mode 100644 index 00000000..a52c6f56 --- /dev/null +++ b/stdlib/src/llrt/llrt_async_hooks/finalization_registry.rs @@ -0,0 +1,65 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::cell::RefCell; + +use crate::llrt_utils::result::ResultExt; +use rquickjs::{prelude::Func, Ctx, Result, Value}; +use tracing::trace; + +use super::{remove_id_map, update_current_id, AsyncHookState}; + +pub(crate) fn init_finalization_registry(ctx: &Ctx<'_>) -> Result<()> { + let global = ctx.globals(); + + global.set( + "__invokeFinalizationHook", + Func::from(invoke_finalization_hook), + )?; + + let _: () = ctx.eval( + r#" + globalThis.asyncFinalizationRegistry = (() => { + const registry = new FinalizationRegistry(__invokeFinalizationHook); + return { + register(target, heldValue) { + registry.register(target, heldValue); + } + }; + })(); + "#, + )?; + + global.remove("__invokeFinalizationHook")?; + + Ok(()) +} + +fn invoke_finalization_hook<'js>(ctx: Ctx<'js>, uid: Value<'js>) -> Result<()> { + let bind_state = ctx.userdata::>().or_throw(&ctx)?; + let state = bind_state.borrow(); + if state.hooks.is_empty() { + return Ok(()); + } + + let uid = uid.as_number().unwrap() as usize; + + let current_id = remove_id_map(&ctx, uid)?; + if current_id.0 == 0 { + return Ok(()); + } + + update_current_id(&ctx, current_id)?; + trace!("Destroy[{}](async_id, trigger_id): {:?}", uid, current_id); + + for hook in &state.hooks { + if *hook.enabled.as_ref().borrow() { + if let Some(func) = &hook.destroy { + let _ = func + .call::<_, ()>((current_id.0,)) + .or_else(|_| func.call::<_, ()>(())); + } + } + } + Ok(()) +} diff --git a/stdlib/src/llrt/llrt_async_hooks/lib.rs b/stdlib/src/llrt/llrt_async_hooks/lib.rs new file mode 100644 index 00000000..ff24e5a8 --- /dev/null +++ b/stdlib/src/llrt/llrt_async_hooks/lib.rs @@ -0,0 +1,335 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::{cell::RefCell, collections::HashMap, marker::PhantomData, rc::Rc}; + +use crate::llrt_hooking::register_finalization_registry; +use crate::llrt_utils::{ + module::{export_default, ModuleInfo}, + result::ResultExt, +}; +use rquickjs::{ + module::{Declarations, Exports, ModuleDef}, + prelude::Func, + promise::PromiseHookType, + qjs, + runtime::PromiseHook, + Ctx, Function, JsLifetime, Object, Result, Value, +}; +use tracing::trace; + +mod finalization_registry; + +use crate::llrt_async_hooks::finalization_registry::init_finalization_registry; + +struct Hook<'js> { + enabled: Rc>, + init: Option>, + before: Option>, + after: Option>, + promise_resolve: Option>, + destroy: Option>, +} + +struct AsyncHookState<'js> { + hooks: Vec>, +} + +impl Default for AsyncHookState<'_> { + fn default() -> Self { + Self::new() + } +} + +impl AsyncHookState<'_> { + fn new() -> Self { + Self { hooks: Vec::new() } + } +} + +unsafe impl<'js> JsLifetime<'js> for AsyncHookState<'js> { + type Changed<'to> = AsyncHookState<'to>; +} + +struct AsyncHookIds<'js> { + next_async_id: u64, + id_map: HashMap, // (execution_async_id, trigger_async_id) + current_id: (u64, u64), // (execution_async_id, trigger_async_id) + _marker: PhantomData<&'js ()>, +} + +impl Default for AsyncHookIds<'_> { + fn default() -> Self { + Self::new() + } +} + +impl AsyncHookIds<'_> { + fn new() -> Self { + Self { + next_async_id: 1, + id_map: HashMap::new(), + current_id: (1, 1), + _marker: PhantomData, + } + } +} + +unsafe impl<'js> JsLifetime<'js> for AsyncHookIds<'js> { + type Changed<'to> = AsyncHookIds<'to>; +} + +fn create_hook<'js>(ctx: Ctx<'js>, hooks_obj: Object<'js>) -> Result> { + let init = hooks_obj.get::<_, Function>("init").ok(); + let before = hooks_obj.get::<_, Function>("before").ok(); + let after = hooks_obj.get::<_, Function>("after").ok(); + let promise_resolve = hooks_obj.get::<_, Function>("promiseResolve").ok(); + let destroy = hooks_obj.get::<_, Function>("destroy").ok(); + let enabled = Rc::new(RefCell::new(false)); + + let hook = Hook { + enabled: enabled.clone(), + init, + before, + after, + promise_resolve, + destroy, + }; + + let binding = ctx.userdata::>().or_throw(&ctx)?; + let mut state = binding.borrow_mut(); + state.hooks.push(hook); + + let obj = Object::new(ctx.clone())?; + { + let enabled_clone = enabled.clone(); + obj.set( + "enable", + Function::new(ctx.clone(), move || -> Result<()> { + *enabled_clone.borrow_mut() = true; + Ok(()) + }), + )?; + } + { + let enabled_clone = enabled.clone(); + obj.set( + "disable", + Function::new(ctx.clone(), move || -> Result<()> { + *enabled_clone.borrow_mut() = false; + Ok(()) + }), + )?; + } + + Ok(obj.into()) +} + +fn current_id() -> u64 { + // NOTE: This method is now obsolete. Therefore, it does not return a valid value. + // But we will define it because it is used by cls-hooked. + 0 +} + +fn execution_async_id(ctx: Ctx<'_>) -> Result { + let bind_ids = ctx.userdata::>().or_throw(&ctx)?; + let ids = bind_ids.borrow(); + Ok(ids.current_id.0) +} + +fn trigger_async_id(ctx: Ctx<'_>) -> Result { + let bind_ids = ctx.userdata::>().or_throw(&ctx)?; + let ids = bind_ids.borrow(); + Ok(ids.current_id.1) +} + +pub struct AsyncHooksModule; + +impl ModuleDef for AsyncHooksModule { + fn declare(declare: &Declarations) -> Result<()> { + declare.declare("createHook")?; + declare.declare("currentId")?; + declare.declare("executionAsyncId")?; + declare.declare("triggerAsyncId")?; + declare.declare("default")?; + + Ok(()) + } + + fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { + export_default(ctx, exports, |default| { + default.set("createHook", Func::from(create_hook))?; + default.set("currentId", Func::from(current_id))?; + default.set("executionAsyncId", Func::from(execution_async_id))?; + default.set("triggerAsyncId", Func::from(trigger_async_id))?; + + Ok(()) + })?; + + Ok(()) + } +} + +impl From for ModuleInfo { + fn from(val: AsyncHooksModule) -> Self { + ModuleInfo { + name: "async_hooks", + module: val, + } + } +} + +pub fn init(ctx: &Ctx<'_>) -> Result<()> { + let global = ctx.globals(); + + let _ = ctx.store_userdata(RefCell::new(AsyncHookState::default())); + let _ = ctx.store_userdata(RefCell::new(AsyncHookIds::default())); + + global.set( + "invokeAsyncHook", + Func::from( + move |ctx: Ctx<'_>, type_: String, async_type: String, uid: usize| { + let type_ = match type_.as_ref() { + "init" => PromiseHookType::Init, + "before" => PromiseHookType::Before, + "after" => PromiseHookType::After, + "resolve" => PromiseHookType::Resolve, + _ => return, + }; + + let _ = invoke_async_hook(&ctx, type_, async_type.as_ref(), uid, None); + }, + ), + )?; + + init_finalization_registry(ctx)?; + + Ok(()) +} + +pub fn promise_hook_tracker() -> PromiseHook { + Box::new( + |ctx: Ctx<'_>, type_: PromiseHookType, promise: Value<'_>, parent: Value<'_>| { + // SAFETY: Since it checks in advance whether it is an Object type, we can always get a pointer to the object. + let object = promise + .as_object() + .map(|v| unsafe { qjs::JS_VALUE_GET_PTR(v.as_raw()) } as usize) + .unwrap(); + let parent = parent + .as_object() + .map(|v| unsafe { qjs::JS_VALUE_GET_PTR(v.as_raw()) } as usize); + + if type_ == PromiseHookType::Init { + let _ = register_finalization_registry(&ctx, promise, object); + } + + let _ = invoke_async_hook(&ctx, type_, "PROMISE", object, parent); + }, + ) +} + +fn invoke_async_hook( + ctx: &Ctx<'_>, + type_: PromiseHookType, + async_type: &str, + object: usize, + parent: Option, +) -> Result<()> { + let bind_state = ctx.userdata::>().or_throw(ctx)?; + let state = bind_state.borrow(); + + if state.hooks.is_empty() { + return Ok(()); + } + + match type_ { + PromiseHookType::Init => { + let current_id = insert_id_map(ctx, object, parent, async_type == "PROMISE")?; + trace!("Init(async_id, trigger_id): {:?}", current_id); + update_current_id(ctx, current_id)?; + + for hook in &state.hooks { + if *hook.enabled.as_ref().borrow() { + if let Some(func) = &hook.init { + let _ = func + .call::<_, ()>((current_id.0, async_type, current_id.1)) + .or_else(|_| func.call::<_, ()>((current_id.0, async_type))) + .or_else(|_| func.call::<_, ()>((current_id.0,))) + .or_else(|_| func.call::<_, ()>(())); + } + } + } + } + PromiseHookType::Before | PromiseHookType::After | PromiseHookType::Resolve => { + let current_id = get_id_map(ctx, object)?; + if current_id.0 == 0 { + return Ok(()); + } + + let _type = match type_ { + PromiseHookType::Before => "Before", + PromiseHookType::After => "After", + PromiseHookType::Resolve => "Resolve", + _ => unreachable!(), + }; + trace!("{}(async_id, trigger_id): {:?}", _type, current_id); + update_current_id(ctx, current_id)?; + + for hook in &state.hooks { + if *hook.enabled.as_ref().borrow() { + if let Some(func) = match type_ { + PromiseHookType::Before => &hook.before, + PromiseHookType::After => &hook.after, + PromiseHookType::Resolve => &hook.promise_resolve, + _ => unreachable!(), + } { + let _ = func + .call::<_, ()>((current_id.0,)) + .or_else(|_| func.call::<_, ()>(())); + } + } + } + } + } + Ok(()) +} + +fn insert_id_map( + ctx: &Ctx<'_>, + target: usize, + parent: Option, + is_promise: bool, +) -> Result<(u64, u64)> { + let bind_ids = ctx.userdata::>().or_throw(ctx)?; + let mut ids = bind_ids.borrow_mut(); + ids.next_async_id = ids.next_async_id.wrapping_add(1); + let async_id = ids.next_async_id; + let trigger_id = parent + .and_then(|tid| ids.id_map.get(&tid)) + .map(|id| id.0) + .unwrap_or(if is_promise { 1 } else { ids.current_id.1 }); + ids.id_map.insert(target, (async_id, trigger_id)); + Ok((async_id, trigger_id)) +} + +fn get_id_map(ctx: &Ctx<'_>, target: usize) -> Result<(u64, u64)> { + let bind_ids = ctx.userdata::>().or_throw(ctx)?; + let ids = bind_ids.borrow(); + Ok(*ids.id_map.get(&target).unwrap_or(&(0, 0))) +} + +fn remove_id_map(ctx: &Ctx<'_>, target: usize) -> Result<(u64, u64)> { + let bind_ids = ctx.userdata::>().or_throw(ctx)?; + let mut ids = bind_ids.borrow_mut(); + Ok(ids + .id_map + .remove_entry(&target) + .map(|(_, (async_id, trigger_id))| (async_id, trigger_id)) + .unwrap_or((0, 0))) +} + +fn update_current_id(ctx: &Ctx<'_>, id: (u64, u64)) -> Result<()> { + let bind_ids = ctx.userdata::>().or_throw(ctx)?; + bind_ids.borrow_mut().current_id = id; + Ok(()) +} diff --git a/stdlib/src/llrt/llrt_buffer/array_buffer_view.rs b/stdlib/src/llrt/llrt_buffer/array_buffer_view.rs new file mode 100644 index 00000000..2d0baa91 --- /dev/null +++ b/stdlib/src/llrt/llrt_buffer/array_buffer_view.rs @@ -0,0 +1,159 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#![allow(clippy::uninlined_format_args)] + +use std::ptr::NonNull; + +use rquickjs::{ArrayBuffer, Ctx, Error, FromJs, IntoJs, Object, Result, TypedArray, Value}; + +use crate::llrt_buffer::Buffer; + +pub struct ArrayBufferView<'js> { + value: Value<'js>, + buffer: Option, +} + +struct RawArrayBuffer { + len: usize, + ptr: NonNull, +} + +impl RawArrayBuffer { + pub fn new(len: usize, ptr: NonNull) -> Self { + Self { len, ptr } + } +} + +impl<'js> IntoJs<'js> for ArrayBufferView<'js> { + fn into_js(self, _ctx: &Ctx<'js>) -> Result> { + Ok(self.value) + } +} + +impl<'js> FromJs<'js> for ArrayBufferView<'js> { + fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let ty_name = value.type_name(); + let obj = Object::from_value(value.clone()) + .map_err(|_| Error::new_from_js(ty_name, "ArrayBufferView"))?; + + if let Some(array_buffer) = ArrayBuffer::from_object(obj.clone()) { + let buffer = array_buffer + .as_raw() + .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); + return Ok(ArrayBufferView { value, buffer }); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + let buffer = typed_array + .as_raw() + .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); + return Ok(ArrayBufferView { value, buffer }); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + let buffer = typed_array + .as_raw() + .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); + return Ok(ArrayBufferView { value, buffer }); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + let buffer = typed_array + .as_raw() + .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); + return Ok(ArrayBufferView { value, buffer }); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + let buffer = typed_array + .as_raw() + .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); + return Ok(ArrayBufferView { value, buffer }); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + let buffer = typed_array + .as_raw() + .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); + return Ok(ArrayBufferView { value, buffer }); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + let buffer = typed_array + .as_raw() + .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); + return Ok(ArrayBufferView { value, buffer }); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + let buffer = typed_array + .as_raw() + .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); + return Ok(ArrayBufferView { value, buffer }); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + let buffer = typed_array + .as_raw() + .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); + return Ok(ArrayBufferView { value, buffer }); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + let buffer = typed_array + .as_raw() + .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); + return Ok(ArrayBufferView { value, buffer }); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + let buffer = typed_array + .as_raw() + .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); + return Ok(ArrayBufferView { value, buffer }); + } + + if let Ok(array_buffer) = obj.get::<_, ArrayBuffer>("buffer") { + let buffer = array_buffer + .as_raw() + .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); + return Ok(ArrayBufferView { value, buffer }); + } + + Err(Error::new_from_js(ty_name, "ArrayBufferView")) + } +} + +impl<'js> ArrayBufferView<'js> { + pub fn from_buffer(ctx: &Ctx<'js>, buffer: Buffer) -> Result { + let value = buffer.into_js(ctx)?; + Self::from_js(ctx, value) + } + + pub fn len(&self) -> usize { + self.buffer.as_ref().map(|b| b.len).unwrap_or(0) + } + + #[allow(dead_code)] + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + pub fn as_bytes(&self) -> Option<&[u8]> { + self.buffer + .as_ref() + .map(|b| unsafe { std::slice::from_raw_parts(b.ptr.as_ptr(), b.len) }) + } + + /// Mutable buffer for the view. + /// + /// # Safety + /// This is only safe if you have a lock on the runtime. + /// Do not pass it directly to other threads. + pub fn as_bytes_mut(&mut self) -> Option<&mut [u8]> { + self.buffer + .as_ref() + .map(|b| unsafe { std::slice::from_raw_parts_mut(b.ptr.as_ptr(), b.len) }) + } +} diff --git a/stdlib/src/llrt/llrt_buffer/blob.rs b/stdlib/src/llrt/llrt_buffer/blob.rs new file mode 100644 index 00000000..c77165d3 --- /dev/null +++ b/stdlib/src/llrt/llrt_buffer/blob.rs @@ -0,0 +1,436 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::ops::RangeInclusive; + +use crate::llrt_stream_web::{ + readable_byte_stream_controller_close_stream, + readable_byte_stream_controller_enqueue_bytes_borrowed, utils::promise::PromisePrimordials, + CancelAlgorithm, PullAlgorithm, ReadableStream, ReadableStreamControllerClass, +}; +use crate::llrt_utils::{ + array_buffer::shared_array_buffer_view, + bytes::{get_lossy_string, ObjectBytes}, + object::not_a_object_error, + primordials::Primordial, + result::ResultExt, + string::get_coerced_defined_string, +}; +use rquickjs::{ + atom::PredefinedAtom, class::Trace, function::Opt, prelude::This, Array, ArrayBuffer, Class, + Coerced, Ctx, Exception, FromJs, IntoJs, JsIterator, Result, TypedArray, Value, +}; + +use super::file::File; + +struct ArrayPartsIter<'js> { + array: Array<'js>, + index: usize, +} + +impl<'js> ArrayPartsIter<'js> { + fn new(array: Array<'js>) -> Self { + Self { array, index: 0 } + } +} + +impl<'js> Iterator for ArrayPartsIter<'js> { + type Item = Result>; + + fn next(&mut self) -> Option { + let len: usize = match self.array.as_object().get(PredefinedAtom::Length) { + Ok(v) => v, + Err(e) => return Some(Err(e)), + }; + if self.index >= len { + return None; + } + let result = self.array.get(self.index); + self.index += 1; + Some(result) + } +} + +enum EndingType { + Native, + Transparent, +} + +#[cfg(windows)] +const LINE_ENDING: &[u8] = b"\r\n"; +#[cfg(not(windows))] +const LINE_ENDING: &[u8] = b"\n"; + +#[rquickjs::class] +#[derive(Trace, Clone, rquickjs::JsLifetime)] +pub struct Blob<'js> { + /// Bytes live in a JS-owned `ArrayBuffer` so `.arrayBuffer()` / `.bytes()` + /// / `.stream()` can hand out refcount-bumped views without copying. + data: ArrayBuffer<'js>, + mime_type: String, +} + +fn normalize_type(mut mime_type: String) -> String { + static INVALID_RANGE: RangeInclusive = 0x0020..=0x007E; + + let bytes = unsafe { mime_type.as_bytes_mut() }; + for byte in bytes { + if !INVALID_RANGE.contains(byte) { + return String::new(); + } + byte.make_ascii_lowercase(); + } + mime_type +} + +#[rquickjs::methods] +impl<'js> Blob<'js> { + #[qjs(constructor)] + pub fn new( + ctx: Ctx<'js>, + this: This>, + parts: Opt>, + options: Opt>, + ) -> Result { + if this.as_function().is_none() { + return Err(Exception::throw_type( + &ctx, + "Failed to construct 'Blob': Please use the 'new' operator", + )); + } + + Self::from_parts(ctx, parts, options) + } + + #[qjs(get)] + pub fn size(&self) -> usize { + self.data.len() + } + + #[qjs(get, rename = "type")] + pub fn mime_type(&self) -> String { + self.mime_type.clone() + } + + pub async fn text(&self) -> String { + String::from_utf8_lossy(self.as_bytes()).to_string() + } + + #[qjs(rename = "arrayBuffer")] + pub async fn array_buffer(&self, ctx: Ctx<'js>) -> Result> { + //should be mutable according to spec, thus copy is required + ArrayBuffer::new_copy(ctx, self.as_bytes()) + } + + pub async fn bytes(&self, ctx: Ctx<'js>) -> Result> { + //should be mutable according to spec, thus copy is required + let ab = ArrayBuffer::new_copy(ctx, self.as_bytes())?; + TypedArray::::from_arraybuffer(ab).map(|t| t.into_value()) + } + + pub fn slice( + &self, + ctx: Ctx<'js>, + start: Opt>, + end: Opt>, + content_type: Opt>, + ) -> Result> { + let start = start.0.and_then(|v| v.as_number()).map(clamp_long_long); + let end = end.0.and_then(|v| v.as_number()).map(clamp_long_long); + Self::slice_blob(self, &ctx, start, end, content_type.0) + } + + pub fn stream(&self, ctx: Ctx<'js>) -> Result> { + let data = self.data.clone(); + let pull = PullAlgorithm::from_fn_once( + move |ctx: Ctx<'js>, controller: ReadableStreamControllerClass<'js>| { + let ctrl = match controller { + ReadableStreamControllerClass::ReadableStreamByteController(c) => c, + _ => return Err(Exception::throw_type(&ctx, "Expected byte controller")), + }; + let len = data.len(); + if len != 0 { + let view = shared_array_buffer_view(&ctx, &data, 0, len)?; + readable_byte_stream_controller_enqueue_bytes_borrowed( + ctx.clone(), + ctrl.clone(), + view, + )?; + } + readable_byte_stream_controller_close_stream(ctx.clone(), ctrl)?; + Ok(PromisePrimordials::get(&ctx)? + .promise_resolved_with_undefined + .clone()) + }, + ); + // Byte-source stream so callers can use `getReader({ mode: 'byob' })`. + // Matches spec: Blob.stream() returns a `type: "bytes"` ReadableStream. + let stream = ReadableStream::from_byte_pull_algorithm( + ctx, + pull, + CancelAlgorithm::ReturnPromiseUndefined, + )?; + Ok(stream.into_value()) + } + + #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] + pub fn to_string_tag() -> &'static str { + stringify!(Blob) + } + + #[qjs(static, rename = PredefinedAtom::SymbolHasInstance)] + pub fn has_instance(value: Value<'js>) -> bool { + if let Some(obj) = value.as_object() { + return obj.instance_of::() || obj.instance_of::(); + } + false + } + + #[qjs(skip)] + pub fn slice_blob( + &self, + ctx: &Ctx<'js>, + start: Option, + end: Option, + content_type: Option>, + ) -> Result> { + let bytes = self.as_bytes(); + let len = bytes.len(); + let start = start.unwrap_or_default(); + let start = if start < 0 { + (len as isize + start).max(0) as usize + } else { + len.min(start as usize) + }; + let end = end.unwrap_or(len as isize); + let end = if end < 0 { + (len as isize + end).max(0) as usize + } else { + len.min(end as usize) + }; + let data = shared_array_buffer_view(ctx, &self.data, start, end.saturating_sub(start))?; + let mime_type = get_coerced_defined_string(&content_type); + let mime_type = mime_type.map(normalize_type).unwrap_or_default(); + Ok(Blob { mime_type, data }) + } +} + +impl<'js> Blob<'js> { + pub fn from_bytes(ctx: &Ctx<'js>, data: Vec, content_type: Option) -> Result { + let mime_type = content_type.map(normalize_type).unwrap_or_default(); + let data = ArrayBuffer::new(ctx.clone(), data)?; + Ok(Self { mime_type, data }) + } + + pub fn from_parts( + ctx: Ctx<'js>, + parts: Opt>, + options: Opt>, + ) -> Result { + if let Some(options) = options.0.as_ref() { + if !options.is_null() && !options.is_undefined() && options.as_object().is_none() { + return Err(not_a_object_error(&ctx, "options")); + } + } + + let mut endings = EndingType::Transparent; + if let Some(options) = options.0.as_ref() { + if let Some(opts) = options.as_object() { + if opts.contains_key("endings")? { + if let Some(parsed) = parse_endings(&ctx, opts.get("endings")?)? { + endings = parsed; + } + } + } + } + + let bytes = if let Some(parts) = parts.0 { + bytes_from_parts(&ctx, parts, endings)? + } else { + Vec::new() + }; + + let mut mime_type = String::new(); + if let Some(options) = options.0.as_ref() { + if let Some(opts) = options.as_object() { + if let Some(x) = opts.get::<_, Option>>("type")? { + mime_type = normalize_type(x.to_string()); + } + } + } + + // Transfer Vec ownership to JS — QuickJS calls the drop callback when + // the ArrayBuffer is GC'd, so no extra Rust-side copy. + let data = ArrayBuffer::new(ctx, bytes)?; + + Ok(Self { data, mime_type }) + } + + pub fn get_bytes(&self) -> Vec { + self.as_bytes().to_vec() + } + + /// Zero-copy access to the underlying `ArrayBuffer`. Cloning the handle is + /// cheap (it's a JS-refcount bump); no bytes are copied. Useful for + /// consumers that want to pass the Blob body on to hyper via + /// `ObjectBytes::DataView` without the `get_bytes()` allocation. + pub fn array_buffer_ref(&self) -> ArrayBuffer<'js> { + self.data.clone() + } + + /// Borrow the underlying bytes directly. Returns `&[]` if the ArrayBuffer + /// has been detached (shouldn't happen in normal blob flow). + pub fn as_bytes(&self) -> &[u8] { + self.data.as_bytes().unwrap_or(&[]) + } +} + +fn bytes_from_parts<'js>( + ctx: &Ctx<'js>, + parts: Value<'js>, + endings: EndingType, +) -> Result> { + if parts.is_undefined() { + return Ok(Vec::new()); + } + + if let Some(array) = parts.clone().into_array() { + return process_parts(ctx, ArrayPartsIter::new(array), endings); + } + + process_parts(ctx, JsIterator::from_js(ctx, parts)?, endings) +} + +fn process_parts<'js, I>(ctx: &Ctx<'js>, iter: I, endings: EndingType) -> Result> +where + I: IntoIterator>>, +{ + let mut data = Vec::new(); + for elem in iter { + let elem = elem?; + if let Some(arr) = elem.as_array() { + let string = array_to_string(arr)?; + data.extend_from_slice(string.as_bytes()); + continue; + } + if let Some(object) = elem.as_object() { + if let Some(x) = Class::::from_object(object) { + data.extend_from_slice(x.borrow().as_bytes()); + continue; + } + if let Some(x) = Class::::from_object(object) { + let file = x.borrow(); + let end = Some(file.size().try_into().or_throw(ctx)?); + let mime_type = Some(file.mime_type().into_js(ctx)?); + let sub = file.slice(ctx.clone(), Opt(Some(0)), Opt(end), Opt(mime_type))?; + data.extend_from_slice(sub.as_bytes()); + continue; + } + if let Ok(x) = ObjectBytes::from(ctx, object) { + data.extend_from_slice(x.as_bytes(ctx).map_err(|_| { + Exception::throw_type(ctx, "Cannot create a blob with detached buffer") + })?); + continue; + } + if let Some(x) = ArrayBuffer::from_object(object.clone()) { + data.extend_from_slice(x.as_bytes().ok_or_else(|| { + Exception::throw_type(ctx, "Cannot create a blob with detached buffer") + })?); + continue; + } + } + + let string = if elem.is_string() { + get_lossy_string(elem)? + } else { + Coerced::::from_js(ctx, elem)?.0 + }; + if let EndingType::Transparent = endings { + data.extend_from_slice(string.as_bytes()); + } else { + let len = string.len(); + data.reserve(len); + + let bytes = string.as_bytes(); + let mut iter = bytes.iter(); + + let mut start = 0usize; + let mut i = 0usize; + let line_ending_is_n = LINE_ENDING[0] == b'\n'; + + while let Some(byte) = iter.next() { + if byte == &b'\r' { + if let Some(next_byte) = iter.next() { + data.extend(&bytes[start..i]); + i += 1; + start = i + 1; + if next_byte != &b'\n' { + data.extend([b'\r', *next_byte]); + } else { + data.extend(LINE_ENDING); + } + } + } else if byte == &b'\n' && !line_ending_is_n { + data.extend(&bytes[start..i]); + data.extend(LINE_ENDING); + start = i + 1; + }; + i += 1; + } + + if start < len { + data.extend(&bytes[start..len]); + } + } + } + Ok(data) +} + +fn parse_endings<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> Result> { + if value.is_undefined() { + return Ok(None); + } + let endings = match Coerced::::from_js(ctx, value)?.0.as_str() { + "transparent" => Some(EndingType::Transparent), + "native" => Some(EndingType::Native), + _ => { + return Err(Exception::throw_type( + ctx, + r#"expected 'endings' to be either 'transparent' or 'native'"#, + )); + } + }; + Ok(endings) +} + +fn array_to_string(array: &Array) -> Result { + let mut itoa_buffer = itoa::Buffer::new(); + let mut ryu_buffer = ryu::Buffer::new(); + + let parts = array + .clone() + .into_iter() + .map(|value| { + let value = value?; + if let Some(string) = value.as_string() { + Ok(string.to_string()?) + } else if let Some(number) = value.as_int() { + Ok(itoa_buffer.format(number).to_string()) + } else if let Some(number) = value.as_float() { + Ok(ryu_buffer.format(number).to_string()) + } else { + Ok(String::new()) + } + }) + .collect::>>()?; + + Ok(parts.join(",")) +} + +fn clamp_long_long(value: f64) -> isize { + if value.is_nan() { + return 0; + } + let rounded = value.round_ties_even(); + rounded.clamp(isize::MIN as f64, isize::MAX as f64) as isize +} diff --git a/stdlib/src/llrt/llrt_buffer/buffer.rs b/stdlib/src/llrt/llrt_buffer/buffer.rs new file mode 100644 index 00000000..48371c0d --- /dev/null +++ b/stdlib/src/llrt/llrt_buffer/buffer.rs @@ -0,0 +1,1039 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::{mem::MaybeUninit, slice}; + +use crate::llrt_encoding::Encoder; +use crate::llrt_utils::{ + bytes::{get_array_bytes, get_start_end_indexes, ObjectBytes}, + error_messages::{ERROR_MSG_ARRAY_BUFFER_DETACHED, ERROR_MSG_NOT_ARRAY_BUFFER}, + iterable_enum, + primordials::Primordial, + result::ResultExt, + string::{get_coerced_string, get_string}, +}; +use rquickjs::{ + atom::PredefinedAtom, + function::{Constructor, Opt}, + prelude::{Func, Rest, This}, + Array, ArrayBuffer, Ctx, Exception, Function, IntoJs, JsLifetime, Object, Result, TypedArray, + Value, +}; + +#[derive(JsLifetime)] +pub struct BufferPrimordials<'js> { + constructor: Constructor<'js>, +} + +impl<'js> Primordial<'js> for BufferPrimordials<'js> { + fn new(ctx: &Ctx<'js>) -> Result + where + Self: Sized, + { + let constructor: Constructor = ctx.globals().get(stringify!(Buffer))?; + + Ok(Self { constructor }) + } +} + +pub struct Buffer(pub Vec); + +fn resolve_view_bytes<'js>( + ctx: &Ctx<'js>, + array_buffer: ArrayBuffer<'js>, + byte_length: usize, + byte_offset: usize, +) -> Result<&'js mut [u8]> { + let raw = array_buffer + .as_raw() + .ok_or(ERROR_MSG_ARRAY_BUFFER_DETACHED) + .or_throw(ctx)?; + + if byte_offset > raw.len || byte_length > raw.len - byte_offset { + return Err(Exception::throw_range( + ctx, + "The value of \"byteOffset\" is out of range", + )); + } + + // SAFETY: bounds checked above. + Ok(unsafe { slice::from_raw_parts_mut(raw.ptr.as_ptr().add(byte_offset), byte_length) }) +} + +impl<'js> IntoJs<'js> for Buffer { + fn into_js(self, ctx: &Ctx<'js>) -> Result> { + let array_buffer = ArrayBuffer::new(ctx.clone(), self.0)?; + Self::from_array_buffer(ctx, array_buffer) + } +} + +impl<'js> Buffer { + pub fn alloc(length: usize) -> Self { + Self(vec![0; length]) + } + + pub fn to_string(&self, ctx: &Ctx<'js>, encoding: &str) -> Result { + Encoder::from_str(encoding) + .and_then(|enc| enc.encode_to_string(self.0.as_ref(), true)) + .or_throw(ctx) + } + + fn from_array_buffer(ctx: &Ctx<'js>, buffer: ArrayBuffer<'js>) -> Result> { + BufferPrimordials::get(ctx)? + .constructor + .construct((buffer,)) + } + + fn from_array_buffer_offset_length( + ctx: &Ctx<'js>, + array_buffer: ArrayBuffer<'js>, + offset: usize, + length: usize, + ) -> Result> { + BufferPrimordials::get(ctx)? + .constructor + .construct((array_buffer, offset, length)) + } + + fn from_encoding( + ctx: &Ctx<'js>, + mut bytes: Vec, + encoding: Option, + ) -> Result> { + if let Some(encoding) = encoding { + let encoder = Encoder::from_str(&encoding).or_throw(ctx)?; + bytes = encoder.decode(bytes).or_throw(ctx)?; + } + Buffer(bytes).into_js(ctx) + } + + fn from_string_encoding( + ctx: &Ctx<'js>, + string: String, + encoding: Option, + ) -> Result> { + let bytes = if let Some(encoding) = encoding { + let encoder = Encoder::from_str(&encoding).or_throw(ctx)?; + encoder.decode_from_string(string).or_throw(ctx)? + } else { + string.into_bytes() + }; + Buffer(bytes).into_js(ctx) + } +} + +// Static Methods +fn alloc<'js>( + ctx: Ctx<'js>, + length: usize, + fill: Opt>, + encoding: Opt, +) -> Result> { + if let Some(value) = fill.0 { + if let Some(value) = value.as_string() { + let string = value.to_string()?; + + if let Some(encoding) = encoding.0 { + let encoder = Encoder::from_str(&encoding).or_throw(&ctx)?; + let bytes = encoder.decode_from_string(string).or_throw(&ctx)?; + return alloc_byte_ref(&ctx, &bytes, length); + } + + let byte_ref = string.as_bytes(); + + return alloc_byte_ref(&ctx, byte_ref, length); + } + if let Some(value) = value.as_int() { + let bytes = vec![value as u8; length]; + return Buffer(bytes).into_js(&ctx); + } + if let Some(obj) = value.as_object() { + if let Some(ob) = ObjectBytes::from_array_buffer(obj)? { + let bytes = ob.as_bytes(&ctx)?; + return alloc_byte_ref(&ctx, bytes, length); + } + } + } + + Buffer(vec![0; length]).into_js(&ctx) +} + +fn alloc_byte_ref<'js>(ctx: &Ctx<'js>, byte_ref: &[u8], length: usize) -> Result> { + let mut bytes = vec![0; length]; + let byte_ref_length = byte_ref.len(); + for i in 0..length { + bytes[i] = byte_ref[i % byte_ref_length]; + } + Buffer(bytes).into_js(ctx) +} + +fn alloc_unsafe(ctx: Ctx<'_>, size: usize) -> Result> { + let mut bytes: Vec> = Vec::with_capacity(size); + unsafe { + bytes.set_len(size); + } + + Buffer(maybeuninit_to_u8(bytes)).into_js(&ctx) +} + +fn maybeuninit_to_u8(vec: Vec>) -> Vec { + let len = vec.len(); + let capacity = vec.capacity(); + let ptr = vec.as_ptr() as *mut u8; + + std::mem::forget(vec); + + // This conversion is safe because MaybeUninit has the same memory layout as u8, meaning the underlying bytes are identical. + // Since Vec and Vec share the same memory representation, a simple reinterpretation of the pointer is valid. + // Additionally, Vec::from_raw_parts correctly reconstructs the vector using the original length and capacity, ensuring that memory ownership remains consistent. + // The call to std::mem::forget(vec) prevents the original Vec from being dropped, avoiding double frees or memory corruption. + // However, this conversion is only safe if all elements of MaybeUninit are properly initialized. + // If any uninitialized values exist, reading them as u8 would lead to undefined behavior. + unsafe { Vec::from_raw_parts(ptr, len, capacity) } +} + +fn alloc_unsafe_slow(ctx: Ctx<'_>, size: usize) -> Result> { + let layout = std::alloc::Layout::array::(size).or_throw(&ctx)?; + + let bytes = unsafe { + let ptr = std::alloc::alloc(layout); + if ptr.is_null() { + return Err(Exception::throw_internal(&ctx, "Memory allocation failed")); + } + Vec::from_raw_parts(ptr, size, size) + }; + Buffer(bytes).into_js(&ctx) +} + +fn byte_length<'js>(ctx: Ctx<'js>, value: Value<'js>, encoding: Opt) -> Result { + //slow path + if let Some(encoding) = encoding.0 { + let encoder = Encoder::from_str(&encoding).or_throw(&ctx)?; + let a = ObjectBytes::from(&ctx, &value)?; + let bytes = a.as_bytes(&ctx)?; + return Ok(encoder.decode(bytes).or_throw(&ctx)?.len()); + } + //fast path + if let Some(val) = value.as_string() { + return Ok(val.to_string()?.len()); + } + + if value.is_array() { + let array = value.as_array().unwrap(); + + for val in array.iter::() { + val.or_throw_msg(&ctx, "array value is not u8")?; + } + + return Ok(array.len()); + } + + if let Some(obj) = value.as_object() { + if let Some(ob) = ObjectBytes::from_array_buffer(obj)? { + return Ok(ob.as_bytes(&ctx)?.len()); + } + } + + Err(Exception::throw_message( + &ctx, + "value must be typed DataView, Buffer, ArrayBuffer, Uint8Array or string", + )) +} + +fn concat<'js>(ctx: Ctx<'js>, list: Array<'js>, max_length: Opt) -> Result> { + let mut bytes = Vec::new(); + let mut total_length = 0; + let mut length; + for value in list.iter::() { + let typed_array = TypedArray::::from_object(value?)?; + let bytes_ref: &[u8] = typed_array.as_ref(); + + length = bytes_ref.len(); + + if length == 0 { + continue; + } + + if let Some(max_length) = max_length.0 { + total_length += length; + if total_length > max_length { + let diff = max_length - (total_length - length); + bytes.extend_from_slice(&bytes_ref[0..diff]); + break; + } + } + bytes.extend_from_slice(bytes_ref); + } + + Buffer(bytes).into_js(&ctx) +} + +fn from<'js>( + ctx: Ctx<'js>, + value: Value<'js>, + offset_or_encoding: Opt>, + length: Opt, +) -> Result> { + let mut encoding: Option = None; + let mut offset = 0; + + if let Some(offset_or_encoding) = offset_or_encoding.0 { + if offset_or_encoding.is_string() { + encoding = Some(offset_or_encoding.get()?); + } else if offset_or_encoding.is_number() { + offset = offset_or_encoding.get()?; + } + } + + // WARN: This is currently bugged for strings that can't be converted to utf8 + // See https://github.com/quickjs-ng/quickjs/issues/992 + if let Some(string) = get_string(&value)? { + return Buffer::from_string_encoding(&ctx, string, encoding)?.into_js(&ctx); + } + if let Some(bytes) = get_array_bytes(&value, offset, length.0)? { + return Buffer::from_encoding(&ctx, bytes, encoding)?.into_js(&ctx); + } + + if let Some(obj) = value.as_object() { + if let Some(ab_bytes) = ObjectBytes::from_array_buffer(obj)? { + let bytes = ab_bytes.as_bytes(&ctx)?; + let (start, end) = get_start_end_indexes(bytes.len(), length.0, offset); + + //buffers from buffer should be copied + if obj + .get::<_, Option>(PredefinedAtom::Meta)? + .as_deref() + == Some(stringify!(Buffer)) + || encoding.is_some() + { + let bytes = bytes.into(); + return Buffer::from_encoding(&ctx, bytes, encoding)?.into_js(&ctx); + } else { + let (array_buffer, _, source_offset) = ab_bytes.get_array_buffer()?.unwrap(); //we know it's an array buffer + return Buffer::from_array_buffer_offset_length( + &ctx, + array_buffer, + start + source_offset, + end - start, + ); + } + } + } + + if let Some(string) = get_coerced_string(&value) { + return Buffer::from_string_encoding(&ctx, string, encoding)?.into_js(&ctx); + } + + Err(Exception::throw_message( + &ctx, + "value must be typed DataView, Buffer, ArrayBuffer, Uint8Array or interpretable as string", + )) +} + +fn is_buffer<'js>(ctx: Ctx<'js>, value: Value<'js>) -> Result { + if let Some(object) = value.as_object() { + let constructor = BufferPrimordials::get(&ctx)?; + return Ok(object.is_instance_of(&constructor.constructor)); + } + + Ok(false) +} + +fn is_encoding(value: Value) -> Result { + if let Some(js_string) = value.as_string() { + let std_string = js_string.to_string()?; + return Ok(Encoder::from_str(std_string.as_str()).is_ok()); + } + + Ok(false) +} + +// Prototype Methods +fn copy<'js>( + this: This>, + ctx: Ctx<'js>, + target: ObjectBytes<'js>, + args: Rest, +) -> Result { + let mut args_iter = args.0.into_iter(); + let target_start = args_iter.next().unwrap_or_default(); + let source_start = args_iter.next().unwrap_or_default(); + let source_end = args_iter.next().unwrap_or_else(|| this.0.len()); + + let source_bytes = ObjectBytes::from(&ctx, this.0.as_inner())?; + let source_bytes = source_bytes.as_bytes(&ctx)?; + + if source_start > source_bytes.len() { + return Err(Exception::throw_range( + &ctx, + "The value of \"sourceStart\" is out of range", + )); + } + + // sourceEnd is clamped (not an error), unlike sourceStart above. + let source_end = source_end.min(source_bytes.len()); + + let mut copyable_length = 0; + + if source_start >= source_end { + return Ok(copyable_length); + } + + if let Some((array_buffer, target_byte_length, target_byte_offset)) = + target.get_array_buffer()? + { + let target_bytes = + resolve_view_bytes(&ctx, array_buffer, target_byte_length, target_byte_offset)?; + + if target_start <= target_bytes.len() { + copyable_length = (source_end - source_start).min(target_bytes.len() - target_start); + + target_bytes[target_start..target_start + copyable_length] + .copy_from_slice(&source_bytes[source_start..source_start + copyable_length]); + } + } + + Ok(copyable_length) +} + +fn subarray<'js>( + this: This>, + ctx: Ctx<'js>, + start: Opt, + end: Opt, +) -> Result> { + let view = TypedArray::::from_object(this.0.clone())?; + + let array_buffer = view.arraybuffer()?; + let view_offset = this.0.get::<_, isize>("byteOffset")?; + let view_length = this.0.get::<_, isize>("byteLength")?; + + let start_index = start.map_or(0, |s| { + if s < 0 { + (view_length + s).max(0) + } else { + s.min(view_length) + } + }); + + let end_index = end.map_or(view_length, |e| { + if e < 0 { + (view_length + e).max(0) + } else { + e.min(view_length) + } + }); + + let length = (end_index - start_index).max(0) as usize; + let new_offset = (view_offset + start_index).max(0) as usize; + + Buffer::from_array_buffer_offset_length(&ctx, array_buffer, new_offset, length) +} + +fn to_string( + this: This>, + ctx: Ctx, + encoding: Opt, + start: Opt, + end: Opt, +) -> Result { + let typed_array = TypedArray::::from_object(this.0)?; + let bytes: &[u8] = typed_array.as_ref(); + + let start = start + .0 + .map(|s| s.max(0) as usize) + .unwrap_or(0) + .min(bytes.len()); + let end = end + .0 + .map(|e| e.max(0) as usize) + .unwrap_or(bytes.len()) + .min(bytes.len()); + let bytes = &bytes[start..end]; + + let encoder = Encoder::from_optional_str(encoding.as_deref()).or_throw(&ctx)?; + encoder.encode_to_string(bytes, true).or_throw(&ctx) +} + +fn write<'js>( + this: This>, + ctx: Ctx<'js>, + string: String, + args: Rest>, +) -> Result { + let (offset, length, encoding) = get_write_parameters(&ctx, &args, this.0.len())?; + + let target = ObjectBytes::from(&ctx, this.0.as_inner())?; + + let mut writable_length = 0; + + if let Some((array_buffer, target_byte_length, target_byte_offset)) = + target.get_array_buffer()? + { + let target_bytes = + resolve_view_bytes(&ctx, array_buffer, target_byte_length, target_byte_offset)?; + + let encoder = Encoder::from_str(&encoding).or_throw(&ctx)?; + + if encoder.as_label() == "utf-8" { + let (source_slice, valid_length) = safe_byte_slice(&string, length.min(string.len())); + writable_length = valid_length; + target_bytes[offset..offset + writable_length].copy_from_slice(source_slice); + } else { + let decode_bytes = encoder.decode_from_string(string).or_throw(&ctx)?; + writable_length = length.min(decode_bytes.len()); + target_bytes[offset..offset + writable_length] + .copy_from_slice(&decode_bytes[..writable_length]); + }; + } + + Ok(writable_length) +} + +fn get_write_parameters<'js>( + ctx: &Ctx<'js>, + args: &Rest>, + len: usize, +) -> Result<(usize, usize, String)> { + let mut offset = 0; + let mut length = len; + let mut encoding = "utf8".to_owned(); + + if let Some(v1) = args.0.first() { + if let Some(s) = v1.as_string() { + return Ok((0, len, s.to_string()?)); + } + offset = v1.as_int().unwrap_or(0) as usize; + if offset > len { + return Err(Exception::throw_range( + ctx, + "The value of \"offset\" is out of range", + )); + } + length = len - offset; + } + + if let Some(v2) = args.0.get(1) { + if let Some(s) = v2.as_string() { + return Ok((offset, len - offset, s.to_string()?)); + } + length = v2 + .as_int() + .map_or(len - offset, |l| (l as usize).min(len - offset)); + } + + if let Some(v3) = args.0.get(2) { + if let Some(s) = v3.as_string() { + encoding = s.to_string()?; + } + } + + Ok((offset, length, encoding)) +} + +fn safe_byte_slice(s: &str, end: usize) -> (&[u8], usize) { + let bytes = s.as_bytes(); + + if bytes.len() <= end { + return (bytes, bytes.len()); + } + + let valid_end = s + .char_indices() + .map(|(i, _)| i) + .rfind(|&i| i <= end) + .unwrap_or(0); + + (&bytes[0..valid_end], valid_end) +} + +#[derive(Clone, Copy)] +pub enum Endian { + Little, + Big, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum NumberKind { + Int8, + UInt8, + Int16, + UInt16, + Int32, + UInt32, + Float32, + Float64, + BigInt, + BigUInt, +} + +impl NumberKind { + pub fn bits(&self) -> u8 { + match self { + NumberKind::Int8 => 8, + NumberKind::UInt8 => 8, + NumberKind::Int16 => 16, + NumberKind::UInt16 => 16, + NumberKind::Int32 => 32, + NumberKind::UInt32 => 32, + NumberKind::Float32 => 32, + NumberKind::Float64 => 64, + NumberKind::BigInt => 64, + NumberKind::BigUInt => 64, + } + } + + pub fn is_signed(&self) -> bool { + matches!( + self, + NumberKind::Int8 | NumberKind::Int16 | NumberKind::Int32 + ) + } + + pub fn prototype(&self) -> &'static [(Endian, &'static str, Option<&'static str>)] { + match self { + NumberKind::Int8 => &[(Endian::Little, "Int8", None)], + NumberKind::UInt8 => &[(Endian::Little, "UInt8", Some("Uint8"))], + NumberKind::Int16 => &[ + (Endian::Little, "Int16LE", None), + (Endian::Big, "Int16BE", None), + ], + NumberKind::UInt16 => &[ + (Endian::Little, "UInt16LE", Some("Uint16LE")), + (Endian::Big, "UInt16BE", Some("Uint16BE")), + ], + NumberKind::Int32 => &[ + (Endian::Little, "Int32LE", None), + (Endian::Big, "Int32BE", None), + ], + NumberKind::UInt32 => &[ + (Endian::Little, "UInt32LE", Some("Uint32LE")), + (Endian::Big, "UInt32BE", Some("Uint32BE")), + ], + NumberKind::Float32 => &[ + (Endian::Little, "FloatLE", None), + (Endian::Big, "FloatBE", None), + ], + NumberKind::Float64 => &[ + (Endian::Little, "DoubleLE", None), + (Endian::Big, "DoubleBE", None), + ], + NumberKind::BigInt => &[ + (Endian::Little, "BigInt64LE", None), + (Endian::Big, "BigInt64BE", None), + ], + NumberKind::BigUInt => &[ + (Endian::Little, "BigUInt64LE", Some("BigUint64LE")), + (Endian::Big, "BigUInt64BE", Some("BigUint64BE")), + ], + } + } +} + +iterable_enum!( + NumberKind, Int8, UInt8, Int16, UInt16, Int32, UInt32, Float32, Float64, BigInt, BigUInt +); + +#[allow(clippy::too_many_arguments)] +fn write_buf<'js>( + this: &This>, + ctx: &Ctx<'js>, + value: &Value<'js>, + offset: &Opt, + endian: Endian, + kind: NumberKind, +) -> Result { + let offset = offset.0.unwrap_or_default(); + + // Extract and convert value + let (byte_count, bytes) = match kind { + NumberKind::BigInt => { + let Some(bigint) = value.as_big_int() else { + return Err(Exception::throw_type(ctx, "Expected BigInt")); + }; + let (byte_count, val) = (8, bigint.clone().to_i64().or_throw(ctx)? as u64); + (byte_count, endian_bytes(val, endian)) + } + NumberKind::BigUInt => { + return Err(Exception::throw_type(ctx, "Uint64 is not supported")); + } + NumberKind::Float32 => { + let Some(float_val) = value.as_float() else { + return Err(Exception::throw_type(ctx, "Expected number")); + }; + match endian { + Endian::Big => (4, (float_val as f32).to_bits().to_be_bytes().to_vec()), + Endian::Little => (4, (float_val as f32).to_bits().to_le_bytes().to_vec()), + } + } + NumberKind::Float64 => { + let Some(float_val) = value.as_float() else { + return Err(Exception::throw_type(ctx, "Expected number")); + }; + match endian { + Endian::Big => (8, float_val.to_bits().to_be_bytes().to_vec()), + Endian::Little => (8, float_val.to_bits().to_le_bytes().to_vec()), + } + } + NumberKind::Int8 + | NumberKind::UInt8 + | NumberKind::Int16 + | NumberKind::UInt16 + | NumberKind::Int32 + | NumberKind::UInt32 => { + let Some(int_val) = value.as_number() else { + return Err(Exception::throw_type(ctx, "Expected number")); + }; + let int_val = int_val as i64; + let bit_mask = (1i64 << kind.bits()) - 1; + let max_val = if kind.is_signed() { + (1i64 << (kind.bits() - 1)) - 1 + } else { + bit_mask + }; + let min_val = if kind.is_signed() { -max_val - 1 } else { 0 }; + + if int_val < min_val || int_val > max_val { + return Err(Exception::throw_range(ctx, "Value out of range")); + } + + let masked = int_val & bit_mask; + ( + (kind.bits() / 8) as usize, + shifted_bytes(masked as u64, kind.bits(), endian), + ) + } + }; + + if offset >= this.0.len() || offset + byte_count > this.0.len() { + return Err(Exception::throw_range( + ctx, + "The specified offset is out of range", + )); + } + + let target = ObjectBytes::from(ctx, this.0.as_inner())?; + let mut writable_length = 0; + + if let Some((array_buffer, target_byte_length, target_byte_offset)) = + target.get_array_buffer()? + { + let target_bytes = + resolve_view_bytes(ctx, array_buffer, target_byte_length, target_byte_offset)?; + + writable_length = offset + bytes.len(); + target_bytes[offset..writable_length].copy_from_slice(&bytes); + } + + Ok(writable_length) +} + +fn read_buf<'js>( + this: &This>, + ctx: &Ctx<'js>, + offset: &Opt, + endian: Endian, + kind: NumberKind, +) -> Result> { + // Retrieve the array buffer + let target = ObjectBytes::from(ctx, this.0.as_inner())?; + let Some((array_buffer, target_byte_length, target_byte_offset)) = target.get_array_buffer()? + else { + return Err(Exception::throw_message(ctx, ERROR_MSG_NOT_ARRAY_BUFFER)); + }; + let target_bytes = + resolve_view_bytes(ctx, array_buffer, target_byte_length, target_byte_offset)?; + + // Enforce the bounds + let start = offset.0.unwrap_or_default(); + let end = start + (kind.bits() / 8) as usize; + if end > target_bytes.len() { + return Err(Exception::throw_range( + ctx, + "The value of \"offset\" is out of range", + )); + } + + let bytes = &target_bytes[start..end]; + + let value = match kind { + NumberKind::BigInt => { + let value = match endian { + Endian::Big => i64::from_be_bytes(bytes.try_into().unwrap()), + Endian::Little => i64::from_le_bytes(bytes.try_into().unwrap()), + }; + Value::new_big_int(ctx.clone(), value)? + } + NumberKind::BigUInt => { + return Err(Exception::throw_type(ctx, "Uint64 is not supported")); + } + NumberKind::Float32 => { + let value = match endian { + Endian::Big => f32::from_be_bytes(bytes.try_into().unwrap()), + Endian::Little => f32::from_le_bytes(bytes.try_into().unwrap()), + }; + Value::new_float(ctx.clone(), value as f64) + } + NumberKind::Float64 => { + let value = match endian { + Endian::Big => f64::from_be_bytes(bytes.try_into().unwrap()), + Endian::Little => f64::from_le_bytes(bytes.try_into().unwrap()), + }; + Value::new_float(ctx.clone(), value) + } + NumberKind::Int8 => { + let value = match endian { + Endian::Big => i8::from_be_bytes(bytes.try_into().unwrap()), + Endian::Little => i8::from_le_bytes(bytes.try_into().unwrap()), + }; + Value::new_int(ctx.clone(), value as i32) + } + NumberKind::UInt8 => { + let value = match endian { + Endian::Big => u8::from_be_bytes(bytes.try_into().unwrap()), + Endian::Little => u8::from_le_bytes(bytes.try_into().unwrap()), + }; + Value::new_int(ctx.clone(), value as i32) + } + NumberKind::Int16 => { + let value = match endian { + Endian::Big => i16::from_be_bytes(bytes.try_into().unwrap()), + Endian::Little => i16::from_le_bytes(bytes.try_into().unwrap()), + }; + Value::new_int(ctx.clone(), value as i32) + } + NumberKind::UInt16 => { + let value = match endian { + Endian::Big => u16::from_be_bytes(bytes.try_into().unwrap()), + Endian::Little => u16::from_le_bytes(bytes.try_into().unwrap()), + }; + Value::new_int(ctx.clone(), value as i32) + } + NumberKind::Int32 => { + let value = match endian { + Endian::Big => i32::from_be_bytes(bytes.try_into().unwrap()), + Endian::Little => i32::from_le_bytes(bytes.try_into().unwrap()), + }; + Value::new_int(ctx.clone(), value) + } + NumberKind::UInt32 => { + let value = match endian { + Endian::Big => u32::from_be_bytes(bytes.try_into().unwrap()), + Endian::Little => u32::from_le_bytes(bytes.try_into().unwrap()), + }; + Value::new_float(ctx.clone(), value as f64) + } + }; + Ok(value) +} + +// Pure mathematical byte generation +fn endian_bytes(mut val: u64, endian: Endian) -> Vec { + let mut bytes = vec![0u8; 8]; + + #[allow(clippy::needless_range_loop)] + for i in 0..8 { + bytes[i] = match endian { + Endian::Big => (val >> (56 - i * 8)) as u8, + Endian::Little => (val >> (i * 8)) as u8, + }; + // Clear processed bits + match endian { + Endian::Big => val &= !(0xFF << ((7 - i) * 8)), + Endian::Little => val &= !(0xFF << (i * 8)), + } + } + bytes +} + +fn shifted_bytes(mut val: u64, bits: u8, endian: Endian) -> Vec { + let byte_count = (bits / 8) as usize; + let mut bytes = vec![0u8; byte_count]; + + #[allow(clippy::needless_range_loop)] + for i in 0..byte_count { + let shift = match endian { + Endian::Big => (byte_count - 1 - i) * 8, + Endian::Little => i * 8, + }; + bytes[i] = (val >> shift) as u8; + val &= !(0xFF << shift); // Clear processed bits + } + bytes +} + +pub(crate) fn set_prototype<'js>(ctx: &Ctx<'js>, constructor: Object<'js>) -> Result<()> { + let _ = &constructor.set("alloc", Func::from(alloc))?; + let _ = &constructor.set("allocUnsafe", Func::from(alloc_unsafe))?; + let _ = &constructor.set("allocUnsafeSlow", Func::from(alloc_unsafe_slow))?; + let _ = &constructor.set("byteLength", Func::from(byte_length))?; + let _ = &constructor.set("concat", Func::from(concat))?; + let _ = &constructor.set(PredefinedAtom::From, Func::from(from))?; + let _ = &constructor.set("isBuffer", Func::from(is_buffer))?; + let _ = &constructor.set("isEncoding", Func::from(is_encoding))?; + + let prototype: &Object = &constructor.get(PredefinedAtom::Prototype)?; + prototype.set("copy", Func::from(copy))?; + prototype.set("subarray", Func::from(subarray))?; + prototype.set(PredefinedAtom::ToString, Func::from(to_string))?; + prototype.set("write", Func::from(write))?; + + // Set all write and read methods + for kind in NumberKind::iter() { + for (endian, name, alias) in kind.prototype() { + let write_func = Function::new(ctx.clone(), |t, c, v, o| { + write_buf(&t, &c, &v, &o, *endian, *kind) + })?; + let read_func = + Function::new(ctx.clone(), |t, c, o| read_buf(&t, &c, &o, *endian, *kind))?; + if let Some(alias) = alias { + prototype.set(["write", alias].concat(), write_func.clone())?; + prototype.set(["read", alias].concat(), read_func.clone())?; + } + prototype.set(["write", name].concat(), write_func)?; + prototype.set(["read", name].concat(), read_func)?; + } + } + + //not assessable from js + prototype.prop(PredefinedAtom::Meta, stringify!(Buffer))?; + + ctx.globals().set(stringify!(Buffer), constructor)?; + + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::llrt_test::{call_test, test_async_with, ModuleEvaluator}; + + use crate::llrt_buffer::BufferModule; + + #[tokio::test] + async fn test_subarray() { + test_async_with(|ctx| { + Box::pin(async move { + crate::llrt_buffer::init(&ctx).unwrap(); + ModuleEvaluator::eval_rust::(ctx.clone(), "buffer") + .await + .unwrap(); + + let data = "hello world".to_string().into_bytes(); + let module = ModuleEvaluator::eval_js( + ctx.clone(), + "test", + r#" + import { Buffer } from 'buffer'; + + export async function test(data) { + let buffer = Buffer.from(data); + let sub = buffer.subarray(6, 11); // "world" part + return sub.toString(); + } + "#, + ) + .await + .unwrap(); + let result = call_test::(&ctx, &module, (data,)).await; + assert_eq!(result, "world"); + }) + }) + .await; + } + + #[tokio::test] + async fn test_subarray_partial() { + test_async_with(|ctx| { + Box::pin(async move { + crate::llrt_buffer::init(&ctx).unwrap(); + ModuleEvaluator::eval_rust::(ctx.clone(), "buffer") + .await + .unwrap(); + + let data = "hello world".to_string().into_bytes(); + let module = ModuleEvaluator::eval_js( + ctx.clone(), + "test", + r#" + import { Buffer } from 'buffer'; + + export async function test(data) { + let buffer = Buffer.from(data); + let sub = buffer.subarray(0, 5); // "hello" part + return sub.toString(); + } + "#, + ) + .await + .unwrap(); + let result = call_test::(&ctx, &module, (data,)).await; + assert_eq!(result, "hello"); + }) + }) + .await; + } + + #[tokio::test] + async fn test_subarray_out_of_bounds() { + test_async_with(|ctx| { + Box::pin(async move { + crate::llrt_buffer::init(&ctx).unwrap(); + ModuleEvaluator::eval_rust::(ctx.clone(), "buffer") + .await + .unwrap(); + + let data = "hello world".to_string().into_bytes(); + let module = ModuleEvaluator::eval_js( + ctx.clone(), + "test", + r#" + import { Buffer } from 'buffer'; + + export async function test(data) { + let buffer = Buffer.from(data); + let sub = buffer.subarray(6, 20); // "world" part but goes out of bounds + return sub.toString(); + } + "#, + ) + .await + .unwrap(); + let result = call_test::(&ctx, &module, (data,)).await; + assert_eq!(result, "world"); + }) + }) + .await; + } + + #[tokio::test] + async fn test_read_int_32_be() { + test_async_with(|ctx| { + Box::pin(async move { + crate::llrt_buffer::init(&ctx).unwrap(); + ModuleEvaluator::eval_rust::(ctx.clone(), "buffer") + .await + .unwrap(); + + let data = "hello world".to_string().into_bytes(); + let module = ModuleEvaluator::eval_js( + ctx.clone(), + "test", + r#" + import { Buffer } from 'buffer'; + + export async function test(data) { + const buf = Buffer.from([1, 2, 3, 4, 0, 0, 0, 0]); + return buf.readInt32BE(); + } + "#, + ) + .await + .unwrap(); + let result = call_test::(&ctx, &module, (data,)).await; + assert_eq!(result, 0x01020304); + }) + }) + .await; + } +} diff --git a/stdlib/src/llrt/llrt_buffer/file.rs b/stdlib/src/llrt/llrt_buffer/file.rs new file mode 100644 index 00000000..75629940 --- /dev/null +++ b/stdlib/src/llrt/llrt_buffer/file.rs @@ -0,0 +1,129 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use crate::llrt_utils::time; +use rquickjs::{ + atom::PredefinedAtom, class::Trace, function::Opt, ArrayBuffer, Coerced, Ctx, Exception, + IntoJs, Object, Result, Value, +}; + +use super::blob::Blob; + +#[rquickjs::class] +#[derive(Trace, Clone, rquickjs::JsLifetime)] +pub struct File<'js> { + blob: Blob<'js>, + filename: String, + last_modified: i64, +} + +#[rquickjs::methods] +impl<'js> File<'js> { + #[qjs(constructor)] + fn new( + ctx: Ctx<'js>, + data: Value<'js>, + filename: Coerced, + options: Opt>, + ) -> Result { + let mut last_modified = time::now_millis(); + + if let Some(ref opts) = options.0 { + if opts.is_bool() || opts.is_float() || opts.is_int() || opts.is_string() { + return Err(Exception::throw_type(&ctx, "Invalid options")); + } + + if let Some(v) = opts.as_object() { + if let Some(x) = v.get::<_, Option>>("lastModified")? { + last_modified = x.0; + } + } + } + + let blob = Blob::from_parts(ctx, Opt(Some(data)), options)?; + + Ok(Self { + blob, + filename: filename.0, + last_modified, + }) + } + + #[qjs(get)] + pub fn size(&self) -> usize { + self.blob.size() + } + + #[qjs(get)] + pub fn name(&self) -> String { + self.filename.clone() + } + + #[qjs(get, rename = "type")] + pub fn mime_type(&self) -> String { + self.blob.mime_type() + } + + #[qjs(get, rename = "lastModified")] + pub fn last_modified(&self) -> i64 { + self.last_modified + } + + pub fn slice( + &self, + ctx: Ctx<'js>, + start: Opt, + end: Opt, + content_type: Opt>, + ) -> Result> { + self.blob.slice_blob(&ctx, start.0, end.0, content_type.0) + } + + pub async fn text(&mut self) -> String { + self.blob.text().await + } + + #[qjs(rename = "arrayBuffer")] + pub async fn array_buffer(&self, ctx: Ctx<'js>) -> Result> { + self.blob.array_buffer(ctx).await + } + + pub async fn bytes(&self, ctx: Ctx<'js>) -> Result> { + self.blob.bytes(ctx).await + } + + #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] + pub fn to_string_tag() -> &'static str { + stringify!(File) + } +} + +impl<'js> File<'js> { + pub fn from_bytes( + ctx: &Ctx<'js>, + data: Vec, + filename: String, + mime_type: Option, + ) -> Result { + let options = Opt(Some({ + let obj = Object::new(ctx.clone())?; + obj.set("type", mime_type.clone().unwrap_or("".into()).into_js(ctx)?)?; + obj.into_js(ctx)? + })); + let blob = Blob::from_parts(ctx.clone(), Opt(Some(data.into_js(ctx)?)), options)?; + + Ok(Self { + blob, + filename, + last_modified: time::now_millis(), + }) + } + + pub fn get_blob(&self) -> Blob<'js> { + self.blob.clone() + } + + pub fn set_filename(&mut self, filename: String) { + self.filename = filename; + } +} diff --git a/stdlib/src/llrt/llrt_buffer/lib.rs b/stdlib/src/llrt/llrt_buffer/lib.rs new file mode 100644 index 00000000..b491ba60 --- /dev/null +++ b/stdlib/src/llrt/llrt_buffer/lib.rs @@ -0,0 +1,102 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use crate::llrt_utils::{ + module::{export_default, ModuleInfo}, + object::define_subclass, + primordials::{BasePrimordials, Primordial}, +}; +use rquickjs::{ + function::{Args, Constructor, Rest}, + module::{Declarations, Exports, ModuleDef}, + Class, Ctx, Function, IntoJs, Object, Result, Value, +}; + +pub use self::array_buffer_view::*; +pub use self::blob::*; +pub use self::buffer::*; +pub use self::file::*; + +mod array_buffer_view; +mod blob; +mod buffer; +mod file; + +pub struct BufferModule; + +impl ModuleDef for BufferModule { + fn declare(declare: &Declarations) -> Result<()> { + declare.declare(stringify!(Buffer))?; + declare.declare("atob")?; + declare.declare("btoa")?; + declare.declare("constants")?; + declare.declare("default")?; + Ok(()) + } + + fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { + let globals = ctx.globals(); + let buf: Constructor = globals.get(stringify!(Buffer))?; + + let constants = Object::new(ctx.clone())?; + constants.set("MAX_LENGTH", u32::MAX)?; // For QuickJS + constants.set("MAX_STRING_LENGTH", (1 << 30) - 1)?; // For QuickJS + + let atob: Function = ctx.globals().get("atob")?; + let btoa: Function = ctx.globals().get("btoa")?; + + export_default(ctx, exports, |default| { + default.set(stringify!(Buffer), buf)?; + default.set("atob", atob.into_js(ctx)?)?; + default.set("btoa", btoa.into_js(ctx)?)?; + default.set("constants", constants)?; + Ok(()) + })?; + + Ok(()) + } +} + +impl From for ModuleInfo { + fn from(val: BufferModule) -> Self { + ModuleInfo { + name: "buffer", + module: val, + } + } +} + +pub fn init<'js>(ctx: &Ctx<'js>) -> Result<()> { + let globals = ctx.globals(); + BasePrimordials::init(ctx)?; + + // Buffer extends the native Uint8Array: it forwards construction to the + // Uint8Array constructor and inherits its static and prototype members. + let uint8array = BasePrimordials::get(ctx)?.constructor_uint8array.clone(); + let buffer_ctor = define_subclass( + ctx, + stringify!(Buffer), + &uint8array, + |ctx: Ctx<'js>, args: Rest>| { + let uint8array = &BasePrimordials::get(&ctx)?.constructor_uint8array; + let mut ctor_args = Args::new(ctx.clone(), args.0.len()); + ctor_args.push_args(args.0)?; + ctor_args.construct::(uint8array) + }, + )?; + let buffer: Object = buffer_ctor.into_value().into_object().unwrap(); + set_prototype(ctx, buffer)?; + + BufferPrimordials::init(ctx)?; + + // Blob + Class::::define(&globals)?; + + // File + Class::::define(&globals)?; + + //init primordials + let _ = BufferPrimordials::get(ctx)?; + + Ok(()) +} diff --git a/stdlib/src/llrt/llrt_compression/lib.rs b/stdlib/src/llrt/llrt_compression/lib.rs new file mode 100644 index 00000000..f8b3091d --- /dev/null +++ b/stdlib/src/llrt/llrt_compression/lib.rs @@ -0,0 +1,99 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +pub mod streaming; + +#[cfg(any(any(), all()))] +pub mod zstd { + use std::io::{BufReader, Read, Result}; + + use zstd::stream::read::{Decoder as ZstdDecoder, Encoder as ZstdEncoder}; + pub use zstd::DEFAULT_COMPRESSION_LEVEL; + + pub fn encoder(r: R, level: i32) -> Result>> { + ZstdEncoder::new(r, level) + } + + pub fn decoder(r: R) -> Result>> { + ZstdDecoder::new(r) + } +} + +#[cfg(any(any(), all()))] +pub mod deflate { + use std::io::Read; + + use flate2::read::{DeflateDecoder, DeflateEncoder}; + pub use flate2::Compression; + + pub fn encoder(r: R, level: Compression) -> DeflateEncoder { + DeflateEncoder::new(r, level) + } + + pub fn decoder(r: R) -> DeflateDecoder { + DeflateDecoder::new(r) + } +} + +#[cfg(any(any(), all()))] +pub mod gz { + use std::io::Read; + + use flate2::read::{GzDecoder, GzEncoder}; + pub use flate2::Compression; + + pub fn encoder(r: R, level: Compression) -> GzEncoder { + GzEncoder::new(r, level) + } + + pub fn decoder(r: R) -> GzDecoder { + GzDecoder::new(r) + } +} + +#[cfg(any(any(), all()))] +pub mod zlib { + use std::io::Read; + + use flate2::read::{ZlibDecoder, ZlibEncoder}; + pub use flate2::Compression; + + pub fn encoder(r: R, level: Compression) -> ZlibEncoder { + ZlibEncoder::new(r, level) + } + + pub fn decoder(r: R) -> ZlibDecoder { + ZlibDecoder::new(r) + } +} + +#[cfg(any())] +pub mod brotli { + use std::io::BufRead; + + use brotlic::{CompressorReader as BrotliEncoder, DecompressorReader as BrotliDecoder}; + + pub fn encoder(r: R) -> BrotliEncoder { + BrotliEncoder::new(r) + } + + pub fn decoder(r: R) -> BrotliDecoder { + BrotliDecoder::new(r) + } +} + +#[cfg(all(not(any()), all()))] +pub mod brotli { + use std::io::Read; + + use brotli::{CompressorReader as BrotliEncoder, Decompressor as BrotliDecoder}; + + pub fn encoder(r: R) -> BrotliEncoder { + BrotliEncoder::new(r, 8_096, 11, 22) + } + + pub fn decoder(r: R) -> BrotliDecoder { + BrotliDecoder::new(r, 8_096) + } +} diff --git a/stdlib/src/llrt/llrt_compression/streaming.rs b/stdlib/src/llrt/llrt_compression/streaming.rs new file mode 100644 index 00000000..e8cafe34 --- /dev/null +++ b/stdlib/src/llrt/llrt_compression/streaming.rs @@ -0,0 +1,98 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::io::{self, Write}; + +#[cfg(all(not(any()), all()))] +use brotli as brotlic; + +/// Streaming decompressor that maintains state across chunks +pub enum StreamingDecoder { + #[cfg(any(any(), all()))] + Gzip(flate2::write::GzDecoder>), + #[cfg(any(any(), all()))] + Deflate(flate2::write::ZlibDecoder>), + #[cfg(any(any(), all()))] + Zstd(zstd::stream::write::Decoder<'static, Vec>), + #[cfg(any(any(), all()))] + Brotli(brotlic::DecompressorWriter>), + Identity, +} + +impl StreamingDecoder { + pub fn new(encoding: &str) -> io::Result { + match encoding { + #[cfg(any(any(), all()))] + "gzip" => Ok(Self::Gzip(flate2::write::GzDecoder::new(Vec::new()))), + #[cfg(any(any(), all()))] + "deflate" => Ok(Self::Deflate(flate2::write::ZlibDecoder::new(Vec::new()))), + #[cfg(any(any(), all()))] + "zstd" => Ok(Self::Zstd(zstd::stream::write::Decoder::new(Vec::new())?)), + #[cfg(any())] + "br" => Ok(Self::Brotli(brotlic::DecompressorWriter::new(Vec::new()))), + #[cfg(all(not(any()), all()))] + "br" => Ok(Self::Brotli(brotlic::DecompressorWriter::new( + Vec::new(), + 8_096, + ))), + "" | "identity" => Ok(Self::Identity), + _ => Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("unsupported encoding: {}", encoding), + )), + } + } + + /// Decompress a chunk of data, returning the decompressed output + pub fn decompress_chunk(&mut self, input: &[u8]) -> io::Result> { + match self { + Self::Identity => Ok(input.to_vec()), + #[cfg(any(any(), all()))] + Self::Gzip(decoder) => { + decoder.write_all(input)?; + decoder.flush()?; + Ok(std::mem::take(decoder.get_mut())) + } + #[cfg(any(any(), all()))] + Self::Deflate(decoder) => { + decoder.write_all(input)?; + decoder.flush()?; + Ok(std::mem::take(decoder.get_mut())) + } + #[cfg(any(any(), all()))] + Self::Zstd(decoder) => { + decoder.write_all(input)?; + decoder.flush()?; + Ok(std::mem::take(decoder.get_mut())) + } + #[cfg(any(any(), all()))] + Self::Brotli(decoder) => { + decoder.write_all(input)?; + decoder.flush()?; + Ok(std::mem::take(decoder.get_mut())) + } + } + } + + /// Finish decompression and return any remaining data + pub fn finish(self) -> io::Result> { + match self { + Self::Identity => Ok(Vec::new()), + #[cfg(any(any(), all()))] + Self::Gzip(decoder) => decoder.finish(), + #[cfg(any(any(), all()))] + Self::Deflate(decoder) => decoder.finish(), + #[cfg(any(any(), all()))] + Self::Zstd(decoder) => Ok(decoder.into_inner()), + #[cfg(any())] + Self::Brotli(decoder) => decoder + .into_inner() + .map_err(|e| io::Error::other(e.to_string())), + #[cfg(all(not(any()), all()))] + Self::Brotli(decoder) => decoder + .into_inner() + .map_err(|_| io::Error::other("brotli decompression failed")), + } + } +} diff --git a/stdlib/src/llrt/llrt_context/lib.rs b/stdlib/src/llrt/llrt_context/lib.rs new file mode 100644 index 00000000..c35a4731 --- /dev/null +++ b/stdlib/src/llrt/llrt_context/lib.rs @@ -0,0 +1,93 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::future::Future; +use std::sync::OnceLock; + +use crate::llrt_utils::primordials::{BasePrimordials, Primordial}; +use rquickjs::{atom::PredefinedAtom, CatchResultExt, CaughtError, Ctx, Object, Result}; +use tokio::sync::oneshot::{self, Receiver}; +use tracing::trace; + +#[allow(clippy::type_complexity)] +static ERROR_HANDLER: OnceLock Fn(&Ctx<'js>, CaughtError<'js>) + Sync + Send>> = + OnceLock::new(); + +pub trait CtxExtension<'js> { + /// Despite naming, this will not necessarily exit the parent process. + /// It depends on the handler set by `set_spawn_error_handler`. + fn spawn_exit(&self, future: F) -> Result> + where + F: Future> + 'js, + R: 'js; + + fn spawn_exit_simple(&self, future: F) + where + F: Future> + 'js; +} + +impl<'js> CtxExtension<'js> for Ctx<'js> { + fn spawn_exit(&self, future: F) -> Result> + where + F: Future> + 'js, + R: 'js, + { + let ctx = self.clone(); + + let primordials = BasePrimordials::get(self)?; + let type_error: Object = primordials.constructor_type_error.construct(())?; + let stack: Option = type_error.get(PredefinedAtom::Stack).ok(); + + let (join_channel_tx, join_channel_rx) = oneshot::channel(); + + self.spawn(async move { + match future.await.catch(&ctx) { + Ok(res) => { + //result here doesn't matter if receiver has dropped + let _ = join_channel_tx.send(res); + } + Err(err) => handle_spawn_error(&ctx, err, stack), + } + }); + Ok(join_channel_rx) + } + + /// Same as above but fire & forget and without a forced stack trace collection + fn spawn_exit_simple(&self, future: F) + where + F: Future> + 'js, + { + let ctx = self.clone(); + self.spawn(async move { + if let Err(err) = future.await.catch(&ctx) { + handle_spawn_error(&ctx, err, None) + } + }); + } +} + +fn handle_spawn_error<'js>(ctx: &Ctx<'js>, err: CaughtError<'js>, stack: Option) { + let error_handler = if let Some(handler) = ERROR_HANDLER.get() { + handler + } else { + trace!("Future error: {:?}", err); + return; + }; + if let CaughtError::Exception(err) = err { + if err.stack().is_none() { + if let Some(stack) = stack { + err.set(PredefinedAtom::Stack, stack).unwrap(); + } + } + error_handler(ctx, CaughtError::Exception(err)); + } else { + error_handler(ctx, err); + } +} + +pub fn set_spawn_error_handler(handler: F) +where + F: for<'js> Fn(&Ctx<'js>, CaughtError<'js>) + Sync + Send + 'static, +{ + _ = ERROR_HANDLER.set(Box::new(handler)); +} diff --git a/stdlib/src/llrt/llrt_crypto/crc32.rs b/stdlib/src/llrt/llrt_crypto/crc32.rs new file mode 100644 index 00000000..a9fc8eb2 --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/crc32.rs @@ -0,0 +1,72 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::hash::Hasher; + +use crate::llrt_utils::bytes::ObjectBytes; +use crc32c::Crc32cHasher; +use rquickjs::{prelude::This, Class, Ctx, Result}; + +#[rquickjs::class] +#[derive(rquickjs::class::Trace, rquickjs::JsLifetime)] +pub struct Crc32c { + #[qjs(skip_trace)] + hasher: crc32c::Crc32cHasher, +} + +#[rquickjs::methods] +impl Crc32c { + #[qjs(constructor)] + fn new() -> Self { + Self { + hasher: Crc32cHasher::default(), + } + } + + #[qjs(rename = "digest")] + fn crc32c_digest(&self) -> u64 { + self.hasher.finish() + } + + #[qjs(rename = "update")] + fn crc32c_update<'js>( + this: This>, + ctx: Ctx<'js>, + bytes: ObjectBytes<'js>, + ) -> Result> { + this.0.borrow_mut().hasher.write(bytes.as_bytes(&ctx)?); + Ok(this.0) + } +} + +#[rquickjs::class] +#[derive(rquickjs::class::Trace, rquickjs::JsLifetime)] +pub struct Crc32 { + #[qjs(skip_trace)] + hasher: crc32fast::Hasher, +} + +#[rquickjs::methods] +impl Crc32 { + #[qjs(constructor)] + fn new() -> Self { + Self { + hasher: crc32fast::Hasher::new(), + } + } + + #[qjs(rename = "digest")] + fn crc32_digest(&self) -> u64 { + self.hasher.finish() + } + + #[qjs(rename = "update")] + fn crc32_update<'js>( + this: This>, + ctx: Ctx<'js>, + bytes: ObjectBytes<'js>, + ) -> Result> { + this.0.borrow_mut().hasher.write(bytes.as_bytes(&ctx)?); + Ok(this.0) + } +} diff --git a/stdlib/src/llrt/llrt_crypto/hash.rs b/stdlib/src/llrt/llrt_crypto/hash.rs new file mode 100644 index 00000000..c0ffbd71 --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/hash.rs @@ -0,0 +1,217 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use crate::llrt_buffer::Buffer; +use crate::llrt_utils::{bytes::ObjectBytes, iterable_enum, result::ResultExt}; +use rquickjs::{ + class::Trace, function::Opt, prelude::This, Class, Ctx, IntoJs, JsLifetime, Result, Value, +}; + +use super::encoded_bytes; +use crate::llrt_crypto::provider::{CryptoError, CryptoProvider, HmacProvider, SimpleDigest}; +use crate::llrt_crypto::CRYPTO_PROVIDER; + +#[derive(Debug, Clone, Copy)] +pub enum HashAlgorithm { + Md5, + Sha1, + Sha256, + Sha384, + Sha512, +} + +iterable_enum!(HashAlgorithm, Md5, Sha1, Sha256, Sha384, Sha512); + +impl TryFrom<&str> for HashAlgorithm { + type Error = String; + fn try_from(s: &str) -> std::result::Result { + Ok(match s.to_ascii_uppercase().as_str() { + "MD5" => HashAlgorithm::Md5, + "MD-5" => HashAlgorithm::Md5, + "SHA1" => HashAlgorithm::Sha1, + "SHA-1" => HashAlgorithm::Sha1, + "SHA256" => HashAlgorithm::Sha256, + "SHA-256" => HashAlgorithm::Sha256, + "SHA384" => HashAlgorithm::Sha384, + "SHA-384" => HashAlgorithm::Sha384, + "SHA512" => HashAlgorithm::Sha512, + "SHA-512" => HashAlgorithm::Sha512, + _ => return Err(["'", s, "' not available"].concat()), + }) + } +} + +impl HashAlgorithm { + pub fn class_name(&self) -> &'static str { + match self { + HashAlgorithm::Md5 => "Md5", + HashAlgorithm::Sha1 => "Sha1", + HashAlgorithm::Sha256 => "Sha256", + HashAlgorithm::Sha384 => "Sha384", + HashAlgorithm::Sha512 => "Sha512", + } + } + + pub fn as_str(&self) -> &'static str { + match self { + HashAlgorithm::Md5 => "MD5", + HashAlgorithm::Sha1 => "SHA-1", + HashAlgorithm::Sha256 => "SHA-256", + HashAlgorithm::Sha384 => "SHA-384", + HashAlgorithm::Sha512 => "SHA-512", + } + } + + pub fn as_numeric_str(&self) -> &'static str { + match self { + HashAlgorithm::Md5 => "md5", + HashAlgorithm::Sha1 => "1", + HashAlgorithm::Sha256 => "256", + HashAlgorithm::Sha384 => "384", + HashAlgorithm::Sha512 => "512", + } + } + + pub fn digest_len(&self) -> usize { + match self { + HashAlgorithm::Md5 => 16, + HashAlgorithm::Sha1 => 20, + HashAlgorithm::Sha256 => 32, + HashAlgorithm::Sha384 => 48, + HashAlgorithm::Sha512 => 64, + } + } + + pub fn block_len(&self) -> usize { + match self { + HashAlgorithm::Md5 => 64, + HashAlgorithm::Sha1 => 64, + HashAlgorithm::Sha256 => 64, + HashAlgorithm::Sha384 => 128, + HashAlgorithm::Sha512 => 128, + } + } + + pub(super) fn from_strict_str(s: &str) -> std::result::Result { + Ok(match s { + "SHA-1" => HashAlgorithm::Sha1, + "SHA-256" => HashAlgorithm::Sha256, + "SHA-384" => HashAlgorithm::Sha384, + "SHA-512" => HashAlgorithm::Sha512, + _ => return Err(CryptoError::UnsupportedAlgorithm), + }) + } +} + +type ProviderDigest = ::Digest; +type ProviderHmac = ::Hmac; + +#[derive(Trace, JsLifetime)] +#[rquickjs::class] +pub struct Hash { + #[qjs(skip_trace)] + digest: Option, + #[qjs(skip_trace)] + hmac: Option, +} + +impl Hash { + pub fn new(ctx: Ctx<'_>, algorithm: String) -> Result { + let algorithm = HashAlgorithm::try_from(algorithm.as_str()).or_throw(&ctx)?; + Ok(Self { + digest: Some(CRYPTO_PROVIDER.digest(algorithm)), + hmac: None, + }) + } + + pub fn new_hmac<'js>( + ctx: Ctx<'js>, + algorithm: String, + secret: ObjectBytes<'js>, + ) -> Result { + let algorithm = HashAlgorithm::try_from(algorithm.as_str()).or_throw(&ctx)?; + let key = secret.as_bytes(&ctx)?; + Ok(Self { + digest: None, + hmac: Some(CRYPTO_PROVIDER.hmac(algorithm, key)), + }) + } + + fn do_update(&mut self, data: &[u8]) { + if let Some(ref mut d) = self.digest { + d.update(data); + } else if let Some(ref mut h) = self.hmac { + h.update(data); + } + } + + fn do_finalize(&mut self) -> Option> { + if let Some(d) = self.digest.take() { + Some(d.finalize()) + } else { + self.hmac.take().map(|h| h.finalize()) + } + } +} + +#[rquickjs::methods] +impl Hash { + #[qjs(rename = "digest")] + fn hash_digest<'js>(&mut self, ctx: Ctx<'js>, encoding: Opt) -> Result> { + let result = self + .do_finalize() + .ok_or_else(|| rquickjs::Exception::throw_message(&ctx, "Digest already called"))?; + + let Some(encoding) = encoding.0 else { + return Buffer(result).into_js(&ctx); + }; + + match encoded_bytes(&ctx, &result, &encoding)? { + Some(encoded) => Ok(encoded), + None => Buffer(result).into_js(&ctx), + } + } + + #[qjs(rename = "update")] + fn hash_update<'js>( + this: This>, + ctx: Ctx<'js>, + bytes: ObjectBytes<'js>, + ) -> Result> { + let bytes = bytes.as_bytes(&ctx)?; + this.0.borrow_mut().do_update(bytes); + Ok(this.0) + } +} + +#[derive(Trace, JsLifetime)] +#[rquickjs::class] +pub struct Hmac { + #[qjs(skip_trace)] + hash: Hash, +} + +impl Hmac { + pub fn new<'js>(ctx: Ctx<'js>, algorithm: String, key_value: ObjectBytes<'js>) -> Result { + Ok(Self { + hash: Hash::new_hmac(ctx, algorithm, key_value)?, + }) + } +} + +#[rquickjs::methods] +impl Hmac { + fn digest<'js>(&mut self, ctx: Ctx<'js>, encoding: Opt) -> Result> { + self.hash.hash_digest(ctx, encoding) + } + + fn update<'js>( + this: This>, + ctx: Ctx<'js>, + bytes: ObjectBytes<'js>, + ) -> Result> { + let bytes = bytes.as_bytes(&ctx)?; + this.0.borrow_mut().hash.do_update(bytes); + Ok(this.0) + } +} diff --git a/stdlib/src/llrt/llrt_crypto/lib.rs b/stdlib/src/llrt/llrt_crypto/lib.rs new file mode 100644 index 00000000..faab2edc --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/lib.rs @@ -0,0 +1,385 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Compile-time checks for conflicting crypto features +#[cfg(all(all(), any()))] +compile_error!("Features `crypto-rust` and `crypto-openssl` are mutually exclusive"); + +#[cfg(all(all(), any()))] +compile_error!("Features `crypto-rust` and `crypto-ring` are mutually exclusive"); + +#[cfg(all(all(), any()))] +compile_error!("Features `crypto-rust` and `crypto-graviola` are mutually exclusive"); + +#[cfg(all(any(), any()))] +compile_error!("Features `crypto-openssl` and `crypto-ring` are mutually exclusive"); + +#[cfg(all(any(), any()))] +compile_error!("Features `crypto-openssl` and `crypto-graviola` are mutually exclusive"); + +#[cfg(all(any(), any()))] +compile_error!("Features `crypto-ring` and `crypto-graviola` are mutually exclusive"); + +mod crc32; +mod hash; +mod subtle; + +mod provider; + +use std::slice; + +use crate::llrt_buffer::Buffer; +use crate::llrt_context::CtxExtension; +use crate::llrt_encoding::{bytes_to_b64_string, bytes_to_hex_string}; +use crate::llrt_exceptions::DOMException; +use crate::llrt_utils::{ + bytes::{get_start_end_indexes, ObjectBytes}, + error::ErrorExtensions, + error_messages::{ERROR_MSG_ARRAY_BUFFER_DETACHED, ERROR_MSG_NOT_ARRAY_BUFFER}, + module::{export_default, ModuleInfo}, + result::ResultExt, +}; +use once_cell::sync::Lazy; +use rand::RngExt; +use rquickjs::prelude::Async; +use rquickjs::{ + atom::PredefinedAtom, + function::{Constructor, Opt}, + module::{Declarations, Exports, ModuleDef}, + prelude::{Func, Rest}, + Class, Ctx, Error, Exception, Function, IntoJs, Null, Object, Result, Value, +}; +pub use subtle::CryptoKey; +use subtle::{ + subtle_decrypt, subtle_derive_bits, subtle_derive_key, subtle_digest, subtle_encrypt, + subtle_export_key, subtle_generate_key, subtle_import_key, subtle_sign, subtle_unwrap_key, + subtle_verify, subtle_wrap_key, SubtleCrypto, +}; + +use self::{ + crc32::{Crc32, Crc32c}, + hash::{Hash, HashAlgorithm, Hmac}, +}; + +static CRYPTO_PROVIDER: Lazy = + Lazy::new(|| provider::DefaultProvider {}); + +fn encoded_bytes<'js>(ctx: &Ctx<'js>, bytes: &[u8], encoding: &str) -> Result>> { + match encoding { + "hex" => { + let hex = bytes_to_hex_string(bytes); + let hex = rquickjs::String::from_str(ctx.clone(), &hex)?; + Ok(Some(Value::from_string(hex))) + } + "base64" => { + let b64 = bytes_to_b64_string(bytes); + let b64 = rquickjs::String::from_str(ctx.clone(), &b64)?; + Ok(Some(Value::from_string(b64))) + } + _ => Ok(None), + } +} + +#[inline] +pub fn random_byte_array(length: usize) -> Vec { + let mut vec = vec![0u8; length]; + rand::rng().fill(&mut vec[..]); + vec +} + +fn get_random_bytes(ctx: Ctx, length: usize) -> Result { + let random_bytes = random_byte_array(length); + Buffer(random_bytes).into_js(&ctx) +} + +fn get_random_int(first: i64, second: Opt) -> Result { + let mut rng = rand::rng(); + let random_number = match second.0 { + Some(max) => rng.random_range(first..max), + None => rng.random_range(0..first), + }; + + Ok(random_number) +} + +fn random_fill<'js>(ctx: Ctx<'js>, obj: Object<'js>, args: Rest>) -> Result<()> { + let args_iter = args.0.into_iter(); + let mut args_iter = args_iter.rev(); + + let callback: Function = args_iter + .next() + .and_then(|v| v.into_function()) + .or_throw_msg(&ctx, "Callback required")?; + let size = args_iter + .next() + .and_then(|arg| arg.as_int()) + .map(|i| i as usize); + let offset = args_iter + .next() + .and_then(|arg| arg.as_int()) + .map(|i| i as usize); + + ctx.clone().spawn_exit(async move { + if let Err(err) = random_fill_sync(ctx.clone(), obj.clone(), Opt(offset), Opt(size)) { + let err = err.into_value(&ctx)?; + () = callback.call((err,))?; + + return Ok(()); + } + () = callback.call((Null.into_js(&ctx), obj))?; + Ok::<_, Error>(()) + })?; + Ok(()) +} + +fn random_fill_sync<'js>( + ctx: Ctx<'js>, + obj: Object<'js>, + offset: Opt, + size: Opt, +) -> Result> { + let offset = offset.unwrap_or(0); + + if let Some(object_bytes) = ObjectBytes::from_array_buffer(&obj)? { + let (array_buffer, source_length, source_offset) = object_bytes + .get_array_buffer()? + .expect(ERROR_MSG_NOT_ARRAY_BUFFER); + let raw = array_buffer + .as_raw() + .ok_or(ERROR_MSG_ARRAY_BUFFER_DETACHED) + .or_throw(&ctx)?; + + if offset > source_length { + return Err(Exception::throw_range( + &ctx, + "The value of \"offset\" is out of range", + )); + } + if let Some(size) = size.0 { + if offset + size > source_length { + return Err(Exception::throw_range( + &ctx, + "The value of \"size + offset\" is out of range", + )); + } + } + + let (start, end) = get_start_end_indexes(source_length, size.0, offset); + + // SAFETY: source_offset..+source_length stays in the backing buffer; + // start/end are clamped to it above. + let bytes = unsafe { + slice::from_raw_parts_mut(raw.ptr.as_ptr().add(source_offset), source_length) + }; + + rand::rng().fill(&mut bytes[start..end]); + } + + Ok(obj) +} + +fn get_random_values<'js>(ctx: Ctx<'js>, obj: Object<'js>) -> Result> { + if let Some(object_bytes) = ObjectBytes::from_array_buffer(&obj)? { + if matches!( + object_bytes, + ObjectBytes::F64Array(_) + | ObjectBytes::F32Array(_) + | ObjectBytes::F16Array(_) + | ObjectBytes::DataView(_, _, _) + ) { + return Err(DOMException::type_mismatch_error( + &ctx, + "getRandomValues requires an integer TypedArray", + )); + } + + let (array_buffer, source_length, source_offset) = object_bytes + .get_array_buffer()? + .expect(ERROR_MSG_NOT_ARRAY_BUFFER); + let raw = array_buffer + .as_raw() + .ok_or(ERROR_MSG_ARRAY_BUFFER_DETACHED) + .or_throw(&ctx)?; + + if source_length > 0x10000 { + return Err(DOMException::quota_exceeded_error( + &ctx, + "The requested length exceeds 65,536 bytes", + )); + } + + let bytes = unsafe { + std::slice::from_raw_parts_mut(raw.ptr.as_ptr().add(source_offset), source_length) + }; + + rand::rng().fill(bytes) + } + + Ok(obj) +} + +fn uuidv4() -> String { + let uuid = rand::random::() & 0xFFFFFFFFFFFF4FFFBFFFFFFFFFFFFFFF | 0x40008000000000000000; + + static HEX_CHARS: &[u8; 16] = b"0123456789abcdef"; + let bytes = uuid.to_be_bytes(); + + let mut buf = [0u8; 36]; + + // Precomputed positions for 32 hex digits (excluding hyphens) + static HEX_POS: [usize; 32] = [ + 0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 14, 15, 16, 17, 19, 20, 21, 22, 24, 25, 26, 27, 28, + 29, 30, 31, 32, 33, 34, 35, + ]; + + // Map each byte to its hex representation + let mut hex_idx = 0; + for &byte in &bytes[..] { + let high = HEX_CHARS[(byte >> 4) as usize]; + let low = HEX_CHARS[(byte & 0x0f) as usize]; + + buf[HEX_POS[hex_idx]] = high; + buf[HEX_POS[hex_idx + 1]] = low; + hex_idx += 2; + } + + // Insert hyphens at standard positions + buf[8] = b'-'; + buf[13] = b'-'; + buf[18] = b'-'; + buf[23] = b'-'; + + // SAFETY: The buffer only contains valid UTF-8 characters (hex digits and hyphens) + // that were explicitly set from the HEX_CHARS array and hyphen literals + unsafe { String::from_utf8_unchecked(buf.to_vec()) } +} + +#[rquickjs::class] +#[derive(rquickjs::JsLifetime, rquickjs::class::Trace)] +struct Crypto {} + +#[rquickjs::methods] +impl Crypto { + #[qjs(constructor)] + pub fn new(ctx: Ctx<'_>) -> Result { + Err(Exception::throw_type(&ctx, "Illegal constructor")) + } + + #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] + pub fn to_string_tag() -> &'static str { + stringify!(Crypto) + } +} + +pub fn init(ctx: &Ctx<'_>) -> Result<()> { + let globals = ctx.globals(); + + Class::::define(&globals)?; + let crypto = Class::instance(ctx.clone(), Crypto {})?; + + crypto.set("createHash", Func::from(Hash::new))?; + crypto.set("createHmac", Func::from(Hmac::new))?; + crypto.set("randomBytes", Func::from(get_random_bytes))?; + crypto.set("randomInt", Func::from(get_random_int))?; + crypto.set("randomUUID", Func::from(uuidv4))?; + crypto.set("randomFillSync", Func::from(random_fill_sync))?; + crypto.set("randomFill", Func::from(random_fill))?; + crypto.set("getRandomValues", Func::from(get_random_values))?; + + Class::::define(&globals)?; + Class::::define(&globals)?; + + let subtle = Class::instance(ctx.clone(), SubtleCrypto {})?; + subtle.set("decrypt", Func::from(Async(subtle_decrypt)))?; + subtle.set("deriveKey", Func::from(Async(subtle_derive_key)))?; + subtle.set("deriveBits", Func::from(Async(subtle_derive_bits)))?; + subtle.set("digest", Func::from(Async(subtle_digest)))?; + subtle.set("encrypt", Func::from(Async(subtle_encrypt)))?; + subtle.set("exportKey", Func::from(Async(subtle_export_key)))?; + subtle.set("generateKey", Func::from(Async(subtle_generate_key)))?; + subtle.set("importKey", Func::from(Async(subtle_import_key)))?; + subtle.set("sign", Func::from(Async(subtle_sign)))?; + subtle.set("verify", Func::from(Async(subtle_verify)))?; + subtle.set("wrapKey", Func::from(Async(subtle_wrap_key)))?; + subtle.set("unwrapKey", Func::from(Async(subtle_unwrap_key)))?; + crypto.set("subtle", subtle)?; + + globals.set("crypto", crypto)?; + + Ok(()) +} + +pub struct CryptoModule; + +impl ModuleDef for CryptoModule { + fn declare(declare: &Declarations) -> Result<()> { + declare.declare("createHash")?; + declare.declare("createHmac")?; + declare.declare("Crc32")?; + declare.declare("Crc32c")?; + declare.declare("randomBytes")?; + declare.declare("randomUUID")?; + declare.declare("randomInt")?; + declare.declare("randomFillSync")?; + declare.declare("randomFill")?; + declare.declare("getRandomValues")?; + + for algorithm in HashAlgorithm::iter() { + declare.declare(algorithm.class_name())?; + } + + declare.declare("crypto")?; + declare.declare("webcrypto")?; + declare.declare("default")?; + + Ok(()) + } + + fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { + export_default(ctx, exports, |default| { + for algorithm in HashAlgorithm::iter() { + let class_name: &str = algorithm.class_name(); + let algo_name = String::from(algorithm.as_str()); + + let ctor = Constructor::new_class::( + ctx.clone(), + move |ctx: Ctx<'js>, secret: Opt>| match secret.0 { + Some(secret) => Hash::new_hmac(ctx, algo_name.clone(), secret), + None => Hash::new(ctx, algo_name.clone()), + }, + )?; + + default.set(class_name, ctor)?; + } + + let crypto: Object = ctx.globals().get("crypto")?; + + Class::::define(default)?; + Class::::define(default)?; + + default.set("createHash", Func::from(Hash::new))?; + default.set("createHmac", Func::from(Hmac::new))?; + default.set("randomBytes", Func::from(get_random_bytes))?; + default.set("randomInt", Func::from(get_random_int))?; + default.set("randomUUID", Func::from(uuidv4))?; + default.set("randomFillSync", Func::from(random_fill_sync))?; + default.set("randomFill", Func::from(random_fill))?; + default.set("getRandomValues", Func::from(get_random_values))?; + default.set("crypto", crypto.clone())?; + default.set("webcrypto", crypto)?; + Ok(()) + })?; + + Ok(()) + } +} + +impl From for ModuleInfo { + fn from(val: CryptoModule) -> Self { + ModuleInfo { + name: "crypto", + module: val, + } + } +} diff --git a/stdlib/src/llrt/llrt_crypto/provider/graviola.rs b/stdlib/src/llrt/llrt_crypto/provider/graviola.rs new file mode 100644 index 00000000..c7b56047 --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/provider/graviola.rs @@ -0,0 +1,571 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Graviola crypto provider - a high-performance crypto library using formally verified assembler. +//! +//! Supported: SHA256/384/512, HMAC, AES-GCM +//! Not supported: Most other operations due to API limitations + +use graviola::{ + aead::AesGcm, + hashing::{hmac::Hmac, Hash, HashContext, Sha256, Sha384, Sha512}, +}; + +use crate::llrt_crypto::hash::HashAlgorithm; +use crate::llrt_crypto::provider::{ + AesMode, CryptoError, CryptoProvider, HmacProvider, SimpleDigest, +}; +use crate::llrt_crypto::subtle::EllipticCurve; + +pub struct GraviolaProvider; + +pub enum GraviolaDigest { + Sha256(::Context), + Sha384(::Context), + Sha512(::Context), +} + +impl SimpleDigest for GraviolaDigest { + fn update(&mut self, data: &[u8]) { + match self { + GraviolaDigest::Sha256(h) => h.update(data), + GraviolaDigest::Sha384(h) => h.update(data), + GraviolaDigest::Sha512(h) => h.update(data), + } + } + + fn finalize(self) -> Vec { + match self { + GraviolaDigest::Sha256(h) => h.finish().as_ref().to_vec(), + GraviolaDigest::Sha384(h) => h.finish().as_ref().to_vec(), + GraviolaDigest::Sha512(h) => h.finish().as_ref().to_vec(), + } + } +} + +pub enum GraviolaHmac { + Sha256(Hmac), + Sha384(Hmac), + Sha512(Hmac), +} + +impl HmacProvider for GraviolaHmac { + fn update(&mut self, data: &[u8]) { + match self { + GraviolaHmac::Sha256(h) => h.update(data), + GraviolaHmac::Sha384(h) => h.update(data), + GraviolaHmac::Sha512(h) => h.update(data), + } + } + + fn finalize(self) -> Vec { + match self { + GraviolaHmac::Sha256(h) => h.finish().as_ref().to_vec(), + GraviolaHmac::Sha384(h) => h.finish().as_ref().to_vec(), + GraviolaHmac::Sha512(h) => h.finish().as_ref().to_vec(), + } + } +} + +impl CryptoProvider for GraviolaProvider { + type Digest = GraviolaDigest; + type Hmac = GraviolaHmac; + + fn digest(&self, algorithm: HashAlgorithm) -> Self::Digest { + match algorithm { + HashAlgorithm::Sha256 => GraviolaDigest::Sha256(Sha256::new()), + HashAlgorithm::Sha384 => GraviolaDigest::Sha384(Sha384::new()), + HashAlgorithm::Sha512 => GraviolaDigest::Sha512(Sha512::new()), + _ => panic!("Unsupported digest algorithm for Graviola"), + } + } + + fn hmac(&self, algorithm: HashAlgorithm, key: &[u8]) -> Self::Hmac { + match algorithm { + HashAlgorithm::Sha256 => GraviolaHmac::Sha256(Hmac::::new(key)), + HashAlgorithm::Sha384 => GraviolaHmac::Sha384(Hmac::::new(key)), + HashAlgorithm::Sha512 => GraviolaHmac::Sha512(Hmac::::new(key)), + _ => panic!("Unsupported HMAC algorithm for Graviola"), + } + } + + fn ecdsa_sign( + &self, + _curve: EllipticCurve, + _private_key_der: &[u8], + _digest: &[u8], + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn ecdsa_verify( + &self, + _curve: EllipticCurve, + _public_key_sec1: &[u8], + _signature: &[u8], + _digest: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn ed25519_sign(&self, _private_key_der: &[u8], _data: &[u8]) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn ed25519_verify( + &self, + _public_key_bytes: &[u8], + _signature: &[u8], + _data: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn rsa_pss_sign( + &self, + _private_key_der: &[u8], + _digest: &[u8], + _salt_length: usize, + _hash_alg: HashAlgorithm, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn rsa_pss_verify( + &self, + _public_key_der: &[u8], + _signature: &[u8], + _digest: &[u8], + _salt_length: usize, + _hash_alg: HashAlgorithm, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn rsa_pkcs1v15_sign( + &self, + _private_key_der: &[u8], + _digest: &[u8], + _hash_alg: HashAlgorithm, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn rsa_pkcs1v15_verify( + &self, + _public_key_der: &[u8], + _signature: &[u8], + _digest: &[u8], + _hash_alg: HashAlgorithm, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn rsa_oaep_encrypt( + &self, + _public_key_der: &[u8], + _data: &[u8], + _hash_alg: HashAlgorithm, + _label: Option<&[u8]>, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn rsa_oaep_decrypt( + &self, + _private_key_der: &[u8], + _data: &[u8], + _hash_alg: HashAlgorithm, + _label: Option<&[u8]>, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn ecdh_derive_bits( + &self, + _curve: EllipticCurve, + _private_key_der: &[u8], + _public_key_sec1: &[u8], + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn x25519_derive_bits( + &self, + _private_key: &[u8], + _public_key: &[u8], + ) -> Result, CryptoError> { + // Graviola doesn't expose from_bytes for X25519 PrivateKey + Err(CryptoError::UnsupportedAlgorithm) + } + + fn aes_encrypt( + &self, + mode: AesMode, + key: &[u8], + iv: &[u8], + data: &[u8], + additional_data: Option<&[u8]>, + ) -> Result, CryptoError> { + match mode { + AesMode::Gcm { .. } => { + let nonce: [u8; 12] = iv.try_into().map_err(|_| CryptoError::InvalidData(None))?; + if !matches!(key.len(), 16 | 32) { + return Err(CryptoError::InvalidKey(None)); + } + let aead = AesGcm::new(key); + let aad = additional_data.unwrap_or(&[]); + let mut ciphertext = data.to_vec(); + let mut tag = [0u8; 16]; + aead.encrypt(&nonce, aad, &mut ciphertext, &mut tag); + ciphertext.extend_from_slice(&tag); + Ok(ciphertext) + } + _ => Err(CryptoError::UnsupportedAlgorithm), + } + } + + fn aes_decrypt( + &self, + mode: AesMode, + key: &[u8], + iv: &[u8], + data: &[u8], + additional_data: Option<&[u8]>, + ) -> Result, CryptoError> { + match mode { + AesMode::Gcm { .. } => { + let nonce: [u8; 12] = iv.try_into().map_err(|_| CryptoError::InvalidData(None))?; + if !matches!(key.len(), 16 | 32) { + return Err(CryptoError::InvalidKey(None)); + } + if data.len() < 16 { + return Err(CryptoError::InvalidData(None)); + } + let aead = AesGcm::new(key); + let aad = additional_data.unwrap_or(&[]); + let (ciphertext, tag) = data.split_at(data.len() - 16); + let tag: [u8; 16] = tag.try_into().unwrap(); + let mut plaintext = ciphertext.to_vec(); + aead.decrypt(&nonce, aad, &mut plaintext, &tag) + .map_err(|_| CryptoError::DecryptionFailed(None))?; + Ok(plaintext) + } + _ => Err(CryptoError::UnsupportedAlgorithm), + } + } + + fn aes_kw_wrap(&self, _kek: &[u8], _key: &[u8]) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn aes_kw_unwrap(&self, _kek: &[u8], _wrapped_key: &[u8]) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn hkdf_derive_key( + &self, + _key: &[u8], + _salt: &[u8], + _info: &[u8], + _length: usize, + _hash_alg: HashAlgorithm, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn pbkdf2_derive_key( + &self, + _password: &[u8], + _salt: &[u8], + _iterations: u32, + _length: usize, + _hash_alg: HashAlgorithm, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn generate_aes_key(&self, length_bits: u16) -> Result, CryptoError> { + if !matches!(length_bits, 128 | 256) { + return Err(CryptoError::InvalidLength); + } + Ok(crate::llrt_crypto::random_byte_array( + (length_bits / 8) as usize, + )) + } + + fn generate_hmac_key( + &self, + hash_alg: HashAlgorithm, + length_bits: u16, + ) -> Result, CryptoError> { + let length_bytes = if length_bits == 0 { + match hash_alg { + HashAlgorithm::Sha256 => 64, + HashAlgorithm::Sha384 | HashAlgorithm::Sha512 => 128, + _ => return Err(CryptoError::UnsupportedAlgorithm), + } + } else { + (length_bits / 8) as usize + }; + Ok(crate::llrt_crypto::random_byte_array(length_bytes)) + } + + fn generate_ec_key(&self, _curve: EllipticCurve) -> Result<(Vec, Vec), CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn generate_ed25519_key(&self) -> Result<(Vec, Vec), CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn generate_x25519_key(&self) -> Result<(Vec, Vec), CryptoError> { + // Graviola doesn't expose as_bytes for X25519 PrivateKey + Err(CryptoError::UnsupportedAlgorithm) + } + + fn generate_rsa_key( + &self, + _modulus_length: u32, + _public_exponent: &[u8], + ) -> Result<(Vec, Vec), CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn import_rsa_public_key_pkcs1( + &self, + _der: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_rsa_private_key_pkcs1( + &self, + _der: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_rsa_public_key_spki( + &self, + _der: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_rsa_private_key_pkcs8( + &self, + _der: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_rsa_public_key_pkcs1(&self, _key_data: &[u8]) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_rsa_public_key_spki(&self, _key_data: &[u8]) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_rsa_private_key_pkcs8(&self, _key_data: &[u8]) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_ec_public_key_sec1( + &self, + _data: &[u8], + _curve: EllipticCurve, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_ec_public_key_spki( + &self, + _der: &[u8], + _curve: EllipticCurve, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_ec_private_key_pkcs8( + &self, + _der: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_ec_private_key_sec1( + &self, + _data: &[u8], + _curve: EllipticCurve, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_ec_public_key_sec1( + &self, + _key_data: &[u8], + _curve: EllipticCurve, + _is_private: bool, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_ec_public_key_spki( + &self, + _key_data: &[u8], + _curve: EllipticCurve, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_ec_private_key_pkcs8( + &self, + _key_data: &[u8], + _curve: EllipticCurve, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_okp_public_key_raw( + &self, + _data: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_okp_public_key_spki( + &self, + _der: &[u8], + _expected_oid: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_okp_private_key_pkcs8( + &self, + _der: &[u8], + _expected_oid: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_okp_public_key_raw( + &self, + _key_data: &[u8], + _is_private: bool, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_okp_public_key_spki( + &self, + _key_data: &[u8], + _oid: &[u8], + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_okp_private_key_pkcs8( + &self, + _key_data: &[u8], + _oid: &[u8], + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_rsa_jwk( + &self, + _jwk: super::RsaJwkImport<'_>, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_rsa_jwk( + &self, + _key_data: &[u8], + _is_private: bool, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_ec_jwk( + &self, + _jwk: super::EcJwkImport<'_>, + _curve: EllipticCurve, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_ec_jwk( + &self, + _key_data: &[u8], + _curve: EllipticCurve, + _is_private: bool, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_okp_jwk( + &self, + _jwk: super::OkpJwkImport<'_>, + _is_ed25519: bool, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_okp_jwk( + &self, + _key_data: &[u8], + _is_private: bool, + _is_ed25519: bool, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } +} + +// Hybrid types for graviola-rust: Graviola for SHA256/384/512, RustCrypto for MD5/SHA1 +#[cfg(any())] +pub enum GraviolaRustDigest { + Graviola(GraviolaDigest), + Rust(super::rust::RustDigest), +} + +#[cfg(any())] +impl GraviolaRustDigest { + pub fn new(algorithm: HashAlgorithm) -> Self { + match algorithm { + HashAlgorithm::Sha256 | HashAlgorithm::Sha384 | HashAlgorithm::Sha512 => { + Self::Graviola(GraviolaProvider.digest(algorithm)) + } + _ => Self::Rust(super::rust::RustCryptoProvider.digest(algorithm)), + } + } +} + +#[cfg(any())] +impl SimpleDigest for GraviolaRustDigest { + fn update(&mut self, data: &[u8]) { + match self { + Self::Graviola(d) => d.update(data), + Self::Rust(d) => d.update(data), + } + } + fn finalize(self) -> Vec { + match self { + Self::Graviola(d) => d.finalize(), + Self::Rust(d) => d.finalize(), + } + } +} + +#[cfg(any())] +pub enum GraviolaRustHmac { + Graviola(GraviolaHmac), + Rust(super::rust::RustHmac), +} + +#[cfg(any())] +impl GraviolaRustHmac { + pub fn new(algorithm: HashAlgorithm, key: &[u8]) -> Self { + match algorithm { + HashAlgorithm::Sha256 | HashAlgorithm::Sha384 | HashAlgorithm::Sha512 => { + Self::Graviola(GraviolaProvider.hmac(algorithm, key)) + } + _ => Self::Rust(super::rust::RustCryptoProvider.hmac(algorithm, key)), + } + } +} + +#[cfg(any())] +impl HmacProvider for GraviolaRustHmac { + fn update(&mut self, data: &[u8]) { + match self { + Self::Graviola(h) => h.update(data), + Self::Rust(h) => h.update(data), + } + } + fn finalize(self) -> Vec { + match self { + Self::Graviola(h) => h.finalize(), + Self::Rust(h) => h.finalize(), + } + } +} diff --git a/stdlib/src/llrt/llrt_crypto/provider/mod.rs b/stdlib/src/llrt/llrt_crypto/provider/mod.rs new file mode 100644 index 00000000..cff8c5e9 --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/provider/mod.rs @@ -0,0 +1,1268 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +// Ensure only one crypto provider is selected +#[cfg(all(all(), any()))] +compile_error!("Features `crypto-rust` and `crypto-openssl` are mutually exclusive"); + +#[cfg(all(all(), any()))] +compile_error!("Features `crypto-rust` and `crypto-ring` are mutually exclusive"); + +#[cfg(all(all(), any()))] +compile_error!("Features `crypto-rust` and `crypto-graviola` are mutually exclusive"); + +#[cfg(all(any(), any()))] +compile_error!("Features `crypto-ring` and `crypto-openssl` are mutually exclusive"); + +#[cfg(all(any(), any()))] +compile_error!("Features `crypto-ring` and `crypto-graviola` are mutually exclusive"); + +#[cfg(all(any(), any()))] +compile_error!("Features `crypto-openssl` and `crypto-graviola` are mutually exclusive"); + +#[cfg(all(any(), any()))] +compile_error!("Features `crypto-ring-rust` and `crypto-graviola-rust` are mutually exclusive"); + +#[cfg(any(any(), any()))] +mod graviola; + +#[cfg(any())] +mod openssl; + +#[cfg(any(any(), any()))] +mod ring; + +#[cfg(all())] +mod rust; + +use crate::llrt_crypto::hash::HashAlgorithm; +use crate::llrt_crypto::subtle::EllipticCurve; + +#[derive(Debug)] +#[allow(dead_code)] +pub struct RsaImportResult { + pub key_data: Vec, + pub modulus_length: u32, + pub public_exponent: Vec, + pub is_private: bool, +} + +#[derive(Debug)] +#[allow(dead_code)] +pub struct EcImportResult { + pub key_data: Vec, + pub is_private: bool, +} + +#[derive(Debug)] +#[allow(dead_code)] +pub struct OkpImportResult { + pub key_data: Vec, + pub is_private: bool, +} + +/// RSA JWK components for import (all values are raw bytes, not base64) +#[derive(Debug)] +#[allow(dead_code)] +pub struct RsaJwkImport<'a> { + pub n: &'a [u8], // modulus + pub e: &'a [u8], // public exponent + pub d: Option<&'a [u8]>, // private exponent + pub p: Option<&'a [u8]>, // first prime + pub q: Option<&'a [u8]>, // second prime + pub dp: Option<&'a [u8]>, // first factor CRT exponent + pub dq: Option<&'a [u8]>, // second factor CRT exponent + pub qi: Option<&'a [u8]>, // first CRT coefficient +} + +/// RSA JWK components for export +#[derive(Debug)] +#[allow(dead_code)] +pub struct RsaJwkExport { + pub n: Vec, + pub e: Vec, + pub d: Option>, + pub p: Option>, + pub q: Option>, + pub dp: Option>, + pub dq: Option>, + pub qi: Option>, +} + +/// EC JWK components for import (all values are raw bytes) +#[derive(Debug)] +#[allow(dead_code)] +pub struct EcJwkImport<'a> { + pub x: &'a [u8], + pub y: &'a [u8], + pub d: Option<&'a [u8]>, +} + +/// EC JWK components for export +#[derive(Debug)] +#[allow(dead_code)] +pub struct EcJwkExport { + pub x: Vec, + pub y: Vec, + pub d: Option>, +} + +/// OKP (Ed25519/X25519) JWK components for import +#[derive(Debug)] +#[allow(dead_code)] +pub struct OkpJwkImport<'a> { + pub x: &'a [u8], // public key + pub d: Option<&'a [u8]>, // private key +} + +/// OKP JWK components for export +#[derive(Debug)] +#[allow(dead_code)] +pub struct OkpJwkExport { + pub x: Vec, + pub d: Option>, +} + +pub trait SimpleDigest: Send { + fn update(&mut self, data: &[u8]); + fn finalize(self) -> Vec + where + Self: Sized; +} + +#[derive(Debug, Clone, Copy)] +#[allow(dead_code)] +pub enum AesMode { + Ctr { counter_length: u32 }, + Cbc, + Gcm { tag_length: u8 }, +} + +#[allow(dead_code)] +pub trait CryptoProvider { + type Digest: SimpleDigest; + type Hmac: HmacProvider; + + // Digest operations + fn digest(&self, algorithm: HashAlgorithm) -> Self::Digest; + + // HMAC operations + fn hmac(&self, algorithm: HashAlgorithm, key: &[u8]) -> Self::Hmac; + + // ECDSA operations + fn ecdsa_sign( + &self, + curve: EllipticCurve, + private_key_der: &[u8], + digest: &[u8], + ) -> Result, CryptoError>; + fn ecdsa_verify( + &self, + curve: EllipticCurve, + public_key_sec1: &[u8], + signature: &[u8], + digest: &[u8], + ) -> Result; + + // EdDSA operations + fn ed25519_sign(&self, private_key_der: &[u8], data: &[u8]) -> Result, CryptoError>; + fn ed25519_verify( + &self, + public_key_bytes: &[u8], + signature: &[u8], + data: &[u8], + ) -> Result; + + // RSA operations + fn rsa_pss_sign( + &self, + private_key_der: &[u8], + digest: &[u8], + salt_length: usize, + hash_alg: HashAlgorithm, + ) -> Result, CryptoError>; + fn rsa_pss_verify( + &self, + public_key_der: &[u8], + signature: &[u8], + digest: &[u8], + salt_length: usize, + hash_alg: HashAlgorithm, + ) -> Result; + fn rsa_pkcs1v15_sign( + &self, + private_key_der: &[u8], + digest: &[u8], + hash_alg: HashAlgorithm, + ) -> Result, CryptoError>; + fn rsa_pkcs1v15_verify( + &self, + public_key_der: &[u8], + signature: &[u8], + digest: &[u8], + hash_alg: HashAlgorithm, + ) -> Result; + fn rsa_oaep_encrypt( + &self, + public_key_der: &[u8], + data: &[u8], + hash_alg: HashAlgorithm, + label: Option<&[u8]>, + ) -> Result, CryptoError>; + fn rsa_oaep_decrypt( + &self, + private_key_der: &[u8], + data: &[u8], + hash_alg: HashAlgorithm, + label: Option<&[u8]>, + ) -> Result, CryptoError>; + + // ECDH operations + fn ecdh_derive_bits( + &self, + curve: EllipticCurve, + private_key_der: &[u8], + public_key_sec1: &[u8], + ) -> Result, CryptoError>; + + // X25519 operations + fn x25519_derive_bits( + &self, + private_key: &[u8], + public_key: &[u8], + ) -> Result, CryptoError>; + + // AES operations + fn aes_encrypt( + &self, + mode: AesMode, + key: &[u8], + iv: &[u8], + data: &[u8], + additional_data: Option<&[u8]>, + ) -> Result, CryptoError>; + fn aes_decrypt( + &self, + mode: AesMode, + key: &[u8], + iv: &[u8], + data: &[u8], + additional_data: Option<&[u8]>, + ) -> Result, CryptoError>; + + // AES-KW operations + fn aes_kw_wrap(&self, kek: &[u8], key: &[u8]) -> Result, CryptoError>; + fn aes_kw_unwrap(&self, kek: &[u8], wrapped_key: &[u8]) -> Result, CryptoError>; + + // KDF operations + fn hkdf_derive_key( + &self, + key: &[u8], + salt: &[u8], + info: &[u8], + length: usize, + hash_alg: HashAlgorithm, + ) -> Result, CryptoError>; + fn pbkdf2_derive_key( + &self, + password: &[u8], + salt: &[u8], + iterations: u32, + length: usize, + hash_alg: HashAlgorithm, + ) -> Result, CryptoError>; + + fn generate_aes_key(&self, length_bits: u16) -> Result, CryptoError>; + fn generate_hmac_key( + &self, + hash_alg: HashAlgorithm, + length_bits: u16, + ) -> Result, CryptoError>; + fn generate_ec_key(&self, curve: EllipticCurve) -> Result<(Vec, Vec), CryptoError>; // (private, public) + fn generate_ed25519_key(&self) -> Result<(Vec, Vec), CryptoError>; + fn generate_x25519_key(&self) -> Result<(Vec, Vec), CryptoError>; + fn generate_rsa_key( + &self, + modulus_length: u32, + public_exponent: &[u8], + ) -> Result<(Vec, Vec), CryptoError>; + + // RSA key import from DER formats + fn import_rsa_public_key_pkcs1(&self, der: &[u8]) -> Result; + fn import_rsa_private_key_pkcs1(&self, der: &[u8]) -> Result; + fn import_rsa_public_key_spki(&self, der: &[u8]) -> Result; + fn import_rsa_private_key_pkcs8(&self, der: &[u8]) -> Result; + + // RSA key export to DER formats + fn export_rsa_public_key_pkcs1(&self, key_data: &[u8]) -> Result, CryptoError>; + fn export_rsa_public_key_spki(&self, key_data: &[u8]) -> Result, CryptoError>; + fn export_rsa_private_key_pkcs8(&self, key_data: &[u8]) -> Result, CryptoError>; + + // EC key import from DER formats + fn import_ec_public_key_sec1( + &self, + data: &[u8], + curve: EllipticCurve, + ) -> Result; + fn import_ec_public_key_spki( + &self, + der: &[u8], + curve: EllipticCurve, + ) -> Result; + fn import_ec_private_key_pkcs8(&self, der: &[u8]) -> Result; + fn import_ec_private_key_sec1( + &self, + data: &[u8], + curve: EllipticCurve, + ) -> Result; + + // EC key export + fn export_ec_public_key_sec1( + &self, + key_data: &[u8], + curve: EllipticCurve, + is_private: bool, + ) -> Result, CryptoError>; + fn export_ec_public_key_spki( + &self, + key_data: &[u8], + curve: EllipticCurve, + ) -> Result, CryptoError>; + fn export_ec_private_key_pkcs8( + &self, + key_data: &[u8], + curve: EllipticCurve, + ) -> Result, CryptoError>; + + // OKP (Ed25519/X25519) key import + fn import_okp_public_key_raw(&self, data: &[u8]) -> Result; + fn import_okp_public_key_spki( + &self, + der: &[u8], + expected_oid: &[u8], + ) -> Result; + fn import_okp_private_key_pkcs8( + &self, + der: &[u8], + expected_oid: &[u8], + ) -> Result; + + // OKP key export + fn export_okp_public_key_raw( + &self, + key_data: &[u8], + is_private: bool, + ) -> Result, CryptoError>; + fn export_okp_public_key_spki( + &self, + key_data: &[u8], + oid: &[u8], + ) -> Result, CryptoError>; + fn export_okp_private_key_pkcs8( + &self, + key_data: &[u8], + oid: &[u8], + ) -> Result, CryptoError>; + + // JWK import/export + fn import_rsa_jwk(&self, jwk: RsaJwkImport<'_>) -> Result; + fn export_rsa_jwk( + &self, + key_data: &[u8], + is_private: bool, + ) -> Result; + fn import_ec_jwk( + &self, + jwk: EcJwkImport<'_>, + curve: EllipticCurve, + ) -> Result; + fn export_ec_jwk( + &self, + key_data: &[u8], + curve: EllipticCurve, + is_private: bool, + ) -> Result; + + // OKP JWK import/export + fn import_okp_jwk( + &self, + jwk: OkpJwkImport<'_>, + is_ed25519: bool, + ) -> Result; + fn export_okp_jwk( + &self, + key_data: &[u8], + is_private: bool, + is_ed25519: bool, + ) -> Result; +} + +pub trait HmacProvider: Send { + fn update(&mut self, data: &[u8]); + fn finalize(self) -> Vec + where + Self: Sized; +} + +#[derive(Debug)] +#[allow(dead_code)] +pub enum CryptoError { + InvalidKey(Option>), + InvalidData(Option>), + InvalidSignature(Option>), + InvalidLength, + SigningFailed(Option>), + VerificationFailed, + OperationFailed(Option>), + UnsupportedAlgorithm, + DerivationFailed(Option>), + EncryptionFailed(Option>), + DecryptionFailed(Option>), + InvalidAccess(Option>), +} + +impl std::fmt::Display for CryptoError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CryptoError::InvalidKey(None) => write!(f, "Invalid key"), + CryptoError::InvalidKey(Some(msg)) => write!(f, "Invalid key: {}", msg), + CryptoError::InvalidData(None) => write!(f, "Invalid data"), + CryptoError::InvalidData(Some(msg)) => write!(f, "Invalid data: {}", msg), + CryptoError::InvalidSignature(None) => write!(f, "Invalid signature"), + CryptoError::InvalidSignature(Some(msg)) => write!(f, "Invalid signature: {}", msg), + CryptoError::InvalidLength => write!(f, "Invalid length"), + CryptoError::SigningFailed(None) => write!(f, "Signing failed"), + CryptoError::SigningFailed(Some(msg)) => write!(f, "Signing failed: {}", msg), + CryptoError::VerificationFailed => write!(f, "Verification failed"), + CryptoError::OperationFailed(None) => write!(f, "Operation failed"), + CryptoError::OperationFailed(Some(msg)) => write!(f, "Operation failed: {}", msg), + CryptoError::UnsupportedAlgorithm => write!(f, "Unsupported algorithm"), + CryptoError::DerivationFailed(None) => write!(f, "Derivation failed"), + CryptoError::DerivationFailed(Some(msg)) => write!(f, "Derivation failed: {}", msg), + CryptoError::EncryptionFailed(None) => write!(f, "Encryption failed"), + CryptoError::EncryptionFailed(Some(msg)) => write!(f, "Encryption failed: {}", msg), + CryptoError::DecryptionFailed(None) => write!(f, "Decryption failed"), + CryptoError::DecryptionFailed(Some(msg)) => write!(f, "Decryption failed: {}", msg), + CryptoError::InvalidAccess(None) => write!(f, "Invalid access"), + CryptoError::InvalidAccess(Some(msg)) => write!(f, "Invalid access: {}", msg), + } + } +} + +impl std::error::Error for CryptoError {} + +pub fn parse_rsa_public_exponent(public_exponent: &[u8]) -> Result { + match public_exponent { + [0x01, 0x00, 0x01] => Ok(65537), + [0x03] => Ok(3), + bytes if bytes.ends_with(&[0x03]) && bytes[..bytes.len() - 1].iter().all(|&b| b == 0) => { + Ok(3) + } + _ => Err(CryptoError::OperationFailed(None)), + } +} + +#[cfg(any())] +pub type DefaultProvider = openssl::OpenSslProvider; + +#[cfg(all())] +pub type DefaultProvider = rust::RustCryptoProvider; + +#[cfg(any())] +pub type DefaultProvider = ring::RingProvider; + +#[cfg(any())] +pub type DefaultProvider = RingRustProvider; + +#[cfg(all(any(), not(any())))] +pub type DefaultProvider = graviola::GraviolaProvider; + +#[cfg(any())] +pub type DefaultProvider = GraviolaRustProvider; + +// Macro to generate hybrid providers that delegate to RustCrypto +#[cfg(any(any(), any()))] +macro_rules! impl_hybrid_provider { + ($name:ident, $digest:ty, $hmac:ty, $digest_fn:expr, $hmac_fn:expr, $aes_encrypt:expr, $aes_decrypt:expr) => { + pub struct $name; + impl CryptoProvider for $name { + type Digest = $digest; + type Hmac = $hmac; + fn digest(&self, alg: HashAlgorithm) -> Self::Digest { + $digest_fn(alg) + } + fn hmac(&self, alg: HashAlgorithm, key: &[u8]) -> Self::Hmac { + $hmac_fn(alg, key) + } + fn ecdsa_sign( + &self, + c: EllipticCurve, + k: &[u8], + d: &[u8], + ) -> Result, CryptoError> { + rust::RustCryptoProvider.ecdsa_sign(c, k, d) + } + fn ecdsa_verify( + &self, + c: EllipticCurve, + k: &[u8], + s: &[u8], + d: &[u8], + ) -> Result { + rust::RustCryptoProvider.ecdsa_verify(c, k, s, d) + } + fn ed25519_sign(&self, k: &[u8], d: &[u8]) -> Result, CryptoError> { + rust::RustCryptoProvider.ed25519_sign(k, d) + } + fn ed25519_verify(&self, k: &[u8], s: &[u8], d: &[u8]) -> Result { + rust::RustCryptoProvider.ed25519_verify(k, s, d) + } + fn rsa_pss_sign( + &self, + k: &[u8], + d: &[u8], + s: usize, + a: HashAlgorithm, + ) -> Result, CryptoError> { + rust::RustCryptoProvider.rsa_pss_sign(k, d, s, a) + } + fn rsa_pss_verify( + &self, + k: &[u8], + s: &[u8], + d: &[u8], + sl: usize, + a: HashAlgorithm, + ) -> Result { + rust::RustCryptoProvider.rsa_pss_verify(k, s, d, sl, a) + } + fn rsa_pkcs1v15_sign( + &self, + k: &[u8], + d: &[u8], + a: HashAlgorithm, + ) -> Result, CryptoError> { + rust::RustCryptoProvider.rsa_pkcs1v15_sign(k, d, a) + } + fn rsa_pkcs1v15_verify( + &self, + k: &[u8], + s: &[u8], + d: &[u8], + a: HashAlgorithm, + ) -> Result { + rust::RustCryptoProvider.rsa_pkcs1v15_verify(k, s, d, a) + } + fn rsa_oaep_encrypt( + &self, + k: &[u8], + d: &[u8], + a: HashAlgorithm, + l: Option<&[u8]>, + ) -> Result, CryptoError> { + rust::RustCryptoProvider.rsa_oaep_encrypt(k, d, a, l) + } + fn rsa_oaep_decrypt( + &self, + k: &[u8], + d: &[u8], + a: HashAlgorithm, + l: Option<&[u8]>, + ) -> Result, CryptoError> { + rust::RustCryptoProvider.rsa_oaep_decrypt(k, d, a, l) + } + fn ecdh_derive_bits( + &self, + c: EllipticCurve, + pk: &[u8], + pubk: &[u8], + ) -> Result, CryptoError> { + rust::RustCryptoProvider.ecdh_derive_bits(c, pk, pubk) + } + fn x25519_derive_bits(&self, pk: &[u8], pubk: &[u8]) -> Result, CryptoError> { + rust::RustCryptoProvider.x25519_derive_bits(pk, pubk) + } + fn aes_encrypt( + &self, + m: AesMode, + k: &[u8], + iv: &[u8], + d: &[u8], + aad: Option<&[u8]>, + ) -> Result, CryptoError> { + $aes_encrypt(m, k, iv, d, aad) + } + fn aes_decrypt( + &self, + m: AesMode, + k: &[u8], + iv: &[u8], + d: &[u8], + aad: Option<&[u8]>, + ) -> Result, CryptoError> { + $aes_decrypt(m, k, iv, d, aad) + } + fn aes_kw_wrap(&self, kek: &[u8], k: &[u8]) -> Result, CryptoError> { + rust::RustCryptoProvider.aes_kw_wrap(kek, k) + } + fn aes_kw_unwrap(&self, kek: &[u8], w: &[u8]) -> Result, CryptoError> { + rust::RustCryptoProvider.aes_kw_unwrap(kek, w) + } + fn hkdf_derive_key( + &self, + k: &[u8], + s: &[u8], + i: &[u8], + l: usize, + a: HashAlgorithm, + ) -> Result, CryptoError> { + rust::RustCryptoProvider.hkdf_derive_key(k, s, i, l, a) + } + fn pbkdf2_derive_key( + &self, + p: &[u8], + s: &[u8], + i: u32, + l: usize, + a: HashAlgorithm, + ) -> Result, CryptoError> { + rust::RustCryptoProvider.pbkdf2_derive_key(p, s, i, l, a) + } + fn generate_aes_key(&self, b: u16) -> Result, CryptoError> { + rust::RustCryptoProvider.generate_aes_key(b) + } + fn generate_hmac_key(&self, a: HashAlgorithm, b: u16) -> Result, CryptoError> { + rust::RustCryptoProvider.generate_hmac_key(a, b) + } + fn generate_ec_key(&self, c: EllipticCurve) -> Result<(Vec, Vec), CryptoError> { + rust::RustCryptoProvider.generate_ec_key(c) + } + fn generate_ed25519_key(&self) -> Result<(Vec, Vec), CryptoError> { + rust::RustCryptoProvider.generate_ed25519_key() + } + fn generate_x25519_key(&self) -> Result<(Vec, Vec), CryptoError> { + rust::RustCryptoProvider.generate_x25519_key() + } + fn generate_rsa_key( + &self, + b: u32, + e: &[u8], + ) -> Result<(Vec, Vec), CryptoError> { + rust::RustCryptoProvider.generate_rsa_key(b, e) + } + fn import_rsa_public_key_pkcs1( + &self, + d: &[u8], + ) -> Result { + rust::RustCryptoProvider.import_rsa_public_key_pkcs1(d) + } + fn import_rsa_private_key_pkcs1( + &self, + d: &[u8], + ) -> Result { + rust::RustCryptoProvider.import_rsa_private_key_pkcs1(d) + } + fn import_rsa_public_key_spki(&self, d: &[u8]) -> Result { + rust::RustCryptoProvider.import_rsa_public_key_spki(d) + } + fn import_rsa_private_key_pkcs8( + &self, + d: &[u8], + ) -> Result { + rust::RustCryptoProvider.import_rsa_private_key_pkcs8(d) + } + fn export_rsa_public_key_pkcs1(&self, d: &[u8]) -> Result, CryptoError> { + rust::RustCryptoProvider.export_rsa_public_key_pkcs1(d) + } + fn export_rsa_public_key_spki(&self, d: &[u8]) -> Result, CryptoError> { + rust::RustCryptoProvider.export_rsa_public_key_spki(d) + } + fn export_rsa_private_key_pkcs8(&self, d: &[u8]) -> Result, CryptoError> { + rust::RustCryptoProvider.export_rsa_private_key_pkcs8(d) + } + fn import_ec_public_key_sec1( + &self, + d: &[u8], + c: EllipticCurve, + ) -> Result { + rust::RustCryptoProvider.import_ec_public_key_sec1(d, c) + } + fn import_ec_public_key_spki( + &self, + d: &[u8], + c: EllipticCurve, + ) -> Result { + rust::RustCryptoProvider.import_ec_public_key_spki(d, c) + } + fn import_ec_private_key_pkcs8(&self, d: &[u8]) -> Result { + rust::RustCryptoProvider.import_ec_private_key_pkcs8(d) + } + fn import_ec_private_key_sec1( + &self, + d: &[u8], + c: EllipticCurve, + ) -> Result { + rust::RustCryptoProvider.import_ec_private_key_sec1(d, c) + } + fn export_ec_public_key_sec1( + &self, + d: &[u8], + c: EllipticCurve, + p: bool, + ) -> Result, CryptoError> { + rust::RustCryptoProvider.export_ec_public_key_sec1(d, c, p) + } + fn export_ec_public_key_spki( + &self, + d: &[u8], + c: EllipticCurve, + ) -> Result, CryptoError> { + rust::RustCryptoProvider.export_ec_public_key_spki(d, c) + } + fn export_ec_private_key_pkcs8( + &self, + d: &[u8], + c: EllipticCurve, + ) -> Result, CryptoError> { + rust::RustCryptoProvider.export_ec_private_key_pkcs8(d, c) + } + fn import_okp_public_key_raw(&self, d: &[u8]) -> Result { + rust::RustCryptoProvider.import_okp_public_key_raw(d) + } + fn import_okp_public_key_spki( + &self, + d: &[u8], + o: &[u8], + ) -> Result { + rust::RustCryptoProvider.import_okp_public_key_spki(d, o) + } + fn import_okp_private_key_pkcs8( + &self, + d: &[u8], + o: &[u8], + ) -> Result { + rust::RustCryptoProvider.import_okp_private_key_pkcs8(d, o) + } + fn export_okp_public_key_raw(&self, d: &[u8], p: bool) -> Result, CryptoError> { + rust::RustCryptoProvider.export_okp_public_key_raw(d, p) + } + fn export_okp_public_key_spki( + &self, + d: &[u8], + o: &[u8], + ) -> Result, CryptoError> { + rust::RustCryptoProvider.export_okp_public_key_spki(d, o) + } + fn export_okp_private_key_pkcs8( + &self, + d: &[u8], + o: &[u8], + ) -> Result, CryptoError> { + rust::RustCryptoProvider.export_okp_private_key_pkcs8(d, o) + } + fn import_rsa_jwk(&self, j: RsaJwkImport<'_>) -> Result { + rust::RustCryptoProvider.import_rsa_jwk(j) + } + fn export_rsa_jwk(&self, d: &[u8], p: bool) -> Result { + rust::RustCryptoProvider.export_rsa_jwk(d, p) + } + fn import_ec_jwk( + &self, + j: EcJwkImport<'_>, + c: EllipticCurve, + ) -> Result { + rust::RustCryptoProvider.import_ec_jwk(j, c) + } + fn export_ec_jwk( + &self, + d: &[u8], + c: EllipticCurve, + p: bool, + ) -> Result { + rust::RustCryptoProvider.export_ec_jwk(d, c, p) + } + fn import_okp_jwk( + &self, + j: OkpJwkImport<'_>, + is_ed25519: bool, + ) -> Result { + rust::RustCryptoProvider.import_okp_jwk(j, is_ed25519) + } + fn export_okp_jwk( + &self, + d: &[u8], + is_private: bool, + is_ed25519: bool, + ) -> Result { + rust::RustCryptoProvider.export_okp_jwk(d, is_private, is_ed25519) + } + } + }; +} + +#[cfg(any())] +impl_hybrid_provider!( + RingRustProvider, + ring::RingDigestType, + ring::RingHmacType, + |a| ring::RingProvider.digest(a), + |a, k| ring::RingProvider.hmac(a, k), + |m, k, iv, d, aad| rust::RustCryptoProvider.aes_encrypt(m, k, iv, d, aad), + |m, k, iv, d, aad| rust::RustCryptoProvider.aes_decrypt(m, k, iv, d, aad) +); + +#[cfg(any())] +fn graviola_aes_supported() -> bool { + #[cfg(target_arch = "aarch64")] + { + std::arch::is_aarch64_feature_detected!("aes") + } + #[cfg(target_arch = "x86_64")] + { + std::arch::is_x86_feature_detected!("aes") + } + #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] + { + false + } +} + +#[cfg(any())] +impl_hybrid_provider!( + GraviolaRustProvider, + graviola::GraviolaRustDigest, + graviola::GraviolaRustHmac, + graviola::GraviolaRustDigest::new, + graviola::GraviolaRustHmac::new, + |m: AesMode, k: &[u8], iv: &[u8], d: &[u8], aad: Option<&[u8]>| { + if graviola_aes_supported() + && matches!(m, AesMode::Gcm { .. }) + && matches!(k.len(), 16 | 32) + { + graviola::GraviolaProvider.aes_encrypt(m, k, iv, d, aad) + } else { + rust::RustCryptoProvider.aes_encrypt(m, k, iv, d, aad) + } + }, + |m: AesMode, k: &[u8], iv: &[u8], d: &[u8], aad: Option<&[u8]>| { + if graviola_aes_supported() + && matches!(m, AesMode::Gcm { .. }) + && matches!(k.len(), 16 | 32) + { + graviola::GraviolaProvider.aes_decrypt(m, k, iv, d, aad) + } else { + rust::RustCryptoProvider.aes_decrypt(m, k, iv, d, aad) + } + } +); + +#[cfg(test)] +mod tests { + use super::*; + + fn provider() -> impl CryptoProvider { + #[cfg(all())] + return rust::RustCryptoProvider; + #[cfg(any())] + return RingRustProvider; + #[cfg(any())] + return GraviolaRustProvider; + #[cfg(any())] + return openssl::OpenSslProvider; + #[cfg(any())] + return ring::RingProvider; + #[cfg(all(any(), not(any())))] + return graviola::GraviolaProvider; + } + + fn to_hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{:02x}", b)).collect() + } + + // SHA digest tests + #[test] + fn test_sha256_digest() { + let p = provider(); + let mut digest = p.digest(HashAlgorithm::Sha256); + digest.update(b"hello world"); + let result = digest.finalize(); + assert_eq!(result.len(), 32); + assert_eq!( + to_hex(&result), + "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" + ); + } + + #[test] + fn test_sha384_digest() { + let p = provider(); + let mut digest = p.digest(HashAlgorithm::Sha384); + digest.update(b"hello world"); + let result = digest.finalize(); + assert_eq!(result.len(), 48); + } + + #[test] + fn test_sha512_digest() { + let p = provider(); + let mut digest = p.digest(HashAlgorithm::Sha512); + digest.update(b"hello world"); + let result = digest.finalize(); + assert_eq!(result.len(), 64); + } + + // HMAC tests + #[test] + fn test_hmac_sha256() { + let p = provider(); + let key = b"secret key"; + let mut hmac = p.hmac(HashAlgorithm::Sha256, key); + hmac.update(b"hello world"); + let result = hmac.finalize(); + assert_eq!(result.len(), 32); + } + + // AES-GCM tests - only for providers that support AES + #[cfg(any(all(), any(), any(), any()))] + #[test] + fn test_aes_gcm_128_roundtrip() { + let p = provider(); + let key = [0u8; 16]; + let iv = [0u8; 12]; + let plaintext = b"hello world"; + let aad = b"additional data"; + + let ciphertext = p + .aes_encrypt( + AesMode::Gcm { tag_length: 128 }, + &key, + &iv, + plaintext, + Some(aad), + ) + .unwrap(); + + assert_eq!(ciphertext.len(), plaintext.len() + 16); // plaintext + tag + + let decrypted = p + .aes_decrypt( + AesMode::Gcm { tag_length: 128 }, + &key, + &iv, + &ciphertext, + Some(aad), + ) + .unwrap(); + + assert_eq!(decrypted, plaintext); + } + + #[cfg(any(all(), any(), any(), any()))] + #[test] + fn test_aes_gcm_256_roundtrip() { + let p = provider(); + let key = [0u8; 32]; + let iv = [0u8; 12]; + let plaintext = b"hello world"; + + let ciphertext = p + .aes_encrypt(AesMode::Gcm { tag_length: 128 }, &key, &iv, plaintext, None) + .unwrap(); + + let decrypted = p + .aes_decrypt( + AesMode::Gcm { tag_length: 128 }, + &key, + &iv, + &ciphertext, + None, + ) + .unwrap(); + + assert_eq!(decrypted, plaintext); + } + + #[cfg(any(all(), any(), any(), any()))] + #[test] + fn test_aes_gcm_wrong_key_fails() { + let p = provider(); + let key = [0u8; 16]; + let wrong_key = [1u8; 16]; + let iv = [0u8; 12]; + let plaintext = b"hello world"; + + let ciphertext = p + .aes_encrypt(AesMode::Gcm { tag_length: 128 }, &key, &iv, plaintext, None) + .unwrap(); + + let result = p.aes_decrypt( + AesMode::Gcm { tag_length: 128 }, + &wrong_key, + &iv, + &ciphertext, + None, + ); + + assert!(result.is_err()); + } + + // Key generation tests - only for providers that support key generation + #[cfg(any(all(), any(), any(), any()))] + #[test] + fn test_generate_aes_key_128() { + let p = provider(); + let key = p.generate_aes_key(128).unwrap(); + assert_eq!(key.len(), 16); + } + + #[cfg(any(all(), any(), any(), any()))] + #[test] + fn test_generate_aes_key_256() { + let p = provider(); + let key = p.generate_aes_key(256).unwrap(); + assert_eq!(key.len(), 32); + } + + #[cfg(any(all(), any(), any(), any()))] + #[test] + fn test_generate_hmac_key() { + let p = provider(); + let key = p.generate_hmac_key(HashAlgorithm::Sha256, 256).unwrap(); + assert_eq!(key.len(), 32); + } + + // Tests that require full crypto support + #[cfg(any(all(), any(), any(), any()))] + mod full_provider_tests { + use super::*; + + #[test] + fn test_aes_cbc_roundtrip() { + let p = provider(); + let key = [0u8; 16]; + let iv = [0u8; 16]; + let plaintext = b"hello world12345"; // 16 bytes for block alignment + + let ciphertext = p + .aes_encrypt(AesMode::Cbc, &key, &iv, plaintext, None) + .unwrap(); + + let decrypted = p + .aes_decrypt(AesMode::Cbc, &key, &iv, &ciphertext, None) + .unwrap(); + + assert_eq!(decrypted, plaintext); + } + + #[test] + fn test_aes_ctr_roundtrip() { + let p = provider(); + let key = [0u8; 16]; + let iv = [0u8; 16]; + let plaintext = b"hello world"; + + let ciphertext = p + .aes_encrypt( + AesMode::Ctr { counter_length: 64 }, + &key, + &iv, + plaintext, + None, + ) + .unwrap(); + + let decrypted = p + .aes_decrypt( + AesMode::Ctr { counter_length: 64 }, + &key, + &iv, + &ciphertext, + None, + ) + .unwrap(); + + assert_eq!(decrypted, plaintext); + } + + #[test] + fn test_aes_kw_roundtrip() { + let p = provider(); + let kek = [0u8; 16]; + let key_to_wrap = [1u8; 16]; + + let wrapped = p.aes_kw_wrap(&kek, &key_to_wrap).unwrap(); + let unwrapped = p.aes_kw_unwrap(&kek, &wrapped).unwrap(); + + assert_eq!(unwrapped, key_to_wrap); + } + + #[test] + fn test_hkdf_derive() { + let p = provider(); + let ikm = b"input key material"; + let salt = b"salt"; + let info = b"info"; + + let derived = p + .hkdf_derive_key(ikm, salt, info, 32, HashAlgorithm::Sha256) + .unwrap(); + + assert_eq!(derived.len(), 32); + } + + #[test] + fn test_pbkdf2_derive() { + let p = provider(); + let password = b"password"; + let salt = b"salt"; + + let derived = p + .pbkdf2_derive_key(password, salt, 1000, 32, HashAlgorithm::Sha256) + .unwrap(); + + assert_eq!(derived.len(), 32); + } + + #[test] + fn test_ec_p256_sign_verify() { + let p = provider(); + let (private_key, public_key) = p.generate_ec_key(EllipticCurve::P256).unwrap(); + + // Create a digest to sign + let mut digest = p.digest(HashAlgorithm::Sha256); + digest.update(b"message to sign"); + let hash = digest.finalize(); + + let signature = p + .ecdsa_sign(EllipticCurve::P256, &private_key, &hash) + .unwrap(); + + let valid = p + .ecdsa_verify(EllipticCurve::P256, &public_key, &signature, &hash) + .unwrap(); + + assert!(valid); + } + + #[test] + fn test_ec_p384_sign_verify() { + let p = provider(); + let (private_key, public_key) = p.generate_ec_key(EllipticCurve::P384).unwrap(); + + let mut digest = p.digest(HashAlgorithm::Sha384); + digest.update(b"message to sign"); + let hash = digest.finalize(); + + let signature = p + .ecdsa_sign(EllipticCurve::P384, &private_key, &hash) + .unwrap(); + + let valid = p + .ecdsa_verify(EllipticCurve::P384, &public_key, &signature, &hash) + .unwrap(); + + assert!(valid); + } + + #[test] + fn test_ed25519_sign_verify() { + let p = provider(); + let (private_key, public_key) = p.generate_ed25519_key().unwrap(); + + let message = b"message to sign"; + let signature = p.ed25519_sign(&private_key, message).unwrap(); + + let valid = p.ed25519_verify(&public_key, &signature, message).unwrap(); + + assert!(valid); + } + + #[test] + fn test_x25519_key_exchange() { + let p = provider(); + let (alice_private, alice_public) = p.generate_x25519_key().unwrap(); + let (bob_private, bob_public) = p.generate_x25519_key().unwrap(); + + let alice_shared = p.x25519_derive_bits(&alice_private, &bob_public).unwrap(); + let bob_shared = p.x25519_derive_bits(&bob_private, &alice_public).unwrap(); + + assert_eq!(alice_shared, bob_shared); + assert_eq!(alice_shared.len(), 32); + } + + #[test] + fn test_ecdh_p256_key_exchange() { + let p = provider(); + let (alice_private, alice_public) = p.generate_ec_key(EllipticCurve::P256).unwrap(); + let (bob_private, bob_public) = p.generate_ec_key(EllipticCurve::P256).unwrap(); + + let alice_shared = p + .ecdh_derive_bits(EllipticCurve::P256, &alice_private, &bob_public) + .unwrap(); + let bob_shared = p + .ecdh_derive_bits(EllipticCurve::P256, &bob_private, &alice_public) + .unwrap(); + + assert_eq!(alice_shared, bob_shared); + } + + #[test] + fn test_rsa_pss_sign_verify() { + let p = provider(); + let (private_key, public_key) = p.generate_rsa_key(2048, &[1, 0, 1]).unwrap(); + + let mut digest = p.digest(HashAlgorithm::Sha256); + digest.update(b"message to sign"); + let hash = digest.finalize(); + + let signature = p + .rsa_pss_sign(&private_key, &hash, 32, HashAlgorithm::Sha256) + .unwrap(); + + let valid = p + .rsa_pss_verify(&public_key, &signature, &hash, 32, HashAlgorithm::Sha256) + .unwrap(); + + assert!(valid); + } + + #[test] + fn test_rsa_pkcs1v15_sign_verify() { + let p = provider(); + let (private_key, public_key) = p.generate_rsa_key(2048, &[1, 0, 1]).unwrap(); + + let mut digest = p.digest(HashAlgorithm::Sha256); + digest.update(b"message to sign"); + let hash = digest.finalize(); + + let signature = p + .rsa_pkcs1v15_sign(&private_key, &hash, HashAlgorithm::Sha256) + .unwrap(); + + let valid = p + .rsa_pkcs1v15_verify(&public_key, &signature, &hash, HashAlgorithm::Sha256) + .unwrap(); + + assert!(valid); + } + + #[test] + fn test_rsa_oaep_encrypt_decrypt() { + let p = provider(); + let (private_key, public_key) = p.generate_rsa_key(2048, &[1, 0, 1]).unwrap(); + + let plaintext = b"secret message"; + + let ciphertext = p + .rsa_oaep_encrypt(&public_key, plaintext, HashAlgorithm::Sha256, None) + .unwrap(); + + let decrypted = p + .rsa_oaep_decrypt(&private_key, &ciphertext, HashAlgorithm::Sha256, None) + .unwrap(); + + assert_eq!(decrypted, plaintext); + } + } +} diff --git a/stdlib/src/llrt/llrt_crypto/provider/openssl.rs b/stdlib/src/llrt/llrt_crypto/provider/openssl.rs new file mode 100644 index 00000000..f223c662 --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/provider/openssl.rs @@ -0,0 +1,1319 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! OpenSSL crypto provider - uses OpenSSL for cryptographic operations. + +use openssl::bn::BigNum; +use openssl::derive::Deriver; +use openssl::ec::{EcGroup, EcKey}; +use openssl::ecdsa::EcdsaSig; +use openssl::hash::{Hasher, MessageDigest}; +use openssl::md::Md; +use openssl::nid::Nid; +use openssl::pkey::{Id, PKey}; +use openssl::pkey_ctx::PkeyCtx; +use openssl::rand::rand_bytes; +use openssl::rsa::{Padding, Rsa}; +use openssl::sign::{Signer, Verifier}; +use openssl::symm::{self, Cipher}; + +use crate::llrt_crypto::hash::HashAlgorithm; +use crate::llrt_crypto::provider::{ + AesMode, CryptoError, CryptoProvider, HmacProvider, SimpleDigest, +}; +use crate::llrt_crypto::subtle::EllipticCurve; + +pub struct OpenSslProvider; + +pub enum OpenSslDigest { + Md5(Hasher), + Sha1(Hasher), + Sha256(Hasher), + Sha384(Hasher), + Sha512(Hasher), +} + +impl SimpleDigest for OpenSslDigest { + fn update(&mut self, data: &[u8]) { + match self { + OpenSslDigest::Md5(h) + | OpenSslDigest::Sha1(h) + | OpenSslDigest::Sha256(h) + | OpenSslDigest::Sha384(h) + | OpenSslDigest::Sha512(h) => { + let _ = h.update(data); + } + } + } + + fn finalize(mut self) -> Vec { + match self { + OpenSslDigest::Md5(ref mut h) + | OpenSslDigest::Sha1(ref mut h) + | OpenSslDigest::Sha256(ref mut h) + | OpenSslDigest::Sha384(ref mut h) + | OpenSslDigest::Sha512(ref mut h) => { + h.finish().map(|d| d.to_vec()).unwrap_or_default() + } + } + } +} + +pub struct OpenSslHmac { + signer: Signer<'static>, +} + +impl HmacProvider for OpenSslHmac { + fn update(&mut self, data: &[u8]) { + let _ = self.signer.update(data); + } + + fn finalize(self) -> Vec { + self.signer.sign_to_vec().unwrap_or_default() + } +} + +fn get_message_digest(alg: HashAlgorithm) -> MessageDigest { + match alg { + HashAlgorithm::Md5 => MessageDigest::md5(), + HashAlgorithm::Sha1 => MessageDigest::sha1(), + HashAlgorithm::Sha256 => MessageDigest::sha256(), + HashAlgorithm::Sha384 => MessageDigest::sha384(), + HashAlgorithm::Sha512 => MessageDigest::sha512(), + } +} + +fn get_md(alg: HashAlgorithm) -> &'static openssl::md::MdRef { + match alg { + HashAlgorithm::Md5 => Md::md5(), + HashAlgorithm::Sha1 => Md::sha1(), + HashAlgorithm::Sha256 => Md::sha256(), + HashAlgorithm::Sha384 => Md::sha384(), + HashAlgorithm::Sha512 => Md::sha512(), + } +} + +fn curve_to_nid(curve: EllipticCurve) -> Nid { + match curve { + EllipticCurve::P256 => Nid::X9_62_PRIME256V1, + EllipticCurve::P384 => Nid::SECP384R1, + EllipticCurve::P521 => Nid::SECP521R1, + } +} + +fn get_ec_group(curve: EllipticCurve) -> Result { + let nid = curve_to_nid(curve); + EcGroup::from_curve_name(nid) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into()))) +} + +impl CryptoProvider for OpenSslProvider { + type Digest = OpenSslDigest; + type Hmac = OpenSslHmac; + + fn digest(&self, algorithm: HashAlgorithm) -> Self::Digest { + let md = get_message_digest(algorithm); + let hasher = Hasher::new(md).expect("Failed to create hasher"); + match algorithm { + HashAlgorithm::Md5 => OpenSslDigest::Md5(hasher), + HashAlgorithm::Sha1 => OpenSslDigest::Sha1(hasher), + HashAlgorithm::Sha256 => OpenSslDigest::Sha256(hasher), + HashAlgorithm::Sha384 => OpenSslDigest::Sha384(hasher), + HashAlgorithm::Sha512 => OpenSslDigest::Sha512(hasher), + } + } + + fn hmac(&self, algorithm: HashAlgorithm, key: &[u8]) -> Self::Hmac { + let md = get_message_digest(algorithm); + let pkey = PKey::hmac(key).expect("Failed to create HMAC key"); + let signer = unsafe { + std::mem::transmute::, Signer<'static>>( + Signer::new(md, &pkey).expect("Failed to create signer"), + ) + }; + OpenSslHmac { signer } + } + + fn ecdsa_sign( + &self, + curve: EllipticCurve, + private_key_der: &[u8], + digest: &[u8], + ) -> Result, CryptoError> { + let group = get_ec_group(curve)?; + let ec_key = EcKey::private_key_from_der(private_key_der) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let sig = EcdsaSig::sign(digest, &ec_key) + .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; + let r = sig.r().to_vec(); + let s = sig.s().to_vec(); + let coord_len = (group.degree() as usize).div_ceil(8); + let mut result = vec![0u8; coord_len * 2]; + result[coord_len - r.len()..coord_len].copy_from_slice(&r); + result[coord_len * 2 - s.len()..].copy_from_slice(&s); + Ok(result) + } + + fn ecdsa_verify( + &self, + curve: EllipticCurve, + public_key_sec1: &[u8], + signature: &[u8], + digest: &[u8], + ) -> Result { + let group = get_ec_group(curve)?; + let ec_key = EcKey::public_key_from_der(public_key_sec1).or_else(|_| { + let point = openssl::ec::EcPoint::from_bytes( + &group, + public_key_sec1, + &mut openssl::bn::BigNumContext::new().unwrap(), + ) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + EcKey::from_public_key(&group, &point) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) + })?; + let coord_len = signature.len() / 2; + let r = BigNum::from_slice(&signature[..coord_len]) + .map_err(|e| CryptoError::InvalidSignature(Some(e.to_string().into())))?; + let s = BigNum::from_slice(&signature[coord_len..]) + .map_err(|e| CryptoError::InvalidSignature(Some(e.to_string().into())))?; + let sig = EcdsaSig::from_private_components(r, s) + .map_err(|e| CryptoError::InvalidSignature(Some(e.to_string().into())))?; + Ok(sig.verify(digest, &ec_key).unwrap_or(false)) + } + + fn ed25519_sign(&self, private_key_der: &[u8], data: &[u8]) -> Result, CryptoError> { + let pkey = PKey::private_key_from_der(private_key_der) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let mut signer = Signer::new_without_digest(&pkey) + .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; + signer + .sign_oneshot_to_vec(data) + .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into()))) + } + + fn ed25519_verify( + &self, + public_key_bytes: &[u8], + signature: &[u8], + data: &[u8], + ) -> Result { + let pkey = PKey::public_key_from_raw_bytes(public_key_bytes, Id::ED25519) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let mut verifier = Verifier::new_without_digest(&pkey) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + Ok(verifier.verify_oneshot(signature, data).unwrap_or(false)) + } + + fn rsa_pss_sign( + &self, + private_key_der: &[u8], + digest: &[u8], + salt_length: usize, + hash_alg: HashAlgorithm, + ) -> Result, CryptoError> { + let rsa = Rsa::private_key_from_der(private_key_der) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let pkey = + PKey::from_rsa(rsa).map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let md = get_message_digest(hash_alg); + let mut signer = Signer::new(md, &pkey) + .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; + signer + .set_rsa_padding(Padding::PKCS1_PSS) + .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; + signer + .set_rsa_pss_saltlen(openssl::sign::RsaPssSaltlen::custom(salt_length as i32)) + .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; + signer + .set_rsa_mgf1_md(md) + .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; + signer + .update(digest) + .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; + signer + .sign_to_vec() + .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into()))) + } + + fn rsa_pss_verify( + &self, + public_key_der: &[u8], + signature: &[u8], + digest: &[u8], + salt_length: usize, + hash_alg: HashAlgorithm, + ) -> Result { + let rsa = Rsa::public_key_from_der(public_key_der) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let pkey = + PKey::from_rsa(rsa).map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let md = get_message_digest(hash_alg); + let mut verifier = Verifier::new(md, &pkey) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + verifier + .set_rsa_padding(Padding::PKCS1_PSS) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + verifier + .set_rsa_pss_saltlen(openssl::sign::RsaPssSaltlen::custom(salt_length as i32)) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + verifier + .set_rsa_mgf1_md(md) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + verifier + .update(digest) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + Ok(verifier.verify(signature).unwrap_or(false)) + } + + fn rsa_pkcs1v15_sign( + &self, + private_key_der: &[u8], + digest: &[u8], + hash_alg: HashAlgorithm, + ) -> Result, CryptoError> { + let rsa = Rsa::private_key_from_der(private_key_der) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let pkey = + PKey::from_rsa(rsa).map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let md = get_message_digest(hash_alg); + let mut signer = Signer::new(md, &pkey) + .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; + signer + .set_rsa_padding(Padding::PKCS1) + .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; + signer + .update(digest) + .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; + signer + .sign_to_vec() + .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into()))) + } + + fn rsa_pkcs1v15_verify( + &self, + public_key_der: &[u8], + signature: &[u8], + digest: &[u8], + hash_alg: HashAlgorithm, + ) -> Result { + let rsa = Rsa::public_key_from_der(public_key_der) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let pkey = + PKey::from_rsa(rsa).map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let md = get_message_digest(hash_alg); + let mut verifier = Verifier::new(md, &pkey) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + verifier + .set_rsa_padding(Padding::PKCS1) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + verifier + .update(digest) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + Ok(verifier.verify(signature).unwrap_or(false)) + } + + fn rsa_oaep_encrypt( + &self, + public_key_der: &[u8], + data: &[u8], + hash_alg: HashAlgorithm, + label: Option<&[u8]>, + ) -> Result, CryptoError> { + let rsa = Rsa::public_key_from_der(public_key_der) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let pkey = + PKey::from_rsa(rsa).map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let mut ctx = PkeyCtx::new(&pkey) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + ctx.encrypt_init() + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + ctx.set_rsa_padding(Padding::PKCS1_OAEP) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + ctx.set_rsa_oaep_md(get_md(hash_alg)) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + ctx.set_rsa_mgf1_md(get_md(hash_alg)) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + if let Some(lbl) = label { + ctx.set_rsa_oaep_label(lbl) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + } + let mut out = vec![0u8; pkey.size()]; + let len = ctx + .encrypt(data, Some(&mut out)) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + out.truncate(len); + Ok(out) + } + + fn rsa_oaep_decrypt( + &self, + private_key_der: &[u8], + data: &[u8], + hash_alg: HashAlgorithm, + label: Option<&[u8]>, + ) -> Result, CryptoError> { + let rsa = Rsa::private_key_from_der(private_key_der) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let pkey = + PKey::from_rsa(rsa).map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let mut ctx = PkeyCtx::new(&pkey) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + ctx.decrypt_init() + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + ctx.set_rsa_padding(Padding::PKCS1_OAEP) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + ctx.set_rsa_oaep_md(get_md(hash_alg)) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + ctx.set_rsa_mgf1_md(get_md(hash_alg)) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + if let Some(lbl) = label { + ctx.set_rsa_oaep_label(lbl) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + } + let mut out = vec![0u8; pkey.size()]; + let len = ctx + .decrypt(data, Some(&mut out)) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + out.truncate(len); + Ok(out) + } + + fn ecdh_derive_bits( + &self, + curve: EllipticCurve, + private_key_der: &[u8], + public_key_sec1: &[u8], + ) -> Result, CryptoError> { + let group = get_ec_group(curve)?; + let private_ec = EcKey::private_key_from_der(private_key_der) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let private_pkey = PKey::from_ec_key(private_ec) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let public_ec = EcKey::public_key_from_der(public_key_sec1).or_else(|_| { + let point = openssl::ec::EcPoint::from_bytes( + &group, + public_key_sec1, + &mut openssl::bn::BigNumContext::new().unwrap(), + ) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + EcKey::from_public_key(&group, &point) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) + })?; + let public_pkey = PKey::from_ec_key(public_ec) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let mut deriver = Deriver::new(&private_pkey) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + deriver + .set_peer(&public_pkey) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + deriver + .derive_to_vec() + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into()))) + } + + fn x25519_derive_bits( + &self, + private_key: &[u8], + public_key: &[u8], + ) -> Result, CryptoError> { + let private_pkey = PKey::private_key_from_raw_bytes(private_key, Id::X25519) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let public_pkey = PKey::public_key_from_raw_bytes(public_key, Id::X25519) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let mut deriver = Deriver::new(&private_pkey) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + deriver + .set_peer(&public_pkey) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + deriver + .derive_to_vec() + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into()))) + } + + fn aes_encrypt( + &self, + mode: AesMode, + key: &[u8], + iv: &[u8], + data: &[u8], + additional_data: Option<&[u8]>, + ) -> Result, CryptoError> { + match mode { + AesMode::Cbc => { + let cipher = match key.len() { + 16 => Cipher::aes_128_cbc(), + 24 => Cipher::aes_192_cbc(), + 32 => Cipher::aes_256_cbc(), + _ => { + return Err(CryptoError::InvalidKey(Some( + "Invalid AES key length".into(), + ))) + } + }; + symm::encrypt(cipher, key, Some(iv), data) + .map_err(|e| CryptoError::EncryptionFailed(Some(e.to_string().into()))) + } + AesMode::Ctr { .. } => { + let cipher = match key.len() { + 16 => Cipher::aes_128_ctr(), + 24 => Cipher::aes_192_ctr(), + 32 => Cipher::aes_256_ctr(), + _ => { + return Err(CryptoError::InvalidKey(Some( + "Invalid AES key length".into(), + ))) + } + }; + symm::encrypt(cipher, key, Some(iv), data) + .map_err(|e| CryptoError::EncryptionFailed(Some(e.to_string().into()))) + } + AesMode::Gcm { tag_length } => { + let cipher = match key.len() { + 16 => Cipher::aes_128_gcm(), + 24 => Cipher::aes_192_gcm(), + 32 => Cipher::aes_256_gcm(), + _ => { + return Err(CryptoError::InvalidKey(Some( + "Invalid AES key length".into(), + ))) + } + }; + let tag_len = (tag_length / 8) as usize; + let mut tag = vec![0u8; tag_len]; + let ciphertext = symm::encrypt_aead( + cipher, + key, + Some(iv), + additional_data.unwrap_or(&[]), + data, + &mut tag, + ) + .map_err(|e| CryptoError::EncryptionFailed(Some(e.to_string().into())))?; + let mut result = ciphertext; + result.extend_from_slice(&tag); + Ok(result) + } + } + } + + fn aes_decrypt( + &self, + mode: AesMode, + key: &[u8], + iv: &[u8], + data: &[u8], + additional_data: Option<&[u8]>, + ) -> Result, CryptoError> { + match mode { + AesMode::Cbc => { + let cipher = match key.len() { + 16 => Cipher::aes_128_cbc(), + 24 => Cipher::aes_192_cbc(), + 32 => Cipher::aes_256_cbc(), + _ => { + return Err(CryptoError::InvalidKey(Some( + "Invalid AES key length".into(), + ))) + } + }; + symm::decrypt(cipher, key, Some(iv), data) + .map_err(|e| CryptoError::DecryptionFailed(Some(e.to_string().into()))) + } + AesMode::Ctr { .. } => { + let cipher = match key.len() { + 16 => Cipher::aes_128_ctr(), + 24 => Cipher::aes_192_ctr(), + 32 => Cipher::aes_256_ctr(), + _ => { + return Err(CryptoError::InvalidKey(Some( + "Invalid AES key length".into(), + ))) + } + }; + symm::decrypt(cipher, key, Some(iv), data) + .map_err(|e| CryptoError::DecryptionFailed(Some(e.to_string().into()))) + } + AesMode::Gcm { tag_length } => { + let cipher = match key.len() { + 16 => Cipher::aes_128_gcm(), + 24 => Cipher::aes_192_gcm(), + 32 => Cipher::aes_256_gcm(), + _ => { + return Err(CryptoError::InvalidKey(Some( + "Invalid AES key length".into(), + ))) + } + }; + let tag_len = (tag_length / 8) as usize; + if data.len() < tag_len { + return Err(CryptoError::InvalidData(Some( + "Data too short for GCM tag".into(), + ))); + } + let (ciphertext, tag) = data.split_at(data.len() - tag_len); + symm::decrypt_aead( + cipher, + key, + Some(iv), + additional_data.unwrap_or(&[]), + ciphertext, + tag, + ) + .map_err(|e| CryptoError::DecryptionFailed(Some(e.to_string().into()))) + } + } + } + + fn aes_kw_wrap(&self, kek: &[u8], key: &[u8]) -> Result, CryptoError> { + use openssl::aes::{wrap_key, AesKey}; + let aes_key = AesKey::new_encrypt(kek).map_err(|_| CryptoError::InvalidKey(None))?; + let mut out = vec![0u8; key.len() + 8]; + wrap_key(&aes_key, None, &mut out, key).map_err(|_| CryptoError::OperationFailed(None))?; + Ok(out) + } + + fn aes_kw_unwrap(&self, kek: &[u8], wrapped_key: &[u8]) -> Result, CryptoError> { + use openssl::aes::{unwrap_key, AesKey}; + let aes_key = AesKey::new_decrypt(kek).map_err(|_| CryptoError::InvalidKey(None))?; + let mut out = vec![0u8; wrapped_key.len() - 8]; + unwrap_key(&aes_key, None, &mut out, wrapped_key) + .map_err(|_| CryptoError::OperationFailed(None))?; + Ok(out) + } + + fn hkdf_derive_key( + &self, + key: &[u8], + salt: &[u8], + info: &[u8], + length: usize, + hash_alg: HashAlgorithm, + ) -> Result, CryptoError> { + use openssl::pkey_ctx::HkdfMode; + let md = get_md(hash_alg); + let mut ctx = PkeyCtx::new_id(Id::HKDF) + .map_err(|e| CryptoError::DerivationFailed(Some(e.to_string().into())))?; + ctx.derive_init() + .map_err(|e| CryptoError::DerivationFailed(Some(e.to_string().into())))?; + ctx.set_hkdf_md(md) + .map_err(|e| CryptoError::DerivationFailed(Some(e.to_string().into())))?; + ctx.set_hkdf_mode(HkdfMode::EXTRACT_THEN_EXPAND) + .map_err(|e| CryptoError::DerivationFailed(Some(e.to_string().into())))?; + ctx.set_hkdf_key(key) + .map_err(|e| CryptoError::DerivationFailed(Some(e.to_string().into())))?; + if !salt.is_empty() { + ctx.set_hkdf_salt(salt) + .map_err(|e| CryptoError::DerivationFailed(Some(e.to_string().into())))?; + } + if !info.is_empty() { + ctx.add_hkdf_info(info) + .map_err(|e| CryptoError::DerivationFailed(Some(e.to_string().into())))?; + } + let mut out = vec![0u8; length]; + ctx.derive(Some(&mut out)) + .map_err(|e| CryptoError::DerivationFailed(Some(e.to_string().into())))?; + Ok(out) + } + + fn pbkdf2_derive_key( + &self, + password: &[u8], + salt: &[u8], + iterations: u32, + length: usize, + hash_alg: HashAlgorithm, + ) -> Result, CryptoError> { + let md = get_message_digest(hash_alg); + let mut out = vec![0u8; length]; + openssl::pkcs5::pbkdf2_hmac(password, salt, iterations as usize, md, &mut out) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + Ok(out) + } + + fn generate_aes_key(&self, length_bits: u16) -> Result, CryptoError> { + let length_bytes = (length_bits / 8) as usize; + let mut key = vec![0u8; length_bytes]; + rand_bytes(&mut key) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + Ok(key) + } + + fn generate_hmac_key( + &self, + hash_alg: HashAlgorithm, + length_bits: u16, + ) -> Result, CryptoError> { + let length_bytes = if length_bits == 0 { + match hash_alg { + HashAlgorithm::Md5 => 16, + HashAlgorithm::Sha1 => 20, + HashAlgorithm::Sha256 => 32, + HashAlgorithm::Sha384 => 48, + HashAlgorithm::Sha512 => 64, + } + } else { + (length_bits / 8) as usize + }; + let mut key = vec![0u8; length_bytes]; + rand_bytes(&mut key) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + Ok(key) + } + + fn generate_ec_key(&self, curve: EllipticCurve) -> Result<(Vec, Vec), CryptoError> { + let group = get_ec_group(curve)?; + let ec_key = EcKey::generate(&group) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + let pkey = PKey::from_ec_key(ec_key.clone()) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + // Return PKCS#8 DER for private key (consistent with RustCrypto) + let private_der = pkey + .private_key_to_der() + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + // Return SEC1 uncompressed point for public key (consistent with RustCrypto) + let mut bn_ctx = openssl::bn::BigNumContext::new() + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + let public_sec1 = ec_key + .public_key() + .to_bytes( + &group, + openssl::ec::PointConversionForm::UNCOMPRESSED, + &mut bn_ctx, + ) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + Ok((private_der, public_sec1)) + } + + fn generate_ed25519_key(&self) -> Result<(Vec, Vec), CryptoError> { + let pkey = PKey::generate_ed25519() + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + let private_der = pkey + .private_key_to_der() + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + let public_raw = pkey + .raw_public_key() + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + Ok((private_der, public_raw)) + } + + fn generate_x25519_key(&self) -> Result<(Vec, Vec), CryptoError> { + let pkey = PKey::generate_x25519() + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + let private_raw = pkey + .raw_private_key() + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + let public_raw = pkey + .raw_public_key() + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + Ok((private_raw, public_raw)) + } + + fn generate_rsa_key( + &self, + modulus_length: u32, + public_exponent: &[u8], + ) -> Result<(Vec, Vec), CryptoError> { + let exp = BigNum::from_slice(public_exponent) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let rsa = Rsa::generate_with_e(modulus_length, &exp) + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + let private_der = rsa + .private_key_to_der() + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + let public_der = rsa + .public_key_to_der() + .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; + Ok((private_der, public_der)) + } + + fn import_rsa_public_key_pkcs1( + &self, + der: &[u8], + ) -> Result { + let rsa = Rsa::public_key_from_der(der) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let modulus_length = rsa.n().num_bits() as u32; + let public_exponent = rsa.e().to_vec(); + let key_data = rsa + .public_key_to_der() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + Ok(super::RsaImportResult { + key_data, + modulus_length, + public_exponent, + is_private: false, + }) + } + + fn import_rsa_private_key_pkcs1( + &self, + der: &[u8], + ) -> Result { + let rsa = Rsa::private_key_from_der(der) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let modulus_length = rsa.n().num_bits() as u32; + let public_exponent = rsa.e().to_vec(); + let key_data = rsa + .private_key_to_der() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + Ok(super::RsaImportResult { + key_data, + modulus_length, + public_exponent, + is_private: true, + }) + } + + fn import_rsa_public_key_spki( + &self, + der: &[u8], + ) -> Result { + let pkey = PKey::public_key_from_der(der) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let rsa = pkey + .rsa() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let modulus_length = rsa.n().num_bits() as u32; + let public_exponent = rsa.e().to_vec(); + let key_data = rsa + .public_key_to_der() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + Ok(super::RsaImportResult { + key_data, + modulus_length, + public_exponent, + is_private: false, + }) + } + + fn import_rsa_private_key_pkcs8( + &self, + der: &[u8], + ) -> Result { + let pkey = PKey::private_key_from_der(der) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let rsa = pkey + .rsa() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let modulus_length = rsa.n().num_bits() as u32; + let public_exponent = rsa.e().to_vec(); + let key_data = rsa + .private_key_to_der() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + Ok(super::RsaImportResult { + key_data, + modulus_length, + public_exponent, + is_private: true, + }) + } + + fn export_rsa_public_key_pkcs1(&self, key_data: &[u8]) -> Result, CryptoError> { + let rsa = Rsa::public_key_from_der(key_data) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + rsa.public_key_to_der() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) + } + + fn export_rsa_public_key_spki(&self, key_data: &[u8]) -> Result, CryptoError> { + let rsa = Rsa::public_key_from_der(key_data) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let pkey = + PKey::from_rsa(rsa).map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + pkey.public_key_to_der() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) + } + + fn export_rsa_private_key_pkcs8(&self, key_data: &[u8]) -> Result, CryptoError> { + let rsa = Rsa::private_key_from_der(key_data) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let pkey = + PKey::from_rsa(rsa).map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + pkey.private_key_to_der() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) + } + + fn import_ec_public_key_sec1( + &self, + data: &[u8], + curve: EllipticCurve, + ) -> Result { + let nid = curve_to_nid(curve); + let group = EcGroup::from_curve_name(nid) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let mut ctx = openssl::bn::BigNumContext::new() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let point = openssl::ec::EcPoint::from_bytes(&group, data, &mut ctx) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let ec_key = EcKey::from_public_key(&group, &point) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let pkey = PKey::from_ec_key(ec_key) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + Ok(super::EcImportResult { + key_data: pkey + .public_key_to_der() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?, + is_private: false, + }) + } + + fn import_ec_public_key_spki( + &self, + der: &[u8], + _curve: EllipticCurve, + ) -> Result { + let pkey = PKey::public_key_from_der(der) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + Ok(super::EcImportResult { + key_data: pkey + .public_key_to_der() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?, + is_private: false, + }) + } + + fn import_ec_private_key_pkcs8( + &self, + der: &[u8], + ) -> Result { + let pkey = PKey::private_key_from_der(der) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + Ok(super::EcImportResult { + key_data: pkey + .private_key_to_der() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?, + is_private: true, + }) + } + + fn import_ec_private_key_sec1( + &self, + data: &[u8], + curve: EllipticCurve, + ) -> Result { + let nid = curve_to_nid(curve); + let group = EcGroup::from_curve_name(nid) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let bn = BigNum::from_slice(data) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let generator = group + .generator_opt() + .ok_or_else(|| CryptoError::InvalidKey(Some("EC group has no generator".into())))?; + let ec_key = EcKey::from_private_components(&group, &bn, generator) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let pkey = PKey::from_ec_key(ec_key) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + Ok(super::EcImportResult { + key_data: pkey + .private_key_to_der() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?, + is_private: true, + }) + } + + fn export_ec_public_key_sec1( + &self, + key_data: &[u8], + _curve: EllipticCurve, + is_private: bool, + ) -> Result, CryptoError> { + let mut ctx = openssl::bn::BigNumContext::new() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + if is_private { + let ec_key = PKey::private_key_from_der(key_data) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))? + .ec_key() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + ec_key + .public_key() + .to_bytes( + ec_key.group(), + openssl::ec::PointConversionForm::UNCOMPRESSED, + &mut ctx, + ) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) + } else { + let ec_key = PKey::public_key_from_der(key_data) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))? + .ec_key() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + ec_key + .public_key() + .to_bytes( + ec_key.group(), + openssl::ec::PointConversionForm::UNCOMPRESSED, + &mut ctx, + ) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) + } + } + + fn export_ec_public_key_spki( + &self, + key_data: &[u8], + _curve: EllipticCurve, + ) -> Result, CryptoError> { + let pkey = PKey::public_key_from_der(key_data) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + pkey.public_key_to_der() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) + } + + fn export_ec_private_key_pkcs8( + &self, + key_data: &[u8], + _curve: EllipticCurve, + ) -> Result, CryptoError> { + let pkey = PKey::private_key_from_der(key_data) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + pkey.private_key_to_der() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) + } + + fn import_okp_public_key_raw( + &self, + data: &[u8], + ) -> Result { + if data.len() != 32 { + return Err(CryptoError::InvalidKey(None)); + } + Ok(super::OkpImportResult { + key_data: data.to_vec(), + is_private: false, + }) + } + + fn import_okp_public_key_spki( + &self, + der: &[u8], + _expected_oid: &[u8], + ) -> Result { + let pkey = PKey::public_key_from_der(der) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let raw = pkey + .raw_public_key() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + Ok(super::OkpImportResult { + key_data: raw, + is_private: false, + }) + } + + fn import_okp_private_key_pkcs8( + &self, + der: &[u8], + _expected_oid: &[u8], + ) -> Result { + Ok(super::OkpImportResult { + key_data: der.to_vec(), + is_private: true, + }) + } + + fn export_okp_public_key_raw( + &self, + key_data: &[u8], + is_private: bool, + ) -> Result, CryptoError> { + if is_private { + let pkey = PKey::private_key_from_der(key_data) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + pkey.raw_public_key() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) + } else { + Ok(key_data.to_vec()) + } + } + + fn export_okp_public_key_spki( + &self, + key_data: &[u8], + _oid: &[u8], + ) -> Result, CryptoError> { + // key_data is raw public key, need to wrap in SPKI + let pkey = PKey::public_key_from_raw_bytes(key_data, Id::ED25519) + .or_else(|_| PKey::public_key_from_raw_bytes(key_data, Id::X25519)) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + pkey.public_key_to_der() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) + } + + fn export_okp_private_key_pkcs8( + &self, + key_data: &[u8], + _oid: &[u8], + ) -> Result, CryptoError> { + // key_data is already PKCS8 + Ok(key_data.to_vec()) + } + + fn import_rsa_jwk( + &self, + jwk: super::RsaJwkImport<'_>, + ) -> Result { + let n = BigNum::from_slice(jwk.n) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let e = BigNum::from_slice(jwk.e) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let modulus_length = n.num_bits() as u32; + let pub_exp_bytes = jwk.e.to_vec(); + + if let ( + Some(d_bytes), + Some(p_bytes), + Some(q_bytes), + Some(dp_bytes), + Some(dq_bytes), + Some(qi_bytes), + ) = (jwk.d, jwk.p, jwk.q, jwk.dp, jwk.dq, jwk.qi) + { + let d = BigNum::from_slice(d_bytes) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let p = BigNum::from_slice(p_bytes) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let q = BigNum::from_slice(q_bytes) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let dp = BigNum::from_slice(dp_bytes) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let dq = BigNum::from_slice(dq_bytes) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let qi = BigNum::from_slice(qi_bytes) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + + let rsa = Rsa::from_private_components(n, e, d, p, q, dp, dq, qi) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let pkey = PKey::from_rsa(rsa) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + Ok(super::RsaImportResult { + key_data: pkey + .private_key_to_der() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?, + modulus_length, + public_exponent: pub_exp_bytes, + is_private: true, + }) + } else { + let rsa = Rsa::from_public_components(n, e) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let pkey = PKey::from_rsa(rsa) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + Ok(super::RsaImportResult { + key_data: pkey + .public_key_to_der() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?, + modulus_length, + public_exponent: pub_exp_bytes, + is_private: false, + }) + } + } + + fn export_rsa_jwk( + &self, + key_data: &[u8], + is_private: bool, + ) -> Result { + if is_private { + let pkey = PKey::private_key_from_der(key_data) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let rsa = pkey + .rsa() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + Ok(super::RsaJwkExport { + n: rsa.n().to_vec(), + e: rsa.e().to_vec(), + d: Some(rsa.d().to_vec()), + p: rsa.p().map(|v| v.to_vec()), + q: rsa.q().map(|v| v.to_vec()), + dp: rsa.dmp1().map(|v| v.to_vec()), + dq: rsa.dmq1().map(|v| v.to_vec()), + qi: rsa.iqmp().map(|v| v.to_vec()), + }) + } else { + let pkey = PKey::public_key_from_der(key_data) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let rsa = pkey + .rsa() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + Ok(super::RsaJwkExport { + n: rsa.n().to_vec(), + e: rsa.e().to_vec(), + d: None, + p: None, + q: None, + dp: None, + dq: None, + qi: None, + }) + } + } + + fn import_ec_jwk( + &self, + jwk: super::EcJwkImport<'_>, + curve: EllipticCurve, + ) -> Result { + let nid = curve_to_nid(curve); + let group = EcGroup::from_curve_name(nid) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let x = BigNum::from_slice(jwk.x) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let y = BigNum::from_slice(jwk.y) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let pub_key = EcKey::from_public_key_affine_coordinates(&group, &x, &y) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + + if let Some(d_bytes) = jwk.d { + let d = BigNum::from_slice(d_bytes) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let priv_key = EcKey::from_private_components(&group, &d, pub_key.public_key()) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let pkey = PKey::from_ec_key(priv_key) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + Ok(super::EcImportResult { + key_data: pkey + .private_key_to_der() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?, + is_private: true, + }) + } else { + // Return SEC1 uncompressed point for public key (consistent with generate_ec_key) + let mut ctx = openssl::bn::BigNumContext::new() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let sec1 = pub_key + .public_key() + .to_bytes( + &group, + openssl::ec::PointConversionForm::UNCOMPRESSED, + &mut ctx, + ) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + Ok(super::EcImportResult { + key_data: sec1, + is_private: false, + }) + } + } + + fn export_ec_jwk( + &self, + key_data: &[u8], + curve: EllipticCurve, + is_private: bool, + ) -> Result { + let nid = curve_to_nid(curve); + let group = EcGroup::from_curve_name(nid) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let mut ctx = openssl::bn::BigNumContext::new() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + + if is_private { + let pkey = PKey::private_key_from_der(key_data) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let ec_key = pkey + .ec_key() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let mut x = + BigNum::new().map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let mut y = + BigNum::new().map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + ec_key + .public_key() + .affine_coordinates(&group, &mut x, &mut y, &mut ctx) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + Ok(super::EcJwkExport { + x: x.to_vec(), + y: y.to_vec(), + d: Some(ec_key.private_key().to_vec()), + }) + } else { + // key_data is SEC1 uncompressed point (0x04 || x || y) + let coord_len = match curve { + EllipticCurve::P256 => 32, + EllipticCurve::P384 => 48, + EllipticCurve::P521 => 66, + }; + if key_data.len() != 1 + 2 * coord_len || key_data[0] != 0x04 { + return Err(CryptoError::InvalidKey(None)); + } + let x = key_data[1..1 + coord_len].to_vec(); + let y = key_data[1 + coord_len..].to_vec(); + Ok(super::EcJwkExport { x, y, d: None }) + } + } + + fn import_okp_jwk( + &self, + jwk: super::OkpJwkImport<'_>, + is_ed25519: bool, + ) -> Result { + let id = if is_ed25519 { Id::ED25519 } else { Id::X25519 }; + if let Some(d) = jwk.d { + // Private key + let pkey = PKey::private_key_from_raw_bytes(d, id) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + if is_ed25519 { + // Ed25519: return PKCS8 DER + Ok(super::OkpImportResult { + key_data: pkey + .private_key_to_der() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?, + is_private: true, + }) + } else { + // X25519: return raw bytes + Ok(super::OkpImportResult { + key_data: d.to_vec(), + is_private: true, + }) + } + } else { + // Public key - store raw bytes + Ok(super::OkpImportResult { + key_data: jwk.x.to_vec(), + is_private: false, + }) + } + } + + fn export_okp_jwk( + &self, + key_data: &[u8], + is_private: bool, + is_ed25519: bool, + ) -> Result { + if is_private { + if is_ed25519 { + // Ed25519: key_data is PKCS8 DER + let pkey = PKey::private_key_from_der(key_data) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let d = pkey + .raw_private_key() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let x = pkey + .raw_public_key() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + Ok(super::OkpJwkExport { x, d: Some(d) }) + } else { + // X25519: key_data is raw 32-byte secret + let pkey = PKey::private_key_from_raw_bytes(key_data, Id::X25519) + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + let x = pkey + .raw_public_key() + .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; + Ok(super::OkpJwkExport { + x, + d: Some(key_data.to_vec()), + }) + } + } else { + // Public key - key_data is raw bytes + Ok(super::OkpJwkExport { + x: key_data.to_vec(), + d: None, + }) + } + } +} diff --git a/stdlib/src/llrt/llrt_crypto/provider/ring.rs b/stdlib/src/llrt/llrt_crypto/provider/ring.rs new file mode 100644 index 00000000..c1e58227 --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/provider/ring.rs @@ -0,0 +1,544 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use crate::llrt_crypto::hash::HashAlgorithm; +use crate::llrt_crypto::provider::{ + AesMode, CryptoError, CryptoProvider, HmacProvider, SimpleDigest, +}; +use crate::llrt_crypto::subtle::EllipticCurve; +use md5::{Digest, Md5 as Md5Hasher}; +use ring::{digest, hmac}; + +pub struct RingProvider; + +pub enum RingDigestType { + Sha1(RingDigest), + Sha256(RingDigest), + Sha384(RingDigest), + Sha512(RingDigest), + Md5(RingMd5), +} + +pub enum RingHmacType { + Sha1(RingHmacSha1), + Sha256(RingHmacSha256), + Sha384(RingHmacSha384), + Sha512(RingHmacSha512), +} + +impl SimpleDigest for RingDigestType { + fn update(&mut self, data: &[u8]) { + match self { + RingDigestType::Sha1(d) => d.update(data), + RingDigestType::Sha256(d) => d.update(data), + RingDigestType::Sha384(d) => d.update(data), + RingDigestType::Sha512(d) => d.update(data), + RingDigestType::Md5(d) => d.update(data), + } + } + + fn finalize(self) -> Vec { + match self { + RingDigestType::Sha1(d) => d.finalize(), + RingDigestType::Sha256(d) => d.finalize(), + RingDigestType::Sha384(d) => d.finalize(), + RingDigestType::Sha512(d) => d.finalize(), + RingDigestType::Md5(d) => d.finalize(), + } + } +} + +impl HmacProvider for RingHmacType { + fn update(&mut self, data: &[u8]) { + match self { + RingHmacType::Sha1(h) => h.update(data), + RingHmacType::Sha256(h) => h.update(data), + RingHmacType::Sha384(h) => h.update(data), + RingHmacType::Sha512(h) => h.update(data), + } + } + + fn finalize(self) -> Vec { + match self { + RingHmacType::Sha1(h) => h.finalize(), + RingHmacType::Sha256(h) => h.finalize(), + RingHmacType::Sha384(h) => h.finalize(), + RingHmacType::Sha512(h) => h.finalize(), + } + } +} + +// Simple wrapper for Ring digest +pub struct RingDigest { + algorithm: &'static digest::Algorithm, + data: Vec, +} + +impl RingDigest { + fn new(algorithm: &'static digest::Algorithm) -> Self { + Self { + algorithm, + data: Vec::new(), + } + } +} + +impl SimpleDigest for RingDigest { + fn update(&mut self, data: &[u8]) { + self.data.extend_from_slice(data); + } + + fn finalize(self) -> Vec { + digest::digest(self.algorithm, &self.data).as_ref().to_vec() + } +} + +// MD5 wrapper +pub struct RingMd5(Md5Hasher); + +impl SimpleDigest for RingMd5 { + fn update(&mut self, data: &[u8]) { + Digest::update(&mut self.0, data); + } + + fn finalize(self) -> Vec { + self.0.finalize().to_vec() + } +} + +// HMAC implementations +pub struct RingHmacSha1(hmac::Context); +pub struct RingHmacSha256(hmac::Context); +pub struct RingHmacSha384(hmac::Context); +pub struct RingHmacSha512(hmac::Context); + +impl HmacProvider for RingHmacSha1 { + fn update(&mut self, data: &[u8]) { + self.0.update(data); + } + fn finalize(self) -> Vec { + self.0.sign().as_ref().to_vec() + } +} +impl HmacProvider for RingHmacSha256 { + fn update(&mut self, data: &[u8]) { + self.0.update(data); + } + fn finalize(self) -> Vec { + self.0.sign().as_ref().to_vec() + } +} +impl HmacProvider for RingHmacSha384 { + fn update(&mut self, data: &[u8]) { + self.0.update(data); + } + fn finalize(self) -> Vec { + self.0.sign().as_ref().to_vec() + } +} +impl HmacProvider for RingHmacSha512 { + fn update(&mut self, data: &[u8]) { + self.0.update(data); + } + fn finalize(self) -> Vec { + self.0.sign().as_ref().to_vec() + } +} + +impl CryptoProvider for RingProvider { + type Digest = RingDigestType; + type Hmac = RingHmacType; + + fn digest(&self, algorithm: HashAlgorithm) -> Self::Digest { + match algorithm { + HashAlgorithm::Md5 => RingDigestType::Md5(RingMd5(Md5Hasher::new())), + HashAlgorithm::Sha1 => { + RingDigestType::Sha1(RingDigest::new(&digest::SHA1_FOR_LEGACY_USE_ONLY)) + } + HashAlgorithm::Sha256 => RingDigestType::Sha256(RingDigest::new(&digest::SHA256)), + HashAlgorithm::Sha384 => RingDigestType::Sha384(RingDigest::new(&digest::SHA384)), + HashAlgorithm::Sha512 => RingDigestType::Sha512(RingDigest::new(&digest::SHA512)), + } + } + + fn hmac(&self, algorithm: HashAlgorithm, key: &[u8]) -> Self::Hmac { + match algorithm { + HashAlgorithm::Md5 => { + panic!("HMAC-MD5 not supported by Ring provider"); + } + HashAlgorithm::Sha1 => RingHmacType::Sha1(RingHmacSha1(hmac::Context::with_key( + &hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, key), + ))), + HashAlgorithm::Sha256 => RingHmacType::Sha256(RingHmacSha256(hmac::Context::with_key( + &hmac::Key::new(hmac::HMAC_SHA256, key), + ))), + HashAlgorithm::Sha384 => RingHmacType::Sha384(RingHmacSha384(hmac::Context::with_key( + &hmac::Key::new(hmac::HMAC_SHA384, key), + ))), + HashAlgorithm::Sha512 => RingHmacType::Sha512(RingHmacSha512(hmac::Context::with_key( + &hmac::Key::new(hmac::HMAC_SHA512, key), + ))), + } + } + + fn ecdsa_sign( + &self, + _curve: EllipticCurve, + _private_key_der: &[u8], + _digest: &[u8], + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn ecdsa_verify( + &self, + _curve: EllipticCurve, + _public_key_sec1: &[u8], + _signature: &[u8], + _digest: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn ed25519_sign(&self, _private_key_der: &[u8], _data: &[u8]) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn ed25519_verify( + &self, + _public_key_bytes: &[u8], + _signature: &[u8], + _data: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn rsa_pss_sign( + &self, + _private_key_der: &[u8], + _digest: &[u8], + _salt_length: usize, + _hash_alg: HashAlgorithm, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn rsa_pss_verify( + &self, + _public_key_der: &[u8], + _signature: &[u8], + _digest: &[u8], + _salt_length: usize, + _hash_alg: HashAlgorithm, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn rsa_pkcs1v15_sign( + &self, + _private_key_der: &[u8], + _digest: &[u8], + _hash_alg: HashAlgorithm, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn rsa_pkcs1v15_verify( + &self, + _public_key_der: &[u8], + _signature: &[u8], + _digest: &[u8], + _hash_alg: HashAlgorithm, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn rsa_oaep_encrypt( + &self, + _public_key_der: &[u8], + _data: &[u8], + _hash_alg: HashAlgorithm, + _label: Option<&[u8]>, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn rsa_oaep_decrypt( + &self, + _private_key_der: &[u8], + _data: &[u8], + _hash_alg: HashAlgorithm, + _label: Option<&[u8]>, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn ecdh_derive_bits( + &self, + _curve: EllipticCurve, + _private_key_der: &[u8], + _public_key_sec1: &[u8], + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn x25519_derive_bits( + &self, + _private_key: &[u8], + _public_key: &[u8], + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn aes_encrypt( + &self, + _mode: AesMode, + _key: &[u8], + _iv: &[u8], + _data: &[u8], + _additional_data: Option<&[u8]>, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn aes_decrypt( + &self, + _mode: AesMode, + _key: &[u8], + _iv: &[u8], + _data: &[u8], + _additional_data: Option<&[u8]>, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn aes_kw_wrap(&self, _kek: &[u8], _key: &[u8]) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn aes_kw_unwrap(&self, _kek: &[u8], _wrapped_key: &[u8]) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn hkdf_derive_key( + &self, + _key: &[u8], + _salt: &[u8], + _info: &[u8], + _length: usize, + _hash_alg: HashAlgorithm, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn pbkdf2_derive_key( + &self, + _password: &[u8], + _salt: &[u8], + _iterations: u32, + _length: usize, + _hash_alg: HashAlgorithm, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn generate_aes_key(&self, _length_bits: u16) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn generate_hmac_key( + &self, + _hash_alg: HashAlgorithm, + _length_bits: u16, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn generate_ec_key(&self, _curve: EllipticCurve) -> Result<(Vec, Vec), CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn generate_ed25519_key(&self) -> Result<(Vec, Vec), CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn generate_x25519_key(&self) -> Result<(Vec, Vec), CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn generate_rsa_key( + &self, + _modulus_length: u32, + _public_exponent: &[u8], + ) -> Result<(Vec, Vec), CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + + fn import_rsa_public_key_pkcs1( + &self, + _der: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_rsa_private_key_pkcs1( + &self, + _der: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_rsa_public_key_spki( + &self, + _der: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_rsa_private_key_pkcs8( + &self, + _der: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_rsa_public_key_pkcs1(&self, _key_data: &[u8]) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_rsa_public_key_spki(&self, _key_data: &[u8]) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_rsa_private_key_pkcs8(&self, _key_data: &[u8]) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_ec_public_key_sec1( + &self, + _data: &[u8], + _curve: EllipticCurve, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_ec_public_key_spki( + &self, + _der: &[u8], + _curve: EllipticCurve, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_ec_private_key_pkcs8( + &self, + _der: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_ec_private_key_sec1( + &self, + _data: &[u8], + _curve: EllipticCurve, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_ec_public_key_sec1( + &self, + _key_data: &[u8], + _curve: EllipticCurve, + _is_private: bool, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_ec_public_key_spki( + &self, + _key_data: &[u8], + _curve: EllipticCurve, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_ec_private_key_pkcs8( + &self, + _key_data: &[u8], + _curve: EllipticCurve, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_okp_public_key_raw( + &self, + _data: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_okp_public_key_spki( + &self, + _der: &[u8], + _expected_oid: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_okp_private_key_pkcs8( + &self, + _der: &[u8], + _expected_oid: &[u8], + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_okp_public_key_raw( + &self, + _key_data: &[u8], + _is_private: bool, + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_okp_public_key_spki( + &self, + _key_data: &[u8], + _oid: &[u8], + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_okp_private_key_pkcs8( + &self, + _key_data: &[u8], + _oid: &[u8], + ) -> Result, CryptoError> { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_rsa_jwk( + &self, + _jwk: super::RsaJwkImport<'_>, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_rsa_jwk( + &self, + _key_data: &[u8], + _is_private: bool, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_ec_jwk( + &self, + _jwk: super::EcJwkImport<'_>, + _curve: EllipticCurve, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_ec_jwk( + &self, + _key_data: &[u8], + _curve: EllipticCurve, + _is_private: bool, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn import_okp_jwk( + &self, + _jwk: super::OkpJwkImport<'_>, + _is_ed25519: bool, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } + fn export_okp_jwk( + &self, + _key_data: &[u8], + _is_private: bool, + _is_ed25519: bool, + ) -> Result { + Err(CryptoError::UnsupportedAlgorithm) + } +} diff --git a/stdlib/src/llrt/llrt_crypto/provider/rust/aes_variants.rs b/stdlib/src/llrt/llrt_crypto/provider/rust/aes_variants.rs new file mode 100644 index 00000000..482957b7 --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/provider/rust/aes_variants.rs @@ -0,0 +1,285 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! AES cipher variant types for SubtleCrypto operations. +//! Only available when the `_rustcrypto` feature is enabled. + +use aes::cipher::BlockModeDecrypt; +use aes::cipher::BlockModeEncrypt; + +use aes::{ + cipher::{ + block_padding::{Error as PaddingError, Pkcs7}, + consts::{U12, U13, U14, U15, U16, U4, U8}, + InvalidLength, KeyIvInit, StreamCipher, StreamCipherError, + }, + Aes128, Aes192, Aes256, +}; +use aes_gcm::{ + aead::{Aead, Payload}, + AesGcm, KeyInit, Nonce, +}; +use ctr::{Ctr128BE, Ctr32BE, Ctr64BE}; + +#[allow(dead_code)] +pub enum AesCbcEncVariant { + Aes128(cbc::Encryptor), + Aes192(cbc::Encryptor), + Aes256(cbc::Encryptor), +} + +#[allow(dead_code)] +impl AesCbcEncVariant { + pub fn new(key_len: u16, key: &[u8], iv: &[u8]) -> std::result::Result { + let variant: AesCbcEncVariant = match key_len { + 128 => Self::Aes128(cbc::Encryptor::new_from_slices(key, iv)?), + 192 => Self::Aes192(cbc::Encryptor::new_from_slices(key, iv)?), + 256 => Self::Aes256(cbc::Encryptor::new_from_slices(key, iv)?), + _ => return Err(InvalidLength), + }; + + Ok(variant) + } + + pub fn encrypt(self, data: &[u8]) -> Vec { + match self { + Self::Aes128(v) => v.encrypt_padded_vec::(data), + Self::Aes192(v) => v.encrypt_padded_vec::(data), + Self::Aes256(v) => v.encrypt_padded_vec::(data), + } + } +} + +#[allow(dead_code)] +pub enum AesCbcDecVariant { + Aes128(cbc::Decryptor), + Aes192(cbc::Decryptor), + Aes256(cbc::Decryptor), +} + +#[allow(dead_code)] +impl AesCbcDecVariant { + pub fn new(key_len: u16, key: &[u8], iv: &[u8]) -> std::result::Result { + let variant: AesCbcDecVariant = match key_len { + 128 => Self::Aes128(cbc::Decryptor::new_from_slices(key, iv)?), + 192 => Self::Aes192(cbc::Decryptor::new_from_slices(key, iv)?), + 256 => Self::Aes256(cbc::Decryptor::new_from_slices(key, iv)?), + _ => return Err(InvalidLength), + }; + + Ok(variant) + } + + pub fn decrypt(self, data: &[u8]) -> std::result::Result, PaddingError> { + Ok(match self { + Self::Aes128(v) => v.decrypt_padded_vec::(data)?, + Self::Aes192(v) => v.decrypt_padded_vec::(data)?, + Self::Aes256(v) => v.decrypt_padded_vec::(data)?, + }) + } +} + +#[allow(dead_code)] +pub enum AesCtrVariant { + Aes128Ctr32(Ctr32BE), + Aes128Ctr64(Ctr64BE), + Aes128Ctr128(Ctr128BE), + Aes192Ctr32(Ctr32BE), + Aes192Ctr64(Ctr64BE), + Aes192Ctr128(Ctr128BE), + Aes256Ctr32(Ctr32BE), + Aes256Ctr64(Ctr64BE), + Aes256Ctr128(Ctr128BE), +} + +#[allow(dead_code)] +impl AesCtrVariant { + pub fn new( + key_len: u16, + encryption_length: u32, + key: &[u8], + counter: &[u8], + ) -> std::result::Result { + let variant: AesCtrVariant = match (key_len, encryption_length) { + (128, 32) => Self::Aes128Ctr32(Ctr32BE::new_from_slices(key, counter)?), + (128, 64) => Self::Aes128Ctr64(Ctr64BE::new_from_slices(key, counter)?), + (128, 128) => Self::Aes128Ctr128(Ctr128BE::new_from_slices(key, counter)?), + (192, 32) => Self::Aes192Ctr32(Ctr32BE::new_from_slices(key, counter)?), + (192, 64) => Self::Aes192Ctr64(Ctr64BE::new_from_slices(key, counter)?), + (192, 128) => Self::Aes192Ctr128(Ctr128BE::new_from_slices(key, counter)?), + (256, 32) => Self::Aes256Ctr32(Ctr32BE::new_from_slices(key, counter)?), + (256, 64) => Self::Aes256Ctr64(Ctr64BE::new_from_slices(key, counter)?), + (256, 128) => Self::Aes256Ctr128(Ctr128BE::new_from_slices(key, counter)?), + _ => return Err(InvalidLength), + }; + + Ok(variant) + } + + pub fn encrypt(&mut self, data: &[u8]) -> std::result::Result, StreamCipherError> { + let mut ciphertext = data.to_vec(); + match self { + Self::Aes128Ctr32(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes128Ctr64(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes128Ctr128(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes192Ctr32(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes192Ctr64(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes192Ctr128(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes256Ctr32(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes256Ctr64(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes256Ctr128(v) => v.try_apply_keystream(&mut ciphertext)?, + } + Ok(ciphertext) + } + + pub fn decrypt(&mut self, data: &[u8]) -> std::result::Result, StreamCipherError> { + let mut ciphertext = data.to_vec(); + match self { + Self::Aes128Ctr32(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes128Ctr64(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes128Ctr128(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes192Ctr32(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes192Ctr64(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes192Ctr128(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes256Ctr32(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes256Ctr64(v) => v.try_apply_keystream(&mut ciphertext)?, + Self::Aes256Ctr128(v) => v.try_apply_keystream(&mut ciphertext)?, + } + Ok(ciphertext) + } +} + +pub enum AesGcmVariant { + Aes128Gcm32(AesGcm), + Aes192Gcm32(AesGcm), + Aes256Gcm32(AesGcm), + Aes128Gcm64(AesGcm), + Aes192Gcm64(AesGcm), + Aes256Gcm64(AesGcm), + Aes128Gcm96(AesGcm), + Aes192Gcm96(AesGcm), + Aes256Gcm96(AesGcm), + Aes128Gcm104(AesGcm), + Aes192Gcm104(AesGcm), + Aes256Gcm104(AesGcm), + Aes128Gcm112(AesGcm), + Aes192Gcm112(AesGcm), + Aes256Gcm112(AesGcm), + Aes128Gcm120(AesGcm), + Aes192Gcm120(AesGcm), + Aes256Gcm120(AesGcm), + Aes128Gcm128(AesGcm), + Aes192Gcm128(AesGcm), + Aes256Gcm128(AesGcm), +} + +#[allow(dead_code)] +impl AesGcmVariant { + pub fn new( + key_len: u16, + tag_length: u8, + key: &[u8], + ) -> std::result::Result { + let variant = match (key_len, tag_length) { + (128, 32) => Self::Aes128Gcm32(AesGcm::new_from_slice(key)?), + (192, 32) => Self::Aes192Gcm32(AesGcm::new_from_slice(key)?), + (256, 32) => Self::Aes256Gcm32(AesGcm::new_from_slice(key)?), + (128, 64) => Self::Aes128Gcm64(AesGcm::new_from_slice(key)?), + (192, 64) => Self::Aes192Gcm64(AesGcm::new_from_slice(key)?), + (256, 64) => Self::Aes256Gcm64(AesGcm::new_from_slice(key)?), + (128, 96) => Self::Aes128Gcm96(AesGcm::new_from_slice(key)?), + (192, 96) => Self::Aes192Gcm96(AesGcm::new_from_slice(key)?), + (256, 96) => Self::Aes256Gcm96(AesGcm::new_from_slice(key)?), + (128, 104) => Self::Aes128Gcm104(AesGcm::new_from_slice(key)?), + (192, 104) => Self::Aes192Gcm104(AesGcm::new_from_slice(key)?), + (256, 104) => Self::Aes256Gcm104(AesGcm::new_from_slice(key)?), + (128, 112) => Self::Aes128Gcm112(AesGcm::new_from_slice(key)?), + (192, 112) => Self::Aes192Gcm112(AesGcm::new_from_slice(key)?), + (256, 112) => Self::Aes256Gcm112(AesGcm::new_from_slice(key)?), + (128, 120) => Self::Aes128Gcm120(AesGcm::new_from_slice(key)?), + (192, 120) => Self::Aes192Gcm120(AesGcm::new_from_slice(key)?), + (256, 120) => Self::Aes256Gcm120(AesGcm::new_from_slice(key)?), + (128, 128) => Self::Aes128Gcm128(AesGcm::new_from_slice(key)?), + (192, 128) => Self::Aes192Gcm128(AesGcm::new_from_slice(key)?), + (256, 128) => Self::Aes256Gcm128(AesGcm::new_from_slice(key)?), + _ => return Err(InvalidLength), + }; + + Ok(variant) + } + + pub fn encrypt( + &self, + nonce: &[u8], + msg: &[u8], + aad: Option<&[u8]>, + ) -> std::result::Result, aes_gcm::Error> { + let plaintext: Payload = Payload { + msg, + aad: aad.unwrap_or_default(), + }; + let nonce: &ctr::cipher::Array<_, _> = + &Nonce::::try_from(nonce).map_err(|_| aes_gcm::Error)?; + match self { + Self::Aes128Gcm32(v) => v.encrypt(nonce, plaintext), + Self::Aes192Gcm32(v) => v.encrypt(nonce, plaintext), + Self::Aes256Gcm32(v) => v.encrypt(nonce, plaintext), + Self::Aes128Gcm64(v) => v.encrypt(nonce, plaintext), + Self::Aes192Gcm64(v) => v.encrypt(nonce, plaintext), + Self::Aes256Gcm64(v) => v.encrypt(nonce, plaintext), + Self::Aes128Gcm96(v) => v.encrypt(nonce, plaintext), + Self::Aes192Gcm96(v) => v.encrypt(nonce, plaintext), + Self::Aes256Gcm96(v) => v.encrypt(nonce, plaintext), + Self::Aes128Gcm104(v) => v.encrypt(nonce, plaintext), + Self::Aes192Gcm104(v) => v.encrypt(nonce, plaintext), + Self::Aes256Gcm104(v) => v.encrypt(nonce, plaintext), + Self::Aes128Gcm112(v) => v.encrypt(nonce, plaintext), + Self::Aes192Gcm112(v) => v.encrypt(nonce, plaintext), + Self::Aes256Gcm112(v) => v.encrypt(nonce, plaintext), + Self::Aes128Gcm120(v) => v.encrypt(nonce, plaintext), + Self::Aes192Gcm120(v) => v.encrypt(nonce, plaintext), + Self::Aes256Gcm120(v) => v.encrypt(nonce, plaintext), + Self::Aes128Gcm128(v) => v.encrypt(nonce, plaintext), + Self::Aes192Gcm128(v) => v.encrypt(nonce, plaintext), + Self::Aes256Gcm128(v) => v.encrypt(nonce, plaintext), + } + } + + pub fn decrypt( + &self, + nonce: &[u8], + msg: &[u8], + aad: Option<&[u8]>, + ) -> std::result::Result, aes_gcm::Error> { + let ciphertext: Payload = Payload { + msg, + aad: aad.unwrap_or_default(), + }; + let nonce: &ctr::cipher::Array<_, _> = + &Nonce::::try_from(nonce).map_err(|_| aes_gcm::Error)?; + match self { + Self::Aes128Gcm32(v) => v.decrypt(nonce, ciphertext), + Self::Aes192Gcm32(v) => v.decrypt(nonce, ciphertext), + Self::Aes256Gcm32(v) => v.decrypt(nonce, ciphertext), + Self::Aes128Gcm64(v) => v.decrypt(nonce, ciphertext), + Self::Aes192Gcm64(v) => v.decrypt(nonce, ciphertext), + Self::Aes256Gcm64(v) => v.decrypt(nonce, ciphertext), + Self::Aes128Gcm96(v) => v.decrypt(nonce, ciphertext), + Self::Aes192Gcm96(v) => v.decrypt(nonce, ciphertext), + Self::Aes256Gcm96(v) => v.decrypt(nonce, ciphertext), + Self::Aes128Gcm104(v) => v.decrypt(nonce, ciphertext), + Self::Aes192Gcm104(v) => v.decrypt(nonce, ciphertext), + Self::Aes256Gcm104(v) => v.decrypt(nonce, ciphertext), + Self::Aes128Gcm112(v) => v.decrypt(nonce, ciphertext), + Self::Aes192Gcm112(v) => v.decrypt(nonce, ciphertext), + Self::Aes256Gcm112(v) => v.decrypt(nonce, ciphertext), + Self::Aes128Gcm120(v) => v.decrypt(nonce, ciphertext), + Self::Aes192Gcm120(v) => v.decrypt(nonce, ciphertext), + Self::Aes256Gcm120(v) => v.decrypt(nonce, ciphertext), + Self::Aes128Gcm128(v) => v.decrypt(nonce, ciphertext), + Self::Aes192Gcm128(v) => v.decrypt(nonce, ciphertext), + Self::Aes256Gcm128(v) => v.decrypt(nonce, ciphertext), + } + } +} diff --git a/stdlib/src/llrt/llrt_crypto/provider/rust/mod.rs b/stdlib/src/llrt/llrt_crypto/provider/rust/mod.rs new file mode 100644 index 00000000..8cfce0be --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/provider/rust/mod.rs @@ -0,0 +1,1654 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +mod aes_variants; + +use std::num::NonZeroU32; + +use aes::cipher::{ + block_padding::Pkcs7, BlockModeDecrypt, BlockModeEncrypt, KeyIvInit, StreamCipher, + StreamCipherError, +}; +use aes_gcm::{ + aead::{Aead, Payload}, + KeyInit, Nonce, +}; +use aes_kw::{KwAes128, KwAes192, KwAes256}; +use cbc::{Decryptor, Encryptor}; +use ctr::{cipher::Array, Ctr128BE, Ctr32BE, Ctr64BE}; +use der::{ + asn1::{BitStringRef, OctetString, OctetStringRef}, + Decode, Encode, +}; +use ecdsa::signature::hazmat::PrehashVerifier; +use ed25519_dalek::{Signature, Signer, VerifyingKey}; +use elliptic_curve::{consts::U12, sec1::ToSec1Point, Generate}; +use hkdf::Hkdf; +use hmac::{Hmac as HmacImpl, Mac}; +use p256::{ + ecdsa::{ + Signature as P256Signature, SigningKey as P256SigningKey, VerifyingKey as P256VerifyingKey, + }, + SecretKey as P256SecretKey, +}; +use p384::{ + ecdsa::{ + Signature as P384Signature, SigningKey as P384SigningKey, VerifyingKey as P384VerifyingKey, + }, + SecretKey as P384SecretKey, +}; +use p521::{ + ecdsa::{ + Signature as P521Signature, SigningKey as P521SigningKey, VerifyingKey as P521VerifyingKey, + }, + SecretKey as P521SecretKey, +}; +use pbkdf2::pbkdf2; +use pkcs8::{DecodePrivateKey, EncodePrivateKey}; +use rsa::pkcs1::{ + DecodeRsaPrivateKey, DecodeRsaPublicKey, EncodeRsaPrivateKey, EncodeRsaPublicKey, +}; +use rsa::signature::hazmat::PrehashSigner; +use rsa::{ + pss::Pss, + sha2::{Digest, Sha256, Sha384, Sha512}, + BoxedUint, Oaep, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey, +}; +use sha1::Sha1; + +use crate::llrt_crypto::{ + hash::HashAlgorithm, + provider::{ + parse_rsa_public_exponent, AesMode, CryptoError, CryptoProvider, HmacProvider, SimpleDigest, + }, + random_byte_array, + subtle::EllipticCurve, +}; + +use aes_variants::AesGcmVariant; + +impl From for CryptoError { + fn from(_: aes::cipher::InvalidLength) -> Self { + CryptoError::InvalidLength + } +} + +impl From for CryptoError { + fn from(_: StreamCipherError) -> Self { + CryptoError::OperationFailed(None) + } +} + +// Digest implementation using sha2/md5 crates +pub enum RustDigest { + Md5(md5::Md5), + Sha1(Sha1), + Sha256(Sha256), + Sha384(Sha384), + Sha512(Sha512), +} + +impl SimpleDigest for RustDigest { + fn update(&mut self, data: &[u8]) { + match self { + RustDigest::Md5(h) => Digest::update(h, data), + RustDigest::Sha1(h) => Digest::update(h, data), + RustDigest::Sha256(h) => Digest::update(h, data), + RustDigest::Sha384(h) => Digest::update(h, data), + RustDigest::Sha512(h) => Digest::update(h, data), + } + } + + fn finalize(self) -> Vec { + match self { + RustDigest::Md5(h) => h.finalize().to_vec(), + RustDigest::Sha1(h) => h.finalize().to_vec(), + RustDigest::Sha256(h) => h.finalize().to_vec(), + RustDigest::Sha384(h) => h.finalize().to_vec(), + RustDigest::Sha512(h) => h.finalize().to_vec(), + } + } +} + +// HMAC implementation using hmac crate +pub enum RustHmac { + Sha1(HmacImpl), + Sha256(HmacImpl), + Sha384(HmacImpl), + Sha512(HmacImpl), +} + +impl HmacProvider for RustHmac { + fn update(&mut self, data: &[u8]) { + match self { + RustHmac::Sha1(h) => Mac::update(h, data), + RustHmac::Sha256(h) => Mac::update(h, data), + RustHmac::Sha384(h) => Mac::update(h, data), + RustHmac::Sha512(h) => Mac::update(h, data), + } + } + + fn finalize(self) -> Vec { + match self { + RustHmac::Sha1(h) => h.finalize().into_bytes().to_vec(), + RustHmac::Sha256(h) => h.finalize().into_bytes().to_vec(), + RustHmac::Sha384(h) => h.finalize().into_bytes().to_vec(), + RustHmac::Sha512(h) => h.finalize().into_bytes().to_vec(), + } + } +} + +// Main Crypto Provider +#[derive(Default)] +pub struct RustCryptoProvider; + +impl CryptoProvider for RustCryptoProvider { + type Digest = RustDigest; + type Hmac = RustHmac; + + fn digest(&self, algorithm: HashAlgorithm) -> Self::Digest { + match algorithm { + HashAlgorithm::Md5 => RustDigest::Md5(md5::Md5::new()), + HashAlgorithm::Sha1 => RustDigest::Sha1(Sha1::new()), + HashAlgorithm::Sha256 => RustDigest::Sha256(Sha256::new()), + HashAlgorithm::Sha384 => RustDigest::Sha384(Sha384::new()), + HashAlgorithm::Sha512 => RustDigest::Sha512(Sha512::new()), + } + } + + fn hmac(&self, algorithm: HashAlgorithm, key: &[u8]) -> Self::Hmac { + match algorithm { + HashAlgorithm::Md5 => panic!("HMAC-MD5 not supported"), + HashAlgorithm::Sha1 => RustHmac::Sha1(HmacImpl::::new_from_slice(key).unwrap()), + HashAlgorithm::Sha256 => { + RustHmac::Sha256(HmacImpl::::new_from_slice(key).unwrap()) + } + HashAlgorithm::Sha384 => { + RustHmac::Sha384(HmacImpl::::new_from_slice(key).unwrap()) + } + HashAlgorithm::Sha512 => { + RustHmac::Sha512(HmacImpl::::new_from_slice(key).unwrap()) + } + } + } + + fn ecdsa_sign( + &self, + curve: EllipticCurve, + private_key_der: &[u8], + digest: &[u8], + ) -> Result, CryptoError> { + match curve { + EllipticCurve::P256 => { + let secret_key = P256SecretKey::from_pkcs8_der(private_key_der) + .map_err(|_| CryptoError::InvalidKey(None))?; + let signing_key = P256SigningKey::from(secret_key); + let signature: p256::ecdsa::Signature = signing_key + .sign_prehash(digest) + .map_err(|_| CryptoError::SigningFailed(None))?; + Ok(signature.to_bytes().to_vec()) + } + EllipticCurve::P384 => { + let secret_key = P384SecretKey::from_pkcs8_der(private_key_der) + .map_err(|_| CryptoError::InvalidKey(None))?; + let signing_key = P384SigningKey::from(secret_key); + let signature: p384::ecdsa::Signature = signing_key + .sign_prehash(digest) + .map_err(|_| CryptoError::SigningFailed(None))?; + Ok(signature.to_bytes().to_vec()) + } + EllipticCurve::P521 => { + let secret_key = P521SecretKey::from_pkcs8_der(private_key_der) + .map_err(|_| CryptoError::InvalidKey(None))?; + let signing_key = P521SigningKey::from(secret_key); + let signature: p521::ecdsa::Signature = signing_key + .sign_prehash(digest) + .map_err(|_| CryptoError::SigningFailed(None))?; + Ok(signature.to_bytes().to_vec()) + } + } + } + + fn ecdsa_verify( + &self, + curve: EllipticCurve, + public_key_sec1: &[u8], + signature: &[u8], + digest: &[u8], + ) -> Result { + match curve { + EllipticCurve::P256 => { + let verifying_key = P256VerifyingKey::from_sec1_bytes(public_key_sec1) + .map_err(|_| CryptoError::InvalidKey(None))?; + let sig = P256Signature::from_slice(signature) + .map_err(|_| CryptoError::InvalidSignature(None))?; + Ok(verifying_key.verify_prehash(digest, &sig).is_ok()) + } + EllipticCurve::P384 => { + let verifying_key = P384VerifyingKey::from_sec1_bytes(public_key_sec1) + .map_err(|_| CryptoError::InvalidKey(None))?; + let sig = P384Signature::from_slice(signature) + .map_err(|_| CryptoError::InvalidSignature(None))?; + Ok(verifying_key.verify_prehash(digest, &sig).is_ok()) + } + EllipticCurve::P521 => { + let verifying_key = P521VerifyingKey::from_sec1_bytes(public_key_sec1) + .map_err(|_| CryptoError::InvalidKey(None))?; + let sig = P521Signature::from_slice(signature) + .map_err(|_| CryptoError::InvalidSignature(None))?; + Ok(verifying_key.verify_prehash(digest, &sig).is_ok()) + } + } + } + + fn ed25519_sign(&self, private_key_der: &[u8], data: &[u8]) -> Result, CryptoError> { + let signing_key = ed25519_dalek::SigningKey::from_pkcs8_der(private_key_der) + .map_err(|_| CryptoError::InvalidKey(None))?; + let signature = signing_key + .try_sign(data) + .map_err(|_| CryptoError::InvalidSignature(None))?; + Ok(signature.to_bytes().to_vec()) + } + + fn ed25519_verify( + &self, + public_key_bytes: &[u8], + signature: &[u8], + data: &[u8], + ) -> Result { + let public_key = VerifyingKey::from_bytes( + public_key_bytes + .try_into() + .map_err(|_| CryptoError::InvalidKey(None))?, + ) + .map_err(|_| CryptoError::InvalidKey(None))?; + let signature = Signature::from_bytes( + signature + .try_into() + .map_err(|_| CryptoError::InvalidSignature(None))?, + ); + Ok(public_key.verify_strict(data, &signature).is_ok()) + } + + fn rsa_pss_sign( + &self, + private_key_der: &[u8], + digest: &[u8], + salt_length: usize, + hash_alg: HashAlgorithm, + ) -> Result, CryptoError> { + let mut rng = rand::rng(); + let private_key = RsaPrivateKey::from_pkcs1_der(private_key_der) + .map_err(|_| CryptoError::InvalidKey(None))?; + + match hash_alg { + HashAlgorithm::Sha1 => private_key + .sign_with_rng(&mut rng, Pss::::new_with_salt(salt_length), digest) + .map_err(|_| CryptoError::SigningFailed(None)), + HashAlgorithm::Sha256 => private_key + .sign_with_rng(&mut rng, Pss::::new_with_salt(salt_length), digest) + .map_err(|_| CryptoError::SigningFailed(None)), + HashAlgorithm::Sha384 => private_key + .sign_with_rng(&mut rng, Pss::::new_with_salt(salt_length), digest) + .map_err(|_| CryptoError::SigningFailed(None)), + HashAlgorithm::Sha512 => private_key + .sign_with_rng(&mut rng, Pss::::new_with_salt(salt_length), digest) + .map_err(|_| CryptoError::SigningFailed(None)), + _ => Err(CryptoError::UnsupportedAlgorithm), + } + } + + fn rsa_pss_verify( + &self, + public_key_der: &[u8], + signature: &[u8], + digest: &[u8], + salt_length: usize, + hash_alg: HashAlgorithm, + ) -> Result { + let public_key = RsaPublicKey::from_pkcs1_der(public_key_der) + .map_err(|_| CryptoError::InvalidKey(None))?; + + match hash_alg { + HashAlgorithm::Sha1 => Ok(public_key + .verify(Pss::::new_with_salt(salt_length), digest, signature) + .is_ok()), + HashAlgorithm::Sha256 => Ok(public_key + .verify(Pss::::new_with_salt(salt_length), digest, signature) + .is_ok()), + HashAlgorithm::Sha384 => Ok(public_key + .verify(Pss::::new_with_salt(salt_length), digest, signature) + .is_ok()), + HashAlgorithm::Sha512 => Ok(public_key + .verify(Pss::::new_with_salt(salt_length), digest, signature) + .is_ok()), + _ => Err(CryptoError::UnsupportedAlgorithm), + } + } + + fn rsa_pkcs1v15_sign( + &self, + private_key_der: &[u8], + digest: &[u8], + hash_alg: HashAlgorithm, + ) -> Result, CryptoError> { + let mut rng = rand::rng(); + let private_key = RsaPrivateKey::from_pkcs1_der(private_key_der) + .map_err(|_| CryptoError::InvalidKey(None))?; + + match hash_alg { + HashAlgorithm::Sha1 => private_key + .sign_with_rng(&mut rng, Pkcs1v15Sign::new::(), digest) + .map_err(|_| CryptoError::SigningFailed(None)), + HashAlgorithm::Sha256 => private_key + .sign_with_rng(&mut rng, Pkcs1v15Sign::new::(), digest) + .map_err(|_| CryptoError::SigningFailed(None)), + HashAlgorithm::Sha384 => private_key + .sign_with_rng(&mut rng, Pkcs1v15Sign::new::(), digest) + .map_err(|_| CryptoError::SigningFailed(None)), + HashAlgorithm::Sha512 => private_key + .sign_with_rng(&mut rng, Pkcs1v15Sign::new::(), digest) + .map_err(|_| CryptoError::SigningFailed(None)), + _ => Err(CryptoError::UnsupportedAlgorithm), + } + } + + fn rsa_pkcs1v15_verify( + &self, + public_key_der: &[u8], + signature: &[u8], + digest: &[u8], + hash_alg: HashAlgorithm, + ) -> Result { + let public_key = RsaPublicKey::from_pkcs1_der(public_key_der) + .map_err(|_| CryptoError::InvalidKey(None))?; + + match hash_alg { + HashAlgorithm::Sha1 => Ok(public_key + .verify(Pkcs1v15Sign::new::(), digest, signature) + .is_ok()), + HashAlgorithm::Sha256 => Ok(public_key + .verify(Pkcs1v15Sign::new::(), digest, signature) + .is_ok()), + HashAlgorithm::Sha384 => Ok(public_key + .verify(Pkcs1v15Sign::new::(), digest, signature) + .is_ok()), + HashAlgorithm::Sha512 => Ok(public_key + .verify(Pkcs1v15Sign::new::(), digest, signature) + .is_ok()), + _ => Err(CryptoError::UnsupportedAlgorithm), + } + } + + fn rsa_oaep_encrypt( + &self, + public_key_der: &[u8], + data: &[u8], + hash_alg: HashAlgorithm, + label: Option<&[u8]>, + ) -> Result, CryptoError> { + let mut rng = rand::rng(); + let public_key = RsaPublicKey::from_pkcs1_der(public_key_der) + .map_err(|_| CryptoError::InvalidKey(None))?; + + match hash_alg { + HashAlgorithm::Sha1 => { + let mut padding = Oaep::::new(); + if let Some(l) = label { + if !l.is_empty() { + padding.label = Some(l.into()); + } + } + public_key + .encrypt(&mut rng, padding, data) + .map_err(|_| CryptoError::EncryptionFailed(None)) + } + HashAlgorithm::Sha256 => { + let mut padding = Oaep::::new(); + if let Some(l) = label { + if !l.is_empty() { + padding.label = Some(l.into()); + } + } + public_key + .encrypt(&mut rng, padding, data) + .map_err(|_| CryptoError::EncryptionFailed(None)) + } + HashAlgorithm::Sha384 => { + let mut padding = Oaep::::new(); + if let Some(l) = label { + if !l.is_empty() { + padding.label = Some(l.into()); + } + } + public_key + .encrypt(&mut rng, padding, data) + .map_err(|_| CryptoError::EncryptionFailed(None)) + } + HashAlgorithm::Sha512 => { + let mut padding = Oaep::::new(); + if let Some(l) = label { + if !l.is_empty() { + padding.label = Some(l.into()); + } + } + public_key + .encrypt(&mut rng, padding, data) + .map_err(|_| CryptoError::EncryptionFailed(None)) + } + _ => Err(CryptoError::UnsupportedAlgorithm), + } + } + + fn rsa_oaep_decrypt( + &self, + private_key_der: &[u8], + data: &[u8], + hash_alg: HashAlgorithm, + label: Option<&[u8]>, + ) -> Result, CryptoError> { + let private_key = RsaPrivateKey::from_pkcs1_der(private_key_der) + .map_err(|_| CryptoError::InvalidKey(None))?; + + match hash_alg { + HashAlgorithm::Sha1 => { + let mut padding = Oaep::::new(); + if let Some(l) = label { + if !l.is_empty() { + padding.label = Some(l.into()); + } + } + private_key + .decrypt(padding, data) + .map_err(|_| CryptoError::DecryptionFailed(None)) + } + HashAlgorithm::Sha256 => { + let mut padding = Oaep::::new(); + if let Some(l) = label { + if !l.is_empty() { + padding.label = Some(l.into()); + } + } + private_key + .decrypt(padding, data) + .map_err(|_| CryptoError::DecryptionFailed(None)) + } + HashAlgorithm::Sha384 => { + let mut padding = Oaep::::new(); + if let Some(l) = label { + if !l.is_empty() { + padding.label = Some(l.into()); + } + } + private_key + .decrypt(padding, data) + .map_err(|_| CryptoError::DecryptionFailed(None)) + } + HashAlgorithm::Sha512 => { + let mut padding = Oaep::::new(); + if let Some(l) = label { + if !l.is_empty() { + padding.label = Some(l.into()); + } + } + private_key + .decrypt(padding, data) + .map_err(|_| CryptoError::DecryptionFailed(None)) + } + _ => Err(CryptoError::UnsupportedAlgorithm), + } + } + + fn ecdh_derive_bits( + &self, + curve: EllipticCurve, + private_key_der: &[u8], + public_key_sec1: &[u8], + ) -> Result, CryptoError> { + match curve { + EllipticCurve::P256 => { + let secret_key = P256SecretKey::from_pkcs8_der(private_key_der) + .map_err(|_| CryptoError::InvalidKey(None))?; + let public_key = p256::PublicKey::from_sec1_bytes(public_key_sec1) + .map_err(|_| CryptoError::InvalidKey(None))?; + let shared_secret = p256::elliptic_curve::ecdh::diffie_hellman( + secret_key.to_nonzero_scalar(), + public_key.as_affine(), + ); + Ok(shared_secret.raw_secret_bytes().to_vec()) + } + EllipticCurve::P384 => { + let secret_key = P384SecretKey::from_pkcs8_der(private_key_der) + .map_err(|_| CryptoError::InvalidKey(None))?; + let public_key = p384::PublicKey::from_sec1_bytes(public_key_sec1) + .map_err(|_| CryptoError::InvalidKey(None))?; + let shared_secret = p384::elliptic_curve::ecdh::diffie_hellman( + secret_key.to_nonzero_scalar(), + public_key.as_affine(), + ); + Ok(shared_secret.raw_secret_bytes().to_vec()) + } + EllipticCurve::P521 => { + let secret_key = P521SecretKey::from_pkcs8_der(private_key_der) + .map_err(|_| CryptoError::InvalidKey(None))?; + let public_key = p521::PublicKey::from_sec1_bytes(public_key_sec1) + .map_err(|_| CryptoError::InvalidKey(None))?; + let shared_secret = p521::elliptic_curve::ecdh::diffie_hellman( + secret_key.to_nonzero_scalar(), + public_key.as_affine(), + ); + Ok(shared_secret.raw_secret_bytes().to_vec()) + } + } + } + + fn x25519_derive_bits( + &self, + private_key: &[u8], + public_key: &[u8], + ) -> Result, CryptoError> { + let private_array: [u8; 32] = private_key + .try_into() + .map_err(|_| CryptoError::InvalidKey(None))?; + let public_array: [u8; 32] = public_key + .try_into() + .map_err(|_| CryptoError::InvalidKey(None))?; + + let secret_key = x25519_dalek::StaticSecret::from(private_array); + let public_key = x25519_dalek::PublicKey::from(public_array); + let shared_secret = secret_key.diffie_hellman(&public_key); + + if shared_secret.as_bytes().iter().all(|b| *b == 0) { + return Err(CryptoError::OperationFailed(None)); + } + + Ok(shared_secret.as_bytes().to_vec()) + } + + fn aes_encrypt( + &self, + mode: AesMode, + key: &[u8], + iv: &[u8], + data: &[u8], + additional_data: Option<&[u8]>, + ) -> Result, CryptoError> { + match mode { + AesMode::Cbc => match key.len() { + 16 => { + let encryptor = Encryptor::::new_from_slices(key, iv)?; + Ok(encryptor.encrypt_padded_vec::(data)) + } + 24 => { + let encryptor = Encryptor::::new_from_slices(key, iv)?; + Ok(encryptor.encrypt_padded_vec::(data)) + } + 32 => { + let encryptor = Encryptor::::new_from_slices(key, iv)?; + Ok(encryptor.encrypt_padded_vec::(data)) + } + _ => Err(CryptoError::InvalidKey(None)), + }, + AesMode::Ctr { counter_length } => { + let mut ciphertext = data.to_vec(); + match (key.len(), counter_length) { + (16, 32) => { + let mut cipher = Ctr32BE::::new_from_slices(key, iv)?; + cipher.try_apply_keystream(&mut ciphertext)?; + } + (16, 64) => { + let mut cipher = Ctr64BE::::new_from_slices(key, iv)?; + cipher.try_apply_keystream(&mut ciphertext)?; + } + (16, 128) => { + let mut cipher = Ctr128BE::::new_from_slices(key, iv)?; + cipher.try_apply_keystream(&mut ciphertext)?; + } + (24, 32) => { + let mut cipher = Ctr32BE::::new_from_slices(key, iv)?; + cipher.try_apply_keystream(&mut ciphertext)?; + } + (24, 64) => { + let mut cipher = Ctr64BE::::new_from_slices(key, iv)?; + cipher.try_apply_keystream(&mut ciphertext)?; + } + (24, 128) => { + let mut cipher = Ctr128BE::::new_from_slices(key, iv)?; + cipher.try_apply_keystream(&mut ciphertext)?; + } + (32, 32) => { + let mut cipher = Ctr32BE::::new_from_slices(key, iv)?; + cipher.try_apply_keystream(&mut ciphertext)?; + } + (32, 64) => { + let mut cipher = Ctr64BE::::new_from_slices(key, iv)?; + cipher.try_apply_keystream(&mut ciphertext)?; + } + (32, 128) => { + let mut cipher = Ctr128BE::::new_from_slices(key, iv)?; + cipher.try_apply_keystream(&mut ciphertext)?; + } + _ => return Err(CryptoError::InvalidKey(None)), + } + Ok(ciphertext) + } + AesMode::Gcm { tag_length } => { + let variant = AesGcmVariant::new((key.len() * 8) as u16, tag_length, key)?; + let nonce: &Array<_, _> = + &Nonce::::try_from(iv).map_err(|_| CryptoError::InvalidData(None))?; + + let plaintext = Payload { + msg: data, + aad: additional_data.unwrap_or_default(), + }; + + match variant { + AesGcmVariant::Aes128Gcm32(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes192Gcm32(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes256Gcm32(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes128Gcm64(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes192Gcm64(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes256Gcm64(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes128Gcm96(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes192Gcm96(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes256Gcm96(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes128Gcm104(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes192Gcm104(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes256Gcm104(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes128Gcm112(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes192Gcm112(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes256Gcm112(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes128Gcm120(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes192Gcm120(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes256Gcm120(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes128Gcm128(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes192Gcm128(v) => v.encrypt(nonce, plaintext), + AesGcmVariant::Aes256Gcm128(v) => v.encrypt(nonce, plaintext), + } + .map_err(|_| CryptoError::EncryptionFailed(None)) + } + } + } + + fn aes_decrypt( + &self, + mode: AesMode, + key: &[u8], + iv: &[u8], + data: &[u8], + additional_data: Option<&[u8]>, + ) -> Result, CryptoError> { + match mode { + AesMode::Cbc => match key.len() { + 16 => { + let decryptor = Decryptor::::new_from_slices(key, iv)?; + decryptor + .decrypt_padded_vec::(data) + .map_err(|_| CryptoError::DecryptionFailed(None)) + } + 24 => { + let decryptor = Decryptor::::new_from_slices(key, iv)?; + decryptor + .decrypt_padded_vec::(data) + .map_err(|_| CryptoError::DecryptionFailed(None)) + } + 32 => { + let decryptor = Decryptor::::new_from_slices(key, iv)?; + decryptor + .decrypt_padded_vec::(data) + .map_err(|_| CryptoError::DecryptionFailed(None)) + } + _ => Err(CryptoError::InvalidKey(None)), + }, + AesMode::Ctr { .. } => { + // CTR decryption is the same as encryption + self.aes_encrypt(mode, key, iv, data, additional_data) + } + AesMode::Gcm { tag_length } => { + let variant = AesGcmVariant::new((key.len() * 8) as u16, tag_length, key)?; + let nonce: &Array<_, _> = + &Nonce::::try_from(iv).map_err(|_| CryptoError::InvalidData(None))?; + + let ciphertext = Payload { + msg: data, + aad: additional_data.unwrap_or_default(), + }; + + match variant { + AesGcmVariant::Aes128Gcm32(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes192Gcm32(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes256Gcm32(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes128Gcm64(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes192Gcm64(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes256Gcm64(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes128Gcm96(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes192Gcm96(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes256Gcm96(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes128Gcm104(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes192Gcm104(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes256Gcm104(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes128Gcm112(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes192Gcm112(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes256Gcm112(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes128Gcm120(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes192Gcm120(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes256Gcm120(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes128Gcm128(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes192Gcm128(v) => v.decrypt(nonce, ciphertext), + AesGcmVariant::Aes256Gcm128(v) => v.decrypt(nonce, ciphertext), + } + .map_err(|_| CryptoError::DecryptionFailed(None)) + } + } + } + + fn aes_kw_wrap(&self, kek: &[u8], key: &[u8]) -> Result, CryptoError> { + match kek.len() { + 16 => { + let kw = + KwAes128::new_from_slice(kek).map_err(|_| CryptoError::InvalidKey(None))?; + let mut buf = vec![0u8; key.len() + 8]; + let result = kw + .wrap_key(key, &mut buf) + .map_err(|_| CryptoError::OperationFailed(None))?; + Ok(result.to_vec()) + } + 24 => { + let kw = + KwAes192::new_from_slice(kek).map_err(|_| CryptoError::InvalidKey(None))?; + let mut buf = vec![0u8; key.len() + 8]; + let result = kw + .wrap_key(key, &mut buf) + .map_err(|_| CryptoError::OperationFailed(None))?; + Ok(result.to_vec()) + } + 32 => { + let kw = + KwAes256::new_from_slice(kek).map_err(|_| CryptoError::InvalidKey(None))?; + let mut buf = vec![0u8; key.len() + 8]; + let result = kw + .wrap_key(key, &mut buf) + .map_err(|_| CryptoError::OperationFailed(None))?; + Ok(result.to_vec()) + } + _ => Err(CryptoError::InvalidKey(None)), + } + } + + fn aes_kw_unwrap(&self, kek: &[u8], wrapped_key: &[u8]) -> Result, CryptoError> { + match kek.len() { + 16 => { + let kw = + KwAes128::new_from_slice(kek).map_err(|_| CryptoError::InvalidKey(None))?; + let mut buf = vec![0u8; wrapped_key.len()]; + let result = kw + .unwrap_key(wrapped_key, &mut buf) + .map_err(|_| CryptoError::OperationFailed(None))?; + Ok(result.to_vec()) + } + 24 => { + let kw = + KwAes192::new_from_slice(kek).map_err(|_| CryptoError::InvalidKey(None))?; + let mut buf = vec![0u8; wrapped_key.len()]; + let result = kw + .unwrap_key(wrapped_key, &mut buf) + .map_err(|_| CryptoError::OperationFailed(None))?; + Ok(result.to_vec()) + } + 32 => { + let kw = + KwAes256::new_from_slice(kek).map_err(|_| CryptoError::InvalidKey(None))?; + let mut buf = vec![0u8; wrapped_key.len()]; + let result = kw + .unwrap_key(wrapped_key, &mut buf) + .map_err(|_| CryptoError::OperationFailed(None))?; + Ok(result.to_vec()) + } + _ => Err(CryptoError::InvalidKey(None)), + } + } + + fn hkdf_derive_key( + &self, + key: &[u8], + salt: &[u8], + info: &[u8], + length: usize, + hash_alg: HashAlgorithm, + ) -> Result, CryptoError> { + let mut out = vec![0u8; length]; + + match hash_alg { + HashAlgorithm::Sha1 => { + let prk = Hkdf::::new(Some(salt), key); + prk.expand(info, &mut out) + } + HashAlgorithm::Sha256 => { + let prk = Hkdf::::new(Some(salt), key); + prk.expand(info, &mut out) + } + HashAlgorithm::Sha384 => { + let prk = Hkdf::::new(Some(salt), key); + prk.expand(info, &mut out) + } + HashAlgorithm::Sha512 => { + let prk = Hkdf::::new(Some(salt), key); + prk.expand(info, &mut out) + } + _ => return Err(CryptoError::UnsupportedAlgorithm), + } + .map_err(|_| CryptoError::DerivationFailed(None))?; + Ok(out) + } + + fn pbkdf2_derive_key( + &self, + password: &[u8], + salt: &[u8], + iterations: u32, + length: usize, + hash_alg: HashAlgorithm, + ) -> Result, CryptoError> { + let mut out = vec![0; length]; + let iterations = NonZeroU32::new(iterations).ok_or(CryptoError::InvalidData(None))?; + match hash_alg { + HashAlgorithm::Sha1 => { + pbkdf2::>(password, salt, iterations.get(), &mut out) + } + HashAlgorithm::Sha256 => { + pbkdf2::>(password, salt, iterations.get(), &mut out) + } + HashAlgorithm::Sha384 => { + pbkdf2::>(password, salt, iterations.get(), &mut out) + } + HashAlgorithm::Sha512 => { + pbkdf2::>(password, salt, iterations.get(), &mut out) + } + _ => return Err(CryptoError::UnsupportedAlgorithm), + } + .map_err(|_| CryptoError::InvalidLength)?; + Ok(out) + } + + fn generate_aes_key(&self, length_bits: u16) -> Result, CryptoError> { + let length_bytes = (length_bits / 8) as usize; + if !matches!(length_bits, 128 | 192 | 256) { + return Err(CryptoError::InvalidLength); + } + Ok(random_byte_array(length_bytes)) + } + + fn generate_hmac_key( + &self, + hash_alg: HashAlgorithm, + length_bits: u16, + ) -> Result, CryptoError> { + let length_bytes = if length_bits == 0 { + hash_alg.block_len() + } else { + (length_bits / 8) as usize + }; + + if length_bytes > 128 { + return Err(CryptoError::InvalidLength); + } + + Ok(random_byte_array(length_bytes)) + } + + fn generate_ec_key(&self, curve: EllipticCurve) -> Result<(Vec, Vec), CryptoError> { + let mut rng = rand::rng(); + + match curve { + EllipticCurve::P256 => { + let key = P256SecretKey::try_generate_from_rng(&mut rng) + .map_err(|_| CryptoError::OperationFailed(None))?; + let pkcs8 = key + .to_pkcs8_der() + .map_err(|_| CryptoError::OperationFailed(None))?; + let private_key = pkcs8.as_bytes().to_vec(); + let public_key = key.public_key().to_sec1_bytes().to_vec(); + Ok((private_key, public_key)) + } + EllipticCurve::P384 => { + let key = P384SecretKey::try_generate_from_rng(&mut rng) + .map_err(|_| CryptoError::OperationFailed(None))?; + let pkcs8 = key + .to_pkcs8_der() + .map_err(|_| CryptoError::OperationFailed(None))?; + let private_key = pkcs8.as_bytes().to_vec(); + let public_key = key.public_key().to_sec1_bytes().to_vec(); + Ok((private_key, public_key)) + } + EllipticCurve::P521 => { + let key = P521SecretKey::try_generate_from_rng(&mut rng) + .map_err(|_| CryptoError::OperationFailed(None))?; + let pkcs8 = key + .to_pkcs8_der() + .map_err(|_| CryptoError::OperationFailed(None))?; + let private_key = pkcs8.as_bytes().to_vec(); + let public_key = key.public_key().to_sec1_bytes().to_vec(); + Ok((private_key, public_key)) + } + } + } + + fn generate_ed25519_key(&self) -> Result<(Vec, Vec), CryptoError> { + let mut rng = rand::rng(); + let private_key = ed25519_dalek::SigningKey::generate(&mut rng) + .to_pkcs8_der() + .map_err(|_| CryptoError::OperationFailed(None))? + .as_bytes() + .to_vec(); + let signing_key = ed25519_dalek::SigningKey::from_pkcs8_der(&private_key) + .map_err(|_| CryptoError::OperationFailed(None))?; + let public_key = signing_key.verifying_key().to_bytes().to_vec(); + Ok((private_key, public_key)) + } + + fn generate_x25519_key(&self) -> Result<(Vec, Vec), CryptoError> { + let mut rng = rand::rng(); + let secret_key = x25519_dalek::StaticSecret::random_from_rng(&mut rng); + let private_key = secret_key.as_bytes().to_vec(); + let public_key = x25519_dalek::PublicKey::from(&secret_key) + .as_bytes() + .to_vec(); + Ok((private_key, public_key)) + } + + fn generate_rsa_key( + &self, + modulus_length: u32, + public_exponent: &[u8], + ) -> Result<(Vec, Vec), CryptoError> { + let exponent = parse_rsa_public_exponent(public_exponent)?; + + let exp = BoxedUint::from(exponent); + let mut rng = rand::rng(); + let rsa_private_key = RsaPrivateKey::new_with_exp(&mut rng, modulus_length as usize, exp) + .map_err(|_| CryptoError::OperationFailed(None))?; + + let public_key = rsa_private_key + .to_public_key() + .to_pkcs1_der() + .map_err(|_| CryptoError::OperationFailed(None))?; + let private_key = rsa_private_key + .to_pkcs1_der() + .map_err(|_| CryptoError::OperationFailed(None))?; + + Ok(( + private_key.as_bytes().to_vec(), + public_key.as_bytes().to_vec(), + )) + } + + fn import_rsa_public_key_pkcs1( + &self, + der: &[u8], + ) -> Result { + use der::Decode; + let public_key = + rsa::pkcs1::RsaPublicKey::from_der(der).map_err(|_| CryptoError::InvalidKey(None))?; + let modulus_length = public_key.modulus.as_bytes().len() * 8; + let public_exponent = public_key.public_exponent.as_bytes().to_vec(); + let key_data = public_key + .to_der() + .map_err(|_| CryptoError::InvalidKey(None))?; + Ok(super::RsaImportResult { + key_data, + modulus_length: modulus_length as u32, + public_exponent, + is_private: false, + }) + } + + fn import_rsa_private_key_pkcs1( + &self, + der: &[u8], + ) -> Result { + use der::Decode; + let private_key = + rsa::pkcs1::RsaPrivateKey::from_der(der).map_err(|_| CryptoError::InvalidKey(None))?; + let modulus_length = private_key.modulus.as_bytes().len() * 8; + let public_exponent = private_key.public_exponent.as_bytes().to_vec(); + let key_data = private_key + .to_der() + .map_err(|_| CryptoError::InvalidKey(None))?; + Ok(super::RsaImportResult { + key_data, + modulus_length: modulus_length as u32, + public_exponent, + is_private: true, + }) + } + + fn import_rsa_public_key_spki( + &self, + der: &[u8], + ) -> Result { + use der::Decode; + let spki = spki::SubjectPublicKeyInfoRef::try_from(der) + .map_err(|_| CryptoError::InvalidKey(None))?; + let public_key = rsa::pkcs1::RsaPublicKey::from_der(spki.subject_public_key.raw_bytes()) + .map_err(|_| CryptoError::InvalidKey(None))?; + let modulus_length = public_key.modulus.as_bytes().len() * 8; + let public_exponent = public_key.public_exponent.as_bytes().to_vec(); + let key_data = public_key + .to_der() + .map_err(|_| CryptoError::InvalidKey(None))?; + Ok(super::RsaImportResult { + key_data, + modulus_length: modulus_length as u32, + public_exponent, + is_private: false, + }) + } + + fn import_rsa_private_key_pkcs8( + &self, + der: &[u8], + ) -> Result { + use der::Decode; + let pk_info = + pkcs8::PrivateKeyInfoRef::from_der(der).map_err(|_| CryptoError::InvalidKey(None))?; + let private_key = rsa::pkcs1::RsaPrivateKey::from_der(pk_info.private_key.as_bytes()) + .map_err(|_| CryptoError::InvalidKey(None))?; + let modulus_length = private_key.modulus.as_bytes().len() * 8; + let public_exponent = private_key.public_exponent.as_bytes().to_vec(); + let key_data = pk_info.private_key.as_bytes().to_vec(); + Ok(super::RsaImportResult { + key_data, + modulus_length: modulus_length as u32, + public_exponent, + is_private: true, + }) + } + + fn export_rsa_public_key_pkcs1(&self, key_data: &[u8]) -> Result, CryptoError> { + // key_data is already PKCS1 DER + Ok(key_data.to_vec()) + } + + fn export_rsa_public_key_spki(&self, key_data: &[u8]) -> Result, CryptoError> { + use der::{Decode, Encode}; + let public_key = rsa::pkcs1::RsaPublicKey::from_der(key_data) + .map_err(|_| CryptoError::InvalidKey(None))?; + let spki = spki::SubjectPublicKeyInfo { + algorithm: spki::AlgorithmIdentifier:: { + oid: const_oid::db::rfc5912::RSA_ENCRYPTION, + parameters: Some(der::asn1::Null.into()), + }, + subject_public_key: spki::der::asn1::BitString::from_bytes( + &public_key + .to_der() + .map_err(|_| CryptoError::InvalidKey(None))?, + ) + .map_err(|_| CryptoError::InvalidKey(None))?, + }; + spki.to_der().map_err(|_| CryptoError::InvalidKey(None)) + } + + fn export_rsa_private_key_pkcs8(&self, key_data: &[u8]) -> Result, CryptoError> { + let private_key = + RsaPrivateKey::from_pkcs1_der(key_data).map_err(|_| CryptoError::InvalidKey(None))?; + private_key + .to_pkcs8_der() + .map(|doc| doc.as_bytes().to_vec()) + .map_err(|_| CryptoError::InvalidKey(None)) + } + + fn import_ec_public_key_sec1( + &self, + data: &[u8], + curve: EllipticCurve, + ) -> Result { + let key_data = match curve { + EllipticCurve::P256 => { + let public_key = p256::PublicKey::from_sec1_bytes(data) + .map_err(|_| CryptoError::InvalidKey(None))?; + public_key.to_sec1_point(false).as_bytes().to_vec() + } + EllipticCurve::P384 => { + let public_key = p384::PublicKey::from_sec1_bytes(data) + .map_err(|_| CryptoError::InvalidKey(None))?; + public_key.to_sec1_point(false).as_bytes().to_vec() + } + EllipticCurve::P521 => { + let public_key = p521::PublicKey::from_sec1_bytes(data) + .map_err(|_| CryptoError::InvalidKey(None))?; + public_key.to_sec1_point(false).as_bytes().to_vec() + } + }; + + Ok(super::EcImportResult { + key_data, + is_private: false, + }) + } + + fn import_ec_public_key_spki( + &self, + der: &[u8], + curve: EllipticCurve, + ) -> Result { + let spki = spki::SubjectPublicKeyInfoRef::try_from(der) + .map_err(|_| CryptoError::InvalidKey(None))?; + let point = spki.subject_public_key.raw_bytes(); + self.import_ec_public_key_sec1(point, curve) + } + + fn import_ec_private_key_pkcs8( + &self, + der: &[u8], + ) -> Result { + Ok(super::EcImportResult { + key_data: der.to_vec(), + is_private: true, + }) + } + + fn import_ec_private_key_sec1( + &self, + data: &[u8], + curve: EllipticCurve, + ) -> Result { + // Convert SEC1 private key to PKCS8 + let pkcs8_der = match curve { + EllipticCurve::P256 => { + let key = + P256SecretKey::from_slice(data).map_err(|_| CryptoError::InvalidKey(None))?; + key.to_pkcs8_der() + .map_err(|_| CryptoError::InvalidKey(None))? + .as_bytes() + .to_vec() + } + EllipticCurve::P384 => { + let key = + P384SecretKey::from_slice(data).map_err(|_| CryptoError::InvalidKey(None))?; + key.to_pkcs8_der() + .map_err(|_| CryptoError::InvalidKey(None))? + .as_bytes() + .to_vec() + } + EllipticCurve::P521 => { + let key = + P521SecretKey::from_slice(data).map_err(|_| CryptoError::InvalidKey(None))?; + key.to_pkcs8_der() + .map_err(|_| CryptoError::InvalidKey(None))? + .as_bytes() + .to_vec() + } + }; + Ok(super::EcImportResult { + key_data: pkcs8_der, + is_private: true, + }) + } + + fn export_ec_public_key_sec1( + &self, + key_data: &[u8], + curve: EllipticCurve, + is_private: bool, + ) -> Result, CryptoError> { + if is_private { + // Extract public key from PKCS8 private key + match curve { + EllipticCurve::P256 => { + let key = P256SecretKey::from_pkcs8_der(key_data) + .map_err(|_| CryptoError::InvalidKey(None))?; + Ok(key.public_key().to_sec1_point(false).as_bytes().to_vec()) + } + EllipticCurve::P384 => { + let key = P384SecretKey::from_pkcs8_der(key_data) + .map_err(|_| CryptoError::InvalidKey(None))?; + Ok(key.public_key().to_sec1_point(false).as_bytes().to_vec()) + } + EllipticCurve::P521 => { + let key = P521SecretKey::from_pkcs8_der(key_data) + .map_err(|_| CryptoError::InvalidKey(None))?; + Ok(key.public_key().to_sec1_point(false).as_bytes().to_vec()) + } + } + } else { + // key_data is already SEC1 encoded + Ok(key_data.to_vec()) + } + } + + fn export_ec_public_key_spki( + &self, + key_data: &[u8], + curve: EllipticCurve, + ) -> Result, CryptoError> { + use der::Encode; + use elliptic_curve::pkcs8::AssociatedOid; + let curve_oid = match curve { + EllipticCurve::P256 => p256::NistP256::OID, + EllipticCurve::P384 => p384::NistP384::OID, + EllipticCurve::P521 => p521::NistP521::OID, + }; + let spki = spki::SubjectPublicKeyInfo { + algorithm: spki::AlgorithmIdentifier:: { + oid: elliptic_curve::ALGORITHM_OID, + parameters: Some(curve_oid), + }, + subject_public_key: spki::der::asn1::BitString::from_bytes(key_data) + .map_err(|_| CryptoError::InvalidKey(None))?, + }; + spki.to_der().map_err(|_| CryptoError::InvalidKey(None)) + } + + fn export_ec_private_key_pkcs8( + &self, + key_data: &[u8], + _curve: EllipticCurve, + ) -> Result, CryptoError> { + // key_data is already PKCS8 + Ok(key_data.to_vec()) + } + + fn import_okp_public_key_raw( + &self, + data: &[u8], + ) -> Result { + if data.len() != 32 { + return Err(CryptoError::InvalidLength); + } + Ok(super::OkpImportResult { + key_data: data.to_vec(), + is_private: false, + }) + } + + fn import_okp_public_key_spki( + &self, + der: &[u8], + _expected_oid: &[u8], + ) -> Result { + let spki = spki::SubjectPublicKeyInfoRef::try_from(der) + .map_err(|_| CryptoError::InvalidKey(None))?; + Ok(super::OkpImportResult { + key_data: spki.subject_public_key.raw_bytes().to_vec(), + is_private: false, + }) + } + + fn import_okp_private_key_pkcs8( + &self, + der: &[u8], + _expected_oid: &[u8], + ) -> Result { + Ok(super::OkpImportResult { + key_data: der.to_vec(), + is_private: true, + }) + } + + fn export_okp_public_key_raw( + &self, + key_data: &[u8], + is_private: bool, + ) -> Result, CryptoError> { + if is_private { + // Extract public key from PKCS8 - for X25519/Ed25519 + use der::Decode; + let pk_info = pkcs8::PrivateKeyInfoRef::from_der(key_data) + .map_err(|_| CryptoError::InvalidKey(None))?; + // The private key is wrapped in an OCTET STRING, skip the tag+length (2 bytes) + let private_key_bytes = pk_info.private_key.as_bytes(); + let seed = if private_key_bytes.len() > 2 && private_key_bytes[0] == 0x04 { + &private_key_bytes[2..] + } else { + private_key_bytes + }; + let bytes: [u8; 32] = seed.try_into().map_err(|_| CryptoError::InvalidKey(None))?; + let secret = x25519_dalek::StaticSecret::from(bytes); + let public = x25519_dalek::PublicKey::from(&secret); + Ok(public.as_bytes().to_vec()) + } else { + Ok(key_data.to_vec()) + } + } + + fn export_okp_public_key_spki( + &self, + key_data: &[u8], + oid: &[u8], + ) -> Result, CryptoError> { + use der::Encode; + let oid = const_oid::ObjectIdentifier::from_bytes(oid) + .map_err(|_| CryptoError::InvalidKey(None))?; + let spki = spki::SubjectPublicKeyInfo { + algorithm: spki::AlgorithmIdentifierOwned { + oid, + parameters: None, + }, + subject_public_key: spki::der::asn1::BitString::from_bytes(key_data) + .map_err(|_| CryptoError::InvalidKey(None))?, + }; + spki.to_der().map_err(|_| CryptoError::InvalidKey(None)) + } + + fn export_okp_private_key_pkcs8( + &self, + key_data: &[u8], + oid: &[u8], + ) -> Result, CryptoError> { + // Ed25519: key_data is already PKCS#8. + if oid == const_oid::db::rfc8410::ID_ED_25519.as_bytes() { + return Ok(key_data.to_vec()); + } + // X25519: key_data is the raw 32-byte private scalar. + if oid == const_oid::db::rfc8410::ID_X_25519.as_bytes() { + if key_data.len() != 32 { + return Err(CryptoError::InvalidKey(None)); + } + + // RFC 8410 requires the privateKey field to contain + // an encoded OCTET STRING containing the 32-byte scalar. + let inner = OctetStringRef::new(key_data).map_err(|_| CryptoError::InvalidKey(None))?; + let inner_der = inner.to_der().map_err(|_| CryptoError::InvalidKey(None))?; + let pk_info = pkcs8::PrivateKeyInfoRef { + algorithm: spki::AlgorithmIdentifier { + oid: const_oid::db::rfc8410::ID_X_25519, + parameters: None, + }, + private_key: OctetStringRef::new(&inner_der) + .map_err(|_| CryptoError::InvalidKey(None))?, + public_key: None, + }; + return pk_info.to_der().map_err(|_| CryptoError::InvalidKey(None)); + } + Err(CryptoError::InvalidKey(None)) + } + + fn import_rsa_jwk( + &self, + jwk: super::RsaJwkImport<'_>, + ) -> Result { + use der::{asn1::UintRef, Encode}; + let modulus = UintRef::new(jwk.n).map_err(|_| CryptoError::InvalidKey(None))?; + let public_exponent = UintRef::new(jwk.e).map_err(|_| CryptoError::InvalidKey(None))?; + let modulus_length = (modulus.as_bytes().len() * 8) as u32; + let pub_exp_bytes = public_exponent.as_bytes().to_vec(); + + if let (Some(d), Some(p), Some(q), Some(dp), Some(dq), Some(qi)) = + (jwk.d, jwk.p, jwk.q, jwk.dp, jwk.dq, jwk.qi) + { + let private_key = rsa::pkcs1::RsaPrivateKey { + modulus, + public_exponent, + private_exponent: UintRef::new(d).map_err(|_| CryptoError::InvalidKey(None))?, + prime1: UintRef::new(p).map_err(|_| CryptoError::InvalidKey(None))?, + prime2: UintRef::new(q).map_err(|_| CryptoError::InvalidKey(None))?, + exponent1: UintRef::new(dp).map_err(|_| CryptoError::InvalidKey(None))?, + exponent2: UintRef::new(dq).map_err(|_| CryptoError::InvalidKey(None))?, + coefficient: UintRef::new(qi).map_err(|_| CryptoError::InvalidKey(None))?, + other_prime_infos: None, + }; + Ok(super::RsaImportResult { + key_data: private_key + .to_der() + .map_err(|_| CryptoError::InvalidKey(None))?, + modulus_length, + public_exponent: pub_exp_bytes, + is_private: true, + }) + } else { + let public_key = rsa::pkcs1::RsaPublicKey { + modulus, + public_exponent, + }; + Ok(super::RsaImportResult { + key_data: public_key + .to_der() + .map_err(|_| CryptoError::InvalidKey(None))?, + modulus_length, + public_exponent: pub_exp_bytes, + is_private: false, + }) + } + } + + fn export_rsa_jwk( + &self, + key_data: &[u8], + is_private: bool, + ) -> Result { + use der::Decode; + if is_private { + let key = rsa::pkcs1::RsaPrivateKey::from_der(key_data) + .map_err(|_| CryptoError::InvalidKey(None))?; + Ok(super::RsaJwkExport { + n: key.modulus.as_bytes().to_vec(), + e: key.public_exponent.as_bytes().to_vec(), + d: Some(key.private_exponent.as_bytes().to_vec()), + p: Some(key.prime1.as_bytes().to_vec()), + q: Some(key.prime2.as_bytes().to_vec()), + dp: Some(key.exponent1.as_bytes().to_vec()), + dq: Some(key.exponent2.as_bytes().to_vec()), + qi: Some(key.coefficient.as_bytes().to_vec()), + }) + } else { + let key = rsa::pkcs1::RsaPublicKey::from_der(key_data) + .map_err(|_| CryptoError::InvalidKey(None))?; + Ok(super::RsaJwkExport { + n: key.modulus.as_bytes().to_vec(), + e: key.public_exponent.as_bytes().to_vec(), + d: None, + p: None, + q: None, + dp: None, + dq: None, + qi: None, + }) + } + } + + fn import_ec_jwk( + &self, + jwk: super::EcJwkImport<'_>, + curve: EllipticCurve, + ) -> Result { + if let Some(d) = jwk.d { + // Private key - convert to PKCS8 + let pkcs8_der = match curve { + EllipticCurve::P256 => { + let key = + P256SecretKey::from_slice(d).map_err(|_| CryptoError::InvalidKey(None))?; + key.to_pkcs8_der() + .map_err(|_| CryptoError::InvalidKey(None))? + .as_bytes() + .to_vec() + } + EllipticCurve::P384 => { + let key = + P384SecretKey::from_slice(d).map_err(|_| CryptoError::InvalidKey(None))?; + key.to_pkcs8_der() + .map_err(|_| CryptoError::InvalidKey(None))? + .as_bytes() + .to_vec() + } + EllipticCurve::P521 => { + let key = + P521SecretKey::from_slice(d).map_err(|_| CryptoError::InvalidKey(None))?; + key.to_pkcs8_der() + .map_err(|_| CryptoError::InvalidKey(None))? + .as_bytes() + .to_vec() + } + }; + Ok(super::EcImportResult { + key_data: pkcs8_der, + is_private: true, + }) + } else { + // Public key - encode as SEC1 uncompressed point + let mut point = Vec::with_capacity(1 + jwk.x.len() + jwk.y.len()); + point.push(0x04); // uncompressed + point.extend_from_slice(jwk.x); + point.extend_from_slice(jwk.y); + Ok(super::EcImportResult { + key_data: point, + is_private: false, + }) + } + } + + fn export_ec_jwk( + &self, + key_data: &[u8], + curve: EllipticCurve, + is_private: bool, + ) -> Result { + let coord_len = match curve { + EllipticCurve::P256 => 32, + EllipticCurve::P384 => 48, + EllipticCurve::P521 => 66, + }; + if is_private { + // key_data is PKCS8 - use elliptic_curve's SecretKey to parse it + let (x, y, d) = match curve { + EllipticCurve::P256 => { + let sk = P256SecretKey::from_pkcs8_der(key_data) + .map_err(|_| CryptoError::InvalidKey(None))?; + let pk = sk.public_key(); + let pt = pk.to_sec1_point(false); + ( + pt.x().unwrap().to_vec(), + pt.y().unwrap().to_vec(), + sk.to_bytes().to_vec(), + ) + } + EllipticCurve::P384 => { + let sk = P384SecretKey::from_pkcs8_der(key_data) + .map_err(|_| CryptoError::InvalidKey(None))?; + let pk = sk.public_key(); + let pt = pk.to_sec1_point(false); + ( + pt.x().unwrap().to_vec(), + pt.y().unwrap().to_vec(), + sk.to_bytes().to_vec(), + ) + } + EllipticCurve::P521 => { + let sk = P521SecretKey::from_pkcs8_der(key_data) + .map_err(|_| CryptoError::InvalidKey(None))?; + let pk = sk.public_key(); + let pt = pk.to_sec1_point(false); + ( + pt.x().unwrap().to_vec(), + pt.y().unwrap().to_vec(), + sk.to_bytes().to_vec(), + ) + } + }; + Ok(super::EcJwkExport { x, y, d: Some(d) }) + } else { + // key_data is SEC1 uncompressed point (0x04 || x || y) + if key_data.len() != 1 + 2 * coord_len || key_data[0] != 0x04 { + return Err(CryptoError::InvalidKey(None)); + } + let x = key_data[1..1 + coord_len].to_vec(); + let y = key_data[1 + coord_len..].to_vec(); + Ok(super::EcJwkExport { x, y, d: None }) + } + } + + fn import_okp_jwk( + &self, + jwk: super::OkpJwkImport<'_>, + is_ed25519: bool, + ) -> Result { + if let Some(d) = jwk.d { + // Private key - for Ed25519 we need PKCS8, for X25519 we store raw + if is_ed25519 { + // Ed25519: construct PKCS8 from raw private key + let pk_info = pkcs8::PrivateKeyInfoRef { + algorithm: spki::AlgorithmIdentifier { + oid: const_oid::db::rfc8410::ID_ED_25519, + parameters: None, + }, + private_key: OctetStringRef::new(d) + .map_err(|_| CryptoError::InvalidKey(None))?, + public_key: Some( + BitStringRef::from_bytes(jwk.x) + .map_err(|_| CryptoError::InvalidKey(None))?, + ), + }; + let der = pk_info + .to_der() + .map_err(|_| CryptoError::InvalidKey(None))?; + Ok(super::OkpImportResult { + key_data: der, + is_private: true, + }) + } else { + // X25519: store raw 32-byte secret + Ok(super::OkpImportResult { + key_data: d.to_vec(), + is_private: true, + }) + } + } else { + // Public key - store raw bytes + Ok(super::OkpImportResult { + key_data: jwk.x.to_vec(), + is_private: false, + }) + } + } + + fn export_okp_jwk( + &self, + key_data: &[u8], + is_private: bool, + is_ed25519: bool, + ) -> Result { + if is_private { + if is_ed25519 { + // Ed25519: key_data is complete PKCS#8 DER. + let pk_info = pkcs8::PrivateKeyInfoRef::from_der(key_data) + .map_err(|_| CryptoError::InvalidKey(None))?; + let d = OctetString::from_der(pk_info.private_key.as_bytes()) + .map_err(|_| CryptoError::InvalidKey(None))? + .as_bytes() + .to_vec(); + + if d.len() != 32 { + return Err(CryptoError::InvalidKey(None)); + } + + let x = pk_info + .public_key + .ok_or(CryptoError::InvalidKey(None))? + .raw_bytes() + .to_vec(); + + if x.len() != 32 { + return Err(CryptoError::InvalidKey(None)); + } + + Ok(super::OkpJwkExport { x, d: Some(d) }) + } else { + // X25519: key_data is raw 32-byte secret + let secret = x25519_dalek::StaticSecret::from( + <[u8; 32]>::try_from(key_data).map_err(|_| CryptoError::InvalidKey(None))?, + ); + let public = x25519_dalek::PublicKey::from(&secret); + Ok(super::OkpJwkExport { + x: public.as_bytes().to_vec(), + d: Some(key_data.to_vec()), + }) + } + } else { + // Public key - key_data is raw bytes + Ok(super::OkpJwkExport { + x: key_data.to_vec(), + d: None, + }) + } + } +} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/crypto_key.rs b/stdlib/src/llrt/llrt_crypto/subtle/crypto_key.rs new file mode 100644 index 00000000..fc3d751d --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/subtle/crypto_key.rs @@ -0,0 +1,165 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::rc::Rc; + +use crate::llrt_utils::{clone::StructuredClone, str_enum}; +use rquickjs::{ + atom::PredefinedAtom, + class::{Trace, Tracer}, + Class, Ctx, Exception, IntoJs, Object, Result, Value, +}; + +use crate::llrt_crypto::provider::CryptoError; + +use super::key_algorithm::KeyAlgorithm; + +#[derive(PartialEq, Clone, Copy)] +pub enum KeyKind { + Secret, + Private, + Public, +} + +str_enum!(KeyKind,Secret => "secret", Private => "private", Public => "public"); + +#[rquickjs::class] +#[derive(rquickjs::JsLifetime)] +pub struct CryptoKey<'js> { + pub kind: KeyKind, + pub extractable: bool, + pub algorithm: KeyAlgorithm, + pub name: Box, + pub usages: Vec, + pub handle: Rc<[u8]>, + algorithm_cache: Option>, + usages_cache: Option>, +} + +impl<'js> CryptoKey<'js> { + pub fn new( + kind: KeyKind, + name: N, + extractable: bool, + algorithm: KeyAlgorithm, + usages: Vec, + handle: H, + ) -> Self + where + N: Into>, + H: Into>, + { + Self { + kind, + extractable, + algorithm, + name: name.into(), + usages, + handle: handle.into(), + algorithm_cache: None, + usages_cache: None, + } + } +} + +impl<'js> Trace<'js> for CryptoKey<'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + if let Some(cached) = &self.algorithm_cache { + cached.trace(tracer); + } + if let Some(cached) = &self.usages_cache { + cached.trace(tracer); + } + } +} + +impl<'js> StructuredClone<'js> for CryptoKey<'js> { + fn structured_clone(&self, ctx: &Ctx<'js>) -> Result> { + Ok(Class::instance( + ctx.clone(), + CryptoKey { + kind: self.kind, + extractable: self.extractable, + algorithm: self.algorithm.clone(), + name: self.name.clone(), + usages: self.usages.clone(), + handle: self.handle.clone(), + algorithm_cache: None, + usages_cache: None, + }, + )? + .into_value()) + } +} + +#[rquickjs::methods] +impl<'js> CryptoKey<'js> { + #[qjs(constructor)] + fn constructor(ctx: Ctx<'_>) -> Result { + Err(Exception::throw_type(&ctx, "Illegal constructor")) + } + + #[qjs(get, rename = "type")] + pub fn get_type(&self) -> &str { + self.kind.as_str() + } + + #[qjs(get)] + pub fn extractable(&self) -> bool { + self.extractable + } + + #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] + pub fn to_string_tag() -> &'static str { + stringify!(CryptoKey) + } + + #[qjs(get)] + pub fn algorithm(&mut self, ctx: Ctx<'js>) -> Result> { + if let Some(cached) = &self.algorithm_cache { + return Ok(cached.clone().into_value()); + } + let obj = self.algorithm.as_object(&ctx, self.name.as_ref())?; + self.algorithm_cache = Some(obj.clone()); + Ok(obj.into_value()) + } + + #[qjs(get)] + pub fn usages(&mut self, ctx: Ctx<'js>) -> Result> { + if let Some(cached) = &self.usages_cache { + return Ok(cached.clone()); + } + let arr = self.usages.clone().into_js(&ctx)?; + self.usages_cache = Some(arr.clone()); + Ok(arr) + } +} + +impl<'js> CryptoKey<'js> { + pub fn check_validity(&self, usage: &str) -> std::result::Result<(), CryptoError> { + for key in self.usages.iter() { + if key == usage { + return Ok(()); + } + } + Err(CryptoError::InvalidAccess(Some( + [ + "CryptoKey with '", + self.name.as_ref(), + "', doesn't support '", + usage, + "'", + ] + .concat() + .into(), + ))) + } + + pub fn check_kind(&self, expected: KeyKind) -> std::result::Result<(), CryptoError> { + if self.kind != expected { + return Err(CryptoError::InvalidAccess(Some("Invalid key type".into()))); + } + + Ok(()) + } +} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/derive_algorithm.rs b/stdlib/src/llrt/llrt_crypto/subtle/derive_algorithm.rs new file mode 100644 index 00000000..bde1715f --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/subtle/derive_algorithm.rs @@ -0,0 +1,77 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::rc::Rc; + +use crate::llrt_utils::object::ObjectExt; +use rquickjs::{Class, Ctx, FromJs, Result, Value}; + +use super::{ + algorithm_invalid_access_error, algorithm_mismatch_error, algorithm_not_supported_error, + crypto_key::{CryptoKey, KeyKind}, + key_algorithm::{EcAlgorithm, KeyAlgorithm, KeyDerivation}, + normalize_algorithm_name, + util::ResultDomExt, + EllipticCurve, +}; + +#[derive(Debug)] +pub enum DeriveAlgorithm { + X25519 { + public_key: Rc<[u8]>, + }, + Ecdh { + curve: EllipticCurve, + ec_algorithm: EcAlgorithm, + public_key: Rc<[u8]>, + }, + Derive(KeyDerivation), +} + +impl<'js> FromJs<'js> for DeriveAlgorithm { + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let obj = value.into_object_or_throw(ctx, "algorithm")?; + + let name: String = obj.get_required("name", "algorithm")?; + let name = normalize_algorithm_name(&name); + + Ok(match name.as_str() { + "X25519" => { + let public_key: Class = obj.get_required("public", "algorithm")?; + let public_key = public_key.borrow(); + + public_key.check_kind(KeyKind::Public).or_throw_dom(ctx)?; + + if !matches!(public_key.algorithm, KeyAlgorithm::X25519) { + return algorithm_invalid_access_error(ctx, &name); + } + + DeriveAlgorithm::X25519 { + public_key: public_key.handle.clone(), + } + } + "ECDH" => { + let public_key: Class = obj.get_required("public", "algorithm")?; + let public_key = public_key.borrow(); + + public_key.check_kind(KeyKind::Public).or_throw_dom(ctx)?; + + if let KeyAlgorithm::Ec { + curve, algorithm, .. + } = &public_key.algorithm + { + DeriveAlgorithm::Ecdh { + curve: *curve, + ec_algorithm: algorithm.clone(), + public_key: public_key.handle.clone(), + } + } else { + return algorithm_mismatch_error(ctx, &name); + } + } + "HKDF" => DeriveAlgorithm::Derive(KeyDerivation::for_hkdf_object(ctx, obj)?), + "PBKDF2" => DeriveAlgorithm::Derive(KeyDerivation::for_pbkf2_object(&ctx, obj)?), + _ => return algorithm_not_supported_error(ctx), + }) + } +} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/derive_bits.rs b/stdlib/src/llrt/llrt_crypto/subtle/derive_bits.rs new file mode 100644 index 00000000..e371a27c --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/subtle/derive_bits.rs @@ -0,0 +1,184 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::future::Future; + +use crate::llrt_exceptions::DOMException; +use crate::llrt_utils::result::ResultExt; +use rquickjs::{prelude::Opt, ArrayBuffer, Class, Ctx, FromJs, Result, Value}; + +use crate::llrt_crypto::{provider::CryptoProvider, CRYPTO_PROVIDER}; + +use super::{ + algorithm_invalid_access_error, algorithm_mismatch_error, + crypto_key::{CryptoKey, KeyKind}, + derive_algorithm::DeriveAlgorithm, + key_algorithm::{EcAlgorithm, KeyAlgorithm, KeyDerivation}, + util::ResultDomExt, + EllipticCurve, +}; + +pub fn subtle_derive_bits<'js>( + ctx: Ctx<'js>, + algorithm: Value<'js>, + base_key: Class<'js, CryptoKey<'js>>, + length: Opt>, +) -> impl Future>> + 'js { + let prepared = DeriveAlgorithm::from_js(&ctx, algorithm); + + async move { + let algorithm = prepared?; + + let base_key = base_key.borrow(); + base_key.check_validity("deriveBits").or_throw_dom(&ctx)?; + + let length = parse_derive_bits_length(&ctx, length)?; + let bytes = derive_bits(&ctx, &algorithm, &base_key, length)?; + + ArrayBuffer::new(ctx, bytes) + } +} + +pub(super) fn derive_bits( + ctx: &Ctx<'_>, + algorithm: &DeriveAlgorithm, + base_key: &CryptoKey, + length: DeriveBitsLength, +) -> Result> { + match algorithm { + DeriveAlgorithm::Ecdh { + curve, + ec_algorithm, + public_key, + } => { + if !matches!(ec_algorithm, EcAlgorithm::Ecdh) { + return algorithm_invalid_access_error(ctx, "ECDH"); + } + if let KeyAlgorithm::Ec { + curve: base_key_curve, + algorithm, + } = &base_key.algorithm + { + if curve == base_key_curve + && base_key.kind == KeyKind::Private + && matches!(algorithm, EcAlgorithm::Ecdh) + { + let length = match length { + DeriveBitsLength::Default => match curve { + EllipticCurve::P256 => 256, + EllipticCurve::P384 => 384, + EllipticCurve::P521 => 528, + }, + DeriveBitsLength::Specified(length) => length, + }; + let bytes = CRYPTO_PROVIDER + .ecdh_derive_bits(*curve, &base_key.handle, public_key) + .or_throw_dom(ctx)?; + return truncate_derived_bits(ctx, bytes, length); + } + + return Err(DOMException::invalid_access_error( + ctx, + "ECDH curve must be same as baseKey", + )); + } + algorithm_mismatch_error(ctx, "ECDH") + } + DeriveAlgorithm::X25519 { public_key } => { + if !matches!(base_key.algorithm, KeyAlgorithm::X25519) { + return algorithm_mismatch_error(ctx, "X25519"); + } + let length = match length { + DeriveBitsLength::Default => 256, + DeriveBitsLength::Specified(length) if length <= 256 => length, + DeriveBitsLength::Specified(_) => { + return Err(DOMException::operation_error(ctx, "Invalid length")); + } + }; + let bytes = CRYPTO_PROVIDER + .x25519_derive_bits(&base_key.handle, public_key) + .or_throw_dom(ctx)?; + + truncate_derived_bits(ctx, bytes, length) + } + DeriveAlgorithm::Derive(KeyDerivation::Hkdf { hash, salt, info }) => { + if !matches!(base_key.algorithm, KeyAlgorithm::HkdfImport) { + return algorithm_invalid_access_error(ctx, "HKDF"); + } + let length = match length { + DeriveBitsLength::Specified(length) if length % 8 == 0 => length, + _ => { + return Err(DOMException::operation_error(ctx, "Invalid length")); + } + }; + let out_length = (length / 8).try_into().or_throw(ctx)?; + CRYPTO_PROVIDER + .hkdf_derive_key(&base_key.handle, salt, info, out_length, *hash) + .or_throw(ctx) + } + DeriveAlgorithm::Derive(KeyDerivation::Pbkdf2 { + hash, + salt, + iterations, + }) => { + if !matches!(base_key.algorithm, KeyAlgorithm::Pbkdf2Import) { + return algorithm_invalid_access_error(ctx, "PBKDF2"); + } + let length = match length { + DeriveBitsLength::Specified(length) if length % 8 == 0 => length, + _ => { + return Err(DOMException::operation_error(ctx, "Invalid length")); + } + }; + let out_length = (length / 8).try_into().or_throw(ctx)?; + CRYPTO_PROVIDER + .pbkdf2_derive_key(&base_key.handle, salt, *iterations, out_length, *hash) + .or_throw(ctx) + } + } +} + +fn truncate_derived_bits(ctx: &Ctx<'_>, mut bytes: Vec, length: u32) -> Result> { + let max_bits = (bytes.len() * 8) as u32; + + if length > max_bits { + return Err(DOMException::operation_error( + ctx, + "Requested length exceeds derived secret size", + )); + } + + let byte_length = length.div_ceil(8) as usize; + bytes.truncate(byte_length); + + let remainder = (length % 8) as u8; + if remainder != 0 { + let mask = 0xff << (8 - remainder); + if let Some(last) = bytes.last_mut() { + *last &= mask; + } + } + + Ok(bytes) +} + +pub(super) enum DeriveBitsLength { + Default, + Specified(u32), +} + +fn parse_derive_bits_length<'js>( + ctx: &Ctx<'js>, + length: Opt>, +) -> Result { + match length.0 { + None => Ok(DeriveBitsLength::Default), + Some(value) if value.is_null() || value.is_undefined() => Ok(DeriveBitsLength::Default), + Some(value) => { + let length = u32::from_js(ctx, value) + .map_err(|_| DOMException::operation_error(ctx, "Invalid length"))?; + + Ok(DeriveBitsLength::Specified(length)) + } + } +} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/derive_keys.rs b/stdlib/src/llrt/llrt_crypto/subtle/derive_keys.rs new file mode 100644 index 00000000..c6b67a5b --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/subtle/derive_keys.rs @@ -0,0 +1,89 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::future::Future; + +use rquickjs::{Array, Class, Ctx, FromJs, Result, Value}; + +use super::{ + algorithm_not_supported_error, + crypto_key::{CryptoKey, KeyKind}, + derive_algorithm::DeriveAlgorithm, + derive_bits::{derive_bits, DeriveBitsLength}, + key_algorithm::{KeyAlgorithm, KeyAlgorithmMode, KeyAlgorithmWithUsages}, + util::ResultDomExt, +}; + +pub fn subtle_derive_key<'js>( + ctx: Ctx<'js>, + algorithm: Value<'js>, + base_key: Class<'js, CryptoKey<'js>>, + derived_key_algorithm: Value<'js>, + extractable: bool, + key_usages: Array<'js>, +) -> impl Future>>> + 'js { + let prepared = prepare_derive_key(&ctx, algorithm, derived_key_algorithm, key_usages); + + async move { + let (algorithm, key_algorithm) = prepared?; + + derive_key(&ctx, &algorithm, &base_key, extractable, key_algorithm) + } +} + +fn prepare_derive_key<'js>( + ctx: &Ctx<'js>, + algorithm: Value<'js>, + derived_key_algorithm: Value<'js>, + key_usages: Array<'js>, +) -> Result<(DeriveAlgorithm, KeyAlgorithmWithUsages)> { + let algorithm = DeriveAlgorithm::from_js(ctx, algorithm)?; + + let key_algorithm = KeyAlgorithm::from_js( + ctx, + KeyAlgorithmMode::Derive, + derived_key_algorithm, + key_usages, + )?; + + Ok((algorithm, key_algorithm)) +} + +fn derive_key<'js>( + ctx: &Ctx<'js>, + algorithm: &DeriveAlgorithm, + base_key: &Class<'js, CryptoKey<'js>>, + extractable: bool, + key_algorithm: KeyAlgorithmWithUsages, +) -> Result>> { + let length = match &key_algorithm.algorithm { + KeyAlgorithm::Aes { length, .. } => *length, + KeyAlgorithm::Hmac { length, .. } => *length, + KeyAlgorithm::Derive { .. } => 0, + _ => { + return algorithm_not_supported_error(ctx); + } + }; + + let base_key = base_key.borrow(); + + base_key.check_validity("deriveKey").or_throw_dom(ctx)?; + + let bytes = derive_bits( + ctx, + algorithm, + &base_key, + DeriveBitsLength::Specified(length as u32), + )?; + + let key = CryptoKey::new( + KeyKind::Secret, + key_algorithm.name, + extractable, + key_algorithm.algorithm, + key_algorithm.public_usages, + bytes, + ); + + Class::instance(ctx.clone(), key) +} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/digest.rs b/stdlib/src/llrt/llrt_crypto/subtle/digest.rs new file mode 100644 index 00000000..137cde8f --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/subtle/digest.rs @@ -0,0 +1,59 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use std::future::Future; + +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use crate::llrt_utils::{bytes::ObjectBytes, object::ObjectExt, result::ResultExt}; +use rquickjs::{ArrayBuffer, Ctx, Result, Value}; + +use crate::llrt_crypto::{ + hash::HashAlgorithm, + provider::{CryptoProvider, SimpleDigest}, + CRYPTO_PROVIDER, +}; + +use super::algorithm_not_supported_error; + +pub fn subtle_digest<'js>( + ctx: Ctx<'js>, + algorithm: Value<'js>, + data: ObjectBytes<'js>, +) -> impl Future>> + 'js { + // Snapshot inputs synchronously so mutating/detaching the buffer after the call can't affect the result (WPT digest.https.any.js). + let prepared = prepare_digest(&ctx, algorithm, data); + + async move { + let (hash_algorithm, input) = prepared?; + let bytes = digest(&hash_algorithm, &input); + ArrayBuffer::new(ctx, bytes) + } +} + +fn prepare_digest<'js>( + ctx: &Ctx<'js>, + algorithm: Value<'js>, + data: ObjectBytes<'js>, +) -> Result<(HashAlgorithm, Vec)> { + let algorithm = if let Some(s) = algorithm.as_string() { + s.to_string().or_throw(ctx)? + } else if let Some(name) = algorithm.get_optional::<_, String>("name")? { + name + } else { + return Err(rquickjs::Exception::throw_type( + ctx, + "Algorithm 'name' property required", + )); + }; + let hash_algorithm = match HashAlgorithm::try_from(algorithm.as_str()) { + Ok(h) => h, + Err(_) => return algorithm_not_supported_error(ctx), + }; + let input = data.as_bytes_opt().map(<[u8]>::to_vec).unwrap_or_default(); + Ok((hash_algorithm, input)) +} + +pub fn digest(hash_algorithm: &HashAlgorithm, data: &[u8]) -> Vec { + let mut hasher = CRYPTO_PROVIDER.digest(*hash_algorithm); + hasher.update(data); + hasher.finalize() +} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/encryption.rs b/stdlib/src/llrt/llrt_crypto/subtle/encryption.rs new file mode 100644 index 00000000..a66a374a --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/subtle/encryption.rs @@ -0,0 +1,269 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::{borrow::Cow, future::Future}; + +use crate::llrt_exceptions::DOMException; +use crate::llrt_utils::{bytes::ObjectBytes, result::ResultExt}; +use rquickjs::{ArrayBuffer, Class, Ctx, Exception, FromJs, Result, Value}; + +use crate::llrt_crypto::{ + provider::{AesMode, CryptoProvider}, + CRYPTO_PROVIDER, +}; + +use super::{ + algorithm_mismatch_error, + encryption_algorithm::EncryptionAlgorithm, + key_algorithm::{AesAlgorithm, KeyAlgorithm}, + util::ResultDomExt, + CryptoKey, EncryptionMode, +}; + +pub fn subtle_decrypt<'js>( + ctx: Ctx<'js>, + algorithm: Value<'js>, + key: Class<'js, CryptoKey<'js>>, + data: ObjectBytes<'js>, +) -> impl Future>> + 'js { + let prepared = prepare_encrypt_decrypt(&ctx, algorithm, key, data); + + async move { + let (algorithm, key, input) = prepared?; + + let key = key.borrow(); + key.check_validity("decrypt").or_throw_dom(&ctx)?; + + let bytes = encrypt_decrypt( + &ctx, + &algorithm, + &key, + &input, + EncryptionMode::Encryption, + EncryptionOperation::Decrypt, + )?; + ArrayBuffer::new(ctx, bytes) + } +} + +pub fn subtle_encrypt<'js>( + ctx: Ctx<'js>, + algorithm: Value<'js>, + key: Class<'js, CryptoKey<'js>>, + data: ObjectBytes<'js>, +) -> impl Future>> + 'js { + let prepared = prepare_encrypt_decrypt(&ctx, algorithm, key, data); + + async move { + let (algorithm, key, input) = prepared?; + + let key = key.borrow(); + key.check_validity("encrypt").or_throw_dom(&ctx)?; + + let bytes = encrypt_decrypt( + &ctx, + &algorithm, + &key, + &input, + EncryptionMode::Encryption, + EncryptionOperation::Encrypt, + )?; + ArrayBuffer::new(ctx, bytes) + } +} + +fn prepare_encrypt_decrypt<'js>( + ctx: &Ctx<'js>, + algorithm: Value<'js>, + key: Class<'js, CryptoKey<'js>>, + data: ObjectBytes<'js>, +) -> Result<(EncryptionAlgorithm, Class<'js, CryptoKey<'js>>, Vec)> { + let algorithm = EncryptionAlgorithm::from_js(ctx, algorithm)?; + let input = data.as_bytes_opt().map(<[u8]>::to_vec).unwrap_or_default(); + Ok((algorithm, key, input)) +} + +pub enum EncryptionOperation { + Encrypt, + Decrypt, +} + +pub fn encrypt_decrypt( + ctx: &Ctx<'_>, + algorithm: &EncryptionAlgorithm, + key: &CryptoKey, + data: &[u8], + mode: EncryptionMode, + operation: EncryptionOperation, +) -> Result> { + let handle = key.handle.as_ref(); + let bytes = match algorithm { + EncryptionAlgorithm::AesCbc { iv } => { + validate_aes_length(ctx, key, handle, AesAlgorithm::Cbc)?; + + match operation { + EncryptionOperation::Encrypt => CRYPTO_PROVIDER + .aes_encrypt(AesMode::Cbc, handle, iv, data, None) + .or_throw_dom(ctx)?, + EncryptionOperation::Decrypt => CRYPTO_PROVIDER + .aes_decrypt(AesMode::Cbc, handle, iv, data, None) + .or_throw_dom(ctx)?, + } + } + EncryptionAlgorithm::AesCtr { + counter, + length: encryption_length, + } => { + validate_aes_length(ctx, key, handle, AesAlgorithm::Ctr)?; + match operation { + EncryptionOperation::Encrypt => CRYPTO_PROVIDER + .aes_encrypt( + AesMode::Ctr { + counter_length: *encryption_length, + }, + handle, + counter, + data, + None, + ) + .or_throw_dom(ctx)?, + EncryptionOperation::Decrypt => CRYPTO_PROVIDER + .aes_decrypt( + AesMode::Ctr { + counter_length: *encryption_length, + }, + handle, + counter, + data, + None, + ) + .or_throw_dom(ctx)?, + } + } + EncryptionAlgorithm::AesGcm { + iv, + tag_length, + additional_data, + } => { + validate_aes_length(ctx, key, handle, AesAlgorithm::Gcm)?; + let aad = additional_data.as_deref(); + + match operation { + EncryptionOperation::Encrypt => CRYPTO_PROVIDER + .aes_encrypt( + AesMode::Gcm { + tag_length: *tag_length, + }, + handle, + iv, + data, + aad, + ) + .or_throw_dom(ctx)?, + EncryptionOperation::Decrypt => { + let tag_len = (*tag_length as usize) / 8; + if data.len() < tag_len { + return Err(DOMException::operation_error( + ctx, + "Invalid ciphertext length", + )); + } + // Pass the full data (ciphertext + tag) to the decrypt function + CRYPTO_PROVIDER + .aes_decrypt( + AesMode::Gcm { + tag_length: *tag_length, + }, + handle, + iv, + data, + aad, + ) + .or_throw_dom(ctx)? + } + } + } + EncryptionAlgorithm::AesKw => { + let padding = match mode { + EncryptionMode::Encryption => { + return Err(Exception::throw_message( + ctx, + "AES-KW can only be used for wrapping keys", + )); + } + EncryptionMode::Wrapping(padding) => padding, + }; + + match operation { + EncryptionOperation::Encrypt => { + // Pad data to multiple of 8 bytes if needed + let mut padded_data = Cow::Borrowed(data); + if !data.len().is_multiple_of(8) { + let pad_len = 8 - (data.len() % 8); + let mut padded = data.to_vec(); + padded.extend(std::iter::repeat_n(padding, pad_len)); + padded_data = Cow::Owned(padded) + } + CRYPTO_PROVIDER + .aes_kw_wrap(handle, &padded_data) + .or_throw_dom(ctx)? + } + EncryptionOperation::Decrypt => { + let unwrapped = CRYPTO_PROVIDER.aes_kw_unwrap(handle, data).or_throw(ctx)?; + // Remove padding if present + if padding != 0 { + let trimmed: Vec = unwrapped + .into_iter() + .rev() + .skip_while(|&b| b == padding) + .collect::>() + .into_iter() + .rev() + .collect(); + trimmed + } else { + unwrapped + } + } + } + } + EncryptionAlgorithm::RsaOaep { label } => { + let hash = match &key.algorithm { + KeyAlgorithm::Rsa { hash, .. } => hash, + _ => return algorithm_mismatch_error(ctx, "RSA-OAEP"), + }; + + match operation { + EncryptionOperation::Encrypt => CRYPTO_PROVIDER + .rsa_oaep_encrypt(handle, data, *hash, label.as_deref()) + .or_throw_dom(ctx)?, + EncryptionOperation::Decrypt => CRYPTO_PROVIDER + .rsa_oaep_decrypt(handle, data, *hash, label.as_deref()) + .or_throw_dom(ctx)?, + } + } + }; + Ok(bytes) +} + +pub fn validate_aes_length( + ctx: &Ctx<'_>, + key: &CryptoKey, + handle: &[u8], + expected_algorithm: AesAlgorithm, +) -> Result<()> { + match &key.algorithm { + KeyAlgorithm::Aes { algorithm, length } if *algorithm == expected_algorithm => { + if *length != handle.len() as u16 * 8 { + return Err(DOMException::operation_error(ctx, "Invalid AES key length")); + } + Ok(()) + } + KeyAlgorithm::Aes { .. } => Err(DOMException::invalid_access_error( + ctx, + "AES algorithm mismatch", + )), + + _ => algorithm_mismatch_error(ctx, "AES"), + } +} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/encryption_algorithm.rs b/stdlib/src/llrt/llrt_crypto/subtle/encryption_algorithm.rs new file mode 100644 index 00000000..ce1c580c --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/subtle/encryption_algorithm.rs @@ -0,0 +1,120 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use crate::llrt_exceptions::DOMException; +use crate::llrt_utils::{bytes::ObjectBytes, object::ObjectExt}; +use rquickjs::{Ctx, Exception, FromJs, Result, Value}; + +use super::{algorithm_not_supported_error, normalize_algorithm_name, to_name_and_maybe_object}; + +#[derive(Debug)] +pub enum EncryptionAlgorithm { + AesCbc { + iv: Box<[u8]>, + }, + AesCtr { + counter: Box<[u8]>, + length: u32, + }, + AesGcm { + iv: Box<[u8]>, + tag_length: u8, + additional_data: Option>, + }, + RsaOaep { + label: Option>, + }, + AesKw, +} + +impl<'js> FromJs<'js> for EncryptionAlgorithm { + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let (name, obj) = to_name_and_maybe_object(ctx, value)?; + let name = normalize_algorithm_name(&name); + + match name.as_str() { + "AES-CBC" => { + let obj = obj?; + let iv = obj + .get_required::<_, ObjectBytes>("iv", "algorithm")? + .into_bytes(ctx)? + .into_boxed_slice(); + + if iv.len() != 16 { + return Err(DOMException::operation_error( + ctx, + "invalid length of iv. Currently supported 16 bytes", + )); + } + + Ok(EncryptionAlgorithm::AesCbc { iv }) + } + "AES-CTR" => { + let obj = obj?; + let counter = obj + .get_required::<_, ObjectBytes>("counter", "algorithm")? + .into_bytes(ctx)? + .into_boxed_slice(); + + let length = obj.get_required::<_, u32>("length", "algorithm")?; + + if !matches!(length, 32 | 64 | 128) { + return Err(DOMException::operation_error( + ctx, + "invalid counter length. Currently supported 32/64/128 bits", + )); + } + + Ok(EncryptionAlgorithm::AesCtr { counter, length }) + } + "AES-GCM" => { + let obj = obj?; + let iv = obj + .get_required::<_, ObjectBytes>("iv", "algorithm")? + .into_bytes(ctx)? + .into_boxed_slice(); + + //FIXME only 12? 96 maybe recommended? + if iv.len() != 12 { + return Err(Exception::throw_type( + ctx, + "invalid length of iv. Currently supported 12 bytes", + )); + } + + let additional_data = obj + .get_optional::<_, ObjectBytes>("additionalData")? + .map(|v| v.into_bytes(ctx)) + .transpose()? + .map(|vec| vec.into_boxed_slice()); + + let tag_length = obj.get_optional::<_, u8>("tagLength")?.unwrap_or(128); + + //ensure tag length is supported using a match statement 32, 64, 96, 104, 112, 120, or 128 + if !matches!(tag_length, 32 | 64 | 96 | 104 | 112 | 120 | 128) { + return Err(DOMException::operation_error(ctx, "Invalid tagLength")); + } + + Ok(EncryptionAlgorithm::AesGcm { + iv, + additional_data, + tag_length, + }) + } + "RSA-OAEP" => { + let label = if let Ok(obj) = obj { + obj.get_optional::<_, ObjectBytes>("label")? + .map(|bytes| bytes.into_bytes(ctx)) + .transpose()? + .map(|vec| vec.into_boxed_slice()) + } else { + None + }; + + Ok(EncryptionAlgorithm::RsaOaep { label }) + } + "AES-KW" => Ok(EncryptionAlgorithm::AesKw), + _ => algorithm_not_supported_error(ctx), + } + } +} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/export_key.rs b/stdlib/src/llrt/llrt_crypto/subtle/export_key.rs new file mode 100644 index 00000000..cf0f9802 --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/subtle/export_key.rs @@ -0,0 +1,229 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Unified key export implementation using CryptoProvider trait. + +use crate::llrt_encoding::bytes_to_b64_url_safe_string; +use rquickjs::{ArrayBuffer, Class, Ctx, Exception, Object, Result}; + +use crate::llrt_crypto::provider::CryptoProvider; +use crate::llrt_crypto::CRYPTO_PROVIDER; + +use super::{ + crypto_key::KeyKind, + key_algorithm::{KeyAlgorithm, KeyFormat}, + util::ResultDomExt, + CryptoKey, +}; + +pub fn algorithm_export_error(ctx: &Ctx<'_>, algorithm: &str, format: &str) -> Result { + Err(Exception::throw_message( + ctx, + &["Export of ", algorithm, " as ", format, " is not supported"].concat(), + )) +} + +pub enum ExportOutput<'js> { + Bytes(Vec), + Object(Object<'js>), +} + +pub async fn subtle_export_key<'js>( + ctx: Ctx<'js>, + format: KeyFormat, + key: Class<'js, CryptoKey<'js>>, +) -> Result> { + let key = key.borrow(); + let export = export_key(&ctx, format, &key)?; + Ok(match export { + ExportOutput::Bytes(bytes) => ArrayBuffer::new(ctx, bytes)?.into_object(), + ExportOutput::Object(object) => object, + }) +} + +pub fn export_key<'js>( + ctx: &Ctx<'js>, + format: KeyFormat, + key: &CryptoKey, +) -> Result> { + if !key.extractable { + return Err(Exception::throw_type( + ctx, + "The CryptoKey is non extractable", + )); + } + let bytes = match format { + KeyFormat::Jwk => return Ok(ExportOutput::Object(export_jwk(ctx, key)?)), + KeyFormat::Raw => export_raw(ctx, key), + KeyFormat::Spki => export_spki(ctx, key), + KeyFormat::Pkcs8 => export_pkcs8(ctx, key), + }?; + Ok(ExportOutput::Bytes(bytes)) +} + +fn export_raw(ctx: &Ctx<'_>, key: &CryptoKey) -> Result> { + if key.kind == KeyKind::Private { + return Err(Exception::throw_type( + ctx, + "Private Crypto keys can't be exported as raw format", + )); + } + match &key.algorithm { + KeyAlgorithm::Aes { .. } | KeyAlgorithm::Hmac { .. } => Ok(key.handle.to_vec()), + KeyAlgorithm::Ec { curve, .. } => CRYPTO_PROVIDER + .export_ec_public_key_sec1(&key.handle, *curve, false) + .or_throw_dom(ctx), + KeyAlgorithm::Ed25519 => CRYPTO_PROVIDER + .export_okp_public_key_raw(&key.handle, false) + .or_throw_dom(ctx), + KeyAlgorithm::X25519 => CRYPTO_PROVIDER + .export_okp_public_key_raw(&key.handle, false) + .or_throw_dom(ctx), + KeyAlgorithm::Rsa { .. } => CRYPTO_PROVIDER + .export_rsa_public_key_pkcs1(&key.handle) + .or_throw_dom(ctx), + _ => algorithm_export_error(ctx, &key.name, "raw"), + } +} + +fn export_pkcs8(ctx: &Ctx<'_>, key: &CryptoKey) -> Result> { + if key.kind != KeyKind::Private { + return Err(Exception::throw_type( + ctx, + "Public or Secret Crypto keys can't be exported as pkcs8 format", + )); + } + match &key.algorithm { + KeyAlgorithm::Ec { curve, .. } => CRYPTO_PROVIDER + .export_ec_private_key_pkcs8(&key.handle, *curve) + .or_throw_dom(ctx), + KeyAlgorithm::Ed25519 => CRYPTO_PROVIDER + .export_okp_private_key_pkcs8( + &key.handle, + const_oid::db::rfc8410::ID_ED_25519.as_bytes(), + ) + .or_throw_dom(ctx), + KeyAlgorithm::X25519 => CRYPTO_PROVIDER + .export_okp_private_key_pkcs8( + &key.handle, + const_oid::db::rfc8410::ID_X_25519.as_bytes(), + ) + .or_throw_dom(ctx), + KeyAlgorithm::Rsa { .. } => CRYPTO_PROVIDER + .export_rsa_private_key_pkcs8(&key.handle) + .or_throw_dom(ctx), + _ => algorithm_export_error(ctx, &key.name, "pkcs8"), + } +} + +fn export_spki(ctx: &Ctx<'_>, key: &CryptoKey) -> Result> { + if key.kind != KeyKind::Public { + return Err(Exception::throw_type( + ctx, + "Private or Secret Crypto keys can't be exported as spki format", + )); + } + match &key.algorithm { + KeyAlgorithm::Ec { curve, .. } => CRYPTO_PROVIDER + .export_ec_public_key_spki(&key.handle, *curve) + .or_throw_dom(ctx), + KeyAlgorithm::Ed25519 => CRYPTO_PROVIDER + .export_okp_public_key_spki(&key.handle, const_oid::db::rfc8410::ID_ED_25519.as_bytes()) + .or_throw_dom(ctx), + KeyAlgorithm::X25519 => CRYPTO_PROVIDER + .export_okp_public_key_spki(&key.handle, const_oid::db::rfc8410::ID_X_25519.as_bytes()) + .or_throw_dom(ctx), + KeyAlgorithm::Rsa { .. } => CRYPTO_PROVIDER + .export_rsa_public_key_spki(&key.handle) + .or_throw_dom(ctx), + _ => algorithm_export_error(ctx, &key.name, "spki"), + } +} + +fn export_jwk<'js>(ctx: &Ctx<'js>, key: &CryptoKey) -> Result> { + let obj = Object::new(ctx.clone())?; + obj.set("key_ops", key.usages.clone())?; + obj.set("ext", true)?; + + match &key.algorithm { + KeyAlgorithm::Aes { length, .. } => { + let prefix = match length { + 128 => "A128", + 192 => "A192", + 256 => "A256", + _ => unreachable!(), + }; + let suffix = &key.name[("AES-".len())..]; + obj.set("kty", "oct")?; + obj.set("k", bytes_to_b64_url_safe_string(&key.handle))?; + obj.set("alg", [prefix, suffix].concat())?; + } + KeyAlgorithm::Hmac { hash, .. } => { + obj.set("kty", "oct")?; + obj.set("alg", ["HS", &hash.as_str()[4..]].concat())?; + obj.set("k", bytes_to_b64_url_safe_string(&key.handle))?; + } + KeyAlgorithm::Ec { curve, .. } => { + let jwk = CRYPTO_PROVIDER + .export_ec_jwk(&key.handle, *curve, key.kind == KeyKind::Private) + .or_throw_dom(ctx)?; + obj.set("kty", "EC")?; + obj.set("crv", curve.as_str())?; + obj.set("x", bytes_to_b64_url_safe_string(&jwk.x))?; + obj.set("y", bytes_to_b64_url_safe_string(&jwk.y))?; + if let Some(d) = jwk.d { + obj.set("d", bytes_to_b64_url_safe_string(&d))?; + } + } + KeyAlgorithm::Ed25519 => { + let jwk = CRYPTO_PROVIDER + .export_okp_jwk(&key.handle, key.kind == KeyKind::Private, true) + .or_throw_dom(ctx)?; + obj.set("kty", "OKP")?; + obj.set("crv", "Ed25519")?; + obj.set("x", bytes_to_b64_url_safe_string(&jwk.x))?; + obj.set("alg", "Ed25519")?; + if let Some(d) = jwk.d { + obj.set("d", bytes_to_b64_url_safe_string(&d))?; + } + } + KeyAlgorithm::X25519 => { + let jwk = CRYPTO_PROVIDER + .export_okp_jwk(&key.handle, key.kind == KeyKind::Private, false) + .or_throw_dom(ctx)?; + obj.set("kty", "OKP")?; + obj.set("crv", "X25519")?; + obj.set("x", bytes_to_b64_url_safe_string(&jwk.x))?; + if let Some(d) = jwk.d { + obj.set("d", bytes_to_b64_url_safe_string(&d))?; + } + } + KeyAlgorithm::Rsa { hash, .. } => { + let jwk = CRYPTO_PROVIDER + .export_rsa_jwk(&key.handle, key.kind == KeyKind::Private) + .or_throw_dom(ctx)?; + let alg_suffix = hash.as_numeric_str(); + let alg_prefix = match key.name.as_ref() { + "RSASSA-PKCS1-v1_5" => "RS", + "RSA-PSS" => "PS", + "RSA-OAEP" => "RSA-OAEP-", + _ => unreachable!(), + }; + obj.set("kty", "RSA")?; + obj.set("n", bytes_to_b64_url_safe_string(&jwk.n))?; + obj.set("e", bytes_to_b64_url_safe_string(&jwk.e))?; + obj.set("alg", [alg_prefix, alg_suffix].concat())?; + if let Some(d) = jwk.d { + obj.set("d", bytes_to_b64_url_safe_string(&d))?; + obj.set("p", bytes_to_b64_url_safe_string(&jwk.p.unwrap()))?; + obj.set("q", bytes_to_b64_url_safe_string(&jwk.q.unwrap()))?; + obj.set("dp", bytes_to_b64_url_safe_string(&jwk.dp.unwrap()))?; + obj.set("dq", bytes_to_b64_url_safe_string(&jwk.dq.unwrap()))?; + obj.set("qi", bytes_to_b64_url_safe_string(&jwk.qi.unwrap()))?; + } + } + _ => return algorithm_export_error(ctx, &key.name, "jwk"), + } + Ok(obj) +} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/generate_key.rs b/stdlib/src/llrt/llrt_crypto/subtle/generate_key.rs new file mode 100644 index 00000000..223fe4c5 --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/subtle/generate_key.rs @@ -0,0 +1,137 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use crate::llrt_exceptions::DOMException; +use rquickjs::{object::Property, Array, Class, Ctx, Object, Result, Value}; + +use crate::llrt_crypto::{hash::HashAlgorithm, provider::CryptoProvider, CRYPTO_PROVIDER}; + +use super::{ + algorithm_not_supported_error, + crypto_key::{CryptoKey, KeyKind}, + key_algorithm::{KeyAlgorithm, KeyAlgorithmMode, KeyAlgorithmWithUsages}, + util::ResultDomExt, +}; + +pub async fn subtle_generate_key<'js>( + ctx: Ctx<'js>, + algorithm: Value<'js>, + extractable: Value<'js>, + key_usages: Array<'js>, +) -> Result> { + let KeyAlgorithmWithUsages { + name, + algorithm: key_algorithm, + private_usages, + public_usages, + } = KeyAlgorithm::from_js(&ctx, KeyAlgorithmMode::Generate, algorithm, key_usages)?; + + let (private_key, public_or_secret_key) = generate_key(&ctx, &key_algorithm)?; + + let Some(extractable) = extractable.as_bool() else { + return Err(DOMException::not_supported_error(&ctx, "Invalid parameter")); + }; + + if matches!( + key_algorithm, + KeyAlgorithm::Aes { .. } | KeyAlgorithm::Hmac { .. } + ) { + return Ok(Class::instance( + ctx, + CryptoKey::new( + KeyKind::Secret, + name, + extractable, + key_algorithm, + public_usages, + public_or_secret_key, + ), + )? + .into_value()); + } + + let private_key = Class::instance( + ctx.clone(), + CryptoKey::new( + KeyKind::Private, + name.clone(), + extractable, + key_algorithm.clone(), + private_usages, + private_key, + ), + )?; + + let public_key = Class::instance( + ctx.clone(), + CryptoKey::new( + KeyKind::Public, + name, + true, + key_algorithm, + public_usages, + public_or_secret_key, + ), + )?; + + let key_pair = Object::new(ctx.clone())?; + key_pair.prop("privateKey", Property::from(private_key).enumerable())?; + key_pair.prop("publicKey", Property::from(public_key).enumerable())?; + Ok(key_pair.into_value()) +} + +fn generate_key(ctx: &Ctx<'_>, algorithm: &KeyAlgorithm) -> Result<(Vec, Vec)> { + match algorithm { + KeyAlgorithm::Aes { length, .. } => { + // Default to AES-256 + let key = CRYPTO_PROVIDER + .generate_aes_key(*length) + .or_throw_dom_with_msg(ctx, "AES key generation failed")?; + Ok((vec![], key)) + } + KeyAlgorithm::Hmac { hash, length } => { + let key = CRYPTO_PROVIDER + .generate_hmac_key(*hash, *length) + .or_throw_dom_with_msg(ctx, "HMAC key generation failed")?; + Ok((vec![], key)) + } + KeyAlgorithm::Ec { curve, .. } => CRYPTO_PROVIDER + .generate_ec_key(*curve) + .or_throw_dom_with_msg(ctx, "EC key generation failed"), + KeyAlgorithm::Ed25519 => CRYPTO_PROVIDER + .generate_ed25519_key() + .or_throw_dom_with_msg(ctx, "Ed25519 key generation failed"), + KeyAlgorithm::X25519 => CRYPTO_PROVIDER + .generate_x25519_key() + .or_throw_dom_with_msg(ctx, "X25519 key generation failed"), + KeyAlgorithm::Rsa { + modulus_length, + public_exponent, + .. + } => CRYPTO_PROVIDER + .generate_rsa_key(*modulus_length, public_exponent.as_ref()) + .or_throw_dom_with_msg(ctx, "RSA key generation failed"), + _ => algorithm_not_supported_error(ctx), + } +} + +#[allow(dead_code)] +fn generate_symmetric_key(_ctx: &Ctx<'_>, length: usize) -> Result> { + Ok(crate::llrt_crypto::random_byte_array(length)) +} + +#[allow(dead_code)] +pub fn get_hash_length(ctx: &Ctx, hash: &HashAlgorithm, length: u16) -> Result { + if length == 0 { + return Ok(hash.block_len()); + } + + if !length.is_multiple_of(8) || (length / 8) as usize > 128 { + return Err(DOMException::not_supported_error( + ctx, + "Invalid HMAC key length", + )); + } + + Ok((length / 8) as usize) +} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/import_key.rs b/stdlib/src/llrt/llrt_crypto/subtle/import_key.rs new file mode 100644 index 00000000..403d4c68 --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/subtle/import_key.rs @@ -0,0 +1,76 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use crate::llrt_exceptions::DOMException; +use crate::llrt_utils::{bytes::ObjectBytes, object::ObjectExt}; +use rquickjs::{Array, Class, Ctx, FromJs, Result, Value}; + +use super::{ + crypto_key::{CryptoKey, KeyKind}, + key_algorithm::{ + KeyAlgorithm, KeyAlgorithmMode, KeyAlgorithmWithUsages, KeyFormat, KeyFormatData, + }, +}; + +pub async fn subtle_import_key<'js>( + ctx: Ctx<'js>, + format: KeyFormat, + key_data: Value<'js>, + algorithm: Value<'js>, + extractable: bool, + key_usages: Array<'js>, +) -> Result>> { + let format = match format { + KeyFormat::Raw => KeyFormatData::Raw(ObjectBytes::from_js(&ctx, key_data)?), + KeyFormat::Pkcs8 => KeyFormatData::Pkcs8(ObjectBytes::from_js(&ctx, key_data)?), + KeyFormat::Spki => KeyFormatData::Spki(ObjectBytes::from_js(&ctx, key_data)?), + KeyFormat::Jwk => KeyFormatData::Jwk(key_data.into_object_or_throw(&ctx, "keyData")?), + }; + + import_key(ctx, format, algorithm, extractable, key_usages) +} + +pub fn import_key<'js>( + ctx: Ctx<'js>, + format: KeyFormatData<'js>, + algorithm: Value<'js>, + extractable: bool, + key_usages: Array<'js>, +) -> Result>> { + if extractable { + if let KeyFormatData::Jwk(jwk) = &format { + if matches!(jwk.get_optional::<_, bool>("ext")?, Some(false)) { + return Err(DOMException::data_error(&ctx, "JWK is not extractable")); + } + } + } + + let mut kind = KeyKind::Public; + let mut data = Vec::new(); + + let KeyAlgorithmWithUsages { + name, + algorithm: key_algorithm, + public_usages, + private_usages, + } = KeyAlgorithm::from_js( + &ctx, + KeyAlgorithmMode::Import { + kind: &mut kind, + data: &mut data, + format, + }, + algorithm, + key_usages, + )?; + + let usages = match kind { + KeyKind::Public | KeyKind::Secret => public_usages, + KeyKind::Private => private_usages, + }; + + Class::instance( + ctx, + CryptoKey::new(kind, name, extractable, key_algorithm, usages, data), + ) +} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/key_algorithm.rs b/stdlib/src/llrt/llrt_crypto/subtle/key_algorithm.rs new file mode 100644 index 00000000..32ec9ed0 --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/subtle/key_algorithm.rs @@ -0,0 +1,1609 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#![allow(clippy::uninlined_format_args)] + +use std::rc::Rc; + +#[cfg(all())] +use crate::llrt_encoding::bytes_from_b64_url_safe; +use crate::llrt_exceptions::DOMException; +use crate::llrt_utils::{bytes::ObjectBytes, object::ObjectExt, result::ResultExt, str_enum}; +#[cfg(all())] +use der::{ + asn1::{BitStringRef, OctetString, OctetStringRef}, + Decode, Encode, +}; +#[cfg(all())] +use ed25519_dalek::SigningKey; +#[cfg(all())] +use pkcs8::PrivateKeyInfoRef; +use rquickjs::{ + atom::PredefinedAtom, Array, Ctx, Exception, FromJs, Object, Result, TypedArray, Value, +}; +#[cfg(all())] +use spki::{AlgorithmIdentifier, ObjectIdentifier}; +#[cfg(all())] +use x25519_dalek::{PublicKey, StaticSecret}; + +use crate::llrt_crypto::{hash::HashAlgorithm, provider::parse_rsa_public_exponent}; + +#[cfg(all())] +use super::{algorithm_mismatch_error, util::DataError}; +use super::{ + algorithm_not_supported_error, + crypto_key::KeyKind, + normalize_algorithm_name, to_name_and_maybe_object, + util::{NotSupportedError, ResultDomExt}, + EllipticCurve, +}; + +#[derive(Clone, Copy, PartialEq)] +pub enum KeyUsage { + //7 values, can be max 255 (u8) 0b11111111 + Encrypt, + Decrypt, + WrapKey, + UnwrapKey, + Sign, + Verify, + DeriveKey, + DeriveBits, +} + +impl TryFrom<&str> for KeyUsage { + type Error = String; + + fn try_from(s: &str) -> std::result::Result { + Ok(match s { + "encrypt" => KeyUsage::Encrypt, + "decrypt" => KeyUsage::Decrypt, + "wrapKey" => KeyUsage::WrapKey, + "unwrapKey" => KeyUsage::UnwrapKey, + "sign" => KeyUsage::Sign, + "verify" => KeyUsage::Verify, + "deriveKey" => KeyUsage::DeriveKey, + "deriveBits" => KeyUsage::DeriveBits, + _ => return Err(["Invalid key usage: ", s].concat()), + }) + } +} + +impl KeyUsage { + fn classify_and_check_usages<'js>( + ctx: &Ctx<'js>, + key_usage_algorithm: KeyUsageAlgorithm, + key_usages: &Array<'js>, + private_usages: &mut Vec, + public_usages: &mut Vec, + kind: Option<&KeyKind>, + ) -> Result<()> { + let (mut private_usages_mask, mut public_usages_mask) = key_usage_algorithm.masks(); + + match kind { + Some(KeyKind::Private) => public_usages_mask = 0, + Some(KeyKind::Secret) | Some(KeyKind::Public) => private_usages_mask = 0, + None => {} + }; + + let allowed_usages = private_usages_mask | public_usages_mask; + + let mut generated_public_usages = Vec::with_capacity(4); + let mut generated_private_usages = Vec::with_capacity(4); + + let mut has_any_usages = false; + + for usage in key_usages.iter::() { + has_any_usages = true; + let value = usage?; + let usage = KeyUsage::try_from(value.as_str()).or_throw(ctx)?; + let usage = usage.mask(); + if allowed_usages & usage != usage { + return Err(Exception::throw_syntax( + ctx, + &["Invalid key usage '", &value, "'"].concat(), + )); + } + + if private_usages_mask == public_usages_mask { + generated_private_usages.push(value.clone()); + generated_public_usages.push(value); + } else if private_usages_mask & usage == usage { + generated_private_usages.push(value); + } else if public_usages_mask & usage == usage { + generated_public_usages.push(value); + } + } + + *private_usages = generated_private_usages; + *public_usages = generated_public_usages; + + if !has_any_usages + && key_usage_algorithm.requires_non_empty_usages() + && !matches!(kind, Some(KeyKind::Public)) + { + return Err(Exception::throw_syntax(ctx, "Key usages empty")); + } + + if private_usages != public_usages { + let valid_usage = match kind { + Some(KeyKind::Secret) | Some(KeyKind::Public) => { + private_usages.is_empty() && !public_usages.is_empty() + } + Some(KeyKind::Private) => !private_usages.is_empty() && public_usages.is_empty(), + None => true, + }; + + if !valid_usage { + return Err(Exception::throw_syntax(ctx, "Invalid key usage")); + } + } + + Ok(()) + } + + const fn mask(self) -> u16 { + 1 << self as u16 + } +} + +#[repr(u16)] +#[derive(Clone, Copy)] +pub enum KeyUsageAlgorithm { + //single mask algorithms (symmetric) + AesKw = KeyUsage::WrapKey.mask() | KeyUsage::UnwrapKey.mask(), + //all non-KW AES + Symmetric = (KeyUsage::Encrypt.mask()) + | (KeyUsage::Decrypt.mask()) + | (KeyUsage::WrapKey.mask()) + | (KeyUsage::UnwrapKey.mask()), + + Hmac = (KeyUsage::Sign.mask()) | (KeyUsage::Verify.mask()), + + // asymmetric derive algorithms - use high bits as private usages + // ECDH/X25519 + DeriveAsymmetric = ((KeyUsage::DeriveKey.mask() | KeyUsage::DeriveBits.mask()) << 8), + + // HKDF/PBKDF2 + DeriveSymmetric = KeyUsage::DeriveKey.mask() | KeyUsage::DeriveBits.mask(), + + RsaOaep = ((KeyUsage::Decrypt.mask() | KeyUsage::UnwrapKey.mask()) << 8) //private + | KeyUsage::Encrypt.mask() | KeyUsage::WrapKey.mask(), //public + + //ECDSA, ED25519, all non-OEAP RSA + Sign = (KeyUsage::Sign.mask() << 8) //private + | KeyUsage::Verify.mask(), //public +} +impl KeyUsageAlgorithm { + fn masks(&self) -> (u16, u16) { + let value = *self as u16; + let private_mask = value >> 8; + let public_mask = value & 0xFF; + (private_mask, public_mask) + } + + fn requires_non_empty_usages(self) -> bool { + matches!( + self, + Self::Symmetric + | Self::AesKw + | Self::Hmac + | Self::DeriveAsymmetric + | Self::DeriveSymmetric + | Self::Sign + | Self::RsaOaep + ) + } +} + +#[derive(Debug, Clone)] +pub enum KeyDerivation { + Hkdf { + hash: HashAlgorithm, + salt: Box<[u8]>, + info: Box<[u8]>, + }, + Pbkdf2 { + hash: HashAlgorithm, + salt: Box<[u8]>, + iterations: u32, + }, +} + +impl KeyDerivation { + pub fn for_hkdf_object<'js>(ctx: &Ctx<'js>, obj: Object<'js>) -> Result { + let hash = extract_sha_hash(ctx, &obj)?; + + let salt = obj + .get_required::<_, ObjectBytes>("salt", "algorithm")? + .into_bytes(ctx)? + .into_boxed_slice(); + + let info = obj + .get_required::<_, ObjectBytes>("info", "algorithm")? + .into_bytes(ctx)? + .into_boxed_slice(); + + Ok(KeyDerivation::Hkdf { hash, salt, info }) + } + + pub fn for_pbkf2_object<'js>(ctx: &&Ctx<'js>, obj: Object<'js>) -> Result { + let hash = extract_sha_hash(ctx, &obj)?; + + let salt = obj + .get_required::<_, ObjectBytes>("salt", "algorithm")? + .into_bytes(ctx)? + .into_boxed_slice(); + + let iterations = obj.get_required("iterations", "algorithm")?; + Ok(KeyDerivation::Pbkdf2 { + hash, + salt, + iterations, + }) + } +} + +#[derive(Debug, Clone)] +pub enum EcAlgorithm { + Ecdh, + Ecdsa, +} + +#[derive(PartialEq, Debug, Clone)] +pub enum AesAlgorithm { + Cbc, + Ctr, + Gcm, + Kw, +} + +#[derive(Debug, Clone)] +pub enum KeyAlgorithm { + Aes { + length: u16, + algorithm: AesAlgorithm, + }, + Ec { + curve: EllipticCurve, + algorithm: EcAlgorithm, + }, + X25519, + Ed25519, + Hmac { + hash: HashAlgorithm, + length: u16, + }, + Rsa { + modulus_length: u32, + public_exponent: Rc>, + hash: HashAlgorithm, + }, + Derive(KeyDerivation), + HkdfImport, + Pbkdf2Import, +} + +pub enum KeyFormat { + Jwk, + Raw, + Spki, + Pkcs8, +} + +str_enum!(KeyFormat, Jwk => "jwk", Raw => "raw", Spki => "spki", Pkcs8 => "pkcs8"); + +impl<'js> FromJs<'js> for KeyFormat { + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + if let Some(string) = value.as_string() { + let string = string.to_string()?; + match string.as_str() { + "jwk" => return Ok(KeyFormat::Jwk), + "raw" => return Ok(KeyFormat::Raw), + "spki" => return Ok(KeyFormat::Spki), + "pkcs8" => return Ok(KeyFormat::Pkcs8), + _ => {} + }; + } + Err(DOMException::not_supported_error( + ctx, + "Key import/export format must be 'jwk','raw','spki' or 'pkcs8'", + )) + } +} + +#[derive(PartialEq)] +pub enum KeyFormatData<'js> { + Jwk(Object<'js>), + Raw(ObjectBytes<'js>), + Spki(ObjectBytes<'js>), + Pkcs8(ObjectBytes<'js>), +} + +#[derive(PartialEq)] +pub enum KeyAlgorithmMode<'a, 'js> { + Import { + format: KeyFormatData<'js>, + kind: &'a mut KeyKind, + data: &'a mut Vec, + }, + Generate, + Derive, +} + +pub struct KeyAlgorithmWithUsages { + pub name: String, + pub algorithm: KeyAlgorithm, + pub public_usages: Vec, + pub private_usages: Vec, +} + +fn from_ed25519<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + algorithm_name: &str, + usages: &Array<'js>, + private_usages: &mut Vec, + public_usages: &mut Vec, +) -> Result { + #[cfg(all())] + #[inline] + fn import<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + algorithm_name: &str, + ) -> Result> { + if let KeyAlgorithmMode::Import { format, kind, data } = mode { + import_okp_key( + ctx, + format, + kind, + data, + const_oid::db::rfc8410::ID_ED_25519, + algorithm_name, + true, + )?; + Ok(Some(*kind)) + } else { + Ok(None) + } + } + + #[cfg(not(all()))] + #[inline] + fn import<'js>( + _ctx: &Ctx<'js>, + _mode: KeyAlgorithmMode<'_, 'js>, + _algorithm_name: &str, + ) -> Result> { + Ok(None) + } + + let key_kind = import(ctx, mode, algorithm_name)?; + KeyUsage::classify_and_check_usages( + ctx, + KeyUsageAlgorithm::Sign, + usages, + private_usages, + public_usages, + key_kind.as_ref(), + )?; + Ok(KeyAlgorithm::Ed25519) +} + +fn from_x25519<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + algorithm_name: &str, + usages: &Array<'js>, + private_usages: &mut Vec, + public_usages: &mut Vec, +) -> Result { + #[cfg(all())] + #[inline] + fn import<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + algorithm_name: &str, + ) -> Result> { + if let KeyAlgorithmMode::Import { format, kind, data } = mode { + import_okp_key( + ctx, + format, + kind, + data, + const_oid::db::rfc8410::ID_X_25519, + algorithm_name, + false, + )?; + Ok(Some(*kind)) + } else { + Ok(None) + } + } + + #[cfg(not(all()))] + #[inline] + fn import<'js>( + _ctx: &Ctx<'js>, + _mode: KeyAlgorithmMode<'_, 'js>, + _algorithm_name: &str, + ) -> Result> { + Ok(None) + } + + let key_kind = import(ctx, mode, algorithm_name)?; + KeyUsage::classify_and_check_usages( + ctx, + KeyUsageAlgorithm::DeriveAsymmetric, + usages, + private_usages, + public_usages, + key_kind.as_ref(), + )?; + Ok(KeyAlgorithm::X25519) +} + +fn from_aes<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + obj: Result>, + algorithm_name: &str, + usages: &Array<'js>, + private_usages: &mut Vec, + public_usages: &mut Vec, +) -> Result { + #[cfg(all())] + #[inline] + fn import<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + obj: Result>, + algorithm_name: &str, + ) -> Result<(u16, Option)> { + if let KeyAlgorithmMode::Import { data, format, kind } = mode { + let length = + import_symmetric_key(ctx, format, kind, data, algorithm_name, None)? as u16; + Ok((length, Some(*kind))) + } else { + let length: u16 = obj?.get_required("length", "algorithm")?; + Ok((length, None)) + } + } + + #[cfg(not(all()))] + #[inline] + fn import<'js>( + _ctx: &Ctx<'js>, + _mode: KeyAlgorithmMode<'_, 'js>, + obj: Result>, + _algorithm_name: &str, + ) -> Result<(u16, Option)> { + let length: u16 = obj?.get_required("length", "algorithm")?; + Ok((length, None)) + } + + let (length, key_kind) = import(ctx, mode, obj, algorithm_name)?; + + if !matches!(length, 128 | 192 | 256) { + return Err(DOMException::operation_error( + ctx, + format!( + "Algorithm 'length' must be one of: 128, 192, or 256 = {}", + length + ), + )); + } + + let algorithm = match algorithm_name { + "AES-CBC" => AesAlgorithm::Cbc, + "AES-CTR" => AesAlgorithm::Ctr, + "AES-GCM" => AesAlgorithm::Gcm, + "AES-KW" => AesAlgorithm::Kw, + _ => return Err(DOMException::operation_error(ctx, "Invalid algorithm name")), + }; + + KeyUsage::classify_and_check_usages( + ctx, + if algorithm == AesAlgorithm::Kw { + KeyUsageAlgorithm::AesKw + } else { + KeyUsageAlgorithm::Symmetric + }, + usages, + private_usages, + public_usages, + key_kind.as_ref(), + )?; + + Ok(KeyAlgorithm::Aes { length, algorithm }) +} + +fn from_hmac<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + obj: Result>, + algorithm_name: &str, + usages: &Array<'js>, + private_usages: &mut Vec, + public_usages: &mut Vec, +) -> Result { + let obj = obj?; + let hash = extract_sha_hash(ctx, &obj)?; + if !matches!( + hash, + HashAlgorithm::Sha1 | HashAlgorithm::Sha256 | HashAlgorithm::Sha384 | HashAlgorithm::Sha512 + ) { + return Err(DOMException::not_supported_error( + ctx, + "Unsupported HMAC hash algorithm", + )); + } + let mut length = match obj.get_optional::<_, u16>("length")? { + Some(length) => length, + None => match mode { + KeyAlgorithmMode::Import { .. } => 0, + _ => (hash.block_len() * 8) as u16, + }, + }; + + #[cfg(all())] + #[inline] + fn import<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + algorithm_name: &str, + hash: &HashAlgorithm, + length: &mut u16, + ) -> Result> { + if let KeyAlgorithmMode::Import { data, format, kind } = mode { + let data_length = + import_symmetric_key(ctx, format, kind, data, algorithm_name, Some(hash))?; + if *length == 0 { + *length = data_length as u16; + } + Ok(Some(*kind)) + } else { + Ok(None) + } + } + + #[cfg(not(all()))] + #[inline] + fn import<'js>( + _ctx: &Ctx<'js>, + _mode: KeyAlgorithmMode<'_, 'js>, + _algorithm_name: &str, + _hash: &HashAlgorithm, + _length: &mut u16, + ) -> Result> { + Ok(None) + } + + let key_kind = import(ctx, mode, algorithm_name, &hash, &mut length)?; + + KeyUsage::classify_and_check_usages( + ctx, + KeyUsageAlgorithm::Hmac, + usages, + private_usages, + public_usages, + key_kind.as_ref(), + )?; + + Ok(KeyAlgorithm::Hmac { hash, length }) +} + +fn from_rsa<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + obj: Result>, + algorithm_name: &str, + usages: &Array<'js>, + private_usages: &mut Vec, + public_usages: &mut Vec, +) -> Result { + let obj = obj?; + let hash = extract_sha_hash(ctx, &obj)?; + let is_generate = mode == KeyAlgorithmMode::Generate; + + #[cfg(all())] + #[inline] + fn import<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + obj: &Object<'js>, + algorithm_name: &str, + hash: &HashAlgorithm, + ) -> Result<(u32, Box<[u8]>, Option)> { + if let KeyAlgorithmMode::Import { format, kind, data } = mode { + let (mod_length, exp) = import_rsa_key(ctx, format, kind, data, algorithm_name, hash)?; + Ok((mod_length, exp, Some(*kind))) + } else { + let modulus_length = obj.get_required("modulusLength", "algorithm")?; + let public_exponent: TypedArray = + obj.get_required("publicExponent", "algorithm")?; + let public_exponent = public_exponent + .as_bytes() + .ok_or_else(|| { + DOMException::not_supported_error(ctx, "Array buffer has been detached") + })? + .to_owned() + .into_boxed_slice(); + Ok((modulus_length, public_exponent, None)) + } + } + + #[cfg(not(all()))] + #[inline] + fn import<'js>( + ctx: &Ctx<'js>, + _mode: KeyAlgorithmMode<'_, 'js>, + obj: &Object<'js>, + _algorithm_name: &str, + _hash: &HashAlgorithm, + ) -> Result<(u32, Box<[u8]>, Option)> { + let modulus_length = obj.get_required("modulusLength", "algorithm")?; + let public_exponent: TypedArray = obj.get_required("publicExponent", "algorithm")?; + let public_exponent = public_exponent + .as_bytes() + .ok_or_else(|| { + DOMException::not_supported_error(ctx, "Array buffer has been detached") + })? + .to_owned() + .into_boxed_slice(); + Ok((modulus_length, public_exponent, None)) + } + + let (modulus_length, public_exponent, key_kind) = + import(ctx, mode, &obj, algorithm_name, &hash)?; + + if is_generate { + parse_rsa_public_exponent(&public_exponent).or_throw_dom(ctx)?; + } + + KeyUsage::classify_and_check_usages( + ctx, + if algorithm_name == "RSA-OAEP" { + KeyUsageAlgorithm::RsaOaep + } else { + KeyUsageAlgorithm::Sign + }, + usages, + private_usages, + public_usages, + key_kind.as_ref(), + )?; + + Ok(KeyAlgorithm::Rsa { + modulus_length, + public_exponent: Rc::new(public_exponent), + hash, + }) +} + +fn from_hkdf<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + obj: Result>, + algorithm_name: &str, + usages: &Array<'js>, + private_usages: &mut Vec, + public_usages: &mut Vec, +) -> Result { + #[cfg(all())] + #[inline] + fn import<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + obj: Result>, + algorithm_name: &str, + ) -> Result<(KeyAlgorithm, Option)> { + match mode { + KeyAlgorithmMode::Import { format, kind, data } => { + import_derive_key(ctx, format, kind, data, algorithm_name)?; + Ok((KeyAlgorithm::HkdfImport, Some(*kind))) + } + KeyAlgorithmMode::Derive => { + let obj = obj?; + Ok(( + KeyAlgorithm::Derive(KeyDerivation::for_hkdf_object(ctx, obj)?), + None, + )) + } + _ => algorithm_not_supported_error(ctx), + } + } + + #[cfg(not(all()))] + #[inline] + fn import<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + obj: Result>, + _algorithm_name: &str, + ) -> Result<(KeyAlgorithm, Option)> { + match mode { + KeyAlgorithmMode::Derive => { + let obj = obj?; + Ok(( + KeyAlgorithm::Derive(KeyDerivation::for_hkdf_object(ctx, obj)?), + None, + )) + } + _ => algorithm_not_supported_error(ctx), + } + } + + let (algorithm, key_kind) = import(ctx, mode, obj, algorithm_name)?; + + KeyUsage::classify_and_check_usages( + ctx, + KeyUsageAlgorithm::DeriveSymmetric, + usages, + private_usages, + public_usages, + key_kind.as_ref(), + )?; + + Ok(algorithm) +} + +fn from_pbkdf2<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + obj: Result>, + algorithm_name: &str, + usages: &Array<'js>, + private_usages: &mut Vec, + public_usages: &mut Vec, +) -> Result { + #[cfg(all())] + #[inline] + fn import<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + obj: Result>, + algorithm_name: &str, + ) -> Result<(KeyAlgorithm, Option)> { + match mode { + KeyAlgorithmMode::Import { format, kind, data } => { + import_derive_key(ctx, format, kind, data, algorithm_name)?; + Ok((KeyAlgorithm::Pbkdf2Import, Some(*kind))) + } + KeyAlgorithmMode::Derive => { + let obj = obj?; + Ok(( + KeyAlgorithm::Derive(KeyDerivation::for_pbkf2_object(&ctx, obj)?), + None, + )) + } + _ => algorithm_not_supported_error(ctx), + } + } + + #[cfg(not(all()))] + #[inline] + fn import<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + obj: Result>, + _algorithm_name: &str, + ) -> Result<(KeyAlgorithm, Option)> { + match mode { + KeyAlgorithmMode::Derive => { + let obj = obj?; + Ok(( + KeyAlgorithm::Derive(KeyDerivation::for_pbkf2_object(&ctx, obj)?), + None, + )) + } + _ => algorithm_not_supported_error(ctx), + } + } + + let (algorithm, key_kind) = import(ctx, mode, obj, algorithm_name)?; + + KeyUsage::classify_and_check_usages( + ctx, + KeyUsageAlgorithm::DeriveSymmetric, + usages, + private_usages, + public_usages, + key_kind.as_ref(), + )?; + + Ok(algorithm) +} + +impl KeyAlgorithm { + pub fn from_js<'js>( + ctx: &Ctx<'js>, + mode: KeyAlgorithmMode<'_, 'js>, + value: Value<'js>, + usages: Array<'js>, + ) -> Result { + // When _subtle-full is not enabled, Import mode is not supported + #[cfg(not(all()))] + if matches!(mode, KeyAlgorithmMode::Import { .. }) { + return Err(DOMException::not_supported_error( + ctx, + "Key import is not supported with this crypto provider", + )); + } + + let (name, obj) = to_name_and_maybe_object(ctx, value)?; + let name = normalize_algorithm_name(&name); + let mut public_usages = vec![]; + let mut private_usages = vec![]; + let algorithm_name = name.as_ref(); + let algorithm = match algorithm_name { + "Ed25519" => from_ed25519( + ctx, + mode, + algorithm_name, + &usages, + &mut private_usages, + &mut public_usages, + )?, + "X25519" => from_x25519( + ctx, + mode, + algorithm_name, + &usages, + &mut private_usages, + &mut public_usages, + )?, + "AES-CBC" | "AES-CTR" | "AES-GCM" | "AES-KW" => from_aes( + ctx, + mode, + obj, + algorithm_name, + &usages, + &mut private_usages, + &mut public_usages, + )?, + "ECDH" => Self::from_ec( + ctx, + mode, + obj, + algorithm_name, + EcAlgorithm::Ecdh, + &usages, + &mut private_usages, + &mut public_usages, + KeyUsageAlgorithm::DeriveAsymmetric, + )?, + "ECDSA" => Self::from_ec( + ctx, + mode, + obj, + algorithm_name, + EcAlgorithm::Ecdsa, + &usages, + &mut private_usages, + &mut public_usages, + KeyUsageAlgorithm::Sign, + )?, + "HMAC" => from_hmac( + ctx, + mode, + obj, + algorithm_name, + &usages, + &mut private_usages, + &mut public_usages, + )?, + "RSA-OAEP" | "RSA-PSS" | "RSASSA-PKCS1-v1_5" => from_rsa( + ctx, + mode, + obj, + algorithm_name, + &usages, + &mut private_usages, + &mut public_usages, + )?, + "HKDF" => from_hkdf( + ctx, + mode, + obj, + algorithm_name, + &usages, + &mut private_usages, + &mut public_usages, + )?, + "PBKDF2" => from_pbkdf2( + ctx, + mode, + obj, + algorithm_name, + &usages, + &mut private_usages, + &mut public_usages, + )?, + _ => return algorithm_not_supported_error(ctx), + }; + + Ok(KeyAlgorithmWithUsages { + name, + algorithm, + public_usages, + private_usages, + }) + } + + pub fn as_object<'js, T: AsRef>(&self, ctx: &Ctx<'js>, name: T) -> Result> { + let obj = Object::new(ctx.clone())?; + obj.set(PredefinedAtom::Name, name.as_ref())?; + match self { + KeyAlgorithm::Aes { length, .. } => { + obj.set(PredefinedAtom::Length, length)?; + } + KeyAlgorithm::Ec { curve, .. } => { + obj.set("namedCurve", curve.as_str())?; + } + + KeyAlgorithm::Hmac { hash, length } => { + let hash_obj = create_hash_object(ctx, hash)?; + obj.set("hash", hash_obj)?; + + obj.set(PredefinedAtom::Length, length)?; + } + KeyAlgorithm::Rsa { + modulus_length, + public_exponent, + hash, + } => { + let public_exponent = public_exponent.as_ref().to_vec(); + let array = TypedArray::new(ctx.clone(), public_exponent)?; + + let hash_obj = create_hash_object(ctx, hash)?; + obj.set("hash", hash_obj)?; + + obj.set("modulusLength", modulus_length)?; + obj.set("publicExponent", array)?; + } + KeyAlgorithm::Derive(KeyDerivation::Hkdf { hash, salt, info }) => { + let salt = TypedArray::::new(ctx.clone(), salt.to_vec())?; + let info = TypedArray::::new(ctx.clone(), info.to_vec())?; + + obj.set("hash", hash.as_str())?; + obj.set("salt", salt)?; + obj.set("info", info)?; + } + KeyAlgorithm::Derive(KeyDerivation::Pbkdf2 { + hash, + salt, + iterations, + }) => { + let salt = TypedArray::::new(ctx.clone(), salt.to_vec())?; + obj.set("hash", hash.as_str())?; + obj.set("salt", salt)?; + obj.set("iterations", iterations)?; + } + _ => {} + }; + Ok(obj) + } + + #[allow(clippy::too_many_arguments)] + fn from_ec<'js>( + ctx: &Ctx<'js>, + #[allow(unused_variables)] mode: KeyAlgorithmMode<'_, 'js>, + obj: Result>, + #[allow(unused_variables)] algorithm_name: &str, + algorithm: EcAlgorithm, + key_usages: &Array<'js>, + private_usages: &mut Vec, + public_usages: &mut Vec, + key_usage_algorithm: KeyUsageAlgorithm, + ) -> Result { + let obj = obj?; + let curve_name: String = obj.get_required("namedCurve", "algorithm")?; + let curve = EllipticCurve::try_from(curve_name.as_str()) + .map_err(NotSupportedError) + .or_throw_dom(ctx)?; + + #[cfg(all())] + let key_kind = if let KeyAlgorithmMode::Import { format, kind, data } = mode { + import_ec_key(ctx, format, kind, data, algorithm_name, &curve, &curve_name)?; + Some(kind) + } else { + None + }; + #[cfg(not(all()))] + let key_kind: Option<&KeyKind> = None; + + KeyUsage::classify_and_check_usages( + ctx, + key_usage_algorithm, + key_usages, + private_usages, + public_usages, + key_kind.as_deref(), + )?; + + Ok(KeyAlgorithm::Ec { curve, algorithm }) + } +} + +#[cfg(all())] +fn import_derive_key<'js>( + ctx: &Ctx<'js>, + format: KeyFormatData<'js>, + kind: &mut KeyKind, + data: &mut Vec, + algorithm_name: &str, +) -> Result<()> { + if let KeyFormatData::Raw(object_bytes) = format { + *data = object_bytes.into_bytes(ctx)?; + *kind = KeyKind::Secret; + } else { + return Err(DOMException::not_supported_error( + ctx, + [algorithm_name, " only supports 'raw' import format"].concat(), + )); + } + + Ok(()) +} + +#[cfg(all())] +fn import_rsa_key<'js>( + ctx: &Ctx<'js>, + format: KeyFormatData<'js>, + kind: &mut KeyKind, + data: &mut Vec, + algorithm_name: &str, + hash: &HashAlgorithm, +) -> Result<(u32, Box<[u8]>)> { + use crate::llrt_crypto::{ + provider::{CryptoProvider, RsaJwkImport}, + CRYPTO_PROVIDER, + }; + + let validate_oid = |other_oid: const_oid::ObjectIdentifier| -> Result<()> { + if other_oid != const_oid::db::rfc5912::RSA_ENCRYPTION { + return algorithm_mismatch_error(ctx, algorithm_name); + } + Ok(()) + }; + + let (modulus_length, public_exponent) = match format { + KeyFormatData::Jwk(object) => { + validate_jwk_kty(ctx, &object, "RSA")?; + + if let Some(alg) = object.get_optional::<_, String>("alg")? { + let numeric_hash_str = match algorithm_name { + "RSASSA-PKCS1-v1_5" => alg.strip_prefix("RS"), + "RSA-PSS" => alg.strip_prefix("PS"), + "RSA-OAEP" => alg.strip_prefix("RSA-OAEP-"), + _ => None, + }; + let Some(numeric_hash_str) = numeric_hash_str else { + return algorithm_mismatch_error(ctx, algorithm_name); + }; + if numeric_hash_str != hash.as_numeric_str() { + return hash_mismatch_error(ctx, hash); + } + } + + let n_bytes = get_jwk_required_bytes(ctx, &object, "n")?; + let e_bytes = get_jwk_required_bytes(ctx, &object, "e")?; + + let d_bytes = get_jwk_optional_bytes(ctx, &object, "d")?; + + let result = if let Some(ref d_bytes) = d_bytes { + let p_bytes = get_jwk_required_bytes(ctx, &object, "p")?; + let q_bytes = get_jwk_required_bytes(ctx, &object, "q")?; + let dp_bytes = get_jwk_required_bytes(ctx, &object, "dp")?; + let dq_bytes = get_jwk_required_bytes(ctx, &object, "dq")?; + let qi_bytes = get_jwk_required_bytes(ctx, &object, "qi")?; + + let jwk = RsaJwkImport { + n: &n_bytes, + e: &e_bytes, + d: Some(d_bytes), + p: Some(&p_bytes), + q: Some(&q_bytes), + dp: Some(&dp_bytes), + dq: Some(&dq_bytes), + qi: Some(&qi_bytes), + }; + CRYPTO_PROVIDER.import_rsa_jwk(jwk).or_throw_dom(ctx)? + } else { + let jwk = RsaJwkImport { + n: &n_bytes, + e: &e_bytes, + d: None, + p: None, + q: None, + dp: None, + dq: None, + qi: None, + }; + CRYPTO_PROVIDER.import_rsa_jwk(jwk).or_throw_dom(ctx)? + }; + + *data = result.key_data; + *kind = if result.is_private { + KeyKind::Private + } else { + KeyKind::Public + }; + (result.modulus_length as usize, result.public_exponent) + } + KeyFormatData::Raw(object_bytes) => { + let result = CRYPTO_PROVIDER + .import_rsa_public_key_pkcs1(object_bytes.as_bytes(ctx)?) + .or_throw_dom(ctx)?; + *data = result.key_data; + *kind = KeyKind::Public; + (result.modulus_length as usize, result.public_exponent) + } + KeyFormatData::Pkcs8(object_bytes) => { + let pk_info = PrivateKeyInfoRef::from_der(object_bytes.as_bytes(ctx)?).or_throw(ctx)?; + validate_oid(pk_info.algorithm.oid)?; + let result = CRYPTO_PROVIDER + .import_rsa_private_key_pkcs8(object_bytes.as_bytes(ctx)?) + .or_throw_dom(ctx)?; + *data = result.key_data; + *kind = KeyKind::Private; + (result.modulus_length as usize, result.public_exponent) + } + KeyFormatData::Spki(object_bytes) => { + let pk_info = spki::SubjectPublicKeyInfoRef::try_from(object_bytes.as_bytes(ctx)?) + .or_throw(ctx)?; + validate_oid(pk_info.algorithm.oid)?; + let result = CRYPTO_PROVIDER + .import_rsa_public_key_spki(object_bytes.as_bytes(ctx)?) + .or_throw_dom(ctx)?; + *data = result.key_data; + *kind = KeyKind::Public; + (result.modulus_length as usize, result.public_exponent) + } + }; + + let public_exponent = public_exponent.into_boxed_slice(); + Ok((modulus_length as u32, public_exponent)) +} + +#[cfg(all())] +fn import_symmetric_key<'js>( + ctx: &Ctx<'js>, + format: KeyFormatData<'js>, + kind: &mut KeyKind, + data: &mut Vec, + algorithm_name: &str, + hash: Option<&HashAlgorithm>, +) -> Result { + *kind = KeyKind::Secret; + + match format { + KeyFormatData::Jwk(object) => { + validate_jwk_kty(ctx, &object, "oct")?; + + let k: String = get_jwk_required_string(ctx, &object, "k")?; + let alg: String = get_jwk_required_string(ctx, &object, "alg")?; + + let prefix = &alg[..1]; + + match (prefix, hash) { + //HMAC - HS256, HS512 etc + ("H", Some(hash)) => { + if &alg[2..] != hash.as_numeric_str() { + return hash_mismatch_error(ctx, hash); + } + } + //AES - A256KW, A256GCM, A256CRT, A512CBC etc + ("A", None) => { + //extract AES-{suffix} + let aes_variant = &alg[4..]; + + if !algorithm_name.ends_with(aes_variant) { + return algorithm_mismatch_error(ctx, algorithm_name); + } + } + _ => return algorithm_mismatch_error(ctx, algorithm_name), + } + + *data = bytes_from_b64_url_safe(k.as_bytes()).or_throw(ctx)?; + Ok(data.len() * 8) + } + KeyFormatData::Raw(object_bytes) => { + let bytes = object_bytes.into_bytes(ctx)?; + + *data = bytes; + Ok(data.len() * 8) + } + _ => algorithm_mismatch_error(ctx, algorithm_name), + } +} + +// EC algorithm OID for validation +#[cfg(all())] +const EC_ALGORITHM_OID: const_oid::ObjectIdentifier = + const_oid::ObjectIdentifier::new_unwrap("1.2.840.10045.2.1"); + +#[cfg(all())] +fn import_ec_key<'js>( + ctx: &Ctx<'js>, + format: KeyFormatData<'js>, + kind: &mut KeyKind, + data: &mut Vec, + algorithm_name: &str, + curve: &EllipticCurve, + curve_name: &str, +) -> Result<()> { + use crate::llrt_crypto::{ + provider::{CryptoProvider, EcJwkImport}, + CRYPTO_PROVIDER, + }; + + let validate_oid = |other_oid: const_oid::ObjectIdentifier| -> Result<()> { + if other_oid != EC_ALGORITHM_OID { + return algorithm_mismatch_error(ctx, algorithm_name); + } + Ok(()) + }; + + // Get expected coordinate length for the curve + let coord_len = match curve { + EllipticCurve::P256 => 32, + EllipticCurve::P384 => 48, + EllipticCurve::P521 => 66, + }; + + match format { + KeyFormatData::Jwk(object) => { + validate_jwk_kty(ctx, &object, "EC")?; + + validate_jwk_use(ctx, &object, true)?; + + validate_jwk_crv(ctx, &object, curve_name)?; + + let x_bytes = get_jwk_required_bytes(ctx, &object, "x")?; + validate_jwk_bytes_len(ctx, algorithm_name, "x coordinate", &x_bytes, coord_len)?; + + let y_bytes = get_jwk_required_bytes(ctx, &object, "y")?; + validate_jwk_bytes_len(ctx, algorithm_name, "y coordinate", &y_bytes, coord_len)?; + + let d_bytes = get_jwk_optional_bytes(ctx, &object, "d")?; + + if let Some(ref d_bytes) = d_bytes { + validate_jwk_bytes_len(ctx, algorithm_name, "private key", d_bytes, coord_len)?; + } + + let jwk = EcJwkImport { + x: &x_bytes, + y: &y_bytes, + d: d_bytes.as_deref(), + }; + + let result = CRYPTO_PROVIDER + .import_ec_jwk(jwk, *curve) + .or_throw_dom(ctx)?; + *data = result.key_data; + *kind = if result.is_private { + KeyKind::Private + } else { + KeyKind::Public + }; + } + KeyFormatData::Raw(object_bytes) => { + let bytes = object_bytes.as_bytes(ctx)?; + let result = CRYPTO_PROVIDER + .import_ec_public_key_sec1(bytes, *curve) + .or_throw_dom(ctx)?; + *data = result.key_data; + *kind = KeyKind::Public; + } + KeyFormatData::Spki(object_bytes) => { + let spki = spki::SubjectPublicKeyInfoRef::try_from(object_bytes.as_bytes(ctx)?) + .or_throw_data_error(ctx)?; + validate_oid(spki.algorithm.oid)?; + let result = CRYPTO_PROVIDER + .import_ec_public_key_spki(object_bytes.as_bytes(ctx)?, *curve) + .or_throw_dom(ctx)?; + *data = result.key_data; + *kind = KeyKind::Public; + } + KeyFormatData::Pkcs8(object_bytes) => { + let pkcs8 = PrivateKeyInfoRef::try_from(object_bytes.as_bytes(ctx)?) + .or_throw_data_error(ctx)?; + validate_oid(pkcs8.algorithm.oid)?; + let result = CRYPTO_PROVIDER + .import_ec_private_key_pkcs8(object_bytes.as_bytes(ctx)?) + .or_throw_dom(ctx)?; + *data = result.key_data; + *kind = KeyKind::Private; + } + }; + Ok(()) +} + +#[cfg(all())] +fn import_okp_key<'js>( + ctx: &Ctx<'js>, + format: KeyFormatData<'js>, + kind: &mut KeyKind, + data: &mut Vec, + oid: ObjectIdentifier, + algorithm_name: &str, + is_ed25519: bool, +) -> Result<()> { + let validate_oid = |other_oid: const_oid::ObjectIdentifier| -> Result<()> { + if other_oid != oid { + return algorithm_mismatch_error(ctx, algorithm_name); + } + Ok(()) + }; + + match format { + KeyFormatData::Jwk(object) => { + validate_jwk_kty(ctx, &object, "OKP")?; + + validate_jwk_crv(ctx, &object, algorithm_name)?; + + if is_ed25519 { + validate_jwk_alg(ctx, &object)?; + } + + validate_jwk_use(ctx, &object, is_ed25519)?; + + let public_key = get_jwk_required_bytes(ctx, &object, "x")?; + validate_jwk_bytes_len(ctx, algorithm_name, "public key", &public_key, 32)?; + + let private_key = get_jwk_optional_bytes(ctx, &object, "d")?; + + if let Some(private_key) = private_key { + validate_jwk_bytes_len(ctx, algorithm_name, "private key", &private_key, 32)?; + + validate_okp_jwk_key_pair(ctx, &private_key, &public_key, is_ed25519)?; + + if is_ed25519 { + // Ed25519 internal representation is the complete PKCS#8 DER. + let inner = OctetStringRef::new(private_key.as_slice()).or_throw(ctx)?; + let inner_der = inner.to_der().or_throw(ctx)?; + let pk_info = PrivateKeyInfoRef { + algorithm: AlgorithmIdentifier { + oid, + parameters: None, + }, + private_key: OctetStringRef::new(&inner_der).or_throw(ctx)?, + public_key: Some(BitStringRef::from_bytes(&public_key).or_throw(ctx)?), + }; + *data = pk_info.to_der().or_throw(ctx)?; + } else { + // X25519 internal representation is raw 32-byte scalar. + *data = private_key; + } + *kind = KeyKind::Private; + } else { + *data = public_key; + *kind = KeyKind::Public; + } + } + KeyFormatData::Raw(object_bytes) => { + let bytes = object_bytes.into_bytes(ctx)?; + if bytes.len() != 32 { + return Err(DOMException::data_error( + ctx, + [algorithm_name, " keys must be 32 bytes long"].concat(), + )); + } + *data = bytes; + *kind = KeyKind::Public; + } + KeyFormatData::Spki(object_bytes) => { + let spki = spki::SubjectPublicKeyInfoRef::try_from(object_bytes.as_bytes(ctx)?) + .or_throw_data_error(ctx)?; + validate_oid(spki.algorithm.oid)?; + + let public_key = spki.subject_public_key.raw_bytes(); + if public_key.len() != 32 { + return Err(DOMException::data_error( + ctx, + [algorithm_name, " public key must be 32 bytes"].concat(), + )); + } + + *data = public_key.to_vec(); + *kind = KeyKind::Public; + } + KeyFormatData::Pkcs8(object_bytes) => { + let bytes = object_bytes.into_bytes(ctx)?; + let pkcs8 = PrivateKeyInfoRef::try_from(bytes.as_slice()).or_throw_data_error(ctx)?; + validate_oid(pkcs8.algorithm.oid)?; + if is_ed25519 { + // Ed25519 internal representation is the complete PKCS#8 DER. + *data = bytes; + } else { + // X25519 internal representation is the inner OCTET STRING. + *data = OctetString::from_der(pkcs8.private_key.as_bytes()) + .or_throw(ctx)? + .as_bytes() + .to_vec(); + if data.len() != 32 { + return Err(DOMException::data_error( + ctx, + [algorithm_name, " private key must be 32 bytes"].concat(), + )); + } + } + *kind = KeyKind::Private; + } + } + + Ok(()) +} + +#[cfg(all())] +fn get_jwk_required_string<'js>( + ctx: &Ctx<'js>, + object: &Object<'js>, + name: &str, +) -> Result { + object + .get_required(name, "keyData") + .or_throw_data_error(ctx) +} + +#[cfg(all())] +fn get_jwk_required_bytes<'js>( + ctx: &Ctx<'js>, + object: &Object<'js>, + name: &str, +) -> Result> { + let value = get_jwk_required_string(ctx, object, name)?; + bytes_from_b64_url_safe(value.as_bytes()).or_throw_data_error(ctx) +} + +#[cfg(all())] +fn get_jwk_optional_bytes<'js>( + ctx: &Ctx<'js>, + object: &Object<'js>, + name: &str, +) -> Result>> { + let value = object.get_optional::<_, String>(name)?; + value + .map(|value| bytes_from_b64_url_safe(value.as_bytes()).or_throw_data_error(ctx)) + .transpose() +} + +#[cfg(all())] +fn validate_jwk_kty<'js>(ctx: &Ctx<'js>, object: &Object<'js>, expected: &str) -> Result<()> { + let kty = get_jwk_required_string(ctx, object, "kty")?; + if kty != expected { + return Err(DOMException::data_error( + ctx, + ["JWK 'kty' parameter must be '", expected, "'"].concat(), + )); + } + Ok(()) +} + +#[cfg(all())] +fn validate_jwk_crv<'js>(ctx: &Ctx<'js>, object: &Object<'js>, expected: &str) -> Result<()> { + let crv = get_jwk_required_string(ctx, object, "crv")?; + if crv != expected { + return Err(DOMException::data_error( + ctx, + ["JWK 'crv' parameter must be '", expected, "'"].concat(), + )); + } + Ok(()) +} + +#[cfg(all())] +fn validate_jwk_use(ctx: &Ctx<'_>, object: &Object<'_>, is_ed25519: bool) -> Result<()> { + if let Some(use_) = object.get_optional::<_, String>("use")? { + let expected = if is_ed25519 { "sig" } else { "enc" }; + if use_ != expected { + return Err(DOMException::data_error( + ctx, + "JWK 'use' parameter is invalid", + )); + } + } + Ok(()) +} + +#[cfg(all())] +fn validate_jwk_alg(ctx: &Ctx<'_>, object: &Object<'_>) -> Result<()> { + if let Some(alg) = object.get_optional::<_, String>("alg")? { + if alg != "Ed25519" && alg != "EdDSA" { + return Err(DOMException::data_error( + ctx, + "JWK 'alg' parameter is invalid", + )); + } + } + Ok(()) +} + +#[cfg(all())] +fn validate_jwk_bytes_len( + ctx: &Ctx<'_>, + algorithm_name: &str, + field: &str, + bytes: &[u8], + expected: usize, +) -> Result<()> { + if bytes.len() != expected { + return Err(DOMException::data_error( + ctx, + [algorithm_name, " JWK ", field, " has invalid length"].concat(), + )); + } + Ok(()) +} + +#[cfg(all())] +fn validate_okp_jwk_key_pair<'js>( + ctx: &Ctx<'js>, + private_key: &[u8], + public_key: &[u8], + is_ed25519: bool, +) -> Result<()> { + let derived_public_key = if is_ed25519 { + let secret_key: [u8; 32] = private_key.try_into().or_throw_data_error(ctx)?; + SigningKey::from_bytes(&secret_key) + .verifying_key() + .to_bytes() + .to_vec() + } else { + let secret_key: [u8; 32] = private_key.try_into().or_throw_data_error(ctx)?; + let secret = StaticSecret::from(secret_key); + PublicKey::from(&secret).as_bytes().to_vec() + }; + if derived_public_key.as_slice() != public_key { + return Err(DOMException::data_error(ctx, "JWK key pair is invalid")); + } + Ok(()) +} + +pub fn extract_sha_hash<'js>(ctx: &Ctx<'js>, obj: &Object<'js>) -> Result { + let hash: Value = obj.get_required("hash", "algorithm")?; + let hash = if let Some(string) = hash.as_string() { + string.to_string() + } else if let Some(obj) = hash.into_object() { + obj.get_required("name", "hash") + } else { + return Err(DOMException::not_supported_error( + ctx, + "hash must be a string or an object", + )); + }?; + let hash = normalize_algorithm_name(&hash); + HashAlgorithm::from_strict_str(hash.as_str()).or_throw_dom(ctx) +} + +fn create_hash_object<'js>(ctx: &Ctx<'js>, hash: &HashAlgorithm) -> Result> { + let hash_obj = Object::new(ctx.clone())?; + hash_obj.set(PredefinedAtom::Name, hash.as_str())?; + Ok(hash_obj) +} + +#[cfg(all())] +pub fn hash_mismatch_error(ctx: &Ctx<'_>, hash: &HashAlgorithm) -> Result { + Err(DOMException::type_mismatch_error( + ctx, + ["Algorithm hash expected to be ", hash.as_str()].concat(), + )) +} + +#[cfg(all())] +trait DataErrorResultExt { + fn or_throw_data_error(self, ctx: &Ctx<'_>) -> Result; +} + +#[cfg(all())] +impl DataErrorResultExt for std::result::Result +where + E: std::fmt::Display, +{ + fn or_throw_data_error(self, ctx: &Ctx<'_>) -> Result { + self.map_err(|e| DataError(e.to_string())).or_throw_dom(ctx) + } +} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/mod.rs b/stdlib/src/llrt/llrt_crypto/subtle/mod.rs new file mode 100644 index 00000000..42eb6e9a --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/subtle/mod.rs @@ -0,0 +1,183 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +mod crypto_key; +mod derive_algorithm; +mod derive_bits; +mod derive_keys; +mod digest; +mod encryption; +mod encryption_algorithm; +#[cfg(all())] +mod export_key; +mod generate_key; +#[cfg(all())] +mod import_key; +#[cfg(all())] +mod key_algorithm; +mod sign; +mod sign_algorithm; +mod util; +mod verify; +#[cfg(all())] +mod wrapping; + +pub use crypto_key::CryptoKey; +pub use derive_bits::subtle_derive_bits; +pub use derive_keys::subtle_derive_key; +pub use digest::subtle_digest; +pub use encryption::subtle_decrypt; +pub use encryption::subtle_encrypt; +#[cfg(all())] +pub use export_key::subtle_export_key; +pub use generate_key::subtle_generate_key; +#[cfg(all())] +pub use import_key::subtle_import_key; +#[cfg(all())] +use key_algorithm::KeyAlgorithm; +pub use sign::subtle_sign; +pub use verify::subtle_verify; +#[cfg(all())] +pub use wrapping::subtle_unwrap_key; +#[cfg(all())] +pub use wrapping::subtle_wrap_key; + +// Stub implementations for limited crypto providers (no _subtle-full) +#[cfg(not(all()))] +mod key_algorithm; +#[cfg(not(all()))] +use key_algorithm::KeyAlgorithm; + +use crate::llrt_exceptions::DOMException; +use crate::llrt_utils::{object::ObjectExt, str_enum}; +use rquickjs::{atom::PredefinedAtom, Ctx, Error, Exception, Object, Result, Value}; + +use crate::llrt_crypto::provider::{CryptoProvider, SimpleDigest}; + +use crate::llrt_crypto::hash::HashAlgorithm; + +#[rquickjs::class] +#[derive(rquickjs::JsLifetime, rquickjs::class::Trace)] +pub struct SubtleCrypto {} + +#[rquickjs::methods] +impl SubtleCrypto { + #[qjs(constructor)] + pub fn new(ctx: Ctx<'_>) -> Result { + Err(Exception::throw_type(&ctx, "Illegal constructor")) + } + + #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] + pub fn to_string_tag() -> &'static str { + stringify!(SubtleCrypto) + } +} + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum EllipticCurve { + P256, + P384, + P521, +} + +str_enum!(EllipticCurve,P256 => "P-256", P384 => "P-384", P521 => "P-521"); + +pub enum EncryptionMode { + Encryption, + #[allow(dead_code)] + Wrapping(u8), //padding byte +} + +pub fn rsa_hash_digest<'a>( + ctx: &Ctx<'_>, + key: &'a CryptoKey, + data: &'a [u8], + algorithm_name: &str, +) -> Result<(&'a HashAlgorithm, Vec)> { + let hash = match &key.algorithm { + KeyAlgorithm::Rsa { hash, .. } => hash, + _ => return algorithm_mismatch_error(ctx, algorithm_name), + }; + if !matches!( + hash, + HashAlgorithm::Sha1 | HashAlgorithm::Sha256 | HashAlgorithm::Sha384 | HashAlgorithm::Sha512 + ) { + return Err(Exception::throw_message( + ctx, + "Only SHA-1, SHA-256, SHA-384 or SHA-512 is supported for RSA", + )); + } + + let mut hasher = crate::llrt_crypto::CRYPTO_PROVIDER.digest(*hash); + hasher.update(data); + let digest = hasher.finalize(); + + Ok((hash, digest)) +} + +pub fn to_name_and_maybe_object<'js>( + ctx: &Ctx<'js>, + value: Value<'js>, +) -> Result<(String, Result>)> { + let obj; + let name = if let Some(string) = value.as_string() { + obj = Err(Error::new_from_js_message( + "string", + "object", + "algorithm is not an object", + )); + string.to_string()? + } else if let Some(object) = value.into_object() { + let name = object.get_required("name", "algorithm")?; + obj = Ok(object); + name + } else { + return Err(Exception::throw_message( + ctx, + "algorithm must be a string or an object", + )); + }; + Ok((name, obj)) +} + +pub fn normalize_algorithm_name(name: &str) -> String { + let name = name.to_ascii_uppercase(); + match name.as_str() { + "ED25519" => "Ed25519".to_string(), + "RSASSA-PKCS1-V1_5" => "RSASSA-PKCS1-v1_5".to_string(), + _ => name, + } +} + +pub fn algorithm_mismatch_error(ctx: &Ctx<'_>, expected_algorithm: &str) -> Result { + Err(DOMException::type_mismatch_error( + ctx, + ["Key algorithm must be ", expected_algorithm].concat(), + )) +} + +pub fn algorithm_not_supported_error(ctx: &Ctx<'_>) -> Result { + Err(DOMException::not_supported_error( + ctx, + "Algorithm not supported", + )) +} + +pub fn algorithm_invalid_access_error(ctx: &Ctx<'_>, expected_algorithm: &str) -> Result { + Err(DOMException::invalid_access_error( + ctx, + ["Key algorithm must be ", expected_algorithm].concat(), + )) +} + +// Stub implementations for providers without _subtle-full +#[cfg(not(all()))] +mod stubs; +#[cfg(not(all()))] +pub use stubs::subtle_export_key; +#[cfg(not(all()))] +pub use stubs::subtle_import_key; +#[cfg(not(all()))] +pub use stubs::subtle_unwrap_key; +#[cfg(not(all()))] +pub use stubs::subtle_wrap_key; diff --git a/stdlib/src/llrt/llrt_crypto/subtle/sign.rs b/stdlib/src/llrt/llrt_crypto/subtle/sign.rs new file mode 100644 index 00000000..81660a1d --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/subtle/sign.rs @@ -0,0 +1,129 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::future::Future; + +use crate::llrt_crypto::provider::{CryptoProvider, HmacProvider}; +use crate::llrt_utils::bytes::ObjectBytes; +use rquickjs::{ArrayBuffer, Class, Ctx, FromJs, Result, Value}; + +use crate::llrt_crypto::CRYPTO_PROVIDER; + +use super::{ + algorithm_invalid_access_error, + crypto_key::{CryptoKey, KeyKind}, + key_algorithm::KeyAlgorithm, + rsa_hash_digest, + sign_algorithm::SigningAlgorithm, + util::ResultDomExt, +}; + +pub fn subtle_sign<'js>( + ctx: Ctx<'js>, + algorithm: Value<'js>, + key: Class<'js, CryptoKey<'js>>, + data: ObjectBytes<'js>, +) -> impl Future>> + 'js { + // Keep preparation outside the async block: Rust async function bodies are deferred until + // polled, while WebCrypto requires call-time algorithm normalization and input snapshotting. + // Retaining the Result lets preparation failures reject the rquickjs-created Promise. + let prepared = prepare_sign(&ctx, algorithm, key, data); + + async move { + let (algorithm, key, data) = prepared?; + let key = key.borrow(); + if key.name.as_ref() != algorithm.name() { + return algorithm_invalid_access_error(&ctx, algorithm.name()); + } + key.check_validity("sign").or_throw_dom(&ctx)?; + let expected_kind = match &algorithm { + SigningAlgorithm::Hmac => KeyKind::Secret, + _ => KeyKind::Private, + }; + key.check_kind(expected_kind).or_throw_dom(&ctx)?; + + let bytes = sign(&ctx, &algorithm, &key, &data)?; + ArrayBuffer::new(ctx, bytes) + } +} + +fn prepare_sign<'js>( + ctx: &Ctx<'js>, + algorithm: Value<'js>, + key: Class<'js, CryptoKey<'js>>, + data: ObjectBytes<'js>, +) -> Result<(SigningAlgorithm, Class<'js, CryptoKey<'js>>, Vec)> { + let algorithm = SigningAlgorithm::from_js(ctx, algorithm)?; + let data = data.as_bytes_opt().unwrap_or_default().to_vec(); + Ok((algorithm, key, data)) +} + +fn sign( + ctx: &Ctx<'_>, + algorithm: &SigningAlgorithm, + key: &CryptoKey, + data: &[u8], +) -> Result> { + let handle = key.handle.as_ref(); + Ok(match algorithm { + SigningAlgorithm::Ecdsa { hash } => { + let curve = match &key.algorithm { + KeyAlgorithm::Ec { curve, .. } => curve, + _ => return algorithm_invalid_access_error(ctx, "ECDSA"), + }; + + let digest = crate::llrt_crypto::subtle::digest::digest(hash, data); + + crate::llrt_crypto::CRYPTO_PROVIDER + .ecdsa_sign(*curve, handle, &digest) + .or_throw_dom(ctx)? + } + SigningAlgorithm::Ed25519 => { + if !matches!(&key.algorithm, KeyAlgorithm::Ed25519) { + return algorithm_invalid_access_error(ctx, "Ed25519"); + } + crate::llrt_crypto::CRYPTO_PROVIDER + .ed25519_sign(handle, data) + .or_throw_dom(ctx)? + } + SigningAlgorithm::Hmac => { + let hash = if let KeyAlgorithm::Hmac { hash, .. } = &key.algorithm { + hash + } else { + return algorithm_invalid_access_error(ctx, "HMAC"); + }; + + let mut hmac = CRYPTO_PROVIDER.hmac(*hash, handle); + hmac.update(data); + hmac.finalize() + } + SigningAlgorithm::RsaPss { salt_length } => { + let (hash, digest) = rsa_hash_digest(ctx, key, data, "RSA-PSS")?; + crate::llrt_crypto::CRYPTO_PROVIDER + .rsa_pss_sign(&key.handle, digest.as_ref(), *salt_length as usize, *hash) + .or_throw_dom(ctx)? + } + SigningAlgorithm::RsassaPkcs1v15 => { + let (hash, digest) = rsa_hash_digest(ctx, key, data, "RSASSA-PKCS1-v1_5")?; + crate::llrt_crypto::CRYPTO_PROVIDER + .rsa_pkcs1v15_sign(&key.handle, digest.as_ref(), *hash) + .or_throw_dom(ctx)? + } + }) +} + +// // Helper function for RSA signing +// fn rsa_sign( +// ctx: &Ctx<'_>, +// key: &CryptoKey, +// algorithm_name: &str, +// data: &[u8], +// sign_fn: F, +// ) -> Result> +// where +// F: FnOnce(&HashAlgorithm, &[u8], &rsa::RsaPrivateKey) -> Result>, +// { +// let (hash, digest) = rsa_hash_digest(ctx, key, data, algorithm_name)?; + +// sign_fn(hash, digest.as_ref()) +// } diff --git a/stdlib/src/llrt/llrt_crypto/subtle/sign_algorithm.rs b/stdlib/src/llrt/llrt_crypto/subtle/sign_algorithm.rs new file mode 100644 index 00000000..c48dfb19 --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/subtle/sign_algorithm.rs @@ -0,0 +1,58 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use crate::llrt_utils::object::ObjectExt; +use rquickjs::{Ctx, FromJs, Result, Value}; + +use crate::llrt_crypto::hash::HashAlgorithm; + +use super::{ + algorithm_not_supported_error, key_algorithm::extract_sha_hash, normalize_algorithm_name, + to_name_and_maybe_object, +}; + +#[derive(Debug)] +pub enum SigningAlgorithm { + Ecdsa { hash: HashAlgorithm }, + Ed25519, + RsaPss { salt_length: u32 }, + RsassaPkcs1v15, + Hmac, +} + +impl<'js> FromJs<'js> for SigningAlgorithm { + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let (name, obj) = to_name_and_maybe_object(ctx, value)?; + let name = normalize_algorithm_name(&name); + + let algorithm = match name.as_str() { + "Ed25519" => SigningAlgorithm::Ed25519, + "HMAC" => SigningAlgorithm::Hmac, + "RSASSA-PKCS1-v1_5" => SigningAlgorithm::RsassaPkcs1v15, + "ECDSA" => { + let obj = obj?; + let hash = extract_sha_hash(ctx, &obj)?; + SigningAlgorithm::Ecdsa { hash } + } + "RSA-PSS" => { + let salt_length = obj?.get_required("saltLength", "algorithm")?; + + SigningAlgorithm::RsaPss { salt_length } + } + _ => return algorithm_not_supported_error(ctx), + }; + Ok(algorithm) + } +} + +impl SigningAlgorithm { + pub fn name(&self) -> &'static str { + match self { + SigningAlgorithm::Ecdsa { .. } => "ECDSA", + SigningAlgorithm::Ed25519 => "Ed25519", + SigningAlgorithm::RsaPss { .. } => "RSA-PSS", + SigningAlgorithm::RsassaPkcs1v15 => "RSASSA-PKCS1-v1_5", + SigningAlgorithm::Hmac => "HMAC", + } + } +} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/stubs.rs b/stdlib/src/llrt/llrt_crypto/subtle/stubs.rs new file mode 100644 index 00000000..e54d224e --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/subtle/stubs.rs @@ -0,0 +1,64 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Stub implementations for SubtleCrypto operations when `_rustcrypto` feature is disabled. +//! These return errors indicating the operation is not supported. + +use rquickjs::{Ctx, Exception, Object, Result, Value}; + +use super::{crypto_key::CryptoKey, encryption_algorithm, key_algorithm}; + +pub async fn subtle_export_key<'js>( + ctx: Ctx<'js>, + _format: key_algorithm::KeyFormat, + _key: rquickjs::Class<'js, CryptoKey<'js>>, +) -> Result> { + Err(Exception::throw_message( + &ctx, + "exportKey is not supported with this crypto provider", + )) +} + +pub async fn subtle_import_key<'js>( + ctx: Ctx<'js>, + _format: key_algorithm::KeyFormat, + _key_data: Value<'js>, + _algorithm: Value<'js>, + _extractable: bool, + _key_usages: rquickjs::Array<'js>, +) -> Result>> { + Err(Exception::throw_message( + &ctx, + "importKey is not supported with this crypto provider", + )) +} + +pub async fn subtle_wrap_key<'js>( + ctx: Ctx<'js>, + _format: key_algorithm::KeyFormat, + _key: rquickjs::Class<'js, CryptoKey<'js>>, + _wrapping_key: rquickjs::Class<'js, CryptoKey<'js>>, + _wrap_algo: encryption_algorithm::EncryptionAlgorithm, +) -> Result> { + Err(Exception::throw_message( + &ctx, + "wrapKey is not supported with this crypto provider", + )) +} + +pub async fn subtle_unwrap_key<'js>( + _format: key_algorithm::KeyFormat, + wrapped_key: rquickjs::ArrayBuffer<'js>, + _unwrapping_key: rquickjs::Class<'js, CryptoKey<'js>>, + _unwrap_algo: encryption_algorithm::EncryptionAlgorithm, + _unwrapped_key_algo: Value<'js>, + _extractable: bool, + _key_usages: rquickjs::Array<'js>, +) -> Result>> { + let ctx = wrapped_key.ctx().clone(); + Err(Exception::throw_message( + &ctx, + "unwrapKey is not supported with this crypto provider", + )) +} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/util.rs b/stdlib/src/llrt/llrt_crypto/subtle/util.rs new file mode 100644 index 00000000..00769d2c --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/subtle/util.rs @@ -0,0 +1,87 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::{fmt::Display, result::Result as StdResult}; + +use crate::llrt_exceptions::DOMException; +use rquickjs::{Ctx, Error, Result}; + +use crate::llrt_crypto::provider::CryptoError; + +pub trait IntoDomException { + fn into_dom_exception(self, ctx: &Ctx, msg: &str) -> Error; +} + +impl IntoDomException for CryptoError { + fn into_dom_exception(self, ctx: &Ctx, msg: &str) -> Error { + let message = with_message(&self, msg); + match self { + CryptoError::UnsupportedAlgorithm => DOMException::not_supported_error(ctx, message), + CryptoError::InvalidLength + | CryptoError::InvalidKey(_) + | CryptoError::InvalidData(_) + | CryptoError::InvalidSignature(_) => DOMException::data_error(ctx, message), + CryptoError::SigningFailed(_) + | CryptoError::VerificationFailed + | CryptoError::OperationFailed(_) + | CryptoError::DerivationFailed(_) + | CryptoError::EncryptionFailed(_) + | CryptoError::DecryptionFailed(_) => DOMException::operation_error(ctx, message), + CryptoError::InvalidAccess(_) => DOMException::invalid_access_error(ctx, message), + } + } +} + +pub trait ResultDomExt { + fn or_throw_dom(self, ctx: &Ctx) -> Result; + fn or_throw_dom_with_msg(self, ctx: &Ctx, msg: &str) -> Result; +} + +impl ResultDomExt for StdResult { + fn or_throw_dom(self, ctx: &Ctx) -> Result { + self.map_err(|e| e.into_dom_exception(ctx, "")) + } + fn or_throw_dom_with_msg(self, ctx: &Ctx, msg: &str) -> Result { + self.map_err(|e| e.into_dom_exception(ctx, msg)) + } +} + +impl ResultDomExt for Option { + fn or_throw_dom(self, ctx: &Ctx) -> Result { + self.ok_or_else(|| DOMException::not_supported_error(ctx, "Value is not present")) + } + fn or_throw_dom_with_msg(self, ctx: &Ctx, msg: &str) -> Result { + let message = if msg.is_empty() { + "Value is not present" + } else { + msg + }; + self.ok_or_else(|| DOMException::not_supported_error(ctx, message)) + } +} + +pub struct NotSupportedError(pub E); + +impl IntoDomException for NotSupportedError { + fn into_dom_exception(self, ctx: &Ctx, msg: &str) -> Error { + DOMException::not_supported_error(ctx, with_message(self.0, msg)) + } +} + +#[allow(dead_code)] +pub struct DataError(pub E); + +#[allow(dead_code)] +impl IntoDomException for DataError { + fn into_dom_exception(self, ctx: &Ctx, msg: &str) -> Error { + DOMException::data_error(ctx, with_message(self.0, msg)) + } +} + +fn with_message(err: E, msg: &str) -> String { + if msg.is_empty() { + err.to_string() + } else { + [msg, ": ", &err.to_string()].concat() + } +} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/verify.rs b/stdlib/src/llrt/llrt_crypto/subtle/verify.rs new file mode 100644 index 00000000..2e84550e --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/subtle/verify.rs @@ -0,0 +1,155 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::future::Future; + +use crate::llrt_crypto::provider::{CryptoError, CryptoProvider, HmacProvider}; +use crate::llrt_utils::bytes::ObjectBytes; +use rquickjs::{Class, Ctx, FromJs, Result, Value}; + +use crate::llrt_crypto::CRYPTO_PROVIDER; + +use super::{ + algorithm_invalid_access_error, + crypto_key::{CryptoKey, KeyKind}, + digest, + key_algorithm::KeyAlgorithm, + rsa_hash_digest, + sign_algorithm::SigningAlgorithm, + util::ResultDomExt, +}; + +pub fn subtle_verify<'js>( + ctx: Ctx<'js>, + algorithm: Value<'js>, + key: Class<'js, CryptoKey<'js>>, + signature: ObjectBytes<'js>, + data: ObjectBytes<'js>, +) -> impl Future> + 'js { + // Keep preparation outside the async block: Rust async function bodies are deferred until + // polled, while WebCrypto requires call-time algorithm normalization and input snapshotting. + // Retaining the Result lets preparation failures reject the rquickjs-created Promise. + let prepared = prepare_verify(&ctx, algorithm, key, signature, data); + + async move { + let PreparedVerify { + algorithm, + key, + signature, + data, + } = prepared?; + let key = key.borrow(); + if key.name.as_ref() != algorithm.name() { + return algorithm_invalid_access_error(&ctx, algorithm.name()); + } + key.check_validity("verify").or_throw_dom(&ctx)?; + let expected_kind = match &algorithm { + SigningAlgorithm::Hmac => KeyKind::Secret, + _ => KeyKind::Public, + }; + key.check_kind(expected_kind).or_throw_dom(&ctx)?; + + verify(&ctx, &algorithm, &key, &signature, &data) + } +} + +struct PreparedVerify<'js> { + algorithm: SigningAlgorithm, + key: Class<'js, CryptoKey<'js>>, + signature: Vec, + data: Vec, +} + +fn prepare_verify<'js>( + ctx: &Ctx<'js>, + algorithm: Value<'js>, + key: Class<'js, CryptoKey<'js>>, + signature: ObjectBytes<'js>, + data: ObjectBytes<'js>, +) -> Result> { + let algorithm = SigningAlgorithm::from_js(ctx, algorithm)?; + let signature = signature.as_bytes_opt().unwrap_or_default().to_vec(); + let data = data.as_bytes_opt().unwrap_or_default().to_vec(); + Ok(PreparedVerify { + algorithm, + key, + signature, + data, + }) +} + +fn verify( + ctx: &Ctx<'_>, + algorithm: &SigningAlgorithm, + key: &CryptoKey, + signature: &[u8], + data: &[u8], +) -> Result { + let handle = key.handle.as_ref(); + Ok(match algorithm { + SigningAlgorithm::Ecdsa { hash } => { + let curve = match &key.algorithm { + KeyAlgorithm::Ec { curve, .. } => curve, + _ => return algorithm_invalid_access_error(ctx, "ECDSA"), + }; + + let digest = digest::digest(hash, data); + + crate::llrt_crypto::CRYPTO_PROVIDER + .ecdsa_verify(*curve, handle, signature, &digest) + .into_verification(ctx)? + } + SigningAlgorithm::Ed25519 => { + if !matches!(&key.algorithm, KeyAlgorithm::Ed25519) { + return algorithm_invalid_access_error(ctx, "Ed25519"); + } + + crate::llrt_crypto::CRYPTO_PROVIDER + .ed25519_verify(handle, signature, data) + .into_verification(ctx)? + } + SigningAlgorithm::Hmac => { + let hash = match &key.algorithm { + KeyAlgorithm::Hmac { hash, .. } => hash, + _ => return algorithm_invalid_access_error(ctx, "HMAC"), + }; + + let mut hmac = CRYPTO_PROVIDER.hmac(*hash, handle); + hmac.update(data); + let computed_signature = hmac.finalize(); + + computed_signature == signature + } + SigningAlgorithm::RsaPss { salt_length } => { + let (hash, digest) = rsa_hash_digest(ctx, key, data, "RSA-PSS")?; + crate::llrt_crypto::CRYPTO_PROVIDER + .rsa_pss_verify( + &key.handle, + signature, + digest.as_ref(), + *salt_length as usize, + *hash, + ) + .into_verification(ctx)? + } + SigningAlgorithm::RsassaPkcs1v15 => { + let (hash, digest) = rsa_hash_digest(ctx, key, data, "RSASSA-PKCS1-v1_5")?; + crate::llrt_crypto::CRYPTO_PROVIDER + .rsa_pkcs1v15_verify(&key.handle, signature, digest.as_ref(), *hash) + .into_verification(ctx)? + } + }) +} + +trait VerificationResultExt { + fn into_verification(self, ctx: &Ctx<'_>) -> Result; +} + +impl VerificationResultExt for std::result::Result { + fn into_verification(self, ctx: &Ctx<'_>) -> Result { + match self { + Err(CryptoError::InvalidSignature(_)) => Ok(false), + result => result.or_throw_dom(ctx), + } + } +} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/wrapping.rs b/stdlib/src/llrt/llrt_crypto/subtle/wrapping.rs new file mode 100644 index 00000000..0dfb61a2 --- /dev/null +++ b/stdlib/src/llrt/llrt_crypto/subtle/wrapping.rs @@ -0,0 +1,93 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use crate::llrt_json::{parse::json_parse, stringify::json_stringify}; +use crate::llrt_utils::{bytes::ObjectBytes, object::ObjectExt, result::ResultExt}; +use rquickjs::{Array, ArrayBuffer, Class, Ctx, Result, Value}; + +use super::{ + crypto_key::CryptoKey, + encryption::{self, encrypt_decrypt}, + encryption_algorithm::EncryptionAlgorithm, + export_key::{export_key, ExportOutput}, + import_key::import_key, + key_algorithm::{KeyFormat, KeyFormatData}, + EncryptionMode, +}; + +pub async fn subtle_wrap_key<'js>( + ctx: Ctx<'js>, + format: KeyFormat, + key: Class<'js, CryptoKey<'js>>, + wrapping_key: Class<'js, CryptoKey<'js>>, + wrap_algo: EncryptionAlgorithm, +) -> Result> { + let key = key.borrow(); + + let export = export_key(&ctx, format, &key)?; + + let (bytes, padding) = match export { + ExportOutput::Bytes(bytes) => (bytes, 0), + ExportOutput::Object(value) => { + let json = json_stringify(&ctx, value.into_value())?.unwrap(); + (json.into_bytes(), b' ') + } + }; + + let wrapping_key = wrapping_key.borrow(); + wrapping_key.check_validity("wrapKey").or_throw(&ctx)?; + + let bytes = encrypt_decrypt( + &ctx, + &wrap_algo, + &wrapping_key, + &bytes, + EncryptionMode::Wrapping(padding), + encryption::EncryptionOperation::Encrypt, + )?; + + ArrayBuffer::new(ctx, bytes) +} + +//cant take more than 7 args +pub async fn subtle_unwrap_key<'js>( + format: KeyFormat, + wrapped_key: Value<'js>, + unwrapping_key: Class<'js, CryptoKey<'js>>, + unwrap_algo: EncryptionAlgorithm, + unwrapped_key_algo: Value<'js>, + extractable: bool, + key_usages: Array<'js>, +) -> Result>> { + let unwrapping_key = unwrapping_key.borrow(); + let ctx = wrapped_key.ctx().clone(); + unwrapping_key.check_validity("unwrapKey").or_throw(&ctx)?; + + let bytes = ObjectBytes::from(&ctx, &wrapped_key)?; + let bytes = bytes.as_bytes(&ctx)?; + + let padding = match format { + KeyFormat::Jwk => b' ', + _ => 0, + }; + + let bytes = encrypt_decrypt( + &ctx, + &unwrap_algo, + &unwrapping_key, + bytes, + EncryptionMode::Wrapping(padding), + encryption::EncryptionOperation::Decrypt, + )?; + + let key_format = match format { + KeyFormat::Jwk => { + KeyFormatData::Jwk(json_parse(&ctx, bytes)?.into_object_or_throw(&ctx, "wrappedKey")?) + } + KeyFormat::Raw => KeyFormatData::Raw(ObjectBytes::Vec(bytes)), + KeyFormat::Spki => KeyFormatData::Spki(ObjectBytes::Vec(bytes)), + KeyFormat::Pkcs8 => KeyFormatData::Pkcs8(ObjectBytes::Vec(bytes)), + }; + + import_key(ctx, key_format, unwrapped_key_algo, extractable, key_usages) +} diff --git a/stdlib/src/llrt/llrt_encoding/lib.rs b/stdlib/src/llrt/llrt_encoding/lib.rs new file mode 100644 index 00000000..26d56f7c --- /dev/null +++ b/stdlib/src/llrt/llrt_encoding/lib.rs @@ -0,0 +1,254 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::borrow::Cow; + +use hex_simd::AsciiCase; + +#[derive(Clone, PartialEq)] +pub enum Encoder { + Hex, + Base64, + Windows1252, + Utf8, + Utf16le, + Utf16be, +} + +const ENCODING_MAP: phf::Map<&'static str, Encoder> = phf::phf_map! { + "buffer" => Encoder::Utf8, + "hex" => Encoder::Hex, + "base64" => Encoder::Base64, + "unicode-1-1-utf-8" => Encoder::Utf8, + "unicode11utf8" => Encoder::Utf8, + "unicode20utf8" => Encoder::Utf8, + "utf-8" => Encoder::Utf8, + "utf8" => Encoder::Utf8, + "x-unicode20utf8" => Encoder::Utf8, + "csunicode" => Encoder::Utf16le, + "iso-10646-ucs-2" => Encoder::Utf16le, + "ucs-2" => Encoder::Utf16le, + "ucs2" => Encoder::Utf16le, + "unicode" => Encoder::Utf16le, + "unicodefeff" => Encoder::Utf16le, + "utf-16" => Encoder::Utf16le, + "utf-16le" => Encoder::Utf16le, + "utf16le" => Encoder::Utf16le, + "unicodefffe" => Encoder::Utf16be, + "utf-16be" => Encoder::Utf16be, + "ansi_x3.4-1968" => Encoder::Windows1252, + "ascii" => Encoder::Windows1252, + "cp1252" => Encoder::Windows1252, + "cp819" => Encoder::Windows1252, + "csisolatin1" => Encoder::Windows1252, + "ibm819" => Encoder::Windows1252, + "iso-8859-1" => Encoder::Windows1252, + "iso-ir-100" => Encoder::Windows1252, + "iso8859-1" => Encoder::Windows1252, + "iso88591" => Encoder::Windows1252, + "iso_8859-1" => Encoder::Windows1252, + "iso_8859-1:1987" => Encoder::Windows1252, + "l1" => Encoder::Windows1252, + "latin1" => Encoder::Windows1252, + "us-ascii" => Encoder::Windows1252, + "windows-1252" => Encoder::Windows1252, + "x-cp1252" => Encoder::Windows1252, +}; + +impl Encoder { + pub fn from_optional_str(encoding: Option<&str>) -> Result { + match encoding { + Some(label) if !label.is_empty() => Self::from_str(label), + _ => Ok(Self::Utf8), + } + } + + #[allow(clippy::should_implement_trait)] + pub fn from_str(encoding: &str) -> Result { + ENCODING_MAP + .get(encoding.trim_ascii().to_ascii_lowercase().as_str()) + .cloned() + .ok_or_else(|| ["The \"", encoding, "\" encoding is not supported"].concat()) + } + + pub fn encode_to_string(&self, bytes: &[u8], lossy: bool) -> Result { + match self { + Self::Hex => Ok(bytes_to_hex_string(bytes)), + Self::Base64 => Ok(bytes_to_b64_string(bytes)), + Self::Utf8 | Self::Windows1252 => bytes_to_utf8_string(bytes, lossy), + Self::Utf16le => bytes_to_utf16_string(bytes, Endian::Little, lossy), + Self::Utf16be => bytes_to_utf16_string(bytes, Endian::Big, lossy), + } + } + + #[allow(dead_code)] + pub fn encode(&self, bytes: &[u8]) -> Result, String> { + match self { + Self::Hex => Ok(bytes_to_hex(bytes)), + Self::Base64 => Ok(bytes_to_b64(bytes)), + Self::Utf8 | Self::Windows1252 | Self::Utf16le | Self::Utf16be => Ok(bytes.to_vec()), + } + } + + pub fn decode<'a, T: Into>>(&self, bytes: T) -> Result, String> { + match self { + Self::Hex => bytes_from_hex(bytes), + Self::Base64 => bytes_from_b64(bytes), + Self::Utf8 | Self::Windows1252 | Self::Utf16le | Self::Utf16be => { + Ok(bytes.into().into()) + } + } + } + + pub fn decode_from_string(&self, string: String) -> Result, String> { + match self { + Self::Hex => bytes_from_hex(string.into_bytes()), + Self::Base64 => bytes_from_b64(string.into_bytes()), + Self::Utf8 | Self::Windows1252 => Ok(string.into_bytes()), + Self::Utf16le => Ok(string + .encode_utf16() + .flat_map(|utf16| utf16.to_le_bytes()) + .collect::>()), + Self::Utf16be => Ok(string + .encode_utf16() + .flat_map(|utf16| utf16.to_be_bytes()) + .collect::>()), + } + } + + pub fn as_label(&self) -> &str { + match self { + Self::Hex => "hex", + Self::Base64 => "base64", + Self::Windows1252 => "windows-1252", + Self::Utf8 => "utf-8", + Self::Utf16le => "utf-16le", + Self::Utf16be => "utf-16be", + } + } +} + +pub fn bytes_to_hex(bytes: &[u8]) -> Vec { + hex_simd::encode_type(bytes, AsciiCase::Lower) +} + +pub fn bytes_from_hex<'a, T: Into>>(hex_bytes: T) -> Result, String> { + hex_simd::decode_to_vec(hex_bytes.into()).map_err(|err| err.to_string()) +} + +pub fn bytes_from_b64<'a, T: Into>>(base64_bytes: T) -> Result, String> { + let bytes: Cow<'a, [u8]> = base64_bytes.into(); + + //need to collect since memchr2_iter is borrowing bytes. This is fine since we're unlikely to contain url safe base64 + let url_safe_byte_positions: Vec = memchr::memchr2_iter(b'-', b'_', &bytes).collect(); + + if url_safe_byte_positions.is_empty() { + return base64_simd::forgiving_decode_to_vec(&bytes).map_err(|e| e.to_string()); + } + + //doesn't allocate for already owned data + let mut bytes = bytes.into_owned(); + for pos in url_safe_byte_positions { + bytes[pos] = match bytes[pos] { + b'-' => b'+', + b'_' => b'/', + _ => unreachable!(), + }; + } + base64_simd::forgiving_decode_to_vec(&bytes).map_err(|e| e.to_string()) +} + +/// Strict standard-base64 decode (single SIMD pass): rejects url-safe chars, +/// whitespace and bad padding, matching @smithy/util-base64 semantics. +pub fn bytes_from_b64_strict(bytes: &[u8]) -> Result, String> { + base64_simd::STANDARD + .decode_to_vec(bytes) + .map_err(|e| e.to_string()) +} + +pub fn bytes_to_b64_string(bytes: &[u8]) -> String { + base64_simd::STANDARD.encode_to_string(bytes) +} + +pub fn bytes_to_b64_url_safe_string(bytes: &[u8]) -> String { + base64_simd::URL_SAFE_NO_PAD.encode_to_string(bytes) +} + +pub fn bytes_from_b64_url_safe(bytes: &[u8]) -> Result, String> { + base64_simd::URL_SAFE_NO_PAD + .decode_to_vec(bytes) + .map_err(|e| e.to_string()) +} + +pub fn bytes_to_b64(bytes: &[u8]) -> Vec { + base64_simd::STANDARD.encode_type(bytes) +} + +pub fn bytes_to_hex_string(bytes: &[u8]) -> String { + hex_simd::encode_to_string(bytes, AsciiCase::Lower) +} + +pub fn bytes_to_utf8_string(bytes: &[u8], lossy: bool) -> Result { + if lossy { + Ok(String::from_utf8_lossy(bytes).to_string()) + } else { + String::from_utf8(bytes.to_vec()).map_err(|e| e.to_string()) + } +} + +#[derive(Clone, Copy)] +pub enum Endian { + Little, + Big, +} + +pub fn bytes_to_utf16_string(bytes: &[u8], endian: Endian, lossy: bool) -> Result { + if !lossy && !bytes.len().is_multiple_of(2) { + return Err("Input byte slice length must be even".to_string()); + } + + let data16: Vec = match endian { + Endian::Little => bytes + .as_chunks::<2>() + .0 + .iter() + .copied() + .map(u16::from_le_bytes) + .collect(), + Endian::Big => bytes + .as_chunks::<2>() + .0 + .iter() + .copied() + .map(u16::from_be_bytes) + .collect(), + }; + + let mut result = if lossy { + String::from_utf16_lossy(&data16) + } else { + String::from_utf16(&data16).map_err(|e| e.to_string())? + }; + + // Odd trailing byte in lossy mode produces a replacement character + if lossy && !bytes.len().is_multiple_of(2) { + result.push('\u{FFFD}'); + } + + Ok(result) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn b64_strict_matches_smithy_semantics() { + // canonical decodes + assert_eq!(bytes_from_b64_strict(b"SGVsbG8=").unwrap(), b"Hello"); + // url-safe, whitespace, bad-padding are rejected (like @smithy/util-base64) + assert!(bytes_from_b64_strict(b"-_8=").is_err()); + assert!(bytes_from_b64_strict(b"SGVs bG8=").is_err()); + assert!(bytes_from_b64_strict(b"SGVsbG8").is_err()); + } +} diff --git a/stdlib/src/llrt/llrt_events/custom_event.rs b/stdlib/src/llrt/llrt_events/custom_event.rs new file mode 100644 index 00000000..4d24be74 --- /dev/null +++ b/stdlib/src/llrt/llrt_events/custom_event.rs @@ -0,0 +1,40 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use rquickjs::{prelude::Opt, Ctx, IntoJs, Null, Result, Value}; + +use crate::llrt_utils::object::ObjectExt; + +#[rquickjs::class] +#[derive(rquickjs::class::Trace, rquickjs::JsLifetime)] +pub struct CustomEvent<'js> { + event_type: String, + detail: Option>, +} + +#[rquickjs::methods] +impl<'js> CustomEvent<'js> { + #[qjs(constructor)] + pub fn new(event_type: String, options: Opt>) -> Result { + let mut detail = None; + if let Some(options) = options.0 { + if let Some(opt) = options.get_optional("detail")? { + detail = opt; + } + } + Ok(Self { event_type, detail }) + } + + #[qjs(get)] + pub fn detail(&self, ctx: Ctx<'js>) -> Result> { + if let Some(detail) = &self.detail { + return Ok(detail.clone()); + } + Null.into_js(&ctx) + } + + #[qjs(get, rename = "type")] + pub fn event_type(&self) -> String { + self.event_type.clone() + } +} diff --git a/stdlib/src/llrt/llrt_events/event.rs b/stdlib/src/llrt/llrt_events/event.rs new file mode 100644 index 00000000..2e81873f --- /dev/null +++ b/stdlib/src/llrt/llrt_events/event.rs @@ -0,0 +1,62 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use rquickjs::{prelude::Opt, Result, Value}; + +use crate::llrt_utils::object::ObjectExt; + +#[rquickjs::class] +#[derive(rquickjs::class::Trace, rquickjs::JsLifetime)] +pub struct Event { + event_type: String, + bubbles: bool, + cancelable: bool, + composed: bool, +} + +#[rquickjs::methods] +impl Event { + #[qjs(constructor)] + pub fn new(event_type: String, options: Opt>) -> Result { + let mut bubbles = false; + let mut cancelable = false; + let mut composed = false; + if let Some(options) = options.0 { + if let Some(opt) = options.get_optional("bubbles")? { + bubbles = opt; + } + if let Some(opt) = options.get_optional("cancelable")? { + cancelable = opt; + } + if let Some(opt) = options.get_optional("composed")? { + composed = opt; + } + } + Ok(Self { + event_type, + bubbles, + cancelable, + composed, + }) + } + + #[qjs(get)] + pub fn bubbles(&self) -> bool { + self.bubbles + } + + #[qjs(get)] + pub fn cancelable(&self) -> bool { + self.cancelable + } + + #[qjs(get)] + pub fn composed(&self) -> bool { + self.composed + } + + #[qjs(get, rename = "type")] + pub fn event_type(&self) -> String { + self.event_type.clone() + } +} diff --git a/stdlib/src/llrt/llrt_events/event_target.rs b/stdlib/src/llrt/llrt_events/event_target.rs new file mode 100644 index 00000000..c4405a2c --- /dev/null +++ b/stdlib/src/llrt/llrt_events/event_target.rs @@ -0,0 +1,44 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::sync::{Arc, RwLock}; + +use rquickjs::{ + class::{Trace, Tracer}, + JsLifetime, +}; + +use super::{Emitter, EventList, Events}; + +#[rquickjs::class] +#[derive(Clone)] +pub struct EventTarget<'js> { + pub events: Events<'js>, +} + +unsafe impl<'js> JsLifetime<'js> for EventTarget<'js> { + type Changed<'to> = EventTarget<'to>; +} + +impl<'js> Emitter<'js> for EventTarget<'js> { + fn get_event_list(&self) -> Arc>> { + self.events.clone() + } +} + +impl<'js> Trace<'js> for EventTarget<'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + self.trace_event_emitter(tracer); + } +} + +#[rquickjs::methods] +impl<'js> EventTarget<'js> { + #[qjs(constructor)] + pub fn new() -> Self { + Self { + #[allow(clippy::arc_with_non_send_sync)] + events: Arc::new(RwLock::new(Vec::new())), + } + } +} diff --git a/stdlib/src/llrt/llrt_events/lib.rs b/stdlib/src/llrt/llrt_events/lib.rs new file mode 100644 index 00000000..d56445ed --- /dev/null +++ b/stdlib/src/llrt/llrt_events/lib.rs @@ -0,0 +1,580 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#![allow( + clippy::mutable_key_type, + clippy::for_kv_map, + clippy::new_without_default +)] +use std::{ + rc::Rc, + sync::{Arc, RwLock}, +}; + +use crate::llrt_utils::{ + error::ErrorExtensions, module::ModuleInfo, object::ObjectExt, result::ResultExt, +}; +use rquickjs::{ + class::{JsClass, Trace, Tracer}, + module::{Declarations, Exports, ModuleDef}, + prelude::{Func, Opt, Rest, This}, + CatchResultExt, Class, Ctx, Function, JsLifetime, Object, Result, String as JsString, Symbol, + Value, +}; +use tracing::trace; + +use self::{custom_event::CustomEvent, event::Event, event_target::EventTarget}; + +pub mod custom_event; +pub mod event; +pub mod event_target; + +#[derive(Clone, Debug)] +pub enum EventKey<'js> { + Symbol(Symbol<'js>), + String(Rc), +} + +impl<'js> EventKey<'js> { + fn from_value(ctx: &Ctx, value: Value<'js>) -> Result { + if value.is_string() { + let key: String = value.get()?; + Ok(EventKey::String(key.into())) + } else { + let sym = value.into_symbol().ok_or("Not a symbol").or_throw(ctx)?; + Ok(EventKey::Symbol(sym)) + } + } +} + +impl Eq for EventKey<'_> {} + +impl PartialEq for EventKey<'_> { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (EventKey::Symbol(symbol1), EventKey::Symbol(symbol2)) => symbol1 == symbol2, + (EventKey::String(str1), EventKey::String(str2)) => str1 == str2, + _ => false, + } + } +} + +pub struct EventItem<'js> { + callback: Function<'js>, + once: bool, +} + +pub type EventList<'js> = Vec<(EventKey<'js>, Vec>)>; +pub type Events<'js> = Arc>>; + +/// Get the hidden symbol used to store the event list on JS objects. +fn events_symbol<'js>(ctx: &Ctx<'js>) -> Result> { + Symbol::new_global(ctx.clone(), "__ee") +} + +/// Convert a Class into an Object for use with Emitter methods. +fn class_to_obj<'js, C: JsClass<'js>>(class: Class<'js, C>) -> Result> { + Object::from_value(class.into_value()) +} + +/// Resolve the event list from a JS object. For native Emitter classes, +/// reads from the native struct. For plain JS objects (e.g. stream.js Readable), +/// lazily creates and stores a native EventEmitter as a hidden property. +#[allow(clippy::arc_with_non_send_sync)] +pub fn resolve_events<'js>(ctx: &Ctx<'js>, obj: &Object<'js>) -> Result> { + // Try native EventEmitter first + if let Some(class) = Class::::from_object(obj) { + return Ok(class.borrow().events.clone()); + } + let sym = events_symbol(ctx)?; + // Check for hidden property + if let Some(ee) = obj.get::<_, Option>>>(sym.clone())? { + return Ok(ee.borrow().events.clone()); + } + // Create and store a new one + let events: Events<'js> = Arc::new(RwLock::new(Vec::new())); + let ee = Class::instance( + ctx.clone(), + EventEmitter { + events: events.clone(), + }, + )?; + obj.set(sym, ee)?; + Ok(events) +} + +#[rquickjs::class] +#[derive(Clone)] +pub struct EventEmitter<'js> { + pub events: Events<'js>, +} + +unsafe impl<'js> JsLifetime<'js> for EventEmitter<'js> { + type Changed<'to> = EventEmitter<'to>; +} + +impl<'js> Emitter<'js> for EventEmitter<'js> { + fn get_event_list(&self) -> Arc>> { + self.events.clone() + } +} + +impl<'js> Trace<'js> for EventEmitter<'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + self.trace_event_emitter(tracer); + } +} + +#[rquickjs::methods] +impl<'js> EventEmitter<'js> { + #[qjs(constructor)] + pub fn new() -> Self { + Self { + #[allow(clippy::arc_with_non_send_sync)] + events: Arc::new(RwLock::new(Vec::new())), + } + } +} + +pub trait EmitError<'js> { + fn emit_error(self, id: &'static str, ctx: &Ctx<'js>, this: Class<'js, C>) -> Result + where + C: Emitter<'js>; +} + +impl<'js, T> EmitError<'js> for Result { + fn emit_error(self, id: &'static str, ctx: &Ctx<'js>, this: Class<'js, C>) -> Result + where + C: Emitter<'js>, + { + if let Err(err) = self.catch(ctx) { + trace!("Error caught in: {}", id); + if this.borrow().has_listener_str("error") { + let error_value = err.into_value(ctx)?; + C::emit_str(this, ctx, "error", vec![error_value], false)?; + return Ok(true); + } + return Err(err.throw(ctx)); + } + Ok(false) + } +} + +pub trait Emitter<'js> +where + Self: JsClass<'js> + Sized + 'js, +{ + fn get_event_list(&self) -> Arc>>; + + fn on_event_changed(&mut self, _event: EventKey<'js>, _added: bool) -> Result<()> { + Ok(()) + } + + /// Resolve the event list from a `this` object. For native classes, + /// extracts from the class data. For plain JS objects, uses the hidden property. + fn resolve_events_from(ctx: &Ctx<'js>, this: &Object<'js>) -> Result> { + if let Some(class) = Class::::from_object(this) { + return Ok(class.borrow().get_event_list()); + } + resolve_events(ctx, this) + } + + fn add_event_emitter_prototype(ctx: &Ctx<'js>) -> Result> { + let proto = Class::::prototype(ctx)? + .or_throw_msg(ctx, "Prototype for EventEmitter not found")?; + + let on = Function::new(ctx.clone(), Self::on)?; + let off = Function::new(ctx.clone(), Self::remove_event_listener)?; + + proto.set("once", Func::from(Self::once))?; + proto.set("on", on.clone())?; + proto.set("emit", Func::from(Self::emit))?; + proto.set("prependListener", Func::from(Self::prepend_listener))?; + proto.set( + "prependOnceListener", + Func::from(Self::prepend_once_listener), + )?; + proto.set("off", off.clone())?; + proto.set("eventNames", Func::from(Self::event_names))?; + proto.set("addListener", on)?; + proto.set("removeListener", off)?; + proto.set("listenerCount", Func::from(Self::listener_count))?; + proto.set("removeAllListeners", Func::from(Self::remove_all_listeners))?; + + Ok(proto) + } + + fn add_event_target_prototype(ctx: &Ctx<'js>) -> Result> { + let proto = Class::::prototype(ctx)? + .or_throw_msg(ctx, "Prototype for EventTarget not found")?; + + let on = Function::new(ctx.clone(), Self::evt_add_event_listener)?; + let off = Function::new(ctx.clone(), Self::remove_event_listener)?; + + proto.set("dispatchEvent", Func::from(Self::evt_dispatch_event))?; + proto.set("addEventListener", on)?; + proto.set("removeEventListener", off)?; + + Ok(proto) + } + + fn trace_event_emitter<'a>(&self, tracer: Tracer<'a, 'js>) { + let events = self.get_event_list(); + let events = events.read().unwrap(); + for (key, items) in events.iter() { + if let EventKey::Symbol(sym) = &key { + tracer.mark(sym); + } + + for item in items { + tracer.mark(&item.callback); + } + } + } + + fn remove_event_listener_str( + this: Class<'js, Self>, + ctx: &Ctx<'js>, + event: &str, + listener: Function<'js>, + ) -> Result> { + let event = to_event(ctx, event)?; + Self::remove_event_listener(This(class_to_obj(this)?), ctx.clone(), event, listener) + } + + fn remove_event_listener( + this: This>, + ctx: Ctx<'js>, + event: Value<'js>, + listener: Function<'js>, + ) -> Result> { + let events = Self::resolve_events_from(&ctx, &this)?; + let mut events = events.write().or_throw(&ctx)?; + + let key = EventKey::from_value(&ctx, event)?; + if let Some(index) = events.iter_mut().position(|(k, _)| k == &key) { + let items = &mut events[index].1; + if let Some(pos) = items.iter().position(|item| item.callback == listener) { + items.remove(pos); + if items.is_empty() { + events.remove(index); + } + } + }; + + Ok(this.0) + } + + fn add_event_listener_str( + this: Class<'js, Self>, + ctx: &Ctx<'js>, + event: &str, + listener: Function<'js>, + prepend: bool, + once: bool, + ) -> Result> { + let event = to_event(ctx, event)?; + Self::add_event_listener( + This(class_to_obj(this)?), + ctx.clone(), + event, + listener, + prepend, + once, + ) + } + + fn once( + this: This>, + ctx: Ctx<'js>, + event: Value<'js>, + listener: Function<'js>, + ) -> Result> { + Self::add_event_listener(this, ctx, event, listener, false, true) + } + + fn on( + this: This>, + ctx: Ctx<'js>, + event: Value<'js>, + listener: Function<'js>, + ) -> Result> { + Self::add_event_listener(this, ctx, event, listener, false, false) + } + + fn prepend_listener( + this: This>, + ctx: Ctx<'js>, + event: Value<'js>, + listener: Function<'js>, + ) -> Result> { + Self::add_event_listener(this, ctx, event, listener, true, false) + } + + fn prepend_once_listener( + this: This>, + ctx: Ctx<'js>, + event: Value<'js>, + listener: Function<'js>, + ) -> Result> { + Self::add_event_listener(this, ctx, event, listener, true, true) + } + + fn evt_add_event_listener( + this: This>, + ctx: Ctx<'js>, + event: Value<'js>, + listener: Function<'js>, + options: Opt>, + ) -> Result> { + let mut once = false; + if let Some(opt) = options.0 { + if let Some(once_opt) = opt.get("once")? { + once = once_opt; + } + } + Self::add_event_listener(this, ctx, event, listener, false, once) + } + + fn add_event_listener( + this: This>, + ctx: Ctx<'js>, + event: Value<'js>, + listener: Function<'js>, + prepend: bool, + once: bool, + ) -> Result> { + let events = Self::resolve_events_from(&ctx, &this)?; + let mut events = events.write().or_throw(&ctx)?; + let key = EventKey::from_value(&ctx, event)?; + let mut is_new = false; + + let items = match events.iter_mut().find(|(k, _)| k == &key) { + Some((_, entry_items)) => entry_items, + None => { + is_new = true; + events.push((key.clone(), Vec::new())); + &mut events.last_mut().unwrap().1 + } + }; + + let item = EventItem { + callback: listener, + once, + }; + if !prepend { + items.push(item); + } else { + items.insert(0, item); + } + if is_new { + if let Some(class) = Class::::from_object(&this.0) { + class.borrow_mut().on_event_changed(key, true)?; + } + } + Ok(this.0) + } + + fn has_listener_str(&self, event: &str) -> bool { + let key = EventKey::String(event.into()); + has_key(self.get_event_list(), key) + } + + #[allow(dead_code)] + fn has_listener(&self, ctx: Ctx<'js>, event: Value<'js>) -> Result { + let key = EventKey::from_value(&ctx, event)?; + Ok(has_key(self.get_event_list(), key)) + } + + #[allow(dead_code)] + fn get_listeners(&self, ctx: &Ctx<'js>, event: Value<'js>) -> Result>> { + let key = EventKey::from_value(ctx, event)?; + Ok(find_all_listeners(self.get_event_list(), key)) + } + + fn get_listeners_str(&self, event: &str) -> Vec> { + let key = EventKey::String(event.into()); + find_all_listeners(self.get_event_list(), key) + } + + fn do_emit( + event: Value<'js>, + this: This>, + ctx: &Ctx<'js>, + args: Rest>, + defer: bool, + ) -> Result { + let events = Self::resolve_events_from(ctx, &this)?; + let mut events = events.write().or_throw(ctx)?; + let key = EventKey::from_value(ctx, event)?; + + if let Some(index) = events.iter_mut().position(|(k, _)| k == &key) { + let items = &mut events[index].1; + let mut callbacks = Vec::with_capacity(items.len()); + items.retain(|item: &EventItem<'_>| { + callbacks.push(item.callback.clone()); + !item.once + }); + if items.is_empty() { + events.remove(index); + if let Some(class) = Class::::from_object(&this.0) { + class.borrow_mut().on_event_changed(key, false)?; + } + } + drop(events); + for callback in callbacks { + let args: Vec> = args.iter().map(|arg| arg.to_owned()).collect(); + let args = Rest(args); + let this_val = This(this.0.clone().into_value()); + if defer { + callback.defer((this_val, args))?; + } else { + callback.call::<_, ()>((this_val, args))?; + } + } + Ok(true) + } else { + Ok(false) + } + } + + fn emit_str( + this: Class<'js, Self>, + ctx: &Ctx<'js>, + event: &str, + args: Vec>, + defer: bool, + ) -> Result<()> { + let event = to_event(ctx, event)?; + Self::do_emit(event, This(class_to_obj(this)?), ctx, args.into(), defer)?; + Ok(()) + } + + fn emit( + this: This>, + ctx: Ctx<'js>, + event: Value<'js>, + args: Rest>, + ) -> Result { + Self::do_emit(event, this, &ctx, args, false) + } + + fn evt_dispatch_event( + this: This>, + ctx: Ctx<'js>, + event: Value<'js>, + ) -> Result { + let event_type = event.get_optional("type")?.unwrap(); + Self::do_emit(event_type, this, &ctx, Rest(vec![event]), false) + } + + fn event_names(this: This>, ctx: Ctx<'js>) -> Result>> { + let events = Self::resolve_events_from(&ctx, &this)?; + let events = events.read().or_throw(&ctx)?; + + let mut names = Vec::with_capacity(events.len()); + for (key, _entry) in events.iter() { + let value = match key { + EventKey::Symbol(symbol) => symbol.clone().into_value(), + EventKey::String(str) => JsString::from_str(ctx.clone(), str)?.into(), + }; + + names.push(value) + } + + Ok(names) + } + + fn listener_count(this: This>, ctx: Ctx<'js>, event: Value<'js>) -> Result { + let events = Self::resolve_events_from(&ctx, &this)?; + let key = EventKey::from_value(&ctx, event)?; + let events = events.read().or_throw(&ctx)?; + Ok(events + .iter() + .find(|(k, _)| k == &key) + .map_or(0, |(_, items)| items.len())) + } + + fn remove_all_listeners( + this: This>, + ctx: Ctx<'js>, + event: Opt>, + ) -> Result> { + let events = Self::resolve_events_from(&ctx, &this)?; + let mut events = events.write().or_throw(&ctx)?; + match event.0 { + Some(event) if !event.is_undefined() => { + let key = EventKey::from_value(&ctx, event)?; + events.retain(|(k, _)| k != &key); + } + _ => events.clear(), + } + Ok(this.0) + } +} + +fn find_all_listeners<'js>( + events: Arc>>, + key: EventKey<'js>, +) -> Vec> { + let events = events.read().unwrap(); + let items = events.iter().find(|(k, _)| k == &key); + if let Some((_, callbacks)) = items { + callbacks.iter().map(|item| item.callback.clone()).collect() + } else { + vec![] + } +} + +fn has_key<'js>(event_list: Arc>>, key: EventKey<'js>) -> bool { + event_list.read().unwrap().iter().any(|(k, _)| k == &key) +} + +fn to_event<'js>(ctx: &Ctx<'js>, event: &str) -> Result> { + let event = JsString::from_str(ctx.clone(), event)?; + Ok(event.into_value()) +} + +pub struct EventsModule; + +impl ModuleDef for EventsModule { + fn declare(declare: &Declarations) -> Result<()> { + declare.declare(stringify!(EventEmitter))?; + declare.declare("default")?; + + Ok(()) + } + + fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { + let ctor = Class::::create_constructor(ctx)? + .expect("Can't create EventEmitter constructor"); + ctor.set(stringify!(EventEmitter), ctor.clone())?; + exports.export(stringify!(EventEmitter), ctor.clone())?; + exports.export("default", ctor)?; + + EventEmitter::add_event_emitter_prototype(ctx)?; + + Ok(()) + } +} + +impl From for ModuleInfo { + fn from(val: EventsModule) -> Self { + ModuleInfo { + name: "events", + module: val, + } + } +} + +pub fn init(ctx: &Ctx<'_>) -> Result<()> { + let globals = ctx.globals(); + + Class::::define(&globals)?; + Class::::define(&globals)?; + Class::::define(&globals)?; + + EventTarget::add_event_target_prototype(ctx)?; + + Ok(()) +} diff --git a/stdlib/src/llrt/llrt_exceptions/lib.rs b/stdlib/src/llrt/llrt_exceptions/lib.rs new file mode 100644 index 00000000..bb27e895 --- /dev/null +++ b/stdlib/src/llrt/llrt_exceptions/lib.rs @@ -0,0 +1,464 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use core::fmt; +use std::fmt::Debug; + +use crate::llrt_utils::{ + object::define_subclass, + option::Undefined, + primordials::{BasePrimordials, Primordial}, +}; +use rquickjs::{ + atom::PredefinedAtom, + class::{ + impl_::{CloneTrait, CloneWrapper}, + JsClass, Trace, + }, + function::{Constructor, Opt}, + object::{Accessor, Property}, + prelude::{Func, This}, + qjs, Class, Coerced, Ctx, Error, Exception, FromJs, IntoJs, JsLifetime, Object, Result, Value, +}; + +#[derive(JsLifetime)] +struct ExceptionPrimordials<'js> { + constructor_dom_exception: Constructor<'js>, + constructor_quota_exceeded_error: Constructor<'js>, +} + +impl<'js> Primordial<'js> for ExceptionPrimordials<'js> { + fn new(ctx: &Ctx<'js>) -> Result { + let globals = ctx.globals(); + Ok(Self { + constructor_dom_exception: globals.get(DOMException::NAME)?, + constructor_quota_exceeded_error: globals.get("QuotaExceededError")?, + }) + } +} + +#[derive(Trace, JsLifetime, Debug)] +pub struct DOMException { + name: String, + message: String, + stack: String, + code: u8, +} + +impl fmt::Display for DOMException { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("DOMException") + .field("name", &self.name()) + .field("message", &self.message()) + .field("stack", &self.stack) + .finish() + } +} + +fn add_constants(obj: &Object<'_>) -> Result<()> { + const CONSTANTS: [(&str, u8); 25] = [ + ("INDEX_SIZE_ERR", 1), + ("DOMSTRING_SIZE_ERR", 2), + ("HIERARCHY_REQUEST_ERR", 3), + ("WRONG_DOCUMENT_ERR", 4), + ("INVALID_CHARACTER_ERR", 5), + ("NO_DATA_ALLOWED_ERR", 6), + ("NO_MODIFICATION_ALLOWED_ERR", 7), + ("NOT_FOUND_ERR", 8), + ("NOT_SUPPORTED_ERR", 9), + ("INUSE_ATTRIBUTE_ERR", 10), + ("INVALID_STATE_ERR", 11), + ("SYNTAX_ERR", 12), + ("INVALID_MODIFICATION_ERR", 13), + ("NAMESPACE_ERR", 14), + ("INVALID_ACCESS_ERR", 15), + ("VALIDATION_ERR", 16), + ("TYPE_MISMATCH_ERR", 17), + ("SECURITY_ERR", 18), + ("NETWORK_ERR", 19), + ("ABORT_ERR", 20), + ("URL_MISMATCH_ERR", 21), + ("QUOTA_EXCEEDED_ERR", 22), + ("TIMEOUT_ERR", 23), + ("INVALID_NODE_TYPE_ERR", 24), + ("DATA_CLONE_ERR", 25), + ]; + + for (key, value) in CONSTANTS { + obj.prop(key, Property::from(value).enumerable())?; + } + + Ok(()) +} + +impl<'js> JsClass<'js> for DOMException { + const NAME: &'static str = "DOMException"; + type Mutable = rquickjs::class::Writable; + fn prototype(ctx: &Ctx<'js>) -> rquickjs::Result>> { + use rquickjs::class::impl_::{MethodImpl, MethodImplementor}; + let proto = Object::new(ctx.clone())?; + let implementor = MethodImpl::::new(); + implementor.implement(&proto)?; + add_constants(&proto)?; + + Ok(Some(proto)) + } + fn constructor(ctx: &Ctx<'js>) -> Result>> { + use rquickjs::class::impl_::{ConstructorCreate, ConstructorCreator}; + let implementor = ConstructorCreate::::new(); + let constructor = implementor + .create_constructor(ctx)? + .expect("DOMException must have a constructor"); + add_constants(&constructor)?; + + Ok(Some(constructor)) + } +} + +impl<'js> IntoJs<'js> for DOMException { + fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> Result> { + let cls = Class::::instance(ctx.clone(), self)?; + IntoJs::into_js(cls, ctx) + } +} + +impl<'js> FromJs<'js> for DOMException +where + for<'a> CloneWrapper<'a, Self>: CloneTrait, +{ + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let value = Class::::from_js(ctx, value)?; + let borrow = value.try_borrow()?; + Ok(CloneWrapper(&*borrow).wrap_clone()) + } +} + +#[rquickjs::methods] +impl DOMException { + #[qjs(constructor)] + pub fn new<'js>( + ctx: Ctx<'js>, + this: This>, + message: Opt>>, + name: Opt>>, + ) -> Result { + // When called with `new`, rquickjs passes the constructor function + // as `this`. Without `new` this is undefined or the global object. + if this.0.as_function().is_none() { + return Err(Exception::throw_type( + &ctx, + "Cannot call the DOMException constructor without 'new'", + )); + } + + let message = match message.0 { + Some(Undefined(Some(message))) => message.0, + _ => String::new(), + }; + + let name = match name.0 { + Some(Undefined(Some(message))) => DOMExceptionName::from(message.0), + _ => DOMExceptionName::Error, + }; + + Self::new_with_name(&ctx, name, message) + } + + #[qjs(skip)] + pub fn new_with_name(ctx: &Ctx<'_>, name: DOMExceptionName, message: String) -> Result { + let primordials = BasePrimordials::get(ctx)?; + + let new: Object = primordials + .constructor_error + .construct((message.clone(),))?; + + Ok(Self { + name: name.as_str().to_string(), + code: name.code(), + message, + stack: new.get::<_, String>(PredefinedAtom::Stack)?, + }) + } + + #[qjs(get, enumerable, configurable)] + fn message(&self) -> &str { + self.message.as_str() + } + + #[qjs(get, enumerable, configurable)] + pub fn name(&self) -> &str { + self.name.as_str() + } + + #[qjs(get, enumerable, configurable)] + pub fn code(&self) -> u8 { + self.code + } + + #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] + pub fn to_string_tag() -> &'static str { + stringify!(DOMException) + } +} + +impl<'js> DOMException { + fn create( + ctx: &Ctx<'js>, + name: DOMExceptionName, + message: impl Into, + ) -> Result> { + let primordials = ExceptionPrimordials::get(ctx)?; + let ctor = match name { + DOMExceptionName::QuotaExceededError => &primordials.constructor_quota_exceeded_error, + _ => &primordials.constructor_dom_exception, + }; + ctor.construct((message.into(), name.as_str())) + } + + fn throw_value(ctx: &Ctx<'js>, value: Value<'js>) -> Error { + unsafe { + let dup = qjs::JS_DupValue(ctx.as_raw().as_ptr(), value.as_raw()); + qjs::JS_Throw(ctx.as_raw().as_ptr(), dup); + } + Error::Exception + } + + fn create_error(ctx: &Ctx<'js>, name: DOMExceptionName, message: impl Into) -> Error { + let value = Self::create(ctx, name, message).expect("failed to create DOMException"); + Self::throw_value(ctx, value) + } + + pub fn not_supported_error(ctx: &Ctx<'js>, message: impl Into) -> Error { + Self::create_error(ctx, DOMExceptionName::NotSupportedError, message) + } + + pub fn type_mismatch_error(ctx: &Ctx<'js>, message: impl Into) -> Error { + Self::create_error(ctx, DOMExceptionName::TypeMismatchError, message) + } + + pub fn operation_error(ctx: &Ctx<'js>, message: impl Into) -> Error { + Self::create_error(ctx, DOMExceptionName::OperationError, message) + } + + pub fn quota_exceeded_error(ctx: &Ctx<'js>, message: impl Into) -> Error { + Self::create_error(ctx, DOMExceptionName::QuotaExceededError, message) + } + + pub fn data_error(ctx: &Ctx<'js>, message: impl Into) -> Error { + Self::create_error(ctx, DOMExceptionName::DataError, message) + } + + pub fn invalid_access_error(ctx: &Ctx<'js>, message: impl Into) -> Error { + Self::create_error(ctx, DOMExceptionName::InvalidAccessError, message) + } + + fn define_quota_exceeded_error(ctx: &Ctx<'js>) -> Result<()> { + let dom_exception: Constructor = ctx.globals().get(Self::NAME)?; + let quota_exceeded_error = define_subclass( + ctx, + "QuotaExceededError", + &dom_exception, + |ctx, message: Opt>>| { + let message = match message.0 { + Some(Undefined(Some(m))) => m.0, + _ => String::new(), + }; + Self::new_with_name(&ctx, DOMExceptionName::QuotaExceededError, message) + }, + )?; + let null = Value::new_null(ctx.clone()); + let proto: Object = quota_exceeded_error.get(PredefinedAtom::Prototype)?; + proto.prop( + "requested", + Property::from(null.clone()).enumerable().configurable(), + )?; + proto.prop("quota", Property::from(null).enumerable().configurable())?; + ctx.globals().prop( + "QuotaExceededError", + Property::from(quota_exceeded_error) + .writable() + .configurable(), + ) + } +} + +macro_rules! create_dom_exception { + ($name:ident, $($variant:ident),+ $(,)?) => { + #[derive(Debug)] + pub enum $name { + $( + $variant, + )+ + Other(String), + } + + impl $name { + pub fn as_str(&self) -> &str { + match self { + $( + Self::$variant => stringify!($variant), + )+ + Self::Other(value) => value, + } + } + } + + impl From for $name { + fn from(value: String) -> Self { + match value.as_str() { + $( + stringify!($variant) => Self::$variant, + )+ + _ => Self::Other(value), + } + } + } + }; +} + +// https://webidl.spec.whatwg.org/#dfn-error-names-table +create_dom_exception!( + DOMExceptionName, + IndexSizeError, + HierarchyRequestError, + WrongDocumentError, + InvalidCharacterError, + NoModificationAllowedError, + NotFoundError, + NotSupportedError, + InUseAttributeError, + InvalidStateError, + SyntaxError, + InvalidModificationError, + NamespaceError, + InvalidAccessError, + TypeMismatchError, + SecurityError, + NetworkError, + AbortError, + URLMismatchError, + QuotaExceededError, + TimeoutError, + InvalidNodeTypeError, + DataCloneError, + EncodingError, + NotReadableError, + UnknownError, + ConstraintError, + DataError, + TransactionInactiveError, + ReadOnlyError, + VersionError, + OperationError, + NotAllowedError, + Error, +); + +impl DOMExceptionName { + fn code(&self) -> u8 { + match self { + DOMExceptionName::IndexSizeError => 1, + DOMExceptionName::HierarchyRequestError => 3, + DOMExceptionName::WrongDocumentError => 4, + DOMExceptionName::InvalidCharacterError => 5, + DOMExceptionName::NoModificationAllowedError => 7, + DOMExceptionName::NotFoundError => 8, + DOMExceptionName::NotSupportedError => 9, + DOMExceptionName::InUseAttributeError => 10, + DOMExceptionName::InvalidStateError => 11, + DOMExceptionName::SyntaxError => 12, + DOMExceptionName::InvalidModificationError => 13, + DOMExceptionName::NamespaceError => 14, + DOMExceptionName::InvalidAccessError => 15, + DOMExceptionName::TypeMismatchError => 17, + DOMExceptionName::SecurityError => 18, + DOMExceptionName::NetworkError => 19, + DOMExceptionName::AbortError => 20, + DOMExceptionName::URLMismatchError => 21, + DOMExceptionName::QuotaExceededError => 22, + DOMExceptionName::TimeoutError => 23, + DOMExceptionName::InvalidNodeTypeError => 24, + DOMExceptionName::DataCloneError => 25, + _ => 0, + } + } +} + +pub fn init(ctx: &Ctx<'_>) -> Result<()> { + let globals = ctx.globals(); + + BasePrimordials::init(ctx)?; + + if let Some(constructor) = Class::::create_constructor(ctx)? { + // the wpt tests expect this particular property descriptor + globals.prop( + DOMException::NAME, + Property::from(constructor).writable().configurable(), + )?; + } + + let dom_ex_proto = Class::::prototype(ctx)?.unwrap(); + dom_ex_proto.set_prototype(Some(&BasePrimordials::get(ctx)?.prototype_error))?; + + DOMException::define_quota_exceeded_error(ctx)?; + ExceptionPrimordials::init(ctx)?; + + // `Error.isError(v)` only returns `true` for objects with QuickJS's + // `[[ErrorData]]` internal slot (class id `JS_CLASS_ERROR`). There is + // no public rquickjs API to tag a class-derived instance with that + // slot, so we replace `Error.isError` with a version that also + // recognizes `DOMException` instances (and its subclasses) via + // `instanceof`. + BasePrimordials::get(ctx)? + .constructor_error + .set("isError", Func::from(is_error))?; + + define_error_stack_accessor(ctx)?; + + Ok(()) +} + +// https://tc39.es/proposal-error-stack-accessor/ moves `stack` to an accessor +// on `Error.prototype`, so DOMException inherits it instead of exposing its own. +// QuickJS still gives plain Error instances an own `stack` data property, which +// shadows this accessor, so the getter only runs for DOMException instances. +fn define_error_stack_accessor<'js>(ctx: &Ctx<'js>) -> Result<()> { + let prototype_error = BasePrimordials::get(ctx)?.prototype_error.clone(); + prototype_error.prop( + PredefinedAtom::Stack, + Accessor::new( + |this: This>| -> Result { + let stack = Class::::from_value(&this.0) + .ok() + .map(|cls| cls.borrow().stack.clone()); + Ok(stack.unwrap_or_default()) + }, + |ctx: Ctx<'js>, this: This>, value: Value<'js>| -> Result<()> { + // SetterThatIgnoresPrototypeProperties: never install on the + // home object itself. + let Some(obj) = this.0.as_object() else { + return Ok(()); + }; + if *obj == BasePrimordials::get(&ctx)?.prototype_error { + return Ok(()); + } + obj.prop( + PredefinedAtom::Stack, + Property::from(value).writable().enumerable().configurable(), + ) + }, + ) + .configurable(), + ) +} + +fn is_error<'js>(ctx: Ctx<'js>, value: Value<'js>) -> Result { + if value.is_error() { + return Ok(true); + } + let Some(obj) = value.as_object() else { + return Ok(false); + }; + let dom_exception: Value = ctx.globals().get(DOMException::NAME)?; + Ok(obj.is_instance_of(&dom_exception)) +} diff --git a/stdlib/src/llrt/llrt_hooking/lib.rs b/stdlib/src/llrt/llrt_hooking/lib.rs new file mode 100644 index 00000000..ec560b66 --- /dev/null +++ b/stdlib/src/llrt/llrt_hooking/lib.rs @@ -0,0 +1,88 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::env; + +use crate::llrt_utils::{object::ObjectExt, provider::ProviderType}; +use once_cell::sync::Lazy; +use rquickjs::{Ctx, Exception, Function, Result, Value}; + +pub static HOOKING_MODE: Lazy = + Lazy::new(|| env::var("LLRT_ASYNC_HOOKS").as_deref() == Ok("1")); + +#[derive(PartialEq)] +pub enum HookType { + Init, + Before, + After, +} + +pub fn invoke_async_hook( + ctx: &Ctx<'_>, + hook_type: HookType, + provider_type: ProviderType, + uid: usize, +) -> Result<()> { + if !HOOKING_MODE.to_owned() { + return Ok(()); + } + + let hook_ = match hook_type { + HookType::Init => "init", + HookType::Before => "before", + HookType::After => "after", + }; + + let provider_ = match provider_type { + ProviderType::None if hook_type != HookType::Init => "", + ProviderType::None => { + return Err(Exception::throw_type( + ctx, + "Asynchronous types cannot be omitted in init hooks.", + )) + } + ProviderType::Resource(s) => &["Resource(", &s, ")"].concat(), + // Userland provider types + ProviderType::Immediate => "Immediate", + ProviderType::Interval => "Interval", + ProviderType::MessagePort => "MessagePort", + ProviderType::Microtask => "Microtask", + ProviderType::TickObject => "TickObject", + ProviderType::Timeout => "Timeout", + // Internal provider types + ProviderType::FsReqCallback => "FSREQCALLBACK", + ProviderType::GetAddrInfoReqWrap => "GETADDRINFOREQWRAP", + ProviderType::GetNameInfoReqWrap => "GETNAMEINFOREQWRAP", + ProviderType::PipeWrap => "PIPEWRAP", + ProviderType::StatWatcher => "STATWACHER", + ProviderType::TcpWrap => "TCPWRAP", + ProviderType::TimerWrap => "TIMERWRAP", + ProviderType::TlsWrap => "TLSWRAP", + ProviderType::UdpWrap => "UDPWRAP", + }; + + let invoke_async_hook = ctx + .globals() + .get_optional::<_, Function>("invokeAsyncHook")?; + if let Some(func) = &invoke_async_hook { + func.call::<_, ()>((hook_, provider_, uid))?; + } + Ok(()) +} + +pub fn register_finalization_registry<'js>( + ctx: &Ctx<'js>, + target: Value<'js>, + uid: usize, +) -> Result<()> { + if !HOOKING_MODE.to_owned() { + return Ok(()); + } + + if let Ok(register) = + ctx.eval::, &str>("globalThis.asyncFinalizationRegistry.register") + { + let _ = register.call::<_, ()>((target, uid)); + } + Ok(()) +} diff --git a/stdlib/src/llrt/llrt_json/escape.rs b/stdlib/src/llrt/llrt_json/escape.rs new file mode 100644 index 00000000..9950d338 --- /dev/null +++ b/stdlib/src/llrt/llrt_json/escape.rs @@ -0,0 +1,341 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +static JSON_ESCAPE_CHARS: [u8; 256] = [ + 0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, 8u8, 9u8, 10u8, 11u8, 12u8, 13u8, 14u8, 15u8, 16u8, + 17u8, 18u8, 19u8, 20u8, 21u8, 22u8, 23u8, 24u8, 25u8, 26u8, 27u8, 28u8, 29u8, 30u8, 31u8, 34u8, + 34u8, 32u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, + 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, + 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, + 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 33u8, 34u8, 34u8, 34u8, 34u8, + 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, + 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, + 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, + 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, + 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, + 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, + 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, + 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, + 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, + 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, +]; +static JSON_ESCAPE_QUOTES: [&str; 34usize] = [ + "\\u0000", "\\u0001", "\\u0002", "\\u0003", "\\u0004", "\\u0005", "\\u0006", "\\u0007", "\\b", + "\\t", "\\n", "\\u000b", "\\f", "\\r", "\\u000e", "\\u000f", "\\u0010", "\\u0011", "\\u0012", + "\\u0013", "\\u0014", "\\u0015", "\\u0016", "\\u0017", "\\u0018", "\\u0019", "\\u001a", + "\\u001b", "\\u001c", "\\u001d", "\\u001e", "\\u001f", "\\\"", "\\\\", +]; + +const ESCAPE_LEN: usize = 34; + +#[cold] +#[inline(always)] +fn write_surrogate_escape(result: &mut String, bytes: &[u8], i: usize) -> usize { + let code_point = ((bytes[i] as u16 & 0x0F) << 12) + | ((bytes[i + 1] as u16 & 0x3F) << 6) + | (bytes[i + 2] as u16 & 0x3F); + + result.push_str("\\u"); + let hex = [ + (code_point >> 12) as u8, + ((code_point >> 8) & 0xF) as u8, + ((code_point >> 4) & 0xF) as u8, + (code_point & 0xF) as u8, + ]; + for h in hex { + result.push(if h < 10 { + (b'0' + h) as char + } else { + (b'a' + h - 10) as char + }); + } + 3 +} + +#[allow(dead_code)] +pub fn escape_json(bytes: &[u8]) -> String { + let mut result = String::new(); + escape_json_string(&mut result, bytes); + result +} + +#[inline(always)] +fn process_byte( + result: &mut String, + bytes: &[u8], + byte: u8, + i: &mut usize, + start: &mut usize, + len: usize, +) { + // Fast path for simple escapes ({<32, 34, 92}); 0xED is filtered out here + // because JSON_ESCAPE_CHARS[0xED] == ESCAPE_LEN. + let c = JSON_ESCAPE_CHARS[byte as usize] as usize; + if c < ESCAPE_LEN { + // SAFETY: c < JSON_ESCAPE_QUOTES.len(); start <= i <= bytes.len(). + let esc = unsafe { JSON_ESCAPE_QUOTES.get_unchecked(c) }.as_bytes(); + let pending = unsafe { bytes.get_unchecked(*start..*i) }; + // Branch-free flush: one reserve + two memcpys (pending may be empty). + unsafe { + let vec = result.as_mut_vec(); + let total = pending.len() + esc.len(); + vec.reserve(total); + let cur = vec.len(); + let dst = vec.as_mut_ptr().add(cur); + std::ptr::copy_nonoverlapping(pending.as_ptr(), dst, pending.len()); + std::ptr::copy_nonoverlapping(esc.as_ptr(), dst.add(pending.len()), esc.len()); + vec.set_len(cur + total); + } + *i += 1; + *start = *i; + return; + } + + // WTF-8 lone surrogate (0xED A0..BF 80..BF) -> \uXXXX. Otherwise pass through. + if byte == 0xED && *i + 2 < len && (bytes[*i + 1] & 0xF0) >= 0xA0 { + if *start < *i { + // SAFETY: start <= i <= len; bytes through i are valid UTF-8/WTF-8. + result.push_str(unsafe { + std::str::from_utf8_unchecked(bytes.get_unchecked(*start..*i)) + }); + } + *i += write_surrogate_escape(result, bytes, *i); + *start = *i; + return; + } + *i += 1; +} + +/// SWAR escape-byte detector: sets the high bit of each byte in the returned +/// u64 for any input byte matching `< 32 || == 34 || == 92 || == 0xED`. May +/// produce false positives (caller's `process_byte` re-validates via the +/// escape table). Little-endian load so byte k -> bit (k*8); recover via +/// `trailing_zeros() / 8`. +#[inline(always)] +fn chunk_escape_mask(chunk: &[u8; 8]) -> u64 { + const ONES: u64 = 0x0101_0101_0101_0101; + const HIGH: u64 = 0x8080_8080_8080_8080; + let x = u64::from_le_bytes(*chunk); + let lt32 = x.wrapping_sub(0x20 * ONES) & !x; + let eq34 = { + let y = x ^ (0x22 * ONES); + y.wrapping_sub(ONES) & !y + }; + let eq92 = { + let y = x ^ (0x5C * ONES); + y.wrapping_sub(ONES) & !y + }; + let eqed = { + let y = x ^ (0xED * ONES); + y.wrapping_sub(ONES) & !y + }; + (lt32 | eq34 | eq92 | eqed) & HIGH +} + +/// Append a JSON-escaped form of `bytes` to `result`. +/// +/// Accepts UTF-8 or WTF-8 (QuickJS uses WTF-8 for JS strings with lone +/// surrogates). Scans 64 bytes at a time as 8x 8-byte SWAR masks; clean +/// strides are skipped without copying, dirty halves jump byte-to-byte via +/// `trailing_zeros`. The trailing <64 bytes are swept the same way and the +/// final <8 fall through to `process_byte`. +#[inline(always)] +pub fn escape_json_string_simple(result: &mut String, bytes: &[u8]) { + let len = bytes.len(); + let mut start = 0; + let mut i = 0; + // Headroom: small strings can expand up to 6x (all-control to \uXXXX); + // larger inputs see <25% density in practice. No-op when `result` is + // already pre-sized (common stringify-accumulator case). + let headroom = if len < 128 { + len * 5 + 16 + } else { + len / 4 + 16 + }; + result.reserve(len + headroom); + + let (chunks64, tail) = bytes.as_chunks::<64>(); + + let mut base = 0usize; + for chunk64 in chunks64 { + // Hand-unrolled to keep 8 independent SWAR dependency chains visible; + // LLVM doesn't reliably do this from a fixed-size array loop. + macro_rules! mask_at { + ($off:expr) => { + chunk_escape_mask((&chunk64[$off..$off + 8]).try_into().unwrap()) + }; + } + let m_0 = mask_at!(0); + let m_1 = mask_at!(8); + let m_2 = mask_at!(16); + let m_3 = mask_at!(24); + let m_4 = mask_at!(32); + let m_5 = mask_at!(40); + let m_6 = mask_at!(48); + let m_7 = mask_at!(56); + if (m_0 | m_1 | m_2 | m_3 | m_4 | m_5 | m_6 | m_7) == 0 { + i = base + 64; + } else { + macro_rules! dispatch { + ($off:expr, $mask:expr) => { + process_dirty_half(result, bytes, base + $off, $mask, &mut i, &mut start, len) + }; + } + dispatch!(0, m_0); + dispatch!(8, m_1); + dispatch!(16, m_2); + dispatch!(24, m_3); + dispatch!(32, m_4); + dispatch!(40, m_5); + dispatch!(48, m_6); + dispatch!(56, m_7); + } + base += 64; + } + + // 0..=63-byte tail: SWAR-sweep 8-byte sub-chunks, then byte-by-byte for <8. + let (sub_chunks, _sub_tail) = tail.as_chunks::<8>(); + for (k, sub) in sub_chunks.iter().enumerate() { + let mask = chunk_escape_mask(sub); + process_dirty_half(result, bytes, base + k * 8, mask, &mut i, &mut start, len); + } + + while i < len { + process_byte(result, bytes, bytes[i], &mut i, &mut start, len); + } + + if start < len { + result.push_str(unsafe { std::str::from_utf8_unchecked(&bytes[start..len]) }); + } +} + +#[inline(always)] +fn process_dirty_half( + result: &mut String, + bytes: &[u8], + half_start: usize, + mask: u64, + i: &mut usize, + start: &mut usize, + len: usize, +) { + let half_end = half_start + 8; + if mask == 0 { + *i = (*i).max(half_end); + return; + } + // A surrogate from the previous half may have consumed up to 2 bytes + // into this one; drop mask bits for those positions. + let mut m = mask & (!0u64 << ((*i - half_start) * 8)); + // Single-bit fast path skips the loop's mask-clearing shift. + if m.count_ones() == 1 { + *i = half_start + (m.trailing_zeros() as usize) / 8; + process_byte(result, bytes, bytes[*i], i, start, len); + *i = (*i).max(half_end); + return; + } + while m != 0 { + *i = half_start + (m.trailing_zeros() as usize) / 8; + process_byte(result, bytes, bytes[*i], i, start, len); + // checked_shl handles consumed >= 8 (shift >= 64) by zeroing m. + let consumed = *i - half_start; + m &= (!0u64).checked_shl((consumed as u32) * 8).unwrap_or(0); + } + *i = (*i).max(half_end); +} + +pub fn escape_json_string(result: &mut String, bytes: &[u8]) { + escape_json_string_simple(result, bytes); +} + +#[cfg(test)] +mod tests { + use crate::llrt_json::escape::escape_json; + + #[test] + fn escape_json_simple() { + assert_eq!(escape_json(b"Hello, World!"), "Hello, World!"); + } + + #[test] + fn escape_json_quotes() { + assert_eq!(escape_json(b"\"quoted\""), "\\\"quoted\\\""); + } + + #[test] + fn escape_json_backslash() { + assert_eq!(escape_json(b"back\\slash"), "back\\\\slash"); + } + + #[test] + fn escape_json_newline() { + assert_eq!(escape_json(b"line\nbreak"), "line\\nbreak"); + } + + #[test] + fn escape_json_tab() { + assert_eq!(escape_json(b"tab\tcharacter"), "tab\\tcharacter"); + } + + #[test] + fn escape_json_unicode() { + assert_eq!( + escape_json("unicode: \u{1F609}".as_bytes()), + "unicode: \u{1F609}" + ); + } + + #[test] + fn escape_json_special_characters() { + assert_eq!( + escape_json(b"!@#$%^&*()_+-=[]{}|;':,.<>?/"), + "!@#$%^&*()_+-=[]{}|;':,.<>?/" + ); + } + + #[test] + fn escape_json_mixed_characters() { + assert_eq!( + escape_json(b"123\"\"45678901\"234567"), + "123\\\"\\\"45678901\\\"234567" + ); + } + + // WTF-8 lone surrogate sequences — emitted by QuickJS when a String contains + // lone surrogate code points (e.g. from JSON.stringify("\uD800")). These must + // be escaped as `\uXXXX` even though they're not valid UTF-8. + #[test] + fn escape_json_lone_surrogate() { + // U+D800 in WTF-8 is 0xED 0xA0 0x80. + assert_eq!(escape_json(&[0xED, 0xA0, 0x80]), "\\ud800"); + } + + #[test] + fn escape_json_lone_surrogate_with_context() { + // Make sure surrogates at different alignments (within, across chunk + // boundaries) are handled correctly. + let mut input = b"abcdefg".to_vec(); // 7 bytes before surrogate + input.extend_from_slice(&[0xED, 0xBF, 0xBF]); // U+DFFF + input.extend_from_slice(b"xyz"); + assert_eq!(escape_json(&input), "abcdefg\\udfffxyz"); + } + + #[test] + fn escape_json_surrogate_at_chunk_boundary() { + // Surrogate starts at byte index 6, spans past the 8-byte chunk boundary. + let mut input = b"abcdef".to_vec(); // 6 bytes + input.extend_from_slice(&[0xED, 0xA0, 0x80]); // U+D800, ends at index 9 + input.extend_from_slice(b"xyz123456789"); + let expected = "abcdef\\ud800xyz123456789"; + assert_eq!(escape_json(&input), expected); + } + + #[test] + fn escape_json_korean_passthrough() { + // Valid Korean Hangul (U+D6C8 "훈") is encoded 0xED 0x9B 0x88 — the + // second byte has high nibble 0x90 < 0xA0 so it must NOT be escaped. + let s = "훈훈훈"; + assert_eq!(escape_json(s.as_bytes()), s); + } +} diff --git a/stdlib/src/llrt/llrt_json/lib.rs b/stdlib/src/llrt/llrt_json/lib.rs new file mode 100644 index 00000000..c4269943 --- /dev/null +++ b/stdlib/src/llrt/llrt_json/lib.rs @@ -0,0 +1,233 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::cmp::min; + +use rquickjs::{ + atom::PredefinedAtom, function::Opt, prelude::Func, Ctx, IntoJs, Object, Result, Value, +}; + +pub mod escape; +pub mod parse; +pub mod stringify; + +use crate::llrt_json::parse::json_parse_string; +use crate::llrt_json::stringify::json_stringify_replacer_space; + +pub fn redefine_static_methods(ctx: &Ctx<'_>) -> Result<()> { + let globals = ctx.globals(); + let json_module: Object = globals.get(PredefinedAtom::JSON)?; + json_module.set("parse", Func::from(json_parse_string))?; + json_module.set( + "stringify", + Func::from(|ctx, value, replacer, space| { + struct StringifyArgs<'js>(Ctx<'js>, Value<'js>, Opt>, Opt>); + let StringifyArgs(ctx, value, replacer, space) = + StringifyArgs(ctx, value, replacer, space); + + let mut space_value = None; + let mut replacer_value = None; + + if let Some(replacer) = replacer.0 { + if let Some(space) = space.0 { + if let Some(space) = space.as_string() { + let mut space = space.clone().to_string()?; + space.truncate(20); + space_value = Some(space); + } + if let Some(number) = space.as_int() { + if number > 0 { + space_value = Some(" ".repeat(min(10, number as usize))); + } + } + } + replacer_value = Some(replacer); + } + + json_stringify_replacer_space(&ctx, value, replacer_value, space_value) + .map(|v| v.into_js(&ctx))? + }), + )?; + Ok(()) +} + +#[cfg(test)] +mod tests { + use crate::llrt_test::test_sync_with; + use rquickjs::{prelude::Func, Array, CatchResultExt, IntoJs, Null, Object, Undefined, Value}; + + use crate::llrt_json::{ + parse::{json_parse, json_parse_string}, + stringify::{json_stringify, json_stringify_replacer_space}, + }; + + static JSON: &str = r#"{"organization":{"name":"TechCorp","founding_year":2000,"departments":[{"name":"Engineering","head":{"name":"Alice Smith","title":"VP of Engineering","contact":{"email":"alice.smith@techcorp.com","phone":"+1 (555) 123-4567"}},"employees":[{"id":101,"name":"Bob Johnson","position":"Software Engineer","contact":{"email":"bob.johnson@techcorp.com","phone":"+1 (555) 234-5678"},"projects":[{"project_id":"P001","name":"Project A","status":"In Progress","description":"Developing a revolutionary software solution for clients.","start_date":"2023-01-15","end_date":null,"team":[{"id":201,"name":"Sara Davis","role":"UI/UX Designer"},{"id":202,"name":"Charlie Brown","role":"Quality Assurance Engineer"}]},{"project_id":"P002","name":"Project B","status":"Completed","description":"Upgrading existing systems to enhance performance.","start_date":"2022-05-01","end_date":"2022-11-30","team":[{"id":203,"name":"Emily White","role":"Systems Architect"},{"id":204,"name":"James Green","role":"Database Administrator"}]}]},{"id":102,"name":"Carol Williams","position":"Senior Software Engineer","contact":{"email":"carol.williams@techcorp.com","phone":"+1 (555) 345-6789"},"projects":[{"project_id":"P001","name":"Project A","status":"In Progress","description":"Working on the backend development of Project A.","start_date":"2023-01-15","end_date":null,"team":[{"id":205,"name":"Alex Turner","role":"DevOps Engineer"},{"id":206,"name":"Mia Garcia","role":"Software Developer"}]},{"project_id":"P003","name":"Project C","status":"Planning","description":"Researching and planning for a future project.","start_date":null,"end_date":null,"team":[]}]}]},{"name":"Marketing","head":{"name":"David Brown","title":"VP of Marketing","contact":{"email":"david.brown@techcorp.com","phone":"+1 (555) 456-7890"}},"employees":[{"id":201,"name":"Eva Miller","position":"Marketing Specialist","contact":{"email":"eva.miller@techcorp.com","phone":"+1 (555) 567-8901"},"campaigns":[{"campaign_id":"C001","name":"Product Launch","status":"Upcoming","description":"Planning for the launch of a new product line.","start_date":"2023-03-01","end_date":null,"team":[{"id":301,"name":"Oliver Martinez","role":"Graphic Designer"},{"id":302,"name":"Sophie Johnson","role":"Content Writer"}]},{"campaign_id":"C002","name":"Brand Awareness","status":"Ongoing","description":"Executing strategies to increase brand visibility.","start_date":"2022-11-15","end_date":"2023-01-31","team":[{"id":303,"name":"Liam Taylor","role":"Social Media Manager"},{"id":304,"name":"Ava Clark","role":"Marketing Analyst"}]}]}]}]}}"#; + + #[tokio::test] + async fn json_parser() { + test_sync_with(|ctx| { + let json_data = [ + r#"{"aa\"\"aaaaaaaaaaaaaaaa":"a","b":"bbb"}"#, + r#"{"a":"aaaaaaaaaaaaaaaaaa","b":"bbb"}"#, + r#"{"a":["a","a","aaaa","a"],"b":"b"}"#, + r#"{"type":"Buffer","data":[10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10]}"#, + r#"{"a":[{"object2":{"key1":"value1","key2":123,"key3":false,"nestedObject":{"nestedKey":"nestedValue"}},"string":"Hello, World!","emptyObj":{},"emptyArr":[],"number":42,"boolean":true,"nullValue":null,"array":[1,2,3,"four",5.5,true,null],"object":{"key1":"value1","key2":123,"key3":false,"nestedObject":{"nestedKey":"nestedValue"}}}]}"#, + JSON, + ]; + + for json_str in json_data { + let json = json_str.to_string(); + let json2 = json.clone(); + + let value = json_parse(&ctx, json2)?; + let new_json = json_stringify_replacer_space(&ctx, value.clone(),None,Some(" ".into()))?.unwrap(); + let builtin_json = ctx.json_stringify_replacer_space(value,Null," ".to_string())?.unwrap().to_string()?; + assert_eq!(new_json, builtin_json); + } + + Ok(()) + }) + .await; + } + + #[tokio::test] + async fn json_parse_non_string() { + test_sync_with(|ctx| { + ctx.globals().set("parse", Func::from(json_parse_string))?; + + let result = ctx.eval::<(), _>("parse({})").catch(&ctx); + + if let Err(err) = result { + assert_eq!( + err.to_string(), + "Error: \"[object Object]\" not valid JSON at index 1 ('o')\n at (eval_script:1:1)\n" + ); + } else { + panic!("expected error") + } + + Ok(()) + }) + .await; + } + + #[tokio::test] + async fn json_stringify_undefined() { + test_sync_with(|ctx| { + let stringified = json_stringify(&ctx, Undefined.into_js(&ctx)?)?; + let stringified_2 = ctx + .json_stringify(Undefined)? + .map(|v| v.to_string().unwrap()); + assert_eq!(stringified, stringified_2); + + let obj: Value = ctx.eval( + r#"let obj = { value: undefined, array: [undefined, null, 1, true, "hello", { [Symbol("sym")]: 1, [undefined]: 2}] };obj;"#, + )?; + + let stringified = json_stringify(&ctx, obj.clone())?; + let stringified_2 = ctx + .json_stringify(obj)? + .map(|v| v.to_string().unwrap()); + assert_eq!(stringified, stringified_2); + + Ok(()) + }) + .await; + } + + #[tokio::test] + async fn json_stringify_objects() { + test_sync_with(|ctx| { + let date: Value = ctx.eval("let obj = { date: new Date(0) };obj;")?; + let stringified = json_stringify(&ctx, date.clone())?.unwrap(); + let stringified_2 = ctx.json_stringify(date)?.unwrap().to_string()?; + assert_eq!(stringified, stringified_2); + Ok(()) + }) + .await; + } + + #[tokio::test] + async fn huge_numbers() { + test_sync_with(|ctx| { + + let big_int_value = json_parse(&ctx, b"99999999999999999999999999999999999999999999999999999999999999999999999999999999999")?; + + let stringified = json_stringify(&ctx, big_int_value.clone())?.unwrap(); + let stringified_2 = ctx.json_stringify(big_int_value)?.unwrap().to_string()?.replace("e+", "e"); + assert_eq!(stringified, stringified_2); + + let big_int_value: Value = ctx.eval("999999999999")?; + let stringified = json_stringify(&ctx, big_int_value.clone())?.unwrap(); + let stringified_2 = ctx.json_stringify(big_int_value)?.unwrap().to_string()?; + assert_eq!(stringified, stringified_2); + + Ok(()) + }) + .await; + } + + #[tokio::test] + async fn json_circular_ref() { + test_sync_with(|ctx| { + let obj1 = Object::new(ctx.clone())?; + let obj2 = Object::new(ctx.clone())?; + let obj3 = Object::new(ctx.clone())?; + let obj4 = Object::new(ctx.clone())?; + obj4.set("key", "value")?; + obj3.set("sub2", obj4.clone())?; + obj2.set("sub1", obj3)?; + obj1.set("root1", obj2.clone())?; + obj1.set("root2", obj2.clone())?; + obj1.set("root3", obj2.clone())?; + + let value = obj1.clone().into_value(); + + let stringified = json_stringify(&ctx, value.clone())?.unwrap(); + let stringified_2 = ctx.json_stringify(value.clone())?.unwrap().to_string()?; + assert_eq!(stringified, stringified_2); + + obj4.set("recursive", obj1.clone())?; + + let stringified = json_stringify(&ctx, value.clone()); + + if let Err(error_message) = stringified.catch(&ctx) { + let error_str = error_message.to_string(); + assert_eq!( + "Error: Circular reference detected at: \"...root1.sub1.sub2.recursive\"\n", + error_str + ) + } else { + panic!("Expected an error, but got Ok"); + } + + let array1 = Array::new(ctx.clone())?; + let array2 = Array::new(ctx.clone())?; + let array3 = Array::new(ctx.clone())?; + + let obj5 = Object::new(ctx.clone())?; + obj5.set("key", obj1.clone())?; + array3.set(2, obj5)?; + array2.set(1, array3)?; + array1.set(0, array2)?; + + obj4.remove("recursive")?; + obj1.set("recursiveArray", array1)?; + + let stringified = json_stringify(&ctx, value.clone()); + + if let Err(error_message) = stringified.catch(&ctx) { + let error_str = error_message.to_string(); + assert_eq!( + "Error: Circular reference detected at: \"...recursiveArray[0][1][2].key\"\n", + error_str + ) + } else { + panic!("Expected an error, but got Ok"); + } + + Ok(()) + }) + .await; + } +} diff --git a/stdlib/src/llrt/llrt_json/parse.rs b/stdlib/src/llrt/llrt_json/parse.rs new file mode 100644 index 00000000..4ee78fcd --- /dev/null +++ b/stdlib/src/llrt/llrt_json/parse.rs @@ -0,0 +1,105 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use crate::llrt_utils::bytes::ObjectBytes; +use rquickjs::{Array, Ctx, Exception, IntoJs, Null, Object, Result, Undefined, Value}; +use simd_json::{Node, StaticNode}; + +pub fn json_parse_string<'js>(ctx: Ctx<'js>, bytes: ObjectBytes<'js>) -> Result> { + let bytes = bytes.as_bytes(&ctx)?; + json_parse(&ctx, bytes) +} + +pub fn json_parse<'js, T: Into>>(ctx: &Ctx<'js>, json: T) -> Result> { + let mut json: Vec = json.into(); + let tape = match simd_json::to_tape(&mut json) { + Ok(tape) => tape, + Err(err) => { + // simd_json is strict about lone / unpaired surrogate escapes + // (`\uXXXX` where XXXX is a surrogate code point). Fall back to + // QuickJS's native `JSON.parse`, which is more permissive and is + // needed for spec compliance with content that round-trips + // through `JSON.stringify` of strings containing lone surrogates. + if err.character() == Some('u') { + if let Ok(value) = ctx.json_parse(json.as_slice()) { + return Ok(value); + } + } + let mut itoa = itoa::Buffer::new(); + let mut error_msg = String::with_capacity(256); + let json_length = json.len(); + if json_length < 128 { + error_msg.reserve(json_length); + error_msg.push('\"'); + error_msg.push_str(&std::string::String::from_utf8_lossy(&json)); + error_msg.push_str("\" "); + } + + error_msg.push_str("not valid JSON at index "); + error_msg.push_str(itoa.format(err.index())); + if let Some(char) = err.character() { + error_msg.push_str(" ('"); + error_msg.push(char); + error_msg.push_str("')"); + } + return Err(Exception::throw_syntax(ctx, &error_msg)); + } + }; + let tape = tape.0; + + if let Some(first) = tape.first() { + return match first { + Node::String(value) => value.into_js(ctx), + Node::Static(node) => static_node_to_value(ctx, *node), + _ => parse_node(ctx, &tape, 0).map(|(value, _)| value), + }; + } + + Undefined.into_js(ctx) +} + +#[inline(always)] +fn static_node_to_value<'js>(ctx: &Ctx<'js>, node: StaticNode) -> Result> { + match node { + StaticNode::I64(value) => value.into_js(ctx), + StaticNode::U64(value) => value.into_js(ctx), + StaticNode::F64(value) => value.into_js(ctx), + StaticNode::Bool(value) => value.into_js(ctx), + StaticNode::Null => Null.into_js(ctx), + } +} + +fn parse_node<'js>(ctx: &Ctx<'js>, tape: &[Node], index: usize) -> Result<(Value<'js>, usize)> { + match tape[index] { + Node::String(value) => Ok((value.into_js(ctx)?, index + 1)), + Node::Static(node) => Ok((static_node_to_value(ctx, node)?, index + 1)), + Node::Object { len, .. } => { + let js_object = Object::new(ctx.clone())?; + let mut current_index = index + 1; + + for _ in 0..len { + if let Node::String(key) = tape[current_index] { + current_index += 1; + let (value, new_index) = parse_node(ctx, tape, current_index)?; + current_index = new_index; + js_object.set(key, value)?; + } + } + + Ok((js_object.into_value(), current_index)) + } + Node::Array { len, .. } => { + let js_array = Array::new(ctx.clone())?; + let mut current_index = index + 1; + + for i in 0..len { + let (value, new_index) = parse_node(ctx, tape, current_index)?; + current_index = new_index; + js_array.set(i, value)?; + } + + Ok((js_array.into_value(), current_index)) + } + } +} diff --git a/stdlib/src/llrt/llrt_json/stringify.rs b/stdlib/src/llrt/llrt_json/stringify.rs new file mode 100644 index 00000000..7c6bb201 --- /dev/null +++ b/stdlib/src/llrt/llrt_json/stringify.rs @@ -0,0 +1,552 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::{collections::HashSet, rc::Rc}; + +use rquickjs::{ + atom::PredefinedAtom, function::This, qjs, Ctx, Exception, Function, Object, Result, Type, + Value, +}; + +use crate::llrt_json::escape::escape_json_string; + +const CIRCULAR_REF_DETECTION_DEPTH: usize = 20; + +struct StringifyContext<'a, 'js> { + ctx: &'a Ctx<'js>, + result: &'a mut String, + value: &'a Value<'js>, + depth: usize, + indentation: Option<&'a str>, + key: Option<&'a str>, + index: Option, + parent: Option<&'a Object<'js>>, + ancestors: &'a mut Vec<(usize, Rc)>, + replacer_fn: Option<&'a Function<'js>>, + include_keys_replacer: Option<&'a HashSet>, + itoa_buffer: &'a mut itoa::Buffer, + ryu_buffer: &'a mut ryu::Buffer, +} + +#[allow(dead_code)] +pub fn json_stringify<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> Result> { + json_stringify_replacer_space(ctx, value, None, None) +} + +#[allow(dead_code)] +pub fn json_stringify_replacer<'js>( + ctx: &Ctx<'js>, + value: Value<'js>, + replacer: Option>, +) -> Result> { + json_stringify_replacer_space(ctx, value, replacer, None) +} + +pub fn json_stringify_replacer_space<'js>( + ctx: &Ctx<'js>, + value: Value<'js>, + replacer: Option>, + indentation: Option, +) -> Result> { + let mut result = String::with_capacity(128); + let mut replacer_fn = None; + let mut include_keys_replacer = None; + + let tmp_function; + + let mut itoa_buffer = itoa::Buffer::new(); + let mut ryu_buffer = ryu::Buffer::new(); + + if let Some(replacer) = replacer { + if let Some(function) = replacer.as_function() { + tmp_function = function.clone(); + replacer_fn = Some(&tmp_function); + } else if let Some(array) = replacer.as_array() { + let mut filter = HashSet::with_capacity(array.len()); + for value in array.clone().into_iter() { + let value = value?; + if let Some(string) = value.as_string() { + filter.insert(string.to_string()?); + } else if let Some(number) = value.as_int() { + filter.insert(itoa_buffer.format(number).to_string()); + } else if let Some(number) = value.as_float() { + filter.insert(ryu_buffer.format(number).to_string()); + } + } + include_keys_replacer = Some(filter); + } + } + + let indentation = indentation.as_deref(); + let include_keys_replacer = include_keys_replacer.as_ref(); + + let mut ancestors = Vec::with_capacity(10); + + let mut context = StringifyContext { + ctx, + result: &mut result, + value: &value, + depth: 0, + indentation: None, + key: None, + index: None, + parent: None, + ancestors: &mut ancestors, + replacer_fn, + include_keys_replacer, + itoa_buffer: &mut itoa_buffer, + ryu_buffer: &mut ryu_buffer, + }; + + match write_primitive(&mut context, false)? { + PrimitiveStatus::Written => { + return Ok(Some(result)); + } + PrimitiveStatus::Ignored => { + return Ok(None); + } + _ => {} + } + + context.depth += 1; + context.indentation = indentation; + iterate(&mut context, None)?; + Ok(Some(result)) +} + +#[inline(always)] +#[cold] +fn write_indentation(result: &mut String, indentation: Option<&str>, depth: usize) { + if let Some(indentation) = indentation { + result.push('\n'); + result.push_str(&indentation.repeat(depth - 1)); + } +} + +#[inline(always)] +#[cold] +fn run_to_json<'js>( + context: &mut StringifyContext<'_, 'js>, + js_object: &Object<'js>, +) -> Result<()> { + let to_json = js_object.get::<_, Function>(PredefinedAtom::ToJSON)?; + let val: Value = to_json.call((This(js_object.clone()),))?; + + //only preserve indentation if we're returning nested data + let indentation = context.indentation.and_then(|indentation| { + matches!( + val.type_of(), + Type::Object | Type::Array | Type::Exception | Type::Proxy + ) + .then_some(indentation) + }); + + append_value( + &mut StringifyContext { + ctx: context.ctx, + result: context.result, + value: &val, + depth: context.depth, + indentation, + key: None, + index: None, + parent: Some(js_object), + ancestors: context.ancestors, + replacer_fn: context.replacer_fn, + include_keys_replacer: context.include_keys_replacer, + itoa_buffer: context.itoa_buffer, + ryu_buffer: context.ryu_buffer, + }, + false, + )?; + Ok(()) +} + +#[derive(PartialEq)] +enum PrimitiveStatus<'js> { + Written, + Ignored, + Iterate(Option>), +} + +#[inline(always)] +#[cold] +fn run_replacer<'js>( + context: &mut StringifyContext<'_, 'js>, + replacer_fn: &Function<'js>, + add_comma: bool, +) -> Result> { + let key = context.key; + let index = context.index; + let value = context.value; + let parent = if let Some(parent) = context.parent { + parent.clone() + } else { + let parent = Object::new(context.ctx.clone())?; + parent.set("", value.clone())?; + parent + }; + let new_value: Value = replacer_fn.call(( + This(parent), + get_key_or_index(context.itoa_buffer, key, index), + value, + ))?; + + write_primitive2(context, add_comma, Some(new_value)) +} + +fn write_primitive<'js>( + context: &mut StringifyContext<'_, 'js>, + add_comma: bool, +) -> Result> { + if let Some(replacer_fn) = context.replacer_fn { + return run_replacer(context, replacer_fn, add_comma); + } + + write_primitive2(context, add_comma, None) +} + +fn write_primitive2<'js>( + context: &mut StringifyContext<'_, 'js>, + add_comma: bool, + new_value: Option>, +) -> Result> { + let key = context.key; + let index = context.index; + let include_keys_replacer = context.include_keys_replacer; + let indentation = context.indentation; + let depth = context.depth; + + let value = new_value.as_ref().unwrap_or(context.value); + + let type_of = value.type_of(); + + if context.index.is_none() + && matches!( + type_of, + Type::Symbol | Type::Undefined | Type::Function | Type::Constructor + ) + { + return Ok(PrimitiveStatus::Ignored); + } + + if matches!(type_of, Type::BigInt) { + return Err(Exception::throw_type( + context.ctx, + "Do not know how to serialize a BigInt", + )); + } + + if let Some(include_keys_replacer) = include_keys_replacer { + let key = get_key_or_index(context.itoa_buffer, key, index); + if !include_keys_replacer.contains(key) { + return Ok(PrimitiveStatus::Ignored); + } + }; + + if let Some(indentation) = indentation { + write_indented_separator(context.result, key, add_comma, indentation, depth); + } else { + write_sep(context.result, add_comma, false); + if let Some(key) = key { + write_key(context.result, key, false); + } + } + + match type_of { + Type::Null | Type::Undefined => context.result.push_str("null"), + Type::Bool => { + let bool_str = if unsafe { value.as_bool().unwrap_unchecked() } { + "true" + } else { + "false" + }; + context.result.push_str(bool_str); + } + Type::Int => context.result.push_str( + context + .itoa_buffer + .format(unsafe { value.as_int().unwrap_unchecked() }), + ), + Type::Float => { + let float_value = unsafe { value.as_float().unwrap_unchecked() }; + const EXP_MASK: u64 = 0x7ff0000000000000; + let bits = float_value.to_bits(); + if bits & EXP_MASK == EXP_MASK { + context.result.push_str("null"); + } else { + let str = context.ryu_buffer.format_finite(float_value); + + let bytes = str.as_bytes(); + let len = bytes.len(); + + context.result.push_str(str); + + if &bytes[len - 2..] == b".0" { + let len = context.result.len(); + unsafe { context.result.as_mut_vec().set_len(len - 2) } + } + } + } + Type::String => { + let js_string = unsafe { value.as_string().unwrap_unchecked() }.clone(); + write_string(context.result, js_string.to_cstring()?.as_str()); + } + _ => return Ok(PrimitiveStatus::Iterate(new_value)), + } + Ok(PrimitiveStatus::Written) +} + +#[inline(always)] +#[cold] +fn write_indented_separator( + result: &mut String, + key: Option<&str>, + add_comma: bool, + indentation: &str, + depth: usize, +) { + write_sep(result, add_comma, true); + result.push_str(&indentation.repeat(depth)); + if let Some(key) = key { + write_key(result, key, true); + } +} + +#[cold] +fn detect_circular_reference( + ctx: &Ctx<'_>, + value: &Object<'_>, + key: Option<&str>, + index: Option, + parent: Option<&Object<'_>>, + ancestors: &mut Vec<(usize, Rc)>, + itoa_buffer: &mut itoa::Buffer, +) -> Result<()> { + let parent_ptr = unsafe { qjs::JS_VALUE_GET_PTR(parent.unwrap_unchecked().as_raw()) as usize }; + let current_ptr = unsafe { qjs::JS_VALUE_GET_PTR(value.as_raw()) as usize }; + + while !ancestors.is_empty() + && match ancestors.last() { + Some((ptr, _)) => ptr != &parent_ptr, + _ => false, + } + { + ancestors.pop(); + } + + if ancestors.iter().any(|(ptr, _)| ptr == ¤t_ptr) { + let mut iter = ancestors.iter_mut(); + + let first = &unsafe { iter.next().unwrap_unchecked() }.1; + + let mut message = iter.rev().take(4).rev().fold( + String::from("Circular reference detected at: \".."), + |mut acc, (_, key)| { + if !key.starts_with('[') { + acc.push('.'); + } + acc.push_str(key); + acc + }, + ); + + if !first.starts_with('[') { + message.push('.'); + } + + message.push_str(first); + message.push('"'); + + return Err(Exception::throw_type(ctx, &message)); + } + ancestors.push(( + current_ptr, + key.map(|k| k.into()).unwrap_or_else(|| { + ["[", itoa_buffer.format(index.unwrap_or_default()), "]"] + .concat() + .into() + }), + )); + + Ok(()) +} + +#[inline(always)] +fn append_value(context: &mut StringifyContext<'_, '_>, add_comma: bool) -> Result { + match write_primitive(context, add_comma)? { + PrimitiveStatus::Written => Ok(true), + PrimitiveStatus::Ignored => Ok(false), + PrimitiveStatus::Iterate(new_value) => { + context.depth += 1; + iterate(context, new_value)?; + Ok(true) + } + } +} + +#[inline(always)] +fn write_key(string: &mut String, key: &str, indent: bool) { + string.push('"'); + escape_json_string(string, key.as_bytes()); + string.push_str("\":"); + if indent { + string.push(' '); + } +} + +#[inline(always)] +fn write_sep(result: &mut String, add_comma: bool, has_indentation: bool) { + if add_comma { + result.push(','); + } + if has_indentation { + result.push('\n'); + } +} + +#[inline(always)] +fn write_string(string: &mut String, value: &str) { + string.push('"'); + escape_json_string(string, value.as_bytes()); + string.push('"'); +} + +#[inline(always)] +fn get_key_or_index<'a>( + itoa_buffer: &'a mut itoa::Buffer, + key: Option<&'a str>, + index: Option, +) -> &'a str { + key.unwrap_or_else(|| itoa_buffer.format(index.unwrap_or_default())) +} + +fn iterate<'js>( + context: &mut StringifyContext<'_, 'js>, + new_value: Option>, +) -> Result<()> { + let mut add_comma; + let mut value_written; + let elem = new_value.as_ref().unwrap_or(context.value); + let depth = context.depth; + let ctx = context.ctx; + let indentation = context.indentation; + match elem.type_of() { + Type::Object | Type::Exception | Type::Proxy => { + let js_object = unsafe { elem.as_object().unwrap_unchecked() }; + if js_object.contains_key(PredefinedAtom::ToJSON)? { + return run_to_json(context, js_object); + } + + //only start detect circular reference at this level + if depth > CIRCULAR_REF_DETECTION_DEPTH { + detect_circular_reference( + ctx, + js_object, + context.key, + context.index, + context.parent, + context.ancestors, + context.itoa_buffer, + )?; + } + + context.result.push('{'); + + value_written = false; + + // Collect keys: js_object.keys() uses JS_GetOwnPropertyNames which can fail for + // Proxy objects. Fall back to Object.keys() in that case. + let keys: Vec = { + let collected: Vec = js_object.keys::().flatten().collect(); + if collected.is_empty() { + // Clear any pending exception and try Object.keys() for Proxy support + ctx.catch(); + ctx.globals() + .get::<_, Object>("Object") + .ok() + .and_then(|o| o.get::<_, Function>("keys").ok()) + .and_then(|f| f.call::<_, Vec>((js_object.clone(),)).ok()) + .unwrap_or_default() + } else { + collected + } + }; + + for key in keys { + let val = js_object.get(&key)?; + + add_comma = append_value( + &mut StringifyContext { + ctx, + result: context.result, + value: &val, + depth, + key: Some(&key), + indentation, + index: None, + parent: Some(js_object), + ancestors: context.ancestors, + replacer_fn: context.replacer_fn, + include_keys_replacer: context.include_keys_replacer, + itoa_buffer: context.itoa_buffer, + ryu_buffer: context.ryu_buffer, + }, + value_written, + )?; + value_written = value_written || add_comma; + } + + if value_written { + write_indentation(context.result, indentation, depth); + } + context.result.push('}'); + } + Type::Array => { + context.result.push('['); + add_comma = false; + value_written = false; + let js_array = unsafe { elem.as_array().unwrap_unchecked() }; + //only start detect circular reference at this level + if depth > CIRCULAR_REF_DETECTION_DEPTH { + detect_circular_reference( + ctx, + js_array.as_object(), + context.key, + context.index, + context.parent, + context.ancestors, + context.itoa_buffer, + )?; + } + for (i, val) in js_array.iter::().enumerate() { + let val = val?; + add_comma = append_value( + &mut StringifyContext { + ctx, + result: context.result, + value: &val, + depth, + key: None, + indentation, + index: Some(i), + parent: Some(js_array), + ancestors: context.ancestors, + replacer_fn: context.replacer_fn, + include_keys_replacer: context.include_keys_replacer, + itoa_buffer: context.itoa_buffer, + ryu_buffer: context.ryu_buffer, + }, + add_comma, + )?; + value_written = value_written || add_comma; + } + if value_written { + write_indentation(context.result, indentation, depth); + } + context.result.push(']'); + } + _ => {} + } + Ok(()) +} diff --git a/stdlib/src/llrt/llrt_path/lib.rs b/stdlib/src/llrt/llrt_path/lib.rs new file mode 100644 index 00000000..7225b753 --- /dev/null +++ b/stdlib/src/llrt/llrt_path/lib.rs @@ -0,0 +1,906 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::{ + borrow::Cow, + path::{Component, Path, PathBuf, MAIN_SEPARATOR, MAIN_SEPARATOR_STR}, +}; + +use crate::llrt_utils::module::{export_default, ModuleInfo}; +use rquickjs::{ + function::Opt, + module::{Declarations, Exports, ModuleDef}, + prelude::{Func, Rest}, + Ctx, Object, Result, +}; + +pub struct PathModule; + +#[cfg(windows)] +const DELIMITER: char = ';'; +#[cfg(not(windows))] +const DELIMITER: char = ':'; + +#[cfg(windows)] +pub const CURRENT_DIR_STR: &str = ".\\"; + +#[cfg(windows)] +const FORWARD_SLASH_STR: &str = "/"; + +#[cfg(not(windows))] +pub const CURRENT_DIR_STR: &str = "./"; + +#[cfg(windows)] +use memchr::{memchr, memchr2, memchr2_iter}; + +#[cfg(windows)] +pub fn replace_backslash(path: impl Into) -> String { + let mut path = path.into(); + let bytes = unsafe { path.as_bytes_mut() }; + + let mut start = 0; + while let Some(pos) = memchr(b'\\', &bytes[start..]) { + bytes[start + pos] = b'/'; + start += pos + 1; + } + path +} + +#[cfg(not(windows))] +pub fn replace_backslash(path: impl Into) -> String { + path.into().replace('\\', "/") +} + +#[cfg(windows)] +fn find_next_separator(s: &str) -> Option { + memchr2(b'\\', b'/', s.as_bytes()) +} + +#[cfg(not(windows))] +fn find_next_separator(s: &str) -> Option { + s.find(MAIN_SEPARATOR) +} + +#[cfg(windows)] +fn find_last_sep(path: &str) -> Option { + memchr2_iter(b'\\', b'/', path.as_bytes()).next_back() +} + +#[cfg(not(windows))] +fn find_last_sep(path: &str) -> Option { + path.rfind(MAIN_SEPARATOR) +} + +pub fn dirname<'a, P: Into>>(path: P) -> String { + let path = path.into(); + let len = path.len(); + + if len == 0 { + return ".".into(); + } + + let bytes = path.as_bytes(); + + #[cfg(windows)] + { + if len == 1 { + return if is_sep(bytes[0]) { + path.into_owned() + } else { + ".".to_string() + }; + } + + // Determine root end and search offset + let (root_end, offset) = if is_sep(bytes[0]) { + if is_sep(bytes[1]) { + // UNC path: \\server\share + parse_unc_root(bytes, len).unwrap_or((1, 1)) + } else { + (1, 1) + } + } else if bytes.len() > 1 && is_drive_letter(bytes[0]) && bytes[1] == b':' { + let r = if len > 2 && is_sep(bytes[2]) { 3 } else { 2 }; + (r, r) + } else { + (0, 0) + }; + + // Find last separator (skipping trailing separators) + let end = find_dirname_end(bytes, offset); + + match end { + Some(e) => &path[..e], + None if root_end > 0 => &path[..root_end], + None => ".", + } + .into() + } + + #[cfg(not(windows))] + { + if len == 1 { + return if bytes[0] == b'/' { + path.into_owned() + } else { + ".".into() + }; + } + + let has_root = bytes[0] == b'/'; + let end = find_dirname_end(bytes, 1); + + match end { + Some(e) if has_root && e == 1 => "//", + Some(e) => &path[..e], + None if has_root => "/", + None => ".", + } + .into() + } +} + +#[cfg(windows)] +fn is_sep(c: u8) -> bool { + c == b'/' || c == b'\\' +} + +#[cfg(windows)] +fn is_drive_letter(c: u8) -> bool { + c.is_ascii_alphabetic() +} + +#[cfg(windows)] +fn parse_unc_root(bytes: &[u8], len: usize) -> Option<(usize, usize)> { + let mut j = 2; + // Skip server name + while j < len && !is_sep(bytes[j]) { + j += 1; + } + if j >= len || j == 2 { + return None; + } + // Skip separators + while j < len && is_sep(bytes[j]) { + j += 1; + } + if j >= len { + return None; + } + let share_start = j; + // Skip share name + while j < len && !is_sep(bytes[j]) { + j += 1; + } + if j == share_start { + return None; + } + if j == len { + return None; + } // UNC root only - caller handles this + Some((j + 1, j + 1)) +} + +fn find_dirname_end(bytes: &[u8], offset: usize) -> Option { + let mut matched_slash = true; + for i in (offset..bytes.len()).rev() { + #[cfg(windows)] + let is_separator = is_sep(bytes[i]); + #[cfg(not(windows))] + let is_separator = bytes[i] == b'/'; + + if is_separator { + if !matched_slash { + return Some(i); + } + } else { + matched_slash = false; + } + } + None +} + +pub fn name_extname(path: &str) -> (&str, &str) { + let path = strip_last_sep(path); + let sep_pos = find_last_sep(path); + + let path = match sep_pos { + Some(idx) => &path[idx + 1..], + None => path, + }; + if path.starts_with('.') { + return (path, ""); + } + match path.rfind('.') { + Some(idx) => path.split_at(idx), + None => (path, ""), + } +} + +fn strip_last_sep(path: &str) -> &str { + if ends_with_sep(path) { + &path[..path.len() - 1] + } else { + path + } +} + +pub fn basename(path: String, suffix: Opt) -> String { + #[cfg(windows)] + { + if path.is_empty() || path == MAIN_SEPARATOR_STR || path == FORWARD_SLASH_STR { + return String::from(""); + } + } + #[cfg(not(windows))] + { + if path.is_empty() || path == MAIN_SEPARATOR_STR { + return String::from(""); + } + } + + let (base, ext) = name_extname(&path); + let mut name = [base, ext].concat(); + if let Some(suffix) = suffix.0 { + if let Some(location) = name.rfind(&suffix) { + name.truncate(location); + return name; + } + } + name +} + +fn extname(path: String) -> String { + let (_, ext) = name_extname(&path); + ext.to_string() +} + +fn format(obj: Object) -> String { + let dir: String = obj.get("dir").unwrap_or_default(); + let root: String = obj.get("root").unwrap_or_default(); + let base: String = obj.get("base").unwrap_or_default(); + let name: String = obj.get("name").unwrap_or_default(); + let ext: String = obj.get("ext").unwrap_or_default(); + + let mut path = String::new(); + if !dir.is_empty() { + path.push_str(&dir); + if !ends_with_sep(&dir) { + path.push(MAIN_SEPARATOR); + } + } else if !root.is_empty() { + path.push_str(&root); + if !ends_with_sep(&root) { + path.push(MAIN_SEPARATOR); + } + } + if !base.is_empty() { + path.push_str(&base); + } else { + path.push_str(&name); + if !ext.is_empty() { + if !ext.starts_with('.') { + path.push('.'); + } + path.push_str(&ext); + } + } + path +} + +fn parse(ctx: Ctx, path_str: String) -> Result { + let obj = Object::new(ctx)?; + let path = Path::new(&path_str); + let parent = path + .parent() + .map(|p| p.to_str().unwrap()) + .unwrap_or_default(); + let filename = path + .file_name() + .map(|n| n.to_str().unwrap()) + .unwrap_or_default(); + + let (name, extension) = name_extname(filename); + + let root = path + .components() + .next() + .and_then(|c| match c { + Component::Prefix(prefix) => prefix.as_os_str().to_str(), + Component::RootDir => c.as_os_str().to_str(), + _ => Some(""), + }) + .unwrap_or_default(); + + obj.set("root", root)?; + obj.set("dir", parent)?; + obj.set("base", [name, extension].concat())?; + obj.set("ext", extension)?; + obj.set("name", name)?; + + Ok(obj) +} + +fn join(parts: Rest) -> String { + join_path(parts.0.iter()) +} + +pub fn join_path(parts: I) -> String +where + S: AsRef, + I: IntoIterator, +{ + join_path_with_separator(parts, false) +} + +pub fn join_path_with_separator(parts: I, force_posix_sep: bool) -> String +where + S: AsRef, + I: IntoIterator, +{ + //fine because we're either moving or storing references + let parts_vec: Vec = parts.into_iter().collect(); + //add one slash plus drive letter + //max is probably parts+size + let likely_max_size = parts_vec + .iter() + .map(|p| p.as_ref().len() + 1) + .sum::() + + 10; + let result = String::with_capacity(likely_max_size); + join_resolve_path(parts_vec, false, result, PathBuf::new(), force_posix_sep) +} + +pub fn resolve_path(parts: I) -> Result +where + S: AsRef, + I: IntoIterator, +{ + resolve_path_with_separator(parts, false) +} + +pub fn resolve_path_with_separator(parts: I, force_posix_sep: bool) -> Result +where + S: AsRef, + I: IntoIterator, +{ + let cwd = std::env::current_dir()?; + + let mut result = cwd.clone().into_os_string().into_string().unwrap(); + //add MAIN_SEPARATOR if we're not on already MAIN_SEPARATOR + if !result.ends_with(MAIN_SEPARATOR) { + result.push(MAIN_SEPARATOR); + } + #[cfg(windows)] + { + if force_posix_sep { + result = result.replace(MAIN_SEPARATOR, FORWARD_SLASH_STR); + } + } + Ok(join_resolve_path(parts, true, result, cwd, force_posix_sep)) +} + +pub fn relative(from: F, to: T) -> Result +where + F: AsRef, + T: AsRef, +{ + let from_ref = from.as_ref(); + let to_ref = to.as_ref(); + if from_ref == to_ref { + return Ok("".into()); + } + + let mut abs_from = None; + + if !is_absolute(from_ref) { + abs_from = Some( + std::env::current_dir()?.to_string_lossy().to_string() + MAIN_SEPARATOR_STR + from_ref, + ); + } + + let mut abs_to = None; + + if !is_absolute(to_ref) { + abs_to = Some( + std::env::current_dir()?.to_string_lossy().to_string() + MAIN_SEPARATOR_STR + to_ref, + ); + } + + let from_ref = abs_from.as_deref().unwrap_or(from_ref); + let to_ref = abs_to.as_deref().unwrap_or(to_ref); + + let mut from_index = 0; + let mut to_index = 0; + // skip common prefix + while from_index < from_ref.len() && to_index < to_ref.len() { + let from_next = find_next_separator(&from_ref[from_index..]) + .unwrap_or(from_ref.len() - from_index) + + from_index; + let to_next = + find_next_separator(&to_ref[to_index..]).unwrap_or(to_ref.len() - to_index) + to_index; + if from_ref[from_index..from_next] != to_ref[to_index..to_next] { + break; + } + from_index = from_next + 1; //move past the separator + to_index = to_next + 1; //move past the separator + } + let mut relative = String::new(); + // add ".." for each remaining component in 'from' + while from_index < from_ref.len() { + let from_next = find_next_separator(&from_ref[from_index..]) + .unwrap_or(from_ref.len() - from_index) + + from_index; + if !relative.is_empty() { + relative.push(MAIN_SEPARATOR); + } + relative.push_str(".."); + from_index = from_next + 1; // Move past the separator + } + // add the remaining components from 'to' + while to_index < to_ref.len() { + let to_next = + find_next_separator(&to_ref[to_index..]).unwrap_or(to_ref.len() - to_index) + to_index; + if !relative.is_empty() { + relative.push(MAIN_SEPARATOR); + } + let component = &to_ref[to_index..to_next]; + if component != "." { + relative.push_str(component); + } + to_index = to_next + 1; // Move past the separator + } + Ok(if relative.is_empty() { + ".".into() + } else { + relative + }) +} + +fn join_resolve_path( + parts: I, + resolve: bool, + mut result: String, + cwd: PathBuf, + force_posix_sep: bool, +) -> String +where + S: AsRef, + I: IntoIterator, +{ + let (sep, sep_str) = if force_posix_sep { + ('/', "/") + } else { + (MAIN_SEPARATOR, MAIN_SEPARATOR_STR) + }; + + let mut resolve_cow: Cow; + let mut empty = true; + let mut prefix_len = 0; + + let mut index_stack = Vec::with_capacity(16); + + // Remove the trailing sep: /a/b// -> /a/b + if ends_with_sep(&result) && result.len() > 1 { + result.truncate(result.len() - 1); + } + + for part in parts { + let mut part_ref: &str = part.as_ref(); + let mut start = 0; + if resolve { + if cfg!(not(windows)) { + if part_ref.starts_with(MAIN_SEPARATOR) { + empty = false; + result = MAIN_SEPARATOR.into(); + start = 1; + } + } else { + let starts_with_sep = starts_with_sep(part_ref); + if starts_with_sep { + let (prefix, _) = get_path_prefix(&cwd); + prefix_len = prefix.len(); + result = prefix; + empty = false; + result.push(sep); + } else { + let path_buf: PathBuf = PathBuf::from(part_ref); + if path_buf.is_absolute() { + empty = false; + let (prefix, mut components) = get_path_prefix(&path_buf); + if !prefix.is_empty() { + components.next(); //consume prefix + } + prefix_len = prefix.len(); + result = prefix; + result.push(sep); + resolve_cow = components + .map(|comp| comp.as_os_str().to_str().unwrap_or_default()) // Convert each component to &str + .collect::>() // Collect into a vector of &str + .join(sep_str) + .into(); + part_ref = resolve_cow.as_ref(); + } + } + } + } else if starts_with_sep(part_ref) && empty { + empty = false; + result.push(sep); + start = 1; + } + + while start < part_ref.len() { + let end = find_next_separator(&part_ref[start..]).map_or(part_ref.len(), |i| i + start); + match &part_ref[start..end] { + ".." => { + if let Some(last_index) = index_stack.pop() { + result.truncate(last_index); + } else if empty { + if let Some(last_index) = find_last_sep(&result) { + result.truncate(last_index); + } + } + } + "" | "." => { + //ignore + } + sub_part => { + let len = result.len(); + if !result.ends_with(sep) && !result.is_empty() { + result.push(sep); + } + result.push_str(sub_part); + result.push(sep); + index_stack.push(len); + } + } + start = end + 1; + } + } + + if result.len() > prefix_len + 1 && ends_with_sep(&result) { + result.truncate(result.len() - 1); + } + + result +} + +pub fn resolve(path: Rest) -> Result { + resolve_path(path.iter()) +} + +fn get_path_prefix(cwd: &Path) -> (String, std::iter::Peekable>) { + let mut components = cwd.components().peekable(); + + let prefix = if let Some(Component::Prefix(prefix)) = components.peek() { + prefix.as_os_str().to_str().unwrap().to_string() + } else { + "".into() + }; + + (prefix, components) +} + +pub fn normalize>(path: P) -> String { + join_path([path].iter()) +} + +#[allow(dead_code)] //used by windows +fn starts_with_sep(path: &str) -> bool { + matches!(path.as_bytes().first().unwrap_or(&0), b'/' | b'\\') +} + +#[cfg(windows)] +pub fn ends_with_sep(path: &str) -> bool { + matches!(path.as_bytes().last().unwrap_or(&0), b'/' | b'\\') +} + +#[cfg(not(windows))] +pub fn ends_with_sep(path: &str) -> bool { + path.ends_with(MAIN_SEPARATOR) +} + +#[cfg(windows)] +pub fn is_absolute(path: &str) -> bool { + starts_with_sep(path) || PathBuf::from(path).is_absolute() +} + +#[cfg(not(windows))] +pub fn is_absolute(path: &str) -> bool { + path.starts_with(MAIN_SEPARATOR) +} + +impl ModuleDef for PathModule { + fn declare(declare: &Declarations) -> Result<()> { + declare.declare("basename")?; + declare.declare("dirname")?; + declare.declare("extname")?; + declare.declare("format")?; + declare.declare("parse")?; + declare.declare("join")?; + declare.declare("resolve")?; + declare.declare("relative")?; + declare.declare("normalize")?; + declare.declare("isAbsolute")?; + declare.declare("delimiter")?; + declare.declare("sep")?; + + declare.declare("default")?; + Ok(()) + } + + fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { + export_default(ctx, exports, |default| { + default.set("dirname", Func::from(dirname::))?; + default.set("basename", Func::from(basename))?; + default.set("extname", Func::from(extname))?; + default.set("format", Func::from(format))?; + default.set("parse", Func::from(parse))?; + default.set("join", Func::from(join))?; + default.set("relative", Func::from(relative::))?; + default.set("resolve", Func::from(resolve))?; + default.set("normalize", Func::from(normalize::))?; + default.set("isAbsolute", Func::from(|s: String| is_absolute(&s)))?; + default.prop("delimiter", DELIMITER.to_string())?; + default.prop("sep", MAIN_SEPARATOR.to_string())?; + Ok(()) + }) + } +} + +impl From for ModuleInfo { + fn from(val: PathModule) -> Self { + ModuleInfo { + name: "path", + module: val, + } + } +} + +#[cfg(test)] +mod tests { + use std::{env::set_current_dir, sync::Mutex}; + + static THREAD_LOCK: Lazy> = Lazy::new(Mutex::default); + + use once_cell::sync::Lazy; + + use super::*; + + #[test] + fn test_relative() { + let _shared = THREAD_LOCK.lock().unwrap(); + let cwd = std::env::current_dir().expect("unable to get current working directory"); + set_current_dir("/").expect("unable to set working directory to /"); + + assert_eq!( + relative("a/b/c", "b/c").unwrap(), + "../../../b/c".replace('/', MAIN_SEPARATOR_STR) + ); + assert_eq!( + relative("/data/orandea/test/aaa", "/data/orandea/impl/bbb").unwrap(), + "../../impl/bbb".replace('/', MAIN_SEPARATOR_STR) + ); + assert_eq!( + relative("/a/b/c", "/a/d").unwrap(), + "../../d".replace('/', MAIN_SEPARATOR_STR) + ); + assert_eq!(relative("/a/b/c", "/a/b/c/d").unwrap(), "d"); + assert_eq!(relative("/a/b/c", "/a/b/c").unwrap(), ""); + + assert_eq!( + relative("a/b", "a/b/c/d").unwrap(), + "c/d".replace('/', MAIN_SEPARATOR_STR) + ); + assert_eq!( + relative("a/b/c", "b/c").unwrap(), + "../../../b/c".replace('/', MAIN_SEPARATOR_STR) + ); + + set_current_dir(cwd).expect("unable to set working directory back"); + } + + #[test] + fn test_dirname() { + assert_eq!(dirname("/usr/local/bin".to_string()), "/usr/local"); + assert_eq!(dirname("/usr/local/".to_string()), "/usr"); + assert_eq!(dirname("usr/local/bin".to_string()), "usr/local"); + assert_eq!(dirname("/".to_string()), "/"); + assert_eq!(dirname("".to_string()), "."); + } + + #[test] + fn test_basename() { + assert_eq!(basename("/usr/local/bin".to_string(), Opt(None)), "bin"); + assert_eq!( + basename("/usr/local/bin.txt".to_string(), Opt(None)), + "bin.txt" + ); + assert_eq!( + basename( + "/usr/local/bin.txt".to_string(), + Opt(Some(".txt".to_string())) + ), + "bin" + ); + assert_eq!(basename("".to_string(), Opt(None)), ""); + assert_eq!(basename("/".to_string(), Opt(None)), ""); + } + + #[test] + fn test_extname() { + assert_eq!(extname("/usr/local/bin.txt".to_string()), ".txt"); + assert_eq!(extname("/usr/local/bin".to_string()), ""); + assert_eq!(extname("file.tar.gz".to_string()), ".gz"); + assert_eq!(extname(".bashrc".to_string()), ""); + assert_eq!(extname("".to_string()), ""); + } + + #[test] + fn test_join() { + // Standard cases + assert_eq!( + join_path(["/usr", "local", "bin"].iter()), + "/usr/local/bin".replace('/', MAIN_SEPARATOR_STR) + ); + assert_eq!( + join_path(["/usr", "/local", "bin"].iter()), + "/usr/local/bin".replace('/', MAIN_SEPARATOR_STR) + ); + assert_eq!( + join_path(["usr", "local", "bin"].iter()), + "usr/local/bin".replace('/', MAIN_SEPARATOR_STR) + ); + assert_eq!(join_path(["", "bin"].iter()), "bin"); + + // Complex cases + assert_eq!( + join_path(["/usr", "..", "local", "bin"].iter()), + "/local/bin".replace('/', MAIN_SEPARATOR_STR) + ); // Parent dir + assert_eq!( + join_path([".", "usr", "local"]), + "usr/local".replace('/', MAIN_SEPARATOR_STR) + ); // Current dir + assert_eq!( + join_path(["/usr", ".", "bin"].iter()), + "/usr/bin".replace('/', MAIN_SEPARATOR_STR) + ); // Current dir in middle + assert_eq!( + join_path(["usr", "local", "bin", ".."].iter()), + "usr/local".replace('/', MAIN_SEPARATOR_STR) + ); // Ending with parent dir + assert_eq!( + join_path(["/usr", "local", "", "bin"].iter()), + "/usr/local/bin".replace('/', MAIN_SEPARATOR_STR) + ); // Empty component in path + assert_eq!( + join_path(["/usr", "local", ".hidden"].iter()), + "/usr/local/.hidden".replace('/', MAIN_SEPARATOR_STR) + ); // Hidden file + } + + #[test] + fn test_resolve_path() { + let _shared = THREAD_LOCK.lock().unwrap(); + let prefix = if cfg!(windows) { + if let Some(Component::Prefix(prefix)) = + std::env::current_dir().unwrap().components().next() + { + prefix.as_os_str().to_str().unwrap().to_string() + } else { + "".into() + } + } else { + "".into() + }; + + assert_eq!( + resolve_path(["", "foo/bar"].iter()).unwrap(), + std::env::current_dir() + .unwrap() + .join("foo/bar".replace('/', MAIN_SEPARATOR_STR)) + .to_string_lossy() + .to_string() + ); + + // Standard cases + assert_eq!( + resolve_path(["/"].iter()).unwrap(), + prefix.clone() + MAIN_SEPARATOR_STR + ); + + // Standard cases + assert_eq!( + resolve_path(["/foo/bar", "../baz"].iter()).unwrap(), + prefix.clone() + &"/foo/baz".replace('/', MAIN_SEPARATOR_STR) + ); + assert_eq!( + resolve_path(["/foo/bar", "./baz"].iter()).unwrap(), + prefix.clone() + &"/foo/bar/baz".replace('/', MAIN_SEPARATOR_STR) + ); + assert_eq!( + resolve_path(["foo/bar", "/baz"].iter()).unwrap(), + prefix.clone() + &"/baz".replace('/', MAIN_SEPARATOR_STR) + ); + + // Complex cases + assert_eq!( + resolve_path(["/foo", "bar", ".", "baz"].iter()).unwrap(), + prefix.clone() + &"/foo/bar/baz".replace('/', MAIN_SEPARATOR_STR) + ); // Current dir in middle + assert_eq!( + resolve_path(["/foo", "bar", "..", "baz"].iter()).unwrap(), + prefix.clone() + &"/foo/baz".replace('/', MAIN_SEPARATOR_STR) + ); // Parent dir in middle + assert_eq!( + resolve_path(["/foo", "bar", "../..", "baz"].iter()).unwrap(), + prefix.clone() + &"/baz".replace('/', MAIN_SEPARATOR_STR) + ); // Double parent dir + assert_eq!( + resolve_path(["/foo", "bar", ".hidden"].iter()).unwrap(), + prefix.clone() + &"/foo/bar/.hidden".replace('/', MAIN_SEPARATOR_STR) + ); // Hidden file + assert_eq!( + resolve_path(["/foo", ".", "bar", "."].iter()).unwrap(), + prefix.clone() + &"/foo/bar".replace('/', MAIN_SEPARATOR_STR) + ); // Multiple current dirs + assert_eq!( + resolve_path(["/foo", "..", "..", "bar"].iter()).unwrap(), + prefix.clone() + &"/bar".replace('/', MAIN_SEPARATOR_STR) + ); // Multiple parent dirs + assert_eq!( + resolve_path(["/foo/bar", "/..", "baz"].iter()).unwrap(), + prefix.clone() + &"/baz".replace('/', MAIN_SEPARATOR_STR) + ); // Parent dir with absolute path + + assert_eq!( + resolve_path(["../foo"].iter()).unwrap(), + std::env::current_dir() + .unwrap() + .parent() + .unwrap() + .join("foo".replace('/', MAIN_SEPARATOR_STR)) + .to_string_lossy() + .to_string() + ); // Start with .. + + assert_eq!( + resolve_path(["../".repeat(32)].iter()).unwrap(), + prefix.clone() + ); // Many .. + } + + #[test] + fn test_normalize() { + assert_eq!( + normalize("/foo//bar//baz"), + "/foo/bar/baz".replace('/', MAIN_SEPARATOR_STR) + ); + assert_eq!( + normalize("/foo/./bar/../baz"), + "/foo/baz".replace('/', MAIN_SEPARATOR_STR) + ); + assert_eq!( + normalize("foo/bar/"), + "foo/bar".replace('/', MAIN_SEPARATOR_STR) + ); + assert_eq!(normalize("./foo"), "foo"); + } + + #[test] + fn test_is_absolute() { + assert!(is_absolute("/usr/local/bin")); + assert!(!is_absolute("usr/local/bin")); + #[cfg(windows)] + assert!(is_absolute("C:\\Program Files")); // for Windows systems + assert!(!is_absolute("./local/bin")); + } + + #[test] + fn test_replace_backslash() { + assert_eq!(replace_backslash("C:\\Program Files"), "C:/Program Files"); + assert_eq!(replace_backslash("/usr/local/bin"), "/usr/local/bin"); + assert_eq!(replace_backslash("C:\\Users\\User\\"), "C:/Users/User/"); + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/lib.rs b/stdlib/src/llrt/llrt_stream_web/lib.rs new file mode 100644 index 00000000..baa68356 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/lib.rs @@ -0,0 +1,181 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use crate::llrt_utils::{ + module::{export_default, ModuleInfo}, + primordials::{BasePrimordials, Primordial}, +}; +use queuing_strategy::{ByteLengthQueuingStrategy, CountQueuingStrategy}; +use readable::{ + ReadableByteStreamController, ReadableStreamBYOBReader, ReadableStreamBYOBRequest, + ReadableStreamDefaultController, ReadableStreamDefaultReader, +}; +use rquickjs::{ + module::{Declarations, Exports, ModuleDef}, + Class, Ctx, Object, Result, +}; +use writable::{WritableStream, WritableStreamDefaultController, WritableStreamDefaultWriter}; + +use crate::llrt_stream_web::{ + readable::{ArrayConstructorPrimordials, IteratorPrimordials}, + transform::{TransformStream, TransformStreamDefaultController}, + utils::promise::PromisePrimordials, + writable::WritableStreamDefaultControllerPrimordials, +}; + +mod queuing_strategy; +pub mod readable; +mod readable_writable_pair; +mod transform; +pub mod utils; +mod writable; + +// Public API for creating streams from Rust +pub use readable::stream::lock_readable_stream; +pub use readable::stream::tee_readable_stream; +pub use readable::stream::try_sync_drain_closed_stream; +pub use readable::stream::ReadableStream; +pub use readable::{ + readable_byte_stream_controller_close_stream, readable_byte_stream_controller_enqueue_bytes, + readable_byte_stream_controller_enqueue_bytes_borrowed, + readable_stream_default_controller_close_stream, + readable_stream_default_controller_enqueue_value, + readable_stream_default_controller_error_stream, ReadableByteStreamControllerClass, + ReadableStreamDefaultControllerClass, +}; +pub use readable::{CancelAlgorithm, PullAlgorithm, ReadableStreamControllerClass, StartAlgorithm}; +pub use readable::{NativePull, NativePullFn, NativePullResult}; + +/// Creates a transform stream using LLRT's built-in Web Streams implementation. +/// +/// This does not consult the global `TransformStream` binding. +pub fn create_transform_stream<'js>( + ctx: &Ctx<'js>, + transformer: Object<'js>, +) -> Result> { + init_primordials(ctx)?; + Ok(TransformStream::from_transformer(ctx.clone(), transformer)?.into_inner()) +} + +fn init_primordials(ctx: &Ctx<'_>) -> Result<()> { + BasePrimordials::init(ctx)?; + PromisePrimordials::init(ctx)?; + ArrayConstructorPrimordials::init(ctx)?; + WritableStreamDefaultControllerPrimordials::init(ctx)?; + IteratorPrimordials::init(ctx)?; + Ok(()) +} + +/// Defines web streams, which are exposed through the "stream/web" Node import, but also at the global scope +/// Web streams consist of Readable, Writable, and Transform streams. Transform is currently unimplemented. +/// +/// https://developer.mozilla.org/en-US/docs/Web/API/Streams_API +/// +/// # ReadableStream +/// ReadableStream knows how to 'pull' objects or bytes from an underlying source, generally a user-defined function or an [async] iterator. +/// A source enqueues data to the stream via a controller, either ReadableStreamDefaultController or a ReadableByteStreamController optionally for byte data. +/// The controller is created at stream initialisation and cannot change. +/// +/// Data is read from the stream using a reader, which is obtained using stream.getReader(). A reader 'locks' the stream for reading, preventing +/// other readers from being created. When a reader is released with `reader.releaseLock()`, the stream goes back to having no reader and a new one can be created. +/// In the case of ReadableByteStreamController, a special reader ReadableStreamBYOBReader may be used, which allows users to provide their own +/// buffer to fill bytes into when reading. Otherwise, ReadableStreamDefaultReader is used by default, and this may also be used with byte streams. +/// +/// A ReadableStream can be 'tee'd', which splits it into two readable streams which both read the same underlying data, potentially at different +/// paces. This is an area of substantial complexity for the implementation, particularly in the case of byte streams as the alternative reader types +/// must be handled correctly. +/// +/// # WritableStream +/// WritableStream knows how to 'push' objects into an underlying sink, generally a user-defined function. It has no special casing for bytes, and so +/// only has one type of controller, WritableStreamDefaultController, and only one type of writer WritableStreamDefaultWriter. The controller is only needed for +/// error handling because writes are signalled via a function call to a user-defined 'write' method which receives the chunk directly. +/// +/// Data is written to the stream using a WritableStreamDefaultWriter, which is obtained using stream.getWriter(). A writer 'locks' the stream for writing, +/// preventing other writers from being created. When a writer is released with `writer.releaseLock()`, the stream goes back to having no writer and a new one can be created. +pub struct StreamWebModule; + +// https://nodejs.org/api/webstreams.html +impl ModuleDef for StreamWebModule { + fn declare(declare: &Declarations) -> Result<()> { + declare.declare(stringify!(ReadableStream))?; + declare.declare(stringify!(ReadableStreamDefaultReader))?; + declare.declare(stringify!(ReadableStreamBYOBReader))?; + declare.declare(stringify!(ReadableStreamDefaultController))?; + declare.declare(stringify!(ReadableByteStreamController))?; + declare.declare(stringify!(ReadableStreamBYOBRequest))?; + + declare.declare(stringify!(WritableStream))?; + declare.declare(stringify!(WritableStreamDefaultWriter))?; + declare.declare(stringify!(WritableStreamDefaultController))?; + + declare.declare(stringify!(TransformStream))?; + declare.declare(stringify!(TransformStreamDefaultController))?; + + declare.declare(stringify!(ByteLengthQueuingStrategy))?; + declare.declare(stringify!(CountQueuingStrategy))?; + + declare.declare("default")?; + Ok(()) + } + + #[inline] + fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { + export_default(ctx, exports, |default| { + Class::::define(default)?; + Class::::define(default)?; + Class::::define(default)?; + Class::::define(default)?; + Class::::define(default)?; + Class::::define(default)?; + + Class::::define(default)?; + Class::::define(default)?; + Class::::define(default)?; + + Class::::define(default)?; + Class::::define(default)?; + + Class::::define(default)?; + Class::::define(default)?; + + Ok(()) + })?; + + Ok(()) + } +} + +impl From for ModuleInfo { + fn from(val: StreamWebModule) -> Self { + ModuleInfo { + name: "stream/web", + module: val, + } + } +} + +pub fn init(ctx: &Ctx) -> Result<()> { + let globals = &ctx.globals(); + + init_primordials(ctx)?; + + // https://min-common-api.proposal.wintertc.org/#api-index + Class::::define(globals)?; + Class::::define(globals)?; + + Class::::define(globals)?; + Class::::define(globals)?; + Class::::define(globals)?; + Class::::define(globals)?; + Class::::define(globals)?; + Class::::define(globals)?; + + Class::::define(globals)?; + Class::::define(globals)?; + + // This is exposed globally by Node even though its not in the min-common-api + Class::::define(globals)?; + + Class::::define(globals)?; + Class::::define(globals)?; + + Ok(()) +} diff --git a/stdlib/src/llrt/llrt_stream_web/queuing_strategy/byte_length.rs b/stdlib/src/llrt/llrt_stream_web/queuing_strategy/byte_length.rs new file mode 100644 index 00000000..4de615d8 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/queuing_strategy/byte_length.rs @@ -0,0 +1,36 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use rquickjs::{class::Trace, methods, Class, Ctx, JsLifetime, Result}; + +use super::{NativeSizeFunction, QueueingStrategyInit}; + +#[derive(JsLifetime, Trace)] +#[rquickjs::class] +pub(crate) struct ByteLengthQueuingStrategy<'js> { + high_water_mark: f64, + size: Class<'js, NativeSizeFunction>, +} + +#[methods(rename_all = "camelCase")] +impl<'js> ByteLengthQueuingStrategy<'js> { + #[qjs(constructor)] + pub(crate) fn new(ctx: Ctx<'js>, init: QueueingStrategyInit) -> Result { + // Set this.[[highWaterMark]] to init["highWaterMark"]. + Ok(Self { + high_water_mark: init.high_water_mark, + size: Class::instance(ctx, NativeSizeFunction::ByteLength)?, + }) + } + + // readonly attribute Function size; + // size is an attribute, not a method, so this function is not itself the size function, but instead returns one + #[qjs(get)] + pub(crate) fn size(&self) -> Class<'js, NativeSizeFunction> { + self.size.clone() + } + + // readonly attribute unrestricted double highWaterMark; + #[qjs(get)] + pub(crate) fn high_water_mark(&self) -> f64 { + self.high_water_mark + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/queuing_strategy/count.rs b/stdlib/src/llrt/llrt_stream_web/queuing_strategy/count.rs new file mode 100644 index 00000000..95460a87 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/queuing_strategy/count.rs @@ -0,0 +1,36 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use rquickjs::{class::Trace, methods, Class, Ctx, JsLifetime, Result}; + +use super::{NativeSizeFunction, QueueingStrategyInit}; + +#[derive(JsLifetime, Trace)] +#[rquickjs::class] +pub(crate) struct CountQueuingStrategy<'js> { + high_water_mark: f64, + size: Class<'js, NativeSizeFunction>, +} + +#[methods(rename_all = "camelCase")] +impl<'js> CountQueuingStrategy<'js> { + #[qjs(constructor)] + pub(crate) fn new(ctx: Ctx<'js>, init: QueueingStrategyInit) -> Result { + // Set this.[[highWaterMark]] to init["highWaterMark"]. + Ok(Self { + high_water_mark: init.high_water_mark, + size: Class::instance(ctx, NativeSizeFunction::Count)?, + }) + } + + // readonly attribute Function size; + // size is an attribute, not a method, so this function is not itself the size function, but instead returns one + #[qjs(get)] + pub(crate) fn size(&self) -> Class<'js, NativeSizeFunction> { + self.size.clone() + } + + // readonly attribute unrestricted double highWaterMark; + #[qjs(get)] + pub(crate) fn high_water_mark(&self) -> f64 { + self.high_water_mark + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/queuing_strategy/mod.rs b/stdlib/src/llrt/llrt_stream_web/queuing_strategy/mod.rs new file mode 100644 index 00000000..ce37c241 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/queuing_strategy/mod.rs @@ -0,0 +1,231 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use rquickjs::{ + class::{JsCell, JsClass, Readable, Trace}, + convert::Coerced, + function::{Constructor, Params}, + prelude::This, + Class, Ctx, Error, Exception, FromJs, Function, JsLifetime, Object, Result, Value, +}; + +pub(crate) use byte_length::ByteLengthQueuingStrategy; +pub(crate) use count::CountQueuingStrategy; + +use crate::llrt_stream_web::utils::ValueOrUndefined; + +mod byte_length; +mod count; +#[cfg(test)] +mod tests; + +/// QueuingStrategy is the structure of a user-provided object describing how backpressure should be signalled. +/// https://streams.spec.whatwg.org/#qs-api +pub(super) struct QueuingStrategy<'js> { + // unrestricted double highWaterMark; + high_water_mark: Option, + // callback QueuingStrategySize = unrestricted double (any chunk); + pub(super) size: Option>, +} + +impl<'js> FromJs<'js> for QueuingStrategy<'js> { + fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let ty_name = value.type_name(); + let obj = value + .as_object() + .ok_or_else(|| Error::new_from_js(ty_name, "Object"))?; + + let high_water_mark = obj + .get_value_or_undefined::<_, Coerced>("highWaterMark")? + .map(|value| value.0); + let size = obj.get_value_or_undefined::<_, _>("size")?; + + Ok(Self { + high_water_mark, + size, + }) + } +} + +impl<'js> QueuingStrategy<'js> { + // https://streams.spec.whatwg.org/#validate-and-normalize-high-water-mark + pub(super) fn extract_high_water_mark( + ctx: &Ctx<'js>, + this: Option, + default_hwm: f64, + ) -> Result { + match this { + // If strategy["highWaterMark"] does not exist, return defaultHWM. + None => Ok(default_hwm), + // Let highWaterMark be strategy["highWaterMark"]. + Some(Self { + high_water_mark: Some(high_water_mark), + .. + }) => { + // If highWaterMark is NaN or highWaterMark < 0, throw a RangeError exception. + if high_water_mark.is_nan() || high_water_mark < 0.0 { + Err(Exception::throw_range(ctx, "Invalid highWaterMark")) + } else { + // Return highWaterMark. + Ok(high_water_mark) + } + } + + // If strategy["highWaterMark"] does not exist, return defaultHWM. + _ => Ok(default_hwm), + } + } + + // https://streams.spec.whatwg.org/#make-size-algorithm-from-size-function + pub(super) fn extract_size_algorithm(this: Option<&Self>) -> SizeAlgorithm<'js> { + // If strategy["size"] does not exist, return an algorithm that returns 1. + match this.as_ref().and_then(|t| t.size.as_ref()) { + None => SizeAlgorithm::AlwaysOne, + Some(size) => SizeAlgorithm::SizeFunction(size.clone()), + } + } +} + +/// SizeAlgorithm represents the two ways we might generate sizes - by calling a function or by simply returning 1.0 (the default) +#[derive(JsLifetime, Trace, Clone)] +pub(super) enum SizeAlgorithm<'js> { + AlwaysOne, + SizeFunction(SizeFunction<'js>), +} + +impl<'js> SizeAlgorithm<'js> { + pub(super) fn call(&self, ctx: Ctx<'js>, chunk: Value<'js>) -> Result> { + match self { + Self::AlwaysOne + | Self::SizeFunction(SizeFunction::Native(NativeSizeFunction::Count)) => { + Ok(SizeValue::Native(1.0)) + } + Self::SizeFunction(SizeFunction::Js(ref f)) => { + f.call((This(Value::new_undefined(ctx.clone())), chunk.clone())) + } + Self::SizeFunction(SizeFunction::Native(NativeSizeFunction::ByteLength)) => { + let size = byte_length_queueing_strategy_size_function(&ctx, &chunk)?; + SizeValue::from_js(&ctx, size) + } + } + } +} + +/// SizeValue abstracts over the sources of size values - they can either come from user-provided size functions, in which case they might +/// be any Value, or (more often) they come from a NativeSizeFunction or the default AlwaysOne algorithm and we can pass around a native Rust type. +pub(super) enum SizeValue<'js> { + Value(Value<'js>), + Native(f64), +} + +impl SizeValue<'_> { + pub(super) fn as_number(&self) -> Option { + match self { + Self::Value(value) => value.as_number(), + Self::Native(size) => Some(*size), + } + } +} + +impl<'js> FromJs<'js> for SizeValue<'js> { + fn from_js(_: &Ctx<'js>, value: Value<'js>) -> Result { + if let Some(size) = value.as_number() { + return Ok(Self::Native(size)); + } + + Ok(Self::Value(value)) + } +} + +/// SizeFunction abstracts over user-provided size functions (from their own queuing strategy implementations) and ones provided by us. +/// We want to be able to recognise the ones that we have provided so we can short-circuit expensive JS calls +#[derive(JsLifetime, Trace, Clone)] +pub(super) enum SizeFunction<'js> { + Js(Function<'js>), + Native(NativeSizeFunction), +} + +impl<'js> FromJs<'js> for SizeFunction<'js> { + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + if let Ok(nsf) = Class::::from_value(&value) { + return Ok(SizeFunction::Native(*nsf.borrow())); + } + + Ok(SizeFunction::Js(Function::from_js(ctx, value)?)) + } +} + +/// QueueingStrategyInit is the dictionary of input parameters for both native queuing strategies +/// https://streams.spec.whatwg.org/#dictdef-queuingstrategyinit +pub(crate) struct QueueingStrategyInit { + high_water_mark: f64, +} + +impl<'js> FromJs<'js> for QueueingStrategyInit { + fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let ty_name = value.type_name(); + let obj = value + .as_object() + .ok_or_else(|| Error::new_from_js(ty_name, "Object"))?; + + let high_water_mark = obj + .get_value_or_undefined::<_, Coerced>("highWaterMark")? + .ok_or_else(|| Error::new_from_js(ty_name, "QueueingStrategyInit"))?; + + Ok(Self { + high_water_mark: high_water_mark.0, + }) + } +} + +/// NativeSizeFunction is a callable class which allows us to keep track that these size functions are not user provided, but +/// in fact represent the native size functions. This allows us to avoid JS calls by noticing that a size function is this class. +#[derive(JsLifetime, Trace, Clone, Copy)] +pub(super) enum NativeSizeFunction { + ByteLength, + Count, +} + +impl<'js> JsClass<'js> for NativeSizeFunction { + const NAME: &'static str = "NativeSizeFunction"; + + const KIND: rquickjs::class::ClassKind = rquickjs::class::ClassKind::Callable; + + type Mutable = Readable; + + fn prototype(ctx: &Ctx<'js>) -> Result>> { + Ok(Some(Function::prototype(ctx.clone()))) + } + + fn constructor(_ctx: &Ctx<'js>) -> Result>> { + Ok(None) + } + + fn call<'a>(this: &JsCell<'js, Self>, params: Params<'a, 'js>) -> Result> { + match &*this.borrow() { + NativeSizeFunction::Count => Ok(Value::new_int(params.ctx().clone(), 1)), + NativeSizeFunction::ByteLength => { + let Some(chunk) = params.arg(0) else { + return Err(Exception::throw_type( + params.ctx(), + "ByteLengthQueuingStrategy expects an argument 'chunk'", + )); + }; + + byte_length_queueing_strategy_size_function(params.ctx(), &chunk) + } + } + } +} + +fn byte_length_queueing_strategy_size_function<'js>( + ctx: &Ctx<'js>, + chunk: &Value<'js>, +) -> Result> { + if let Some(chunk) = chunk.as_object() { + chunk.get("byteLength") + } else { + Err(Exception::throw_type( + ctx, + "ByteLengthQueuingStrategy argument 'chunk' must be an object", + )) + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/queuing_strategy/tests.rs b/stdlib/src/llrt/llrt_stream_web/queuing_strategy/tests.rs new file mode 100644 index 00000000..13f6c302 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/queuing_strategy/tests.rs @@ -0,0 +1,159 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use crate::llrt_test::test_sync_with; + +#[tokio::test] +async fn high_water_mark_uses_javascript_number_conversion() { + test_sync_with(|ctx| { + crate::llrt_stream_web::init(&ctx)?; + ctx.eval::<(), _>( + r#" + const converted = { + valueOf() { + return "7.5"; + }, + }; + + const observed = []; + new ReadableStream({ + start(controller) { + observed.push(controller.desiredSize); + }, + }, { highWaterMark: converted }); + observed.push( + new WritableStream({}, { highWaterMark: converted }) + .getWriter().desiredSize, + ); + + const transform = new TransformStream({ + start(controller) { + observed.push(controller.desiredSize); + }, + }, { highWaterMark: converted }, { highWaterMark: converted }); + observed.push(transform.writable.getWriter().desiredSize); + + observed.push( + new CountQueuingStrategy({ highWaterMark: converted }).highWaterMark, + new ByteLengthQueuingStrategy({ highWaterMark: converted }).highWaterMark, + ); + + if (observed.length !== 6 || observed.some(value => value !== 7.5)) { + throw new Error(`Unexpected highWaterMark values: ${observed}`); + } + + const primitiveConversions = [ + [false, 0], + [true, 1], + ["2.25", 2.25], + ]; + for (const [input, expected] of primitiveConversions) { + const strategy = new CountQueuingStrategy({ highWaterMark: input }); + if (strategy.highWaterMark !== expected) { + throw new Error(`${String(input)} converted to ${strategy.highWaterMark}`); + } + } + "#, + ) + }) + .await; +} + +#[tokio::test] +async fn high_water_mark_conversion_order_and_errors_are_observable() { + test_sync_with(|ctx| { + crate::llrt_stream_web::init(&ctx)?; + ctx.eval::<(), _>( + r#" + const order = []; + new WritableStream({}, { + get highWaterMark() { + order.push("get highWaterMark"); + return { + valueOf() { + order.push("convert highWaterMark"); + return 1; + }, + }; + }, + get size() { + order.push("get size"); + return undefined; + }, + }); + + const expectedOrder = + "get highWaterMark,convert highWaterMark,get size"; + if (order.join() !== expectedOrder) { + throw new Error(`Unexpected conversion order: ${order}`); + } + + const expectedError = new Error("number conversion failed"); + const throwingValue = { + valueOf() { + throw expectedError; + }, + }; + const factories = [ + () => new ReadableStream({}, { highWaterMark: throwingValue }), + () => new WritableStream({}, { highWaterMark: throwingValue }), + () => new TransformStream({}, { highWaterMark: throwingValue }), + () => new TransformStream({}, {}, { highWaterMark: throwingValue }), + () => new CountQueuingStrategy({ highWaterMark: throwingValue }), + () => new ByteLengthQueuingStrategy({ highWaterMark: throwingValue }), + ]; + + for (const factory of factories) { + try { + factory(); + throw new Error("Expected number conversion to throw"); + } catch (error) { + if (error !== expectedError) { + throw new Error(`Unexpected conversion error: ${error}`); + } + } + } + + for (const input of [1n, Symbol("highWaterMark")]) { + try { + new CountQueuingStrategy({ highWaterMark: input }); + throw new Error("Expected ToNumber to reject the value"); + } catch (error) { + if (!(error instanceof TypeError)) { + throw new Error(`Expected TypeError, got ${error}`); + } + } + } + "#, + ) + }) + .await; +} + +#[tokio::test] +async fn invalid_converted_stream_high_water_marks_throw_range_error() { + test_sync_with(|ctx| { + crate::llrt_stream_web::init(&ctx)?; + ctx.eval::<(), _>( + r#" + for (const highWaterMark of ["-1", "not a number"]) { + const factories = [ + () => new ReadableStream({}, { highWaterMark }), + () => new WritableStream({}, { highWaterMark }), + () => new TransformStream({}, { highWaterMark }), + () => new TransformStream({}, {}, { highWaterMark }), + ]; + for (const factory of factories) { + try { + factory(); + throw new Error("Expected invalid highWaterMark to throw"); + } catch (error) { + if (!(error instanceof RangeError)) { + throw new Error(`Expected RangeError, got ${error}`); + } + } + } + } + "#, + ) + }) + .await; +} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/byob_reader.rs b/stdlib/src/llrt/llrt_stream_web/readable/byob_reader.rs new file mode 100644 index 00000000..6742742a --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/readable/byob_reader.rs @@ -0,0 +1,624 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use std::collections::VecDeque; + +use crate::llrt_utils::{bytes::ObjectBytes, primordials::Primordial}; +use rquickjs::{ + atom::PredefinedAtom, + class::{JsClass, OwnedBorrowMut, Trace, Tracer}, + function::Constructor, + methods, + prelude::{Opt, This}, + ArrayBuffer, Class, Ctx, Error, Exception, FromJs, Function, IntoJs, JsLifetime, Object, + Promise, Result, Value, +}; + +use crate::llrt_stream_web::{ + readable::{ + byte_controller::ReadableByteStreamController, + controller::{ReadableStreamController, ReadableStreamControllerClass}, + default_reader::{ReadableStreamDefaultReaderOwned, ReadableStreamReadResult}, + objects::{ReadableStreamBYOBObjects, ReadableStreamObjects}, + reader::{ReadableStreamGenericReader, ReadableStreamReader, ReadableStreamReaderOwned}, + stream::{ReadableStreamOwned, ReadableStreamState}, + }, + utils::{ + promise::{promise_rejected_with_constructor, with_promise_result, ResolveablePromise}, + UnwrapOrUndefined, ValueOrUndefined, + }, +}; + +#[derive(Trace)] +#[rquickjs::class] +pub(crate) struct ReadableStreamBYOBReader<'js> { + pub(super) generic: ReadableStreamGenericReader<'js>, + pub(super) read_into_requests: VecDeque + 'js>>, +} + +pub(crate) type ReadableStreamBYOBReaderClass<'js> = Class<'js, ReadableStreamBYOBReader<'js>>; +pub(crate) type ReadableStreamBYOBReaderOwned<'js> = + OwnedBorrowMut<'js, ReadableStreamBYOBReader<'js>>; + +unsafe impl<'js> JsLifetime<'js> for ReadableStreamBYOBReader<'js> { + type Changed<'to> = ReadableStreamBYOBReader<'to>; +} + +impl<'js> ReadableStreamBYOBReader<'js> { + pub(super) fn readable_stream_byob_reader_error_read_into_requests( + mut objects: ReadableStreamBYOBObjects<'js>, + e: Value<'js>, + ) -> Result> { + // Let readIntoRequests be reader.[[readIntoRequests]]. + let read_into_requests = &mut objects.reader.read_into_requests; + + // Set reader.[[readIntoRequests]] to a new empty list. + let read_into_requests = read_into_requests.split_off(0); + // For each readIntoRequest of readIntoRequests, + for read_into_request in read_into_requests { + // Perform readIntoRequest’s error steps, given e. + objects = read_into_request.error_steps(objects, e.clone())?; + } + + Ok(objects) + } + + pub(super) fn set_up_readable_stream_byob_reader( + ctx: Ctx<'js>, + stream: ReadableStreamOwned<'js>, + ) -> Result<(ReadableStreamOwned<'js>, Class<'js, Self>)> { + // If ! IsReadableStreamLocked(stream) is true, throw a TypeError exception. + if stream.is_readable_stream_locked() { + return Err(Exception::throw_type( + &ctx, + "This stream has already been locked for exclusive reading by another reader", + )); + } + + // If stream.[[controller]] does not implement ReadableByteStreamController, throw a TypeError exception. + match stream.controller { + ReadableStreamControllerClass::ReadableStreamByteController(_) => {} + _ => { + return Err(Exception::throw_type( + &ctx, + "Cannot construct a ReadableStreamBYOBReader for a stream not constructed with a byte source", + )); + } + }; + + // Perform ! ReadableStreamReaderGenericInitialize(reader, stream). + let generic = + ReadableStreamGenericReader::readable_stream_reader_generic_initialize(&ctx, stream)?; + + let mut stream = OwnedBorrowMut::from_class(generic.stream.clone().unwrap()); + + let reader = Class::instance( + ctx.clone(), + Self { + generic, + // Set reader.[[readIntoRequests]] to a new empty list. + read_into_requests: VecDeque::new(), + }, + )?; + + stream.reader = Some(reader.clone().into()); + + Ok((stream, reader)) + } + + pub(super) fn readable_stream_byob_reader_release( + mut objects: ReadableStreamBYOBObjects<'js>, + ) -> Result> { + // Perform ! ReadableStreamReaderGenericRelease(reader). + objects + .reader + .generic + .readable_stream_reader_generic_release(&mut objects.stream, || { + objects.controller.release_steps() + })?; + + // Let e be a new TypeError exception. + let e: Value = objects + .stream + .constructor_type_error + .call(("Reader was released",))?; + // Perform ! ReadableStreamBYOBReaderErrorReadIntoRequests(reader, e). + Self::readable_stream_byob_reader_error_read_into_requests(objects, e) + } + + pub(super) fn readable_stream_byob_reader_read( + ctx: &Ctx<'js>, + // Let stream be reader.[[stream]]. + mut objects: ReadableStreamBYOBObjects<'js>, + view: ViewBytes<'js>, + min: u64, + read_into_request: impl ReadableStreamReadIntoRequest<'js> + 'js, + ) -> Result> { + // Set stream.[[disturbed]] to true. + objects.stream.disturbed = true; + + // If stream.[[state]] is "errored", perform readIntoRequest’s error steps given stream.[[storedError]]. + if let ReadableStreamState::Errored(ref stored_error) = objects.stream.state { + let stored_error = stored_error.clone(); + read_into_request.error_steps(objects, stored_error) + } else { + // Otherwise, perform ! ReadableByteStreamControllerPullInto(stream.[[controller]], view, min, readIntoRequest). + ReadableByteStreamController::readable_byte_stream_controller_pull_into( + ctx, + objects, + view, + min, + read_into_request, + ) + } + } +} + +#[methods(rename_all = "camelCase")] +impl<'js> ReadableStreamBYOBReader<'js> { + // this is required by web platform tests + #[qjs(get)] + pub fn constructor(ctx: Ctx<'js>) -> Result>> { + ::constructor(&ctx) + } + + #[qjs(constructor)] + pub fn new(ctx: Ctx<'js>, stream: ReadableStreamOwned<'js>) -> Result> { + // Perform ? SetUpReadableStreamBYOBReader(this, stream). + let (_, reader) = Self::set_up_readable_stream_byob_reader(ctx, stream)?; + Ok(reader) + } + + fn read( + ctx: Ctx<'js>, + reader: This>, + view: Opt>, + options: Opt>, + ) -> Result> { + with_promise_result(&ctx, || { + let options = match options.0 { + None => ReadableStreamBYOBReaderReadOptions { min: 1 }, + Some(value) => ReadableStreamBYOBReaderReadOptions::from_js(&ctx, value)?, + }; + + let view = ViewBytes::from_value( + &ctx, + &reader.generic.function_array_buffer_is_view, + view.0.as_ref(), + )?; + + let (buffer, byte_length, _) = view.get_array_buffer()?; + + // If view.[[ByteLength]] is 0, return a promise rejected with a TypeError exception. + if byte_length == 0 { + return promise_rejected_with_constructor( + &reader.generic.constructor_type_error, + &reader.generic.promise_primordials, + "view must have non-zero byteLength", + ); + } + + // If view.[[ViewedArrayBuffer]].[[ArrayBufferByteLength]] is 0, return a promise rejected with a TypeError exception. + if buffer.is_empty() { + return promise_rejected_with_constructor( + &reader.generic.constructor_type_error, + &reader.generic.promise_primordials, + "view's buffer must have non-zero byteLength", + ); + } + + // If ! IsDetachedBuffer(view.[[ViewedArrayBuffer]]) is true, return a promise rejected with a TypeError exception. + if buffer.as_bytes().is_none() { + return promise_rejected_with_constructor( + &reader.generic.constructor_type_error, + &reader.generic.promise_primordials, + "view's buffer has been detached", + ); + } + + // If options["min"] is 0, return a promise rejected with a TypeError exception. + if options.min == 0 { + return promise_rejected_with_constructor( + &reader.generic.constructor_type_error, + &reader.generic.promise_primordials, + "options.min must be greater than 0", + ); + } + + // If view has a [[TypedArrayName]] internal slot, + let typed_array_len = match &view.0 { + ObjectBytes::U8Array(a) => Some(a.len()), + ObjectBytes::I8Array(a) => Some(a.len()), + ObjectBytes::U16Array(a) => Some(a.len()), + ObjectBytes::I16Array(a) => Some(a.len()), + ObjectBytes::U32Array(a) => Some(a.len()), + ObjectBytes::I32Array(a) => Some(a.len()), + ObjectBytes::U64Array(a) => Some(a.len()), + ObjectBytes::I64Array(a) => Some(a.len()), + ObjectBytes::F32Array(a) => Some(a.len()), + ObjectBytes::F64Array(a) => Some(a.len()), + _ => None, + }; + if let Some(typed_array_len) = typed_array_len { + // If options["min"] > view.[[ArrayLength]], return a promise rejected with a RangeError exception. + if options.min > typed_array_len as u64 { + return promise_rejected_with_constructor( + &reader.generic.constructor_range_error, + &reader.generic.promise_primordials, + "options.min must be less than or equal to views length", + ); + } + } else { + // Otherwise (i.e., it is a DataView), + // If options["min"] > view.[[ByteLength]], return a promise rejected with a RangeError exception. + if options.min > byte_length as u64 { + return promise_rejected_with_constructor( + &reader.generic.constructor_range_error, + &reader.generic.promise_primordials, + "options.min must be less than or equal to views byteLength", + ); + } + } + + // If this.[[stream]] is undefined, return a promise rejected with a TypeError exception. + if reader.generic.stream.is_none() { + return promise_rejected_with_constructor( + &reader.generic.constructor_type_error, + &reader.generic.promise_primordials, + "Cannot read a stream using a released reader", + ); + } + + // Let promise be a new promise. + let promise = ResolveablePromise::new(&ctx)?; + // Let readIntoRequest be a new read-into request with the following items: + #[derive(Trace)] + struct ReadIntoRequest<'js> { + promise: ResolveablePromise<'js>, + } + + impl<'js> ReadableStreamReadIntoRequest<'js> for ReadIntoRequest<'js> { + // chunk steps, given chunk + // Resolve promise with «[ "value" → chunk, "done" → false ]». + fn chunk_steps( + &self, + objects: ReadableStreamBYOBObjects<'js>, + chunk: Value<'js>, + ) -> Result> { + self.promise.resolve(ReadableStreamReadResult { + value: Some(chunk), + done: false, + })?; + Ok(objects) + } + + // close steps, given chunk + // Resolve promise with «[ "value" → chunk, "done" → true ]». + fn close_steps( + &self, + objects: ReadableStreamBYOBObjects<'js>, + chunk: Value<'js>, + ) -> Result> { + self.promise.resolve(ReadableStreamReadResult { + value: Some(chunk), + done: true, + })?; + Ok(objects) + } + + // error steps, given e + // Reject promise with e. + fn error_steps( + &self, + objects: ReadableStreamBYOBObjects<'js>, + reason: Value<'js>, + ) -> Result> { + self.promise.reject(reason)?; + Ok(objects) + } + } + + let objects = ReadableStreamObjects::from_byob_reader(reader.0); + + // Perform ! ReadableStreamBYOBReaderRead(this, view, options["min"], readIntoRequest). + Self::readable_stream_byob_reader_read( + &ctx, + objects, + view, + options.min, + ReadIntoRequest { + promise: promise.clone(), + }, + )?; + + // Return promise. + Ok(promise.promise) + }) + } + + fn release_lock(reader: This>) -> Result<()> { + // If this.[[stream]] is undefined, return. + if reader.generic.stream.is_none() { + return Ok(()); + }; + + let objects = ReadableStreamObjects::from_byob_reader(reader.0); + + // Perform ! ReadableStreamBYOBReaderRelease(this). + Self::readable_stream_byob_reader_release(objects)?; + + Ok(()) + } + + #[qjs(get)] + fn closed(&self) -> Promise<'js> { + self.generic.closed_promise.promise.clone() + } + + fn cancel( + ctx: Ctx<'js>, + reader: This>, + reason: Opt>, + ) -> Result> { + if reader.generic.stream.is_none() { + // If this.[[stream]] is undefined, return a promise rejected with a TypeError exception. + return promise_rejected_with_constructor( + &reader.generic.constructor_type_error, + &reader.generic.promise_primordials, + "Cannot cancel a stream using a released reader", + ); + } + + let objects = ReadableStreamObjects::from_byob_reader(reader.0); + + // Return ! ReadableStreamReaderGenericCancel(this, reason). + let (promise, _) = ReadableStreamGenericReader::readable_stream_reader_generic_cancel( + ctx.clone(), + objects, + reason.0.unwrap_or_undefined(&ctx), + )?; + Ok(promise) + } +} + +struct ReadableStreamBYOBReaderReadOptions { + min: u64, +} + +impl<'js> FromJs<'js> for ReadableStreamBYOBReaderReadOptions { + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let ty_name = value.type_name(); + let obj = value + .as_object() + .ok_or(Error::new_from_js(ty_name, "Object"))?; + + let min = obj.get_value_or_undefined::<_, f64>("min")?.unwrap_or(1.0); + if min < u64::MIN as f64 || min > u64::MAX as f64 { + return Err(Exception::throw_type( + ctx, + "min on ReadableStreamBYOBReaderReadOptions must fit into unsigned long long", + )); + }; + + Ok(Self { min: min as u64 }) + } +} + +pub(super) trait ReadableStreamReadIntoRequest<'js>: Trace<'js> { + fn chunk_steps( + &self, + objects: ReadableStreamBYOBObjects<'js>, + chunk: Value<'js>, + ) -> Result>; + + fn close_steps( + &self, + objects: ReadableStreamBYOBObjects<'js>, + chunk: Value<'js>, + ) -> Result>; + + fn error_steps( + &self, + objects: ReadableStreamBYOBObjects<'js>, + reason: Value<'js>, + ) -> Result>; +} + +impl<'js> Trace<'js> for Box + 'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + self.as_ref().trace(tracer); + } +} + +#[derive(JsLifetime, Clone)] +pub(super) struct ViewBytes<'js>(ObjectBytes<'js>); + +impl<'js> ViewBytes<'js> { + pub(super) fn from_object( + ctx: &Ctx<'js>, + function_array_buffer_is_view: &Function<'js>, + object: &Object<'js>, + ) -> Result { + if function_array_buffer_is_view.call::<_, bool>((object.clone(),))? { + if let Some(view) = ObjectBytes::from_array_buffer(object)? { + return Ok(Self(view)); + } + } + + Err(Exception::throw_type( + ctx, + "view must be an ArrayBufferView", + )) + } + + pub(super) fn from_value( + ctx: &Ctx<'js>, + function_array_buffer_is_view: &Function<'js>, + value: Option<&Value<'js>>, + ) -> Result { + match value.and_then(Value::as_object) { + None => { + Err(Exception::throw_type( + ctx, + "view must be typed DataView, Buffer, ArrayBuffer, or Uint8Array, but is not an object", + )) + }, + Some(object) => Self::from_object(ctx, function_array_buffer_is_view, object), + } + } + + pub(super) fn get_array_buffer(&self) -> Result<(ArrayBuffer<'js>, usize, usize)> { + Ok(self + .0 + .get_array_buffer()? + .expect("invariant broken; ViewBytes may not contain ObjectBytes::Vec")) + } + + pub(super) fn element_size(&self) -> usize { + match self.0 { + ObjectBytes::U8Array(_) => 1, + ObjectBytes::I8Array(_) => 1, + ObjectBytes::U16Array(_) => 2, + ObjectBytes::I16Array(_) => 2, + ObjectBytes::U32Array(_) => 4, + ObjectBytes::I32Array(_) => 4, + ObjectBytes::U64Array(_) => 8, + ObjectBytes::I64Array(_) => 8, + ObjectBytes::F16Array(_) => 2, + ObjectBytes::F32Array(_) => 4, + ObjectBytes::F64Array(_) => 8, + ObjectBytes::U8ClampedArray(_) => 1, + ObjectBytes::DataView(_, _, _) => 1, + ObjectBytes::Vec(_) => { + panic!("invariant broken; ViewBytes may not contain ObjectBytes::Vec") + } + } + } +} + +#[derive(Clone, JsLifetime)] +pub(crate) struct ArrayConstructorPrimordials<'js> { + pub(super) constructor_uint8array: Constructor<'js>, + constructor_int8array: Constructor<'js>, + constructor_uint16array: Constructor<'js>, + constructor_int16array: Constructor<'js>, + constructor_uint32array: Constructor<'js>, + constructor_int32array: Constructor<'js>, + constructor_uint64array: Constructor<'js>, + constructor_int64array: Constructor<'js>, + constructor_f16array: Constructor<'js>, + constructor_f32array: Constructor<'js>, + constructor_f64array: Constructor<'js>, + constructor_uint8clampedarray: Constructor<'js>, + constructor_data_view: Constructor<'js>, +} + +impl<'js> Trace<'js> for ArrayConstructorPrimordials<'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + self.constructor_uint8array.trace(tracer); + self.constructor_int8array.trace(tracer); + self.constructor_uint16array.trace(tracer); + self.constructor_int16array.trace(tracer); + self.constructor_uint32array.trace(tracer); + self.constructor_int32array.trace(tracer); + self.constructor_uint64array.trace(tracer); + self.constructor_int64array.trace(tracer); + self.constructor_f16array.trace(tracer); + self.constructor_f32array.trace(tracer); + self.constructor_f64array.trace(tracer); + self.constructor_uint8clampedarray.trace(tracer); + self.constructor_data_view.trace(tracer); + } +} + +impl<'js> Primordial<'js> for ArrayConstructorPrimordials<'js> { + fn new(ctx: &Ctx<'js>) -> Result + where + Self: Sized, + { + let globals = ctx.globals(); + Ok(Self { + constructor_uint8array: globals.get(PredefinedAtom::Uint8Array)?, + constructor_int8array: globals.get(PredefinedAtom::Int8Array)?, + constructor_uint16array: globals.get(PredefinedAtom::Uint16Array)?, + constructor_int16array: globals.get(PredefinedAtom::Int16Array)?, + constructor_uint32array: globals.get(PredefinedAtom::Uint32Array)?, + constructor_int32array: globals.get(PredefinedAtom::Int32Array)?, + constructor_uint64array: globals.get(PredefinedAtom::BigUint64Array)?, + constructor_int64array: globals.get(PredefinedAtom::BigInt64Array)?, + constructor_f16array: globals.get(PredefinedAtom::Float16Array)?, + constructor_f32array: globals.get(PredefinedAtom::Float32Array)?, + constructor_f64array: globals.get(PredefinedAtom::Float64Array)?, + constructor_uint8clampedarray: globals.get(PredefinedAtom::Uint8ClampedArray)?, + constructor_data_view: globals.get(PredefinedAtom::DataView)?, + }) + } +} + +impl<'js> ArrayConstructorPrimordials<'js> { + pub(super) fn for_view_bytes(&self, v: &ViewBytes<'js>) -> Constructor<'js> { + match v.0 { + ObjectBytes::U8Array(_) => self.constructor_uint8array.clone(), + ObjectBytes::I8Array(_) => self.constructor_int8array.clone(), + ObjectBytes::U16Array(_) => self.constructor_uint16array.clone(), + ObjectBytes::I16Array(_) => self.constructor_int16array.clone(), + ObjectBytes::U32Array(_) => self.constructor_uint32array.clone(), + ObjectBytes::I32Array(_) => self.constructor_int32array.clone(), + ObjectBytes::U64Array(_) => self.constructor_uint64array.clone(), + ObjectBytes::I64Array(_) => self.constructor_int64array.clone(), + ObjectBytes::F16Array(_) => self.constructor_f16array.clone(), + ObjectBytes::F32Array(_) => self.constructor_f32array.clone(), + ObjectBytes::F64Array(_) => self.constructor_f64array.clone(), + ObjectBytes::U8ClampedArray(_) => self.constructor_uint8clampedarray.clone(), + ObjectBytes::DataView(_, _, _) => self.constructor_data_view.clone(), + ObjectBytes::Vec(_) => { + panic!("invariant broken; ViewBytes may not contain ObjectBytes::Vec") + } + } + } +} + +impl<'js> Trace<'js> for ViewBytes<'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + self.0.trace(tracer); + } +} + +impl<'js> IntoJs<'js> for ViewBytes<'js> { + fn into_js(self, ctx: &Ctx<'js>) -> Result> { + self.0.into_js(ctx) + } +} + +impl<'js> ReadableStreamReader<'js> for ReadableStreamBYOBReaderOwned<'js> { + type Class = ReadableStreamBYOBReaderClass<'js>; + + fn with_reader( + self, + ctx: C, + _: impl FnOnce( + C, + ReadableStreamDefaultReaderOwned<'js>, + ) -> Result<(C, ReadableStreamDefaultReaderOwned<'js>)>, + byob: impl FnOnce( + C, + ReadableStreamBYOBReaderOwned<'js>, + ) -> Result<(C, ReadableStreamBYOBReaderOwned<'js>)>, + _: impl FnOnce(C) -> Result, + ) -> Result<(C, Self)> { + byob(ctx, self) + } + + fn into_inner(self) -> Self::Class { + self.into_inner() + } + + fn from_class(class: Self::Class) -> Self { + OwnedBorrowMut::from_class(class) + } + + fn try_from_erased(erased: Option>) -> Option { + match erased { + Some(ReadableStreamReaderOwned::ReadableStreamBYOBReader(r)) => Some(r), + _ => None, + } + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/byte_controller.rs b/stdlib/src/llrt/llrt_stream_web/readable/byte_controller.rs new file mode 100644 index 00000000..854be6f0 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/readable/byte_controller.rs @@ -0,0 +1,2169 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use std::collections::VecDeque; + +use crate::llrt_utils::{ + error_messages::ERROR_MSG_ARRAY_BUFFER_DETACHED, + option::{Null, Undefined}, + primordials::{BasePrimordials, Primordial}, + result::ResultExt, +}; +use rquickjs::{ + class::{OwnedBorrow, OwnedBorrowMut, Trace, Tracer}, + function::Constructor, + methods, + prelude::{Opt, This}, + ArrayBuffer, Class, Ctx, Error, Exception, Function, IntoJs, JsLifetime, Object, Promise, + Result, TypedArray, Value, +}; + +use crate::llrt_stream_web::{ + readable::{ + byob_reader::{ArrayConstructorPrimordials, ReadableStreamReadIntoRequest, ViewBytes}, + controller::{ + ReadableStreamController, ReadableStreamControllerClass, ReadableStreamControllerOwned, + }, + default_controller::ReadableStreamDefaultControllerOwned, + default_reader::ReadableStreamReadRequest, + objects::{ + ReadableByteStreamObjects, ReadableStreamBYOBObjects, ReadableStreamClassObjects, + ReadableStreamDefaultReaderObjects, ReadableStreamObjects, + }, + reader::ReadableStreamReader, + stream::{ + algorithms::{CancelAlgorithm, PullAlgorithm, StartAlgorithm}, + source::UnderlyingSource, + ReadableStream, ReadableStreamClass, ReadableStreamOwned, ReadableStreamState, + }, + }, + utils::{ + class_from_owned_borrow_mut, + promise::{promise_resolved_with, upon_promise}, + UnwrapOrUndefined, + }, +}; + +#[derive(JsLifetime)] +#[rquickjs::class] +pub struct ReadableByteStreamController<'js> { + auto_allocate_chunk_size: Option, + byob_request: Option>>, + cancel_algorithm: Option>, + close_requested: bool, + pull_again: bool, + pull_algorithm: Option>, + pulling: bool, + pub(super) pending_pull_intos: VecDeque>, + queue: VecDeque>, + queue_total_size: usize, + started: bool, + strategy_hwm: f64, + pub(super) stream: ReadableStreamClass<'js>, + + pub(super) array_constructor_primordials: ArrayConstructorPrimordials<'js>, + constructor_array_buffer: Constructor<'js>, + pub(super) function_array_buffer_is_view: Function<'js>, +} + +impl<'js> Trace<'js> for ReadableByteStreamController<'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + self.auto_allocate_chunk_size.trace(tracer); + self.byob_request.trace(tracer); + self.cancel_algorithm.trace(tracer); + self.pull_algorithm.trace(tracer); + self.pending_pull_intos.trace(tracer); + self.queue.trace(tracer); + self.queue_total_size.trace(tracer); + self.started.trace(tracer); + self.strategy_hwm.trace(tracer); + self.stream.trace(tracer); + self.array_constructor_primordials.trace(tracer); + self.constructor_array_buffer.trace(tracer); + self.function_array_buffer_is_view.trace(tracer); + } +} + +pub type ReadableByteStreamControllerClass<'js> = Class<'js, ReadableByteStreamController<'js>>; +pub(crate) type ReadableByteStreamControllerOwned<'js> = + OwnedBorrowMut<'js, ReadableByteStreamController<'js>>; + +impl<'js> ReadableByteStreamController<'js> { + // SetUpReadableByteStreamControllerFromUnderlyingSource + pub(super) fn set_up_readable_byte_stream_controller_from_underlying_source( + ctx: &Ctx<'js>, + stream: ReadableStreamOwned<'js>, + underlying_source: Null>>, + underlying_source_dict: UnderlyingSource<'js>, + high_water_mark: f64, + ) -> Result<()> { + let (start_algorithm, pull_algorithm, cancel_algorithm, auto_allocate_chunk_size) = ( + // If underlyingSourceDict["start"] exists, then set startAlgorithm to an algorithm which returns the result of invoking underlyingSourceDict["start"] with argument list + // « controller » and callback this value underlyingSource. + underlying_source_dict + .start + .map(|f| StartAlgorithm::Function { + f, + underlying_source: underlying_source.clone(), + }) + .unwrap_or(StartAlgorithm::ReturnUndefined), + // If underlyingSourceDict["pull"] exists, then set pullAlgorithm to an algorithm which returns the result of invoking underlyingSourceDict["pull"] with argument list + // « controller » and callback this value underlyingSource. + underlying_source_dict + .pull + .map(|f| PullAlgorithm::Function { + f, + underlying_source: underlying_source.clone(), + }) + .unwrap_or(PullAlgorithm::ReturnPromiseUndefined), + // If underlyingSourceDict["cancel"] exists, then set cancelAlgorithm to an algorithm which takes an argument reason and returns the result of invoking underlyingSourceDict["cancel"] with argument list + // « reason » and callback this value underlyingSource. + underlying_source_dict + .cancel + .map(|f| CancelAlgorithm::Function { + f, + underlying_source, + }) + .unwrap_or(CancelAlgorithm::ReturnPromiseUndefined), + // Let autoAllocateChunkSize be underlyingSourceDict["autoAllocateChunkSize"], if it exists, or undefined otherwise. + underlying_source_dict.auto_allocate_chunk_size, + ); + + // If autoAllocateChunkSize is 0, then throw a TypeError exception. + if auto_allocate_chunk_size == Some(0) { + return Err(Exception::throw_type( + ctx, + "autoAllocateChunkSize must be greater than 0", + )); + } + + Self::set_up_readable_byte_stream_controller( + ctx.clone(), + stream, + start_algorithm, + pull_algorithm, + cancel_algorithm, + high_water_mark, + auto_allocate_chunk_size, + )?; + + Ok(()) + } + + pub(super) fn set_up_readable_byte_stream_controller( + ctx: Ctx<'js>, + stream: ReadableStreamOwned<'js>, + start_algorithm: StartAlgorithm<'js>, + pull_algorithm: PullAlgorithm<'js>, + cancel_algorithm: CancelAlgorithm<'js>, + high_water_mark: f64, + auto_allocate_chunk_size: Option, + ) -> Result> { + let (stream_class, mut stream) = class_from_owned_borrow_mut(stream); + + let array_constructor_primordials = ArrayConstructorPrimordials::get(&ctx)?.clone(); + let BasePrimordials { + constructor_array_buffer, + function_array_buffer_is_view, + .. + } = &*BasePrimordials::get(&ctx)?; + + let controller = Self { + // Set controller.[[stream]] to stream. + stream: stream_class, + + // Set controller.[[pullAgain]] and controller.[[pulling]] to false. + pull_again: false, + pulling: false, + + // Set controller.[[byobRequest]] to null. + byob_request: None, + + // Perform ! ResetQueue(controller). + queue: VecDeque::new(), + queue_total_size: 0, + + // Set controller.[[closeRequested]] and controller.[[started]] to false. + close_requested: false, + started: false, + + // Set controller.[[strategyHWM]] to highWaterMark. + strategy_hwm: high_water_mark, + + // Set controller.[[pullAlgorithm]] to pullAlgorithm. + pull_algorithm: Some(pull_algorithm), + cancel_algorithm: Some(cancel_algorithm), + + // Set controller.[[autoAllocateChunkSize]] to autoAllocateChunkSize. + auto_allocate_chunk_size, + + pending_pull_intos: VecDeque::new(), + + array_constructor_primordials, + constructor_array_buffer: constructor_array_buffer.clone(), + function_array_buffer_is_view: function_array_buffer_is_view.clone(), + }; + + let controller_class = Class::instance(ctx.clone(), controller)?; + + // Set stream.[[controller]] to controller. + stream.controller = + ReadableStreamControllerClass::ReadableStreamByteController(controller_class.clone()); + + let objects = + ReadableStreamObjects::new_byte(stream, OwnedBorrowMut::from_class(controller_class)) + .refresh_reader(); + + let promise_primordials = objects.stream.promise_primordials.clone(); + + // Let startResult be the result of performing startAlgorithm. + let (start_result, objects_class) = + Self::start_algorithm(ctx.clone(), objects, start_algorithm)?; + + // Let startPromise be a promise resolved with startResult. + let start_promise = promise_resolved_with(&ctx, &promise_primordials, Ok(start_result))?; + + let _ = upon_promise::, _>(ctx.clone(), start_promise, { + let objects_class = objects_class.clone(); + move |ctx, result| { + let mut objects = + ReadableStreamObjects::from_class_no_reader(objects_class).refresh_reader(); + match result { + // Upon fulfillment of startPromise, + Ok(_) => { + // Set controller.[[started]] to true. + objects.controller.started = true; + // Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). + Self::readable_byte_stream_controller_call_pull_if_needed(ctx, objects)?; + Ok(()) + } + // Upon rejection of startPromise with reason r, + Err(r) => { + // Perform ! ReadableByteStreamControllerError(controller, r). + Self::readable_byte_stream_controller_error(objects, r)?; + Ok(()) + } + } + } + })?; + + Ok(objects_class.controller) + } + + fn readable_byte_stream_controller_call_pull_if_needed>( + ctx: Ctx<'js>, + objects: ReadableByteStreamObjects<'js, R>, + ) -> Result> { + // Let shouldPull be ! ReadableByteStreamControllerShouldCallPull(controller). + let (should_pull, mut objects) = + Self::readable_byte_stream_controller_should_call_pull(objects); + + // If shouldPull is false, return. + if !should_pull { + return Ok(objects); + } + + // If controller.[[pulling]] is true, + if objects.controller.pulling { + // Set controller.[[pullAgain]] to true. + objects.controller.pull_again = true; + + // Return. + return Ok(objects); + } + + // Set controller.[[pulling]] to true. + objects.controller.pulling = true; + + // Let pullPromise be the result of performing controller.[[pullAlgorithm]]. + let (pull_promise, objects_class) = Self::pull_algorithm(ctx.clone(), objects)?; + + upon_promise::, ()>(ctx, pull_promise, { + let objects_class = objects_class.clone(); + move |ctx, result| { + let mut objects = + ReadableStreamObjects::from_class_no_reader(objects_class).refresh_reader(); + match result { + // Upon fulfillment of pullPromise, + Ok(_) => { + // Set controller.[[pulling]] to false. + objects.controller.pulling = false; + // If controller.[[pullAgain]] is true, + if objects.controller.pull_again { + // Set controller.[[pullAgain]] to false. + objects.controller.pull_again = false; + // Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). + Self::readable_byte_stream_controller_call_pull_if_needed( + ctx, objects, + )?; + }; + Ok(()) + } + // Upon rejection of pullPromise with reason e, + Err(e) => { + // Perform ! ReadableByteStreamControllerError(controller, e). + Self::readable_byte_stream_controller_error(objects, e)?; + Ok(()) + } + } + } + })?; + + Ok(ReadableStreamObjects::from_class(objects_class)) + } + + fn readable_byte_stream_controller_should_call_pull>( + mut objects: ReadableByteStreamObjects<'js, R>, + ) -> (bool, ReadableByteStreamObjects<'js, R>) { + // Let stream be controller.[[stream]]. + match objects.stream.state { + ReadableStreamState::Readable => {} + // If stream.[[state]] is not "readable", return false. + _ => return (false, objects), + } + + // If controller.[[closeRequested]] is true, return false. + if objects.controller.close_requested { + return (false, objects); + } + + // If controller.[[started]] is false, return false. + if !objects.controller.started { + return (false, objects); + } + + let (mut has_read_requests, mut has_read_into_requests) = (false, false); + objects = objects + .with_reader( + |objects| { + // If ! ReadableStreamHasDefaultReader(stream) is true and ! ReadableStreamGetNumReadRequests(stream) > 0, return true. + if ReadableStream::readable_stream_get_num_read_requests(&objects.reader) > 0 { + has_read_requests = true; + } + Ok(objects) + }, + |objects| { + // If ! ReadableStreamHasBYOBReader(stream) is true and ! ReadableStreamGetNumReadIntoRequests(stream) > 0, return true. + if ReadableStream::readable_stream_get_num_read_into_requests(&objects.reader) + > 0 + { + has_read_into_requests = true; + } + Ok(objects) + }, + Ok, + ) + .unwrap(); + + if has_read_requests || has_read_into_requests { + return (true, objects); + } + + // Let desiredSize be ! ReadableByteStreamControllerGetDesiredSize(controller). + let desired_size = objects + .controller + .readable_byte_stream_controller_get_desired_size(&objects.stream); + + // Assert: desiredSize is not null. + if desired_size.0.expect("desired_size must not be null") > 0.0 { + // If desiredSize > 0, return true. + return (true, objects); + } + + // Return false. + (false, objects) + } + + pub(super) fn readable_byte_stream_controller_error>( + // Let stream be controller.[[stream]]. + mut objects: ReadableByteStreamObjects<'js, R>, + e: Value<'js>, + ) -> Result> { + // If stream.[[state]] is not "readable", return. + if !matches!(objects.stream.state, ReadableStreamState::Readable) { + return Ok(objects); + }; + + // Perform ! ReadableByteStreamControllerClearPendingPullIntos(controller). + objects + .controller + .readable_byte_stream_controller_clear_pending_pull_intos(); + + // Perform ! ResetQueue(controller). + objects.controller.reset_queue(); + + // Perform ! ReadableByteStreamControllerClearAlgorithms(controller). + objects + .controller + .readable_byte_stream_controller_clear_algorithms(); + + // Perform ! ReadableStreamError(stream, e). + ReadableStream::readable_stream_error(objects, e) + } + + fn readable_byte_stream_controller_clear_pending_pull_intos(&mut self) { + // Perform ! ReadableByteStreamControllerInvalidateBYOBRequest(controller). + self.readable_byte_stream_controller_invalidate_byob_request(); + + // Set controller.[[pendingPullIntos]] to a new empty list. + self.pending_pull_intos.clear(); + } + + fn readable_byte_stream_controller_invalidate_byob_request(&mut self) { + let byob_request = match self.byob_request { + // If controller.[[byobRequest]] is null, return. + None => return, + Some(ref byob_request) => byob_request.clone(), + }; + let mut byob_request = OwnedBorrowMut::from_class(byob_request); + byob_request.controller = None; + byob_request.view = None; + + self.byob_request = None; + } + + fn readable_byte_stream_controller_clear_algorithms(&mut self) { + self.pull_algorithm = None; + self.cancel_algorithm = None; + } + + pub(super) fn readable_byte_stream_controller_get_byob_request( + ctx: Ctx<'js>, + controller: OwnedBorrowMut<'js, Self>, + ) -> Result<( + Null>>, + OwnedBorrowMut<'js, Self>, + )> { + // If controller.[[byobRequest]] is null and controller.[[pendingPullIntos]] is not empty, + if controller.byob_request.is_none() && !controller.pending_pull_intos.is_empty() { + // Let firstDescriptor be controller.[[pendingPullIntos]][0]. + let first_descriptor = &controller.pending_pull_intos[0]; + + // Let view be ! Construct(%Uint8Array%, « firstDescriptor’s buffer, firstDescriptor’s byte offset + firstDescriptor’s bytes filled, firstDescriptor’s byte length − firstDescriptor’s bytes filled »). + let view = ViewBytes::from_value( + &ctx, + &controller.function_array_buffer_is_view, + Some( + &controller + .array_constructor_primordials + .constructor_uint8array + .construct(( + first_descriptor.buffer.clone(), + first_descriptor.byte_offset + first_descriptor.bytes_filled, + first_descriptor.byte_length - first_descriptor.bytes_filled, + ))?, + ), + )?; + + let (controller_class, mut controller) = class_from_owned_borrow_mut(controller); + + // Let byobRequest be a new ReadableStreamBYOBRequest. + let byob_request = ReadableStreamBYOBRequest { + // Set byobRequest.[[controller]] to controller. + controller: Some(controller_class), + // Set byobRequest.[[view]] to view. + view: Some(view), + }; + + // Set controller.[[byobRequest]] to byobRequest. + controller.byob_request = Some(Class::instance(ctx, byob_request)?); + + Ok((Null(controller.byob_request.clone()), controller)) + } else { + // Return controller.[[byobRequest]]. + Ok((Null(controller.byob_request.clone()), controller)) + } + } + + fn readable_byte_stream_controller_get_desired_size( + &self, + stream: &ReadableStream<'js>, + ) -> Null { + // Let state be controller.[[stream]].[[state]]. + match stream.state { + // If state is "errored", return null. + ReadableStreamState::Errored(_) => Null(None), + // If state is "closed", return 0. + ReadableStreamState::Closed => Null(Some(0.0)), + // Return controller.[[strategyHWM]] − controller.[[queueTotalSize]]. + _ => Null(Some(self.strategy_hwm - self.queue_total_size as f64)), + } + } + + fn reset_queue(&mut self) { + // Set container.[[queue]] to a new empty list. + self.queue.clear(); + // Set container.[[queueTotalSize]] to 0. + self.queue_total_size = 0; + } + + pub(super) fn readable_byte_stream_controller_close>( + ctx: Ctx<'js>, + // Let stream be controller.[[stream]]. + mut objects: ReadableByteStreamObjects<'js, R>, + ) -> Result> { + // If controller.[[closeRequested]] is true or stream.[[state]] is not "readable", return. + if objects.controller.close_requested + || !matches!(objects.stream.state, ReadableStreamState::Readable) + { + return Ok(objects); + } + + // If controller.[[queueTotalSize]] > 0, + if objects.controller.queue_total_size > 0 { + // Set controller.[[closeRequested]] to true. + objects.controller.close_requested = true; + // Return. + return Ok(objects); + } + + // If controller.[[pendingPullIntos]] is not empty, + // Let firstPendingPullInto be controller.[[pendingPullIntos]][0]. + if let Some(first_pending_pull_into) = objects.controller.pending_pull_intos.front() { + // If the remainder after dividing firstPendingPullInto’s bytes filled by firstPendingPullInto’s element size is not 0, + if first_pending_pull_into.bytes_filled % first_pending_pull_into.element_size != 0 { + // Let e be a new TypeError exception. + let e: Value = objects + .stream + .constructor_type_error + .call(("Insufficient bytes to fill elements in the given buffer",))?; + Self::readable_byte_stream_controller_error(objects, e.clone())?; + return Err(ctx.throw(e)); + } + } + + // Perform ! ReadableByteStreamControllerClearAlgorithms(controller). + objects + .controller + .readable_byte_stream_controller_clear_algorithms(); + + // Perform ! ReadableStreamClose(stream). + ReadableStream::readable_stream_close(ctx, objects) + } + + pub(super) fn readable_byte_stream_controller_enqueue>( + ctx: &Ctx<'js>, + // Let stream be controller.[[stream]]. + objects: ReadableByteStreamObjects<'js, R>, + chunk: ViewBytes<'js>, + ) -> Result> { + Self::readable_byte_stream_controller_enqueue_impl( + ctx, objects, chunk, /*skip_transfer=*/ false, + ) + } + + /// Like [`readable_byte_stream_controller_enqueue`] but skips the + /// spec-mandated `TransferArrayBuffer(chunk)` step on the incoming + /// chunk. Used by producers that already own the backing allocation + /// and want to hand it to the stream without QuickJS copying or + /// detaching it (e.g. `Blob.stream()`, where the backing + /// `ArrayBuffer` must survive multiple `.stream()` calls). + /// + /// Safety vs. correctness: the chunk we enqueue is NOT detached from + /// the producer's perspective, so both producer and consumer see the + /// same underlying bytes. This matches the existing non-isolation + /// behaviour of `Blob.arrayBuffer()` / `Blob.bytes()`, which already + /// return handles that alias the blob's storage. Pending BYOB + /// transfers of reader-provided buffers are unaffected — those are + /// separate buffers and still use spec-mandated transfer. + pub(super) fn readable_byte_stream_controller_enqueue_borrowed>( + ctx: &Ctx<'js>, + objects: ReadableByteStreamObjects<'js, R>, + chunk: ViewBytes<'js>, + ) -> Result> { + Self::readable_byte_stream_controller_enqueue_impl( + ctx, objects, chunk, /*skip_transfer=*/ true, + ) + } + + fn readable_byte_stream_controller_enqueue_impl>( + ctx: &Ctx<'js>, + mut objects: ReadableByteStreamObjects<'js, R>, + chunk: ViewBytes<'js>, + skip_transfer: bool, + ) -> Result> { + // If controller.[[closeRequested]] is true or stream.[[state]] is not "readable", return. + if objects.controller.close_requested + || !matches!(objects.stream.state, ReadableStreamState::Readable) + { + return Ok(objects); + }; + + // Let buffer be chunk.[[ViewedArrayBuffer]]. + // Let byteOffset be chunk.[[ByteOffset]]. + // Let byteLength be chunk.[[ByteLength]]. + let (buffer, byte_length, byte_offset) = chunk.get_array_buffer()?; + + // If ! IsDetachedBuffer(buffer) is true, throw a TypeError exception. + buffer.as_raw().ok_or(Exception::throw_type( + ctx, + "chunk's buffer is detached and so cannot be enqueued", + ))?; + + // Let transferredBuffer be ? TransferArrayBuffer(buffer). + // (When `skip_transfer` is true, the caller guarantees that the + // buffer is already owned exclusively by the stream for the + // purposes of this enqueue — see + // `readable_byte_stream_controller_enqueue_borrowed`.) + let transferred_buffer = if skip_transfer { + buffer + } else { + transfer_array_buffer(buffer)? + }; + + // If controller.[[pendingPullIntos]] is not empty, + // Let firstPendingPullInto be controller.[[pendingPullIntos]][0]. + if !objects.controller.pending_pull_intos.is_empty() { + // If ! IsDetachedBuffer(firstPendingPullInto’s buffer) is true, throw a TypeError exception. + objects.controller.pending_pull_intos[0] + .buffer + .as_raw() + .or_throw_type( + ctx, + "The BYOB request's buffer has been detached and so cannot be filled with an enqueued chunk", + )?; + + // Perform ! ReadableByteStreamControllerInvalidateBYOBRequest(controller). + objects + .controller + .readable_byte_stream_controller_invalidate_byob_request(); + + // Set firstPendingPullInto’s buffer to ! TransferArrayBuffer(firstPendingPullInto’s buffer). + objects.controller.pending_pull_intos[0].buffer = + transfer_array_buffer(objects.controller.pending_pull_intos[0].buffer.clone())?; + + // If firstPendingPullInto’s reader type is "none", perform ? ReadableByteStreamControllerEnqueueDetachedPullIntoToQueue(controller, firstPendingPullInto). + if let PullIntoDescriptorReaderType::None = + objects.controller.pending_pull_intos[0].reader_type + { + objects = Self::readable_byte_stream_enqueue_detached_pull_into_to_queue( + ctx.clone(), + objects, + 0, + )?; + } + } + + objects = objects.with_reader( + // If ! ReadableStreamHasDefaultReader(stream) is true, + |mut objects| { + // Perform ! ReadableByteStreamControllerProcessReadRequestsUsingQueue(controller). + objects = Self::readable_byte_stream_controller_process_read_requests_using_queue( + objects, ctx, + )?; + + // If ! ReadableStreamGetNumReadRequests(stream) is 0, + if ReadableStream::readable_stream_get_num_read_requests(&objects.reader) == 0 { + // Perform ! ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength). + objects + .controller + .readable_byte_stream_controller_enqueue_chunk_to_queue( + transferred_buffer.clone(), + byte_offset, + byte_length, + ) + } else { + // Otherwise, + // If controller.[[pendingPullIntos]] is not empty, + if !objects.controller.pending_pull_intos.is_empty() { + // Perform ! ReadableByteStreamControllerShiftPendingPullInto(controller). + objects + .controller + .readable_byte_stream_controller_shift_pending_pull_into(); + } + + // Let transferredView be ! Construct(%Uint8Array%, « transferredBuffer, byteOffset, byteLength »). + let transferred_view = ViewBytes::from_value( + ctx, + &objects.controller.function_array_buffer_is_view, + Some( + &objects + .controller + .array_constructor_primordials + .constructor_uint8array + .construct(( + transferred_buffer.clone(), + byte_offset, + byte_length, + ))?, + ), + ); + + // Perform ! ReadableStreamFulfillReadRequest(stream, transferredView, false). + objects = ReadableStream::readable_stream_fulfill_read_request( + ctx, + objects, + transferred_view.into_js(ctx)?, + false, + )?; + } + + Ok(objects) + }, + |mut objects| { + // Otherwise, if ! ReadableStreamHasBYOBReader(stream) is true, + // Perform ! ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength). + objects + .controller + .readable_byte_stream_controller_enqueue_chunk_to_queue( + transferred_buffer.clone(), + byte_offset, + byte_length, + ); + // Perform ! ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller). + + Self::readable_byte_stream_controller_process_pull_into_descriptors_using_queue( + ctx, objects, + ) + }, + |mut objects| { + // Otherwise, + // Perform ! ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength). + objects + .controller + .readable_byte_stream_controller_enqueue_chunk_to_queue( + transferred_buffer.clone(), + byte_offset, + byte_length, + ); + + Ok(objects) + }, + )?; + + // Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). + Self::readable_byte_stream_controller_call_pull_if_needed(ctx.clone(), objects) + } + + fn readable_byte_stream_enqueue_detached_pull_into_to_queue>( + ctx: Ctx<'js>, + mut objects: ReadableByteStreamObjects<'js, R>, + pull_into_descriptor_index: usize, + ) -> Result> { + let pull_into_descriptor = + &objects.controller.pending_pull_intos[pull_into_descriptor_index]; + // If pullIntoDescriptor’s bytes filled > 0, perform ? ReadableByteStreamControllerEnqueueClonedChunkToQueue(controller, pullIntoDescriptor’s buffer, pullIntoDescriptor’s byte offset, pullIntoDescriptor’s bytes filled). + if pull_into_descriptor.bytes_filled > 0 { + let buffer = pull_into_descriptor.buffer.clone(); + let byte_offset = pull_into_descriptor.byte_offset; + let bytes_filled = pull_into_descriptor.bytes_filled; + objects = Self::readable_byte_stream_controller_enqueue_cloned_chunk_to_queue( + ctx, + objects, + &buffer, + byte_offset, + bytes_filled, + )?; + } + + // Perform ! ReadableByteStreamControllerShiftPendingPullInto(controller). + objects + .controller + .readable_byte_stream_controller_shift_pending_pull_into(); + + Ok(objects) + } + + fn readable_byte_stream_controller_process_read_requests_using_queue( + mut objects: ReadableStreamDefaultReaderObjects<'js, OwnedBorrowMut<'js, Self>>, + ctx: &Ctx<'js>, + ) -> Result>> { + // While reader.[[readRequests]] is not empty, + while !objects.reader.read_requests.is_empty() { + // If controller.[[queueTotalSize]] is 0, return. + if objects.controller.queue_total_size == 0 { + return Ok(objects); + } + + // Let readRequest be reader.[[readRequests]][0]. + // Remove readRequest from reader.[[readRequests]]. + let read_request = objects.reader.read_requests.pop_front().unwrap(); + // Perform ! ReadableByteStreamControllerFillReadRequestFromQueue(controller, readRequest). + objects = Self::readable_byte_stream_controller_fill_read_request_from_queue( + ctx, + objects, + read_request, + )?; + } + + Ok(objects) + } + + fn readable_byte_stream_controller_shift_pending_pull_into( + &mut self, + ) -> PullIntoDescriptor<'js> { + // Invalidate byobRequest since the first pending pull-into is being removed + self.readable_byte_stream_controller_invalidate_byob_request(); + // Let descriptor be controller.[[pendingPullIntos]][0]. + // Remove descriptor from controller.[[pendingPullIntos]]. + // Return descriptor. + self.pending_pull_intos.pop_front().expect( + "ReadableByteStreamControllerShiftPendingPullInto called on empty pendingPullIntos", + ) + } + + fn readable_byte_stream_controller_enqueue_chunk_to_queue( + &mut self, + buffer: ArrayBuffer<'js>, + byte_offset: usize, + byte_length: usize, + ) { + // Append a new readable byte stream queue entry with buffer buffer, byte offset byteOffset, and byte length byteLength to controller.[[queue]]. + self.queue.push_back(ReadableByteStreamQueueEntry { + buffer, + byte_offset, + byte_length, + }); + + // Set controller.[[queueTotalSize]] to controller.[[queueTotalSize]] + byteLength. + self.queue_total_size += byte_length; + } + + fn readable_byte_stream_controller_process_pull_into_descriptors_using_queue< + R: ReadableStreamReader<'js>, + >( + ctx: &Ctx<'js>, + mut objects: ReadableByteStreamObjects<'js, R>, + ) -> Result> { + // While controller.[[pendingPullIntos]] is not empty, + while !objects.controller.pending_pull_intos.is_empty() { + // If controller.[[queueTotalSize]] is 0, return. + if objects.controller.queue_total_size == 0 { + return Ok(objects); + } + + // Let pullIntoDescriptor be controller.[[pendingPullIntos]][0]. + let mut pull_into_descriptor_ref = PullIntoDescriptorRefMut::Index(0); + + // If ! ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor) is true, + if objects + .controller + .readable_byte_stream_controller_fill_pull_into_descriptor_from_queue( + ctx, + &mut pull_into_descriptor_ref, + )? + { + // Perform ! ReadableByteStreamControllerShiftPendingPullInto(controller). + let pull_into_descriptor = objects + .controller + .readable_byte_stream_controller_shift_pending_pull_into(); + + // Perform ! ReadableByteStreamControllerCommitPullIntoDescriptor(controller.[[stream]], pullIntoDescriptor). + objects = Self::readable_byte_stream_controller_commit_pull_into_descriptor( + ctx.clone(), + objects, + pull_into_descriptor, + )?; + } + } + Ok(objects) + } + + fn readable_byte_stream_controller_enqueue_cloned_chunk_to_queue< + R: ReadableStreamReader<'js>, + >( + ctx: Ctx<'js>, + mut objects: ReadableByteStreamObjects<'js, R>, + buffer: &ArrayBuffer<'js>, + byte_offset: usize, + byte_length: usize, + ) -> Result> { + // Let cloneResult be CloneArrayBuffer(buffer, byteOffset, byteLength, %ArrayBuffer%). + let clone_result = match ArrayBuffer::new_copy( + ctx.clone(), + &buffer.as_bytes().expect( + "ReadableByteStreamControllerEnqueueClonedChunkToQueue called on detached buffer", + )[byte_offset..byte_offset + byte_length], + ) { + Ok(clone_result) => clone_result, + Err(Error::Exception) => { + let err = ctx.catch(); + Self::readable_byte_stream_controller_error(objects, err.clone())?; + return Err(ctx.throw(err)); + } + Err(err) => return Err(err), + }; + + // Perform ! ReadableByteStreamControllerEnqueueChunkToQueue(controller, cloneResult.[[Value]], 0, byteLength). + objects + .controller + .readable_byte_stream_controller_enqueue_chunk_to_queue(clone_result, 0, byte_length); + + Ok(objects) + } + + fn readable_byte_stream_controller_fill_read_request_from_queue( + ctx: &Ctx<'js>, + mut objects: ReadableStreamDefaultReaderObjects<'js, OwnedBorrowMut<'js, Self>>, + read_request: impl ReadableStreamReadRequest<'js>, + ) -> Result>> { + let entry = { + // Assert: controller.[[queueTotalSize]] > 0. + // Let entry be controller.[[queue]][0]. + // Remove entry from controller.[[queue]]. + let entry = objects.controller.queue.pop_front().expect( + "ReadableByteStreamControllerFillReadRequestFromQueue called with empty queue", + ); + + // Set controller.[[queueTotalSize]] to controller.[[queueTotalSize]] − entry’s byte length. + objects.controller.queue_total_size -= entry.byte_length; + + entry + }; + + // Perform ! ReadableByteStreamControllerHandleQueueDrain(controller). + objects = Self::readable_byte_stream_controller_handle_queue_drain(ctx.clone(), objects)?; + + // Let view be ! Construct(%Uint8Array%, « entry’s buffer, entry’s byte offset, entry’s byte length »). + let view: TypedArray = objects + .controller + .array_constructor_primordials + .constructor_uint8array + .construct((entry.buffer, entry.byte_offset, entry.byte_length))?; + + // Perform readRequest’s chunk steps, given view. + read_request.chunk_steps_typed(objects, view.into_value()) + } + + fn readable_byte_stream_controller_fill_pull_into_descriptor_from_queue<'a>( + &'a mut self, + ctx: &Ctx<'js>, + pull_into_descriptor_ref: &mut PullIntoDescriptorRefMut<'js, 'a>, + ) -> Result { + let (mut total_bytes_to_copy_remaining, ready) = { + let pull_into_descriptor = match pull_into_descriptor_ref { + PullIntoDescriptorRefMut::Index(i) => &mut self.pending_pull_intos[*i], + PullIntoDescriptorRefMut::Owned(r) => r, + }; + // Let maxBytesToCopy be min(controller.[[queueTotalSize]], pullIntoDescriptor’s byte length − pullIntoDescriptor’s bytes filled). + let max_bytes_to_copy: usize = std::cmp::min( + self.queue_total_size, + pull_into_descriptor.byte_length - pull_into_descriptor.bytes_filled, + ); + + // Let maxBytesFilled be pullIntoDescriptor’s bytes filled + maxBytesToCopy. + let max_bytes_filled = pull_into_descriptor.bytes_filled + max_bytes_to_copy; + + // Let totalBytesToCopyRemaining be maxBytesToCopy. + let mut total_bytes_to_copy_remaining = max_bytes_to_copy; + + // Let ready be false. + let mut ready = false; + + // Let remainderBytes be the remainder after dividing maxBytesFilled by pullIntoDescriptor’s element size. + let remainder_bytes = max_bytes_filled % pull_into_descriptor.element_size; + + // Let maxAlignedBytes be maxBytesFilled − remainderBytes. + let max_aligned_bytes = max_bytes_filled - remainder_bytes; + + // If maxAlignedBytes ≥ pullIntoDescriptor’s minimum fill, + if max_aligned_bytes >= pull_into_descriptor.minimum_fill { + // Set totalBytesToCopyRemaining to maxAlignedBytes − pullIntoDescriptor’s bytes filled. + total_bytes_to_copy_remaining = + max_aligned_bytes - pull_into_descriptor.bytes_filled; + // Set ready to true. + ready = true + } + + (total_bytes_to_copy_remaining, ready) + }; + + // Let queue be controller.[[queue]]. + // While totalBytesToCopyRemaining > 0, + while total_bytes_to_copy_remaining > 0 { + let bytes_to_copy = { + let pull_into_descriptor = match pull_into_descriptor_ref { + PullIntoDescriptorRefMut::Index(i) => &mut self.pending_pull_intos[*i], + PullIntoDescriptorRefMut::Owned(r) => r, + }; + + // Let headOfQueue be queue[0]. + let head_of_queue = self + .queue + .front_mut() + .expect("empty queue with bytes to copy"); + // Let bytesToCopy be min(totalBytesToCopyRemaining, headOfQueue’s byte length). + let bytes_to_copy: usize = + std::cmp::min(total_bytes_to_copy_remaining, head_of_queue.byte_length); + // Let destStart be pullIntoDescriptor’s byte offset + pullIntoDescriptor’s bytes filled. + let dest_start: usize = + pull_into_descriptor.byte_offset + pull_into_descriptor.bytes_filled; + // Perform ! CopyDataBlockBytes(pullIntoDescriptor’s buffer.[[ArrayBufferData]], destStart, headOfQueue’s buffer.[[ArrayBufferData]], headOfQueue’s byte offset, bytesToCopy). + copy_data_block_bytes( + ctx, + &pull_into_descriptor.buffer, + dest_start, + &head_of_queue.buffer, + head_of_queue.byte_offset, + bytes_to_copy, + )?; + if head_of_queue.byte_length == bytes_to_copy { + // If headOfQueue’s byte length is bytesToCopy, + // Remove queue[0]. + self.queue.pop_front(); + } else { + // Otherwise, + // Set headOfQueue’s byte offset to headOfQueue’s byte offset + bytesToCopy. + head_of_queue.byte_offset += bytes_to_copy; + // Set headOfQueue’s byte length to headOfQueue’s byte length − bytesToCopy. + head_of_queue.byte_length -= bytes_to_copy + } + + // Set controller.[[queueTotalSize]] to controller.[[queueTotalSize]] − bytesToCopy. + self.queue_total_size -= bytes_to_copy; + + bytes_to_copy + }; + + // Perform ! ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesToCopy, pullIntoDescriptor). + self.readable_byte_stream_controller_fill_head_pull_into_descriptor( + bytes_to_copy, + pull_into_descriptor_ref, + ); + + // Set totalBytesToCopyRemaining to totalBytesToCopyRemaining − bytesToCopy. + total_bytes_to_copy_remaining -= bytes_to_copy + } + + Ok(ready) + } + + fn readable_byte_stream_controller_commit_pull_into_descriptor>( + ctx: Ctx<'js>, + objects: ReadableByteStreamObjects<'js, R>, + pull_into_descriptor: PullIntoDescriptor<'js>, + ) -> Result> { + // Let done be false. + let mut done = false; + // If stream.[[state]] is "closed", + if matches!(objects.stream.state, ReadableStreamState::Closed) { + // Set done to true. + done = true + } + + let reader_type = pull_into_descriptor.reader_type; + + // Let filledView be ! ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor). + let filled_view = Self::readable_byte_stream_controller_convert_pull_into_descriptor( + ctx.clone(), + &objects.stream.function_array_buffer_is_view, + pull_into_descriptor, + )?; + + if let PullIntoDescriptorReaderType::Default = reader_type { + // If pullIntoDescriptor’s reader type is "default", + objects.with_assert_default_reader(|objects| { + // Perform ! ReadableStreamFulfillReadRequest(stream, filledView, done). + ReadableStream::readable_stream_fulfill_read_request( + &ctx, + objects, + filled_view.into_js(&ctx)?, + done, + ) + }) + } else { + // Otherwise, + objects.with_assert_byob_reader(|objects| { + // Perform ! ReadableStreamFulfillReadIntoRequest(stream, filledView, done). + ReadableStream::readable_stream_fulfill_read_into_request( + &ctx, + objects, + filled_view, + done, + ) + }) + } + } + + fn readable_byte_stream_controller_handle_queue_drain>( + ctx: Ctx<'js>, + mut objects: ReadableByteStreamObjects<'js, R>, + ) -> Result> { + // If controller.[[queueTotalSize]] is 0 and controller.[[closeRequested]] is true, + if objects.controller.queue_total_size == 0 && objects.controller.close_requested { + // Perform ! ReadableByteStreamControllerClearAlgorithms(controller). + objects + .controller + .readable_byte_stream_controller_clear_algorithms(); + // Perform ! ReadableStreamClose(controller.[[stream]]). + ReadableStream::readable_stream_close(ctx, objects) + } else { + // Otherwise, + // Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). + Self::readable_byte_stream_controller_call_pull_if_needed(ctx.clone(), objects) + } + } + + fn readable_byte_stream_controller_convert_pull_into_descriptor( + ctx: Ctx<'js>, + function_array_buffer_is_view: &Function<'js>, + pull_into_descriptor: PullIntoDescriptor<'js>, + ) -> Result> { + let PullIntoDescriptor { + // Let bytesFilled be pullIntoDescriptor’s bytes filled. + bytes_filled, + // Let elementSize be pullIntoDescriptor’s element size. + element_size, + byte_offset, + buffer, + .. + } = pull_into_descriptor; + // Let buffer be ! TransferArrayBuffer(pullIntoDescriptor’s buffer). + let buffer = transfer_array_buffer(buffer); + // Return ! Construct(pullIntoDescriptor’s view constructor, « buffer, pullIntoDescriptor’s byte offset, bytesFilled ÷ elementSize »). + let view: Object = pull_into_descriptor.view_constructor.construct(( + buffer, + byte_offset, + bytes_filled / element_size, + ))?; + ViewBytes::from_object(&ctx, function_array_buffer_is_view, &view) + } + + pub(super) fn readable_byte_stream_controller_pull_into( + ctx: &Ctx<'js>, + // Let stream be controller.[[stream]]. + mut objects: ReadableStreamBYOBObjects<'js>, + view: ViewBytes<'js>, + min: u64, + read_into_request: impl ReadableStreamReadIntoRequest<'js> + 'js, + ) -> Result> { + // Set elementSize to the element size specified in the typed array constructors table for view.[[TypedArrayName]]. + // Set ctor to the constructor specified in the typed array constructors table for view.[[TypedArrayName]]. + let (element_size, ctor) = ( + view.element_size(), + objects + .controller + .array_constructor_primordials + .for_view_bytes(&view), + ); + + // Let minimumFill be min × elementSize. + let minimum_fill: usize = (min as usize) * element_size; + + // Let byteOffset be view.[[ByteOffset]]. + // Let byteLength be view.[[ByteLength]]. + let (buffer, byte_length, byte_offset) = view.get_array_buffer()?; + + // Let bufferResult be TransferArrayBuffer(view.[[ViewedArrayBuffer]]). + let buffer_result = transfer_array_buffer(buffer); + let buffer = match buffer_result { + // If bufferResult is an abrupt completion, + Err(Error::Exception) => { + // Perform readIntoRequest’s error steps, given bufferResult.[[Value]]. + objects = read_into_request.error_steps(objects, ctx.catch())?; + // Return. + return Ok(objects); + } + Err(err) => return Err(err), + // Let buffer be bufferResult.[[Value]]. + Ok(buffer) => buffer, + }; + + let buffer_byte_length = buffer.len(); + // Let pullIntoDescriptor be a new pull-into descriptor with + let mut pull_into_descriptor = PullIntoDescriptor { + buffer, + buffer_byte_length, + byte_offset, + byte_length, + bytes_filled: 0, + minimum_fill, + element_size, + view_constructor: ctor.clone(), + reader_type: PullIntoDescriptorReaderType::Byob, + }; + + // If controller.[[pendingPullIntos]] is not empty, + if !objects.controller.pending_pull_intos.is_empty() { + // Append pullIntoDescriptor to controller.[[pendingPullIntos]]. + objects + .controller + .pending_pull_intos + .push_back(pull_into_descriptor); + + // Perform ! ReadableStreamAddReadIntoRequest(stream, readIntoRequest). + ReadableStream::readable_stream_add_read_into_request( + &mut objects.reader, + read_into_request, + ); + + // Return. + return Ok(objects); + } + + // If stream.[[state]] is "closed", + if matches!(objects.stream.state, ReadableStreamState::Closed) { + // Let emptyView be ! Construct(ctor, « pullIntoDescriptor’s buffer, pullIntoDescriptor’s byte offset, 0 »). + let empty_view: Value<'js> = ctor.construct(( + pull_into_descriptor.buffer, + pull_into_descriptor.byte_offset, + 0, + ))?; + + // Perform readIntoRequest’s close steps, given emptyView. + objects = read_into_request.close_steps(objects, empty_view)?; + + // Return. + return Ok(objects); + } + + // If controller.[[queueTotalSize]] > 0, + if objects.controller.queue_total_size > 0 { + // If ! ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor) is true, + if objects + .controller + .readable_byte_stream_controller_fill_pull_into_descriptor_from_queue( + ctx, + &mut PullIntoDescriptorRefMut::Owned(&mut pull_into_descriptor), + )? + { + // Let filledView be ! ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor). + let filled_view = objects + .controller + .readable_byte_steam_controller_convert_pull_into_descriptor( + pull_into_descriptor, + )?; + + // Perform ! ReadableByteStreamControllerHandleQueueDrain(controller). + objects = + Self::readable_byte_stream_controller_handle_queue_drain(ctx.clone(), objects)?; + + // Perform readIntoRequest’s chunk steps, given filledView. + // Return. + return read_into_request.chunk_steps(objects, filled_view); + } + + // If controller.[[closeRequested]] is true, + if objects.controller.close_requested { + // Let e be a TypeError exception. + let e: Value = objects + .stream + .constructor_type_error + .call(("Insufficient bytes to fill elements in the given buffer",))?; + + // Perform ! ReadableByteStreamControllerError(controller, e). + objects = Self::readable_byte_stream_controller_error(objects, e.clone())?; + + // Perform readIntoRequest’s error steps, given e. + // Return. + return read_into_request.error_steps(objects, e); + } + } + + // Append pullIntoDescriptor to controller.[[pendingPullIntos]]. + objects + .controller + .pending_pull_intos + .push_back(pull_into_descriptor); + + // Perform ! ReadableStreamAddReadIntoRequest(stream, readIntoRequest). + ReadableStream::readable_stream_add_read_into_request( + &mut objects.reader, + read_into_request, + ); + + // Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). + Self::readable_byte_stream_controller_call_pull_if_needed(ctx.clone(), objects) + } + + fn readable_byte_steam_controller_convert_pull_into_descriptor( + &mut self, + pull_into_descriptor: PullIntoDescriptor<'js>, + ) -> Result> { + // Let bytesFilled be pullIntoDescriptor’s bytes filled. + let bytes_filled = pull_into_descriptor.bytes_filled; + + // Let elementSize be pullIntoDescriptor’s element size. + let element_size = pull_into_descriptor.element_size; + + // Let buffer be ! TransferArrayBuffer(pullIntoDescriptor’s buffer). + let buffer = transfer_array_buffer(pull_into_descriptor.buffer)?; + + // Return ! Construct(pullIntoDescriptor’s view constructor, « buffer, pullIntoDescriptor’s byte offset, bytesFilled ÷ elementSize »). + pull_into_descriptor.view_constructor.construct(( + buffer, + pull_into_descriptor.byte_offset, + bytes_filled / element_size, + )) + } + + pub(super) fn readable_byte_stream_controller_respond>( + ctx: Ctx<'js>, + mut objects: ReadableByteStreamObjects<'js, R>, + bytes_written: usize, + ) -> Result<()> { + // Let firstDescriptor be controller.[[pendingPullIntos]][0]. + let first_descriptor = &mut objects.controller.pending_pull_intos[0]; + + // Let state be controller.[[stream]].[[state]]. + match objects.stream.state { + // If state is "closed", + ReadableStreamState::Closed => { + // If bytesWritten is not 0, throw a TypeError exception. + if bytes_written != 0 { + return Err(Exception::throw_type( + &ctx, + "bytesWritten must be 0 when calling respond() on a closed stream", + )); + } + } + // Otherwise, + _ => { + // If bytesWritten is 0, throw a TypeError exception. + if bytes_written == 0 { + return Err(Exception::throw_type( + &ctx, + "bytesWritten must be greater than 0 when calling respond() on a readable stream", + )); + } + + // If firstDescriptor’s bytes filled + bytesWritten > firstDescriptor’s byte length, throw a RangeError exception. + if first_descriptor.bytes_filled + bytes_written > first_descriptor.byte_length { + return Err(Exception::throw_range(&ctx, "bytesWritten out of range'")); + } + } + }; + + // Set firstDescriptor’s buffer to ! TransferArrayBuffer(firstDescriptor’s buffer). + first_descriptor.buffer = transfer_array_buffer(first_descriptor.buffer.clone())?; + + // Perform ? ReadableByteStreamControllerRespondInternal(controller, bytesWritten). + Self::readable_byte_stream_controller_respond_internal(ctx, objects, bytes_written) + } + + fn readable_byte_stream_controller_respond_internal>( + ctx: Ctx<'js>, + mut objects: ReadableByteStreamObjects<'js, R>, + bytes_written: usize, + ) -> Result<()> { + // Let firstDescriptor be controller.[[pendingPullIntos]][0]. + let first_descriptor_index = 0; + + // Perform ! ReadableByteStreamControllerInvalidateBYOBRequest(controller). + objects + .controller + .readable_byte_stream_controller_invalidate_byob_request(); + + // Let state be controller.[[stream]].[[state]]. + match objects.stream.state { + // If state is "closed", + ReadableStreamState::Closed => { + // Perform ! ReadableByteStreamControllerRespondInClosedState(controller, firstDescriptor). + objects = Self::readable_byte_stream_controller_respond_in_closed_state( + ctx.clone(), + objects, + first_descriptor_index, + )?; + } + // Otherwise + _ => { + // Perform ? ReadableByteStreamControllerRespondInReadableState(controller, bytesWritten, firstDescriptor). + objects = Self::readable_byte_stream_controller_respond_in_readable_state( + ctx.clone(), + objects, + bytes_written, + first_descriptor_index, + )? + } + }; + + _ = Self::readable_byte_stream_controller_call_pull_if_needed(ctx, objects)?; + Ok(()) + } + + fn readable_byte_stream_controller_respond_in_closed_state>( + ctx: Ctx<'js>, + // Let stream be controller.[[stream]]. + mut objects: ReadableByteStreamObjects<'js, R>, + first_descriptor_index: usize, + ) -> Result> { + // If firstDescriptor’s reader type is "none", perform ! ReadableByteStreamControllerShiftPendingPullInto(controller). + if let PullIntoDescriptorReaderType::None = + objects.controller.pending_pull_intos[first_descriptor_index].reader_type + { + objects + .controller + .readable_byte_stream_controller_shift_pending_pull_into(); + } + + // If ! ReadableStreamHasBYOBReader(stream) is true, + objects.with_reader( + Ok, + |mut objects| { + // While ! ReadableStreamGetNumReadIntoRequests(stream) > 0, + while ReadableStream::readable_stream_get_num_read_into_requests(&objects.reader) + > 0 + { + // Let pullIntoDescriptor be ! ReadableByteStreamControllerShiftPendingPullInto(controller). + let pull_into_descriptor = objects + .controller + .readable_byte_stream_controller_shift_pending_pull_into(); + + // Perform ! ReadableByteStreamControllerCommitPullIntoDescriptor(stream, pullIntoDescriptor). + objects = Self::readable_byte_stream_controller_commit_pull_into_descriptor( + ctx.clone(), + objects, + pull_into_descriptor, + )?; + } + + Ok(objects) + }, + Ok, + ) + } + + fn readable_byte_stream_controller_respond_in_readable_state>( + ctx: Ctx<'js>, + // Let stream be controller.[[stream]]. + mut objects: ReadableByteStreamObjects<'js, R>, + bytes_written: usize, + pull_into_descriptor_index: usize, + ) -> Result> { + // Perform ! ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesWritten, pullIntoDescriptor). + objects + .controller + .readable_byte_stream_controller_fill_head_pull_into_descriptor( + bytes_written, + &mut PullIntoDescriptorRefMut::Index(pull_into_descriptor_index), + ); + + // If pullIntoDescriptor’s reader type is "none", + if let PullIntoDescriptorReaderType::None = + objects.controller.pending_pull_intos[pull_into_descriptor_index].reader_type + { + // Perform ? ReadableByteStreamControllerEnqueueDetachedPullIntoToQueue(controller, pullIntoDescriptor). + objects = Self::readable_byte_stream_enqueue_detached_pull_into_to_queue( + ctx.clone(), + objects, + pull_into_descriptor_index, + )?; + // Perform ! ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller). + // Return. + return Self::readable_byte_stream_controller_process_pull_into_descriptors_using_queue( + &ctx, objects, + ); + } + + // If pullIntoDescriptor’s bytes filled < pullIntoDescriptor’s minimum fill, return. + if objects.controller.pending_pull_intos[pull_into_descriptor_index].bytes_filled + < objects.controller.pending_pull_intos[pull_into_descriptor_index].minimum_fill + { + return Ok(objects); + } + + // Perform ! ReadableByteStreamControllerShiftPendingPullInto(controller). + let mut pull_into_descriptor = objects + .controller + .readable_byte_stream_controller_shift_pending_pull_into(); + + // Let remainderSize be the remainder after dividing pullIntoDescriptor’s bytes filled by pullIntoDescriptor’s element size. + let remainder_size = pull_into_descriptor.bytes_filled % pull_into_descriptor.element_size; + + // If remainderSize > 0, + if remainder_size > 0 { + // Let end be pullIntoDescriptor’s byte offset + pullIntoDescriptor’s bytes filled. + let end = pull_into_descriptor.byte_offset + pull_into_descriptor.bytes_filled; + + let buffer = pull_into_descriptor.buffer.clone(); + + // Perform ? ReadableByteStreamControllerEnqueueClonedChunkToQueue(controller, pullIntoDescriptor’s buffer, end − remainderSize, remainderSize). + objects = Self::readable_byte_stream_controller_enqueue_cloned_chunk_to_queue( + ctx.clone(), + objects, + &buffer, + end - remainder_size, + remainder_size, + )?; + } + + // Set pullIntoDescriptor’s bytes filled to pullIntoDescriptor’s bytes filled − remainderSize. + pull_into_descriptor.bytes_filled -= remainder_size; + + // Perform ! ReadableByteStreamControllerCommitPullIntoDescriptor(controller.[[stream]], pullIntoDescriptor). + objects = Self::readable_byte_stream_controller_commit_pull_into_descriptor( + ctx.clone(), + objects, + pull_into_descriptor, + )?; + + // Perform ! ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller). + Self::readable_byte_stream_controller_process_pull_into_descriptors_using_queue( + &ctx, objects, + ) + } + + pub(super) fn readable_byte_stream_controller_respond_with_new_view< + R: ReadableStreamReader<'js>, + >( + ctx: Ctx<'js>, + mut objects: ReadableByteStreamObjects<'js, R>, + view: ViewBytes<'js>, + ) -> Result<()> { + // Let firstDescriptor be controller.[[pendingPullIntos]][0]. + let first_descriptor_index = 0; + + let (buffer, byte_length, byte_offset) = view.get_array_buffer()?; + + // Let state be controller.[[stream]].[[state]]. + match objects.stream.state { + // If state is "closed", + ReadableStreamState::Closed => { + // If view.[[ByteLength]] is not 0, throw a TypeError exception. + if byte_length != 0 { + return Err(Exception::throw_type(&ctx, "The view's length must be 0 when calling respondWithNewView() on a closed stream")); + } + } + // Otherwise + _ => { + // If view.[[ByteLength]] is 0, throw a TypeError exception. + if byte_length == 0 { + return Err(Exception::throw_type(&ctx, "The view's length must be greater than 0 when calling respondWithNewView() on a readable stream")); + } + } + }; + + { + let first_descriptor = + &mut objects.controller.pending_pull_intos[first_descriptor_index]; + + // If firstDescriptor’s byte offset + firstDescriptor’ bytes filled is not view.[[ByteOffset]], throw a RangeError exception. + if first_descriptor.byte_offset + first_descriptor.bytes_filled != byte_offset { + return Err(Exception::throw_range( + &ctx, + "The region specified by view does not match byobRequest", + )); + }; + + // If firstDescriptor’s buffer byte length is not view.[[ViewedArrayBuffer]].[[ByteLength]], throw a RangeError exception. + if first_descriptor.buffer_byte_length != buffer.len() { + return Err(Exception::throw_range( + &ctx, + "The buffer of view has different capacity than byobRequest", + )); + }; + + // If firstDescriptor’s bytes filled + view.[[ByteLength]] > firstDescriptor’s byte length, throw a RangeError exception. + if first_descriptor.bytes_filled + byte_length > first_descriptor.byte_length { + return Err(Exception::throw_range( + &ctx, + "The region specified by view is larger than byobRequest", + )); + } + + // Set firstDescriptor’s buffer to ? TransferArrayBuffer(view.[[ViewedArrayBuffer]]). + first_descriptor.buffer = transfer_array_buffer(buffer)?; + } + + // Perform ? ReadableByteStreamControllerRespondInternal(controller, viewByteLength). + Self::readable_byte_stream_controller_respond_internal(ctx, objects, byte_length) + } + + fn readable_byte_stream_controller_fill_head_pull_into_descriptor<'a>( + &mut self, + size: usize, + pull_into_descriptor_ref: &mut PullIntoDescriptorRefMut<'js, 'a>, + ) { + let pull_into_descriptor = match pull_into_descriptor_ref { + PullIntoDescriptorRefMut::Index(i) => &mut self.pending_pull_intos[*i], + PullIntoDescriptorRefMut::Owned(r) => *r, + }; + + // Set pullIntoDescriptor’s bytes filled to bytes filled + size. + pull_into_descriptor.bytes_filled += size; + } + + fn start_algorithm>( + ctx: Ctx<'js>, + objects: ReadableByteStreamObjects<'js, R>, + start_algorithm: StartAlgorithm<'js>, + ) -> Result<( + Value<'js>, + ReadableStreamClassObjects<'js, OwnedBorrowMut<'js, Self>, R>, + )> { + let objects_class = objects.into_inner(); + + Ok(( + start_algorithm.call( + ctx, + ReadableStreamControllerClass::ReadableStreamByteController( + objects_class.controller.clone(), + ), + )?, + objects_class, + )) + } + + fn pull_algorithm>( + ctx: Ctx<'js>, + objects: ReadableByteStreamObjects<'js, R>, + ) -> Result<( + Promise<'js>, + ReadableStreamClassObjects<'js, OwnedBorrowMut<'js, Self>, R>, + )> { + let pull_algorithm = objects + .controller + .pull_algorithm + .clone() + .expect("pull algorithm used after ReadableStreamDefaultControllerClearAlgorithms"); + let promise_primordials = objects.stream.promise_primordials.clone(); + let objects_class = objects.into_inner(); + + Ok(( + pull_algorithm.call( + ctx, + &promise_primordials, + ReadableStreamControllerClass::ReadableStreamByteController( + objects_class.controller.clone(), + ), + )?, + objects_class, + )) + } + + fn cancel_algorithm>( + ctx: Ctx<'js>, + objects: ReadableByteStreamObjects<'js, R>, + reason: Value<'js>, + ) -> Result<( + Promise<'js>, + ReadableStreamClassObjects<'js, OwnedBorrowMut<'js, Self>, R>, + )> { + let cancel_algorithm = + objects.controller.cancel_algorithm.clone().expect( + "cancel algorithm used after ReadableStreamDefaultControllerClearAlgorithms", + ); + let promise_primordials = objects.stream.promise_primordials.clone(); + let objects_class = objects.into_inner(); + + Ok(( + cancel_algorithm.call(ctx, &promise_primordials, reason)?, + objects_class, + )) + } +} + +#[methods(rename_all = "camelCase")] +impl<'js> ReadableByteStreamController<'js> { + #[qjs(constructor)] + fn new(ctx: Ctx<'js>) -> Result> { + Err(Exception::throw_type(&ctx, "Illegal constructor")) + } + + // readonly attribute ReadableStreamBYOBRequest? byobRequest; + #[qjs(get, rename = "byobRequest")] + fn byob_request_getter( + ctx: Ctx<'js>, + controller: This>, + ) -> Result>>> { + // Use `try_borrow_mut` so that reentrant access during enqueue + // (e.g. via a patched `Object.prototype.then` getter, WPT + // `readable-byte-streams/patched-global`) doesn't hard-error with + // "can't borrow" when the outer enqueue already holds the mut + // borrow. If the controller IS currently borrowed, we can still + // answer correctly by reading the state via an immutable try_borrow; + // materialization is only needed when state is consistent. + if let Ok(owned) = rquickjs::class::OwnedBorrowMut::try_from_class(controller.0.clone()) { + let (request, _) = Self::readable_byte_stream_controller_get_byob_request(ctx, owned)?; + return Ok(request); + } + // Reentrant access mid-enqueue: can't acquire immutable borrow + // either (because enqueue holds mut). Return null — the spec's + // observable state during this transient window is that the + // byob request has been invalidated (the enqueue path clears it + // as pull-into descriptors are filled). + Ok(Null(None)) + } + + // readonly attribute unrestricted double? desiredSize; + #[qjs(get)] + fn desired_size(&self) -> Null { + let stream = OwnedBorrow::from_class(self.stream.clone()); + self.readable_byte_stream_controller_get_desired_size(&stream) + } + + // undefined close(); + fn close(ctx: Ctx<'js>, controller: This>) -> Result<()> { + // If this.[[closeRequested]] is true, throw a TypeError exception. + if controller.close_requested { + return Err(Exception::throw_type(&ctx, "close() called more than once")); + } + + let objects = ReadableStreamObjects::from_byte_controller(controller.0).refresh_reader(); + + if !matches!(objects.stream.state, ReadableStreamState::Readable) { + return Err(Exception::throw_type( + &ctx, + "close() called when stream is not readable", + )); + }; + + // Perform ? ReadableByteStreamControllerClose(this). + Self::readable_byte_stream_controller_close(ctx, objects)?; + Ok(()) + } + + // undefined enqueue(ArrayBufferView chunk); + fn enqueue( + this: This>, + ctx: Ctx<'js>, + chunk: Value<'js>, + ) -> Result<()> { + let chunk = ViewBytes::from_value(&ctx, &this.function_array_buffer_is_view, Some(&chunk))?; + + let (array_buffer, byte_length, _) = chunk.get_array_buffer()?; + + // If chunk.[[ByteLength]] is 0, throw a TypeError exception. + if byte_length == 0 { + return Err(Exception::throw_type( + &ctx, + "chunk must have non-zero byteLength", + )); + } + + // If chunk.[[ViewedArrayBuffer]].[[ArrayBufferByteLength]] is 0, throw a TypeError exception. + if array_buffer.is_empty() { + return Err(Exception::throw_type( + &ctx, + "chunk must have non-zero buffer byteLength", + )); + } + + // If this.[[closeRequested]] is true, throw a TypeError exception. + if this.close_requested { + return Err(Exception::throw_type(&ctx, "stream is closed or draining")); + } + + let objects = ReadableStreamObjects::from_byte_controller(this.0).refresh_reader(); + + // If this.[[stream]].[[state]] is not "readable", throw a TypeError exception. + if !matches!(objects.stream.state, ReadableStreamState::Readable) { + return Err(Exception::throw_type( + &ctx, + "The stream is not in the readable state and cannot be enqueued to", + )); + }; + + // Return ? ReadableByteStreamControllerEnqueue(this, chunk). + Self::readable_byte_stream_controller_enqueue(&ctx, objects, chunk)?; + Ok(()) + } + + // undefined error(optional any e); + fn error( + ctx: Ctx<'js>, + controller: This>, + e: Opt>, + ) -> Result<()> { + let objects = ReadableStreamObjects::from_byte_controller(controller.0).refresh_reader(); + + // Perform ! ReadableByteStreamControllerError(this, e). + Self::readable_byte_stream_controller_error(objects, e.0.unwrap_or_undefined(&ctx))?; + Ok(()) + } +} + +impl<'js> ReadableStreamController<'js> for ReadableByteStreamControllerOwned<'js> { + type Class = ReadableByteStreamControllerClass<'js>; + + fn with_controller( + self, + ctx: C, + _: impl FnOnce( + C, + ReadableStreamDefaultControllerOwned<'js>, + ) -> Result<(O, ReadableStreamDefaultControllerOwned<'js>)>, + byte: impl FnOnce( + C, + ReadableByteStreamControllerOwned<'js>, + ) -> Result<(O, ReadableByteStreamControllerOwned<'js>)>, + ) -> Result<(O, Self)> { + let (ctx, reader) = byte(ctx, self)?; + Ok((ctx, reader)) + } + + fn into_inner(self) -> Self::Class { + OwnedBorrowMut::into_inner(self) + } + + fn from_class(class: Self::Class) -> Self { + OwnedBorrowMut::from_class(class) + } + + fn into_erased(self) -> ReadableStreamControllerOwned<'js> { + ReadableStreamControllerOwned::ReadableStreamByteController(self) + } + + fn try_from_erased(erased: ReadableStreamControllerOwned<'js>) -> Option { + match erased { + ReadableStreamControllerOwned::ReadableStreamDefaultController(_) => None, + ReadableStreamControllerOwned::ReadableStreamByteController(r) => Some(r), + } + } + + fn pull_steps( + ctx: &Ctx<'js>, + mut objects: ReadableStreamDefaultReaderObjects<'js, Self>, + read_request: impl ReadableStreamReadRequest<'js> + 'js, + ) -> Result> { + // If this.[[queueTotalSize]] > 0, + if objects.controller.queue_total_size > 0 { + // Perform ! ReadableByteStreamControllerFillReadRequestFromQueue(this, readRequest). + // Return. + return ReadableByteStreamController::readable_byte_stream_controller_fill_read_request_from_queue( + ctx, + objects, + read_request, + ); + } + + // Let autoAllocateChunkSize be this.[[autoAllocateChunkSize]]. + let auto_allocate_chunk_size = objects.controller.auto_allocate_chunk_size; + + // If autoAllocateChunkSize is not undefined, + if let Some(auto_allocate_chunk_size) = auto_allocate_chunk_size { + // Let buffer be Construct(%ArrayBuffer%, « autoAllocateChunkSize »). + let buffer: ArrayBuffer = match objects + .controller + .constructor_array_buffer + .construct((auto_allocate_chunk_size,)) + { + // If buffer is an abrupt completion, + Err(Error::Exception) => { + // Perform readRequest’s error steps, given buffer.[[Value]]. + return read_request.error_steps_typed(objects, ctx.catch()); + } + Err(err) => return Err(err), + Ok(buffer) => buffer, + }; + + // Let pullIntoDescriptor be a new pull-into descriptor with... + let pull_into_descriptor = PullIntoDescriptor { + buffer, + buffer_byte_length: auto_allocate_chunk_size, + byte_offset: 0, + byte_length: auto_allocate_chunk_size, + bytes_filled: 0, + minimum_fill: 1, + element_size: 1, + view_constructor: objects + .controller + .array_constructor_primordials + .constructor_uint8array + .clone(), + reader_type: PullIntoDescriptorReaderType::Default, + }; + + // Append pullIntoDescriptor to this.[[pendingPullIntos]]. + objects + .controller + .pending_pull_intos + .push_back(pull_into_descriptor); + } + + // Perform ! ReadableStreamAddReadRequest(stream, readRequest). + objects + .stream + .readable_stream_add_read_request(&mut objects.reader, read_request); + + // Perform ! ReadableByteStreamControllerCallPullIfNeeded(this). + ReadableByteStreamController::readable_byte_stream_controller_call_pull_if_needed( + ctx.clone(), + objects, + ) + } + + fn cancel_steps>( + ctx: &Ctx<'js>, + mut objects: ReadableStreamObjects<'js, Self, R>, + reason: Value<'js>, + ) -> Result<(Promise<'js>, ReadableStreamObjects<'js, Self, R>)> { + // Perform ! ReadableByteStreamControllerClearPendingPullIntos(this). + objects + .controller + .readable_byte_stream_controller_clear_pending_pull_intos(); + + // Perform ! ResetQueue(this). + objects.controller.reset_queue(); + + // Let result be the result of performing this.[[cancelAlgorithm]], passing in reason. + let (result, objects_class) = + ReadableByteStreamController::cancel_algorithm(ctx.clone(), objects, reason)?; + + objects = ReadableStreamObjects::from_class(objects_class); + + // Perform ! ReadableByteStreamControllerClearAlgorithms(this). + objects + .controller + .readable_byte_stream_controller_clear_algorithms(); + + // Return result. + Ok((result, objects)) + } + + fn release_steps(&mut self) { + // If this.[[pendingPullIntos]] is not empty, + if !self.pending_pull_intos.is_empty() { + // Let firstPendingPullInto be this.[[pendingPullIntos]][0]. + let first_pending_pull_into = &mut self.pending_pull_intos[0]; + + // Set firstPendingPullInto’s reader type to "none". + first_pending_pull_into.reader_type = PullIntoDescriptorReaderType::None; + + // Set this.[[pendingPullIntos]] to the list « firstPendingPullInto ». + _ = self.pending_pull_intos.split_off(1); + } + } +} + +#[derive(JsLifetime, Trace, Clone)] +#[rquickjs::class] +pub(crate) struct ReadableStreamBYOBRequest<'js> { + pub(super) view: Option>, + controller: Option>, +} + +#[methods(rename_all = "camelCase")] +impl<'js> ReadableStreamBYOBRequest<'js> { + #[qjs(constructor)] + fn new(ctx: Ctx<'js>) -> Result> { + Err(Exception::throw_type(&ctx, "Illegal constructor")) + } + + #[qjs(get)] + fn view(&self) -> Null> { + Null(self.view.clone()) + } + + fn respond( + ctx: Ctx<'js>, + byob_request: This>, + bytes_written: usize, + ) -> Result<()> { + // If this.[[controller]] is undefined, throw a TypeError exception. + let (controller, view) = match (&byob_request.controller, &byob_request.view) { + (Some(controller), Some(view)) => (controller.clone(), view), + _ => { + return Err(Exception::throw_type( + &ctx, + "This BYOB request has been invalidated", + )); + } + }; + let (buffer, _, _) = view.get_array_buffer()?; + drop(byob_request); + + // If ! IsDetachedBuffer(this.[[view]].[[ArrayBuffer]]) is true, throw a TypeError exception. + if buffer.as_bytes().is_none() { + return Err(Exception::throw_type( + &ctx, + "The BYOB request's buffer has been detached and so cannot be used as a response", + )); + } + + let objects = + ReadableStreamObjects::from_byte_controller(OwnedBorrowMut::from_class(controller)) + .refresh_reader(); + + // Perform ? ReadableByteStreamControllerRespond(this.[[controller]], bytesWritten). + ReadableByteStreamController::readable_byte_stream_controller_respond( + ctx, + objects, + bytes_written, + ) + } + + fn respond_with_new_view( + ctx: Ctx<'js>, + byob_request: This>, + view: Opt>, + ) -> Result<()> { + // If this.[[controller]] is undefined, throw a TypeError exception. + let controller = match &byob_request.controller { + Some(controller) => controller.clone(), + _ => { + return Err(Exception::throw_type( + &ctx, + "This BYOB request has been invalidated", + )); + } + }; + drop(byob_request); + + let controller = OwnedBorrowMut::from_class(controller); + + let view = ViewBytes::from_value( + &ctx, + &controller.function_array_buffer_is_view, + view.0.as_ref(), + )?; + + let (buffer, _, _) = view.get_array_buffer()?; + + // If ! IsDetachedBuffer(view.[[ViewedArrayBuffer]]) is true, throw a TypeError exception. + if buffer.as_bytes().is_none() { + return Err(Exception::throw_type( + &ctx, + "The given view's buffer has been detached and so cannot be used as a response", + )); + } + + let objects = ReadableStreamObjects::from_byte_controller(controller).refresh_reader(); + + // Return ? ReadableByteStreamControllerRespondWithNewView(this.[[controller]], view). + ReadableByteStreamController::readable_byte_stream_controller_respond_with_new_view( + ctx, objects, view, + ) + } +} + +#[derive(JsLifetime)] +pub(super) struct PullIntoDescriptor<'js> { + buffer: ArrayBuffer<'js>, + buffer_byte_length: usize, + byte_offset: usize, + byte_length: usize, + bytes_filled: usize, + minimum_fill: usize, + element_size: usize, + view_constructor: Constructor<'js>, + reader_type: PullIntoDescriptorReaderType, +} + +impl<'js> Trace<'js> for PullIntoDescriptor<'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + self.buffer.trace(tracer); + self.buffer_byte_length.trace(tracer); + self.byte_offset.trace(tracer); + self.byte_length.trace(tracer); + self.bytes_filled.trace(tracer); + self.minimum_fill.trace(tracer); + self.element_size.trace(tracer); + self.view_constructor.trace(tracer); + self.reader_type.trace(tracer); + } +} + +enum PullIntoDescriptorRefMut<'js, 'a> { + Index(usize), + Owned(&'a mut PullIntoDescriptor<'js>), +} + +#[derive(Trace, Clone, Copy)] +enum PullIntoDescriptorReaderType { + Default, + Byob, + None, +} + +#[derive(JsLifetime)] +struct ReadableByteStreamQueueEntry<'js> { + buffer: ArrayBuffer<'js>, + byte_offset: usize, + byte_length: usize, +} + +impl<'js> Trace<'js> for ReadableByteStreamQueueEntry<'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + self.buffer.trace(tracer); + self.byte_offset.trace(tracer); + self.byte_length.trace(tracer) + } +} + +fn transfer_array_buffer(buffer: ArrayBuffer<'_>) -> Result> { + buffer.get::<_, Function>("transfer")?.call((This(buffer),)) +} + +fn copy_data_block_bytes( + ctx: &Ctx<'_>, + to_block: &ArrayBuffer, + to_index: usize, + from_block: &ArrayBuffer, + from_index: usize, + count: usize, +) -> Result<()> { + let to_raw = to_block + .as_raw() + .ok_or(ERROR_MSG_ARRAY_BUFFER_DETACHED) + .or_throw(ctx)?; + let to_slice = unsafe { std::slice::from_raw_parts_mut(to_raw.ptr.as_ptr(), to_raw.len) }; + let from_raw = from_block + .as_raw() + .ok_or(ERROR_MSG_ARRAY_BUFFER_DETACHED) + .or_throw(ctx)?; + let from_slice = unsafe { std::slice::from_raw_parts(from_raw.ptr.as_ptr(), from_raw.len) }; + + to_slice[to_index..to_index + count] + .copy_from_slice(&from_slice[from_index..from_index + count]); + Ok(()) +} + +/// Public API for enqueuing a `Uint8Array` (built from the caller-supplied +/// `ArrayBuffer`) into a byte stream controller from Rust code. Used by +/// byte-source streams created via `ReadableStream::from_byte_pull_algorithm`. +pub fn readable_byte_stream_controller_enqueue_bytes<'js>( + ctx: Ctx<'js>, + controller: ReadableByteStreamControllerClass<'js>, + buffer: ArrayBuffer<'js>, +) -> Result<()> { + readable_byte_stream_controller_enqueue_bytes_inner(ctx, controller, buffer, false) +} + +/// Zero-copy variant of [`readable_byte_stream_controller_enqueue_bytes`]: +/// the incoming `ArrayBuffer` is NOT transferred/detached before being +/// queued. The producer keeps the buffer alive through the stream's +/// `'js` queue entry, so consumers get a `Uint8Array` that views directly +/// into the producer's storage. +/// +/// Only call this when the caller can guarantee the backing allocation +/// won't be mutated out from under readers (e.g. `Blob.stream()`, where +/// the blob's `ArrayBuffer` is never written after construction). For the +/// normal spec-compliant flow that detaches the source, use +/// [`readable_byte_stream_controller_enqueue_bytes`]. +pub fn readable_byte_stream_controller_enqueue_bytes_borrowed<'js>( + ctx: Ctx<'js>, + controller: ReadableByteStreamControllerClass<'js>, + buffer: ArrayBuffer<'js>, +) -> Result<()> { + readable_byte_stream_controller_enqueue_bytes_inner(ctx, controller, buffer, true) +} + +fn readable_byte_stream_controller_enqueue_bytes_inner<'js>( + ctx: Ctx<'js>, + controller: ReadableByteStreamControllerClass<'js>, + buffer: ArrayBuffer<'js>, + skip_transfer: bool, +) -> Result<()> { + let byte_length = buffer.len(); + if byte_length == 0 { + return Ok(()); + } + let view = rquickjs::TypedArray::::from_arraybuffer(buffer)?; + let borrow = OwnedBorrowMut::from_class(controller); + let chunk = ViewBytes::from_value( + &ctx, + &borrow.function_array_buffer_is_view, + Some(&view.into_value()), + )?; + let objects = ReadableStreamObjects::from_byte_controller(borrow).refresh_reader(); + if skip_transfer { + ReadableByteStreamController::readable_byte_stream_controller_enqueue_borrowed( + &ctx, objects, chunk, + )?; + } else { + ReadableByteStreamController::readable_byte_stream_controller_enqueue( + &ctx, objects, chunk, + )?; + } + Ok(()) +} + +/// Public API for closing a byte stream controller from Rust code. +pub fn readable_byte_stream_controller_close_stream<'js>( + ctx: Ctx<'js>, + controller: ReadableByteStreamControllerClass<'js>, +) -> Result<()> { + let borrow = OwnedBorrowMut::from_class(controller); + let objects = ReadableStreamObjects::from_byte_controller(borrow).refresh_reader(); + ReadableByteStreamController::readable_byte_stream_controller_close(ctx, objects)?; + Ok(()) +} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/controller.rs b/stdlib/src/llrt/llrt_stream_web/readable/controller.rs new file mode 100644 index 00000000..887ccedd --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/readable/controller.rs @@ -0,0 +1,200 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use rquickjs::{ + class::{OwnedBorrowMut, Trace}, + Ctx, IntoJs, JsLifetime, Promise, Result, Value, +}; + +use crate::llrt_stream_web::readable::{ + byte_controller::{ReadableByteStreamControllerClass, ReadableByteStreamControllerOwned}, + default_controller::{ + ReadableStreamDefaultControllerClass, ReadableStreamDefaultControllerOwned, + }, + default_reader::ReadableStreamReadRequest, + objects::{ReadableStreamDefaultReaderObjects, ReadableStreamObjects}, + reader::ReadableStreamReader, +}; + +pub(crate) trait ReadableStreamController<'js>: Sized { + type Class: Clone + Trace<'js>; + + fn with_controller( + self, + ctx: C, + default: impl FnOnce( + C, + ReadableStreamDefaultControllerOwned<'js>, + ) -> Result<(O, ReadableStreamDefaultControllerOwned<'js>)>, + byte: impl FnOnce( + C, + ReadableByteStreamControllerOwned<'js>, + ) -> Result<(O, ReadableByteStreamControllerOwned<'js>)>, + ) -> Result<(O, Self)>; + + fn into_inner(self) -> Self::Class; + fn from_class(class: Self::Class) -> Self; + + fn into_erased(self) -> ReadableStreamControllerOwned<'js>; + fn try_from_erased(erased: ReadableStreamControllerOwned<'js>) -> Option; + + fn pull_steps( + ctx: &Ctx<'js>, + objects: ReadableStreamDefaultReaderObjects<'js, Self>, + read_request: impl ReadableStreamReadRequest<'js> + 'js, + ) -> Result>; + + fn cancel_steps>( + ctx: &Ctx<'js>, + objects: ReadableStreamObjects<'js, Self, R>, + reason: Value<'js>, + ) -> Result<(Promise<'js>, ReadableStreamObjects<'js, Self, R>)>; + + fn release_steps(&mut self); +} + +#[derive(JsLifetime, Trace, Clone)] +pub enum ReadableStreamControllerClass<'js> { + ReadableStreamDefaultController(ReadableStreamDefaultControllerClass<'js>), + ReadableStreamByteController(ReadableByteStreamControllerClass<'js>), + Uninitialised, // Only for use when initialising a Stream - should never be present later on +} + +impl<'js> IntoJs<'js> for ReadableStreamControllerClass<'js> { + fn into_js(self, ctx: &Ctx<'js>) -> Result> { + match self { + Self::ReadableStreamDefaultController(c) => c.into_js(ctx), + Self::ReadableStreamByteController(c) => c.into_js(ctx), + Self::Uninitialised => { + panic!("Tried to convert an uninitialised controller class to JS") + } + } + } +} + +pub(crate) enum ReadableStreamControllerOwned<'js> { + ReadableStreamDefaultController(ReadableStreamDefaultControllerOwned<'js>), + ReadableStreamByteController(ReadableByteStreamControllerOwned<'js>), +} + +impl<'js> ReadableStreamController<'js> for ReadableStreamControllerOwned<'js> { + type Class = ReadableStreamControllerClass<'js>; + + fn with_controller( + self, + ctx: C, + default: impl FnOnce( + C, + ReadableStreamDefaultControllerOwned<'js>, + ) -> Result<(O, ReadableStreamDefaultControllerOwned<'js>)>, + byob: impl FnOnce( + C, + ReadableByteStreamControllerOwned<'js>, + ) -> Result<(O, ReadableByteStreamControllerOwned<'js>)>, + ) -> Result<(O, Self)> { + match self { + ReadableStreamControllerOwned::ReadableStreamDefaultController(r) => { + let (ctx, r) = default(ctx, r)?; + Ok((ctx, Self::ReadableStreamDefaultController(r))) + } + ReadableStreamControllerOwned::ReadableStreamByteController(r) => { + let (ctx, r) = byob(ctx, r)?; + Ok((ctx, Self::ReadableStreamByteController(r))) + } + } + } + + fn into_inner(self) -> Self::Class { + match self { + ReadableStreamControllerOwned::ReadableStreamDefaultController(c) => { + ReadableStreamControllerClass::ReadableStreamDefaultController(c.into_inner()) + } + ReadableStreamControllerOwned::ReadableStreamByteController(c) => { + ReadableStreamControllerClass::ReadableStreamByteController(c.into_inner()) + } + } + } + + fn from_class(class: Self::Class) -> Self { + match class { + ReadableStreamControllerClass::ReadableStreamDefaultController(class) => { + ReadableStreamControllerOwned::ReadableStreamDefaultController( + OwnedBorrowMut::from_class(class), + ) + } + ReadableStreamControllerClass::ReadableStreamByteController(class) => { + ReadableStreamControllerOwned::ReadableStreamByteController( + OwnedBorrowMut::from_class(class), + ) + } + ReadableStreamControllerClass::Uninitialised => { + panic!("Tried to borrow an uninitialised controller class") + } + } + } + + fn into_erased(self) -> ReadableStreamControllerOwned<'js> { + self + } + + fn try_from_erased(erased: ReadableStreamControllerOwned<'js>) -> Option { + Some(erased) + } + + fn pull_steps( + ctx: &Ctx<'js>, + objects: ReadableStreamDefaultReaderObjects<'js, Self>, + read_request: impl ReadableStreamReadRequest<'js> + 'js, + ) -> Result> { + objects + .with_controller( + read_request, + |read_request, objects| { + ReadableStreamDefaultControllerOwned::<'js>::pull_steps( + ctx, + objects, + read_request, + ) + .map(|objects| ((), objects)) + }, + |read_request, objects| { + ReadableByteStreamControllerOwned::<'js>::pull_steps(ctx, objects, read_request) + .map(|objects| ((), objects)) + }, + ) + .map(|((), objects)| objects) + } + + fn cancel_steps>( + ctx: &Ctx<'js>, + objects: ReadableStreamObjects<'js, Self, R>, + reason: Value<'js>, + ) -> Result<(Promise<'js>, ReadableStreamObjects<'js, Self, R>)> { + objects.with_controller( + reason, + |reason, objects| { + ReadableStreamDefaultControllerOwned::<'js>::cancel_steps(ctx, objects, reason) + }, + |reason, objects| { + ReadableByteStreamControllerOwned::<'js>::cancel_steps(ctx, objects, reason) + }, + ) + } + + fn release_steps(&mut self) { + match self { + ReadableStreamControllerOwned::ReadableStreamDefaultController(c) => c.release_steps(), + ReadableStreamControllerOwned::ReadableStreamByteController(c) => c.release_steps(), + } + } +} + +impl<'js> From> for ReadableStreamControllerOwned<'js> { + fn from(value: ReadableStreamDefaultControllerOwned<'js>) -> Self { + Self::ReadableStreamDefaultController(value) + } +} + +impl<'js> From> for ReadableStreamControllerOwned<'js> { + fn from(value: ReadableByteStreamControllerOwned<'js>) -> Self { + Self::ReadableStreamByteController(value) + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/default_controller.rs b/stdlib/src/llrt/llrt_stream_web/readable/default_controller.rs new file mode 100644 index 00000000..892564e0 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/readable/default_controller.rs @@ -0,0 +1,960 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use crate::llrt_utils::option::{Null, Undefined}; +use rquickjs::{ + class::{OwnedBorrow, OwnedBorrowMut, Trace}, + methods, + prelude::{Opt, This}, + Class, Ctx, Error, Exception, JsLifetime, Object, Promise, Result, Value, +}; +use std::{future, pin::Pin, rc::Rc}; + +/// Native async pull: returns Ok(Some(chunk)) or Ok(None) for EOF. +/// Result of a native pull: data ready, EOF, or need async. +pub enum NativePullResult<'js> { + /// Data chunk ready synchronously + Ready(Value<'js>), + /// EOF — no more data + Eof, + /// Need async — returns a future for the pending case + Pending(Pin>>> + 'js>>), +} + +pub type NativePullFn<'js> = dyn Fn(&Ctx<'js>) -> Result> + 'js; + +/// Wrapper satisfying JsLifetime/Trace. +pub struct NativePull<'js>(pub Rc>); +impl<'js> Clone for NativePull<'js> { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} +unsafe impl<'js> JsLifetime<'js> for NativePull<'js> { + type Changed<'to> = NativePull<'to>; +} +impl<'js> Trace<'js> for NativePull<'js> { + fn trace<'a>(&self, _: rquickjs::class::Tracer<'a, 'js>) {} +} + +use crate::llrt_stream_web::{ + queuing_strategy::{SizeAlgorithm, SizeValue}, + readable::{ + byte_controller::ReadableByteStreamControllerOwned, + controller::{ + ReadableStreamController, ReadableStreamControllerClass, ReadableStreamControllerOwned, + }, + default_reader::{ReadableStreamDefaultReaderOrUndefined, ReadableStreamReadRequest}, + objects::{ + ReadableStreamClassObjects, ReadableStreamDefaultControllerObjects, + ReadableStreamDefaultReaderObjects, ReadableStreamObjects, + }, + reader::ReadableStreamReader, + stream::{ + algorithms::{CancelAlgorithm, PullAlgorithm, StartAlgorithm}, + source::UnderlyingSource, + ReadableStream, ReadableStreamClass, ReadableStreamOwned, ReadableStreamState, + }, + }, + utils::{ + class_from_owned_borrow_mut, + promise::{promise_resolved_with, upon_promise}, + queue::QueueWithSizes, + UnwrapOrUndefined, + }, +}; + +#[derive(JsLifetime, Trace)] +#[rquickjs::class] +pub struct ReadableStreamDefaultController<'js> { + cancel_algorithm: Option>, + pub(super) close_requested: bool, + pull_again: bool, + pull_algorithm: Option>, + pub(crate) pulling: bool, + pub(crate) container: QueueWithSizes<'js>, + started: bool, + strategy_hwm: f64, + strategy_size_algorithm: Option>, + pub(super) stream: ReadableStreamClass<'js>, + pub native_pull: Option>, + /// Whether this stream was created with `{type: 'owning'}`. Owning streams + /// accept a non-empty `transfer` array in `controller.enqueue` and + /// structurally transfer each buffer before queueing; non-owning streams + /// throw when a non-empty `transfer` list is provided. + #[qjs(skip_trace)] + pub(super) is_owning_type: bool, +} + +impl<'js> Drop for ReadableStreamDefaultController<'js> { + fn drop(&mut self) { + self.native_pull = None; + } +} + +pub type ReadableStreamDefaultControllerClass<'js> = + Class<'js, ReadableStreamDefaultController<'js>>; +pub(super) type ReadableStreamDefaultControllerOwned<'js> = + OwnedBorrowMut<'js, ReadableStreamDefaultController<'js>>; + +impl<'js> ReadableStreamDefaultController<'js> { + pub(super) fn set_up_readable_stream_default_controller_from_underlying_source( + ctx: Ctx<'js>, + stream: ReadableStreamOwned<'js>, + underlying_source: Null>>, + underlying_source_dict: UnderlyingSource<'js>, + high_water_mark: f64, + size_algorithm: SizeAlgorithm<'js>, + is_owning_type: bool, + ) -> Result<()> { + let (start_algorithm, pull_algorithm, cancel_algorithm) = ( + // If underlyingSourceDict["start"] exists, then set startAlgorithm to an algorithm which returns the result of invoking underlyingSourceDict["start"] with argument list + // « controller » and callback this value underlyingSource. + underlying_source_dict + .start + .map(|f| StartAlgorithm::Function { + f, + underlying_source: underlying_source.clone(), + }) + .unwrap_or(StartAlgorithm::ReturnUndefined), + // If underlyingSourceDict["pull"] exists, then set pullAlgorithm to an algorithm which returns the result of invoking underlyingSourceDict["pull"] with argument list + // « controller » and callback this value underlyingSource. + underlying_source_dict + .pull + .map(|f| PullAlgorithm::Function { + f, + underlying_source: underlying_source.clone(), + }) + .unwrap_or(PullAlgorithm::ReturnPromiseUndefined), + // If underlyingSourceDict["cancel"] exists, then set cancelAlgorithm to an algorithm which takes an argument reason and returns the result of invoking underlyingSourceDict["cancel"] with argument list + // « reason » and callback this value underlyingSource. + underlying_source_dict + .cancel + .map(|f| CancelAlgorithm::Function { + f, + underlying_source, + }) + .unwrap_or(CancelAlgorithm::ReturnPromiseUndefined), + ); + + // Perform ? SetUpReadableStreamDefaultController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm). + Self::set_up_readable_stream_default_controller( + ctx.clone(), + stream, + start_algorithm, + pull_algorithm, + cancel_algorithm, + high_water_mark, + size_algorithm, + is_owning_type, + )?; + + Ok(()) + } + + #[allow(clippy::too_many_arguments)] + pub(super) fn set_up_readable_stream_default_controller( + ctx: Ctx<'js>, + stream: ReadableStreamOwned<'js>, + start_algorithm: StartAlgorithm<'js>, + pull_algorithm: PullAlgorithm<'js>, + cancel_algorithm: CancelAlgorithm<'js>, + high_water_mark: f64, + size_algorithm: SizeAlgorithm<'js>, + is_owning_type: bool, + ) -> Result> { + let (stream_class, mut stream) = class_from_owned_borrow_mut(stream); + + let controller = ReadableStreamDefaultController { + // Set controller.[[stream]] to stream. + stream: stream_class.clone(), + + // Perform ! ResetQueue(controller). + container: QueueWithSizes::new(), + + // Set controller.[[started]], controller.[[closeRequested]], controller.[[pullAgain]], and controller.[[pulling]] to false. + started: false, + close_requested: false, + pull_again: false, + pulling: false, + + // Set controller.[[strategySizeAlgorithm]] to sizeAlgorithm and controller.[[strategyHWM]] to highWaterMark. + strategy_size_algorithm: Some(size_algorithm), + strategy_hwm: high_water_mark, + + // Set controller.[[pullAlgorithm]] to pullAlgorithm. + pull_algorithm: Some(pull_algorithm), + // Set controller.[[cancelAlgorithm]] to cancelAlgorithm. + cancel_algorithm: Some(cancel_algorithm), + native_pull: None, + is_owning_type, + }; + + let controller_class = Class::instance(ctx.clone(), controller)?; + + // Set stream.[[controller]] to controller. + stream.controller = ReadableStreamControllerClass::ReadableStreamDefaultController( + controller_class.clone(), + ); + + let objects = ReadableStreamObjects::new_default( + stream, + OwnedBorrowMut::from_class(controller_class), + ); + + let promise_primordials = objects.stream.promise_primordials.clone(); + + // Let startResult be the result of performing startAlgorithm. (This might throw an exception.) + let (start_result, objects_class) = + Self::start_algorithm(ctx.clone(), objects, start_algorithm)?; + + // Let startPromise be a promise resolved with startResult. + let start_promise = promise_resolved_with(&ctx, &promise_primordials, Ok(start_result))?; + + let _ = upon_promise::, _>(ctx.clone(), start_promise, { + let objects_class = objects_class.clone(); + move |ctx, result| { + let mut objects = + ReadableStreamObjects::from_class_no_reader(objects_class).refresh_reader(); + + match result { + // Upon fulfillment of startPromise, + Ok(_) => { + // Set controller.[[started]] to true. + objects.controller.started = true; + // Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). + Self::readable_stream_default_controller_call_pull_if_needed(ctx, objects)?; + } + // Upon rejection of startPromise with reason r, + Err(r) => { + // Perform ! ReadableByteStreamControllerError(controller, r). + Self::readable_stream_default_controller_error(objects, r)?; + } + } + Ok(()) + } + })?; + + Ok(objects_class.controller) + } + + fn readable_stream_default_controller_call_pull_if_needed< + R: ReadableStreamDefaultReaderOrUndefined<'js>, + >( + ctx: Ctx<'js>, + objects: ReadableStreamDefaultControllerObjects<'js, R>, + ) -> Result> { + // Let shouldPull be ! ReadableStreamDefaultControllerShouldCallPull(controller). + + let (should_pull, mut objects) = + ReadableStreamDefaultController::readable_stream_default_controller_should_call_pull( + objects, + ); + + // If shouldPull is false, return. + if !should_pull { + return Ok(objects); + } + + // If controller.[[pulling]] is true, + if objects.controller.pulling { + // Set controller.[[pullAgain]] to true. + objects.controller.pull_again = true; + + // Return. + return Ok(objects); + } + + // Set controller.[[pulling]] to true. + objects.controller.pulling = true; + + // Let pullPromise be the result of performing controller.[[pullAlgorithm]]. + let (pull_promise, objects_class) = Self::pull_algorithm(ctx.clone(), objects)?; + + upon_promise::, _>(ctx.clone(), pull_promise, { + let objects_class = objects_class.clone(); + move |ctx, result| { + let mut objects = + ReadableStreamObjects::from_class_no_reader(objects_class).refresh_reader(); + match result { + // Upon fulfillment of pullPromise, + Ok(_) => { + // Set controller.[[pulling]] to false. + objects.controller.pulling = false; + // If controller.[[pullAgain]] is true, + if objects.controller.pull_again { + // Set controller.[[pullAgain]] to false. + objects.controller.pull_again = false; + // Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(controller). + Self::readable_stream_default_controller_call_pull_if_needed( + ctx, objects, + )?; + }; + Ok(()) + } + // Upon rejection of pullPromise with reason e, + Err(e) => { + // Perform ! ReadableStreamDefaultControllerError(controller, e). + Self::readable_stream_default_controller_error(objects, e)?; + Ok(()) + } + } + } + })?; + + Ok(ReadableStreamObjects::from_class(objects_class)) + } + + pub(super) fn readable_stream_default_controller_error>( + // Let stream be controller.[[stream]]. + mut objects: ReadableStreamDefaultControllerObjects<'js, R>, + e: Value<'js>, + ) -> Result> { + // If stream.[[state]] is not "readable", return. + if !matches!(objects.stream.state, ReadableStreamState::Readable) { + return Ok(objects); + }; + + // Perform ! ResetQueue(controller). + objects.controller.container.reset_queue(); + + // Perform ! ReadableStreamDefaultControllerClearAlgorithms(controller). + objects + .controller + .readable_stream_default_controller_clear_algorithms(); + + // Perform ! ReadableStreamError(stream, e). + ReadableStream::readable_stream_error(objects, e) + } + + fn readable_stream_default_controller_should_call_pull< + R: ReadableStreamDefaultReaderOrUndefined<'js>, + >( + mut objects: ReadableStreamDefaultControllerObjects<'js, R>, + ) -> (bool, ReadableStreamDefaultControllerObjects<'js, R>) { + // Let stream be controller.[[stream]]. + // If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(controller) is false, return false. + if !objects + .controller + .readable_stream_default_controller_can_close_or_enqueue(&objects.stream) + { + return (false, objects); + } + + // If controller.[[started]] is false, return false. + if !objects.controller.started { + return (false, objects); + } + + { + let mut ret = false; + // If ! IsReadableStreamLocked(stream) is true and ! ReadableStreamGetNumReadRequests(stream) > 0, return true. + objects = objects + .with_some_reader( + |objects| { + if ReadableStream::readable_stream_get_num_read_requests(&objects.reader) + > 0 + { + ret = true + } + Ok(objects) + }, + Ok, + ) + .unwrap(); + if ret { + return (true, objects); + } + } + + // Let desiredSize be ! ReadableStreamDefaultControllerGetDesiredSize(controller). + let desired_size = objects.controller + .readable_stream_default_controller_get_desired_size(&objects.stream) + .0 + .expect( + "desiredSize should not be null during ReadableStreamDefaultControllerShouldCallPull", + ); + // If desiredSize > 0, return true. + if desired_size > 0.0 { + return (true, objects); + } + + // Return false. + (false, objects) + } + + fn readable_stream_default_controller_clear_algorithms(&mut self) { + self.pull_algorithm = None; + self.cancel_algorithm = None; + self.strategy_size_algorithm = None; + self.native_pull = None; + } + + fn readable_stream_default_controller_can_close_or_enqueue( + &self, + stream: &ReadableStream<'js>, + ) -> bool { + // Let state be controller.[[stream]].[[state]]. + match stream.state { + // If controller.[[closeRequested]] is false and state is "readable", return true. + ReadableStreamState::Readable if !self.close_requested => true, + // Otherwise, return false. + _ => false, + } + } + + pub(crate) fn readable_stream_default_controller_get_desired_size( + &self, + stream: &ReadableStream<'js>, + ) -> Null { + // Let state be controller.[[stream]].[[state]]. + match stream.state { + // If state is "errored", return null. + ReadableStreamState::Errored(_) => Null(None), + // If state is "closed", return 0. + ReadableStreamState::Closed => Null(Some(0.0)), + // Return controller.[[strategyHWM]] − controller.[[queueTotalSize]]. + ReadableStreamState::Readable => { + Null(Some(self.strategy_hwm - self.container.queue_total_size)) + } + } + } + + pub(super) fn readable_stream_default_controller_close>( + ctx: Ctx<'js>, + // Let stream be controller.[[stream]]. + mut objects: ReadableStreamDefaultControllerObjects<'js, R>, + ) -> Result> { + // If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(controller) is false, return. + if !objects + .controller + .readable_stream_default_controller_can_close_or_enqueue(&objects.stream) + { + return Ok(objects); + } + + // Set controller.[[closeRequested]] to true. + objects.controller.close_requested = true; + + // If controller.[[queue]] is empty, + if objects.controller.container.queue.is_empty() { + // Perform ! ReadableStreamDefaultControllerClearAlgorithms(controller). + objects + .controller + .readable_stream_default_controller_clear_algorithms(); + // Perform ! ReadableStreamClose(stream). + objects = ReadableStream::readable_stream_close(ctx, objects)?; + } + + Ok(objects) + } + + pub(super) fn readable_stream_default_controller_enqueue< + R: ReadableStreamDefaultReaderOrUndefined<'js>, + >( + ctx: Ctx<'js>, + // Let stream be controller.[[stream]]. + mut objects: ReadableStreamDefaultControllerObjects<'js, R>, + chunk: Value<'js>, + ) -> Result> { + // If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(controller) is false, return. + if !objects + .controller + .readable_stream_default_controller_can_close_or_enqueue(&objects.stream) + { + return Ok(objects); + } + + let mut els = true; + // If ! IsReadableStreamLocked(stream) is true and ! ReadableStreamGetNumReadRequests(stream) > 0, perform ! ReadableStreamFulfillReadRequest(stream, chunk, false). + objects = objects.with_some_reader( + |objects| { + if ReadableStream::readable_stream_get_num_read_requests(&objects.reader) > 0 { + els = false; + ReadableStream::readable_stream_fulfill_read_request( + &ctx, + objects, + chunk.clone(), + false, + ) + } else { + Ok(objects) + } + }, + Ok, + )?; + + if els { + // Let result be the result of performing controller.[[strategySizeAlgorithm]], passing in chunk, and interpreting the result as a completion record. + let (result, objects_class) = + Self::strategy_size_algorithm(ctx.clone(), objects, chunk.clone()); + + objects = ReadableStreamObjects::from_class(objects_class); + + match result { + // If result is an abrupt completion, + Err(Error::Exception) => { + let err = ctx.catch(); + // Perform ! ReadableStreamDefaultControllerError(controller, result.[[Value]]). + Self::readable_stream_default_controller_error(objects, err.clone())?; + + return Err(ctx.throw(err)); + } + // Let chunkSize be result.[[Value]]. + Ok(chunk_size) => { + // Let enqueueResult be EnqueueValueWithSize(controller, chunk, chunkSize). + let enqueue_result = objects + .controller + .container + .enqueue_value_with_size(&ctx, chunk, chunk_size); + + match enqueue_result { + // If enqueueResult is an abrupt completion, + Err(Error::Exception) => { + let err = ctx.catch(); + // Perform ! ReadableStreamDefaultControllerError(controller, enqueueResult.[[Value]]). + Self::readable_stream_default_controller_error(objects, err.clone())?; + return Err(ctx.throw(err)); + } + Err(err) => return Err(err), + Ok(()) => {} + } + } + Err(err) => return Err(err), + } + } + + // Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(controller). + Self::readable_stream_default_controller_call_pull_if_needed(ctx, objects) + } + + fn start_algorithm>( + ctx: Ctx<'js>, + objects: ReadableStreamDefaultControllerObjects<'js, R>, + start_algorithm: StartAlgorithm<'js>, + ) -> Result<( + Value<'js>, + ReadableStreamClassObjects<'js, OwnedBorrowMut<'js, Self>, R>, + )> { + let objects_class = objects.into_inner(); + + Ok(( + start_algorithm.call( + ctx, + ReadableStreamControllerClass::ReadableStreamDefaultController( + objects_class.controller.clone(), + ), + )?, + objects_class, + )) + } + + fn pull_algorithm>( + ctx: Ctx<'js>, + objects: ReadableStreamDefaultControllerObjects<'js, R>, + ) -> Result<( + Promise<'js>, + ReadableStreamClassObjects<'js, OwnedBorrowMut<'js, Self>, R>, + )> { + let pull_algorithm = objects + .controller + .pull_algorithm + .clone() + .expect("pull algorithm used after ReadableStreamDefaultControllerClearAlgorithms"); + let promise_primordials = objects.stream.promise_primordials.clone(); + let objects_class = objects.into_inner(); + + Ok(( + pull_algorithm.call( + ctx, + &promise_primordials, + ReadableStreamControllerClass::ReadableStreamDefaultController( + objects_class.controller.clone(), + ), + )?, + objects_class, + )) + } + + fn strategy_size_algorithm>( + ctx: Ctx<'js>, + objects: ReadableStreamDefaultControllerObjects<'js, R>, + chunk: Value<'js>, + ) -> ( + Result>, + ReadableStreamClassObjects<'js, OwnedBorrowMut<'js, Self>, R>, + ) { + let strategy_size_algorithm = objects + .controller + .strategy_size_algorithm + .clone() + .expect("size algorithm used after ReadableStreamDefaultControllerClearAlgorithms"); + let objects_class = objects.into_inner(); + + (strategy_size_algorithm.call(ctx, chunk), objects_class) + } + + pub(super) fn cancel_algorithm>( + ctx: Ctx<'js>, + objects: ReadableStreamDefaultControllerObjects<'js, R>, + reason: Value<'js>, + ) -> Result<( + Promise<'js>, + ReadableStreamClassObjects<'js, OwnedBorrowMut<'js, Self>, R>, + )> { + let cancel_algorithm = + objects.controller.cancel_algorithm.clone().expect( + "cancel algorithm used after ReadableStreamDefaultControllerClearAlgorithms", + ); + let promise_primordials = objects.stream.promise_primordials.clone(); + let objects_class = objects.into_inner(); + + Ok(( + cancel_algorithm.call(ctx, &promise_primordials, reason)?, + objects_class, + )) + } +} + +#[methods(rename_all = "camelCase")] +impl<'js> ReadableStreamDefaultController<'js> { + // this is required by web platform tests for unclear reasons + fn constructor() -> Self { + unimplemented!() + } + + #[qjs(constructor)] + fn new(ctx: Ctx<'js>) -> Result> { + Err(Exception::throw_type(&ctx, "Illegal constructor")) + } + + // readonly attribute unrestricted double? desiredSize; + #[qjs(get)] + fn desired_size(&self) -> Null { + let stream = OwnedBorrow::from_class(self.stream.clone()); + self.readable_stream_default_controller_get_desired_size(&stream) + } + + // undefined close(); + fn close(ctx: Ctx<'js>, controller: This>) -> Result<()> { + let objects = ReadableStreamObjects::from_default_controller(controller.0); + + // If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(this) is false, throw a TypeError exception. + if !objects + .controller + .readable_stream_default_controller_can_close_or_enqueue(&objects.stream) + { + return Err(Exception::throw_type( + &ctx, + "The stream is not in a state that permits close", + )); + } + + // Perform ! ReadableStreamDefaultControllerClose(this). + Self::readable_stream_default_controller_close(ctx, objects)?; + Ok(()) + } + + // undefined enqueue(optional any chunk, optional ReadableStreamEnqueueOptions options = {}); + fn enqueue( + ctx: Ctx<'js>, + controller: This>, + chunk: Opt>, + options: Opt>, + ) -> Result<()> { + // Handle the `transfer` option per the `type: 'owning'` ReadableStream + // proposal (WPT `streams/readable-streams/owning-type`). The option + // is only meaningful on owning-type streams; any other stream throws + // `TypeError` if the caller passes a non-empty transfer list. + // + // WebIDL getter semantics apply: property access must propagate. + let mut transfer_list: Option> = None; + if let Some(opts) = options.0.as_ref().and_then(|v| v.as_object()) { + transfer_list = opts.get::<_, Option>>("transfer")?; + } + let has_transfer_items = transfer_list.as_ref().is_some_and(|arr| !arr.is_empty()); + if has_transfer_items && !controller.is_owning_type { + return Err(Exception::throw_type(&ctx, "transfer list is not empty")); + } + // Detach each buffer in the transfer list (owning-type streams). Uses + // JS `ArrayBuffer.prototype.transfer()` which returns a new buffer + // with the same bytes and detaches the original. We re-bind the + // chunk to the new buffer if it was the same reference. + let chunk_value = chunk.0.clone().unwrap_or_undefined(&ctx); + let transferred_chunk = if has_transfer_items && controller.is_owning_type { + transfer_owning_chunk(&ctx, chunk_value.clone(), &transfer_list.unwrap())? + } else { + chunk_value + }; + + let objects = ReadableStreamObjects::from_default_controller(controller.0); + + // If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(this) is false, throw a TypeError exception. + if !objects + .controller + .readable_stream_default_controller_can_close_or_enqueue(&objects.stream) + { + return Err(Exception::throw_type( + &ctx, + "The stream is not in a state that permits enqueue", + )); + } + + objects.with_reader( + |objects| { + // Perform ? ReadableStreamDefaultControllerEnqueue(this, chunk). + Self::readable_stream_default_controller_enqueue( + ctx.clone(), + objects, + transferred_chunk.clone(), + ) + }, + |_| panic!("Default controller must not have byob reader"), + |objects| { + // Perform ? ReadableStreamDefaultControllerEnqueue(this, chunk). + Self::readable_stream_default_controller_enqueue( + ctx.clone(), + objects, + transferred_chunk.clone(), + ) + }, + )?; + + Ok(()) + } + + // undefined error(optional any e); + fn error( + ctx: Ctx<'js>, + controller: This>, + e: Opt>, + ) -> Result<()> { + let objects = ReadableStreamObjects::from_default_controller(controller.0); + + // Perform ! ReadableStreamDefaultControllerError(this, e). + Self::readable_stream_default_controller_error(objects, e.0.unwrap_or_undefined(&ctx))?; + Ok(()) + } +} + +impl<'js> ReadableStreamController<'js> for ReadableStreamDefaultControllerOwned<'js> { + type Class = ReadableStreamDefaultControllerClass<'js>; + + fn with_controller( + self, + ctx: C, + default: impl FnOnce( + C, + ReadableStreamDefaultControllerOwned<'js>, + ) -> Result<(O, ReadableStreamDefaultControllerOwned<'js>)>, + _: impl FnOnce( + C, + ReadableByteStreamControllerOwned<'js>, + ) -> Result<(O, ReadableByteStreamControllerOwned<'js>)>, + ) -> Result<(O, Self)> { + let (ctx, reader) = default(ctx, self)?; + Ok((ctx, reader)) + } + + fn into_inner(self) -> Self::Class { + OwnedBorrowMut::into_inner(self) + } + + fn from_class(class: Self::Class) -> Self { + OwnedBorrowMut::from_class(class) + } + + fn into_erased(self) -> ReadableStreamControllerOwned<'js> { + ReadableStreamControllerOwned::ReadableStreamDefaultController(self) + } + + fn try_from_erased(erased: ReadableStreamControllerOwned<'js>) -> Option { + match erased { + ReadableStreamControllerOwned::ReadableStreamDefaultController(r) => Some(r), + ReadableStreamControllerOwned::ReadableStreamByteController(_) => None, + } + } + + fn pull_steps( + ctx: &Ctx<'js>, + mut objects: ReadableStreamDefaultReaderObjects<'js, Self>, + read_request: impl ReadableStreamReadRequest<'js> + 'js, + ) -> Result> { + // If this.[[queue]] is not empty, + if !objects.controller.container.queue.is_empty() { + // Let chunk be ! DequeueValue(this). + let chunk = objects.controller.container.dequeue_value(); + // If this.[[closeRequested]] is true and this.[[queue]] is empty, + if objects.controller.close_requested && objects.controller.container.queue.is_empty() { + // Perform ! ReadableStreamDefaultControllerClearAlgorithms(this). + objects + .controller + .readable_stream_default_controller_clear_algorithms(); + // Perform ! ReadableStreamClose(stream). + objects = ReadableStream::readable_stream_close(ctx.clone(), objects)?; + } else { + // Otherwise, perform ! ReadableStreamDefaultControllerCallPullIfNeeded(this). + objects = + ReadableStreamDefaultController::readable_stream_default_controller_call_pull_if_needed( + ctx.clone(), + objects, + )?; + } + + // Perform readRequest’s chunk steps, given chunk. + read_request.chunk_steps_typed(objects, chunk) + } else { + // Otherwise, + // Perform ! ReadableStreamAddReadRequest(stream, readRequest). + objects + .stream + .readable_stream_add_read_request(&mut objects.reader, read_request); + // Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(this). + + ReadableStreamDefaultController::readable_stream_default_controller_call_pull_if_needed( + ctx.clone(), + objects, + ) + } + } + + fn cancel_steps>( + ctx: &Ctx<'js>, + mut objects: ReadableStreamObjects<'js, Self, R>, + reason: Value<'js>, + ) -> Result<(Promise<'js>, ReadableStreamObjects<'js, Self, R>)> { + // Perform ! ResetQueue(this). + objects.controller.container.reset_queue(); + + // Let result be the result of performing this.[[cancelAlgorithm]], passing reason. + let (result, objects_class) = + ReadableStreamDefaultController::cancel_algorithm(ctx.clone(), objects, reason)?; + + objects = ReadableStreamObjects::from_class(objects_class); + // Perform ! ReadableStreamDefaultControllerClearAlgorithms(this). + objects + .controller + .readable_stream_default_controller_clear_algorithms(); + + // Return result. + Ok((result, objects)) + } + + fn release_steps(&mut self) {} +} + +/// Public API for enqueuing data into a default controller from Rust code +pub fn readable_stream_default_controller_enqueue_value<'js>( + ctx: Ctx<'js>, + controller: ReadableStreamDefaultControllerClass<'js>, + chunk: Value<'js>, +) -> Result<()> { + let objects = + ReadableStreamObjects::from_default_controller(OwnedBorrowMut::from_class(controller)); + + if !objects + .controller + .readable_stream_default_controller_can_close_or_enqueue(&objects.stream) + { + return Ok(()); // Silently ignore if can't enqueue + } + + objects.with_reader( + |objects| { + ReadableStreamDefaultController::readable_stream_default_controller_enqueue( + ctx.clone(), + objects, + chunk.clone(), + ) + }, + |_| panic!("Default controller must not have byob reader"), + |objects| { + ReadableStreamDefaultController::readable_stream_default_controller_enqueue( + ctx.clone(), + objects, + chunk.clone(), + ) + }, + )?; + + Ok(()) +} + +/// Public API for closing a default controller from Rust code +pub fn readable_stream_default_controller_close_stream<'js>( + ctx: Ctx<'js>, + controller: ReadableStreamDefaultControllerClass<'js>, +) -> Result<()> { + let objects = + ReadableStreamObjects::from_default_controller(OwnedBorrowMut::from_class(controller)); + + if !objects + .controller + .readable_stream_default_controller_can_close_or_enqueue(&objects.stream) + { + return Ok(()); + } + + ReadableStreamDefaultController::readable_stream_default_controller_close(ctx, objects)?; + Ok(()) +} + +/// Public API for erroring a default controller from Rust code +pub fn readable_stream_default_controller_error_stream<'js>( + controller: ReadableStreamDefaultControllerClass<'js>, + error: Value<'js>, +) -> Result<()> { + let objects = + ReadableStreamObjects::from_default_controller(OwnedBorrowMut::from_class(controller)); + + objects.with_reader( + |objects| { + ReadableStreamDefaultController::readable_stream_default_controller_error( + objects, + error.clone(), + ) + }, + |_| panic!("Default controller must not have byob reader"), + |objects| { + ReadableStreamDefaultController::readable_stream_default_controller_error( + objects, + error.clone(), + ) + }, + )?; + + Ok(()) +} + +/// Structurally transfer each `ArrayBuffer` in `transfer_list` (detaches the +/// original) and, if `chunk` references the same buffer, rebind it to the +/// transferred copy. Called for `controller.enqueue(chunk, { transfer })` on +/// `type: 'owning'` ReadableStreams. +fn transfer_owning_chunk<'js>( + ctx: &Ctx<'js>, + chunk: Value<'js>, + transfer_list: &rquickjs::Array<'js>, +) -> Result> { + use rquickjs::ArrayBuffer; + let mut chunk_replacement: Option> = None; + for v in transfer_list.iter::>() { + let v = v?; + let Some(ab) = ArrayBuffer::from_value(v.clone()) else { + return Err(rquickjs::Exception::throw_type( + ctx, + "transfer list item is not an ArrayBuffer", + )); + }; + // JS object identity: if this transfer-list entry IS the chunk + // itself, record that we need to replace the chunk with the + // transferred copy. Compare before calling transfer() (which + // detaches the buffer). + let is_chunk = chunk == v; + // Use JS `ArrayBuffer.prototype.transfer()` which returns a new + // buffer of the same byteLength and detaches the original. + let transfer_fn: rquickjs::Function<'js> = ab.as_object().get("transfer")?; + let new_buf: Value<'js> = transfer_fn.call((rquickjs::function::This(ab.clone()),))?; + if is_chunk && chunk_replacement.is_none() { + chunk_replacement = Some(new_buf); + } + } + Ok(chunk_replacement.unwrap_or(chunk)) +} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/default_reader.rs b/stdlib/src/llrt/llrt_stream_web/readable/default_reader.rs new file mode 100644 index 00000000..bd8ea5ba --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/readable/default_reader.rs @@ -0,0 +1,540 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use crate::llrt_stream_web::readable::default_controller::{NativePull, NativePullResult}; +use crate::llrt_stream_web::{ + readable::{ + byob_reader::ReadableStreamBYOBReaderOwned, + controller::{ReadableStreamController, ReadableStreamControllerClass}, + objects::{ReadableStreamDefaultReaderObjects, ReadableStreamObjects}, + reader::{ + ReadableStreamGenericReader, ReadableStreamReader, ReadableStreamReaderOwned, + UndefinedReader, + }, + stream::{ReadableStream, ReadableStreamOwned, ReadableStreamState}, + }, + utils::{ + promise::{ + promise_rejected_with_constructor, promise_resolved_with, PromisePrimordials, + ResolveablePromise, + }, + UnwrapOrUndefined, + }, +}; +use rquickjs::{ + atom::PredefinedAtom, + class::{OwnedBorrowMut, Trace, Tracer}, + methods, + prelude::{Opt, This}, + Class, Ctx, Exception, IntoJs, JsLifetime, Object, Promise, Result, Value, +}; +use std::collections::VecDeque; + +#[derive(Trace)] +#[rquickjs::class] +pub(crate) struct ReadableStreamDefaultReader<'js> { + pub(super) generic: ReadableStreamGenericReader<'js>, + pub(super) read_requests: VecDeque + 'js>>, +} + +pub(crate) type ReadableStreamDefaultReaderClass<'js> = + Class<'js, ReadableStreamDefaultReader<'js>>; +pub(crate) type ReadableStreamDefaultReaderOwned<'js> = + OwnedBorrowMut<'js, ReadableStreamDefaultReader<'js>>; + +unsafe impl<'js> JsLifetime<'js> for ReadableStreamDefaultReader<'js> { + type Changed<'to> = ReadableStreamDefaultReader<'to>; +} + +impl<'js> ReadableStreamDefaultReader<'js> { + pub(super) fn readable_stream_default_reader_error_read_requests< + C: ReadableStreamController<'js>, + >( + mut objects: ReadableStreamDefaultReaderObjects<'js, C>, + e: Value<'js>, + ) -> Result> { + // Let readRequests be reader.[[readRequests]]. + let read_requests = &mut objects.reader.read_requests; + + // Set reader.[[readRequests]] to a new empty list. + let read_requests = read_requests.split_off(0); + + // For each readRequest of readRequests, + for read_request in read_requests { + // Perform readRequest’s error steps, given e. + objects = read_request.error_steps_typed(objects, e.clone())?; + } + + Ok(objects) + } + + pub(super) fn readable_stream_default_reader_read< + 'closure, + C: ReadableStreamController<'js>, + >( + ctx: &Ctx<'js>, + // Let stream be reader.[[stream]]. + mut objects: ReadableStreamDefaultReaderObjects<'js, C>, + read_request: impl ReadableStreamReadRequest<'js> + 'js, + ) -> Result> { + // Set stream.[[disturbed]] to true. + objects.stream.disturbed = true; + match objects.stream.state { + // If stream.[[state]] is "closed", perform readRequest’s close steps. + ReadableStreamState::Closed => read_request.close_steps_typed(ctx, objects), + // Otherwise, if stream.[[state]] is "errored", perform readRequest’s error steps given stream.[[storedError]]. + ReadableStreamState::Errored(ref stored_error) => { + let stored_error = stored_error.clone(); + read_request.error_steps_typed(objects, stored_error) + } + // Otherwise, + _ => { + // Perform ! stream.[[controller]].[[PullSteps]](readRequest). + C::pull_steps(ctx, objects, read_request) + } + } + } + + pub(super) fn set_up_readable_stream_default_reader( + ctx: &Ctx<'js>, + stream: ReadableStreamOwned<'js>, + ) -> Result<(ReadableStreamOwned<'js>, Class<'js, Self>)> { + // If ! IsReadableStreamLocked(stream) is true, throw a TypeError exception. + if stream.is_readable_stream_locked() { + return Err(Exception::throw_type( + ctx, + "This stream has already been locked for exclusive reading by another reader", + )); + } + + // Perform ! ReadableStreamReaderGenericInitialize(reader, stream). + let generic = + ReadableStreamGenericReader::readable_stream_reader_generic_initialize(ctx, stream)?; + let mut stream = OwnedBorrowMut::from_class(generic.stream.clone().unwrap()); + + let reader = Class::instance( + ctx.clone(), + Self { + generic, + // Set reader.[[readRequests]] to a new empty list. + read_requests: VecDeque::new(), + }, + )?; + + stream.reader = Some(reader.clone().into()); + + Ok((stream, reader)) + } + + pub(super) fn readable_stream_default_reader_release>( + mut objects: ReadableStreamDefaultReaderObjects<'js, C>, + ) -> Result> { + // Clear cached native_pull to release captured resources + objects.reader.read_requests.clear(); + // Perform ! ReadableStreamReaderGenericRelease(reader). + objects + .reader + .generic + .readable_stream_reader_generic_release(&mut objects.stream, || { + objects.controller.release_steps() + })?; + + // Let e be a new TypeError exception. + let e: Value = objects + .stream + .constructor_type_error + .call(("Reader was released",))?; + + // Perform ! ReadableStreamDefaultReaderErrorReadRequests(reader, e). + Self::readable_stream_default_reader_error_read_requests(objects, e) + } +} + +#[methods(rename_all = "camelCase")] +impl<'js> ReadableStreamDefaultReader<'js> { + #[qjs(constructor)] + pub fn new(ctx: Ctx<'js>, stream: ReadableStreamOwned<'js>) -> Result> { + // Perform ? SetUpReadableStreamDefaultReader(this, stream). + let (_, reader) = Self::set_up_readable_stream_default_reader(&ctx, stream)?; + Ok(reader) + } + + fn read(ctx: Ctx<'js>, reader: This>) -> Result> { + // If this.[[stream]] is undefined, return a promise rejected with a TypeError exception. + let Some(stream_class) = &reader.generic.stream else { + return promise_rejected_with_constructor( + &reader.generic.constructor_type_error, + &reader.generic.promise_primordials, + "Cannot read from a stream using a released reader", + ) + .map(|p| p.into_value()); + }; + + // Fast-path: if the controller has a native_pull and the queue is empty, + // bypass the full spec algorithm and read directly without promise wrapping. + if let Some(np) = try_get_native_pull(stream_class) { + stream_class.borrow_mut().disturbed = true; + return read_native(&ctx, &np, &reader.generic.promise_primordials); + } + + read_default(&ctx, reader.0) + } + + fn release_lock(reader: This>) -> Result<()> { + if reader.generic.stream.is_none() { + // If this.[[stream]] is undefined, return. + return Ok(()); + } + + let objects = ReadableStreamObjects::from_default_reader(reader.0); + + // Perform ! ReadableStreamDefaultReaderRelease(this). + Self::readable_stream_default_reader_release(objects)?; + Ok(()) + } + + #[qjs(get)] + fn closed(&self) -> Promise<'js> { + self.generic.closed_promise.promise.clone() + } + + fn cancel( + ctx: Ctx<'js>, + reader: This>, + reason: Opt>, + ) -> Result> { + if reader.generic.stream.is_none() { + // If this.[[stream]] is undefined, return a promise rejected with a TypeError exception. + return promise_rejected_with_constructor( + &reader.generic.constructor_type_error, + &reader.generic.promise_primordials, + "Cannot cancel a stream using a released reader", + ); + }; + + let objects = ReadableStreamObjects::from_default_reader(reader.0); + + // Return ! ReadableStreamReaderGenericCancel(this, reason). + let (promise, _) = ReadableStreamGenericReader::readable_stream_reader_generic_cancel( + ctx.clone(), + objects, + reason.0.unwrap_or_undefined(&ctx), + )?; + Ok(promise) + } +} + +impl<'js> ReadableStreamReader<'js> for ReadableStreamDefaultReaderOwned<'js> { + type Class = ReadableStreamDefaultReaderClass<'js>; + + fn with_reader( + self, + ctx: C, + default: impl FnOnce( + C, + ReadableStreamDefaultReaderOwned<'js>, + ) -> Result<(C, ReadableStreamDefaultReaderOwned<'js>)>, + _: impl FnOnce( + C, + ReadableStreamBYOBReaderOwned<'js>, + ) -> Result<(C, ReadableStreamBYOBReaderOwned<'js>)>, + _: impl FnOnce(C) -> Result, + ) -> Result<(C, Self)> { + default(ctx, self) + } + + fn into_inner(self) -> Self::Class { + self.into_inner() + } + + fn from_class(class: Self::Class) -> Self { + OwnedBorrowMut::from_class(class) + } + + fn try_from_erased(erased: Option>) -> Option { + match erased { + Some(ReadableStreamReaderOwned::ReadableStreamDefaultReader(r)) => Some(r), + _ => None, + } + } +} + +/// Check if the stream's controller has a native_pull fast-path available. +fn try_get_native_pull<'js>( + stream_class: &Class<'js, ReadableStream<'js>>, +) -> Option> { + let stream = stream_class.borrow(); + + // Early exit if state is not Readable + if !matches!(stream.state, ReadableStreamState::Readable) { + return None; + } + + let ReadableStreamControllerClass::ReadableStreamDefaultController(ctrl) = &stream.controller + else { + return None; + }; + + let ctrl = ctrl.borrow(); + + if ctrl.container.queue.is_empty() && !ctrl.pulling { + ctrl.native_pull.clone() + } else { + None + } +} + +/// Read using the native_pull fast-path, bypassing the full spec algorithm. +fn read_native<'js>( + ctx: &Ctx<'js>, + np: &NativePull<'js>, + primordials: &PromisePrimordials<'js>, +) -> Result> { + match (np.0)(ctx)? { + // Synchronous data — wrap in a resolved promise to satisfy the spec + // (reader.read() must always return a Promise). + NativePullResult::Ready(chunk) => { + let result = ReadableStreamReadResult { + value: Some(chunk), + done: false, + } + .into_js(ctx)?; + promise_resolved_with(ctx, primordials, Ok(result)).map(|p| p.into_value()) + } + NativePullResult::Eof => { + let result = ReadableStreamReadResult { + value: None, + done: true, + } + .into_js(ctx)?; + promise_resolved_with(ctx, primordials, Ok(result)).map(|p| p.into_value()) + } + // Async data — must return a promise + NativePullResult::Pending(fut) => { + let promise = Promise::wrap_future(ctx, async move { + fut.await.map(|chunk| ReadableStreamReadResult { + done: chunk.is_none(), + value: chunk, + }) + })?; + Ok(promise.into_value()) + } + } +} + +/// Read using the standard spec algorithm (ReadableStreamDefaultReaderRead). +fn read_default<'js>( + ctx: &Ctx<'js>, + reader: OwnedBorrowMut<'js, ReadableStreamDefaultReader<'js>>, +) -> Result> { + let objects = ReadableStreamObjects::from_default_reader(reader); + // Let promise be a new promise. + let promise = ResolveablePromise::new(ctx)?; + + // Let readRequest be a new read request with the following items: + #[derive(Trace)] + struct ReadRequest<'js> { + promise: ResolveablePromise<'js>, + } + + impl<'js> ReadableStreamReadRequest<'js> for ReadRequest<'js> { + // chunk steps, given chunk + // Resolve promise with «[ "value" → chunk, "done" → false ]». + fn chunk_steps( + &self, + objects: ReadableStreamDefaultReaderObjects<'js>, + chunk: Value<'js>, + ) -> Result> { + self.promise.resolve(ReadableStreamReadResult { + value: Some(chunk), + done: false, + })?; + Ok(objects) + } + + // close steps + // Resolve promise with «[ "value" → undefined, "done" → true ]». + fn close_steps( + &self, + _: &Ctx<'js>, + objects: ReadableStreamDefaultReaderObjects<'js>, + ) -> Result> { + self.promise.resolve(ReadableStreamReadResult { + value: None, + done: true, + })?; + Ok(objects) + } + + fn error_steps( + &self, + objects: ReadableStreamDefaultReaderObjects<'js>, + e: Value<'js>, + ) -> Result> { + self.promise.reject(e)?; + Ok(objects) + } + } + + // Perform ! ReadableStreamDefaultReaderRead(this, readRequest). + ReadableStreamDefaultReader::readable_stream_default_reader_read( + ctx, + objects, + ReadRequest { + promise: promise.clone(), + }, + )?; + + // Return promise. + Ok(promise.promise.into_value()) +} + +pub(crate) trait ReadableStreamDefaultReaderOrUndefined<'js>: + ReadableStreamReader<'js> +{ +} + +impl<'js> ReadableStreamDefaultReaderOrUndefined<'js> for ReadableStreamDefaultReaderOwned<'js> {} + +impl<'js> ReadableStreamDefaultReaderOrUndefined<'js> + for Option> +{ +} + +impl ReadableStreamDefaultReaderOrUndefined<'_> for UndefinedReader {} + +pub(crate) trait ReadableStreamReadRequest<'js>: Trace<'js> { + fn chunk_steps_typed>( + &self, + objects: ReadableStreamDefaultReaderObjects<'js, C>, + chunk: Value<'js>, + ) -> Result> + where + Self: Sized, + { + let mut erased = ReadableStreamObjects { + stream: objects.stream, + controller: objects.controller.into_erased(), + reader: objects.reader, + }; + + erased = self.chunk_steps(erased, chunk)?; + + Ok(ReadableStreamObjects { + stream: erased.stream, + controller: C::try_from_erased(erased.controller) + .expect("chunk steps must not change type of controller"), + reader: erased.reader, + }) + } + + fn chunk_steps( + &self, + objects: ReadableStreamDefaultReaderObjects<'js>, + chunk: Value<'js>, + ) -> Result>; + + fn close_steps_typed>( + &self, + ctx: &Ctx<'js>, + objects: ReadableStreamDefaultReaderObjects<'js, C>, + ) -> Result> + where + Self: Sized, + { + let mut erased = ReadableStreamObjects { + stream: objects.stream, + controller: objects.controller.into_erased(), + reader: objects.reader, + }; + + erased = self.close_steps(ctx, erased)?; + + Ok(ReadableStreamObjects { + stream: erased.stream, + controller: C::try_from_erased(erased.controller) + .expect("close steps must not change type of controller"), + reader: erased.reader, + }) + } + + fn close_steps( + &self, + ctx: &Ctx<'js>, + objects: ReadableStreamDefaultReaderObjects<'js>, + ) -> Result>; + + fn error_steps_typed>( + &self, + objects: ReadableStreamDefaultReaderObjects<'js, C>, + reason: Value<'js>, + ) -> Result> + where + Self: Sized, + { + let mut erased = ReadableStreamObjects { + stream: objects.stream, + controller: objects.controller.into_erased(), + reader: objects.reader, + }; + + erased = self.error_steps(erased, reason)?; + + Ok(ReadableStreamObjects { + stream: erased.stream, + controller: C::try_from_erased(erased.controller) + .expect("error steps must not change type of controller"), + reader: erased.reader, + }) + } + + fn error_steps( + &self, + objects: ReadableStreamDefaultReaderObjects<'js>, + reason: Value<'js>, + ) -> Result>; +} + +impl<'js> Trace<'js> for Box + 'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + self.as_ref().trace(tracer); + } +} + +impl<'js> ReadableStreamReadRequest<'js> for Box + 'js> { + fn chunk_steps( + &self, + objects: ReadableStreamDefaultReaderObjects<'js>, + chunk: Value<'js>, + ) -> Result> { + self.as_ref().chunk_steps(objects, chunk) + } + + fn close_steps( + &self, + ctx: &Ctx<'js>, + objects: ReadableStreamDefaultReaderObjects<'js>, + ) -> Result> { + self.as_ref().close_steps(ctx, objects) + } + + fn error_steps( + &self, + objects: ReadableStreamDefaultReaderObjects<'js>, + reason: Value<'js>, + ) -> Result> { + self.as_ref().error_steps(objects, reason) + } +} + +pub(super) struct ReadableStreamReadResult<'js> { + pub(super) value: Option>, + pub(super) done: bool, +} + +impl<'js> IntoJs<'js> for ReadableStreamReadResult<'js> { + fn into_js(self, ctx: &Ctx<'js>) -> Result> { + let obj = Object::new(ctx.clone())?; + obj.set(PredefinedAtom::Value, self.value)?; + obj.set("done", self.done)?; + Ok(obj.into_value()) + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/iterator.rs b/stdlib/src/llrt/llrt_stream_web/readable/iterator.rs new file mode 100644 index 00000000..ca8a9f2f --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/readable/iterator.rs @@ -0,0 +1,698 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use std::{ + rc::Rc, + sync::atomic::{AtomicBool, Ordering}, +}; + +use crate::llrt_utils::primordials::{BasePrimordials, Primordial}; +use rquickjs::{ + atom::PredefinedAtom, + class::{ + impl_::{CloneTrait, CloneWrapper}, + JsClass, OwnedBorrow, OwnedBorrowMut, Trace, Tracer, + }, + function::Constructor, + methods, + prelude::{Opt, This}, + Class, Coerced, Ctx, Error, Exception, FromJs, Function, IntoAtom, IntoJs, JsLifetime, Object, + Promise, Result, Symbol, Type, Value, +}; + +use crate::llrt_stream_web::{ + readable::{ + controller::ReadableStreamControllerOwned, + default_reader::{ + ReadableStreamDefaultReader, ReadableStreamDefaultReaderOwned, + ReadableStreamReadRequest, ReadableStreamReadResult, + }, + objects::{ + ReadableStreamClassObjects, ReadableStreamDefaultReaderObjects, ReadableStreamObjects, + }, + reader::ReadableStreamGenericReader, + }, + utils::{ + class_from_owned_borrow_mut, + promise::{promise_resolved_with, PromisePrimordials}, + promise::{upon_promise, upon_promise_fulfilment, ResolveablePromise}, + UnwrapOrUndefined, + }, +}; + +pub(super) enum IteratorKind { + Async, +} + +#[derive(Trace)] +pub(super) struct IteratorRecord<'js> { + pub(super) iterator: Object<'js>, + next_method: Function<'js>, + #[qjs(skip_trace)] + done: AtomicBool, + sync_to_async_iterator: Function<'js>, +} + +impl<'js> IteratorRecord<'js> { + pub(super) fn get_iterator( + ctx: &Ctx<'js>, + obj: Value<'js>, + kind: IteratorKind, + ) -> Result { + let method: Option> = match kind { + // If kind is async, then + IteratorKind::Async => { + // Let method be ? GetMethod(obj, %Symbol.asyncIterator%). + let method = get_method(ctx, obj.clone(), Symbol::async_iterator(ctx.clone()))?; + // If method is undefined, then + if method.is_none() { + // Let syncMethod be ? GetMethod(obj, %Symbol.iterator%). + let sync_method = get_method(ctx, obj.clone(), Symbol::iterator(ctx.clone()))?; + + // If syncMethod is undefined, throw a TypeError exception. + let sync_method = match sync_method { + None => { + return Err(Exception::throw_type(ctx, "Object is not an iterator")); + } + Some(sync_method) => sync_method, + }; + + // Let syncIteratorRecord be ? GetIteratorFromMethod(obj, syncMethod). + let sync_iterator_record = + Self::get_iterator_from_method(ctx, &obj, sync_method)?; + + // Return CreateAsyncFromSyncIterator(syncIteratorRecord). + return sync_iterator_record.create_async_from_sync_iterator(ctx); + } + + method + } + }; + + // If method is undefined, throw a TypeError exception. + match method { + None => Err(Exception::throw_type(ctx, "Object is not an iterator")), + Some(method) => { + // Return ? GetIteratorFromMethod(obj, method). + Self::get_iterator_from_method(ctx, &obj, method) + } + } + } + + fn get_iterator_from_method( + ctx: &Ctx<'js>, + obj: &Value<'js>, + method: Function<'js>, + ) -> Result { + // Let iterator be ? Call(method, obj). + let iterator: Value<'js> = method.call((This(obj),))?; + let iterator = match iterator.into_object() { + Some(iterator) => iterator, + None => { + return Err(Exception::throw_type( + ctx, + "The iterator method must return an object", + )); + } + }; + // Let nextMethod be ? Get(iterator, "next"). + let next_method = iterator.get(PredefinedAtom::Next)?; + // Let iteratorRecord be the Iterator Record { [[Iterator]]: iterator, [[NextMethod]]: nextMethod, [[Done]]: false }. + // Return iteratorRecord. + Ok(Self { + iterator, + next_method, + done: AtomicBool::new(false), + sync_to_async_iterator: IteratorPrimordials::get(ctx)? + .sync_to_async_iterator + .clone(), + }) + } + + fn create_async_from_sync_iterator(self, ctx: &Ctx<'js>) -> Result { + let sync_iterable = Object::new(ctx.clone())?; + sync_iterable.set( + Symbol::iterator(ctx.clone()), + Function::new(ctx.clone(), { + let iterator = self.iterator.clone(); + move || iterator.clone() + }), + )?; + + let async_iterator: Object<'js> = self.sync_to_async_iterator.call((sync_iterable,))?; + + let next_method = async_iterator.get(PredefinedAtom::Next)?; + + Ok(Self { + iterator: async_iterator, + next_method, + done: AtomicBool::new(false), + sync_to_async_iterator: self.sync_to_async_iterator, + }) + } + + pub(super) fn iterator_next( + &self, + ctx: &Ctx<'js>, + value: Option>, + ) -> Result> { + let result: Result> = match value { + // If value is not present, then + None => { + // Let result be Completion(Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]])). + + self.next_method.call((This(self.iterator.clone()),)) + } + // Else, + Some(value) => { + // Let result be Completion(Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]], « value »)). + self.next_method.call((This(self.iterator.clone()), value)) + } + }; + + let result = match result { + // If result is a throw completion, then + Err(Error::Exception) => { + // Set iteratorRecord.[[Done]] to true. + self.done.store(true, Ordering::Release); + // Return ? result. + return Err(Error::Exception); + } + Err(err) => return Err(err), + // Set result to ! result. + Ok(result) => result, + }; + + let result = match result.into_object() { + // If result is not an Object, then + None => { + // Set iteratorRecord.[[Done]] to true. + self.done.store(true, Ordering::Release); + return Err(Exception::throw_type( + ctx, + "The iterator.next() method must return an object", + )); + } + Some(result) => result, + }; + // Return result. + Ok(result) + } + + pub(super) fn iterator_complete(iterator_result: &Object<'js>) -> Result { + let done: Coerced = iterator_result.get(PredefinedAtom::Done)?; + Ok(done.0) + } + + pub(super) fn iterator_value(iterator_result: &Object<'js>) -> Result> { + iterator_result.get(PredefinedAtom::Value) + } +} + +pub(super) struct ReadableStreamAsyncIterator<'js> { + objects: ReadableStreamClassObjects< + 'js, + ReadableStreamControllerOwned<'js>, + ReadableStreamDefaultReaderOwned<'js>, + >, + prevent_cancel: bool, + is_finished: Rc, + ongoing_promise: Option>, + + promise_primordials: PromisePrimordials<'js>, + end_of_iteration: Symbol<'js>, +} + +impl<'js> Trace<'js> for ReadableStreamAsyncIterator<'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + Trace::<'js>::trace(&self.objects, tracer); + if let Some(ongoing_promise) = &self.ongoing_promise { + ongoing_promise.trace(tracer); + } + Trace::<'js>::trace(&self.end_of_iteration, tracer); + } +} + +unsafe impl<'js> JsLifetime<'js> for ReadableStreamAsyncIterator<'js> { + type Changed<'to> = ReadableStreamAsyncIterator<'to>; +} + +impl<'js> ReadableStreamAsyncIterator<'js> { + pub(super) fn new( + ctx: Ctx<'js>, + objects: ReadableStreamClassObjects< + 'js, + ReadableStreamControllerOwned<'js>, + ReadableStreamDefaultReaderOwned<'js>, + >, + promise_primordials: PromisePrimordials<'js>, + prevent_cancel: bool, + ) -> Result> { + let end_of_iteration = IteratorPrimordials::get(&ctx)?.end_of_iteration.clone(); + + Class::instance( + ctx, + Self { + objects, + prevent_cancel, + is_finished: Rc::new(AtomicBool::new(false)), + ongoing_promise: None, + promise_primordials, + end_of_iteration, + }, + ) + } +} + +// Custom JsClass implementation needed until prototype, function names and function lengths can be influenced in the class derivation macro +impl<'js> JsClass<'js> for ReadableStreamAsyncIterator<'js> { + const NAME: &'static str = "ReadableStreamAsyncIterator"; + type Mutable = rquickjs::class::Writable; + fn prototype(ctx: &Ctx<'js>) -> Result>> { + use rquickjs::class::impl_::MethodImplementor; + let proto = Object::new(ctx.clone())?; + let primordial = IteratorPrimordials::get(ctx)?; + proto.set_prototype(Some(&primordial.async_iterator_prototype))?; + let implementor = rquickjs::class::impl_::MethodImpl::::new(); + implementor.implement(&proto)?; + let next_fn: Function<'js> = proto.get("next")?; + // yup, the wpt tests really do check these. + next_fn.set_name("next")?; + let return_fn: Function<'js> = proto.get("return")?; + return_fn.set_name("return")?; + return_fn.set_length(1)?; + // Make `next` and `return` enumerable per WebIDL (rquickjs defaults to + // non-enumerable, but the async-iterator.any.js WPT tests check this). + let define_property: Function<'js> = ctx + .globals() + .get::<_, Object<'js>>("Object")? + .get("defineProperty")?; + for name in ["next", "return"] { + let value: Value<'js> = proto.get(name)?; + let desc = Object::new(ctx.clone())?; + desc.set("value", value)?; + desc.set("writable", true)?; + desc.set("enumerable", true)?; + desc.set("configurable", true)?; + define_property.call::<_, ()>((proto.clone(), name, desc))?; + } + Ok(Some(proto)) + } + fn constructor(ctx: &Ctx<'js>) -> Result>> { + use rquickjs::class::impl_::ConstructorCreator; + let implementor = rquickjs::class::impl_::ConstructorCreate::::new(); + (&implementor).create_constructor(ctx) + } +} +impl<'js> IntoJs<'js> for ReadableStreamAsyncIterator<'js> { + fn into_js(self, ctx: &Ctx<'js>) -> Result> { + let cls = Class::::instance(ctx.clone(), self)?; + IntoJs::into_js(cls, ctx) + } +} + +impl<'js> FromJs<'js> for ReadableStreamAsyncIterator<'js> +where + for<'a> CloneWrapper<'a, Self>: CloneTrait, +{ + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + use rquickjs::class::impl_::CloneTrait; + let value = Class::::from_js(ctx, value)?; + let borrow = value.try_borrow()?; + Ok(CloneWrapper(&*borrow).wrap_clone()) + } +} + +#[methods] +impl<'js> ReadableStreamAsyncIterator<'js> { + fn next(ctx: Ctx<'js>, iterator: This>) -> Result> { + let is_finished = iterator.is_finished.clone(); + + let next_steps = move |ctx: Ctx<'js>, iterator: &Self, iterator_class: Class<'js, Self>| { + if is_finished.load(Ordering::Acquire) { + return promise_resolved_with( + &ctx, + &iterator.promise_primordials, + Ok(ReadableStreamReadResult { + value: None, + done: true, + } + .into_js(&ctx)?), + ); + } + + let next_promise = Self::next_steps(&ctx, iterator)?; + + upon_promise( + ctx, + next_promise, + move |ctx, result: std::result::Result, _>| { + let mut iterator = OwnedBorrowMut::from_class(iterator_class); + match result { + Ok(next) => { + iterator.ongoing_promise = None; + if next.as_symbol() == Some(&iterator.end_of_iteration) { + iterator.is_finished.store(true, Ordering::Release); + Ok(ReadableStreamReadResult { + value: None, + done: true, + }) + } else { + Ok(ReadableStreamReadResult { + value: Some(next), + done: false, + }) + } + } + Err(reason) => { + iterator.ongoing_promise = None; + iterator.is_finished.store(true, Ordering::Release); + Err(ctx.throw(reason)) + } + } + }, + ) + }; + + let (iterator_class, mut iterator) = class_from_owned_borrow_mut(iterator.0); + let ongoing_promise = iterator.ongoing_promise.take(); + + let ongoing_promise = match ongoing_promise { + Some(ongoing_promise) => upon_promise( + ctx, + ongoing_promise, + move |ctx, _: std::result::Result, _>| { + let iterator = OwnedBorrow::from_class(iterator_class.clone()); + next_steps(ctx, &iterator, iterator_class) + }, + )?, + None => next_steps(ctx, &iterator, iterator_class)?, + }; + + Ok(iterator.ongoing_promise.insert(ongoing_promise).clone()) + } + + #[qjs(rename = "return")] + fn r#return( + ctx: Ctx<'js>, + iterator: This>, + value: Opt>, + ) -> Result> { + let is_finished = iterator.is_finished.clone(); + let value = value.0.unwrap_or_undefined(&ctx); + + let return_steps = { + let value = value.clone(); + move |ctx: Ctx<'js>, iterator: &Self| { + if is_finished.swap(true, Ordering::AcqRel) { + return promise_resolved_with( + &ctx, + &iterator.promise_primordials, + Ok(ReadableStreamReadResult { + value: Some(value), + done: true, + } + .into_js(&ctx)?), + ); + } + + Self::return_steps(ctx.clone(), iterator, value) + } + }; + + let (iterator_class, mut iterator) = class_from_owned_borrow_mut(iterator.0); + let ongoing_promise = iterator.ongoing_promise.take(); + + let ongoing_promise = match ongoing_promise { + Some(ongoing_promise) => upon_promise( + ctx.clone(), + ongoing_promise, + move |ctx, _: std::result::Result, _>| { + let iterator = OwnedBorrow::from_class(iterator_class.clone()); + return_steps(ctx, &iterator) + }, + )?, + None => return_steps(ctx.clone(), &iterator)?, + }; + + iterator.ongoing_promise = Some(ongoing_promise.clone()); + + upon_promise_fulfilment(ctx, ongoing_promise, move |_, ()| { + Ok(ReadableStreamReadResult { + value: Some(value), + done: true, + }) + }) + } +} + +impl<'js> ReadableStreamAsyncIterator<'js> { + // The get the next iteration result steps for a ReadableStream, given stream and iterator, are: + fn next_steps(ctx: &Ctx<'js>, iterator: &Self) -> Result> { + // Let reader be iterator’s reader. + let objects = iterator.objects.clone(); + + // Let promise be a new promise. + let promise = ResolveablePromise::new(ctx)?; + + // Let readRequest be a new read request with the following items: + #[derive(Trace)] + struct ReadRequest<'js> { + promise: ResolveablePromise<'js>, + end_of_iteration: Symbol<'js>, + } + + impl<'js> ReadableStreamReadRequest<'js> for ReadRequest<'js> { + fn chunk_steps( + &self, + objects: ReadableStreamDefaultReaderObjects<'js>, + chunk: Value<'js>, + ) -> Result> { + // Resolve promise with chunk. + self.promise.resolve(chunk)?; + Ok(objects) + } + + fn close_steps( + &self, + _ctx: &Ctx<'js>, + mut objects: ReadableStreamDefaultReaderObjects<'js>, + ) -> Result> { + // Perform ! ReadableStreamDefaultReaderRelease(reader). + objects = + ReadableStreamDefaultReader::readable_stream_default_reader_release(objects)?; + + // Resolve promise with end of iteration. + self.promise.resolve(self.end_of_iteration.clone())?; + Ok(objects) + } + + fn error_steps( + &self, + mut objects: ReadableStreamDefaultReaderObjects<'js>, + reason: Value<'js>, + ) -> Result> { + // Perform ! ReadableStreamDefaultReaderRelease(reader). + objects = + ReadableStreamDefaultReader::readable_stream_default_reader_release(objects)?; + + // Reject promise with e. + self.promise.reject(reason)?; + Ok(objects) + } + } + + let objects = ReadableStreamObjects::from_class(objects); + + // Perform ! ReadableStreamDefaultReaderRead(this, readRequest). + ReadableStreamDefaultReader::readable_stream_default_reader_read( + ctx, + objects, + ReadRequest { + promise: promise.clone(), + end_of_iteration: iterator.end_of_iteration.clone(), + }, + )?; + + // Return promise. + Ok(promise.promise) + } + + // The asynchronous iterator return steps for a ReadableStream, given stream, iterator, and arg, are: + fn return_steps(ctx: Ctx<'js>, iterator: &Self, arg: Value<'js>) -> Result> { + // Let reader be iterator’s reader. + let objects = ReadableStreamObjects::from_class(iterator.objects.clone()); + + // If iterator’s prevent cancel is false: + if !iterator.prevent_cancel { + // Let result be ! ReadableStreamReaderGenericCancel(reader, arg). + let (result, objects) = + ReadableStreamGenericReader::readable_stream_reader_generic_cancel( + ctx.clone(), + objects, + arg, + )?; + + // Perform ! ReadableStreamDefaultReaderRelease(reader). + ReadableStreamDefaultReader::readable_stream_default_reader_release(objects)?; + + // Return result. + return Ok(result); + } + + // Perform ! ReadableStreamDefaultReaderRelease(reader). + ReadableStreamDefaultReader::readable_stream_default_reader_release(objects)?; + + // Return a promise resolved with undefined. + Ok(iterator + .promise_primordials + .promise_resolved_with_undefined + .clone()) + } +} + +#[derive(Clone, JsLifetime, Trace)] +pub(crate) struct IteratorPrimordials<'js> { + end_of_iteration: Symbol<'js>, + sync_to_async_iterator: Function<'js>, + async_iterator_prototype: Object<'js>, +} + +impl<'js> Primordial<'js> for IteratorPrimordials<'js> { + fn new(ctx: &Ctx<'js>) -> Result + where + Self: Sized, + { + let sync_to_async_iterator = ctx.eval::, _>( + r#" + (syncIterable) => (async function* () { + return yield* syncIterable; + })() + "#, + )?; + + // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncIterator + // ```js + // const AsyncIteratorPrototype = Object.getPrototypeOf( + // Object.getPrototypeOf(Object.getPrototypeOf((async function* () {})())), + // ); + // ``` + let async_iterator_prototype = ctx + .eval::, _>("(async function* () {})()")? + .get_prototype() + .as_ref() + .and_then(Object::get_prototype) + .as_ref() + .and_then(Object::get_prototype) + .expect("async iterator prototype not found"); + + Ok(Self { + end_of_iteration: Symbol::new_global(ctx.clone(), "async iterator end of iteration")?, + sync_to_async_iterator, + async_iterator_prototype, + }) + } +} + +// https://tc39.es/ecma262/multipage/abstract-operations.html#sec-getmethod +fn get_method<'js>( + ctx: &Ctx<'js>, + value: Value<'js>, + property: impl IntoAtom<'js>, +) -> Result>> { + // 1. Let func be ? GetV(V, P). + let func = get_v(ctx, value, property)?; + + // 2. If func is either undefined or null, return undefined. + if func.is_undefined() || func.is_null() { + return Ok(None); + } + + match func.into_function() { + // 3. If IsCallable(func) is false, throw a TypeError exception. + None => Err(Exception::throw_type(ctx, "not a function")), + // 4. Return func. + Some(func) => Ok(Some(func)), + } +} + +// https://tc39.es/ecma262/multipage/abstract-operations.html#sec-getv +fn get_v<'js>( + ctx: &Ctx<'js>, + value: Value<'js>, + property: impl IntoAtom<'js>, +) -> Result> { + // 1. Let O be ? ToObject(V). + let o: Object<'js> = to_object(ctx, value)?; + + // 2. Return ? O.[[Get]](P, V). + o.get(property) +} + +// https://tc39.es/ecma262/multipage/abstract-operations.html#sec-toobject +fn to_object<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> Result> { + let base_primordials = BasePrimordials::get(ctx)?; + + match value.type_of() { + // Return a new Boolean object whose [[BooleanData]] internal slot is set to argument + Type::Bool => base_primordials.constructor_bool.construct((value,))?, + // Return a new Number object whose [[NumberData]] internal slot is set to argument + Type::Int | Type::Float => base_primordials.constructor_number.construct((value,))?, + // Return a new String object whose [[StringData]] internal slot is set to argument + Type::String => base_primordials.constructor_string.construct((value,))?, + // Return a new Symbol object whose [[SymbolData]] internal slot is set to argument + // `new Symbol` is invalid but we can use `Object(symbol) + Type::Symbol => base_primordials.constructor_object.call((value,))?, + // Return a new BigInt object whose [[BigIntData]] internal slot is set to argument + // `new BigInt` is invalid but we can use `Object(bigInt) + Type::BigInt => base_primordials.constructor_object.call((value,))?, + // Return argument + typ if typ.interpretable_as(Type::Object) => Ok(value.into_object().unwrap()), + // Throw a TypeError exception. + typ => Err(Exception::throw_type( + ctx, + &format!("{typ} cannot be converted to an object"), + )), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::llrt_test::test_sync_with; + use rquickjs::BigInt; + + #[tokio::test] + async fn test_to_object() { + test_sync_with(|ctx| { + BasePrimordials::init(&ctx)?; + let good_values: [Value; 7] = [ + Value::new_bool(ctx.clone(), false), + Value::new_int(ctx.clone(), 123), + Value::new_float(ctx.clone(), 1.5), + rquickjs::String::from_str(ctx.clone(), "abc")?.into_value(), + Symbol::new_global(ctx.clone(), "def")?.into_value(), + BigInt::from_i64(ctx.clone(), 123456)?.into_value(), + Object::new(ctx.clone())?.into_value(), + ]; + + for value in good_values { + to_object(&ctx, value)?; + } + + let bad_values: [Value; 3] = [ + Value::new_uninitialized(ctx.clone()), + Value::new_undefined(ctx.clone()), + Value::new_null(ctx.clone()), + ]; + + for value in bad_values { + let ty = value.type_of(); + if to_object(&ctx, value).is_ok() { + panic!("Values of type {ty} should not be convertible to object") + } + } + + Ok(()) + }) + .await; + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/mod.rs b/stdlib/src/llrt/llrt_stream_web/readable/mod.rs new file mode 100644 index 00000000..aefc0078 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/readable/mod.rs @@ -0,0 +1,31 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +mod byob_reader; +mod byte_controller; +mod controller; +mod default_controller; +mod default_reader; +mod iterator; +mod objects; +mod reader; +pub mod stream; + +pub(crate) use byob_reader::{ArrayConstructorPrimordials, ReadableStreamBYOBReader}; +pub use byte_controller::ReadableByteStreamController; +pub(crate) use byte_controller::ReadableStreamBYOBRequest; +pub use byte_controller::{ + readable_byte_stream_controller_close_stream, readable_byte_stream_controller_enqueue_bytes, + readable_byte_stream_controller_enqueue_bytes_borrowed, ReadableByteStreamControllerClass, +}; +pub(crate) use default_controller::ReadableStreamDefaultController; +pub use default_controller::{ + readable_stream_default_controller_close_stream, + readable_stream_default_controller_enqueue_value, + readable_stream_default_controller_error_stream, NativePull, NativePullFn, NativePullResult, + ReadableStreamDefaultControllerClass, +}; +pub(crate) use default_reader::ReadableStreamDefaultReader; +pub(crate) use iterator::IteratorPrimordials; +pub(crate) use stream::ReadableStreamClass; + +pub use controller::ReadableStreamControllerClass; +pub use stream::{CancelAlgorithm, PullAlgorithm, StartAlgorithm}; diff --git a/stdlib/src/llrt/llrt_stream_web/readable/objects.rs b/stdlib/src/llrt/llrt_stream_web/readable/objects.rs new file mode 100644 index 00000000..9b626f7c --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/readable/objects.rs @@ -0,0 +1,459 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use rquickjs::{ + class::{OwnedBorrowMut, Trace, Tracer}, + Result, +}; + +use crate::llrt_stream_web::readable::{ + byob_reader::ReadableStreamBYOBReaderOwned, + byte_controller::ReadableByteStreamControllerOwned, + controller::{ + ReadableStreamController, ReadableStreamControllerClass, ReadableStreamControllerOwned, + }, + default_controller::ReadableStreamDefaultControllerOwned, + default_reader::{ReadableStreamDefaultReaderOrUndefined, ReadableStreamDefaultReaderOwned}, + reader::{ReadableStreamReader, ReadableStreamReaderOwned, UndefinedReader}, + stream::{ReadableStream, ReadableStreamClass, ReadableStreamOwned}, +}; + +pub(crate) struct ReadableStreamObjects<'js, C, R> { + pub(super) stream: ReadableStreamOwned<'js>, + pub(super) controller: C, + pub(super) reader: R, +} + +pub(super) type ReadableStreamDefaultControllerObjects<'js, R> = + ReadableStreamObjects<'js, ReadableStreamDefaultControllerOwned<'js>, R>; +pub(super) type ReadableStreamDefaultReaderObjects<'js, C = ReadableStreamControllerOwned<'js>> = + ReadableStreamObjects<'js, C, ReadableStreamDefaultReaderOwned<'js>>; +pub(super) type ReadableByteStreamObjects<'js, R> = + ReadableStreamObjects<'js, ReadableByteStreamControllerOwned<'js>, R>; +pub(super) type ReadableStreamBYOBObjects<'js> = ReadableStreamObjects< + 'js, + ReadableByteStreamControllerOwned<'js>, + ReadableStreamBYOBReaderOwned<'js>, +>; + +pub(crate) struct ReadableStreamClassObjects< + 'js, + C: ReadableStreamController<'js>, + R: ReadableStreamReader<'js>, +> { + pub(crate) stream: ReadableStreamClass<'js>, + pub(super) controller: C::Class, + pub(super) reader: R::Class, +} + +// derive(Clone) isn't clever enough to figure out that C and R don't need to implement Clone, but only C::Class and R::Class. +impl<'js, C: ReadableStreamController<'js>, R: ReadableStreamReader<'js>> Clone + for ReadableStreamClassObjects<'js, C, R> +{ + fn clone(&self) -> Self { + Self { + stream: self.stream.clone(), + controller: self.controller.clone(), + reader: self.reader.clone(), + } + } +} + +// derive(Trace) isn't clever enough to figure out that C and R don't need to implement Trace, but only C::Class and R::Class. +impl<'js, C: ReadableStreamController<'js>, R: ReadableStreamReader<'js>> Trace<'js> + for ReadableStreamClassObjects<'js, C, R> +{ + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + self.stream.trace(tracer); + self.controller.trace(tracer); + self.reader.trace(tracer); + } +} + +impl<'js, C: ReadableStreamController<'js>, R: ReadableStreamReader<'js>> + ReadableStreamClassObjects<'js, C, R> +{ + pub(super) fn set_reader>( + self, + reader: RNext::Class, + ) -> ReadableStreamClassObjects<'js, C, RNext> { + drop(self.reader); + ReadableStreamClassObjects { + stream: self.stream, + controller: self.controller, + reader, + } + } +} + +impl<'js, C: ReadableStreamController<'js>, R: ReadableStreamReader<'js>> + ReadableStreamObjects<'js, C, R> +{ + pub(super) fn with_assert_default_controller( + mut self, + f: impl FnOnce( + ReadableStreamDefaultControllerObjects<'js, R>, + ) -> Result>, + ) -> Result { + ((), self) = self.with_controller( + (), + |(), controller| Ok(((), f(controller)?)), + |_, _| panic!("expected default controller, found byte controller"), + )?; + Ok(self) + } + + pub(super) fn with_assert_byte_controller( + mut self, + f: impl FnOnce(ReadableByteStreamObjects<'js, R>) -> Result>, + ) -> Result { + ((), self) = self.with_controller( + (), + |_, _| panic!("expected byte controller, found default controller"), + |(), controller| Ok(((), f(controller)?)), + )?; + Ok(self) + } + + pub(super) fn with_controller( + self, + ctx: Ctx, + default: impl FnOnce( + Ctx, + ReadableStreamDefaultControllerObjects<'js, R>, + ) -> Result<(O, ReadableStreamDefaultControllerObjects<'js, R>)>, + byte: impl FnOnce( + Ctx, + ReadableByteStreamObjects<'js, R>, + ) -> Result<(O, ReadableByteStreamObjects<'js, R>)>, + ) -> Result<(O, Self)> { + let ((out, stream, reader), controller) = self.controller.with_controller( + (ctx, self.stream, self.reader), + |(ctx, stream, reader), controller| { + let (out, objects) = default( + ctx, + ReadableStreamObjects { + stream, + controller, + reader, + }, + )?; + + Ok(((out, objects.stream, objects.reader), objects.controller)) + }, + |(ctx, stream, reader), controller| { + let (out, objects) = byte( + ctx, + ReadableStreamObjects { + stream, + controller, + reader, + }, + )?; + + Ok(((out, objects.stream, objects.reader), objects.controller)) + }, + )?; + + Ok(( + out, + Self { + stream, + controller, + reader, + }, + )) + } + + pub(super) fn with_assert_byob_reader( + self, + f: impl FnOnce(ReadableStreamBYOBObjects<'js>) -> Result>, + ) -> Result { + self.with_reader( + |_| panic!("expected byob reader, found default reader"), + f, + |_| panic!("expected byob reader, found no reader"), + ) + } + + pub(super) fn with_assert_default_reader( + self, + f: impl FnOnce( + ReadableStreamDefaultReaderObjects<'js, C>, + ) -> Result>, + ) -> Result { + self.with_reader( + f, + |_| panic!("expected default reader, found byob reader"), + |_| panic!("expected default reader, found no reader"), + ) + } + + pub(super) fn with_reader( + mut self, + default: impl FnOnce( + ReadableStreamDefaultReaderObjects<'js, C>, + ) -> Result>, + byob: impl FnOnce(ReadableStreamBYOBObjects<'js>) -> Result>, + none: impl FnOnce( + ReadableStreamObjects<'js, C, UndefinedReader>, + ) -> Result>, + ) -> Result { + ((self.stream, self.controller), self.reader) = self.reader.with_reader( + (self.stream, self.controller), + |(stream, controller), reader| { + let objects = default(ReadableStreamObjects { + stream, + controller, + reader, + })?; + + Ok(((objects.stream, objects.controller), objects.reader)) + }, + |(mut stream, mut controller), mut reader| { + ((stream, reader), controller) = controller.with_controller( + (stream, reader), + |_, _| panic!("byob reader must have a byte controller"), + |(stream, reader), controller| { + let objects = byob(ReadableStreamObjects { + stream, + controller, + reader, + })?; + + Ok(((objects.stream, objects.reader), objects.controller)) + }, + )?; + + Ok(((stream, controller), reader)) + }, + |(stream, controller)| { + let objects = none(ReadableStreamObjects { + stream, + controller, + reader: UndefinedReader, + })?; + + Ok((objects.stream, objects.controller)) + }, + )?; + + Ok(self) + } + + pub(super) fn into_inner(self) -> ReadableStreamClassObjects<'js, C, R> { + ReadableStreamClassObjects { + stream: self.stream.into_inner(), + controller: self.controller.into_inner(), + reader: self.reader.into_inner(), + } + } + + pub(super) fn from_class(objects_class: ReadableStreamClassObjects<'js, C, R>) -> Self { + Self { + stream: OwnedBorrowMut::from_class(objects_class.stream), + controller: C::from_class(objects_class.controller), + reader: R::from_class(objects_class.reader), + } + } + + pub(super) fn from_class_no_reader( + objects_class: ReadableStreamClassObjects<'js, C, R>, + ) -> ReadableStreamObjects<'js, C, UndefinedReader> { + ReadableStreamObjects { + stream: OwnedBorrowMut::from_class(objects_class.stream), + controller: C::from_class(objects_class.controller), + reader: UndefinedReader, + } + } + + pub(super) fn clear_reader(self) -> ReadableStreamObjects<'js, C, UndefinedReader> { + drop(self.reader); + ReadableStreamObjects { + stream: self.stream, + controller: self.controller, + reader: UndefinedReader, + } + } +} + +impl<'js> + ReadableStreamDefaultControllerObjects<'js, Option>> +{ + pub(super) fn from_default_controller( + controller: ReadableStreamDefaultControllerOwned<'js>, + ) -> Self { + Self::new_default( + OwnedBorrowMut::from_class(controller.stream.clone()), + controller, + ) + } + + pub(super) fn new_default( + stream: ReadableStreamOwned<'js>, + controller: ReadableStreamDefaultControllerOwned<'js>, + ) -> Self { + ReadableStreamObjects { + stream, + controller, + reader: UndefinedReader, + } + .refresh_reader() + } +} + +impl<'js, R: ReadableStreamReader<'js>> ReadableStreamDefaultControllerObjects<'js, R> { + pub(super) fn refresh_reader( + mut self, + ) -> ReadableStreamDefaultControllerObjects<'js, Option>> + { + drop(self.reader); + let reader = self.stream.reader_mut(); + ReadableStreamObjects { + stream: self.stream, + controller: self.controller, + reader: ReadableStreamReader::try_from_erased(reader) + .expect("default controller must have default reader or no reader"), + } + } +} + +impl<'js> ReadableByteStreamObjects<'js, UndefinedReader> { + pub(super) fn from_byte_controller(controller: ReadableByteStreamControllerOwned<'js>) -> Self { + Self::new_byte( + OwnedBorrowMut::from_class(controller.stream.clone()), + controller, + ) + } + + pub(super) fn new_byte( + stream: ReadableStreamOwned<'js>, + controller: ReadableByteStreamControllerOwned<'js>, + ) -> Self { + ReadableStreamObjects { + stream, + controller, + reader: UndefinedReader, + } + } + + pub(super) fn set_reader>( + self, + reader: RNext, + ) -> ReadableByteStreamObjects<'js, RNext> { + ReadableStreamObjects { + stream: self.stream, + controller: self.controller, + reader, + } + } +} + +impl<'js> ReadableStreamBYOBObjects<'js> { + pub(super) fn from_byob_reader(reader: ReadableStreamBYOBReaderOwned<'js>) -> Self { + let stream = OwnedBorrowMut::from_class( + reader + .generic + .stream + .clone() + .expect("ReadableStreamBYOBReader must have a stream"), + ); + let controller = match &stream.controller { + ReadableStreamControllerClass::ReadableStreamByteController(c) => c.clone(), + _ => panic!("ReadableStreamBYOBReader stream must have byte controller"), + }; + Self { + stream, + controller: OwnedBorrowMut::from_class(controller), + reader, + } + } +} + +impl<'js, R: ReadableStreamReader<'js>> ReadableByteStreamObjects<'js, R> { + pub(super) fn refresh_reader( + mut self, + ) -> ReadableByteStreamObjects<'js, Option>> { + drop(self.reader); + let reader = self.stream.reader_mut(); + ReadableStreamObjects { + stream: self.stream, + controller: self.controller, + reader, + } + } +} + +impl<'js, C: ReadableStreamController<'js>, R: ReadableStreamDefaultReaderOrUndefined<'js>> + ReadableStreamObjects<'js, C, R> +{ + pub(super) fn with_some_reader( + self, + default: impl FnOnce( + ReadableStreamDefaultReaderObjects<'js, C>, + ) -> Result>, + none: impl FnOnce( + ReadableStreamObjects<'js, C, UndefinedReader>, + ) -> Result>, + ) -> Result { + self.with_reader( + default, + |_| panic!("byob reader cannot implement DefaultReaderOrUndefined"), + none, + ) + } +} + +impl<'js> ReadableStreamObjects<'js, ReadableStreamControllerOwned<'js>, UndefinedReader> { + pub(super) fn from_stream(stream: ReadableStreamOwned<'js>) -> Self { + let controller = ReadableStreamControllerOwned::from_class(stream.controller.clone()); + Self::new(stream, controller) + } + + fn new( + stream: OwnedBorrowMut<'js, ReadableStream<'js>>, + controller: ReadableStreamControllerOwned<'js>, + ) -> Self { + ReadableStreamObjects { + stream, + controller, + reader: UndefinedReader, + } + } +} + +impl<'js, R: ReadableStreamReader<'js>> + ReadableStreamObjects<'js, ReadableStreamControllerOwned<'js>, R> +{ + pub(super) fn refresh_reader( + mut self, + ) -> ReadableStreamObjects< + 'js, + ReadableStreamControllerOwned<'js>, + Option>, + > { + drop(self.reader); + let reader = self.stream.reader_mut(); + ReadableStreamObjects { + stream: self.stream, + controller: self.controller, + reader, + } + } +} + +impl<'js> ReadableStreamDefaultReaderObjects<'js> { + pub(super) fn from_default_reader(reader: ReadableStreamDefaultReaderOwned<'js>) -> Self { + let stream = OwnedBorrowMut::from_class( + reader + .generic + .stream + .clone() + .expect("ReadableStreamDefaultReader must have a stream"), + ); + let controller = ReadableStreamControllerOwned::from_class(stream.controller.clone()); + Self { + stream, + controller, + reader, + } + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/reader.rs b/stdlib/src/llrt/llrt_stream_web/readable/reader.rs new file mode 100644 index 00000000..87d34c1f --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/readable/reader.rs @@ -0,0 +1,404 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use rquickjs::{ + class::{OwnedBorrowMut, Trace, Tracer}, + function::Constructor, + Ctx, Error, FromJs, Function, IntoJs, JsLifetime, Promise, Result, Value, +}; + +use crate::llrt_stream_web::{ + readable::{ + byob_reader::ReadableStreamBYOBReader, + byob_reader::{ReadableStreamBYOBReaderClass, ReadableStreamBYOBReaderOwned}, + controller::ReadableStreamController, + default_reader::{ + ReadableStreamDefaultReader, ReadableStreamDefaultReaderClass, + ReadableStreamDefaultReaderOwned, + }, + objects::ReadableStreamObjects, + stream::{ReadableStream, ReadableStreamClass, ReadableStreamOwned, ReadableStreamState}, + }, + utils::promise::{PromisePrimordials, ResolveablePromise}, +}; + +pub(crate) trait ReadableStreamReader<'js>: Sized + 'js { + type Class: Clone + Trace<'js>; + + fn with_reader( + self, + ctx: C, + default: impl FnOnce( + C, + ReadableStreamDefaultReaderOwned<'js>, + ) -> Result<(C, ReadableStreamDefaultReaderOwned<'js>)>, + byob: impl FnOnce( + C, + ReadableStreamBYOBReaderOwned<'js>, + ) -> Result<(C, ReadableStreamBYOBReaderOwned<'js>)>, + none: impl FnOnce(C) -> Result, + ) -> Result<(C, Self)>; + + fn into_inner(self) -> Self::Class; + + fn from_class(class: Self::Class) -> Self; + + fn try_from_erased(erased: Option>) -> Option; +} + +// typedef (ReadableStreamDefaultController or ReadableByteStreamController) ReadableStreamController; +#[derive(JsLifetime, Clone, PartialEq, Eq)] +pub(crate) enum ReadableStreamReaderClass<'js> { + ReadableStreamDefaultReader(ReadableStreamDefaultReaderClass<'js>), + ReadableStreamBYOBReader(ReadableStreamBYOBReaderClass<'js>), +} + +impl<'js> ReadableStreamReaderClass<'js> { + pub(super) fn closed_promise(&self) -> Promise<'js> { + match self { + Self::ReadableStreamDefaultReader(r) => { + r.borrow().generic.closed_promise.promise.clone() + } + Self::ReadableStreamBYOBReader(r) => r.borrow().generic.closed_promise.promise.clone(), + } + } +} + +impl<'js> From> for ReadableStreamReaderClass<'js> { + fn from(value: ReadableStreamDefaultReaderClass<'js>) -> Self { + Self::ReadableStreamDefaultReader(value) + } +} + +impl<'js> From> for ReadableStreamReaderClass<'js> { + fn from(value: ReadableStreamBYOBReaderClass<'js>) -> Self { + Self::ReadableStreamBYOBReader(value) + } +} + +pub(crate) enum ReadableStreamReaderOwned<'js> { + ReadableStreamDefaultReader(ReadableStreamDefaultReaderOwned<'js>), + ReadableStreamBYOBReader(ReadableStreamBYOBReaderOwned<'js>), +} + +impl<'js> ReadableStreamReader<'js> for ReadableStreamReaderOwned<'js> { + type Class = ReadableStreamReaderClass<'js>; + + fn with_reader( + self, + ctx: C, + default: impl FnOnce( + C, + ReadableStreamDefaultReaderOwned<'js>, + ) -> Result<(C, ReadableStreamDefaultReaderOwned<'js>)>, + byob: impl FnOnce( + C, + ReadableStreamBYOBReaderOwned<'js>, + ) -> Result<(C, ReadableStreamBYOBReaderOwned<'js>)>, + _: impl FnOnce(C) -> Result, + ) -> Result<(C, Self)> { + match self { + Self::ReadableStreamDefaultReader(r) => { + let (ctx, r) = default(ctx, r)?; + Ok((ctx, Self::ReadableStreamDefaultReader(r))) + } + Self::ReadableStreamBYOBReader(r) => { + let (ctx, r) = byob(ctx, r)?; + Ok((ctx, Self::ReadableStreamBYOBReader(r))) + } + } + } + + fn into_inner(self) -> Self::Class { + match self { + ReadableStreamReaderOwned::ReadableStreamDefaultReader(r) => { + ReadableStreamReaderClass::ReadableStreamDefaultReader(r.into_inner()) + } + ReadableStreamReaderOwned::ReadableStreamBYOBReader(r) => { + ReadableStreamReaderClass::ReadableStreamBYOBReader(r.into_inner()) + } + } + } + + fn from_class(class: Self::Class) -> Self { + match class { + ReadableStreamReaderClass::ReadableStreamDefaultReader(r) => { + Self::ReadableStreamDefaultReader(OwnedBorrowMut::from_class(r)) + } + ReadableStreamReaderClass::ReadableStreamBYOBReader(r) => { + Self::ReadableStreamBYOBReader(OwnedBorrowMut::from_class(r)) + } + } + } + + fn try_from_erased(erased: Option>) -> Option { + erased + } +} + +impl<'js, T: ReadableStreamReader<'js>> ReadableStreamReader<'js> for Option { + type Class = Option<>::Class>; + + fn with_reader( + self, + mut ctx: C, + default: impl FnOnce( + C, + ReadableStreamDefaultReaderOwned<'js>, + ) -> Result<(C, ReadableStreamDefaultReaderOwned<'js>)>, + byob: impl FnOnce( + C, + ReadableStreamBYOBReaderOwned<'js>, + ) -> Result<(C, ReadableStreamBYOBReaderOwned<'js>)>, + none: impl FnOnce(C) -> Result, + ) -> Result<(C, Self)> { + match self { + Some(mut reader) => { + (ctx, reader) = reader.with_reader(ctx, default, byob, none)?; + Ok((ctx, Some(reader))) + } + None => Ok((none(ctx)?, None)), + } + } + + fn into_inner(self) -> Self::Class { + self.map(ReadableStreamReader::into_inner) + } + + fn from_class(class: Self::Class) -> Self { + class.map(ReadableStreamReader::from_class) + } + + fn try_from_erased(erased: Option>) -> Option { + match erased { + Some(r) => Some(Some(T::try_from_erased(Some(r))?)), + None => Some(None), + } + } +} + +#[derive(Clone, Trace)] +pub(crate) struct UndefinedReader; + +impl<'js> ReadableStreamReader<'js> for UndefinedReader { + type Class = UndefinedReader; + + fn with_reader( + self, + ctx: C, + _: impl FnOnce( + C, + ReadableStreamDefaultReaderOwned<'js>, + ) -> Result<(C, ReadableStreamDefaultReaderOwned<'js>)>, + _: impl FnOnce( + C, + ReadableStreamBYOBReaderOwned<'js>, + ) -> Result<(C, ReadableStreamBYOBReaderOwned<'js>)>, + none: impl FnOnce(C) -> Result, + ) -> Result<(C, Self)> { + Ok((none(ctx)?, self)) + } + + fn into_inner(self) -> Self::Class { + UndefinedReader + } + + fn from_class(_: Self::Class) -> Self { + UndefinedReader + } + + fn try_from_erased(erased: Option>) -> Option { + match erased { + None => Some(UndefinedReader), + _ => None, + } + } +} + +impl<'js> From> for ReadableStreamReaderOwned<'js> { + fn from(value: ReadableStreamDefaultReaderOwned<'js>) -> Self { + Self::ReadableStreamDefaultReader(value) + } +} + +impl<'js> From> for ReadableStreamReaderOwned<'js> { + fn from(value: ReadableStreamBYOBReaderOwned<'js>) -> Self { + Self::ReadableStreamBYOBReader(value) + } +} + +#[derive(JsLifetime)] +pub struct ReadableStreamGenericReader<'js> { + pub(super) closed_promise: ResolveablePromise<'js>, + pub(super) stream: Option>, + pub(super) promise_primordials: PromisePrimordials<'js>, + pub(super) constructor_type_error: Constructor<'js>, + pub(super) constructor_range_error: Constructor<'js>, + pub(super) function_array_buffer_is_view: Function<'js>, +} + +impl<'js> Trace<'js> for ReadableStreamGenericReader<'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + self.closed_promise.trace(tracer); + self.stream.trace(tracer); + self.promise_primordials.trace(tracer); + // Constructor and Function are persistent - trace their underlying values + self.constructor_type_error.as_value().trace(tracer); + self.constructor_range_error.as_value().trace(tracer); + self.function_array_buffer_is_view.as_value().trace(tracer); + } +} + +impl<'js> ReadableStreamGenericReader<'js> { + pub(super) fn readable_stream_reader_generic_initialize( + ctx: &Ctx<'js>, + stream: OwnedBorrowMut<'js, ReadableStream<'js>>, + ) -> Result { + let closed_promise = match stream.state { + // If stream.[[state]] is "readable", + ReadableStreamState::Readable => { + // Set reader.[[closedPromise]] to a new promise. + ResolveablePromise::new(ctx)? + } + // Otherwise, if stream.[[state]] is "closed", + ReadableStreamState::Closed => { + // Set reader.[[closedPromise]] to a promise resolved with undefined. + ResolveablePromise::resolved_with_undefined(&stream.promise_primordials) + } + // Otherwise, + ReadableStreamState::Errored(ref stored_error) => { + // Set reader.[[closedPromise]] to a promise rejected with stream.[[storedError]]. + let promise = ResolveablePromise::rejected_with( + &stream.promise_primordials, + stored_error.clone(), + )?; + + // Set reader.[[closedPromise]].[[PromiseIsHandled]] to true. + promise.set_is_handled()?; + + promise + } + }; + + let promise_primordials = stream.promise_primordials.clone(); + let constructor_type_error = stream.constructor_type_error.clone(); + let constructor_range_error = stream.constructor_range_error.clone(); + let function_array_buffer_is_view = stream.function_array_buffer_is_view.clone(); + + Ok(Self { + // Set reader.[[stream]] to stream. + stream: Some(stream.into_inner()), + closed_promise, + promise_primordials, + constructor_type_error, + constructor_range_error, + function_array_buffer_is_view, + }) + } + + pub(super) fn readable_stream_reader_generic_release( + &mut self, + + stream: &mut ReadableStream<'js>, + controller_release_steps: impl FnOnce(), + ) -> Result<()> { + // Let stream be reader.[[stream]]. + // Assert: stream is not undefined. + + // If stream.[[state]] is "readable", reject reader.[[closedPromise]] with a TypeError exception. + if let ReadableStreamState::Readable = stream.state { + self.closed_promise.reject_with_constructor( + &stream.constructor_type_error, + "Reader was released and can no longer be used to monitor the stream's closedness", + )?; + } else { + // Otherwise, set reader.[[closedPromise]] to a promise rejected with a TypeError exception. + self.closed_promise = ResolveablePromise::rejected_with_constructor( + &stream.promise_primordials, + &stream.constructor_type_error, + "Reader was released and can no longer be used to monitor the stream's closedness", + )?; + } + + // Set reader.[[closedPromise]].[[PromiseIsHandled]] to true. + self.closed_promise.set_is_handled()?; + + // Perform ! stream.[[controller]].[[ReleaseSteps]](). + controller_release_steps(); + + // Set stream.[[reader]] to undefined. + stream.reader = None; + + // Set reader.[[stream]] to undefined. + self.stream = None; + + Ok(()) + } + + pub(super) fn readable_stream_reader_generic_cancel< + C: ReadableStreamController<'js>, + R: ReadableStreamReader<'js>, + >( + ctx: Ctx<'js>, + // Let stream be reader.[[stream]]. + objects: ReadableStreamObjects<'js, C, R>, + reason: Value<'js>, + ) -> Result<(Promise<'js>, ReadableStreamObjects<'js, C, R>)> { + // Return ! ReadableStreamCancel(stream, reason). + ReadableStream::readable_stream_cancel(ctx, objects, reason) + } +} + +impl<'js> ReadableStreamReaderClass<'js> { + pub fn acquire_readable_stream_default_reader( + ctx: Ctx<'js>, + stream: ReadableStreamOwned<'js>, + ) -> Result<( + ReadableStreamOwned<'js>, + ReadableStreamDefaultReaderClass<'js>, + )> { + ReadableStreamDefaultReader::set_up_readable_stream_default_reader(&ctx, stream) + } + + pub(super) fn acquire_readable_stream_byob_reader( + ctx: Ctx<'js>, + stream: ReadableStreamOwned<'js>, + ) -> Result<(ReadableStreamOwned<'js>, ReadableStreamBYOBReaderClass<'js>)> { + ReadableStreamBYOBReader::set_up_readable_stream_byob_reader(ctx, stream) + } +} + +impl<'js> IntoJs<'js> for ReadableStreamReaderClass<'js> { + fn into_js(self, ctx: &Ctx<'js>) -> Result> { + match self { + Self::ReadableStreamDefaultReader(r) => r.into_js(ctx), + Self::ReadableStreamBYOBReader(r) => r.into_js(ctx), + } + } +} + +impl<'js> Trace<'js> for ReadableStreamReaderClass<'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + match self { + Self::ReadableStreamDefaultReader(r) => r.trace(tracer), + Self::ReadableStreamBYOBReader(r) => r.trace(tracer), + } + } +} + +impl<'js> FromJs<'js> for ReadableStreamReaderClass<'js> { + fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let ty_name = value.type_name(); + let obj = value + .as_object() + .ok_or(Error::new_from_js(ty_name, "Object"))?; + + if let Ok(default) = obj.into_class() { + return Ok(Self::ReadableStreamDefaultReader(default)); + } + + if let Ok(default) = obj.into_class() { + return Ok(Self::ReadableStreamBYOBReader(default)); + } + + Err(Error::new_from_js(ty_name, "ReadableStreamReader")) + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/stream/algorithms.rs b/stdlib/src/llrt/llrt_stream_web/readable/stream/algorithms.rs new file mode 100644 index 00000000..f677e4f4 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/readable/stream/algorithms.rs @@ -0,0 +1,281 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use std::{cell::RefCell, rc::Rc}; + +use crate::llrt_utils::option::{Null, Undefined}; +use crate::llrt_utils::primordials::Primordial; +use rquickjs::{ + class::Trace, prelude::This, Class, Ctx, Function, JsLifetime, Object, Promise, Result, Value, +}; + +use crate::llrt_stream_web::{ + readable::controller::ReadableStreamControllerClass, + transform::{ + controller::TransformStreamDefaultControllerClass, + stream::{self as transform_stream, TransformStreamClass}, + }, + utils::promise::{promise_resolved_with, PromisePrimordials}, +}; + +use super::tee::TeeState; + +#[derive(Clone)] +pub enum StartAlgorithm<'js> { + ReturnUndefined, + Function { + f: Function<'js>, + underlying_source: Null>>, + }, +} + +impl<'js> StartAlgorithm<'js> { + pub(crate) fn call( + &self, + ctx: Ctx<'js>, + controller: ReadableStreamControllerClass<'js>, + ) -> Result> { + match self { + StartAlgorithm::ReturnUndefined => Ok(Value::new_undefined(ctx.clone())), + StartAlgorithm::Function { + f, + underlying_source, + } => f.call::<_, Value>((This(underlying_source.clone()), controller)), + } + } +} + +type PullRustFn<'js> = + Box, ReadableStreamControllerClass<'js>) -> Result> + 'js>; + +#[allow(private_interfaces)] +#[derive(Clone)] +pub enum PullAlgorithm<'js> { + ReturnPromiseUndefined, + Function { + f: Function<'js>, + underlying_source: Null>>, + }, + RustFunction(Rc>), + Tee(Class<'js, TeeState<'js>>), + Transform(TransformStreamClass<'js>), +} + +impl<'js> Trace<'js> for PullAlgorithm<'js> { + fn trace<'a>(&self, tracer: rquickjs::class::Tracer<'a, 'js>) { + match self { + Self::ReturnPromiseUndefined => {} + Self::Function { + f, + underlying_source, + } => { + f.trace(tracer); + underlying_source.trace(tracer); + } + Self::RustFunction(_) => {} + Self::Tee(state) => state.trace(tracer), + Self::Transform(stream) => stream.trace(tracer), + } + } +} + +unsafe impl<'js> JsLifetime<'js> for PullAlgorithm<'js> { + type Changed<'to> = PullAlgorithm<'to>; +} + +impl<'js> PullAlgorithm<'js> { + pub fn from_fn( + f: impl Fn(Ctx<'js>, ReadableStreamControllerClass<'js>) -> Result> + 'js, + ) -> Self { + Self::RustFunction(Rc::new(Box::new(f))) + } + + /// Wrap a one-shot pull closure. Subsequent invocations after the first + /// resolve with `undefined` without calling `f` again — useful for + /// streams that enqueue their whole payload in one go and then close. + pub fn from_fn_once( + f: impl FnOnce(Ctx<'js>, ReadableStreamControllerClass<'js>) -> Result> + 'js, + ) -> Self { + type OnceSlot<'js> = Rc< + RefCell< + Option< + Box< + dyn FnOnce( + Ctx<'js>, + ReadableStreamControllerClass<'js>, + ) -> Result> + + 'js, + >, + >, + >, + >; + let slot: OnceSlot<'js> = Rc::new(RefCell::new(Some(Box::new(f)))); + Self::from_fn(move |ctx, ctrl| { + if let Some(f) = slot.borrow_mut().take() { + f(ctx, ctrl) + } else { + Ok(PromisePrimordials::get(&ctx)? + .promise_resolved_with_undefined + .clone()) + } + }) + } + + pub(super) fn from_tee_state(state: Class<'js, TeeState<'js>>) -> Self { + Self::Tee(state) + } + + pub(crate) fn call( + &self, + ctx: Ctx<'js>, + promise_primordials: &PromisePrimordials<'js>, + controller: ReadableStreamControllerClass<'js>, + ) -> Result> { + match self { + PullAlgorithm::ReturnPromiseUndefined => { + Ok(promise_primordials.promise_resolved_with_undefined.clone()) + } + PullAlgorithm::Function { + f, + underlying_source, + } => promise_resolved_with( + &ctx, + promise_primordials, + f.call::<_, Value>((This(underlying_source.clone()), controller)), + ), + PullAlgorithm::RustFunction(f) => f(ctx, controller), + PullAlgorithm::Tee(state) => { + crate::llrt_stream_web::readable::stream::tee::tee_pull_algorithm( + ctx, + state.clone(), + ) + } + PullAlgorithm::Transform(stream) => { + transform_stream::source_pull_algorithm(ctx, stream) + } + } + } +} + +type CancelRustFn<'js> = Box) -> Result> + 'js>; + +#[allow(private_interfaces)] +pub enum CancelAlgorithm<'js> { + ReturnPromiseUndefined, + Function { + f: Function<'js>, + underlying_source: Null>>, + }, + RustFunction(Rc>>>), + Tee1(Class<'js, TeeState<'js>>), + Tee2(Class<'js, TeeState<'js>>), + Transform { + stream: TransformStreamClass<'js>, + controller: TransformStreamDefaultControllerClass<'js>, + }, +} + +impl<'js> Clone for CancelAlgorithm<'js> { + fn clone(&self) -> Self { + match self { + Self::ReturnPromiseUndefined => Self::ReturnPromiseUndefined, + Self::Function { + f, + underlying_source, + } => Self::Function { + f: f.clone(), + underlying_source: underlying_source.clone(), + }, + Self::RustFunction(rc) => Self::RustFunction(rc.clone()), + Self::Tee1(state) => Self::Tee1(state.clone()), + Self::Tee2(state) => Self::Tee2(state.clone()), + Self::Transform { stream, controller } => Self::Transform { + stream: stream.clone(), + controller: controller.clone(), + }, + } + } +} + +impl<'js> Trace<'js> for CancelAlgorithm<'js> { + fn trace<'a>(&self, tracer: rquickjs::class::Tracer<'a, 'js>) { + match self { + Self::ReturnPromiseUndefined => {} + Self::Function { + f, + underlying_source, + } => { + f.trace(tracer); + underlying_source.trace(tracer); + } + Self::RustFunction(_) => {} + Self::Tee1(state) | Self::Tee2(state) => state.trace(tracer), + Self::Transform { stream, controller } => { + stream.trace(tracer); + controller.trace(tracer); + } + } + } +} + +unsafe impl<'js> JsLifetime<'js> for CancelAlgorithm<'js> { + type Changed<'to> = CancelAlgorithm<'to>; +} + +impl<'js> CancelAlgorithm<'js> { + pub fn from_fn(f: impl FnOnce(Value<'js>) -> Result> + 'js) -> Self { + Self::RustFunction(Rc::new(RefCell::new(Some(Box::new(f))))) + } + + pub(super) fn from_tee_state_1(state: Class<'js, TeeState<'js>>) -> Self { + Self::Tee1(state) + } + + pub(super) fn from_tee_state_2(state: Class<'js, TeeState<'js>>) -> Self { + Self::Tee2(state) + } + + pub(crate) fn call( + &self, + ctx: Ctx<'js>, + promise_primordials: &PromisePrimordials<'js>, + reason: Value<'js>, + ) -> Result> { + match self { + CancelAlgorithm::ReturnPromiseUndefined => { + Ok(promise_primordials.promise_resolved_with_undefined.clone()) + } + CancelAlgorithm::Function { + f, + underlying_source, + } => { + let result: Result = f.call((This(underlying_source.clone()), reason)); + promise_resolved_with(&ctx, promise_primordials, result) + } + CancelAlgorithm::RustFunction(f) => { + let f = f + .borrow_mut() + .take() + .expect("cancel algorithm must only be called once"); + f(reason) + } + CancelAlgorithm::Tee1(state) => { + crate::llrt_stream_web::readable::stream::tee::tee_cancel_algorithm( + ctx, + state.clone(), + reason, + 0, + ) + } + CancelAlgorithm::Tee2(state) => { + crate::llrt_stream_web::readable::stream::tee::tee_cancel_algorithm( + ctx, + state.clone(), + reason, + 1, + ) + } + CancelAlgorithm::Transform { stream, controller } => { + transform_stream::source_cancel_algorithm(ctx, stream, controller, reason) + } + } + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/stream/mod.rs b/stdlib/src/llrt/llrt_stream_web/readable/stream/mod.rs new file mode 100644 index 00000000..f447ac32 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/readable/stream/mod.rs @@ -0,0 +1,1117 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use std::{cell::OnceCell, panic, rc::Rc}; + +use crate::llrt_stream_web::{ + queuing_strategy::{QueuingStrategy, SizeAlgorithm}, + readable::{ + byob_reader::{ReadableStreamBYOBReader, ReadableStreamReadIntoRequest, ViewBytes}, + byte_controller::{ReadableByteStreamController, ReadableByteStreamControllerClass}, + controller::{ReadableStreamController, ReadableStreamControllerClass}, + default_controller::{ + ReadableStreamDefaultController, ReadableStreamDefaultControllerOwned, + }, + default_reader::{ReadableStreamDefaultReader, ReadableStreamReadRequest}, + iterator::{IteratorKind, IteratorRecord, ReadableStreamAsyncIterator}, + objects::{ + ReadableStreamBYOBObjects, ReadableStreamClassObjects, + ReadableStreamDefaultReaderObjects, ReadableStreamObjects, + }, + reader::{ + ReadableStreamReader, ReadableStreamReaderClass, ReadableStreamReaderOwned, + UndefinedReader, + }, + }, + readable_writable_pair::ReadableWritablePair, + utils::{ + promise::{ + promise_rejected_catch, promise_rejected_with, promise_rejected_with_constructor, + promise_resolved_with, upon_promise_fulfilment, with_promise_result, + PromisePrimordials, + }, + UnwrapOrUndefined, ValueOrUndefined, + }, + writable::WritableStreamOwned, +}; + +use pipe::StreamPipeOptions; +use source::UnderlyingSource; + +use crate::llrt_utils::{ + option::{Null, NullableOpt, Undefined}, + primordials::{BasePrimordials, Primordial}, + result::ResultExt, +}; +pub use algorithms::{CancelAlgorithm, PullAlgorithm, StartAlgorithm}; +use rquickjs::{ + atom::PredefinedAtom, + class::{OwnedBorrowMut, Trace}, + function::Constructor, + prelude::{List, Opt, This}, + Class, Coerced, Ctx, Error, Exception, FromJs, Function, IntoJs, JsLifetime, Object, Promise, + Result, Value, +}; + +pub mod algorithms; +mod pipe; +pub(super) mod source; +mod tee; + +/// Acquire a default reader for the stream, locking it. Subsequent +/// `getReader()` calls from JS will throw per spec. +pub fn lock_readable_stream<'js>( + ctx: Ctx<'js>, + stream: Class<'js, ReadableStream<'js>>, +) -> Result<()> { + let owned = rquickjs::class::OwnedBorrowMut::from_class(stream); + super::reader::ReadableStreamReaderClass::acquire_readable_stream_default_reader(ctx, owned)?; + Ok(()) +} + +/// Fast-path drain for a default-controller ReadableStream whose queue holds +/// all the data synchronously (e.g. the stream was enqueued in `start()` and +/// then closed). Bypasses the JS reader + Promise machinery, so user code +/// that poisons `Object.prototype.then` cannot swap the streamed chunks +/// (WPT `response-stream-with-broken-then`). +/// +/// Returns `Some(chunks)` if the fast path applied, `None` otherwise (stream +/// locked, disturbed, has pending pull, byte controller, not yet closed, +/// etc). Sets `disturbed = true` on success. +pub fn try_sync_drain_closed_stream<'js>( + stream: &Class<'js, ReadableStream<'js>>, +) -> Option>> { + use super::controller::ReadableStreamControllerClass; + use super::default_controller::ReadableStreamDefaultController; + use super::stream::ReadableStreamState; + use rquickjs::class::OwnedBorrowMut; + + let mut stream_ref = stream.try_borrow_mut().ok()?; + if stream_ref.disturbed || stream_ref.is_readable_stream_locked() { + return None; + } + // Stream state must be Readable (not Errored). Closed would also be OK + // but then the queue should already be empty. + if !matches!(stream_ref.state, ReadableStreamState::Readable) { + return None; + } + let controller_class = match &stream_ref.controller { + ReadableStreamControllerClass::ReadableStreamDefaultController(c) => c.clone(), + _ => return None, + }; + let mut controller: OwnedBorrowMut<'js, ReadableStreamDefaultController<'js>> = + OwnedBorrowMut::try_from_class(controller_class).ok()?; + // Only fast-path when close has been requested — otherwise there could + // be more data coming via `pull()` that we'd miss. + if !controller.close_requested { + return None; + } + let mut chunks = Vec::with_capacity(controller.container.queue.len()); + while !controller.container.queue.is_empty() { + chunks.push(controller.container.dequeue_value()); + } + stream_ref.disturbed = true; + // Transition the stream to Closed now that its queue is drained, so that + // later consumers see a consistent state. + stream_ref.state = ReadableStreamState::Closed; + Some(chunks) +} + +/// Tee a ReadableStream into two branches. The stream must not be locked or disturbed. +pub fn tee_readable_stream<'js>( + ctx: Ctx<'js>, + stream: Class<'js, ReadableStream<'js>>, +) -> Result<( + Class<'js, ReadableStream<'js>>, + Class<'js, ReadableStream<'js>>, +)> { + { + let stream_ref = stream.borrow(); + if stream_ref.disturbed { + return Err(Exception::throw_type( + &ctx, + "Cannot tee a disturbed ReadableStream", + )); + } + if stream_ref.is_readable_stream_locked() { + return Err(Exception::throw_type( + &ctx, + "Cannot tee a locked ReadableStream", + )); + } + } + let owned = OwnedBorrowMut::from_class(stream); + let objects = ReadableStreamObjects::from_stream(owned); + ReadableStream::readable_stream_tee(ctx, objects) +} + +#[rquickjs::class] +#[derive(JsLifetime)] +pub struct ReadableStream<'js> { + pub controller: ReadableStreamControllerClass<'js>, + pub disturbed: bool, + pub state: ReadableStreamState<'js>, + pub(crate) reader: Option>, + pub(crate) promise_primordials: PromisePrimordials<'js>, + pub(crate) constructor_type_error: Constructor<'js>, + pub(crate) constructor_range_error: Constructor<'js>, + pub(crate) function_array_buffer_is_view: Function<'js>, +} + +impl<'js> Trace<'js> for ReadableStream<'js> { + fn trace<'a>(&self, tracer: rquickjs::class::Tracer<'a, 'js>) { + self.controller.trace(tracer); + self.state.trace(tracer); + self.reader.trace(tracer); + + self.promise_primordials.trace(tracer); + self.constructor_type_error.trace(tracer); + self.constructor_range_error.trace(tracer); + self.function_array_buffer_is_view.trace(tracer); + } +} + +pub(crate) type ReadableStreamClass<'js> = Class<'js, ReadableStream<'js>>; +pub(crate) type ReadableStreamOwned<'js> = OwnedBorrowMut<'js, ReadableStream<'js>>; + +#[derive(Debug, Trace, Clone, JsLifetime)] +pub enum ReadableStreamState<'js> { + Readable, + Closed, + Errored(Value<'js>), +} + +#[rquickjs::methods(rename_all = "camelCase")] +impl<'js> ReadableStream<'js> { + // Streams Spec: 4.2.4: https://streams.spec.whatwg.org/#rs-prototype + // constructor(optional object underlyingSource, optional QueuingStrategy strategy = {}); + #[qjs(constructor)] + fn new( + ctx: Ctx<'js>, + underlying_source: Opt>>, + queuing_strategy: Opt>>, + ) -> Result> { + // If underlyingSource is missing, set it to null. + let underlying_source = Null(underlying_source.0); + + // Let underlyingSourceDict be underlyingSource, converted to an IDL value of type UnderlyingSource. + let underlying_source_dict = match underlying_source { + Null(None) | Null(Some(Undefined(None))) => UnderlyingSource::default(), + Null(Some(Undefined(Some(ref obj)))) => UnderlyingSource::from_object(obj.clone())?, + }; + + let promise_primordials = PromisePrimordials::get(&ctx)?.clone(); + let base_primordials = BasePrimordials::get(&ctx)?; + + let stream_class = Class::instance( + ctx.clone(), + Self { + // Set stream.[[state]] to "readable". + state: ReadableStreamState::Readable, + // Set stream.[[reader]] and stream.[[storedError]] to undefined. + reader: None, + // Set stream.[[disturbed]] to false. + disturbed: false, + controller: ReadableStreamControllerClass::Uninitialised, + constructor_type_error: base_primordials.constructor_type_error.clone(), + constructor_range_error: base_primordials.constructor_range_error.clone(), + function_array_buffer_is_view: base_primordials + .function_array_buffer_is_view + .clone(), + promise_primordials, + }, + )?; + drop(base_primordials); + let stream = OwnedBorrowMut::from_class(stream_class.clone()); + let queuing_strategy = queuing_strategy.0.and_then(|qs| qs.0); + + match underlying_source_dict.r#type { + // If underlyingSourceDict["type"] is "bytes": + Some(ReadableStreamType::Bytes) => { + // If strategy["size"] exists, throw a RangeError exception. + if queuing_strategy + .as_ref() + .and_then(|qs| qs.size.as_ref()) + .is_some() + { + return Err(Exception::throw_range( + &ctx, + "The strategy for a byte stream cannot have a size function", + )); + } + // Let highWaterMark be ? ExtractHighWaterMark(strategy, 0). + let high_water_mark = + QueuingStrategy::extract_high_water_mark(&ctx, queuing_strategy, 0.0)?; + + // Perform ? SetUpReadableByteStreamControllerFromUnderlyingSource(this, underlyingSource, underlyingSourceDict, highWaterMark). + ReadableByteStreamController::set_up_readable_byte_stream_controller_from_underlying_source( + &ctx, + stream, + underlying_source, + underlying_source_dict, + high_water_mark, + )?; + } + // Otherwise (no type, or "owning" which we treat as a default + // controller that also accepts the `transfer` enqueue option): + None | Some(ReadableStreamType::Owning) => { + let is_owning_type = matches!( + underlying_source_dict.r#type, + Some(ReadableStreamType::Owning) + ); + // Let sizeAlgorithm be ! ExtractSizeAlgorithm(strategy). + let size_algorithm = + QueuingStrategy::extract_size_algorithm(queuing_strategy.as_ref()); + + // Let highWaterMark be ? ExtractHighWaterMark(strategy, 1). + let high_water_mark = + QueuingStrategy::extract_high_water_mark(&ctx, queuing_strategy, 1.0)?; + + // Perform ? SetUpReadableStreamDefaultControllerFromUnderlyingSource(this, underlyingSource, underlyingSourceDict, highWaterMark, sizeAlgorithm). + ReadableStreamDefaultController::set_up_readable_stream_default_controller_from_underlying_source( + ctx, + stream, + underlying_source, + underlying_source_dict, + high_water_mark, + size_algorithm, + is_owning_type, + )?; + } + } + + Ok(stream_class) + } + + // static ReadableStream from(any asyncIterable); + #[qjs(static)] + fn from(ctx: Ctx<'js>, async_iterable: Value<'js>) -> Result> { + // Return ? ReadableStreamFromIterable(asyncIterable). + Self::readable_stream_from_iterable(&ctx, async_iterable) + } + + // readonly attribute boolean locked; + #[qjs(get)] + fn locked(&self) -> bool { + // Return ! IsReadableStreamLocked(this). + self.is_readable_stream_locked() + } + + // Internal property for checking if stream has been read from + #[qjs(get)] + fn disturbed(&self) -> bool { + self.disturbed + } + + // Promise cancel(optional any reason); + fn cancel( + ctx: Ctx<'js>, + stream: This>, + reason: Opt>, + ) -> Result> { + // If ! IsReadableStreamLocked(this) is true, return a promise rejected with a TypeError exception. + if stream.is_readable_stream_locked() { + return promise_rejected_with_constructor( + &stream.constructor_type_error, + &stream.promise_primordials, + "Cannot cancel a stream that already has a reader", + ); + } + + let objects = ReadableStreamObjects::from_stream(stream.0).refresh_reader(); + + let (promise, _) = + Self::readable_stream_cancel(ctx.clone(), objects, reason.0.unwrap_or_undefined(&ctx))?; + Ok(promise) + } + + // ReadableStreamReader getReader(optional ReadableStreamGetReaderOptions options = {}); + fn get_reader( + ctx: Ctx<'js>, + stream: This>, + options: Opt>, + ) -> Result> { + // If options["mode"] does not exist, return ? AcquireReadableStreamDefaultReader(this). + let reader = match options.0 { + None | Some(None | Some(ReadableStreamGetReaderOptions { mode: None })) => { + let (_, reader) = + ReadableStreamReaderClass::acquire_readable_stream_default_reader( + ctx.clone(), + stream.0, + )?; + reader.into() + } + // Return ? AcquireReadableStreamBYOBReader(this). + Some(Some(ReadableStreamGetReaderOptions { + mode: Some(ReadableStreamReaderMode::Byob), + })) => { + let (_, reader) = ReadableStreamReaderClass::acquire_readable_stream_byob_reader( + ctx.clone(), + stream.0, + )?; + reader.into() + } + }; + + Ok(reader) + } + + // ReadableStream pipeThrough(ReadableWritablePair transform, optional StreamPipeOptions options = {}); + fn pipe_through( + ctx: Ctx<'js>, + stream: This>, + transform: ReadableWritablePair<'js>, + options: NullableOpt>, + ) -> Result> { + // If ! IsReadableStreamLocked(this) is true, throw a TypeError exception. + if stream.is_readable_stream_locked() { + return Err(Exception::throw_type( + &ctx, + "ReadableStream.prototype.pipeThrough cannot be used on a locked ReadableStream", + )); + } + + let readable_class = transform.readable.clone(); + let writable = OwnedBorrowMut::from_class(transform.writable); + + // If ! IsWritableStreamLocked(transform["writable"]) is true, throw a TypeError exception. + if writable.is_writable_stream_locked() { + return Err(Exception::throw_type( + &ctx, + "ReadableStream.prototype.pipeThrough cannot be used on a locked WritableStream", + )); + } + + // Let signal be options["signal"] if it exists, or undefined otherwise. + let options = options.0.unwrap_or_default(); + + // Let promise be ! ReadableStreamPipeTo(this, transform["writable"], options["preventClose"], options["preventAbort"], options["preventCancel"], signal). + let promise = ReadableStream::readable_stream_pipe_to( + ctx.clone(), + stream.0, + writable, + options.prevent_close, + options.prevent_abort, + options.prevent_cancel, + options.signal, + )?; + + // Set promise.[[PromiseIsHandled]] to true. + let () = promise + .catch()? + .call((This(promise.clone()), Function::new(ctx, || {})))?; + + // Return transform["readable"]. + Ok(readable_class) + } + + // Promise pipeTo(WritableStream destination, optional StreamPipeOptions options = {}); + fn pipe_to( + ctx: Ctx<'js>, + stream: This>, + destination: Value<'js>, + options: NullableOpt>, + ) -> Result> { + with_promise_result(&ctx, || { + let stream = + ReadableStreamOwned::from_class(Class::from_value(&stream.0).or_throw_type( + &ctx, + "'pipeTo' called on an object that is not a valid instance of ReadableStream.", + )?); + + let options = match options.0 { + Some(options) => Some(StreamPipeOptions::from_js(&ctx, options)?), + None => None, + }; + + // If ! IsReadableStreamLocked(this) is true, return a promise rejected with a TypeError exception. + if stream.is_readable_stream_locked() { + return promise_rejected_with_constructor( + &stream.constructor_type_error, + &stream.promise_primordials, + "ReadableStream.prototype.pipeTo cannot be used on a locked ReadableStream", + ); + } + + let destination = WritableStreamOwned::from_class( + Class::from_value(&destination).or_throw_type(&ctx,"'pipeTo' instructed to pipe to an object that is not a valid instance of WritableStream.")?, + ); + + // If ! IsWritableStreamLocked(destination) is true, return a promise rejected with a TypeError exception. + if destination.is_writable_stream_locked() { + return promise_rejected_with_constructor( + &stream.constructor_type_error, + &stream.promise_primordials, + "ReadableStream.prototype.pipeTo cannot be used on a locked WritableStream", + ); + } + + // Let signal be options["signal"] if it exists, or undefined otherwise. + let options = options.unwrap_or_default(); + + // Return ! ReadableStreamPipeTo(this, destination, options["preventClose"], options["preventAbort"], options["preventCancel"], signal). + Self::readable_stream_pipe_to( + ctx.clone(), + stream, + destination, + options.prevent_close, + options.prevent_abort, + options.prevent_cancel, + options.signal, + ) + }) + } + + // sequence tee(); + fn tee( + ctx: Ctx<'js>, + stream: This>, + ) -> Result, Class<'js, Self>)>> { + Ok(List(Self::readable_stream_tee( + ctx, + ReadableStreamObjects::from_stream(stream.0), + )?)) + } + + #[qjs(rename = PredefinedAtom::SymbolAsyncIterator)] + fn async_iterate( + ctx: Ctx<'js>, + stream: This>, + ) -> Result>> { + Self::values(ctx, stream, Opt(None)) + } + + fn values( + ctx: Ctx<'js>, + stream: This>, + arg: Opt>, + ) -> Result>> { + // Let reader be ? AcquireReadableStreamDefaultReader(stream). + let (stream, reader) = ReadableStreamReaderClass::acquire_readable_stream_default_reader( + ctx.clone(), + stream.0, + )?; + + // Let preventCancel be args[0]["preventCancel"]. + let prevent_cancel = match arg.0 { + None => false, + Some(arg) => matches!(arg.get_value_or_undefined("preventCancel")?, Some(true)), + }; + + let promise_primordials = stream.promise_primordials.clone(); + let controller = stream.controller.clone(); + + ReadableStreamAsyncIterator::new( + ctx, + ReadableStreamClassObjects { + stream: stream.into_inner(), + controller, + reader, + }, + promise_primordials, + prevent_cancel, + ) + } +} + +impl<'js> ReadableStream<'js> { + pub(super) fn readable_stream_error< + C: ReadableStreamController<'js>, + R: ReadableStreamReader<'js>, + >( + // Let reader be stream.[[reader]]. + mut objects: ReadableStreamObjects<'js, C, R>, + e: Value<'js>, + ) -> Result> { + // Set stream.[[state]] to "errored". + // Set stream.[[storedError]] to e. + objects.stream.state = ReadableStreamState::Errored(e.clone()); + + objects = objects.with_reader( + // If reader implements ReadableStreamDefaultReader, + |mut objects| { + // Reject reader.[[closedPromise]] with e. + objects.reader + .generic + .closed_promise + .reject(e.clone())?; + + // Set reader.[[closedPromise]].[[PromiseIsHandled]] to true. + objects.reader.generic.closed_promise.set_is_handled()?; + + // Perform ! ReadableStreamDefaultReaderErrorReadRequests(reader, e). + objects = ReadableStreamDefaultReader::readable_stream_default_reader_error_read_requests( + objects, e.clone(), + )?; + Ok(objects) + }, + // Otherwise, + |mut objects| { + // Reject reader.[[closedPromise]] with e. + objects.reader + .generic + .closed_promise + .reject(e.clone())?; + + // Set reader.[[closedPromise]].[[PromiseIsHandled]] to true. + objects.reader.generic.closed_promise.set_is_handled()?; + + // Perform ! ReadableStreamBYOBReaderErrorReadIntoRequests(reader, e). + objects = ReadableStreamBYOBReader::readable_stream_byob_reader_error_read_into_requests( + objects, e.clone(), + )?; + + Ok(objects) + }, + // If reader is undefined, return. + Ok)?; + + Ok(objects) + } + + pub(super) fn readable_stream_get_num_read_requests( + reader: &ReadableStreamDefaultReader, + ) -> usize { + reader.read_requests.len() + } + + pub(super) fn readable_stream_get_num_read_into_requests( + reader: &ReadableStreamBYOBReader, + ) -> usize { + reader.read_into_requests.len() + } + + pub(super) fn readable_stream_fulfill_read_request>( + ctx: &Ctx<'js>, + // Let reader be stream.[[reader]]. + mut objects: ReadableStreamDefaultReaderObjects<'js, C>, + chunk: Value<'js>, + done: bool, + ) -> Result> { + // Let readRequest be reader.[[readRequests]][0]. + // Remove readRequest from reader.[[readRequests]]. + let read_request = objects + .reader + .read_requests + .pop_front() + .expect("ReadableStreamFulfillReadRequest called with empty readRequests"); + + if done { + // If done is true, perform readRequest’s close steps. + read_request.close_steps_typed(ctx, objects) + } else { + // Otherwise, perform readRequest’s chunk steps, given chunk. + read_request.chunk_steps_typed(objects, chunk) + } + } + + pub(super) fn readable_stream_fulfill_read_into_request( + ctx: &Ctx<'js>, + mut objects: ReadableStreamBYOBObjects<'js>, + chunk: ViewBytes<'js>, + done: bool, + ) -> Result> { + // Let readIntoRequest be reader.[[readIntoRequests]][0]. + // Remove readIntoRequest from reader.[[readIntoRequests]]. + let read_into_request = objects + .reader + .read_into_requests + .pop_front() + .expect("ReadableStreamFulfillReadIntoRequest called with empty readIntoRequests"); + + if done { + // If done is true, perform readIntoRequest’s close steps, given chunk. + read_into_request.close_steps(objects, chunk.into_js(ctx)?) + } else { + // Otherwise, perform readIntoRequest’s chunk steps, given chunk. + read_into_request.chunk_steps(objects, chunk.into_js(ctx)?) + } + } + + pub(super) fn readable_stream_close< + C: ReadableStreamController<'js>, + R: ReadableStreamReader<'js>, + >( + ctx: Ctx<'js>, + // Let reader be stream.[[reader]]. + mut objects: ReadableStreamObjects<'js, C, R>, + ) -> Result> { + // Set stream.[[state]] to "closed". + objects.stream.state = ReadableStreamState::Closed; + + objects.with_reader( + |mut objects| { + // Resolve reader.[[closedPromise]] with undefined. + objects.reader.generic.closed_promise.resolve_undefined()?; + + // If reader implements ReadableStreamDefaultReader, + // Let readRequests be reader.[[readRequests]]. + // Set reader.[[readRequests]] to an empty list. + let read_requests = objects.reader.read_requests.split_off(0); + + // For each readRequest of readRequests, + for read_request in read_requests { + // Perform readRequest’s close steps. + objects = read_request.close_steps_typed(&ctx, objects)?; + } + + Ok(objects) + }, + |objects| { + objects.reader.generic.closed_promise.resolve_undefined()?; + + Ok(objects) + }, + // If reader is undefined, return. + Ok, + ) + } + + pub fn is_readable_stream_locked(&self) -> bool { + // If stream.[[reader]] is undefined, return false. + if self.reader.is_none() { + return false; + } + // Return true. + true + } + + pub(super) fn readable_stream_add_read_request( + &mut self, + reader: &mut ReadableStreamDefaultReader<'js>, + read_request: impl ReadableStreamReadRequest<'js> + 'js, + ) { + reader.read_requests.push_back(Box::new(read_request)); + } + + pub(super) fn readable_stream_cancel< + C: ReadableStreamController<'js>, + R: ReadableStreamReader<'js>, + >( + ctx: Ctx<'js>, + mut objects: ReadableStreamObjects<'js, C, R>, + reason: Value<'js>, + ) -> Result<(Promise<'js>, ReadableStreamObjects<'js, C, R>)> { + // Set stream.[[disturbed]] to true. + objects.stream.disturbed = true; + + match objects.stream.state { + // If stream.[[state]] is "closed", return a promise resolved with undefined. + ReadableStreamState::Closed => Ok(( + // wpt tests expect that this is a new promise every time so we can't duplicate the primordial promise_resolved_with_undefined + promise_resolved_with( + &ctx, + &objects.stream.promise_primordials, + Ok(Value::new_undefined(ctx.clone())), + )?, + objects, + )), + // If stream.[[state]] is "errored", return a promise rejected with stream.[[storedError]]. + ReadableStreamState::Errored(ref stored_error) => Ok(( + promise_rejected_with(&objects.stream.promise_primordials, stored_error.clone())?, + objects, + )), + ReadableStreamState::Readable => { + // Perform ! ReadableStreamClose(stream). + objects = ReadableStream::readable_stream_close(ctx.clone(), objects)?; + // Let reader be stream.[[reader]]. + // If reader is not undefined and reader implements ReadableStreamBYOBReader, + + objects = objects.with_reader( + Ok, + |mut objects| { + // Let readIntoRequests be reader.[[readIntoRequests]]. + // Set reader.[[readIntoRequests]] to an empty list. + let read_into_requests = objects.reader.read_into_requests.split_off(0); + // For each readIntoRequest of readIntoRequests, + for read_into_request in read_into_requests { + // Perform readIntoRequest’s close steps, given undefined. + objects = read_into_request + .close_steps(objects, Value::new_undefined(ctx.clone()))?; + } + + Ok(objects) + }, + Ok, + )?; + + // Let sourceCancelPromise be ! stream.[[controller]].[[CancelSteps]](reason). + let (source_cancel_promise, objects) = C::cancel_steps(&ctx, objects, reason)?; + + // Return the result of reacting to sourceCancelPromise with a fulfillment step that returns undefined. + let promise = upon_promise_fulfilment(ctx, source_cancel_promise, |_, ()| { + Ok(rquickjs::Undefined) + })?; + + Ok((promise, objects)) + } + } + } + + pub(super) fn readable_stream_add_read_into_request( + reader: &mut ReadableStreamBYOBReader<'js>, + read_request: impl ReadableStreamReadIntoRequest<'js> + 'js, + ) { + // Append readRequest to stream.[[reader]].[[readIntoRequests]]. + reader.read_into_requests.push_back(Box::new(read_request)) + } + + // CreateReadableStream(startAlgorithm, pullAlgorithm, cancelAlgorithm[, highWaterMark, [, sizeAlgorithm]]) performs the following steps: + pub(crate) fn create_readable_stream( + ctx: Ctx<'js>, + start_algorithm: StartAlgorithm<'js>, + pull_algorithm: PullAlgorithm<'js>, + cancel_algorithm: CancelAlgorithm<'js>, + high_water_mark: Option, + size_algorithm: Option>, + ) -> Result< + ReadableStreamClassObjects<'js, ReadableStreamDefaultControllerOwned<'js>, UndefinedReader>, + > { + // If highWaterMark was not passed, set it to 1. + let high_water_mark = high_water_mark.unwrap_or(1.0); + + // If sizeAlgorithm was not passed, set it to an algorithm that returns 1. + let size_algorithm = size_algorithm.unwrap_or(SizeAlgorithm::AlwaysOne); + + let base_primordials = BasePrimordials::get(&ctx)?; + + // Let stream be a new ReadableStream. + let stream_class = Class::instance( + ctx.clone(), + Self { + // Set stream.[[state]] to "readable". + state: ReadableStreamState::Readable, + // Set stream.[[reader]] and stream.[[storedError]] to undefined. + reader: None, + // Set stream.[[disturbed]] to false. + disturbed: false, + controller: ReadableStreamControllerClass::Uninitialised, + promise_primordials: PromisePrimordials::get(&ctx)?.clone(), + constructor_range_error: base_primordials.constructor_range_error.clone(), + constructor_type_error: base_primordials.constructor_type_error.clone(), + function_array_buffer_is_view: base_primordials + .function_array_buffer_is_view + .clone(), + }, + )?; + drop(base_primordials); + + // Perform ? SetUpReadableStreamDefaultController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm). + let controller_class = + ReadableStreamDefaultController::set_up_readable_stream_default_controller( + ctx, + OwnedBorrowMut::from_class(stream_class.clone()), + start_algorithm, + pull_algorithm, + cancel_algorithm, + high_water_mark, + size_algorithm, + false, // not owning-type; Rust-side streams never set it + )?; + + // Return stream. + Ok(ReadableStreamClassObjects { + stream: stream_class, + controller: controller_class, + reader: UndefinedReader, + }) + } + + /// Create a ReadableStream from Rust pull/cancel algorithms + pub fn from_pull_algorithm( + ctx: Ctx<'js>, + pull_algorithm: PullAlgorithm<'js>, + cancel_algorithm: CancelAlgorithm<'js>, + ) -> Result> { + Self::from_pull_algorithm_with_options(ctx, pull_algorithm, cancel_algorithm, None) + } + + /// Create a ReadableStream from Rust pull/cancel algorithms with custom highWaterMark + pub fn from_pull_algorithm_with_options( + ctx: Ctx<'js>, + pull_algorithm: PullAlgorithm<'js>, + cancel_algorithm: CancelAlgorithm<'js>, + high_water_mark: Option, + ) -> Result> { + Ok(Self::create_readable_stream( + ctx, + StartAlgorithm::ReturnUndefined, + pull_algorithm, + cancel_algorithm, + high_water_mark, + None, + )? + .stream) + } + + /// Create a byte-source ReadableStream (i.e. `type: "bytes"`) from Rust + /// pull/cancel algorithms. BYOB readers can attach to the returned + /// stream, and the pull algorithm receives a byte controller so it can + /// enqueue `Uint8Array` chunks that stream directly into BYOB reads + /// without copying. + pub fn from_byte_pull_algorithm( + ctx: Ctx<'js>, + pull_algorithm: PullAlgorithm<'js>, + cancel_algorithm: CancelAlgorithm<'js>, + ) -> Result> { + let (stream, _controller) = Self::create_readable_byte_stream( + ctx, + StartAlgorithm::ReturnUndefined, + pull_algorithm, + cancel_algorithm, + )?; + Ok(stream) + } + + // CreateReadableByteStream(startAlgorithm, pullAlgorithm, cancelAlgorithm) performs the following steps: + pub fn create_readable_byte_stream( + ctx: Ctx<'js>, + start_algorithm: StartAlgorithm<'js>, + pull_algorithm: PullAlgorithm<'js>, + cancel_algorithm: CancelAlgorithm<'js>, + ) -> Result<(Class<'js, Self>, ReadableByteStreamControllerClass<'js>)> { + let base_primordials = BasePrimordials::get(&ctx)?; + + // Let stream be a new ReadableStream. + let stream_class = Class::instance( + ctx.clone(), + Self { + // Set stream.[[state]] to "readable". + state: ReadableStreamState::Readable, + // Set stream.[[reader]] and stream.[[storedError]] to undefined. + reader: None, + // Set stream.[[disturbed]] to false. + disturbed: false, + controller: ReadableStreamControllerClass::Uninitialised, + promise_primordials: PromisePrimordials::get(&ctx)?.clone(), + constructor_type_error: base_primordials.constructor_type_error.clone(), + constructor_range_error: base_primordials.constructor_range_error.clone(), + function_array_buffer_is_view: base_primordials + .function_array_buffer_is_view + .clone(), + }, + )?; + drop(base_primordials); + + // Perform ? SetUpReadableStreamDefaultController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm). + let controller_class = + ReadableByteStreamController::set_up_readable_byte_stream_controller( + ctx, + OwnedBorrowMut::from_class(stream_class.clone()), + start_algorithm, + pull_algorithm, + cancel_algorithm, + 0.0, + None, + )?; + + // Return stream. + Ok((stream_class, controller_class)) + } + + fn readable_stream_from_iterable( + ctx: &Ctx<'js>, + async_iterable: Value<'js>, + ) -> Result> { + let stream: Rc>> = Rc::new(OnceCell::new()); + + // Let iteratorRecord be ? GetIterator(asyncIterable, async). + let iterator_record = + IteratorRecord::get_iterator(ctx, async_iterable, IteratorKind::Async)?; + let iterator = iterator_record.iterator.clone(); + + // Let startAlgorithm be an algorithm that returns undefined. + let start_algorithm = StartAlgorithm::ReturnUndefined; + + let promise_primordials = PromisePrimordials::get(ctx)?.clone(); + + // Let pullAlgorithm be the following steps: + let pull_algorithm = { + let stream = stream.clone(); + let promise_primordials = promise_primordials.clone(); + move |ctx: Ctx<'js>, controller: ReadableStreamControllerClass<'js>| { + // Let nextResult be IteratorNext(iteratorRecord). + let next_result: Result> = iterator_record.iterator_next(&ctx, None); + let next_promise = match next_result { + // If nextResult is an abrupt completion, return a promise rejected with nextResult.[[Value]]. + Err(Error::Exception) => { + return promise_rejected_catch(&ctx, &promise_primordials); + } + Err(err) => return Err(err), + // Let nextPromise be a promise resolved with nextResult.[[Value]]. + Ok(next_result) => promise_resolved_with( + &ctx, + &promise_primordials, + Ok(next_result.into_inner()), + )?, + }; + + // Return the result of reacting to nextPromise with the following fulfillment steps, given iterResult: + upon_promise_fulfilment(ctx, next_promise, { + let stream = stream.clone(); + move |ctx, iter_result: Value<'js>| { + let iter_result = match iter_result.into_object() { + // If Type(iterResult) is not Object, throw a TypeError. + None => { + return Err(Exception::throw_type(&ctx, "The promise returned by the iterator.next() method must fulfill with an object")); + } + Some(iter_result) => iter_result, + }; + + // Let done be ? IteratorComplete(iterResult). + let done = IteratorRecord::iterator_complete(&iter_result)?; + + let stream = OwnedBorrowMut::from_class(stream.get().cloned().expect("ReadableStreamFromIterable pull steps called with uninitialised stream")); + let controller = match controller { + ReadableStreamControllerClass::ReadableStreamDefaultController(c) => OwnedBorrowMut::from_class(c), + _ => panic!("ReadableStreamFromIterable pull steps called without default controller") + }; + + let objects = ReadableStreamObjects::new_default(stream, controller); + + // If done is true: + if done { + // Perform ! ReadableStreamDefaultControllerClose(stream.[[controller]]). + ReadableStreamDefaultController::readable_stream_default_controller_close(ctx.clone(), objects)?; + } else { + // Let value be ? IteratorValue(iterResult). + let value = IteratorRecord::iterator_value(&iter_result)?; + + // Perform ! ReadableStreamDefaultControllerEnqueue(stream.[[controller]], value). + ReadableStreamDefaultController::readable_stream_default_controller_enqueue(ctx.clone(), objects, value)?; + } + + Ok(()) + } + }) + } + }; + + // Let cancelAlgorithm be the following steps, given reason: + let cancel_algorithm = { + let ctx = ctx.clone(); + let promise_primordials = promise_primordials.clone(); + move |reason: Value<'js>| { + // Let iterator be iteratorRecord.[[Iterator]]. + + // Let returnMethod be GetMethod(iterator, "return"). + let return_method_val: Value<'js> = match iterator.get(PredefinedAtom::Return) { + Err(Error::Exception) => { + return promise_rejected_catch(&ctx, &promise_primordials); + } + Err(err) => return Err(err), + Ok(val) => val, + }; + + let return_method: Function<'js> = + if return_method_val.is_undefined() || return_method_val.is_null() { + // If returnMethod.[[Value]] is undefined, return a promise resolved with undefined. + return Ok(promise_primordials.promise_resolved_with_undefined.clone()); + } else if let Some(func) = return_method_val.as_function() { + func.clone() + } else { + // returnMethod is not callable — reject with TypeError + let _ = Exception::throw_type(&ctx, "return is not a function"); + return promise_rejected_catch(&ctx, &promise_primordials); + }; + + // Let returnResult be Call(returnMethod.[[Value]], iterator, « reason »). + let return_result: Result> = + return_method.call((This(iterator), reason)); + + let return_result = match return_result { + // If returnResult is an abrupt completion, return a promise rejected with returnResult.[[Value]]. + Err(Error::Exception) => { + return promise_rejected_catch(&ctx, &promise_primordials); + } + Err(err) => return Err(err), + Ok(return_result) => return_result, + }; + + // Let returnPromise be a promise resolved with returnResult.[[Value]]. + let return_promise = + promise_resolved_with(&ctx, &promise_primordials, Ok(return_result))?; + + // Return the result of reacting to returnPromise with the following fulfillment steps, given iterResult: + upon_promise_fulfilment( + ctx, + return_promise, + move |ctx: Ctx<'js>, iter_result: Value<'js>| { + // If Type(iterResult) is not Object, throw a TypeError. + if !iter_result.is_object() { + return Err(Exception::throw_type(&ctx, "The promise returned by the iterator.next() method must fulfill with an object")); + } + // Return undefined. + Ok(rquickjs::Undefined) + }, + ) + } + }; + + let objects_class = ReadableStream::create_readable_stream( + ctx.clone(), + start_algorithm, + PullAlgorithm::from_fn(pull_algorithm), + CancelAlgorithm::from_fn(cancel_algorithm), + Some(0.0), + None, + )?; + _ = stream.set(objects_class.stream.clone()); + Ok(objects_class.stream) + } + + pub(super) fn reader_mut(&mut self) -> Option> { + self.reader + .clone() + .map(ReadableStreamReaderOwned::from_class) + } +} + +// enum ReadableStreamType { "bytes", "owning" }; +enum ReadableStreamType { + Bytes, + Owning, +} + +impl<'js> FromJs<'js> for ReadableStreamType { + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let typ = value.type_of(); + + match Coerced::::from_js(ctx, value)?.as_str() { + "bytes" => Ok(Self::Bytes), + "owning" => Ok(Self::Owning), + _ => Err(Error::new_from_js(typ.as_str(), "ReadableStreamType")), + } + } +} + +struct ReadableStreamGetReaderOptions { + mode: Option, +} + +impl<'js> FromJs<'js> for ReadableStreamGetReaderOptions { + fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let ty_name = value.type_name(); + let obj = value + .as_object() + .ok_or(Error::new_from_js(ty_name, "Object"))?; + + let mode = obj.get_value_or_undefined::<_, ReadableStreamReaderMode>("mode")?; + + Ok(Self { mode }) + } +} + +// enum ReadableStreamReaderMode { "byob" }; +enum ReadableStreamReaderMode { + Byob, +} + +impl<'js> FromJs<'js> for ReadableStreamReaderMode { + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let typ = value.type_of(); + + match Coerced::::from_js(ctx, value)?.as_str() { + "byob" => Ok(Self::Byob), + _ => Err(Error::new_from_js(typ.as_str(), "ReadableStreamReaderMode")), + } + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/stream/pipe.rs b/stdlib/src/llrt/llrt_stream_web/readable/stream/pipe.rs new file mode 100644 index 00000000..6ea6e9e6 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/readable/stream/pipe.rs @@ -0,0 +1,700 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use std::{ + cell::RefCell, + rc::Rc, + sync::atomic::{AtomicBool, Ordering}, +}; + +use crate::llrt_abort::AbortSignal; +use crate::llrt_utils::{option::Undefined, result::ResultExt}; +use rquickjs::{ + class::{OwnedBorrow, Trace}, + prelude::{OnceFn, This}, + Class, Coerced, Ctx, Error, FromJs, Function, Promise, Result, Value, +}; + +use crate::llrt_stream_web::{ + readable::{ + controller::ReadableStreamControllerOwned, + default_reader::{ + ReadableStreamDefaultReader, ReadableStreamDefaultReaderOwned, + ReadableStreamReadRequest, + }, + objects::{ + ReadableStreamClassObjects, ReadableStreamDefaultReaderObjects, ReadableStreamObjects, + }, + reader::ReadableStreamReaderClass, + stream::{ReadableStream, ReadableStreamOwned, ReadableStreamState}, + }, + utils::{ + promise::{ + promise_resolved_with, upon_promise, upon_promise_fulfilment, PromisePrimordials, + ResolveablePromise, + }, + UnwrapOrUndefined, ValueOrUndefined, + }, + writable::{ + WritableStream, WritableStreamClassObjects, WritableStreamDefaultWriter, + WritableStreamDefaultWriterOwned, WritableStreamObjects, WritableStreamOwned, + WritableStreamState, + }, +}; + +impl<'js> ReadableStream<'js> { + pub(super) fn readable_stream_pipe_to( + ctx: Ctx<'js>, + source: ReadableStreamOwned<'js>, + dest: WritableStreamOwned<'js>, + prevent_close: bool, + prevent_abort: bool, + prevent_cancel: bool, + signal: Option>>, + ) -> Result> { + let (source_stored_error, source_closed) = match source.state { + ReadableStreamState::Errored(ref stored_error) => (Some(stored_error.clone()), false), + ReadableStreamState::Closed => (None, true), + _ => (None, false), + }; + let dest_stored_error = dest.stored_error(); + let dest_closing = dest.writable_stream_close_queued_or_in_flight() + || matches!(dest.state, WritableStreamState::Closed); + + let source_controller = source.controller.clone(); + + let dest_controller = dest + .controller + .clone() + .expect("pipeTo called on writable stream without controller"); + + // If source.[[controller]] implements ReadableByteStreamController, let reader be either ! AcquireReadableStreamBYOBReader(source) or ! AcquireReadableStreamDefaultReader(source), at the user agent’s discretion. + // Otherwise, let reader be ! AcquireReadableStreamDefaultReader(source). + let (mut source, reader) = + ReadableStreamReaderClass::acquire_readable_stream_default_reader(ctx.clone(), source)?; + + let source_closed_promise = reader.borrow().generic.closed_promise.promise.clone(); + + // Let writer be ! AcquireWritableStreamDefaultWriter(dest). + let (dest, writer) = + WritableStreamDefaultWriter::acquire_writable_stream_default_writer(&ctx, dest)?; + + let dest_closed_promise = writer.borrow().closed_promise.promise.clone(); + + // Set source.[[disturbed]] to true. + source.disturbed = true; + + let current_write = Rc::new(RefCell::new( + source + .promise_primordials + .promise_resolved_with_undefined + .clone(), + )); + + let promise_primordials = source.promise_primordials.clone(); + let constructor_type_error = source.constructor_type_error.clone(); + + let mut pipe_to = PipeTo { + source_objects: ReadableStreamClassObjects { + stream: source.into_inner(), + controller: source_controller, + reader, + }, + dest_objects: WritableStreamClassObjects { + stream: dest.into_inner(), + controller: dest_controller, + writer, + }, + current_write, + // Let shuttingDown be false. + shutting_down: Rc::new(AtomicBool::new(false)), + signal, + abort_callback: None, + // Let promise be a new promise. + promise: ResolveablePromise::new(&ctx)?, + promise_primordials: promise_primordials.clone(), + }; + + // If signal is not undefined, + if let Some(signal) = &pipe_to.signal { + // Let abortAlgorithm be the following steps: + let abort_algorithm = { + let signal = signal.clone(); + let pipe_to = pipe_to.clone(); + move |ctx: Ctx<'js>| -> Result<()> { + // Let error be signal’s abort reason. + let error = signal.borrow().reason().unwrap_or_undefined(&ctx); + + // Let actions be an empty ordered set. + let mut actions = + Vec::) -> Result>>>::new(); + + // If preventAbort is false, append the following action to actions: + if !prevent_abort { + let dest_objects = pipe_to.dest_objects.clone(); + let error = error.clone(); + actions.push(Box::new(move |ctx| { + let dest_objects = WritableStreamObjects::from_class(dest_objects); + + if matches!(dest_objects.stream.state, WritableStreamState::Writable) { + // If dest.[[state]] is "writable", return ! WritableStreamAbort(dest, error). + let (promise, _) = WritableStream::writable_stream_abort( + ctx, + dest_objects, + Some(error.clone()), + )?; + Ok(promise) + } else { + // Otherwise, return a promise resolved with undefined. + Ok(dest_objects + .stream + .promise_primordials + .promise_resolved_with_undefined + .clone()) + } + })); + } + + // If preventCancel is false, append the following action action to actions: + if !prevent_cancel { + let source_objects = pipe_to.source_objects.clone(); + let error = error.clone(); + actions.push(Box::new(move |ctx| { + let source_objects = ReadableStreamObjects::from_class(source_objects); + + if let ReadableStreamState::Readable = source_objects.stream.state { + // If source.[[state]] is "readable", return ! ReadableStreamCancel(source, error). + let (promise, _) = ReadableStream::readable_stream_cancel( + ctx, + source_objects, + error.clone(), + )?; + + Ok(promise) + } else { + // Otherwise, return a promise resolved with undefined. + Ok(source_objects + .stream + .promise_primordials + .promise_resolved_with_undefined + .clone()) + } + })); + } + + // Shutdown with an action consisting of getting a promise to wait for all of the actions in actions, and with error. + pipe_to.shutdown_with_action( + ctx, + move |ctx| { + let promises: Vec> = actions + .into_iter() + .map(|action| action(ctx.clone())) + .collect::>>()?; + + let all_promises: Promise<'js> = + promise_primordials.promise_all.call(( + This(promise_primordials.promise_constructor.clone()), + promises, + ))?; + + Ok(all_promises) + }, + Some(error), + ) + } + }; + + // If signal is aborted, perform abortAlgorithm and return promise. + { + let signal = signal.borrow(); + + if signal.aborted { + abort_algorithm(ctx.clone())?; + + return Ok(pipe_to.promise.promise); + } + } + + let abort_callback = pipe_to + .abort_callback + .insert(Function::new(ctx.clone(), OnceFn::new(abort_algorithm))?); + + // Add abortAlgorithm to signal. + AbortSignal::set_on_abort(This(signal.clone()), ctx.clone(), abort_callback.clone())?; + } + + // In parallel but not really; see #905, using reader and writer, read all chunks from source and write them to dest. + // Due to the locking provided by the reader and writer, the exact manner in which this happens is not observable to author code, and so there is flexibility in how this is done. + // The following constraints apply regardless of the exact algorithm used: + + // Errors must be propagated forward + PipeTo::is_or_becomes_errored( + ctx.clone(), + source_stored_error, + source_closed_promise.clone(), + { + let pipe_to = pipe_to.clone(); + move |ctx, stored_error| { + if !prevent_abort { + pipe_to.shutdown_with_action( + ctx, + { + let pipe_to = pipe_to.clone(); + let stored_error = stored_error.clone(); + move |ctx| { + let dest_objects = WritableStreamObjects::from_class( + pipe_to.dest_objects.clone(), + ); + + let (promise, _) = WritableStream::writable_stream_abort( + ctx, + dest_objects, + Some(stored_error), + )?; + + Ok(promise) + } + }, + Some(stored_error), + ) + } else { + pipe_to.shutdown(ctx, Some(stored_error)) + } + } + }, + )?; + + // Errors must be propagated backward + PipeTo::is_or_becomes_errored(ctx.clone(), dest_stored_error, dest_closed_promise, { + let pipe_to = pipe_to.clone(); + move |ctx, stored_error| { + if !prevent_cancel { + pipe_to.shutdown_with_action( + ctx, + { + let pipe_to = pipe_to.clone(); + let stored_error = stored_error.clone(); + move |ctx| { + let source_objects = ReadableStreamObjects::from_class( + pipe_to.source_objects.clone(), + ); + + let (promise, _) = ReadableStream::readable_stream_cancel( + ctx, + source_objects, + stored_error, + )?; + + Ok(promise) + } + }, + Some(stored_error), + ) + } else { + pipe_to.shutdown(ctx, Some(stored_error)) + } + } + })?; + + // Closing must be propagated forward + PipeTo::is_or_becomes_closed(ctx.clone(), source_closed, source_closed_promise, { + let pipe_to = pipe_to.clone(); + move |ctx| { + if !prevent_close { + pipe_to.shutdown_with_action( + ctx, + { + let pipe_to = pipe_to.clone(); + move |ctx| { + let dest_objects = WritableStreamObjects::from_class(pipe_to.dest_objects); + + WritableStreamDefaultWriter::writable_stream_default_writer_close_with_error_propagation(ctx, dest_objects) + } + }, + None, + ) + } else { + pipe_to.shutdown(ctx, None) + } + } + })?; + + // Closing must be propagated backward + if dest_closing { + let dest_closed: Value<'js> = constructor_type_error.call(( + "the destination writable stream closed before all data could be piped to it", + ))?; + + if !prevent_cancel { + pipe_to.shutdown_with_action( + ctx.clone(), + { + let pipe_to = pipe_to.clone(); + let dest_closed = dest_closed.clone(); + move |ctx| { + let source_objects = + ReadableStreamObjects::from_class(pipe_to.source_objects.clone()); + + let (promise, _) = ReadableStream::readable_stream_cancel( + ctx, + source_objects, + dest_closed, + )?; + + Ok(promise) + } + }, + Some(dest_closed), + )?; + } else { + pipe_to.shutdown(ctx.clone(), Some(dest_closed))?; + } + } + + let result_promise = pipe_to.promise.promise.clone(); + let pipe_loop_promise = pipe_to.pipe_loop(ctx)?; + pipe_loop_promise.set_is_handled()?; + + Ok(result_promise) + } +} + +#[derive(Clone)] +struct PipeTo<'js> { + source_objects: ReadableStreamClassObjects< + 'js, + ReadableStreamControllerOwned<'js>, + ReadableStreamDefaultReaderOwned<'js>, + >, + dest_objects: WritableStreamClassObjects<'js, WritableStreamDefaultWriterOwned<'js>>, + current_write: Rc>>, + shutting_down: Rc, + signal: Option>>, + abort_callback: Option>, + promise: ResolveablePromise<'js>, + + promise_primordials: PromisePrimordials<'js>, +} + +impl<'js> PipeTo<'js> { + // Using reader and writer, read all chunks from this and write them to dest + // - Backpressure must be enforced + // - Shutdown must stop all activity + fn pipe_loop(self, ctx: Ctx<'js>) -> Result> { + let loop_promise = ResolveablePromise::new(&ctx)?; + + self.next(ctx, false, loop_promise.clone())?; + + Ok(loop_promise) + } + + fn next(&self, ctx: Ctx<'js>, done: bool, loop_promise: ResolveablePromise<'js>) -> Result<()> { + if done { + loop_promise.resolve_undefined()? + } else { + let pipe_step_promise = self.pipe_step(ctx.clone())?; + upon_promise(ctx, pipe_step_promise, { + { + let pipe_to = self.clone(); + move |ctx, result| match result { + Ok(done) => pipe_to.next(ctx, done, loop_promise), + Err(err) => loop_promise.reject(err), + } + } + })?; + } + + Ok(()) + } + + fn pipe_step(&self, ctx: Ctx<'js>) -> Result> { + if self.shutting_down.load(Ordering::Acquire) { + return promise_resolved_with( + &ctx, + &self.promise_primordials, + Ok(Value::new_bool(ctx.clone(), true)), + ); + } + + let writer_ready = self + .dest_objects + .writer + .borrow() + .ready_promise + .promise + .clone(); + + upon_promise_fulfilment(ctx, writer_ready, { + let current_write = self.current_write.clone(); + let source_objects = self.source_objects.clone(); + let dest_objects = self.dest_objects.clone(); + move |ctx: Ctx<'js>, ()| -> Result> { + let read_promise = ResolveablePromise::new(&ctx)?; + + struct ReadRequest<'js> { + dest_objects: + WritableStreamClassObjects<'js, WritableStreamDefaultWriterOwned<'js>>, + current_write: Rc>>, + read_promise: ResolveablePromise<'js>, + } + + impl<'js> Trace<'js> for ReadRequest<'js> { + fn trace<'a>(&self, tracer: rquickjs::class::Tracer<'a, 'js>) { + self.current_write.as_ref().borrow().trace(tracer); + self.read_promise.trace(tracer); + } + } + + impl<'js> ReadableStreamReadRequest<'js> for ReadRequest<'js> { + fn chunk_steps( + &self, + objects: ReadableStreamDefaultReaderObjects<'js>, + chunk: Value<'js>, + ) -> Result> { + let ctx = chunk.ctx().clone(); + + // calling write can trigger user code; ensure we don't hold locks + let objects = objects.into_inner(); + + let dest_objects = + WritableStreamObjects::from_class(self.dest_objects.clone()); + let write_promise = + WritableStreamDefaultWriter::writable_stream_default_writer_write( + ctx.clone(), + dest_objects, + chunk, + )?; + + let write_promise: Promise<'js> = write_promise.catch()?.call(( + This(write_promise.clone()), + Function::new(ctx.clone(), || {}), + ))?; + + self.current_write.replace(write_promise); + self.read_promise + .resolve(Value::new_bool(ctx.clone(), false))?; + + Ok(ReadableStreamObjects::from_class(objects)) + } + + fn close_steps( + &self, + ctx: &Ctx<'js>, + objects: ReadableStreamDefaultReaderObjects<'js>, + ) -> Result> { + self.read_promise + .resolve(Value::new_bool(ctx.clone(), true))?; + + Ok(objects) + } + + fn error_steps( + &self, + objects: ReadableStreamDefaultReaderObjects<'js>, + reason: Value<'js>, + ) -> Result> { + self.read_promise.reject(reason)?; + + Ok(objects) + } + } + + let objects = ReadableStreamObjects::from_class(source_objects); + + let promise = read_promise.promise.clone(); + + ReadableStreamDefaultReader::readable_stream_default_reader_read( + &ctx, + objects, + ReadRequest { + current_write, + read_promise, + dest_objects, + }, + )?; + + Ok(promise) + } + }) + } + + fn is_or_becomes_errored( + ctx: Ctx<'js>, + stored_error: Option>, + promise: Promise<'js>, + action: impl FnOnce(Ctx<'js>, Value<'js>) -> Result<()> + 'js, + ) -> Result<()> { + if let Some(stored_error) = stored_error { + action(ctx, stored_error) + } else { + promise.catch()?.call(( + This(promise.clone()), + Function::new(ctx.clone(), OnceFn::new(action)), + )) + } + } + + fn is_or_becomes_closed( + ctx: Ctx<'js>, + already_closed: bool, + promise: Promise<'js>, + action: impl FnOnce(Ctx<'js>) -> Result<()> + 'js, + ) -> Result<()> { + if already_closed { + action(ctx)?; + } else { + upon_promise_fulfilment(ctx, promise, |ctx, ()| action(ctx))?; + } + Ok(()) + } + + fn shutdown_with_action( + &self, + ctx: Ctx<'js>, + action: impl FnOnce(Ctx<'js>) -> Result> + 'js, + original_error: Option>, + ) -> Result<()> { + if self.shutting_down.swap(true, Ordering::AcqRel) { + // already shutting down + return Ok(()); + } + + let do_the_rest = { + let pipe_to = self.clone(); + move |ctx: Ctx<'js>| -> Result<()> { + let action_promise = action(ctx.clone())?; + upon_promise(ctx, action_promise, move |ctx, result| match result { + Ok(()) => pipe_to.finalize(ctx, original_error), + Err(new_error) => pipe_to.finalize(ctx, Some(new_error)), + })?; + Ok(()) + } + }; + + let writable = { + let dest_stream = OwnedBorrow::from_class(self.dest_objects.stream.clone()); + matches!(dest_stream.state, WritableStreamState::Writable) + && !dest_stream.writable_stream_close_queued_or_in_flight() + }; + + if writable { + let wait_promise = + Self::wait_for_writes_to_finish(ctx.clone(), self.current_write.clone())?; + upon_promise_fulfilment(ctx, wait_promise, |ctx: Ctx<'js>, ()| do_the_rest(ctx))?; + } else { + do_the_rest(ctx)? + } + Ok(()) + } + + fn shutdown(&self, ctx: Ctx<'js>, error: Option>) -> Result<()> { + if self.shutting_down.swap(true, Ordering::AcqRel) { + // already shutting down + return Ok(()); + } + + let writable = { + let dest_stream = OwnedBorrow::from_class(self.dest_objects.stream.clone()); + matches!(dest_stream.state, WritableStreamState::Writable) + && !dest_stream.writable_stream_close_queued_or_in_flight() + }; + + if writable { + let wait_promise = + Self::wait_for_writes_to_finish(ctx.clone(), self.current_write.clone())?; + let pipe_to = self.clone(); + upon_promise_fulfilment(ctx, wait_promise, move |ctx, ()| { + pipe_to.finalize(ctx, error) + })?; + } else { + self.finalize(ctx, error)?; + } + Ok(()) + } + + fn wait_for_writes_to_finish( + ctx: Ctx<'js>, + current_write: Rc>>, + ) -> Result> { + let old_current_write: Promise<'js> = current_write.as_ref().borrow().clone(); + + upon_promise_fulfilment( + ctx, + old_current_write.clone(), + move |ctx: Ctx<'js>, ()| -> Result>> { + if !old_current_write.eq(¤t_write.as_ref().borrow()) { + Ok(Undefined(Some(Self::wait_for_writes_to_finish( + ctx, + current_write, + )?))) + } else { + Ok(Undefined(None)) + } + }, + ) + } + + fn finalize(&self, ctx: Ctx<'js>, error: Option>) -> Result<()> { + let source_objects = ReadableStreamObjects::from_class(self.source_objects.clone()); + let dest_objects = WritableStreamObjects::from_class(self.dest_objects.clone()); + + WritableStreamDefaultWriter::writable_stream_default_writer_release(dest_objects)?; + ReadableStreamDefaultReader::readable_stream_default_reader_release(source_objects)?; + + if let (Some(signal), Some(abort_callback)) = (&self.signal, &self.abort_callback) { + AbortSignal::remove_on_abort( + This(signal.clone()), + ctx.clone(), + abort_callback.clone(), + )?; + } + + if let Some(error) = error { + self.promise.reject(error) + } else { + self.promise.resolve_undefined() + } + } +} + +#[derive(Default)] +pub struct StreamPipeOptions<'js> { + pub prevent_close: bool, + pub prevent_abort: bool, + pub prevent_cancel: bool, + pub signal: Option>>, +} + +impl<'js> FromJs<'js> for StreamPipeOptions<'js> { + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let ty_name = value.type_name(); + let obj = value + .as_object() + .ok_or(Error::new_from_js(ty_name, "Object"))?; + + let get_bool = |key| { + Result::Ok( + obj.get_value_or_undefined::<_, Coerced>(key)? + .map(|b| b.0) + .unwrap_or(false), + ) // missing is treated as false + }; + + let prevent_abort = get_bool("preventAbort")?; + let prevent_cancel = get_bool("preventCancel")?; + let prevent_close = get_bool("preventClose")?; + + let signal = match obj.get_value_or_undefined::<_, Value<'js>>("signal")? { + Some(signal) => Some( + Class::::from_js(ctx, signal) + .or_throw_type(ctx, "Invalid signal argument")?, + ), + None => None, + }; + + Ok(Self { + prevent_close, + prevent_abort, + prevent_cancel, + signal, + }) + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/stream/source.rs b/stdlib/src/llrt/llrt_stream_web/readable/stream/source.rs new file mode 100644 index 00000000..b6afad48 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/readable/stream/source.rs @@ -0,0 +1,36 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use rquickjs::{Function, Object, Result}; + +use crate::llrt_stream_web::{readable::stream::ReadableStreamType, utils::ValueOrUndefined}; + +#[derive(Default)] +pub(crate) struct UnderlyingSource<'js> { + // callback UnderlyingSourceStartCallback = any (ReadableStreamController controller); + pub(crate) start: Option>, + // callback UnderlyingSourcePullCallback = Promise (ReadableStreamController controller); + pub(crate) pull: Option>, + // callback UnderlyingSourceCancelCallback = Promise (optional any reason); + pub(crate) cancel: Option>, + pub(super) r#type: Option, + // [EnforceRange] unsigned long long autoAllocateChunkSize; + pub(crate) auto_allocate_chunk_size: Option, +} + +impl<'js> UnderlyingSource<'js> { + pub(super) fn from_object(obj: Object<'js>) -> Result { + let start = obj.get_value_or_undefined::<_, _>("start")?; + let pull = obj.get_value_or_undefined::<_, _>("pull")?; + let cancel = obj.get_value_or_undefined::<_, _>("cancel")?; + let r#type = obj.get_value_or_undefined::<_, _>("type")?; + let auto_allocate_chunk_size = + obj.get_value_or_undefined::<_, _>("autoAllocateChunkSize")?; + + Ok(Self { + start, + pull, + cancel, + r#type, + auto_allocate_chunk_size, + }) + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/stream/tee.rs b/stdlib/src/llrt/llrt_stream_web/readable/stream/tee.rs new file mode 100644 index 00000000..4303fadb --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/readable/stream/tee.rs @@ -0,0 +1,1713 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use std::{ + cell::{OnceCell, RefCell}, + rc::Rc, + sync::atomic::{AtomicBool, Ordering}, +}; + +use rquickjs::{ + class::{OwnedBorrowMut, Trace, Tracer}, + function::Constructor, + prelude::{List, OnceFn}, + ArrayBuffer, Class, Ctx, Error, Function, IntoJs, JsLifetime, Promise, Result, Value, +}; + +use crate::llrt_stream_web::{ + readable::{ + byob_reader::{ReadableStreamBYOBReader, ReadableStreamReadIntoRequest, ViewBytes}, + byte_controller::{ReadableByteStreamController, ReadableByteStreamControllerOwned}, + controller::{ReadableStreamController, ReadableStreamControllerClass}, + default_controller::{ + ReadableStreamDefaultController, ReadableStreamDefaultControllerOwned, + }, + default_reader::{ + ReadableStreamDefaultReader, ReadableStreamDefaultReaderOwned, + ReadableStreamReadRequest, + }, + objects::{ReadableByteStreamObjects, ReadableStreamDefaultControllerObjects}, + objects::{ + ReadableStreamBYOBObjects, ReadableStreamClassObjects, + ReadableStreamDefaultReaderObjects, ReadableStreamObjects, + }, + reader::{ + ReadableStreamReader, ReadableStreamReaderClass, ReadableStreamReaderOwned, + UndefinedReader, + }, + stream::{ + algorithms::{CancelAlgorithm, PullAlgorithm, StartAlgorithm}, + ReadableStream, ReadableStreamClass, + }, + }, + utils::promise::{upon_promise, ResolveablePromise}, +}; + +/// State for tee() operation, stored in a Class for GC tracing +#[rquickjs::class] +pub(crate) struct TeeState<'js> { + pub(super) stream: ReadableStreamClass<'js>, + pub(super) controller: Class<'js, ReadableStreamDefaultController<'js>>, + pub(super) reader: Class<'js, ReadableStreamDefaultReader<'js>>, + pub(super) cancel_promise: ResolveablePromise<'js>, + pub(super) reading: Rc, + pub(super) read_again: Rc, + pub(super) reason_1: Rc>>, + pub(super) reason_2: Rc>>, + pub(super) branch_1: Rc< + OnceCell< + ReadableStreamClassObjects< + 'js, + ReadableStreamDefaultControllerOwned<'js>, + UndefinedReader, + >, + >, + >, + pub(super) branch_2: Rc< + OnceCell< + ReadableStreamClassObjects< + 'js, + ReadableStreamDefaultControllerOwned<'js>, + UndefinedReader, + >, + >, + >, +} + +unsafe impl<'js> JsLifetime<'js> for TeeState<'js> { + type Changed<'to> = TeeState<'to>; +} + +impl<'js> Trace<'js> for TeeState<'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + self.stream.trace(tracer); + self.controller.trace(tracer); + self.reader.trace(tracer); + self.cancel_promise.trace(tracer); + if let Some(r) = self.reason_1.get() { + r.trace(tracer); + } + if let Some(r) = self.reason_2.get() { + r.trace(tracer); + } + if let Some(b) = self.branch_1.get() { + b.trace(tracer); + } + if let Some(b) = self.branch_2.get() { + b.trace(tracer); + } + } +} + +type ReadableStreamPair<'js> = (ReadableStreamClass<'js>, ReadableStreamClass<'js>); + +impl<'js> ReadableStream<'js> { + pub(super) fn readable_stream_tee>( + ctx: Ctx<'js>, + objects: ReadableStreamObjects<'js, C, UndefinedReader>, + ) -> Result> { + let (streams, _) = objects.with_controller( + ctx, + |ctx, objects| { + let (streams, objects) = Self::readable_stream_default_tee(ctx, objects)?; + Ok((streams, objects.clear_reader())) + }, + |ctx, objects| { + // If stream.[[controller]] implements ReadableByteStreamController, return ? ReadableByteStreamTee(stream). + Self::readable_byte_stream_tee(ctx, objects) + }, + )?; + + Ok(streams) + } + + fn readable_stream_default_tee( + ctx: Ctx<'js>, + mut objects: ReadableStreamDefaultControllerObjects<'js, UndefinedReader>, + ) -> Result<( + ReadableStreamPair<'js>, + ReadableStreamDefaultControllerObjects<'js, ReadableStreamDefaultReaderOwned<'js>>, + )> { + // Let reader be ? AcquireReadableStreamDefaultReader(stream). + let (stream, reader) = ReadableStreamReaderClass::acquire_readable_stream_default_reader( + ctx.clone(), + objects.stream, + )?; + objects.stream = stream; + // Let reading be false. + let reading = Rc::new(AtomicBool::new(false)); + // Let readAgain be false. + let read_again = Rc::new(AtomicBool::new(false)); + // Let canceled1 be false. + // Let canceled2 be false. + // Let reason1 be undefined. + let reason_1 = Rc::new(OnceCell::new()); + // Let reason2 be undefined. + let reason_2 = Rc::new(OnceCell::new()); + // Let branch1 be undefined. + let branch_1: Rc< + OnceCell< + ReadableStreamClassObjects< + 'js, + ReadableStreamDefaultControllerOwned<'js>, + UndefinedReader, + >, + >, + > = Rc::new(OnceCell::new()); + // Let branch2 be undefined. + let branch_2: Rc< + OnceCell< + ReadableStreamClassObjects< + 'js, + ReadableStreamDefaultControllerOwned<'js>, + UndefinedReader, + >, + >, + > = Rc::new(OnceCell::new()); + // Let cancelPromise be a new promise. + let cancel_promise = ResolveablePromise::new(&ctx)?; + + // Let startAlgorithm be an algorithm that returns undefined. + let start_algorithm = StartAlgorithm::ReturnUndefined; + + let objects_class: ReadableStreamClassObjects< + 'js, + ReadableStreamDefaultControllerOwned<'js>, + ReadableStreamDefaultReaderOwned<'js>, + > = objects.into_inner().set_reader(reader); + + // Create TeeState to hold all JS values for GC tracing + let tee_state = Class::instance( + ctx.clone(), + TeeState { + stream: objects_class.stream.clone(), + controller: objects_class.controller.clone(), + reader: objects_class.reader.clone(), + cancel_promise: cancel_promise.clone(), + reading: reading.clone(), + read_again: read_again.clone(), + reason_1: reason_1.clone(), + reason_2: reason_2.clone(), + branch_1: branch_1.clone(), + branch_2: branch_2.clone(), + }, + )?; + + let pull_algorithm = PullAlgorithm::from_tee_state(tee_state.clone()); + let cancel_algorithm_1 = CancelAlgorithm::from_tee_state_1(tee_state.clone()); + let cancel_algorithm_2 = CancelAlgorithm::from_tee_state_2(tee_state.clone()); + + // Set branch1 to ! CreateReadableStream(startAlgorithm, pullAlgorithm, cancel1Algorithm). + let branch_1_objects = { + let objects = Self::create_readable_stream( + ctx.clone(), + start_algorithm.clone(), + pull_algorithm.clone(), + cancel_algorithm_1, + None, + None, + )?; + _ = branch_1.set(objects.clone()); + objects + }; + + // Set branch2 to ! CreateReadableStream(startAlgorithm, pullAlgorithm, cancel2Algorithm). + let branch_2_objects = { + let objects = Self::create_readable_stream( + ctx.clone(), + start_algorithm, + pull_algorithm, + cancel_algorithm_2, + None, + None, + )?; + _ = branch_2.set(objects.clone()); + objects + }; + + upon_promise( + ctx.clone(), + objects_class + .reader + .borrow() + .generic + .closed_promise + .promise + .clone(), + { + let tee_state = tee_state.clone(); + let branch_1_objects = branch_1_objects.clone(); + let branch_2_objects = branch_2_objects.clone(); + move |_, result| match result { + Ok(()) => Ok(()), + // Upon rejection of reader.[[closedPromise]] with reason r, + Err(reason) => { + // Perform ! ReadableStreamDefaultControllerError(branch1.[[controller]], r). + let objects_1 = + ReadableStreamObjects::from_class_no_reader(branch_1_objects) + .refresh_reader(); + ReadableStreamDefaultController::readable_stream_default_controller_error( + objects_1, + reason.clone(), + )?; + + // Perform ! ReadableStreamDefaultControllerError(branch2.[[controller]], r). + let objects_2 = + ReadableStreamObjects::from_class_no_reader(branch_2_objects) + .refresh_reader(); + ReadableStreamDefaultController::readable_stream_default_controller_error( + objects_2, reason, + )?; + // If canceled1 is false or canceled2 is false, resolve cancelPromise with undefined. + let state = tee_state.borrow(); + if state.reason_1.get().is_none() || state.reason_2.get().is_none() { + state.cancel_promise.resolve_undefined()?; + } + + Ok(()) + } + } + }, + )?; + + Ok(( + (branch_1_objects.stream, branch_2_objects.stream), + ReadableStreamObjects::from_class(objects_class), + )) + } +} + +/// Pull algorithm for tee - called from PullAlgorithm::Tee +pub fn tee_pull_algorithm<'js>( + ctx: Ctx<'js>, + state: Class<'js, TeeState<'js>>, +) -> Result> { + let state_ref = state.borrow(); + + // If reading is true, set readAgain to true and return resolved promise + if state_ref.reading.load(Ordering::Acquire) { + state_ref.read_again.store(true, Ordering::Release); + return Ok(state_ref + .stream + .borrow() + .promise_primordials + .promise_resolved_with_undefined + .clone()); + } + + // Set reading to true + state_ref.reading.store(true, Ordering::Release); + + let objects_class: ReadableStreamClassObjects< + 'js, + ReadableStreamDefaultControllerOwned<'js>, + ReadableStreamDefaultReaderOwned<'js>, + > = ReadableStreamClassObjects { + stream: state_ref.stream.clone(), + controller: state_ref.controller.clone(), + reader: state_ref.reader.clone(), + }; + drop(state_ref); + + let mut objects = ReadableStreamObjects::from_class(objects_class.clone()); + + // ReadRequest that just holds TeeState + #[derive(Clone)] + struct TeeReadRequest<'js>(Class<'js, TeeState<'js>>); + + impl<'js> Trace<'js> for TeeReadRequest<'js> { + fn trace<'a>(&self, tracer: rquickjs::class::Tracer<'a, 'js>) { + self.0.trace(tracer); + } + } + + impl<'js> ReadableStreamReadRequest<'js> for TeeReadRequest<'js> { + fn chunk_steps( + &self, + objects: ReadableStreamDefaultReaderObjects<'js>, + chunk: Value<'js>, + ) -> Result> { + let ctx = chunk.ctx().clone(); + let state = self.0.clone(); + + objects.with_assert_default_controller(|objects| { + let objects_class = objects.into_inner(); + let f = { + let ctx = ctx.clone(); + let _objects_class = objects_class.clone(); + move || -> Result<()> { + let s = state.borrow(); + s.read_again.store(false, Ordering::Release); + + let chunk_1 = chunk.clone(); + let chunk_2 = chunk; + + if s.reason_1.get().is_none() { + let objects_1 = ReadableStreamObjects::from_class( + s.branch_1.get().cloned().expect("branch1 not set"), + ) + .refresh_reader(); + ReadableStreamDefaultController::readable_stream_default_controller_enqueue( + ctx.clone(), + objects_1, + chunk_1, + )?; + } + + if s.reason_2.get().is_none() { + let objects_2 = ReadableStreamObjects::from_class( + s.branch_2.get().cloned().expect("branch2 not set"), + ) + .refresh_reader(); + ReadableStreamDefaultController::readable_stream_default_controller_enqueue( + ctx.clone(), + objects_2, + chunk_2, + )?; + } + + s.reading.store(false, Ordering::Release); + + if s.read_again.load(Ordering::Acquire) { + drop(s); + tee_pull_algorithm(ctx, state)?; + } + + Ok(()) + } + }; + + let () = Function::new(ctx, OnceFn::new(f))?.defer(())?; + Ok(ReadableStreamObjects::from_class(objects_class)) + }) + } + + fn close_steps( + &self, + ctx: &Ctx<'js>, + objects: ReadableStreamDefaultReaderObjects<'js>, + ) -> Result> { + let s = self.0.borrow(); + s.reading.store(false, Ordering::Release); + + if s.reason_1.get().is_none() { + let objects_1 = ReadableStreamObjects::from_class( + s.branch_1.get().cloned().expect("branch1 not set"), + ) + .refresh_reader(); + ReadableStreamDefaultController::readable_stream_default_controller_close( + ctx.clone(), + objects_1, + )?; + } + + if s.reason_2.get().is_none() { + let objects_2 = ReadableStreamObjects::from_class( + s.branch_2.get().cloned().expect("branch2 not set"), + ) + .refresh_reader(); + ReadableStreamDefaultController::readable_stream_default_controller_close( + ctx.clone(), + objects_2, + )?; + } + + if s.reason_1.get().is_none() || s.reason_2.get().is_none() { + s.cancel_promise.resolve_undefined()?; + } + + Ok(objects) + } + + fn error_steps( + &self, + objects: ReadableStreamDefaultReaderObjects<'js>, + _e: Value<'js>, + ) -> Result> { + self.0.borrow().reading.store(false, Ordering::Release); + Ok(objects) + } + } + + objects = ReadableStreamDefaultReader::readable_stream_default_reader_read( + &ctx, + objects, + TeeReadRequest(state), + )?; + + Ok(objects + .stream + .promise_primordials + .promise_resolved_with_undefined + .clone()) +} + +/// Cancel algorithm for tee - called from CancelAlgorithm::Tee1/Tee2 +pub fn tee_cancel_algorithm<'js>( + ctx: Ctx<'js>, + state: Class<'js, TeeState<'js>>, + reason: Value<'js>, + branch: usize, +) -> Result> { + let state_ref = state.borrow(); + let objects_class: ReadableStreamClassObjects< + 'js, + ReadableStreamDefaultControllerOwned<'js>, + ReadableStreamDefaultReaderOwned<'js>, + > = ReadableStreamClassObjects { + stream: state_ref.stream.clone(), + controller: state_ref.controller.clone(), + reader: state_ref.reader.clone(), + }; + let objects = ReadableStreamObjects::from_class(objects_class); + ReadableStream::tee_cancel_algorithm_impl( + ctx, + objects, + [&state_ref.reason_1, &state_ref.reason_2], + state_ref.cancel_promise.clone(), + reason, + branch, + ) +} + +impl<'js> ReadableStream<'js> { + // Cancel algorithm for tee - handles both branches + fn tee_cancel_algorithm_impl( + ctx: Ctx<'js>, + objects: ReadableStreamObjects< + 'js, + impl ReadableStreamController<'js>, + impl ReadableStreamReader<'js>, + >, + reasons: [&Rc>>; 2], + cancel_promise: ResolveablePromise<'js>, + reason: Value<'js>, + branch: usize, + ) -> Result> { + let other = 1 - branch; + + // Set canceled[branch] to true, set reason[branch] to reason + reasons[branch] + .set(reason.clone()) + .expect("tee stream already has a cancel reason"); + + // If other branch is also canceled + if let Some(other_reason) = reasons[other].get().cloned() { + // CreateArrayFromList with reasons in correct order + let composite_reason = if branch == 0 { + List((reason, other_reason)) + } else { + List((other_reason, reason)) + }; + let (cancel_result, _) = ReadableStream::readable_stream_cancel( + ctx.clone(), + objects, + composite_reason.into_js(&ctx)?, + )?; + cancel_promise.resolve(cancel_result)?; + } + + Ok(cancel_promise.promise) + } + + fn readable_byte_stream_tee( + ctx: Ctx<'js>, + mut objects: ReadableByteStreamObjects<'js, UndefinedReader>, + ) -> Result<( + ReadableStreamPair<'js>, + ReadableByteStreamObjects<'js, UndefinedReader>, + )> { + // Let reader be ? AcquireReadableStreamDefaultReader(stream). + let (stream, reader) = ReadableStreamReaderClass::acquire_readable_stream_default_reader( + ctx.clone(), + objects.stream, + )?; + objects.stream = stream; + let reader: Rc>> = + Rc::new(RefCell::new(reader.into())); + // Let reading be false. + let reading = Rc::new(AtomicBool::new(false)); + // Let readAgainForBranch1 be false. + let read_again_for_branch_1 = Rc::new(AtomicBool::new(false)); + // Let readAgainForBranch2 be false. + let read_again_for_branch_2 = Rc::new(AtomicBool::new(false)); + // Let canceled1 be false. + // Let canceled2 be false. + // Let reason1 be undefined. + let reason_1 = Rc::new(OnceCell::new()); + // Let reason2 be undefined. + let reason_2 = Rc::new(OnceCell::new()); + // Let branch1 be undefined. + let branch_1: Rc>> = Rc::new(OnceCell::new()); + // Let branch2 be undefined. + let branch_2: Rc>> = Rc::new(OnceCell::new()); + // Let cancelPromise be a new promise. + let cancel_promise = ResolveablePromise::new(&ctx)?; + + let objects_class = objects.into_inner(); + + // Let pull1Algorithm be the following steps: + let pull_1_algorithm = PullAlgorithm::from_fn({ + let objects_class = objects_class.clone(); + let reader = reader.clone(); + let reading = reading.clone(); + let read_again_for_branch_1 = read_again_for_branch_1.clone(); + let read_again_for_branch_2 = read_again_for_branch_2.clone(); + let reason_1 = reason_1.clone(); + let reason_2 = reason_2.clone(); + let branch_1 = branch_1.clone(); + let branch_2 = branch_2.clone(); + let cancel_promise = cancel_promise.clone(); + move |ctx, branch_1_controller| { + let objects = ReadableStreamObjects::from_class(objects_class.clone()); + + let branch_1_controller = OwnedBorrowMut::from_class(match branch_1_controller { + ReadableStreamControllerClass::ReadableStreamByteController(c) => c, + _ => panic!( + "ReadableByteStream tee pull1 algorithm called without branch1 having a byte controller" + ), + }); + + let branch_2 = OwnedBorrowMut::from_class(branch_2.get().cloned().expect("ReadableByteStream tee pull1 algorithm called without branch2 being initialised")); + let branch_2_controller = match branch_2.controller { + ReadableStreamControllerClass::ReadableStreamByteController(ref c) => { + OwnedBorrowMut::from_class(c.clone()) + } + _ => { + panic!("ReadableByteStream tee pull1 algorithm called without branch2 having a byte controller") + } + }; + + Self::readable_byte_stream_pull_1_algorithm( + ctx, + objects, + reader.clone(), + reading.clone(), + read_again_for_branch_1.clone(), + read_again_for_branch_2.clone(), + reason_1.clone(), + reason_2.clone(), + ReadableStreamObjects::new_byte(OwnedBorrowMut::from_class(branch_1.get().cloned().expect("ReadableByteStream tee pull1 algorithm called without branch1 being initialised")), branch_1_controller) , + ReadableStreamObjects::new_byte(branch_2, branch_2_controller), + cancel_promise.clone(), + ) + } + }); + + // Let pull2Algorithm be the following steps: + let pull_2_algorithm = PullAlgorithm::from_fn({ + let objects_class = objects_class.clone(); + let reader = reader.clone(); + let reading = reading.clone(); + let read_again_for_branch_1 = read_again_for_branch_1.clone(); + let read_again_for_branch_2 = read_again_for_branch_2.clone(); + let reason_1 = reason_1.clone(); + let reason_2 = reason_2.clone(); + let branch_1 = branch_1.clone(); + let branch_2 = branch_2.clone(); + let cancel_promise = cancel_promise.clone(); + move |ctx, branch_2_controller| { + let objects = ReadableStreamObjects::from_class(objects_class.clone()); + + let branch_2 = OwnedBorrowMut::from_class(branch_2.get().cloned().expect("ReadableByteStream tee pull2 algorithm called without branch2 being initialised")); + let branch_2_controller = match branch_2_controller { + ReadableStreamControllerClass::ReadableStreamByteController(ref c) => { + OwnedBorrowMut::from_class(c.clone()) + } + _ => { + panic!("ReadableByteStream tee pull2 algorithm called without branch2 having a byte controller") + } + }; + + let branch_1 = OwnedBorrowMut::from_class(branch_1.get().cloned().expect("ReadableByteStream tee pull2 algorithm called without branch1 being initialised")); + let branch_1_controller = match branch_1.controller { + ReadableStreamControllerClass::ReadableStreamByteController(ref c) => { + OwnedBorrowMut::from_class(c.clone()) + } + _ => { + panic!("ReadableByteStream tee pull2 algorithm called without branch1 having a byte controller") + } + }; + Self::readable_byte_stream_pull_2_algorithm( + ctx, + objects, + reader.clone(), + reading.clone(), + read_again_for_branch_1.clone(), + read_again_for_branch_2.clone(), + reason_1.clone(), + reason_2.clone(), + ReadableStreamObjects::new_byte(branch_1, branch_1_controller), + ReadableStreamObjects::new_byte(branch_2, branch_2_controller), + cancel_promise.clone(), + ) + } + }); + + let cancel_algorithm_1 = CancelAlgorithm::from_fn({ + let objects_class = objects_class.clone(); + let reader = reader.clone(); + let reason_1 = reason_1.clone(); + let reason_2 = reason_2.clone(); + let cancel_promise = cancel_promise.clone(); + move |reason: Value<'js>| { + let reader = ReadableStreamReaderOwned::from_class(reader.borrow().clone()); + let objects = ReadableStreamObjects::from_class(objects_class).set_reader(reader); + Self::tee_cancel_algorithm_impl( + reason.ctx().clone(), + objects, + [&reason_1, &reason_2], + cancel_promise, + reason, + 0, + ) + } + }); + + let cancel_algorithm_2 = CancelAlgorithm::from_fn({ + let objects_class = objects_class.clone(); + let reader = reader.clone(); + let reason_1 = reason_1.clone(); + let reason_2 = reason_2.clone(); + let cancel_promise = cancel_promise.clone(); + move |reason: Value<'js>| { + let reader = ReadableStreamReaderOwned::from_class(reader.borrow().clone()); + let objects = ReadableStreamObjects::from_class(objects_class).set_reader(reader); + Self::tee_cancel_algorithm_impl( + reason.ctx().clone(), + objects, + [&reason_1, &reason_2], + cancel_promise, + reason, + 1, + ) + } + }); + + // Let startAlgorithm be an algorithm that returns undefined. + let start_algorithm = StartAlgorithm::ReturnUndefined; + + // Set branch1 to ! CreateReadableByteStream(startAlgorithm, pull1Algorithm, cancel1Algorithm). + let objects_1 = { + let (s, c) = Self::create_readable_byte_stream( + ctx.clone(), + start_algorithm.clone(), + pull_1_algorithm.clone(), + cancel_algorithm_1, + )?; + _ = branch_1.set(s.clone()); + ReadableStreamClassObjects { + stream: s, + controller: c, + reader: UndefinedReader, + } + }; + + // Set branch2 to ! CreateReadableByteStream(startAlgorithm, pull2Algorithm, cancel2Algorithm). + let objects_2 = { + let (s, c) = Self::create_readable_byte_stream( + ctx.clone(), + start_algorithm, + pull_2_algorithm, + cancel_algorithm_2, + )?; + _ = branch_2.set(s.clone()); + ReadableStreamClassObjects { + stream: s, + controller: c, + reader: UndefinedReader, + } + }; + + // Perform forwardReaderError, given reader. + let this_reader = reader.borrow().clone(); + Self::readable_byte_stream_forward_reader_error( + ctx, + reader, + objects_1.clone(), + objects_2.clone(), + reason_1, + reason_2, + this_reader, + cancel_promise, + )?; + + // Return « branch1, branch2 ». + Ok(( + (objects_1.stream, objects_2.stream), + ReadableStreamObjects::from_class(objects_class), + )) + } + + // Let forwardReaderError be the following steps, taking a thisReader argument: + #[allow(clippy::too_many_arguments)] + fn readable_byte_stream_forward_reader_error( + ctx: Ctx<'js>, + reader: Rc>>, + objects_1: ReadableStreamClassObjects< + 'js, + ReadableByteStreamControllerOwned<'js>, + UndefinedReader, + >, + objects_2: ReadableStreamClassObjects< + 'js, + ReadableByteStreamControllerOwned<'js>, + UndefinedReader, + >, + reason_1: Rc>>, + reason_2: Rc>>, + this_reader: ReadableStreamReaderClass<'js>, + cancel_promise: ResolveablePromise<'js>, + ) -> Result<()> { + // Upon rejection of thisReader.[[closedPromise]] with reason r, + upon_promise( + ctx, + this_reader.closed_promise(), + move |_, result| match result { + Err(r) => { + // If thisReader is not reader, return. + if !reader.borrow().eq(&this_reader) { + return Ok(()); + } + + let objects_1 = + ReadableStreamObjects::from_class_no_reader(objects_1).refresh_reader(); + + // Perform ! ReadableByteStreamControllerError(branch1.[[controller]], r). + ReadableByteStreamController::readable_byte_stream_controller_error( + objects_1, + r.clone(), + )?; + + let objects_2 = + ReadableStreamObjects::from_class_no_reader(objects_2).refresh_reader(); + + // Perform ! ReadableByteStreamControllerError(branch2.[[controller]], r). + ReadableByteStreamController::readable_byte_stream_controller_error( + objects_2, + r.clone(), + )?; + + // If canceled1 is false or canceled2 is false, resolve cancelPromise with undefined. + if reason_1.get().is_none() || reason_2.get().is_none() { + cancel_promise.resolve_undefined()?; + } + Ok(()) + } + Ok(()) => Ok(()), + }, + )?; + Ok(()) + } + + // Let pullWithDefaultReader be the following steps: + #[allow(clippy::too_many_arguments)] + fn readable_byte_stream_pull_with_default_reader( + ctx: Ctx<'js>, + mut objects: ReadableByteStreamObjects<'js, UndefinedReader>, + reader: Rc>>, + reading: Rc, + read_again_for_branch_1: Rc, + read_again_for_branch_2: Rc, + reason_1: Rc>>, + reason_2: Rc>>, + objects_1: ReadableByteStreamObjects<'js, UndefinedReader>, + objects_2: ReadableByteStreamObjects<'js, UndefinedReader>, + cancel_promise: ResolveablePromise<'js>, + ) -> Result> { + let objects_class_1 = objects_1.into_inner(); + let objects_class_2 = objects_2.into_inner(); + + // If reader implements ReadableStreamBYOBReader, + let current_reader = reader.borrow().clone(); + let current_reader = match current_reader { + ReadableStreamReaderClass::ReadableStreamBYOBReader(r) => { + let byob_reader = OwnedBorrowMut::from_class(r.clone()); + + // Perform ! ReadableStreamBYOBReaderRelease(reader). + objects = ReadableStreamBYOBReader::readable_stream_byob_reader_release( + objects.set_reader(byob_reader), + )? + .clear_reader(); + // Set reader to ! AcquireReadableStreamDefaultReader(stream). + let (s, new_reader) = + ReadableStreamReaderClass::acquire_readable_stream_default_reader( + ctx.clone(), + objects.stream, + )?; + objects.stream = s; + reader.replace(new_reader.clone().into()); + + // Perform forwardReaderError, given reader. + Self::readable_byte_stream_forward_reader_error( + ctx.clone(), + reader.clone(), + objects_class_1.clone(), + objects_class_2.clone(), + reason_1.clone(), + reason_2.clone(), + new_reader.clone().into(), + cancel_promise.clone(), + )?; + new_reader + } + ReadableStreamReaderClass::ReadableStreamDefaultReader(r) => r, + }; + + // Let readRequest be a read request with the following items: + #[derive(Clone)] + struct ReadRequest<'js> { + reader: Rc>>, + reading: Rc, + read_again_for_branch_1: Rc, + read_again_for_branch_2: Rc, + reason_1: Rc>>, + reason_2: Rc>>, + objects_class_1: ReadableStreamClassObjects< + 'js, + ReadableByteStreamControllerOwned<'js>, + UndefinedReader, + >, + objects_class_2: ReadableStreamClassObjects< + 'js, + ReadableByteStreamControllerOwned<'js>, + UndefinedReader, + >, + cancel_promise: ResolveablePromise<'js>, + } + + impl<'js> Trace<'js> for ReadRequest<'js> { + fn trace<'a>(&self, tracer: rquickjs::class::Tracer<'a, 'js>) { + if let Ok(r) = self.reader.try_borrow() { + r.trace(tracer) + } + if let Some(r) = self.reason_1.get() { + r.trace(tracer) + } + if let Some(r) = self.reason_2.get() { + r.trace(tracer) + } + self.objects_class_1.trace(tracer); + self.objects_class_2.trace(tracer); + self.cancel_promise.trace(tracer); + } + } + + impl<'js> ReadableStreamReadRequest<'js> for ReadRequest<'js> { + fn chunk_steps( + &self, + objects: ReadableStreamDefaultReaderObjects<'js>, + chunk: Value<'js>, + ) -> Result> { + let ctx = chunk.ctx().clone(); + let this = self.clone(); + + objects.with_assert_byte_controller(|objects| { + let constructor_uint8array = objects.controller.array_constructor_primordials.constructor_uint8array.clone(); + let function_array_buffer_is_view = objects.controller.function_array_buffer_is_view.clone(); + let chunk = ViewBytes::from_value(&ctx, &function_array_buffer_is_view, Some(&chunk))?; + let objects_class = objects.into_inner(); + // Queue a microtask to perform the following steps: + let f = { + let ctx = ctx.clone(); + let objects_class = objects_class.clone(); + move || -> Result<()> { + // Set readAgainForBranch1 to false. + this.read_again_for_branch_1.store(false, Ordering::Release); + // Set readAgainForBranch2 to false. + this.read_again_for_branch_2.store(false, Ordering::Release); + + // Let chunk1 and chunk2 be chunk. + let chunk_1 = chunk.clone(); + let mut chunk_2 = chunk.clone(); + + // If canceled1 is false and canceled2 is false, + if this.reason_1.get().is_none() && this.reason_2.get().is_none() { + // Let cloneResult be CloneAsUint8Array(chunk). + match clone_as_uint8_array(ctx.clone(), &constructor_uint8array, &function_array_buffer_is_view, chunk) { + // If cloneResult is an abrupt completion, + Err(Error::Exception) => { + let err = ctx.catch(); + + let objects_1 = + ReadableStreamObjects::from_class(this.objects_class_1); + + // Perform ! ReadableByteStreamControllerError(branch1.[[controller]], cloneResult.[[Value]]). + ReadableByteStreamController::readable_byte_stream_controller_error( + objects_1, + err.clone(), + )?; + + let objects_2 = + ReadableStreamObjects::from_class(this.objects_class_2); + + // Perform ! ReadableByteStreamControllerError(branch2.[[controller]], cloneResult.[[Value]]). + ReadableByteStreamController::readable_byte_stream_controller_error( + objects_2, + err.clone(), + )?; + + // Resolve cancelPromise with ! ReadableStreamCancel(stream, cloneResult.[[Value]]). + let (promise, _) = ReadableStream::readable_stream_cancel( + ctx, + ReadableStreamObjects::from_class(objects_class), + err.clone(), + )?; + this.cancel_promise.resolve(promise)?; + + // Return. + return Ok(()); + }, + // Otherwise, set chunk2 to cloneResult.[[Value]]. + Ok(clone_result) => chunk_2 = clone_result, + Err(err) => return Err(err), + }; + } + + // If canceled1 is false, perform ! ReadableByteStreamControllerEnqueue(branch1.[[controller]], chunk1). + if this.reason_1.get().is_none() { + let objects_1 = ReadableStreamObjects::from_class_no_reader( + this.objects_class_1.clone(), + ).refresh_reader(); + ReadableByteStreamController::readable_byte_stream_controller_enqueue( + &ctx, objects_1, chunk_1, + )?; + } + + // If canceled2 is false, perform ! ReadableByteStreamControllerEnqueue(branch2.[[controller]], chunk2). + if this.reason_2.get().is_none() { + let objects_2 = ReadableStreamObjects::from_class_no_reader( + this.objects_class_2.clone(), + ).refresh_reader(); + ReadableByteStreamController::readable_byte_stream_controller_enqueue( + &ctx, objects_2, chunk_2, + )?; + } + + // Set reading to false. + this.reading.store(false, Ordering::Release); + + let objects_1 = ReadableStreamObjects::from_class(this.objects_class_1); + let objects_2 = ReadableStreamObjects::from_class(this.objects_class_2); + + let objects = ReadableStreamObjects::from_class_no_reader(objects_class); + + // If readAgainForBranch1 is true, perform pull1Algorithm. + if this.read_again_for_branch_1.load(Ordering::Acquire) { + ReadableStream::readable_byte_stream_pull_1_algorithm( + ctx.clone(), + objects, + this.reader, + this.reading, + this.read_again_for_branch_1, + this.read_again_for_branch_2, + this.reason_1, + this.reason_2, + objects_1, + objects_2, + this.cancel_promise, + )?; + } else if this.read_again_for_branch_2.load(Ordering::Acquire) { + // Otherwise, if readAgainForBranch2 is true, perform pull2Algorithm. + ReadableStream::readable_byte_stream_pull_2_algorithm( + ctx.clone(), + objects, + this.reader, + this.reading, + this.read_again_for_branch_1, + this.read_again_for_branch_2, + this.reason_1, + this.reason_2, + objects_1, + objects_2, + this.cancel_promise, + )?; + } + + Ok(()) + } + }; + + let () = Function::new(ctx, OnceFn::new(f))?.defer(())?; + + let objects = ReadableStreamObjects::from_class(objects_class); + + Ok(objects) + }) + } + + fn close_steps( + &self, + ctx: &Ctx<'js>, + objects: ReadableStreamDefaultReaderObjects<'js>, + ) -> Result> { + // Set reading to false. + self.reading.store(false, Ordering::Release); + + let mut objects_1 = + ReadableStreamObjects::from_class_no_reader(self.objects_class_1.clone()) + .refresh_reader(); + + let mut objects_2 = + ReadableStreamObjects::from_class_no_reader(self.objects_class_2.clone()) + .refresh_reader(); + + // If canceled1 is false, perform ! ReadableByteStreamControllerClose(branch1.[[controller]]). + if self.reason_1.get().is_none() { + objects_1 = + ReadableByteStreamController::readable_byte_stream_controller_close( + ctx.clone(), + objects_1, + )?; + } + // If canceled2 is false, perform ! ReadableByteStreamControllerClose(branch2.[[controller]]). + if self.reason_2.get().is_none() { + objects_2 = + ReadableByteStreamController::readable_byte_stream_controller_close( + ctx.clone(), + objects_2, + )?; + } + // If branch1.[[controller]].[[pendingPullIntos]] is not empty, perform ! ReadableByteStreamControllerRespond(branch1.[[controller]], 0). + if !objects_1.controller.pending_pull_intos.is_empty() { + ReadableByteStreamController::readable_byte_stream_controller_respond( + ctx.clone(), + objects_1, + 0, + )? + } + + // If branch2.[[controller]].[[pendingPullIntos]] is not empty, perform ! ReadableByteStreamControllerRespond(branch2.[[controller]], 0). + if !objects_2.controller.pending_pull_intos.is_empty() { + ReadableByteStreamController::readable_byte_stream_controller_respond( + ctx.clone(), + objects_2, + 0, + )? + } + + // If canceled1 is false or canceled2 is false, resolve cancelPromise with undefined. + if self.reason_1.get().is_none() || self.reason_2.get().is_none() { + self.cancel_promise.resolve_undefined()? + } + Ok(objects) + } + + fn error_steps( + &self, + objects: ReadableStreamDefaultReaderObjects<'js>, + _: Value<'js>, + ) -> Result> { + // Set reading to false. + self.reading.store(false, Ordering::Release); + Ok(objects) + } + } + + // Perform ! ReadableStreamDefaultReaderRead(reader, readRequest). + Ok( + ReadableStreamDefaultReader::readable_stream_default_reader_read( + &ctx, + objects.set_reader(OwnedBorrowMut::from_class(current_reader)), + ReadRequest { + reader, + reading, + read_again_for_branch_1, + read_again_for_branch_2, + reason_1, + reason_2, + objects_class_1, + objects_class_2, + cancel_promise, + }, + )? + .clear_reader(), + ) + } + + #[allow(clippy::too_many_arguments)] + fn readable_byte_stream_pull_with_byob_reader( + ctx: Ctx<'js>, + mut objects: ReadableByteStreamObjects<'js, UndefinedReader>, + reader: Rc>>, + reading: Rc, + read_again_for_branch_1: Rc, + read_again_for_branch_2: Rc, + reason_1: Rc>>, + reason_2: Rc>>, + objects_1: ReadableByteStreamObjects<'js, UndefinedReader>, + objects_2: ReadableByteStreamObjects<'js, UndefinedReader>, + cancel_promise: ResolveablePromise<'js>, + view: ViewBytes<'js>, + for_branch_2: bool, + ) -> Result> { + let objects_1 = objects_1.into_inner(); + let objects_2 = objects_2.into_inner(); + + // If reader implements ReadableStreamDefaultReader, + let current_reader = reader.borrow().clone(); + let current_reader = match current_reader { + ReadableStreamReaderClass::ReadableStreamDefaultReader(r) => { + let default_reader = OwnedBorrowMut::from_class(r.clone()); + + // Perform ! ReadableStreamDefaultReaderRelease(reader). + objects = ReadableStreamDefaultReader::readable_stream_default_reader_release( + objects.set_reader(default_reader), + )? + .clear_reader(); + + // Set reader to ! AcquireReadableStreamBYOBReader(stream). + let (s, new_reader) = + ReadableStreamReaderClass::acquire_readable_stream_byob_reader( + ctx.clone(), + objects.stream, + )?; + objects.stream = s; + reader.replace(new_reader.clone().into()); + + // Perform forwardReaderError, given reader. + Self::readable_byte_stream_forward_reader_error( + ctx.clone(), + reader.clone(), + objects_1.clone(), + objects_2.clone(), + reason_1.clone(), + reason_2.clone(), + new_reader.clone().into(), + cancel_promise.clone(), + )?; + + new_reader + } + ReadableStreamReaderClass::ReadableStreamBYOBReader(r) => r.clone(), + }; + + // Let byobBranch be branch2 if forBranch2 is true, and branch1 otherwise. + // Let otherBranch be branch2 if forBranch2 is false, and branch1 otherwise. + let (byob_objects, other_objects) = if for_branch_2 { + (objects_2.clone(), objects_1.clone()) + } else { + (objects_1.clone(), objects_2.clone()) + }; + + // Let readIntoRequest be a read-into request with the following items: + #[derive(Clone)] + struct ReadIntoRequest<'js> { + reader: Rc>>, + reading: Rc, + read_again_for_branch_1: Rc, + read_again_for_branch_2: Rc, + reason_1: Rc>>, + reason_2: Rc>>, + objects_1: ReadableStreamClassObjects< + 'js, + ReadableByteStreamControllerOwned<'js>, + UndefinedReader, + >, + objects_2: ReadableStreamClassObjects< + 'js, + ReadableByteStreamControllerOwned<'js>, + UndefinedReader, + >, + byob_objects: ReadableStreamClassObjects< + 'js, + ReadableByteStreamControllerOwned<'js>, + UndefinedReader, + >, + other_objects: ReadableStreamClassObjects< + 'js, + ReadableByteStreamControllerOwned<'js>, + UndefinedReader, + >, + cancel_promise: ResolveablePromise<'js>, + for_branch_2: bool, + } + + impl<'js> Trace<'js> for ReadIntoRequest<'js> { + fn trace<'a>(&self, tracer: rquickjs::class::Tracer<'a, 'js>) { + if let Ok(r) = self.reader.try_borrow() { + r.trace(tracer) + } + if let Some(r) = self.reason_1.get() { + r.trace(tracer) + } + if let Some(r) = self.reason_2.get() { + r.trace(tracer) + } + self.objects_1.trace(tracer); + self.objects_2.trace(tracer); + self.byob_objects.trace(tracer); + self.other_objects.trace(tracer); + self.cancel_promise.trace(tracer); + } + } + + impl<'js> ReadableStreamReadIntoRequest<'js> for ReadIntoRequest<'js> { + fn chunk_steps( + &self, + objects: ReadableStreamBYOBObjects<'js>, + chunk: Value<'js>, + ) -> Result> { + let ctx = chunk.ctx().clone(); + + let constructor_uint8array = objects + .controller + .array_constructor_primordials + .constructor_uint8array + .clone(); + let function_array_buffer_is_view = + objects.controller.function_array_buffer_is_view.clone(); + let chunk = + ViewBytes::from_value(&ctx, &function_array_buffer_is_view, Some(&chunk))?; + + let objects_class = objects.into_inner(); + + // Queue a microtask to perform the following steps: + let f = { + let ctx = ctx.clone(); + let objects_class = objects_class.clone(); + let this = self.clone(); + move || -> Result<()> { + // Set readAgainForBranch1 to false. + this.read_again_for_branch_1.store(false, Ordering::Release); + // Set readAgainForBranch2 to false. + this.read_again_for_branch_2.store(false, Ordering::Release); + + // Let byobCanceled be canceled2 if forBranch2 is true, and canceled1 otherwise. + // Let otherCanceled be canceled2 if forBranch2 is false, and canceled1 otherwise. + let (byob_canceled, other_canceled) = if this.for_branch_2 { + (this.reason_2.get().is_some(), this.reason_1.get().is_some()) + } else { + (this.reason_1.get().is_some(), this.reason_2.get().is_some()) + }; + + // If otherCanceled is false, + if !other_canceled { + // Let cloneResult be CloneAsUint8Array(chunk). + match clone_as_uint8_array( + ctx.clone(), + &constructor_uint8array, + &function_array_buffer_is_view, + chunk.clone(), + ) { + // If cloneResult is an abrupt completion, + Err(Error::Exception) => { + let err = ctx.catch(); + + let byob_objects = ReadableStreamObjects::from_class_no_reader( + this.byob_objects.clone(), + ) + .refresh_reader(); + + // Perform ! ReadableByteStreamControllerError(byobBranch.[[controller]], cloneResult.[[Value]]). + ReadableByteStreamController::readable_byte_stream_controller_error( + byob_objects, + err.clone(), + )?; + + let other_objects = + ReadableStreamObjects::from_class_no_reader( + this.other_objects.clone(), + ) + .refresh_reader(); + + // Perform ! ReadableByteStreamControllerError(otherBranch.[[controller]], cloneResult.[[Value]]). + ReadableByteStreamController::readable_byte_stream_controller_error( + other_objects, + err.clone(), + )?; + + // Resolve cancelPromise with ! ReadableStreamCancel(stream, cloneResult.[[Value]]). + let (promise, _) = ReadableStream::readable_stream_cancel( + ctx, + ReadableStreamObjects::from_class(objects_class), + err.clone(), + )?; + this.cancel_promise.resolve(promise)?; + + // Return. + return Ok(()); + } + // Otherwise, let clonedChunk be cloneResult.[[Value]]. + Ok(cloned_chunk) => { + // If byobCanceled is false, perform ! ReadableByteStreamControllerRespondWithNewView(byobBranch.[[controller]], chunk). + if !byob_canceled { + let byob_objects = + ReadableStreamObjects::from_class_no_reader( + this.byob_objects.clone(), + ) + .refresh_reader(); + + ReadableByteStreamController::readable_byte_stream_controller_respond_with_new_view(ctx.clone(), byob_objects, chunk)?; + } + + let other_objects = + ReadableStreamObjects::from_class_no_reader( + this.other_objects.clone(), + ) + .refresh_reader(); + + // Perform ! ReadableByteStreamControllerEnqueue(otherBranch.[[controller]], clonedChunk). + ReadableByteStreamController::readable_byte_stream_controller_enqueue(&ctx, other_objects, cloned_chunk)?; + } + Err(err) => return Err(err), + }; + } else if !byob_canceled { + let byob_objects = ReadableStreamObjects::from_class_no_reader( + this.byob_objects.clone(), + ) + .refresh_reader(); + + // Otherwise, if byobCanceled is false, perform ! ReadableByteStreamControllerRespondWithNewView(byobBranch.[[controller]], chunk). + ReadableByteStreamController::readable_byte_stream_controller_respond_with_new_view(ctx.clone(), byob_objects, chunk)?; + } + + let objects_1 = ReadableStreamObjects::from_class(this.objects_1.clone()); + let objects_2 = ReadableStreamObjects::from_class(this.objects_2.clone()); + + // Set reading to false. + this.reading.store(false, Ordering::Release); + + // If readAgainForBranch1 is true, perform pull1Algorithm. + if this.read_again_for_branch_1.load(Ordering::Acquire) { + ReadableStream::readable_byte_stream_pull_1_algorithm( + ctx.clone(), + ReadableStreamObjects::from_class(objects_class).clear_reader(), + this.reader.clone(), + this.reading.clone(), + this.read_again_for_branch_1.clone(), + this.read_again_for_branch_2.clone(), + this.reason_1.clone(), + this.reason_2.clone(), + objects_1, + objects_2, + this.cancel_promise.clone(), + )?; + } else if this.read_again_for_branch_2.load(Ordering::Acquire) { + // Otherwise, if readAgainForBranch2 is true, perform pull2Algorithm. + ReadableStream::readable_byte_stream_pull_2_algorithm( + ctx.clone(), + ReadableStreamObjects::from_class(objects_class).clear_reader(), + this.reader.clone(), + this.reading.clone(), + this.read_again_for_branch_1.clone(), + this.read_again_for_branch_2.clone(), + this.reason_1.clone(), + this.reason_2.clone(), + objects_1, + objects_2, + this.cancel_promise.clone(), + )?; + } + + Ok(()) + } + }; + + let () = Function::new(ctx, OnceFn::new(f))?.defer(())?; + + let objects = ReadableStreamObjects::from_class(objects_class); + + Ok(objects) + } + + fn close_steps( + &self, + objects: ReadableStreamBYOBObjects<'js>, + chunk: Value<'js>, + ) -> Result> { + let ctx = chunk.ctx().clone(); + + // Set reading to false. + self.reading.store(false, Ordering::Release); + + // Let byobCanceled be canceled2 if forBranch2 is true, and canceled1 otherwise. + // Let otherCanceled be canceled2 if forBranch2 is false, and canceled1 otherwise. + let (byob_canceled, other_canceled) = if self.for_branch_2 { + (self.reason_2.get().is_some(), self.reason_1.get().is_some()) + } else { + (self.reason_1.get().is_some(), self.reason_2.get().is_some()) + }; + + // If byobCanceled is false, perform ! ReadableByteStreamControllerClose(byobBranch.[[controller]]). + if !byob_canceled { + let byob_objects = + ReadableStreamObjects::from_class_no_reader(self.byob_objects.clone()) + .refresh_reader(); + + ReadableByteStreamController::readable_byte_stream_controller_close( + ctx.clone(), + byob_objects, + )?; + } + // If otherCanceled is false, perform ! ReadableByteStreamControllerClose(otherBranch.[[controller]]). + if !other_canceled { + let other_objects = + ReadableStreamObjects::from_class_no_reader(self.other_objects.clone()) + .refresh_reader(); + + ReadableByteStreamController::readable_byte_stream_controller_close( + ctx.clone(), + other_objects, + )?; + } + + // If chunk is not undefined, + if !chunk.is_undefined() { + let chunk = ViewBytes::from_value( + &ctx, + &objects.controller.function_array_buffer_is_view, + Some(&chunk), + )?; + + // If byobCanceled is false, perform ! ReadableByteStreamControllerRespondWithNewView(byobBranch.[[controller]], chunk). + if !byob_canceled { + let byob_objects = + ReadableStreamObjects::from_class_no_reader(self.byob_objects.clone()) + .refresh_reader(); + + ReadableByteStreamController::readable_byte_stream_controller_respond_with_new_view(ctx.clone(), byob_objects, chunk)?; + } + + let other_objects = + ReadableStreamObjects::from_class_no_reader(self.other_objects.clone()) + .refresh_reader(); + + // If otherCanceled is false and otherBranch.[[controller]].[[pendingPullIntos]] is not empty, perform ! ReadableByteStreamControllerRespond(otherBranch.[[controller]], 0). + if !other_canceled && !other_objects.controller.pending_pull_intos.is_empty() { + ReadableByteStreamController::readable_byte_stream_controller_respond( + ctx.clone(), + other_objects, + 0, + )?; + } + } + + // If byobCanceled is false or otherCanceled is false, resolve cancelPromise with undefined. + if !byob_canceled || !other_canceled { + self.cancel_promise.resolve_undefined()? + } + + Ok(objects) + } + + fn error_steps( + &self, + objects: ReadableStreamBYOBObjects<'js>, + _: Value<'js>, + ) -> Result> { + // Set reading to false. + self.reading.store(false, Ordering::Release); + Ok(objects) + } + } + + // Perform ! ReadableStreamBYOBReaderRead(reader, view, 1, readIntoRequest). + Ok(ReadableStreamBYOBReader::readable_stream_byob_reader_read( + &ctx, + objects.set_reader(OwnedBorrowMut::from_class(current_reader)), + view, + 1, + ReadIntoRequest { + reader, + reading, + read_again_for_branch_1, + read_again_for_branch_2, + reason_1, + reason_2, + objects_1, + objects_2, + byob_objects, + other_objects, + cancel_promise, + for_branch_2, + }, + )? + .clear_reader()) + } + + // Let pull1Algorithm be the following steps: + #[allow(clippy::too_many_arguments)] + fn readable_byte_stream_pull_1_algorithm( + ctx: Ctx<'js>, + mut objects: ReadableByteStreamObjects<'js, UndefinedReader>, + reader: Rc>>, + reading: Rc, + read_again_for_branch_1: Rc, + read_again_for_branch_2: Rc, + reason_1: Rc>>, + reason_2: Rc>>, + mut objects_1: ReadableByteStreamObjects<'js, UndefinedReader>, + objects_2: ReadableByteStreamObjects<'js, UndefinedReader>, + cancel_promise: ResolveablePromise<'js>, + ) -> Result> { + // If reading is true, + if reading.swap(true, Ordering::AcqRel) { + // Set readAgainForBranch1 to true. + read_again_for_branch_1.store(true, Ordering::Release); + // Return a promise resolved with undefined. + return Ok(objects + .stream + .promise_primordials + .promise_resolved_with_undefined + .clone()); + } + // Set reading to true. + + // Let byobRequest be ! ReadableByteStreamControllerGetBYOBRequest(branch1.[[controller]]). + let (byob_request, branch_1_controller) = + ReadableByteStreamController::readable_byte_stream_controller_get_byob_request( + ctx.clone(), + objects_1.controller, + )?; + objects_1.controller = branch_1_controller; + + // If byobRequest is null, perform pullWithDefaultReader. + objects = match byob_request.0 { + None => Self::readable_byte_stream_pull_with_default_reader( + ctx.clone(), + objects, + reader.clone(), + reading.clone(), + read_again_for_branch_1, + read_again_for_branch_2, + reason_1, + reason_2, + objects_1, + objects_2, + cancel_promise.clone(), + )?, + // Otherwise, perform pullWithBYOBReader, given byobRequest.[[view]] and false. + Some(byob_request) => { + let view = byob_request.borrow().view.clone().expect( + "ReadableByteStream tee pull1Algorithm called with invalidated byobRequest", + ); + Self::readable_byte_stream_pull_with_byob_reader( + ctx.clone(), + objects, + reader.clone(), + reading.clone(), + read_again_for_branch_1, + read_again_for_branch_2, + reason_1, + reason_2, + objects_1, + objects_2, + cancel_promise.clone(), + view, + false, + )? + } + }; + + // Return a promise resolved with undefined. + Ok(objects + .stream + .promise_primordials + .promise_resolved_with_undefined + .clone()) + } + + // Let pull2Algorithm be the following steps: + #[allow(clippy::too_many_arguments)] + fn readable_byte_stream_pull_2_algorithm( + ctx: Ctx<'js>, + mut objects: ReadableByteStreamObjects<'js, UndefinedReader>, + reader: Rc>>, + reading: Rc, + read_again_for_branch_1: Rc, + read_again_for_branch_2: Rc, + reason_1: Rc>>, + reason_2: Rc>>, + objects_1: ReadableByteStreamObjects<'js, UndefinedReader>, + mut objects_2: ReadableByteStreamObjects<'js, UndefinedReader>, + cancel_promise: ResolveablePromise<'js>, + ) -> Result> { + // If reading is true, + if reading.swap(true, Ordering::AcqRel) { + // Set readAgainForBranch2 to true. + read_again_for_branch_2.store(true, Ordering::Release); + // Return a promise resolved with undefined. + return Ok(objects + .stream + .promise_primordials + .promise_resolved_with_undefined + .clone()); + } + // Set reading to true. + + // Let byobRequest be ! ReadableByteStreamControllerGetBYOBRequest(branch2.[[controller]]). + let (byob_request, branch_2_controller) = + ReadableByteStreamController::readable_byte_stream_controller_get_byob_request( + ctx.clone(), + objects_2.controller, + )?; + objects_2.controller = branch_2_controller; + + // If byobRequest is null, perform pullWithDefaultReader. + objects = match byob_request.0 { + None => Self::readable_byte_stream_pull_with_default_reader( + ctx.clone(), + objects, + reader.clone(), + reading.clone(), + read_again_for_branch_1, + read_again_for_branch_2, + reason_1, + reason_2, + objects_1, + objects_2, + cancel_promise, + )?, + // Otherwise, perform pullWithBYOBReader, given byobRequest.[[view]] and true. + Some(byob_request) => Self::readable_byte_stream_pull_with_byob_reader( + ctx.clone(), + objects, + reader.clone(), + reading.clone(), + read_again_for_branch_1, + read_again_for_branch_2, + reason_1, + reason_2, + objects_1, + objects_2, + cancel_promise, + byob_request.borrow().view.clone().expect( + "ReadableByteStream tee pull2Algorithm called with invalidated byobRequest", + ), + true, + )?, + }; + + // Return a promise resolved with undefined. + Ok(objects + .stream + .promise_primordials + .promise_resolved_with_undefined + .clone()) + } +} + +fn clone_as_uint8_array<'js>( + ctx: Ctx<'js>, + constructor_uint8array: &Constructor<'js>, + function_array_buffer_is_view: &Function<'js>, + chunk: ViewBytes<'js>, +) -> Result> { + let (buffer, byte_length, byte_offset) = chunk.get_array_buffer()?; + + // Let buffer be ? CloneArrayBuffer(O.[[ViewedArrayBuffer]], O.[[ByteOffset]], O.[[ByteLength]], %ArrayBuffer%). + let buffer = ArrayBuffer::new_copy( + ctx.clone(), + &buffer + .as_bytes() + .expect("CloneAsUInt8Array called on detached buffer") + [byte_offset..byte_offset + byte_length], + )?; + + // Let array be ! Construct(%Uint8Array%, « buffer »). + // Return array. + ViewBytes::from_value( + &ctx, + function_array_buffer_is_view, + Some(&constructor_uint8array.construct((buffer,))?), + ) +} diff --git a/stdlib/src/llrt/llrt_stream_web/readable_writable_pair.rs b/stdlib/src/llrt/llrt_stream_web/readable_writable_pair.rs new file mode 100644 index 00000000..83c16bbd --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/readable_writable_pair.rs @@ -0,0 +1,25 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use rquickjs::{Ctx, Error, FromJs, Result, Value}; + +use crate::llrt_stream_web::{readable::ReadableStreamClass, writable::WritableStreamClass}; + +/// An object containing a pair of linked streams, one readable and one writable +/// https://streams.spec.whatwg.org/#dictdef-readablewritablepair +pub struct ReadableWritablePair<'js> { + pub readable: ReadableStreamClass<'js>, + pub writable: WritableStreamClass<'js>, +} + +impl<'js> FromJs<'js> for ReadableWritablePair<'js> { + fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { + let ty_name = value.type_name(); + let obj = value + .as_object() + .ok_or(Error::new_from_js(ty_name, "Object"))?; + + let readable = obj.get::<_, ReadableStreamClass<'js>>("readable")?; + let writable = obj.get::<_, WritableStreamClass<'js>>("writable")?; + + Ok(Self { readable, writable }) + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/transform/controller.rs b/stdlib/src/llrt/llrt_stream_web/transform/controller.rs new file mode 100644 index 00000000..ce2f3cda --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/transform/controller.rs @@ -0,0 +1,308 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use rquickjs::{ + class::{OwnedBorrowMut, Trace}, + prelude::{Opt, This}, + Class, Ctx, Exception, Function, JsLifetime, Object, Promise, Result, Value, +}; + +use crate::llrt_stream_web::{ + readable::{ + readable_stream_default_controller_close_stream, + readable_stream_default_controller_enqueue_value, + readable_stream_default_controller_error_stream, ReadableStreamDefaultControllerClass, + }, + utils::promise::{promise_resolved_with, ResolveablePromise}, +}; + +use crate::llrt_utils::primordials::Primordial; + +use super::stream::TransformStreamClass; + +#[rquickjs::class] +#[derive(JsLifetime, Trace)] +pub(crate) struct TransformStreamDefaultController<'js> { + pub(super) stream: TransformStreamClass<'js>, + pub(super) transform_algorithm: Option>, + pub(super) flush_algorithm: Option>, + pub(super) cancel_algorithm: Option>, + pub(super) finish_promise: Option>, +} + +pub(crate) type TransformStreamDefaultControllerClass<'js> = + Class<'js, TransformStreamDefaultController<'js>>; + +fn get_readable_default_controller<'js>( + stream_class: &TransformStreamClass<'js>, +) -> Option> { + let stream = stream_class.borrow(); + let readable = stream.readable.as_ref()?; + let readable = readable.borrow(); + match &readable.controller { + crate::llrt_stream_web::readable::ReadableStreamControllerClass::ReadableStreamDefaultController(c) => { + Some(c.clone()) + }, + _ => None, + } +} + +#[rquickjs::methods(rename_all = "camelCase")] +impl<'js> TransformStreamDefaultController<'js> { + #[qjs(constructor)] + fn new(ctx: Ctx<'js>) -> Result> { + Err(Exception::throw_type(&ctx, "Illegal constructor")) + } + + #[qjs(get)] + fn desired_size(&self) -> Option { + let stream = self.stream.borrow(); + let readable_class = stream.readable.as_ref()?; + let readable = readable_class.borrow(); + match &readable.controller { + crate::llrt_stream_web::readable::ReadableStreamControllerClass::ReadableStreamDefaultController(c) => { + let c = c.borrow(); + c.readable_stream_default_controller_get_desired_size(&readable) + .0 + }, + _ => None, + } + } + + fn enqueue( + ctx: Ctx<'js>, + this: This>, + chunk: Opt>, + ) -> Result<()> { + let chunk = chunk.0.unwrap_or_else(|| Value::new_undefined(ctx.clone())); + let stream_class = this.stream.clone(); + drop(this); + transform_stream_default_controller_enqueue(ctx, &stream_class, chunk) + } + + fn error( + ctx: Ctx<'js>, + this: This>, + reason: Opt>, + ) -> Result<()> { + let reason = reason + .0 + .unwrap_or_else(|| Value::new_undefined(ctx.clone())); + let stream_class = this.stream.clone(); + drop(this); + transform_stream_error(ctx, &stream_class, reason) + } + + fn terminate(ctx: Ctx<'js>, this: This>) -> Result<()> { + let stream_class = this.stream.clone(); + drop(this); + transform_stream_default_controller_terminate(ctx, &stream_class) + } +} + +impl<'js> TransformStreamDefaultController<'js> { + pub(super) fn clear_algorithms(&mut self) { + self.transform_algorithm = None; + self.flush_algorithm = None; + self.cancel_algorithm = None; + } +} + +#[derive(Trace, JsLifetime, Clone)] +pub(super) enum TransformAlgorithm<'js> { + Identity, + Function { + f: Function<'js>, + transformer: Option>, + }, +} + +#[derive(Trace, JsLifetime, Clone)] +pub(super) enum FlushAlgorithm<'js> { + Noop, + Function { + f: Function<'js>, + transformer: Option>, + }, +} + +#[derive(Trace, JsLifetime, Clone)] +pub(super) enum CancelAlgorithm<'js> { + Noop, + Function { + f: Function<'js>, + transformer: Option>, + }, +} + +// --- Abstract operations --- + +pub(super) fn transform_stream_default_controller_enqueue<'js>( + ctx: Ctx<'js>, + stream_class: &TransformStreamClass<'js>, + chunk: Value<'js>, +) -> Result<()> { + let controller_class = get_readable_default_controller(stream_class) + .ok_or_else(|| Exception::throw_type(&ctx, "readable controller not available"))?; + + readable_stream_default_controller_enqueue_value(ctx.clone(), controller_class.clone(), chunk)?; + + // Update backpressure + let has_backpressure = { + let stream = stream_class.borrow(); + let readable_class = stream.readable.as_ref().unwrap(); + let readable = readable_class.borrow(); + let c = controller_class.borrow(); + let desired = c.readable_stream_default_controller_get_desired_size(&readable); + desired.0.is_none_or(|size| size <= 0.0) + }; + + let current_bp = stream_class.borrow().backpressure; + if has_backpressure != current_bp { + transform_stream_set_backpressure(&ctx, stream_class, true)?; + } + + Ok(()) +} + +pub(super) fn transform_stream_default_controller_terminate<'js>( + ctx: Ctx<'js>, + stream_class: &TransformStreamClass<'js>, +) -> Result<()> { + let controller_class = get_readable_default_controller(stream_class) + .ok_or_else(|| Exception::throw_type(&ctx, "readable controller not available"))?; + + readable_stream_default_controller_close_stream(ctx.clone(), controller_class)?; + + let error = ctx.eval::("new TypeError('TransformStream terminated')")?; + transform_stream_error_writable_and_unblock_write(stream_class, error)?; + Ok(()) +} + +pub(super) fn transform_stream_error<'js>( + ctx: Ctx<'js>, + stream_class: &TransformStreamClass<'js>, + e: Value<'js>, +) -> Result<()> { + let controller_class = get_readable_default_controller(stream_class) + .ok_or_else(|| Exception::throw_type(&ctx, "readable controller not available"))?; + + readable_stream_default_controller_error_stream(controller_class, e.clone())?; + transform_stream_error_writable_and_unblock_write(stream_class, e)?; + Ok(()) +} + +pub(super) fn transform_stream_error_writable_and_unblock_write<'js>( + stream_class: &TransformStreamClass<'js>, + _e: Value<'js>, +) -> Result<()> { + let mut stream = stream_class.borrow_mut(); + if let Some(ref controller_class) = stream.controller { + controller_class.borrow_mut().clear_algorithms(); + } + // Always resolve and clear backpressure promise to break reference cycles + if let Some(ref bp) = stream.backpressure_change_promise { + bp.resolve_undefined()?; + } + stream.backpressure_change_promise = None; + stream.backpressure = false; + Ok(()) +} + +pub(super) fn transform_stream_set_backpressure<'js>( + ctx: &Ctx<'js>, + stream_class: &TransformStreamClass<'js>, + backpressure: bool, +) -> Result> { + let new_bp_promise = ResolveablePromise::new(ctx)?; + let promise = new_bp_promise.promise.clone(); + let mut stream = stream_class.borrow_mut(); + if let Some(ref bp_promise) = stream.backpressure_change_promise { + bp_promise.resolve_undefined()?; + } + stream.backpressure_change_promise = Some(new_bp_promise); + stream.backpressure = backpressure; + Ok(promise) +} + +pub(super) fn transform_stream_default_controller_perform_transform<'js>( + ctx: Ctx<'js>, + stream_class: &TransformStreamClass<'js>, + controller_class: &TransformStreamDefaultControllerClass<'js>, + chunk: Value<'js>, +) -> Result> { + let controller = controller_class.borrow(); + let algorithm = controller + .transform_algorithm + .clone() + .expect("transform algorithm must exist"); + drop(controller); + + let promise_primordials = + crate::llrt_stream_web::utils::promise::PromisePrimordials::get(&ctx)?.clone(); + + let transform_promise = match algorithm { + TransformAlgorithm::Identity => { + let result = + transform_stream_default_controller_enqueue(ctx.clone(), stream_class, chunk); + promise_resolved_with( + &ctx, + &promise_primordials, + result.map(|_| Value::new_undefined(ctx.clone())), + )? + } + TransformAlgorithm::Function { f, transformer } => { + let result: Result = + f.call((This(transformer), chunk, controller_class.clone())); + promise_resolved_with(&ctx, &promise_primordials, result)? + } + }; + + Ok(transform_promise) +} + +pub(super) fn perform_flush<'js>( + ctx: Ctx<'js>, + _stream_class: &TransformStreamClass<'js>, + controller_class: &TransformStreamDefaultControllerClass<'js>, +) -> Result> { + let controller = controller_class.borrow(); + let algorithm = controller + .flush_algorithm + .clone() + .unwrap_or(FlushAlgorithm::Noop); + drop(controller); + + let promise_primordials = + crate::llrt_stream_web::utils::promise::PromisePrimordials::get(&ctx)?.clone(); + + match algorithm { + FlushAlgorithm::Noop => Ok(promise_primordials.promise_resolved_with_undefined.clone()), + FlushAlgorithm::Function { f, transformer } => { + let result: Result = f.call((This(transformer), controller_class.clone())); + promise_resolved_with(&ctx, &promise_primordials, result) + } + } +} + +pub(super) fn perform_cancel<'js>( + ctx: Ctx<'js>, + controller_class: &TransformStreamDefaultControllerClass<'js>, + reason: Value<'js>, +) -> Result> { + let controller = controller_class.borrow(); + let algorithm = controller + .cancel_algorithm + .clone() + .unwrap_or(CancelAlgorithm::Noop); + drop(controller); + + let promise_primordials = + crate::llrt_stream_web::utils::promise::PromisePrimordials::get(&ctx)?.clone(); + + match algorithm { + CancelAlgorithm::Noop => Ok(promise_primordials.promise_resolved_with_undefined.clone()), + CancelAlgorithm::Function { f, transformer } => { + let result: Result = f.call((This(transformer), reason)); + promise_resolved_with(&ctx, &promise_primordials, result) + } + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/transform/mod.rs b/stdlib/src/llrt/llrt_stream_web/transform/mod.rs new file mode 100644 index 00000000..7ce3b230 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/transform/mod.rs @@ -0,0 +1,9 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +pub(crate) mod controller; +pub(crate) mod stream; +#[cfg(test)] +mod tests; +mod transformer; + +pub(crate) use controller::TransformStreamDefaultController; +pub(crate) use stream::TransformStream; diff --git a/stdlib/src/llrt/llrt_stream_web/transform/stream.rs b/stdlib/src/llrt/llrt_stream_web/transform/stream.rs new file mode 100644 index 00000000..23e2c8e6 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/transform/stream.rs @@ -0,0 +1,352 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use crate::llrt_utils::option::Undefined; +use rquickjs::{ + class::Trace, + prelude::{Opt, This}, + Class, Ctx, Exception, JsLifetime, Object, Promise, Result, Value, +}; + +use crate::llrt_stream_web::{ + queuing_strategy::QueuingStrategy, + readable::stream::{ + algorithms::{CancelAlgorithm, PullAlgorithm, StartAlgorithm}, + ReadableStream, + }, + utils::promise::ResolveablePromise, + writable::WritableStream, +}; + +use super::{ + controller::{ + self, CancelAlgorithm as TsCancelAlgorithm, FlushAlgorithm, TransformAlgorithm, + TransformStreamDefaultController, TransformStreamDefaultControllerClass, + }, + transformer::Transformer, +}; + +#[rquickjs::class] +#[derive(JsLifetime, Trace)] +pub(crate) struct TransformStream<'js> { + pub(super) readable: Option>>, + pub(super) writable: Option>>, + pub(super) controller: Option>, + pub(super) backpressure: bool, + pub(super) backpressure_change_promise: Option>, +} + +pub(crate) type TransformStreamClass<'js> = Class<'js, TransformStream<'js>>; + +#[rquickjs::methods(rename_all = "camelCase")] +impl<'js> TransformStream<'js> { + pub(crate) fn from_transformer( + ctx: Ctx<'js>, + transformer: Object<'js>, + ) -> Result> { + Self::new( + ctx, + Opt(Some(Undefined(Some(transformer)))), + Opt(None), + Opt(None), + ) + } + + #[qjs(constructor)] + fn new( + ctx: Ctx<'js>, + transformer: Opt>>, + writable_strategy: Opt>>, + readable_strategy: Opt>>, + ) -> Result> { + let transformer_obj = transformer.0.and_then(|u| u.0); + let transformer_dict = transformer_obj + .as_ref() + .map(|obj| Transformer::from_object(obj.clone())) + .transpose()? + .unwrap_or_default(); + + if transformer_dict.readable_type { + return Err(Exception::throw_range( + &ctx, + "readableType is not supported", + )); + } + if transformer_dict.writable_type { + return Err(Exception::throw_range( + &ctx, + "writableType is not supported", + )); + } + + let readable_strategy = readable_strategy.0.and_then(|qs| qs.0); + let writable_strategy = writable_strategy.0.and_then(|qs| qs.0); + + let readable_size = QueuingStrategy::extract_size_algorithm(readable_strategy.as_ref()); + let writable_size = QueuingStrategy::extract_size_algorithm(writable_strategy.as_ref()); + let readable_hwm = QueuingStrategy::extract_high_water_mark(&ctx, readable_strategy, 0.0)?; + let writable_hwm = QueuingStrategy::extract_high_water_mark(&ctx, writable_strategy, 1.0)?; + + // Create the TransformStream instance + let stream_class = Class::instance( + ctx.clone(), + Self { + readable: None, + writable: None, + controller: None, + backpressure: true, + backpressure_change_promise: None, + }, + )?; + + // Initial backpressure change promise + let bp_promise = ResolveablePromise::new(&ctx)?; + stream_class.borrow_mut().backpressure_change_promise = Some(bp_promise); + + // Build controller algorithms + let transform_algorithm = transformer_dict + .transform + .map(|f| TransformAlgorithm::Function { + f, + transformer: transformer_obj.clone(), + }) + .unwrap_or(TransformAlgorithm::Identity); + + let flush_algorithm = transformer_dict + .flush + .map(|f| FlushAlgorithm::Function { + f, + transformer: transformer_obj.clone(), + }) + .unwrap_or(FlushAlgorithm::Noop); + + let cancel_algorithm = transformer_dict + .cancel + .map(|f| TsCancelAlgorithm::Function { + f, + transformer: transformer_obj.clone(), + }) + .unwrap_or(TsCancelAlgorithm::Noop); + + // Create controller + let controller_class = Class::instance( + ctx.clone(), + TransformStreamDefaultController { + stream: stream_class.clone(), + transform_algorithm: Some(transform_algorithm), + flush_algorithm: Some(flush_algorithm), + cancel_algorithm: Some(cancel_algorithm), + finish_promise: None, + }, + )?; + stream_class.borrow_mut().controller = Some(controller_class.clone()); + + // Start promise + let start_promise = ResolveablePromise::new(&ctx)?; + + // --- Create writable side with properly traced algorithm variants --- + let writable_class = WritableStream::create_for_transform( + ctx.clone(), + start_promise.promise.clone(), + stream_class.clone(), + controller_class.clone(), + writable_hwm, + writable_size, + )?; + + // --- Create readable side --- + let pull_algorithm = PullAlgorithm::Transform(stream_class.clone()); + + let cancel_algo = CancelAlgorithm::Transform { + stream: stream_class.clone(), + controller: controller_class.clone(), + }; + + let readable_objects = ReadableStream::create_readable_stream( + ctx.clone(), + StartAlgorithm::ReturnUndefined, + pull_algorithm, + cancel_algo, + Some(readable_hwm), + Some(readable_size), + )?; + + { + let mut stream = stream_class.borrow_mut(); + stream.readable = Some(readable_objects.stream.clone()); + stream.writable = Some(writable_class); + } + + // Invoke start() if present + if let Some(start_fn) = transformer_dict.start { + match start_fn.call::<_, Value>((This(transformer_obj), controller_class)) { + Ok(val) => { + start_promise.resolve(val)?; + } + Err(_) => { + let err = ctx.catch(); + start_promise.reject(err)?; + } + } + } else { + start_promise.resolve_undefined()?; + } + + Ok(stream_class) + } + + #[qjs(get)] + fn readable(&self) -> Option>> { + self.readable.clone() + } + + #[qjs(get)] + fn writable(&self) -> Option>> { + self.writable.clone() + } +} + +// --- Sink algorithms --- + +pub(crate) fn sink_write_algorithm<'js>( + ctx: Ctx<'js>, + stream_class: &TransformStreamClass<'js>, + controller_class: &TransformStreamDefaultControllerClass<'js>, + chunk: Value<'js>, +) -> Result> { + let stream = stream_class.borrow(); + if stream.backpressure { + let bp_promise = stream + .backpressure_change_promise + .as_ref() + .map(|p| p.promise.clone()); + drop(stream); + + if let Some(bp_promise) = bp_promise { + let sc = stream_class.clone(); + let cc = controller_class.clone(); + return crate::llrt_stream_web::utils::promise::upon_promise::, _>( + ctx.clone(), + bp_promise, + move |ctx, _| { + let p = controller::transform_stream_default_controller_perform_transform( + ctx.clone(), + &sc, + &cc, + chunk, + )?; + Ok(p.into_value()) + }, + ); + } + } else { + drop(stream); + } + + controller::transform_stream_default_controller_perform_transform( + ctx, + stream_class, + controller_class, + chunk, + ) +} + +pub(crate) fn sink_close_algorithm<'js>( + ctx: Ctx<'js>, + stream_class: &TransformStreamClass<'js>, + controller_class: &TransformStreamDefaultControllerClass<'js>, +) -> Result> { + let flush_promise = controller::perform_flush(ctx.clone(), stream_class, controller_class)?; + + let sc = stream_class.clone(); + let cc = controller_class.clone(); + crate::llrt_stream_web::utils::promise::upon_promise::, _>( + ctx.clone(), + flush_promise, + move |ctx, result| { + cc.borrow_mut().clear_algorithms(); + match result { + Ok(_) => { + let mut stream = sc.borrow_mut(); + // Resolve any pending backpressure promise to break the cycle + if let Some(ref bp) = stream.backpressure_change_promise { + bp.resolve_undefined()?; + } + stream.backpressure_change_promise = None; + let readable_controller = stream.readable.as_ref().and_then(|readable| { + let r = readable.borrow(); + if let crate::llrt_stream_web::readable::ReadableStreamControllerClass::ReadableStreamDefaultController(c) = &r.controller { + Some(c.clone()) + } else { + None + } + }); + drop(stream); + if let Some(c) = readable_controller { + crate::llrt_stream_web::readable::readable_stream_default_controller_close_stream( + ctx.clone(), + c, + )?; + } + Ok(Value::new_undefined(ctx)) + } + Err(r) => { + controller::transform_stream_error(ctx.clone(), &sc, r.clone())?; + Err(ctx.throw(r)) + } + } + }, + ) +} + +pub(crate) fn sink_abort_algorithm<'js>( + ctx: Ctx<'js>, + controller_class: &TransformStreamDefaultControllerClass<'js>, + reason: Value<'js>, +) -> Result> { + let cancel_promise = controller::perform_cancel(ctx.clone(), controller_class, reason)?; + + let cc = controller_class.clone(); + crate::llrt_stream_web::utils::promise::upon_promise::, _>( + ctx.clone(), + cancel_promise, + move |ctx, result| { + cc.borrow_mut().clear_algorithms(); + match result { + Ok(_) => Ok(Value::new_undefined(ctx)), + Err(r) => Err(ctx.throw(r)), + } + }, + ) +} + +// --- Source algorithms --- + +pub(crate) fn source_pull_algorithm<'js>( + ctx: Ctx<'js>, + stream_class: &TransformStreamClass<'js>, +) -> Result> { + controller::transform_stream_set_backpressure(&ctx, stream_class, false) +} + +pub(crate) fn source_cancel_algorithm<'js>( + ctx: Ctx<'js>, + stream_class: &TransformStreamClass<'js>, + controller_class: &TransformStreamDefaultControllerClass<'js>, + reason: Value<'js>, +) -> Result> { + let cancel_promise = controller::perform_cancel(ctx.clone(), controller_class, reason.clone())?; + + let sc = stream_class.clone(); + let cc = controller_class.clone(); + crate::llrt_stream_web::utils::promise::upon_promise::, _>( + ctx.clone(), + cancel_promise, + move |ctx, result| { + cc.borrow_mut().clear_algorithms(); + controller::transform_stream_error_writable_and_unblock_write(&sc, reason)?; + match result { + Ok(_) => Ok(Value::new_undefined(ctx)), + Err(r) => Err(ctx.throw(r)), + } + }, + ) +} diff --git a/stdlib/src/llrt/llrt_stream_web/transform/tests.rs b/stdlib/src/llrt/llrt_stream_web/transform/tests.rs new file mode 100644 index 00000000..25aff6c9 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/transform/tests.rs @@ -0,0 +1,440 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use crate::llrt_test::test_async_with; +use rquickjs::Promise; + +fn eval_async<'js>(ctx: &rquickjs::Ctx<'js>, js: &str) -> rquickjs::Result> { + ctx.eval(format!("(async () => {{ {js} }})()")) +} + +#[tokio::test] +async fn identity_passthrough() { + test_async_with(|ctx| { + crate::llrt_stream_web::init(&ctx).unwrap(); + Box::pin(async move { + eval_async( + &ctx, + r#" + const ts = new TransformStream(); + const writer = ts.writable.getWriter(); + const reader = ts.readable.getReader(); + + writer.write("one"); + writer.write("two"); + writer.close(); + + const chunks = []; + while (true) { + const { value, done } = await reader.read(); + if (done) break; + chunks.push(value); + } + if (chunks.join(",") !== "one,two") throw new Error("got: " + chunks); + "#, + ) + .unwrap() + .into_future::<()>() + .await + .unwrap(); + }) + }) + .await; +} + +#[tokio::test] +async fn transform_chunks() { + test_async_with(|ctx| { + crate::llrt_stream_web::init(&ctx).unwrap(); + Box::pin(async move { + eval_async( + &ctx, + r#" + const ts = new TransformStream({ + transform(chunk, controller) { + controller.enqueue(chunk.toUpperCase()); + } + }); + const writer = ts.writable.getWriter(); + const reader = ts.readable.getReader(); + + writer.write("hello"); + writer.write("world"); + writer.close(); + + const chunks = []; + while (true) { + const { value, done } = await reader.read(); + if (done) break; + chunks.push(value); + } + if (chunks.join(" ") !== "HELLO WORLD") throw new Error("got: " + chunks); + "#, + ) + .unwrap() + .into_future::<()>() + .await + .unwrap(); + }) + }) + .await; +} + +#[tokio::test] +async fn one_to_many_expansion() { + test_async_with(|ctx| { + crate::llrt_stream_web::init(&ctx).unwrap(); + Box::pin(async move { + eval_async( + &ctx, + r#" + const ts = new TransformStream({ + transform(chunk, controller) { + for (const byte of chunk) { + controller.enqueue(byte); + } + } + }); + const writer = ts.writable.getWriter(); + const reader = ts.readable.getReader(); + + writer.write([1, 2, 3]); + writer.close(); + + const chunks = []; + while (true) { + const { value, done } = await reader.read(); + if (done) break; + chunks.push(value); + } + if (chunks.join(",") !== "1,2,3") throw new Error("got: " + chunks); + "#, + ) + .unwrap() + .into_future::<()>() + .await + .unwrap(); + }) + }) + .await; +} + +#[tokio::test] +async fn readable_high_water_mark_applies_backpressure() { + test_async_with(|ctx| { + crate::llrt_stream_web::init(&ctx).unwrap(); + Box::pin(async move { + ctx.eval::<(), _>( + r#" + globalThis.transformed = []; + globalThis.ts = new TransformStream({ + transform(chunk, controller) { + transformed.push(chunk); + controller.enqueue(chunk); + } + }, undefined, { highWaterMark: 3 }); + globalThis.writer = ts.writable.getWriter(); + [0, 1, 2, 3].forEach(chunk => writer.write(chunk)); + "#, + ) + .unwrap(); + + while ctx.execute_pending_job() {} + assert_eq!( + ctx.eval::("transformed.join(',')").unwrap(), + "0,1,2" + ); + + ctx.eval::<(), _>("globalThis.reader = ts.readable.getReader(); reader.read();") + .unwrap(); + while ctx.execute_pending_job() {} + assert_eq!( + ctx.eval::("transformed.join(',')").unwrap(), + "0,1,2,3" + ); + }) + }) + .await; +} + +#[tokio::test] +async fn flush_on_close() { + test_async_with(|ctx| { + crate::llrt_stream_web::init(&ctx).unwrap(); + Box::pin(async move { + eval_async( + &ctx, + r#" + const ts = new TransformStream({ + transform(chunk, controller) { + controller.enqueue(chunk); + }, + flush(controller) { + controller.enqueue("DONE"); + } + }); + const writer = ts.writable.getWriter(); + const reader = ts.readable.getReader(); + + writer.write("a"); + writer.close(); + + const chunks = []; + while (true) { + const { value, done } = await reader.read(); + if (done) break; + chunks.push(value); + } + if (chunks.join(",") !== "a,DONE") throw new Error("got: " + chunks); + "#, + ) + .unwrap() + .into_future::<()>() + .await + .unwrap(); + }) + }) + .await; +} + +#[tokio::test] +async fn pipe_through_chain() { + test_async_with(|ctx| { + crate::llrt_stream_web::init(&ctx).unwrap(); + Box::pin(async move { + eval_async( + &ctx, + r#" + const source = new ReadableStream({ + start(controller) { + controller.enqueue("hello"); + controller.enqueue("world"); + controller.close(); + } + }); + + const upper = new TransformStream({ + transform(chunk, c) { c.enqueue(chunk.toUpperCase()); } + }); + const exclaim = new TransformStream({ + transform(chunk, c) { c.enqueue(chunk + "!"); } + }); + + const reader = source + .pipeThrough(upper) + .pipeThrough(exclaim) + .getReader(); + + const chunks = []; + while (true) { + const { value, done } = await reader.read(); + if (done) break; + chunks.push(value); + } + if (chunks.join(" ") !== "HELLO! WORLD!") throw new Error("got: " + chunks); + "#, + ) + .unwrap() + .into_future::<()>() + .await + .unwrap(); + }) + }) + .await; +} + +#[tokio::test] +async fn async_transform() { + test_async_with(|ctx| { + crate::llrt_stream_web::init(&ctx).unwrap(); + Box::pin(async move { + eval_async( + &ctx, + r#" + const ts = new TransformStream({ + async transform(chunk, controller) { + await new Promise(r => r()); + controller.enqueue(chunk * 2); + } + }); + const writer = ts.writable.getWriter(); + const reader = ts.readable.getReader(); + + writer.write(5); + writer.close(); + + const { value } = await reader.read(); + if (value !== 10) throw new Error("expected 10, got " + value); + "#, + ) + .unwrap() + .into_future::<()>() + .await + .unwrap(); + }) + }) + .await; +} + +#[tokio::test] +async fn error_propagates_to_reader() { + test_async_with(|ctx| { + crate::llrt_stream_web::init(&ctx).unwrap(); + Box::pin(async move { + eval_async( + &ctx, + r#" + const ts = new TransformStream({ + transform(chunk, controller) { + controller.error(new Error("broken")); + } + }); + const writer = ts.writable.getWriter(); + const reader = ts.readable.getReader(); + + writer.write("x").catch(() => {}); + + try { + await reader.read(); + throw new Error("should have thrown"); + } catch (e) { + if (e.message !== "broken") throw new Error("wrong error: " + e.message); + } + "#, + ) + .unwrap() + .into_future::<()>() + .await + .unwrap(); + }) + }) + .await; +} + +#[tokio::test] +async fn start_receives_controller() { + test_async_with(|ctx| { + crate::llrt_stream_web::init(&ctx).unwrap(); + Box::pin(async move { + eval_async( + &ctx, + r#" + let controllerRef; + const ts = new TransformStream({ + start(controller) { + controllerRef = controller; + controller.enqueue("from-start"); + } + }); + + if (typeof controllerRef.desiredSize !== "number") + throw new Error("controller.desiredSize should be a number"); + + const reader = ts.readable.getReader(); + const { value } = await reader.read(); + if (value !== "from-start") throw new Error("got: " + value); + "#, + ) + .unwrap() + .into_future::<()>() + .await + .unwrap(); + }) + }) + .await; +} + +#[tokio::test] +async fn terminate_closes_readable() { + test_async_with(|ctx| { + crate::llrt_stream_web::init(&ctx).unwrap(); + Box::pin(async move { + eval_async( + &ctx, + r#" + const ts = new TransformStream({ + transform(chunk, controller) { + if (chunk === "stop") { + controller.terminate(); + return; + } + controller.enqueue(chunk); + } + }); + const writer = ts.writable.getWriter(); + const reader = ts.readable.getReader(); + + writer.write("keep").catch(() => {}); + writer.write("stop").catch(() => {}); + + const { value } = await reader.read(); + if (value !== "keep") throw new Error("got: " + value); + + const { done } = await reader.read(); + if (!done) throw new Error("expected stream to be closed after terminate"); + "#, + ) + .unwrap() + .into_future::<()>() + .await + .unwrap(); + }) + }) + .await; +} + +#[tokio::test] +async fn illegal_constructor() { + test_async_with(|ctx| { + crate::llrt_stream_web::init(&ctx).unwrap(); + Box::pin(async move { + eval_async( + &ctx, + r#" + try { + new TransformStreamDefaultController(); + throw new Error("should have thrown"); + } catch (e) { + if (!(e instanceof TypeError)) throw new Error("expected TypeError, got " + e); + } + "#, + ) + .unwrap() + .into_future::<()>() + .await + .unwrap(); + }) + }) + .await; +} + +#[tokio::test] +async fn pipe_to_writable_stream() { + test_async_with(|ctx| { + crate::llrt_stream_web::init(&ctx).unwrap(); + Box::pin(async move { + eval_async( + &ctx, + r#" + const collected = []; + const source = new ReadableStream({ + start(c) { c.enqueue(1); c.enqueue(2); c.enqueue(3); c.close(); } + }); + const transform = new TransformStream({ + transform(chunk, c) { c.enqueue(chunk * 10); } + }); + const sink = new WritableStream({ + write(chunk) { collected.push(chunk); } + }); + + await source.pipeThrough(transform).pipeTo(sink); + + if (collected.join(",") !== "10,20,30") throw new Error("got: " + collected); + "#, + ) + .unwrap() + .into_future::<()>() + .await + .unwrap(); + }) + }) + .await; +} diff --git a/stdlib/src/llrt/llrt_stream_web/transform/transformer.rs b/stdlib/src/llrt/llrt_stream_web/transform/transformer.rs new file mode 100644 index 00000000..ad27cf02 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/transform/transformer.rs @@ -0,0 +1,44 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use rquickjs::{Function, Object, Result}; + +use crate::llrt_stream_web::utils::ValueOrUndefined; + +/// dictionary Transformer { +/// TransformerStartCallback start; +/// TransformerTransformCallback transform; +/// TransformerFlushCallback flush; +/// TransformerCancelCallback cancel; +/// any readableType; +/// any writableType; +/// }; +#[derive(Default)] +pub(super) struct Transformer<'js> { + pub start: Option>, + pub transform: Option>, + pub flush: Option>, + pub cancel: Option>, + pub readable_type: bool, + pub writable_type: bool, +} + +impl<'js> Transformer<'js> { + pub fn from_object(obj: Object<'js>) -> Result { + let start = obj.get_value_or_undefined::<_, _>("start")?; + let transform = obj.get_value_or_undefined::<_, _>("transform")?; + let flush = obj.get_value_or_undefined::<_, _>("flush")?; + let cancel = obj.get_value_or_undefined::<_, _>("cancel")?; + let readable_type: Option> = + obj.get_value_or_undefined("readableType")?; + let writable_type: Option> = + obj.get_value_or_undefined("writableType")?; + + Ok(Self { + start, + transform, + flush, + cancel, + readable_type: readable_type.is_some(), + writable_type: writable_type.is_some(), + }) + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/utils/mod.rs b/stdlib/src/llrt/llrt_stream_web/utils/mod.rs new file mode 100644 index 00000000..3e38532c --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/utils/mod.rs @@ -0,0 +1,58 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use crate::llrt_utils::option::Undefined; +use rquickjs::{ + class::{JsClass, OwnedBorrowMut}, + Class, Ctx, FromJs, IntoAtom, Object, Result, Value, +}; + +pub mod promise; +pub mod queue; + +// the trait used elsewhere in this repo accepts null values as 'None', which causes many web platform tests to fail as they +// like to check that undefined is accepted and null isn't. +pub trait ValueOrUndefined<'js> { + fn get_value_or_undefined + Clone, V: FromJs<'js>>( + &self, + k: K, + ) -> Result>; +} + +impl<'js> ValueOrUndefined<'js> for Object<'js> { + fn get_value_or_undefined + Clone, V: FromJs<'js> + Sized>( + &self, + k: K, + ) -> Result> { + let value = self.get::>(k)?; + Ok(Undefined::from_js(self.ctx(), value)?.0) + } +} + +impl<'js> ValueOrUndefined<'js> for Value<'js> { + fn get_value_or_undefined + Clone, V: FromJs<'js>>( + &self, + k: K, + ) -> Result> { + if let Some(obj) = self.as_object() { + return obj.get_value_or_undefined(k); + } + Ok(None) + } +} + +pub trait UnwrapOrUndefined<'js> { + fn unwrap_or_undefined(self, ctx: &Ctx<'js>) -> Value<'js>; +} + +impl<'js> UnwrapOrUndefined<'js> for Option> { + fn unwrap_or_undefined(self, ctx: &Ctx<'js>) -> Value<'js> { + self.unwrap_or_else(|| Value::new_undefined(ctx.clone())) + } +} + +pub fn class_from_owned_borrow_mut<'js, T: JsClass<'js>>( + borrow: OwnedBorrowMut<'js, T>, +) -> (Class<'js, T>, OwnedBorrowMut<'js, T>) { + let class = borrow.into_inner(); + let borrow = OwnedBorrowMut::from_class(class.clone()); + (class, borrow) +} diff --git a/stdlib/src/llrt/llrt_stream_web/utils/promise.rs b/stdlib/src/llrt/llrt_stream_web/utils/promise.rs new file mode 100644 index 00000000..584f4481 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/utils/promise.rs @@ -0,0 +1,260 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use std::{cell::Cell, rc::Rc}; + +use crate::llrt_utils::primordials::Primordial; +use rquickjs::{ + atom::PredefinedAtom, + class::{Trace, Tracer}, + function::Constructor, + prelude::{IntoArg, OnceFn, This}, + promise::PromiseState, + Ctx, Error, FromJs, Function, IntoJs, JsLifetime, Object, Promise, Result, Value, +}; + +pub fn promise_rejected_with<'js>( + primordials: &PromisePrimordials<'js>, + value: Value<'js>, +) -> Result> { + primordials + .promise_reject + .call((This(primordials.promise_constructor.clone()), value)) +} + +pub fn promise_rejected_catch<'js>( + ctx: &Ctx<'js>, + promise_primordials: &PromisePrimordials<'js>, +) -> Result> { + promise_rejected_with(promise_primordials, ctx.catch()) +} + +pub fn promise_rejected_with_constructor<'js, T: From>( + constructor: &Constructor<'js>, + promise_primordials: &PromisePrimordials<'js>, + msg: &str, +) -> std::result::Result, T> { + let e: Value = constructor.call((msg,))?; + Ok(promise_rejected_with(promise_primordials, e)?) +} + +pub fn promise_resolved_with<'js>( + ctx: &Ctx<'js>, + primordials: &PromisePrimordials<'js>, + value: Result>, +) -> Result> { + match value { + Ok(value) => primordials + .promise_resolve + .call((This(primordials.promise_constructor.clone()), value)), + Err(Error::Exception) => primordials + .promise_reject + .call((This(primordials.promise_constructor.clone()), ctx.catch())), + Err(err) => Err(err), + } +} + +#[derive(JsLifetime, Clone)] +pub struct PromisePrimordials<'js> { + pub promise_constructor: Constructor<'js>, + pub promise_resolve: Function<'js>, + pub promise_reject: Function<'js>, + pub promise_all: Function<'js>, + pub promise_resolved_with_undefined: Promise<'js>, + pub promise_prototype_then: Function<'js>, +} + +impl<'js> Trace<'js> for PromisePrimordials<'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + self.promise_constructor.trace(tracer); + self.promise_resolve.trace(tracer); + self.promise_reject.trace(tracer); + self.promise_all.trace(tracer); + self.promise_resolved_with_undefined.trace(tracer); + self.promise_prototype_then.trace(tracer); + } +} + +impl<'js> Primordial<'js> for PromisePrimordials<'js> { + fn new(ctx: &Ctx<'js>) -> Result + where + Self: Sized, + { + let promise_constructor: Constructor<'js> = ctx.globals().get(PredefinedAtom::Promise)?; + let promise_resolve: Function<'js> = promise_constructor.get("resolve")?; + let promise_reject: Function<'js> = promise_constructor.get("reject")?; + let promise_all: Function<'js> = promise_constructor.get("all")?; + let promise_prototype_then: Function<'js> = promise_constructor + .get::<_, Object>("prototype")? + .get("then")?; + + let promise_resolved_with_undefined = promise_resolve.call(( + This(promise_constructor.clone()), + Value::new_undefined(ctx.clone()), + ))?; + + Ok(Self { + promise_constructor, + promise_resolve, + promise_reject, + promise_all, + promise_resolved_with_undefined, + promise_prototype_then, + }) + } +} + +// https://webidl.spec.whatwg.org/#dfn-perform-steps-once-promise-is-settled +pub fn upon_promise<'js, Input: FromJs<'js> + 'js, Output: IntoJs<'js> + 'js>( + ctx: Ctx<'js>, + promise: Promise<'js>, + then: impl FnOnce(Ctx<'js>, std::result::Result>) -> Result + 'js, +) -> Result> { + let promise_then = PromisePrimordials::get(&ctx)? + .promise_prototype_then + .clone(); + let then_cb = Rc::new(Cell::new(Some(then))); + let then_cb2 = then_cb.clone(); + promise_then.call(( + This(promise), + Function::new( + ctx.clone(), + OnceFn::new(move |ctx, input| { + then_cb + .take() + .expect("Promise.then should only call either resolve or reject")( + ctx, + Ok(input), + ) + }), + ), + Function::new( + ctx, + OnceFn::new(move |ctx, e: Value<'js>| { + then_cb2 + .take() + .expect("Promise.then should only call either resolve or reject")( + ctx, Err(e) + ) + }), + ), + )) +} + +pub fn upon_promise_fulfilment<'js, Input: FromJs<'js> + 'js, Output: IntoJs<'js> + 'js>( + ctx: Ctx<'js>, + promise: Promise<'js>, + then: impl FnOnce(Ctx<'js>, Input) -> Result + 'js, +) -> Result> { + let promise_then = PromisePrimordials::get(&ctx)? + .promise_prototype_then + .clone(); + promise_then.call((This(promise), Function::new(ctx.clone(), OnceFn::new(then)))) +} + +#[derive(Debug, JsLifetime, Clone)] +pub struct ResolveablePromise<'js> { + pub promise: Promise<'js>, + resolve: Option>, + reject: Option>, +} + +impl<'js> ResolveablePromise<'js> { + pub fn new(ctx: &Ctx<'js>) -> Result { + let (promise, resolve, reject) = Promise::new(ctx)?; + Ok(Self { + promise, + resolve: Some(resolve), + reject: Some(reject), + }) + } + + pub fn resolved_with_undefined(primordials: &PromisePrimordials<'js>) -> Self { + Self { + promise: primordials.promise_resolved_with_undefined.clone(), + resolve: None, + reject: None, + } + } + + pub fn rejected_with(primordials: &PromisePrimordials<'js>, error: Value<'js>) -> Result { + Ok(Self { + promise: promise_rejected_with(primordials, error)?, + resolve: None, + reject: None, + }) + } + + pub fn rejected_with_constructor( + primordials: &PromisePrimordials<'js>, + constructor: &Constructor<'js>, + msg: &str, + ) -> Result { + Ok(Self { + promise: promise_rejected_with_constructor::( + constructor, + primordials, + msg, + )?, + resolve: None, + reject: None, + }) + } + + pub fn resolve(&self, value: impl IntoArg<'js>) -> Result<()> { + if let Some(resolve) = &self.resolve { + let () = resolve.call((value,))?; + } + Ok(()) + } + + pub fn resolve_undefined(&self) -> Result<()> { + if let Some(resolve) = &self.resolve { + let () = resolve.call((rquickjs::Undefined,))?; + } + Ok(()) + } + + pub fn reject(&self, value: impl IntoArg<'js>) -> Result<()> { + if let Some(reject) = &self.reject { + let () = reject.call((value,))?; + } + Ok(()) + } + + pub fn reject_with_constructor(&self, constructor: &Constructor<'js>, msg: &str) -> Result<()> { + if let Some(reject) = &self.reject { + let e: Value = constructor.call((msg,))?; + let () = reject.call((e,))?; + } + Ok(()) + } + + pub fn is_pending(&self) -> bool { + self.promise.state() == PromiseState::Pending + } + + pub fn set_is_handled(&self) -> Result<()> { + self.promise.catch()?.call(( + This(self.promise.clone()), + Function::new(self.promise.ctx().clone(), || {}), + )) + } +} + +impl<'js> Trace<'js> for ResolveablePromise<'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + self.promise.trace(tracer); + self.resolve.trace(tracer); + self.reject.trace(tracer); + } +} + +pub fn with_promise_result<'js>( + ctx: &Ctx<'js>, + f: impl FnOnce() -> Result>, +) -> Result> { + match f() { + Ok(value) => Ok(value), + Err(Error::Exception) => promise_rejected_catch(ctx, &*PromisePrimordials::get(ctx)?), + Err(err) => Err(err), + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/utils/queue.rs b/stdlib/src/llrt/llrt_stream_web/utils/queue.rs new file mode 100644 index 00000000..4588b4eb --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/utils/queue.rs @@ -0,0 +1,103 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use std::collections::VecDeque; + +use rquickjs::{class::Trace, Ctx, Exception, JsLifetime, Result, Value}; + +use crate::llrt_stream_web::queuing_strategy::SizeValue; + +/// QueueWithSize is present in readable and writable streams and abstracts away certain queue operations +/// https://streams.spec.whatwg.org/#queue-with-sizes +#[derive(JsLifetime, Trace, Default)] +pub struct QueueWithSizes<'js> { + pub queue: VecDeque>, + pub queue_total_size: f64, +} + +impl<'js> QueueWithSizes<'js> { + pub fn new() -> Self { + Self { + queue: VecDeque::new(), + queue_total_size: 0.0, + } + } + + pub(crate) fn enqueue_value_with_size( + &mut self, + ctx: &Ctx<'js>, + value: Value<'js>, + size: SizeValue<'js>, + ) -> Result<()> { + let size = match is_non_negative_number(size) { + None => { + // If ! IsNonNegativeNumber(size) is false, throw a RangeError exception. + return Err(Exception::throw_range( + ctx, + "Size must be a finite, non-NaN, non-negative number.", + )); + } + Some(size) => size, + }; + + // If size is +∞, throw a RangeError exception. + if size.is_infinite() { + return Err(Exception::throw_range( + ctx, + "Size must be a finite, non-NaN, non-negative number.", + )); + }; + + // Append a new value-with-size with value value and size size to container.[[queue]]. + self.queue.push_back(ValueWithSize { value, size }); + + // Set container.[[queueTotalSize]] to container.[[queueTotalSize]] + size. + self.queue_total_size += size; + + Ok(()) + } + + pub fn dequeue_value(&mut self) -> Value<'js> { + // Let valueWithSize be container.[[queue]][0]. + // Remove valueWithSize from container.[[queue]]. + let value_with_size = self + .queue + .pop_front() + .expect("DequeueValue called with empty queue"); + // Set container.[[queueTotalSize]] to container.[[queueTotalSize]] − valueWithSize’s size. + self.queue_total_size -= value_with_size.size; + // If container.[[queueTotalSize]] < 0, set container.[[queueTotalSize]] to 0. (This can occur due to rounding errors.) + if self.queue_total_size < 0.0 { + self.queue_total_size = 0.0 + } + value_with_size.value + } + + pub fn reset_queue(&mut self) { + // Set container.[[queue]] to a new empty list. + self.queue.clear(); + // Set container.[[queueTotalSize]] to 0. + self.queue_total_size = 0.0; + } +} + +#[derive(JsLifetime, Trace, Clone)] +pub struct ValueWithSize<'js> { + pub value: Value<'js>, + size: f64, +} + +fn is_non_negative_number(value: SizeValue<'_>) -> Option { + // If Type(v) is not Number, return false. + let number = value.as_number()?; + // If v is NaN, return false. + if number.is_nan() { + return None; + } + + // If v < 0, return false. + if number < 0.0 { + return None; + } + + // Return true. + Some(number) +} diff --git a/stdlib/src/llrt/llrt_stream_web/writable/default_controller.rs b/stdlib/src/llrt/llrt_stream_web/writable/default_controller.rs new file mode 100644 index 00000000..b24b6ea8 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/writable/default_controller.rs @@ -0,0 +1,871 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use crate::llrt_abort::{AbortController, AbortSignal}; +use crate::llrt_utils::{ + option::{Null, Undefined}, + primordials::Primordial, +}; +use rquickjs::{ + class::{JsClass, OwnedBorrowMut, Trace}, + function::Constructor, + methods, + prelude::{Opt, This}, + Class, Ctx, Error, Exception, Function, JsLifetime, Object, Promise, Result, Symbol, Value, +}; + +use crate::llrt_stream_web::{ + queuing_strategy::{SizeAlgorithm, SizeValue}, + transform::controller::TransformStreamDefaultControllerClass, + transform::stream::TransformStreamClass, + utils::{ + class_from_owned_borrow_mut, + promise::{promise_resolved_with, upon_promise, PromisePrimordials}, + queue::QueueWithSizes, + UnwrapOrUndefined, + }, + writable::{ + default_writer::WritableStreamDefaultWriterOwned, + objects::{WritableStreamClassObjects, WritableStreamObjects}, + stream::{ + sink::UnderlyingSink, WritableStream, WritableStreamClass, WritableStreamOwned, + WritableStreamState, + }, + writer::{UndefinedWriter, WritableStreamWriter}, + }, +}; + +#[rquickjs::class] +#[derive(JsLifetime, Trace)] +pub(crate) struct WritableStreamDefaultController<'js> { + abort_algorithm: Option>, + close_algorithm: Option>, + container: QueueWithSizes<'js>, + pub(super) started: bool, + strategy_hwm: f64, + strategy_size_algorithm: Option>, + pub(super) abort_controller: Class<'js, AbortController<'js>>, + pub(super) stream: WritableStreamClass<'js>, + write_algorithm: Option>, + + primordials: WritableStreamDefaultControllerPrimordials<'js>, +} + +pub(crate) type WritableStreamDefaultControllerClass<'js> = + Class<'js, WritableStreamDefaultController<'js>>; +pub(crate) type WritableStreamDefaultControllerOwned<'js> = + OwnedBorrowMut<'js, WritableStreamDefaultController<'js>>; + +impl<'js> WritableStreamDefaultController<'js> { + pub(super) fn set_up_writable_stream_default_controller_from_underlying_sink( + ctx: Ctx<'js>, + stream: WritableStreamOwned<'js>, + underlying_sink: Null>>, + underlying_sink_dict: UnderlyingSink<'js>, + high_water_mark: f64, + size_algorithm: SizeAlgorithm<'js>, + ) -> Result<()> { + let (start_algorithm, write_algorithm, close_algorithm, abort_algorithm) = ( + // If underlyingSinkDict["start"] exists, then set startAlgorithm to an algorithm which returns the result of invoking underlyingSinkDict["start"] with argument list + // « controller », exception behavior "rethrow", and callback this value underlyingSink. + underlying_sink_dict + .start + .map(|f| WritableStartAlgorithm::Function { + f, + underlying_sink: underlying_sink.clone(), + }) + .unwrap_or(WritableStartAlgorithm::ReturnUndefined), + // If underlyingSinkDict["write"] exists, then set writeAlgorithm to an algorithm which takes an argument chunk and returns the result of invoking underlyingSinkDict["write"] with argument list + // « chunk, controller » and callback this value underlyingSink. + underlying_sink_dict + .write + .map(|f| WritableWriteAlgorithm::Function { + f, + underlying_sink: underlying_sink.clone(), + }) + .unwrap_or(WritableWriteAlgorithm::ReturnPromiseUndefined), + // If underlyingSinkDict["close"] exists, then set closeAlgorithm to an algorithm which returns the result of invoking underlyingSinkDict["close"] with argument list + // «» and callback this value underlyingSink. + underlying_sink_dict + .close + .map(|f| WritableCloseAlgorithm::Function { + f, + underlying_sink: underlying_sink.clone(), + }) + .unwrap_or(WritableCloseAlgorithm::ReturnPromiseUndefined), + // If underlyingSinkDict["abort"] exists, then set abortAlgorithm to an algorithm which takes an argument reason and returns the result of invoking underlyingSinkDict["abort"] with argument list + // « reason » and callback this value underlyingSink. + underlying_sink_dict + .abort + .map(|f| WritableAbortAlgorithm::Function { + f, + underlying_sink: underlying_sink.clone(), + }) + .unwrap_or(WritableAbortAlgorithm::ReturnPromiseUndefined), + ); + + // Perform ? SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm). + Self::set_up_writable_stream_default_controller( + ctx, + stream, + start_algorithm, + write_algorithm, + close_algorithm, + abort_algorithm, + high_water_mark, + size_algorithm, + ) + } + + #[allow(clippy::too_many_arguments)] + pub(crate) fn set_up_writable_stream_default_controller( + ctx: Ctx<'js>, + stream: WritableStreamOwned<'js>, + start_algorithm: WritableStartAlgorithm<'js>, + write_algorithm: WritableWriteAlgorithm<'js>, + close_algorithm: WritableCloseAlgorithm<'js>, + abort_algorithm: WritableAbortAlgorithm<'js>, + high_water_mark: f64, + size_algorithm: SizeAlgorithm<'js>, + ) -> Result<()> { + // TODO: needed? + let (stream_class, mut stream) = class_from_owned_borrow_mut(stream); + + let controller = Self { + // Set controller.[[stream]] to stream. + stream: stream_class, + + // Perform ! ResetQueue(controller). + container: QueueWithSizes::new(), + + // Set controller.[[abortController]] to a new AbortController. + abort_controller: Class::instance(ctx.clone(), AbortController::new(ctx.clone())?)?, + + // Set controller.[[started]] to false. + started: false, + + // Set controller.[[strategySizeAlgorithm]] to sizeAlgorithm. + strategy_size_algorithm: Some(size_algorithm), + // Set controller.[[strategyHWM]] to highWaterMark. + strategy_hwm: high_water_mark, + + // Set controller.[[writeAlgorithm]] to writeAlgorithm. + write_algorithm: Some(write_algorithm), + // Set controller.[[closeAlgorithm]] to closeAlgorithm. + close_algorithm: Some(close_algorithm), + // Set controller.[[abortAlgorithm]] to abortAlgorithm. + abort_algorithm: Some(abort_algorithm), + + primordials: WritableStreamDefaultControllerPrimordials::get(&ctx)?.clone(), + }; + + let controller_class = Class::instance(ctx.clone(), controller)?; + + // Set stream.[[controller]] to controller. + stream.controller = Some(controller_class.clone()); + + let objects = WritableStreamObjects::from_stream(stream); + + // Let backpressure be ! WritableStreamDefaultControllerGetBackpressure(controller). + let backpressure = objects + .controller + .writable_stream_default_controller_get_backpressure(); + // Perform ! WritableStreamUpdateBackpressure(stream, backpressure). + let objects = WritableStream::writable_stream_update_backpressure( + ctx.clone(), + objects, + backpressure, + )?; + let promise_primordials = objects.stream.promise_primordials.clone(); + + // Let startResult be the result of performing startAlgorithm. (This may throw an exception.) + let (start_result, objects_class) = + Self::start_algorithm(ctx.clone(), objects, start_algorithm)?; + + // Let startPromise be a promise resolved with startResult. + let start_promise = promise_resolved_with(&ctx, &promise_primordials, Ok(start_result))?; + + let _ = upon_promise::, _>(ctx.clone(), start_promise, { + move |ctx, result| { + let mut objects = + WritableStreamObjects::from_class_no_writer(objects_class).refresh_writer(); + match result { + // Upon fulfillment of startPromise, + Ok(_) => { + // Set controller.[[started]] to true. + objects.controller.started = true; + // Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). + Self::writable_stream_default_controller_advance_queue_if_needed( + ctx, objects, + )?; + } + // Upon rejection of startPromise with reason r, + Err(r) => { + // Set controller.[[started]] to true. + objects.controller.started = true; + + // Perform ! WritableStreamDealWithRejection(stream, r). + WritableStream::writable_stream_deal_with_rejection(ctx, objects, r)?; + } + } + Ok(()) + } + })?; + + Ok(()) + } + + pub(super) fn writable_stream_default_controller_close>( + ctx: Ctx<'js>, + mut objects: WritableStreamObjects<'js, W>, + ) -> Result> { + let close_sentinel = objects + .controller + .primordials + .close_sentinel + .as_value() + .clone(); + + // Perform ! EnqueueValueWithSize(controller, close sentinel, 0). + objects.controller.container.enqueue_value_with_size( + &ctx, + close_sentinel, + SizeValue::Native(0.0), + )?; + + // Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). + objects = Self::writable_stream_default_controller_advance_queue_if_needed(ctx, objects)?; + + Ok(objects) + } + + pub(super) fn writable_stream_default_controller_get_desired_size(&self) -> f64 { + self.strategy_hwm - self.container.queue_total_size + } + + pub fn writable_stream_default_controller_get_backpressure(&self) -> bool { + // Let desiredSize be ! WritableStreamDefaultControllerGetDesiredSize(controller). + let desired_size = self.writable_stream_default_controller_get_desired_size(); + // Return true if desiredSize ≤ 0, or false otherwise. + desired_size <= 0.0 + } + + pub(super) fn writable_stream_default_controller_get_chunk_size( + ctx: Ctx<'js>, + mut objects: WritableStreamObjects<'js, WritableStreamDefaultWriterOwned<'js>>, + chunk: Value<'js>, + ) -> Result<( + SizeValue<'js>, + WritableStreamObjects<'js, WritableStreamDefaultWriterOwned<'js>>, + )> { + let (return_value, objects_class) = + Self::strategy_size_algorithm(ctx.clone(), objects, chunk); + + // Let returnValue be the result of performing controller.[[strategySizeAlgorithm]], passing in chunk, and interpreting the result as a completion record. + match return_value { + Ok(chunk_size) => { + objects = WritableStreamObjects::from_class(objects_class); + Ok((chunk_size, objects)) + } + // If returnValue is an abrupt completion, + Err(Error::Exception) => { + let reason = ctx.catch(); + + objects = WritableStreamObjects::from_class(objects_class); + + // Perform ! WritableStreamDefaultControllerErrorIfNeeded(controller, returnValue.[[Value]]). + objects = Self::writable_stream_default_controller_error_if_needed( + ctx.clone(), + objects, + reason, + )?; + + // Return 1. + Ok((SizeValue::Native(1.0), objects)) + } + Err(err) => Err(err), + } + } + + fn writable_stream_default_controller_error_if_needed( + ctx: Ctx<'js>, + objects: WritableStreamObjects<'js, WritableStreamDefaultWriterOwned<'js>>, + error: Value<'js>, + ) -> Result>> { + // If controller.[[stream]].[[state]] is "writable", perform ! WritableStreamDefaultControllerError(controller, error). + if let WritableStreamState::Writable = objects.stream.state { + Self::writable_stream_default_controller_error(ctx, objects, error) + } else { + Ok(objects) + } + } + + fn writable_stream_default_controller_error>( + ctx: Ctx<'js>, + // Let stream be controller.[[stream]]. + mut objects: WritableStreamObjects<'js, W>, + reason: Value<'js>, + ) -> Result> { + // Perform ! WritableStreamDefaultControllerClearAlgorithms(controller). + objects + .controller + .writable_stream_default_controller_clear_algorithms(); + + // Perform ! WritableStreamStartErroring(stream, error). + objects = WritableStream::writable_stream_start_erroring(ctx, objects, reason)?; + + Ok(objects) + } + + fn writable_stream_default_controller_clear_algorithms(&mut self) { + // Set controller.[[writeAlgorithm]] to undefined. + self.write_algorithm = None; + + // Set controller.[[closeAlgorithm]] to undefined. + self.close_algorithm = None; + + // Set controller.[[abortAlgorithm]] to undefined. + self.abort_algorithm = None; + + // Set controller.[[strategySizeAlgorithm]] to undefined. + self.strategy_size_algorithm = None; + } + + pub(super) fn writable_stream_default_controller_write( + ctx: Ctx<'js>, + // Let stream be controller.[[stream]]. + mut objects: WritableStreamObjects<'js, WritableStreamDefaultWriterOwned<'js>>, + chunk: Value<'js>, + chunk_size: SizeValue<'js>, + ) -> Result>> { + // Let enqueueResult be EnqueueValueWithSize(controller, chunk, chunkSize). + let enqueue_result = objects + .controller + .container + .enqueue_value_with_size(&ctx, chunk, chunk_size); + + match enqueue_result { + // If enqueueResult is an abrupt completion, + Err(Error::Exception) => { + let reason = ctx.catch(); + // Perform ! WritableStreamDefaultControllerErrorIfNeeded(controller, enqueueResult.[[Value]]). + objects = + Self::writable_stream_default_controller_error_if_needed(ctx, objects, reason)?; + + return Ok(objects); + } + Err(err) => return Err(err), + Ok(()) => {} + } + + // If ! WritableStreamCloseQueuedOrInFlight(stream) is false and stream.[[state]] is "writable", + if !objects.stream.writable_stream_close_queued_or_in_flight() + && matches!(objects.stream.state, WritableStreamState::Writable) + { + // Let backpressure be ! WritableStreamDefaultControllerGetBackpressure(controller). + let backpressure = objects + .controller + .writable_stream_default_controller_get_backpressure(); + + // Perform ! WritableStreamUpdateBackpressure(stream, backpressure). + objects = WritableStream::writable_stream_update_backpressure( + ctx.clone(), + objects, + backpressure, + )?; + } + + // Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). + let objects = + Self::writable_stream_default_controller_advance_queue_if_needed(ctx, objects)?; + + Ok(objects) + } + + fn writable_stream_default_controller_advance_queue_if_needed>( + ctx: Ctx<'js>, + // Let stream be controller.[[stream]]. + objects: WritableStreamObjects<'js, W>, + ) -> Result> { + // If controller.[[started]] is false, return. + // If stream.[[inFlightWriteRequest]] is not undefined, return. + if !objects.controller.started || objects.stream.in_flight_write_request.is_some() { + return Ok(objects); + } + + // Let state be stream.[[state]]. + + // If state is "erroring", + if let WritableStreamState::Erroring(ref stored_error) = objects.stream.state { + let stored_error = stored_error.clone(); + // Perform ! WritableStreamFinishErroring(stream). + // Return. + return WritableStream::writable_stream_finish_erroring(ctx, objects, stored_error); + } + + let value = match objects.controller.container.queue.front() { + // If controller.[[queue]] is empty, return. + None => { + return Ok(objects); + } + // Let value be ! PeekQueueValue(controller). + Some(value) => value.clone(), + }; + + if value.value.as_symbol() == Some(&objects.controller.primordials.close_sentinel) { + // If value is the close sentinel, perform ! WritableStreamDefaultControllerProcessClose(controller). + Self::writable_stream_default_controller_process_close(ctx, objects) + } else { + // Otherwise, perform ! WritableStreamDefaultControllerProcessWrite(controller, value). + Self::writable_stream_default_controller_process_write(ctx, objects, value.value) + } + } + + fn writable_stream_default_controller_process_close>( + ctx: Ctx<'js>, + // Let stream be controller.[[stream]]. + mut objects: WritableStreamObjects<'js, W>, + ) -> Result> { + // Perform ! WritableStreamMarkCloseRequestInFlight(stream). + objects + .stream + .writable_stream_mark_close_request_in_flight(); + + // Perform ! DequeueValue(controller). + objects.controller.container.dequeue_value(); + + // Assert: controller.[[queue]] is empty. + + // Let sinkClosePromise be the result of performing controller.[[closeAlgorithm]]. + let (sink_close_promise, objects_class) = Self::close_algorithm(&ctx, objects)?; + + objects = WritableStreamObjects::from_class(objects_class.clone()); + + // Perform ! WritableStreamDefaultControllerClearAlgorithms(controller). + objects + .controller + .writable_stream_default_controller_clear_algorithms(); + + upon_promise::, ()>(ctx, sink_close_promise, |ctx, result| { + let objects = WritableStreamObjects::from_class(objects_class); + match result { + // Upon fulfillment of sinkClosePromise, + Ok(_) => { + // Perform ! WritableStreamFinishInFlightClose(stream). + WritableStream::writable_stream_finish_in_flight_close(objects)?; + } + // Upon rejection of sinkClosePromise with reason reason, + Err(reason) => { + // Perform ! WritableStreamFinishInFlightCloseWithError(stream, reason). + WritableStream::writable_stream_finish_in_flight_close_with_error( + ctx, objects, reason, + )?; + } + } + + Ok(()) + })?; + + Ok(objects) + } + + fn writable_stream_default_controller_process_write>( + ctx: Ctx<'js>, + // Let stream be controller.[[stream]]. + mut objects: WritableStreamObjects<'js, W>, + chunk: Value<'js>, + ) -> Result> { + // Perform ! WritableStreamMarkFirstWriteRequestInFlight(stream). + objects + .stream + .writable_stream_mark_first_write_request_in_flight(); + + // Let sinkWritePromise be the result of performing controller.[[writeAlgorithm]], passing in chunk. + let (sink_write_promise, objects_class) = Self::write_algorithm(&ctx, objects, chunk)?; + + // Upon fulfillment of sinkWritePromise, + upon_promise::, ()>(ctx, sink_write_promise, { + let objects_class = objects_class.clone(); + |ctx, result| { + let mut objects = WritableStreamObjects::from_class(objects_class).refresh_writer(); + match result { + Ok(_) => { + // Upon fulfillment of sinkWritePromise, + // Perform ! WritableStreamFinishInFlightWrite(stream). + objects.stream.writable_stream_finish_in_flight_write()?; + + // Let state be stream.[[state]]. + let state = &objects.stream.state; + + // Perform ! DequeueValue(controller). + objects.controller.container.dequeue_value(); + + // If ! WritableStreamCloseQueuedOrInFlight(stream) is false and state is "writable", + if !objects.stream.writable_stream_close_queued_or_in_flight() + && matches!(state, WritableStreamState::Writable) + { + // Let backpressure be ! WritableStreamDefaultControllerGetBackpressure(controller). + let backpressure = objects + .controller + .writable_stream_default_controller_get_backpressure(); + + // Perform ! WritableStreamUpdateBackpressure(stream, backpressure). + objects = WritableStream::writable_stream_update_backpressure( + ctx.clone(), + objects, + backpressure, + )?; + } + + // Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). + WritableStreamDefaultController::writable_stream_default_controller_advance_queue_if_needed(ctx, objects)?; + } + Err(reason) => { + // Upon rejection of sinkWritePromise with reason, + if let WritableStreamState::Writable = objects.stream.state { + // If stream.[[state]] is "writable", perform ! WritableStreamDefaultControllerClearAlgorithms(controller). + objects + .controller + .writable_stream_default_controller_clear_algorithms(); + } + // Perform ! WritableStreamFinishInFlightWriteWithError(stream, reason). + WritableStream::writable_stream_finish_in_flight_write_with_error( + ctx, objects, reason, + )?; + } + } + + Ok(()) + } + })?; + + Ok(WritableStreamObjects::from_class(objects_class)) + } + + pub(super) fn error_steps(&mut self) { + // Perform ! ResetQueue(this). + self.reset_queue() + } + + fn reset_queue(&mut self) { + // Set container.[[queue]] to a new empty list. + self.container.queue.clear(); + // Set container.[[queueTotalSize]] to 0. + self.container.queue_total_size = 0.0; + } + + pub(super) fn abort_steps>( + ctx: &Ctx<'js>, + mut objects: WritableStreamObjects<'js, W>, + reason: Value<'js>, + ) -> Result<(Promise<'js>, WritableStreamObjects<'js, W>)> { + // Let result be the result of performing this.[[abortAlgorithm]], passing reason. + let (result, objects_class) = Self::abort_algorithm(ctx, objects, reason)?; + + objects = WritableStreamObjects::from_class(objects_class); + + // Perform ! WritableStreamDefaultControllerClearAlgorithms(this). + objects + .controller + .writable_stream_default_controller_clear_algorithms(); + + // Return result. + Ok((result, objects)) + } + + fn strategy_size_algorithm( + ctx: Ctx<'js>, + objects: WritableStreamObjects<'js, WritableStreamDefaultWriterOwned<'js>>, + chunk: Value<'js>, + ) -> ( + Result>, + WritableStreamClassObjects<'js, WritableStreamDefaultWriterOwned<'js>>, + ) { + let strategy_size_algorithm = objects + .controller + .strategy_size_algorithm + .clone() + .unwrap_or(SizeAlgorithm::AlwaysOne); + + let objects_class = objects.into_inner(); + + (strategy_size_algorithm.call(ctx, chunk), objects_class) + } + + fn start_algorithm( + ctx: Ctx<'js>, + objects: WritableStreamObjects<'js, UndefinedWriter>, + start_algorithm: WritableStartAlgorithm<'js>, + ) -> Result<(Value<'js>, WritableStreamClassObjects<'js, UndefinedWriter>)> { + let objects_class = objects.into_inner(); + + Ok(( + start_algorithm.call(ctx, objects_class.controller.clone())?, + objects_class, + )) + } + + fn write_algorithm>( + ctx: &Ctx<'js>, + objects: WritableStreamObjects<'js, W>, + chunk: Value<'js>, + ) -> Result<(Promise<'js>, WritableStreamClassObjects<'js, W>)> { + let write_algorithm = + objects.controller.write_algorithm.clone().expect( + "write algorithm used after WritableStreamDefaultControllerClearAlgorithms", + ); + let promise_primordials = objects.stream.promise_primordials.clone(); + let objects_class = objects.into_inner(); + + Ok(( + write_algorithm.call( + ctx, + &promise_primordials, + objects_class.controller.clone().clone(), + chunk, + )?, + objects_class, + )) + } + + fn close_algorithm>( + ctx: &Ctx<'js>, + objects: WritableStreamObjects<'js, W>, + ) -> Result<(Promise<'js>, WritableStreamClassObjects<'js, W>)> { + let close_algorithm = + objects.controller.close_algorithm.clone().expect( + "close algorithm used after WritableStreamDefaultControllerClearAlgorithms", + ); + let promise_primordials = objects.stream.promise_primordials.clone(); + let objects_class = objects.into_inner(); + + Ok(( + close_algorithm.call(ctx, &promise_primordials)?, + objects_class, + )) + } + + fn abort_algorithm>( + ctx: &Ctx<'js>, + objects: WritableStreamObjects<'js, W>, + reason: Value<'js>, + ) -> Result<(Promise<'js>, WritableStreamClassObjects<'js, W>)> { + let abort_algorithm = + objects.controller.abort_algorithm.clone().expect( + "abort algorithm used after WritableStreamDefaultControllerClearAlgorithms", + ); + let promise_primordials = objects.stream.promise_primordials.clone(); + let objects_class = objects.into_inner(); + + Ok(( + abort_algorithm.call(ctx, &promise_primordials, reason)?, + objects_class, + )) + } +} + +#[methods(rename_all = "camelCase")] +impl<'js> WritableStreamDefaultController<'js> { + // this is required by web platform tests + #[qjs(get)] + pub fn constructor(ctx: Ctx<'js>) -> Result>> { + ::constructor(&ctx) + } + + #[qjs(constructor)] + fn new(ctx: Ctx<'js>) -> Result> { + Err(Exception::throw_type(&ctx, "Illegal constructor")) + } + + // readonly attribute AbortSignal signal; + #[qjs(get)] + fn signal(&self) -> Class<'js, AbortSignal<'js>> { + // Return this.[[abortController]]'s signal. + self.abort_controller.borrow().signal() + } + + // undefined error(optional any e); + fn error( + ctx: Ctx<'js>, + controller: This>, + e: Opt>, + ) -> Result<()> { + let objects = WritableStreamObjects::from_controller(controller.0); + + // Let state be this.[[stream]].[[state]]. + // If state is not "writable", return. + if !matches!(objects.stream.state, WritableStreamState::Writable) { + return Ok(()); + } + + // Perform ! WritableStreamDefaultControllerError(this, e). + Self::writable_stream_default_controller_error( + ctx.clone(), + objects.refresh_writer(), + e.0.unwrap_or_undefined(&ctx), + )?; + + Ok(()) + } +} + +#[derive(Clone)] +pub(crate) enum WritableStartAlgorithm<'js> { + ReturnUndefined, + Function { + f: Function<'js>, + underlying_sink: Null>>, + }, + Transform(Promise<'js>), +} + +impl<'js> WritableStartAlgorithm<'js> { + fn call( + &self, + ctx: Ctx<'js>, + controller: WritableStreamDefaultControllerClass<'js>, + ) -> Result> { + match self { + WritableStartAlgorithm::ReturnUndefined => Ok(Value::new_undefined(ctx.clone())), + WritableStartAlgorithm::Function { f, underlying_sink } => { + f.call::<_, Value>((This(underlying_sink.clone()), controller)) + } + WritableStartAlgorithm::Transform(promise) => Ok(promise.clone().into_value()), + } + } +} + +#[derive(JsLifetime, Trace, Clone)] +pub(crate) enum WritableWriteAlgorithm<'js> { + ReturnPromiseUndefined, + Function { + f: Function<'js>, + underlying_sink: Null>>, + }, + Transform { + stream: TransformStreamClass<'js>, + controller: TransformStreamDefaultControllerClass<'js>, + }, +} + +impl<'js> WritableWriteAlgorithm<'js> { + fn call( + &self, + ctx: &Ctx<'js>, + promise_primordials: &PromisePrimordials<'js>, + controller: WritableStreamDefaultControllerClass<'js>, + chunk: Value<'js>, + ) -> Result> { + match self { + WritableWriteAlgorithm::ReturnPromiseUndefined => { + Ok(promise_primordials.promise_resolved_with_undefined.clone()) + } + WritableWriteAlgorithm::Function { f, underlying_sink } => promise_resolved_with( + ctx, + promise_primordials, + f.call::<_, Value>((This(underlying_sink.clone()), chunk, controller)), + ), + WritableWriteAlgorithm::Transform { + stream, + controller: ts_controller, + } => crate::llrt_stream_web::transform::stream::sink_write_algorithm( + ctx.clone(), + stream, + ts_controller, + chunk, + ), + } + } +} + +#[derive(JsLifetime, Trace, Clone)] +pub(crate) enum WritableCloseAlgorithm<'js> { + ReturnPromiseUndefined, + Function { + f: Function<'js>, + underlying_sink: Null>>, + }, + Transform { + stream: TransformStreamClass<'js>, + controller: TransformStreamDefaultControllerClass<'js>, + }, +} + +impl<'js> WritableCloseAlgorithm<'js> { + fn call( + &self, + ctx: &Ctx<'js>, + promise_primordials: &PromisePrimordials<'js>, + ) -> Result> { + match self { + WritableCloseAlgorithm::ReturnPromiseUndefined => { + Ok(promise_primordials.promise_resolved_with_undefined.clone()) + } + WritableCloseAlgorithm::Function { f, underlying_sink } => promise_resolved_with( + ctx, + promise_primordials, + f.call::<_, Value>((This(underlying_sink.clone()),)), + ), + WritableCloseAlgorithm::Transform { stream, controller } => { + crate::llrt_stream_web::transform::stream::sink_close_algorithm( + ctx.clone(), + stream, + controller, + ) + } + } + } +} + +#[derive(JsLifetime, Trace, Clone)] +pub(crate) enum WritableAbortAlgorithm<'js> { + ReturnPromiseUndefined, + Function { + f: Function<'js>, + underlying_sink: Null>>, + }, + Transform { + controller: TransformStreamDefaultControllerClass<'js>, + }, +} + +impl<'js> WritableAbortAlgorithm<'js> { + fn call( + &self, + ctx: &Ctx<'js>, + promise_primordials: &PromisePrimordials<'js>, + reason: Value<'js>, + ) -> Result> { + match self { + WritableAbortAlgorithm::ReturnPromiseUndefined => { + Ok(promise_primordials.promise_resolved_with_undefined.clone()) + } + WritableAbortAlgorithm::Function { f, underlying_sink } => promise_resolved_with( + ctx, + promise_primordials, + f.call::<_, Value>((This(underlying_sink.clone()), reason)), + ), + WritableAbortAlgorithm::Transform { controller } => { + crate::llrt_stream_web::transform::stream::sink_abort_algorithm( + ctx.clone(), + controller, + reason, + ) + } + } + } +} + +#[derive(Trace, Clone, JsLifetime)] +pub(crate) struct WritableStreamDefaultControllerPrimordials<'js> { + close_sentinel: Symbol<'js>, +} + +impl<'js> Primordial<'js> for WritableStreamDefaultControllerPrimordials<'js> { + fn new(ctx: &Ctx<'js>) -> Result + where + Self: Sized, + { + Ok(Self { + close_sentinel: Symbol::new_global(ctx.clone(), "close sentinel")?, + }) + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/writable/default_writer.rs b/stdlib/src/llrt/llrt_stream_web/writable/default_writer.rs new file mode 100644 index 00000000..0a4be1aa --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/writable/default_writer.rs @@ -0,0 +1,497 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use crate::llrt_utils::option::Null; +use rquickjs::{ + class::{JsClass, OwnedBorrow, OwnedBorrowMut, Trace}, + function::Constructor, + prelude::{Opt, This}, + Class, Ctx, Exception, JsLifetime, Promise, Result, Value, +}; + +use crate::llrt_stream_web::{ + utils::{ + promise::{ + promise_rejected_with, promise_rejected_with_constructor, PromisePrimordials, + ResolveablePromise, + }, + UnwrapOrUndefined, + }, + writable::{ + default_controller::WritableStreamDefaultController, + objects::WritableStreamObjects, + stream::{WritableStream, WritableStreamOwned, WritableStreamState}, + writer::WritableStreamWriter, + }, +}; + +#[rquickjs::class] +#[derive(JsLifetime)] +pub(crate) struct WritableStreamDefaultWriter<'js> { + pub(crate) ready_promise: ResolveablePromise<'js>, + pub(crate) closed_promise: ResolveablePromise<'js>, + pub(super) stream: Option>>, + + constructor_type_error: Constructor<'js>, + promise_primordials: PromisePrimordials<'js>, +} + +impl<'js> Trace<'js> for WritableStreamDefaultWriter<'js> { + fn trace<'a>(&self, tracer: rquickjs::class::Tracer<'a, 'js>) { + self.ready_promise.trace(tracer); + self.closed_promise.trace(tracer); + self.stream.trace(tracer); + self.constructor_type_error.trace(tracer); + self.promise_primordials.trace(tracer); + } +} + +pub(crate) type WritableStreamDefaultWriterClass<'js> = + Class<'js, WritableStreamDefaultWriter<'js>>; +pub(crate) type WritableStreamDefaultWriterOwned<'js> = + OwnedBorrowMut<'js, WritableStreamDefaultWriter<'js>>; + +#[rquickjs::methods(rename_all = "camelCase")] +impl<'js> WritableStreamDefaultWriter<'js> { + // this is required by web platform tests + #[qjs(get)] + pub fn constructor(ctx: Ctx<'js>) -> Result>> { + ::constructor(&ctx) + } + + #[qjs(constructor)] + fn new(ctx: Ctx<'js>, stream: WritableStreamOwned<'js>) -> Result> { + // Perform ? SetUpWritableStreamDefaultWriter(this, stream). + let (_, writer) = Self::set_up_writable_stream_default_writer(&ctx, stream)?; + Ok(writer) + } + + #[qjs(get)] + fn closed(writer: This>) -> Promise<'js> { + // Return this.[[closedPromise]]. + writer.0.closed_promise.promise.clone() + } + + #[qjs(get)] + fn desired_size(ctx: Ctx<'js>, writer: This>) -> Result> { + match writer.0.stream { + // If this.[[stream]] is undefined, throw a TypeError exception. + None => Err(Exception::throw_type( + &ctx, + "Cannot desiredSize a stream using a released writer", + )), + Some(ref stream) => { + // Return ! WritableStreamDefaultWriterGetDesiredSize(this). + Self::writable_stream_default_writer_get_desired_size(&OwnedBorrowMut::from_class( + stream.clone(), + )) + } + } + } + + #[qjs(get)] + fn ready(writer: This>) -> Promise<'js> { + // Return this.[[readyPromise]]. + writer.0.ready_promise.promise.clone() + } + + fn abort( + ctx: Ctx<'js>, + writer: This>, + reason: Opt>, + ) -> Result> { + // If this.[[stream]] is undefined, throw a TypeError exception. + if writer.0.stream.is_none() { + promise_rejected_with_constructor( + &writer.constructor_type_error, + &writer.promise_primordials, + "Cannot abort a stream using a released writer", + ) + } else { + let objects = WritableStreamObjects::from_writer(writer.0); + + // Return ! WritableStreamDefaultWriterAbort(this, reason). + Self::writable_stream_default_writer_abort(ctx.clone(), objects, reason.0) + } + } + + fn close(ctx: Ctx<'js>, writer: This>) -> Result> { + // If this.[[stream]] is undefined, throw a TypeError exception. + if writer.0.stream.is_none() { + promise_rejected_with_constructor( + &writer.constructor_type_error, + &writer.promise_primordials, + "Cannot close a stream using a released writer", + ) + } else { + let objects = WritableStreamObjects::from_writer(writer.0); + + // If ! WritableStreamCloseQueuedOrInFlight(stream) is true, return a promise rejected with a TypeError exception. + if objects.stream.writable_stream_close_queued_or_in_flight() { + return promise_rejected_with_constructor( + &objects.writer.constructor_type_error, + &objects.writer.promise_primordials, + "Cannot close an already-closing", + ); + } + + // Return ! WritableStreamDefaultWriterClose(this). + Self::writable_stream_default_writer_close(ctx, objects) + } + } + + fn release_lock(writer: This>) -> Result<()> { + // If stream is undefined, return. + if writer.0.stream.is_none() { + Ok(()) + } else { + let objects = WritableStreamObjects::from_writer(writer.0); + + // Perform ! WritableStreamDefaultWriterRelease(this). + Self::writable_stream_default_writer_release(objects) + } + } + + fn write( + ctx: Ctx<'js>, + writer: This>, + chunk: Opt>, + ) -> Result> { + // If this.[[stream]] is undefined, throw a TypeError exception. + if writer.0.stream.is_none() { + promise_rejected_with_constructor( + &writer.constructor_type_error, + &writer.promise_primordials, + "Cannot write a stream using a released writer", + ) + } else { + let objects = WritableStreamObjects::from_writer(writer.0); + + // Return ! WritableStreamDefaultWriterWrite(this, chunk). + Self::writable_stream_default_writer_write( + ctx.clone(), + objects, + chunk.0.unwrap_or_undefined(&ctx), + ) + } + } +} + +impl<'js> WritableStreamDefaultWriter<'js> { + pub(crate) fn acquire_writable_stream_default_writer( + ctx: &Ctx<'js>, + stream: WritableStreamOwned<'js>, + ) -> Result<(WritableStreamOwned<'js>, Class<'js, Self>)> { + Self::set_up_writable_stream_default_writer(ctx, stream) + } + + pub(super) fn set_up_writable_stream_default_writer( + ctx: &Ctx<'js>, + mut stream: WritableStreamOwned<'js>, + ) -> Result<(WritableStreamOwned<'js>, Class<'js, Self>)> { + // If ! IsWritableStreamLocked(stream) is true, throw a TypeError exception. + if stream.is_writable_stream_locked() { + return Err(Exception::throw_type( + ctx, + "This stream has already been locked for exclusive writing by another writer", + )); + } + + let promise_primordials = stream.promise_primordials.clone(); + let constructor_type_error = stream.constructor_type_error.clone(); + let stream_class = stream.into_inner(); + stream = OwnedBorrowMut::from_class(stream_class.clone()); + + let (ready_promise, closed_promise) = match stream.state { + WritableStreamState::Writable => { + let ready_promise = + if !stream.writable_stream_close_queued_or_in_flight() && stream.backpressure { + // If ! WritableStreamCloseQueuedOrInFlight(stream) is false and stream.[[backpressure]] is true, set writer.[[readyPromise]] to a new promise. + ResolveablePromise::new(ctx)? + } else { + // Otherwise, set writer.[[readyPromise]] to a promise resolved with undefined. + ResolveablePromise::resolved_with_undefined(&stream.promise_primordials) + }; + + // Set writer.[[closedPromise]] to a new promise. + (ready_promise, ResolveablePromise::new(ctx)?) + } + WritableStreamState::Erroring(ref stored_error) => { + let ready_promise = ResolveablePromise::rejected_with( + &stream.promise_primordials, + stored_error.clone(), + )?; + ready_promise.set_is_handled()?; + // Set writer.[[closedPromise]] to a new promise. + (ready_promise, ResolveablePromise::new(ctx)?) + } + WritableStreamState::Closed => { + let promise = + ResolveablePromise::resolved_with_undefined(&stream.promise_primordials); + // Set writer.[[readyPromise]] to a promise resolved with undefined. + // Set writer.[[closedPromise]] to a promise resolved with undefined. + (promise.clone(), promise) + } + // Let storedError be stream.[[storedError]]. + WritableStreamState::Errored(ref stored_error) => { + let promise = ResolveablePromise::rejected_with( + &stream.promise_primordials, + stored_error.clone(), + )?; + promise.set_is_handled()?; + // Set writer.[[readyPromise]] to a promise rejected with storedError. + // Set writer.[[readyPromise]].[[PromiseIsHandled]] to true. + // Set writer.[[closedPromise]] to a promise rejected with storedError. + // Set writer.[[closedPromise]].[[PromiseIsHandled]] to true. + (promise.clone(), promise) + } + }; + + let writer = Self { + ready_promise, + closed_promise, + // Set writer.[[stream]] to stream. + stream: Some(stream_class), + promise_primordials, + constructor_type_error, + }; + + let writer = Class::instance(ctx.clone(), writer)?; + + stream.writer = Some(writer.clone()); + + Ok((stream, writer)) + } + + pub(super) fn writable_stream_default_writer_ensure_ready_promise_rejected( + &mut self, + promise_primordials: &PromisePrimordials<'js>, + error: Value<'js>, + ) -> Result<()> { + if self.ready_promise.is_pending() { + // If writer.[[readyPromise]].[[PromiseState]] is "pending", reject writer.[[readyPromise]] with error. + self.ready_promise.reject(error)?; + } else { + // Otherwise, set writer.[[readyPromise]] to a promise rejected with error. + self.ready_promise = ResolveablePromise::rejected_with(promise_primordials, error)?; + } + + // Set writer.[[readyPromise]].[[PromiseIsHandled]] to true. + self.ready_promise.set_is_handled()?; + Ok(()) + } + + pub(super) fn writable_stream_default_writer_ensure_closed_promise_rejected( + &mut self, + promise_primordials: &PromisePrimordials<'js>, + error: Value<'js>, + ) -> Result<()> { + if self.closed_promise.is_pending() { + // If writer.[[closedPromise]].[[PromiseState]] is "pending", reject writer.[[closedPromise]] with error. + self.closed_promise.reject(error)?; + } else { + // Otherwise, set writer.[[closedPromise]] to a promise rejected with error. + self.closed_promise = ResolveablePromise::rejected_with(promise_primordials, error)?; + } + + // Set writer.[[closedPromise]].[[PromiseIsHandled]] to true. + self.closed_promise.set_is_handled()?; + Ok(()) + } + + pub(super) fn writable_stream_default_writer_get_desired_size( + // Let stream be writer.[[stream]]. + stream: &WritableStream<'js>, + ) -> Result> { + // Let state be stream.[[state]]. + // If state is "errored" or "erroring", return null. + if matches!( + stream.state, + WritableStreamState::Errored(_) | WritableStreamState::Erroring(_) + ) { + return Ok(Null(None)); + } + + // If state is "closed", return 0. + if matches!(stream.state, WritableStreamState::Closed) { + return Ok(Null(Some(0.0))); + } + + // Return ! WritableStreamDefaultControllerGetDesiredSize(stream.[[controller]]). + let controller = OwnedBorrow::from_class( + stream + .controller + .clone() + .expect("Stream in state writable must have a controller"), + ); + + Ok(Null(Some( + controller.writable_stream_default_controller_get_desired_size(), + ))) + } + + fn writable_stream_default_writer_abort( + ctx: Ctx<'js>, + objects: WritableStreamObjects<'js, OwnedBorrowMut<'js, Self>>, + reason: Option>, + ) -> Result> { + // Return ! WritableStreamAbort(stream, reason). + let (promise, _) = WritableStream::writable_stream_abort(ctx, objects, reason)?; + Ok(promise) + } + + fn writable_stream_default_writer_close( + ctx: Ctx<'js>, + objects: WritableStreamObjects<'js, OwnedBorrowMut<'js, Self>>, + ) -> Result> { + // Return ! WritableStreamClose(stream). + let (promise, _) = WritableStream::writable_stream_close(ctx, objects)?; + Ok(promise) + } + + pub(crate) fn writable_stream_default_writer_close_with_error_propagation( + ctx: Ctx<'js>, + // Let stream be writer.[[stream]]. + objects: WritableStreamObjects<'js, OwnedBorrowMut<'js, Self>>, + ) -> Result> { + // Let state be stream.[[state]]. + // If ! WritableStreamCloseQueuedOrInFlight(stream) is true or state is "closed", return a promise resolved with undefined. + if objects.stream.writable_stream_close_queued_or_in_flight() + || matches!(objects.stream.state, WritableStreamState::Closed) + { + return Ok(objects + .stream + .promise_primordials + .promise_resolved_with_undefined + .clone()); + } + + // If state is "errored", return a promise rejected with stream.[[storedError]]. + if let WritableStreamState::Errored(ref stored_error) = objects.stream.state { + return promise_rejected_with( + &objects.stream.promise_primordials, + stored_error.clone(), + ); + } + + // Return ! WritableStreamDefaultWriterClose(writer). + Self::writable_stream_default_writer_close(ctx, objects) + } + + pub(crate) fn writable_stream_default_writer_release( + mut objects: WritableStreamObjects<'js, OwnedBorrowMut<'js, Self>>, + ) -> Result<()> { + // Let releasedError be a new TypeError. + let released_error: Value = objects.stream.constructor_type_error.call(( + "Writer was released and can no longer be used to monitor the stream's closedness", + ))?; + + // Perform ! WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, releasedError). + objects + .writer + .writable_stream_default_writer_ensure_ready_promise_rejected( + &objects.stream.promise_primordials, + released_error.clone(), + )?; + // Perform ! WritableStreamDefaultWriterEnsureClosedPromiseRejected(writer, releasedError). + objects + .writer + .writable_stream_default_writer_ensure_closed_promise_rejected( + &objects.stream.promise_primordials, + released_error, + )?; + + // Set stream.[[writer]] to undefined. + objects.stream.writer = None; + // Set writer.[[stream]] to undefined. + objects.writer.stream = None; + + Ok(()) + } + + pub(crate) fn writable_stream_default_writer_write( + ctx: Ctx<'js>, + objects: WritableStreamObjects<'js, OwnedBorrowMut<'js, Self>>, + chunk: Value<'js>, + ) -> Result> { + // Let chunkSize be ! WritableStreamDefaultControllerGetChunkSize(controller, chunk). + let (chunk_size, mut objects) = + WritableStreamDefaultController::writable_stream_default_controller_get_chunk_size( + ctx.clone(), + objects, + chunk.clone(), + )?; + + let stream_class = objects.stream.into_inner(); + objects.stream = OwnedBorrowMut::from_class(stream_class.clone()); + + // If stream is not equal to writer.[[stream]], return a promise rejected with a TypeError exception. + if objects.writer.stream != Some(stream_class) { + return promise_rejected_with_constructor( + &objects.stream.constructor_type_error, + &objects.stream.promise_primordials, + "Cannot write to a stream using a released writer", + ); + } + + // Let state be stream.[[state]]. + // If state is "errored", return a promise rejected with stream.[[storedError]]. + if let WritableStreamState::Errored(ref stored_error) = objects.stream.state { + return promise_rejected_with( + &objects.stream.promise_primordials, + stored_error.clone(), + ); + } + + // If ! WritableStreamCloseQueuedOrInFlight(stream) is true or state is "closed", return a promise rejected with a TypeError exception indicating that the stream is closing or closed. + if objects.stream.writable_stream_close_queued_or_in_flight() + || matches!(objects.stream.state, WritableStreamState::Closed) + { + return promise_rejected_with_constructor( + &objects.stream.constructor_type_error, + &objects.stream.promise_primordials, + "The stream is closing or closed and cannot be written to", + ); + } + + // If state is "erroring", return a promise rejected with stream.[[storedError]]. + if let WritableStreamState::Erroring(ref stored_error) = objects.stream.state { + return promise_rejected_with( + &objects.stream.promise_primordials, + stored_error.clone(), + ); + } + + // Let promise be ! WritableStreamAddWriteRequest(stream). + let promise = objects.stream.writable_stream_add_write_request(&ctx); + // Perform ! WritableStreamDefaultControllerWrite(controller, chunk, chunkSize). + WritableStreamDefaultController::writable_stream_default_controller_write( + ctx, objects, chunk, chunk_size, + )?; + + // Return promise. + promise + } +} + +impl<'js> WritableStreamWriter<'js> for WritableStreamDefaultWriterOwned<'js> { + type Class = WritableStreamDefaultWriterClass<'js>; + + fn with_writer( + self, + ctx: C, + default: impl FnOnce( + C, + WritableStreamDefaultWriterOwned<'js>, + ) -> Result<(C, WritableStreamDefaultWriterOwned<'js>)>, + _: impl FnOnce(C) -> Result, + ) -> Result<(C, Self)> { + default(ctx, self) + } + + fn into_inner(self) -> Self::Class { + self.into_inner() + } + + fn from_class(class: Self::Class) -> Self { + OwnedBorrowMut::from_class(class) + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/writable/mod.rs b/stdlib/src/llrt/llrt_stream_web/writable/mod.rs new file mode 100644 index 00000000..4b7e3284 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/writable/mod.rs @@ -0,0 +1,17 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +mod default_controller; +mod default_writer; +mod objects; +mod stream; +mod writer; + +pub(crate) use default_controller::{ + WritableAbortAlgorithm, WritableCloseAlgorithm, WritableStartAlgorithm, + WritableStreamDefaultController, WritableStreamDefaultControllerPrimordials, + WritableWriteAlgorithm, +}; +pub(crate) use default_writer::{WritableStreamDefaultWriter, WritableStreamDefaultWriterOwned}; +pub(crate) use objects::{WritableStreamClassObjects, WritableStreamObjects}; +pub(crate) use stream::{ + WritableStream, WritableStreamClass, WritableStreamOwned, WritableStreamState, +}; diff --git a/stdlib/src/llrt/llrt_stream_web/writable/objects.rs b/stdlib/src/llrt/llrt_stream_web/writable/objects.rs new file mode 100644 index 00000000..01bf593c --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/writable/objects.rs @@ -0,0 +1,162 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use rquickjs::{class::OwnedBorrowMut, Class, Result}; + +use crate::llrt_stream_web::writable::{ + default_controller::{ + WritableStreamDefaultControllerClass, WritableStreamDefaultControllerOwned, + }, + default_writer::WritableStreamDefaultWriterOwned, + stream::{WritableStream, WritableStreamOwned}, + writer::{UndefinedWriter, WritableStreamWriter}, +}; + +pub(crate) struct WritableStreamObjects<'js, W> { + pub(crate) stream: WritableStreamOwned<'js>, + pub(crate) controller: WritableStreamDefaultControllerOwned<'js>, + pub(crate) writer: W, +} + +pub(crate) struct WritableStreamClassObjects<'js, W: WritableStreamWriter<'js>> { + pub(crate) stream: Class<'js, WritableStream<'js>>, + pub(crate) controller: WritableStreamDefaultControllerClass<'js>, + pub(crate) writer: W::Class, +} + +impl<'js, W: WritableStreamWriter<'js>> Clone for WritableStreamClassObjects<'js, W> { + fn clone(&self) -> Self { + Self { + stream: self.stream.clone(), + controller: self.controller.clone(), + writer: self.writer.clone(), + } + } +} + +impl<'js, W: WritableStreamWriter<'js>> WritableStreamObjects<'js, W> { + pub(super) fn into_inner(self) -> WritableStreamClassObjects<'js, W> { + WritableStreamClassObjects { + stream: self.stream.into_inner(), + controller: self.controller.into_inner(), + writer: self.writer.into_inner(), + } + } + + pub(crate) fn from_class(objects_class: WritableStreamClassObjects<'js, W>) -> Self { + Self { + stream: OwnedBorrowMut::from_class(objects_class.stream), + controller: OwnedBorrowMut::from_class(objects_class.controller), + writer: W::from_class(objects_class.writer), + } + } + + pub(super) fn from_class_no_writer( + objects_class: WritableStreamClassObjects<'js, W>, + ) -> WritableStreamObjects<'js, UndefinedWriter> { + WritableStreamObjects { + stream: OwnedBorrowMut::from_class(objects_class.stream), + controller: OwnedBorrowMut::from_class(objects_class.controller), + writer: UndefinedWriter, + } + } + + pub(super) fn with_writer( + mut self, + default: impl FnOnce( + WritableStreamObjects<'js, WritableStreamDefaultWriterOwned<'js>>, + ) -> Result< + WritableStreamObjects<'js, WritableStreamDefaultWriterOwned<'js>>, + >, + none: impl FnOnce( + WritableStreamObjects<'js, UndefinedWriter>, + ) -> Result>, + ) -> Result { + ((self.stream, self.controller), self.writer) = self.writer.with_writer( + (self.stream, self.controller), + |(stream, controller), writer| { + let objects = default(WritableStreamObjects { + stream, + controller, + writer, + })?; + + Ok(((objects.stream, objects.controller), objects.writer)) + }, + |(stream, controller)| { + let objects = none(WritableStreamObjects { + stream, + controller, + writer: UndefinedWriter, + })?; + + Ok((objects.stream, objects.controller)) + }, + )?; + + Ok(self) + } +} + +impl<'js, W: WritableStreamWriter<'js>> WritableStreamObjects<'js, W> { + pub(super) fn refresh_writer( + mut self, + ) -> WritableStreamObjects<'js, Option>> { + drop(self.writer); + let writer = self.stream.writer_mut(); + WritableStreamObjects { + stream: self.stream, + controller: self.controller, + writer, + } + } +} + +impl<'js> WritableStreamObjects<'js, UndefinedWriter> { + pub(super) fn from_stream(stream: WritableStreamOwned<'js>) -> Self { + let controller = OwnedBorrowMut::from_class( + stream + .controller + .clone() + .expect("WritableStream must have controller"), + ); + + WritableStreamObjects { + stream, + controller, + writer: UndefinedWriter, + } + } + + pub(super) fn from_controller(controller: WritableStreamDefaultControllerOwned<'js>) -> Self { + let stream = OwnedBorrowMut::from_class(controller.stream.clone()); + + WritableStreamObjects { + stream, + controller, + writer: UndefinedWriter, + } + } +} + +impl<'js> WritableStreamObjects<'js, WritableStreamDefaultWriterOwned<'js>> { + pub(super) fn from_writer(writer: WritableStreamDefaultWriterOwned<'js>) -> Self { + let stream = OwnedBorrowMut::from_class( + writer + .stream + .clone() + .expect("WritableStreamDefaultWriter must have a stream"), + ); + + let controller = OwnedBorrowMut::from_class( + stream + .controller + .clone() + .expect("WritableStreamDefaultWriter stream must have a controller"), + ); + + WritableStreamObjects { + stream, + controller, + writer, + } + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/writable/stream/mod.rs b/stdlib/src/llrt/llrt_stream_web/writable/stream/mod.rs new file mode 100644 index 00000000..264c9cba --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/writable/stream/mod.rs @@ -0,0 +1,772 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use std::collections::VecDeque; + +use crate::llrt_abort::AbortController; +use crate::llrt_utils::{ + option::{Null, Undefined}, + primordials::{BasePrimordials, Primordial}, +}; +use rquickjs::{ + class::{OwnedBorrowMut, Trace, Tracer}, + function::Constructor, + prelude::{Opt, This}, + Class, Ctx, Exception, JsLifetime, Object, Promise, Result, Value, +}; + +use crate::llrt_stream_web::{ + queuing_strategy::QueuingStrategy, + utils::{ + promise::{ + promise_rejected_with_constructor, upon_promise, PromisePrimordials, ResolveablePromise, + }, + UnwrapOrUndefined, + }, + writable::{ + default_controller::{ + WritableStreamDefaultController, WritableStreamDefaultControllerClass, + }, + default_writer::{ + WritableStreamDefaultWriter, WritableStreamDefaultWriterClass, + WritableStreamDefaultWriterOwned, + }, + objects::WritableStreamObjects, + writer::WritableStreamWriter, + }, +}; +use sink::UnderlyingSink; + +pub(super) mod sink; + +#[rquickjs::class] +#[derive(JsLifetime)] +pub struct WritableStream<'js> { + pub(super) backpressure: bool, + close_request: Option>, + pub(crate) controller: Option>, + pub in_flight_write_request: Option>, + in_flight_close_request: Option>, + pending_abort_request: Option>, + pub(crate) state: WritableStreamState<'js>, + pub(crate) writer: Option>, + write_requests: VecDeque>, + pub(super) constructor_type_error: Constructor<'js>, + pub(crate) promise_primordials: PromisePrimordials<'js>, +} + +impl<'js> Trace<'js> for WritableStream<'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + self.close_request.trace(tracer); + self.controller.trace(tracer); + self.in_flight_write_request.trace(tracer); + self.in_flight_close_request.trace(tracer); + self.pending_abort_request.trace(tracer); + self.state.trace(tracer); + self.writer.trace(tracer); + self.write_requests.trace(tracer); + self.constructor_type_error.trace(tracer); + self.promise_primordials.trace(tracer); + } +} + +pub(crate) type WritableStreamClass<'js> = Class<'js, WritableStream<'js>>; +pub(crate) type WritableStreamOwned<'js> = OwnedBorrowMut<'js, WritableStream<'js>>; + +#[rquickjs::methods(rename_all = "camelCase")] +impl<'js> WritableStream<'js> { + // constructor(optional object underlyingSink, optional QueuingStrategy strategy = {}); + #[qjs(constructor)] + fn new( + ctx: Ctx<'js>, + underlying_sink: Opt>>, + queuing_strategy: Opt>>, + ) -> Result> { + // If underlyingSink is missing, set it to null. + let underlying_sink = Null(underlying_sink.0); + + // Let underlyingSinkDict be underlyingSink, converted to an IDL value of type UnderlyingSink. + let underlying_sink_dict = match underlying_sink { + Null(None) | Null(Some(Undefined(None))) => UnderlyingSink::default(), + Null(Some(Undefined(Some(ref obj)))) => UnderlyingSink::from_object(obj.clone())?, + }; + + // If underlyingSinkDict["type"] exists, throw a RangeError exception. + if underlying_sink_dict.r#type.is_some() { + return Err(Exception::throw_range(&ctx, "Invalid type is specified")); + } + + // Perform ! InitializeWritableStream(this). + let stream_class = Class::instance( + ctx.clone(), + Self { + // Set stream.[[state]] to "writable". + state: WritableStreamState::Writable, + // Set stream.[[storedError]], stream.[[writer]], stream.[[controller]], stream.[[inFlightWriteRequest]], stream.[[closeRequest]], stream.[[inFlightCloseRequest]], and stream.[[pendingAbortRequest]] to undefined. + writer: None, + controller: None, + in_flight_write_request: None, + close_request: None, + in_flight_close_request: None, + pending_abort_request: None, + // Set stream.[[writeRequests]] to a new empty list. + write_requests: VecDeque::new(), + // Set stream.[[backpressure]] to false. + backpressure: false, + constructor_type_error: BasePrimordials::get(&ctx)?.constructor_type_error.clone(), + promise_primordials: PromisePrimordials::get(&ctx)?.clone(), + }, + )?; + let stream = OwnedBorrowMut::from_class(stream_class.clone()); + let queuing_strategy = queuing_strategy.0.and_then(|qs| qs.0); + + // Let sizeAlgorithm be ! ExtractSizeAlgorithm(strategy). + let size_algorithm = QueuingStrategy::extract_size_algorithm(queuing_strategy.as_ref()); + + // Let highWaterMark be ? ExtractHighWaterMark(strategy, 1). + let high_water_mark = + QueuingStrategy::extract_high_water_mark(&ctx, queuing_strategy, 1.0)?; + + // Perform ? SetUpWritableStreamDefaultControllerFromUnderlyingSink(this, underlyingSink, underlyingSinkDict, highWaterMark, sizeAlgorithm). + WritableStreamDefaultController::set_up_writable_stream_default_controller_from_underlying_sink(ctx, stream, underlying_sink, underlying_sink_dict, high_water_mark, size_algorithm)?; + + Ok(stream_class) + } + + // readonly attribute boolean locked; + #[qjs(get)] + fn locked(&self) -> bool { + // Return ! IsWritableStreamLocked(this). + self.is_writable_stream_locked() + } + + fn abort( + ctx: Ctx<'js>, + stream: This>, + reason: Opt>, + ) -> Result> { + if stream.is_writable_stream_locked() { + // If ! IsWritableStreamLocked(this) is true, return a promise rejected with a TypeError exception. + return promise_rejected_with_constructor( + &stream.constructor_type_error, + &stream.promise_primordials, + "Cannot abort a stream that already has a writer", + ); + } + + let objects = WritableStreamObjects::from_stream(stream.0); + + // Return ! WritableStreamAbort(this, reason). + let (promise, _) = Self::writable_stream_abort(ctx.clone(), objects, reason.0)?; + + Ok(promise) + } + + fn close(ctx: Ctx<'js>, stream: This>) -> Result> { + if stream.is_writable_stream_locked() { + // If ! IsWritableStreamLocked(this) is true, return a promise rejected with a TypeError exception. + return promise_rejected_with_constructor( + &stream.constructor_type_error, + &stream.promise_primordials, + "Cannot close a stream that already has a writer", + ); + } + + if Self::writable_stream_close_queued_or_in_flight(&stream.0) { + // If ! WritableStreamCloseQueuedOrInFlight(this) is true, return a promise rejected with a TypeError exception. + return promise_rejected_with_constructor( + &stream.constructor_type_error, + &stream.promise_primordials, + "Cannot close an already-closing stream", + ); + } + + let objects = WritableStreamObjects::from_stream(stream.0); + + // Return ! WritableStreamClose(this). + let (promise, _) = Self::writable_stream_close(ctx.clone(), objects)?; + + Ok(promise) + } + + fn get_writer( + ctx: Ctx<'js>, + stream: This>, + ) -> Result> { + // Return ? AcquireWritableStreamDefaultWriter(this). + let (_, writer) = + WritableStreamDefaultWriter::acquire_writable_stream_default_writer(&ctx, stream.0)?; + + Ok(writer) + } +} + +impl<'js> WritableStream<'js> { + /// Create a WritableStream for use by TransformStream with properly traced algorithm variants + pub(crate) fn create_for_transform( + ctx: Ctx<'js>, + start_promise: Promise<'js>, + ts_stream: crate::llrt_stream_web::transform::stream::TransformStreamClass<'js>, + ts_controller: crate::llrt_stream_web::transform::controller::TransformStreamDefaultControllerClass<'js>, + high_water_mark: f64, + size_algorithm: crate::llrt_stream_web::queuing_strategy::SizeAlgorithm<'js>, + ) -> Result> { + let stream_class = Class::instance( + ctx.clone(), + Self { + state: WritableStreamState::Writable, + writer: None, + controller: None, + in_flight_write_request: None, + close_request: None, + in_flight_close_request: None, + pending_abort_request: None, + write_requests: VecDeque::new(), + backpressure: false, + constructor_type_error: BasePrimordials::get(&ctx)?.constructor_type_error.clone(), + promise_primordials: PromisePrimordials::get(&ctx)?.clone(), + }, + )?; + + let stream = OwnedBorrowMut::from_class(stream_class.clone()); + + WritableStreamDefaultController::set_up_writable_stream_default_controller( + ctx, + stream, + super::WritableStartAlgorithm::Transform(start_promise), + super::WritableWriteAlgorithm::Transform { + stream: ts_stream.clone(), + controller: ts_controller.clone(), + }, + super::WritableCloseAlgorithm::Transform { + stream: ts_stream, + controller: ts_controller.clone(), + }, + super::WritableAbortAlgorithm::Transform { + controller: ts_controller, + }, + high_water_mark, + size_algorithm, + )?; + + Ok(stream_class) + } + + pub(crate) fn is_writable_stream_locked(&self) -> bool { + if self.writer.is_none() { + // If stream.[[writer]] is undefined, return false. + false + } else { + // Return true. + true + } + } + + pub(crate) fn writable_stream_abort>( + ctx: Ctx<'js>, + mut objects: WritableStreamObjects<'js, W>, + mut reason: Option>, + ) -> Result<(Promise<'js>, WritableStreamObjects<'js, W>)> { + // If stream.[[state]] is "closed" or "errored", return a promise resolved with undefined. + if matches!( + objects.stream.state, + WritableStreamState::Closed | WritableStreamState::Errored(_) + ) { + return Ok(( + objects + .stream + .promise_primordials + .promise_resolved_with_undefined + .clone(), + objects, + )); + } + + // Signal abort on stream.[[controller]].[[abortController]] with reason. + { + // this executes user code, so we should ensure we hold no locks + let abort_controller = objects.controller.abort_controller.clone(); + let objects_class = objects.into_inner(); + AbortController::abort(ctx.clone(), This(abort_controller), Opt(reason.clone()))?; + objects = WritableStreamObjects::from_class(objects_class); + } + + // Let state be stream.[[state]]. + // If state is "closed" or "errored", return a promise resolved with undefined. + if matches!( + objects.stream.state, + WritableStreamState::Closed | WritableStreamState::Errored(_) + ) { + return Ok(( + objects + .stream + .promise_primordials + .promise_resolved_with_undefined + .clone(), + objects, + )); + } + + // If stream.[[pendingAbortRequest]] is not undefined, return stream.[[pendingAbortRequest]]'s promise. + match objects.stream.pending_abort_request { + None => {} + Some(ref pending_abort_request) => { + return Ok((pending_abort_request.promise.promise.clone(), objects)) + } + } + + let was_already_erroring = match objects.stream.state { + // If state is "erroring", + // Set wasAlreadyErroring to true. + // Set reason to undefined. + WritableStreamState::Erroring(_) => { + reason = None; + true + } + // Let wasAlreadyErroring be false. + _ => false, + }; + + // Let promise be a new promise. + let promise = ResolveablePromise::new(&ctx)?; + + let reason = reason.unwrap_or_undefined(&ctx); + + // Set stream.[[pendingAbortRequest]] to a new pending abort request whose promise is promise, reason is reason, and was already erroring is wasAlreadyErroring. + objects.stream.pending_abort_request = Some(PendingAbortRequest { + promise: promise.clone(), + reason: reason.clone(), + was_already_erroring, + }); + + // If wasAlreadyErroring is false, perform ! WritableStreamStartErroring(stream, reason). + if !was_already_erroring { + objects = Self::writable_stream_start_erroring(ctx, objects, reason)?; + } + + Ok((promise.promise.clone(), objects)) + } + + pub(super) fn writable_stream_close>( + ctx: Ctx<'js>, + mut objects: WritableStreamObjects<'js, W>, + ) -> Result<(Promise<'js>, WritableStreamObjects<'js, W>)> { + // Let state be stream.[[state]]. + // If state is "closed" or "errored", return a promise rejected with a TypeError exception. + if matches!( + objects.stream.state, + WritableStreamState::Closed | WritableStreamState::Errored(_) + ) { + return Ok(( + promise_rejected_with_constructor::( + &objects.stream.constructor_type_error, + &objects.stream.promise_primordials, + "The stream is not in the writable state and cannot be closed", + )?, + objects, + )); + } + + // Let promise be a new promise. + let promise = ResolveablePromise::new(&ctx)?; + // Set stream.[[closeRequest]] to promise. + objects.stream.close_request = Some(promise.clone()); + + // Let writer be stream.[[writer]]. + // If writer is not undefined, and stream.[[backpressure]] is true, and state is "writable", resolve writer.[[readyPromise]] with undefined. + objects = objects.with_writer( + |objects| { + if objects.stream.backpressure + && matches!(objects.stream.state, WritableStreamState::Writable) + { + let () = objects.writer.ready_promise.resolve_undefined()?; + } + Ok(objects) + }, + Ok, + )?; + + // Perform ! WritableStreamDefaultControllerClose(stream.[[controller]]). + objects = WritableStreamDefaultController::writable_stream_default_controller_close( + ctx, objects, + )?; + + // Return promise. + Ok((promise.promise.clone(), objects)) + } + + pub(super) fn writable_stream_start_erroring>( + ctx: Ctx<'js>, + // Let controller be stream.[[controller]]. + // Let writer be stream.[[writer]]. + mut objects: WritableStreamObjects<'js, W>, + reason: Value<'js>, + ) -> Result> { + // Set stream.[[state]] to "erroring". + // Set stream.[[storedError]] to reason. + objects.stream.state = WritableStreamState::Erroring(reason.clone()); + + // If writer is not undefined, perform ! WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason). + objects = objects.with_writer( + |mut objects| { + objects + .writer + .writable_stream_default_writer_ensure_ready_promise_rejected( + &objects.stream.promise_primordials, + reason.clone(), + )?; + Ok(objects) + }, + Ok, + )?; + + // If ! WritableStreamHasOperationMarkedInFlight(stream) is false and controller.[[started]] is true, perform ! WritableStreamFinishErroring(stream). + if !objects + .stream + .writable_stream_has_operation_marked_in_flight() + && objects.controller.started + { + objects = Self::writable_stream_finish_erroring(ctx, objects, reason)?; + } + + Ok(objects) + } + + pub(super) fn writable_stream_finish_erroring>( + ctx: Ctx<'js>, + mut objects: WritableStreamObjects<'js, W>, + // Let storedError be stream.[[storedError]]. + stored_error: Value<'js>, + ) -> Result> { + // Set stream.[[state]] to "errored". + objects.stream.state = WritableStreamState::Errored(stored_error.clone()); + + // Perform ! stream.[[controller]].[[ErrorSteps]](). + objects.controller.error_steps(); + + // For each writeRequest of stream.[[writeRequests]]: + for write_request in &mut objects.stream.write_requests { + let () = write_request.reject(stored_error.clone())?; + } + + // Set stream.[[writeRequests]] to an empty list. + objects.stream.write_requests.clear(); + + // Let abortRequest be stream.[[pendingAbortRequest]]. + // Set stream.[[pendingAbortRequest]] to undefined. + let abort_request = if let Some(pending_abort_request) = + objects.stream.pending_abort_request.take() + { + pending_abort_request + } else { + // If stream.[[pendingAbortRequest]] is undefined, + // Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + objects = + WritableStream::writable_stream_reject_close_and_closed_promise_if_needed(objects)?; + // Return. + return Ok(objects); + }; + + // If abortRequest’s was already erroring is true, + if abort_request.was_already_erroring { + // Reject abortRequest’s promise with storedError. + let () = abort_request.promise.reject(stored_error.clone())?; + + // Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + objects = + WritableStream::writable_stream_reject_close_and_closed_promise_if_needed(objects)?; + + // Return. + return Ok(objects); + } + + // Let promise be ! stream.[[controller]].[[AbortSteps]](abortRequest’s reason). + let (promise, objects) = + WritableStreamDefaultController::abort_steps(&ctx, objects, abort_request.reason)?; + + let objects_class = objects.into_inner(); + + // Upon fulfillment of promise, + let _ = upon_promise::, _>(ctx.clone(), promise, { + let objects_class = objects_class.clone(); + move |_, result| { + let objects = + WritableStreamObjects::from_class_no_writer(objects_class).refresh_writer(); + match result { + // Upon fulfillment of promise, + Ok(_) => { + // Resolve abortRequest’s promise with undefined. + let () = abort_request.promise.resolve_undefined()?; + // Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + WritableStream::writable_stream_reject_close_and_closed_promise_if_needed( + objects, + )?; + Ok(()) + } + // Upon rejection of promise with reason reason, + Err(reason) => { + // Reject abortRequest’s promise with reason. + let () = abort_request.promise.reject(reason)?; + // Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). + WritableStream::writable_stream_reject_close_and_closed_promise_if_needed( + objects, + )?; + Ok(()) + } + } + } + })?; + + Ok(WritableStreamObjects::from_class(objects_class)) + } + + fn writable_stream_reject_close_and_closed_promise_if_needed>( + // Let writer be stream.[[writer]]. + mut objects: WritableStreamObjects<'js, W>, + ) -> Result> { + // If stream.[[closeRequest]] is not undefined, + if let Some(ref close_request) = objects.stream.close_request { + // Reject stream.[[closeRequest]] with stream.[[storedError]]. + let () = close_request.reject(objects.stream.stored_error())?; + // Set stream.[[closeRequest]] to undefined. + objects.stream.close_request = None; + } + + // If writer is not undefined, + objects.with_writer( + |objects| { + // Reject writer.[[closedPromise]] with stream.[[storedError]]. + let () = objects + .writer + .closed_promise + .reject(objects.stream.stored_error())?; + + // Set writer.[[closedPromise]].[[PromiseIsHandled]] to true. + objects.writer.closed_promise.set_is_handled()?; + + Ok(objects) + }, + Ok, + ) + } + + pub(super) fn writable_stream_mark_first_write_request_in_flight(&mut self) { + // Let writeRequest be stream.[[writeRequests]][0]. + // Remove writeRequest from stream.[[writeRequests]]. + let write_request = self.write_requests.pop_front().expect("writable_stream_mark_first_write_request_in_flight must be called with non-empty write requests"); + // Set stream.[[inFlightWriteRequest]] to writeRequest. + self.in_flight_write_request = Some(write_request); + } + + pub(super) fn writable_stream_mark_close_request_in_flight(&mut self) { + // Set stream.[[inFlightCloseRequest]] to stream.[[closeRequest]]. + // Set stream.[[closeRequest]] to undefined. + self.in_flight_close_request = + Some(self.close_request.take().expect( + "writable_stream_mark_close_request_in_flight called without close request", + )) + } + + fn writable_stream_has_operation_marked_in_flight(&self) -> bool { + if self.in_flight_write_request.is_none() && self.in_flight_close_request.is_none() { + // If stream.[[inFlightWriteRequest]] is undefined and stream.[[inFlightCloseRequest]] is undefined, return false. + false + } else { + // Return true. + true + } + } + + pub(crate) fn writable_stream_close_queued_or_in_flight(&self) -> bool { + if self.close_request.is_none() && self.in_flight_close_request.is_none() { + // If stream.[[closeRequest]] is undefined and stream.[[inFlightCloseRequest]] is undefined, return false. + false + } else { + // Return true. + true + } + } + + pub(super) fn writable_stream_add_write_request( + &mut self, + ctx: &Ctx<'js>, + ) -> Result> { + // Let promise be a new promise. + let promise = ResolveablePromise::new(ctx)?; + // Append promise to stream.[[writeRequests]]. + self.write_requests.push_back(promise.clone()); + Ok(promise.promise.clone()) + } + + pub(super) fn writable_stream_finish_in_flight_write_with_error< + W: WritableStreamWriter<'js>, + >( + ctx: Ctx<'js>, + mut objects: WritableStreamObjects<'js, W>, + error: Value<'js>, + ) -> Result<()> { + // Reject stream.[[inFlightWriteRequest]] with error. + // Set stream.[[inFlightWriteRequest]] to undefined. + objects.stream.in_flight_write_request.take().expect("writable_stream_finish_in_flight_write_with_error called without in flight write request").reject(error.clone())?; + + // Perform ! WritableStreamDealWithRejection(stream, error). + Self::writable_stream_deal_with_rejection(ctx, objects, error)?; + + Ok(()) + } + + pub(super) fn writable_stream_finish_in_flight_close_with_error< + W: WritableStreamWriter<'js>, + >( + ctx: Ctx<'js>, + mut objects: WritableStreamObjects<'js, W>, + error: Value<'js>, + ) -> Result<()> { + // Reject stream.[[inFlightCloseRequest]] with error. + // Set stream.[[inFlightCloseRequest]] to undefined. + objects.stream.in_flight_close_request.take().expect("writable_stream_finish_in_flight_close_with_error called without in flight close request").reject(error.clone())?; + + // Assert: stream.[[state]] is "writable" or "erroring". + + // If stream.[[pendingAbortRequest]] is not undefined, + if let Some(pending_abort_request) = objects.stream.pending_abort_request.take() { + // Reject stream.[[pendingAbortRequest]]'s promise with error. + // Set stream.[[pendingAbortRequest]] to undefined. + pending_abort_request.promise.reject(error.clone())? + } + + // Perform ! WritableStreamDealWithRejection(stream, error). + Self::writable_stream_deal_with_rejection(ctx, objects, error)?; + + Ok(()) + } + + pub(super) fn writable_stream_deal_with_rejection>( + ctx: Ctx<'js>, + objects: WritableStreamObjects<'js, W>, + error: Value<'js>, + ) -> Result> { + // Let state be stream.[[state]]. + match &objects.stream.state { + // If state is "writable", + WritableStreamState::Writable => { + // Perform ! WritableStreamStartErroring(stream, error). + Self::writable_stream_start_erroring(ctx, objects, error) + }, + WritableStreamState::Erroring(ref stored_error) => { + let stored_error = stored_error.clone(); + // Perform ! WritableStreamFinishErroring(stream). + Self::writable_stream_finish_erroring(ctx, objects, stored_error) + }, + other => panic!("WritableStreamDealWithRejection must be called in state 'writable' or 'erroring', found {other:?}"), + } + } + + pub(super) fn writable_stream_finish_in_flight_write(&mut self) -> Result<()> { + // Resolve stream.[[inFlightWriteRequest]] with undefined. + // Set stream.[[inFlightWriteRequest]] to undefined. + self.in_flight_write_request + .take() + .expect("writable_stream_finish_in_flight_write called without in flight write request") + .resolve_undefined() + } + + pub(super) fn writable_stream_finish_in_flight_close>( + // Let writer be stream.[[writer]]. + mut objects: WritableStreamObjects<'js, W>, + ) -> Result> { + // Assert: stream.[[inFlightCloseRequest]] is not undefined. + + // Resolve stream.[[inFlightCloseRequest]] with undefined. + // Set stream.[[inFlightCloseRequest]] to undefined. + objects + .stream + .in_flight_close_request + .take() + .expect("writable_stream_finish_in_flight_close called without in flight close request") + .resolve_undefined()?; + + // Let state be stream.[[state]]. + // If state is "erroring", + if let WritableStreamState::Erroring(_) = objects.stream.state { + // Set stream.[[storedError]] to undefined. + // (implicitly covered by change to Closed below) + + // If stream.[[pendingAbortRequest]] is not undefined, + if let Some(pending_abort_request) = objects.stream.pending_abort_request.take() { + // Resolve stream.[[pendingAbortRequest]]'s promise with undefined. + // Set stream.[[pendingAbortRequest]] to undefined. + pending_abort_request.promise.resolve_undefined()?; + } + } + + // Set stream.[[state]] to "closed". + objects.stream.state = WritableStreamState::Closed; + + // If writer is not undefined, resolve writer.[[closedPromise]] with undefined. + objects.with_writer( + |objects| { + objects.writer.closed_promise.resolve_undefined()?; + + Ok(objects) + }, + Ok, + ) + } + + pub(super) fn writable_stream_update_backpressure>( + ctx: Ctx<'js>, + // Let writer be stream.[[writer]]. + mut objects: WritableStreamObjects<'js, W>, + backpressure: bool, + ) -> Result> { + // If writer is not undefined and backpressure is not stream.[[backpressure]], + objects = objects.with_writer( + |mut objects| { + if backpressure != objects.stream.backpressure { + if backpressure { + // If backpressure is true, set writer.[[readyPromise]] to a new promise. + objects.writer.ready_promise = ResolveablePromise::new(&ctx)?; + } else { + // Otherwise, + // Resolve writer.[[readyPromise]] with undefined. + objects.writer.ready_promise.resolve_undefined()? + } + } + + Ok(objects) + }, + Ok, + )?; + + // Set stream.[[backpressure]] to backpressure. + objects.stream.backpressure = backpressure; + + Ok(objects) + } + + pub(super) fn writer_mut(&mut self) -> Option> { + self.writer.clone().map(OwnedBorrowMut::from_class) + } + + pub(crate) fn stored_error(&self) -> Option> { + match self.state { + WritableStreamState::Erroring(ref stored_error) + | WritableStreamState::Errored(ref stored_error) => Some(stored_error.clone()), + _ => None, + } + } +} + +#[derive(Debug, Trace, Clone, JsLifetime)] +pub(crate) enum WritableStreamState<'js> { + Writable, + Closed, + Erroring(Value<'js>), + Errored(Value<'js>), +} + +#[derive(JsLifetime, Trace)] +struct PendingAbortRequest<'js> { + promise: ResolveablePromise<'js>, + reason: Value<'js>, + was_already_erroring: bool, +} diff --git a/stdlib/src/llrt/llrt_stream_web/writable/stream/sink.rs b/stdlib/src/llrt/llrt_stream_web/writable/stream/sink.rs new file mode 100644 index 00000000..a690cecc --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/writable/stream/sink.rs @@ -0,0 +1,35 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use rquickjs::{Function, Object, Result, Value}; + +use crate::llrt_stream_web::utils::ValueOrUndefined; + +#[derive(Default)] +pub struct UnderlyingSink<'js> { + // callback UnderlyingSinkStartCallback = any (WritableStreamDefaultController controller); + pub start: Option>, + // callback UnderlyingSinkWriteCallback = Promise (any chunk, WritableStreamDefaultController controller); + pub write: Option>, + // callback UnderlyingSinkCloseCallback = Promise (); + pub close: Option>, + // callback UnderlyingSinkAbortCallback = Promise (optional any reason); + pub abort: Option>, + pub r#type: Option>, +} + +impl<'js> UnderlyingSink<'js> { + pub fn from_object(obj: Object<'js>) -> Result { + let start = obj.get_value_or_undefined::<_, _>("start")?; + let write = obj.get_value_or_undefined::<_, _>("write")?; + let close = obj.get_value_or_undefined::<_, _>("close")?; + let abort = obj.get_value_or_undefined::<_, _>("abort")?; + let r#type = obj.get_value_or_undefined::<_, _>("type")?; + + Ok(Self { + start, + write, + close, + abort, + r#type, + }) + } +} diff --git a/stdlib/src/llrt/llrt_stream_web/writable/writer.rs b/stdlib/src/llrt/llrt_stream_web/writable/writer.rs new file mode 100644 index 00000000..b0d6ac87 --- /dev/null +++ b/stdlib/src/llrt/llrt_stream_web/writable/writer.rs @@ -0,0 +1,79 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use rquickjs::{class::Trace, Result}; + +use crate::llrt_stream_web::writable::default_writer::WritableStreamDefaultWriterOwned; + +pub(crate) trait WritableStreamWriter<'js>: Sized + 'js { + type Class: Clone + Trace<'js>; + + fn with_writer( + self, + ctx: C, + default: impl FnOnce( + C, + WritableStreamDefaultWriterOwned<'js>, + ) -> Result<(C, WritableStreamDefaultWriterOwned<'js>)>, + none: impl FnOnce(C) -> Result, + ) -> Result<(C, Self)>; + + fn into_inner(self) -> Self::Class; + + fn from_class(class: Self::Class) -> Self; +} + +#[derive(Clone, Trace)] +pub(super) struct UndefinedWriter; + +impl<'js> WritableStreamWriter<'js> for UndefinedWriter { + type Class = UndefinedWriter; + + fn with_writer( + self, + ctx: C, + _: impl FnOnce( + C, + WritableStreamDefaultWriterOwned<'js>, + ) -> Result<(C, WritableStreamDefaultWriterOwned<'js>)>, + none: impl FnOnce(C) -> Result, + ) -> Result<(C, Self)> { + Ok((none(ctx)?, self)) + } + + fn into_inner(self) -> Self::Class { + self + } + + fn from_class(class: Self::Class) -> Self { + class + } +} + +impl<'js, T: WritableStreamWriter<'js>> WritableStreamWriter<'js> for Option { + type Class = Option<>::Class>; + + fn with_writer( + self, + mut ctx: C, + default: impl FnOnce( + C, + WritableStreamDefaultWriterOwned<'js>, + ) -> Result<(C, WritableStreamDefaultWriterOwned<'js>)>, + none: impl FnOnce(C) -> Result, + ) -> Result<(C, Self)> { + match self { + Some(mut writer) => { + (ctx, writer) = writer.with_writer(ctx, default, none)?; + Ok((ctx, Some(writer))) + } + None => Ok((none(ctx)?, None)), + } + } + + fn into_inner(self) -> Self::Class { + self.map(WritableStreamWriter::into_inner) + } + + fn from_class(class: Self::Class) -> Self { + class.map(WritableStreamWriter::from_class) + } +} diff --git a/stdlib/src/llrt/llrt_test/lib.rs b/stdlib/src/llrt/llrt_test/lib.rs new file mode 100644 index 00000000..5c201c79 --- /dev/null +++ b/stdlib/src/llrt/llrt_test/lib.rs @@ -0,0 +1,149 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use std::{ + fs, + path::{Path, PathBuf}, +}; + +use rquickjs::{ + function::IntoArgs, + loader::{BuiltinLoader, ImportAttributes, Resolver}, + markers::ParallelSend, + module::{Evaluated, ModuleDef}, + promise::MaybePromise, + AsyncContext, AsyncRuntime, CatchResultExt, CaughtError, Ctx, FromJs, Function, Module, Result, +}; + +pub async fn given_file(content: &str) -> PathBuf { + let tmp_dir = std::env::temp_dir(); + let path = tmp_dir.join(uuid::Uuid::new_v4().to_string()); + tokio::fs::write(&path, content).await.unwrap(); + path +} + +struct TestResolver; + +impl Resolver for TestResolver { + fn resolve( + &mut self, + _ctx: &Ctx<'_>, + base: &str, + name: &str, + _attributes: Option>, + ) -> Result { + if !name.starts_with(".") { + return Ok(name.into()); + } + let base = Path::new(base); + let combined_path = base.join(name); + Ok(fs::canonicalize(combined_path) + .unwrap() + .to_string_lossy() + .to_string()) + } +} + +pub async fn given_runtime() -> (AsyncRuntime, AsyncContext) { + let rt = AsyncRuntime::new().unwrap(); + rt.set_loader((TestResolver,), (BuiltinLoader::default(),)) + .await; + let ctx = AsyncContext::full(&rt).await.unwrap(); + + (rt, ctx) +} + +pub async fn test_async_with(func: F) +where + F: for<'js> FnOnce(Ctx<'js>) -> std::pin::Pin + 'js>> + + Send, +{ + test_async_with_opts(func, TestOptions::default()).await; +} + +#[derive(Default)] +pub struct TestOptions { + no_pending_jobs: bool, +} + +impl TestOptions { + pub fn new() -> Self { + Self::default() + } + + pub fn no_pending_jobs(mut self) -> Self { + self.no_pending_jobs = true; + self + } +} + +pub async fn test_async_with_opts(func: F, options: TestOptions) +where + F: for<'js> FnOnce(Ctx<'js>) -> std::pin::Pin + 'js>> + + Send, +{ + let (rt, ctx) = given_runtime().await; + + ctx.async_with(async |ctx| func(ctx).await).await; + + if options.no_pending_jobs { + assert!(!rt.is_job_pending().await); + } +} + +pub async fn test_sync_with(func: F) +where + F: for<'js> FnOnce(Ctx<'js>) -> Result<()> + ParallelSend, +{ + let (_rt, ctx) = given_runtime().await; + + ctx.with(|ctx| func(ctx.clone()).catch(&ctx).unwrap()).await; +} + +pub async fn call_test<'js, T, A>(ctx: &Ctx<'js>, module: &Module<'js, Evaluated>, args: A) -> T +where + T: FromJs<'js>, + A: IntoArgs<'js>, +{ + call_test_err(ctx, module, args).await.unwrap() +} + +pub async fn call_test_err<'js, T, A>( + ctx: &Ctx<'js>, + module: &Module<'js, Evaluated>, + args: A, +) -> std::result::Result> +where + T: FromJs<'js>, + A: IntoArgs<'js>, +{ + module + .get::<_, Function>("test") + .catch(ctx)? + .call::<_, MaybePromise>(args) + .catch(ctx)? + .into_future::() + .await + .catch(ctx) +} + +pub struct ModuleEvaluator; + +impl ModuleEvaluator { + pub async fn eval_js<'js>( + ctx: Ctx<'js>, + name: &str, + source: &str, + ) -> Result> { + let (module, module_eval) = Module::declare(ctx, name, source)?.eval()?; + module_eval.into_future::<()>().await?; + Ok(module) + } + + pub async fn eval_rust<'js, M>(ctx: Ctx<'js>, name: &str) -> Result> + where + M: ModuleDef, + { + let (module, module_eval) = Module::evaluate_def::(ctx, name)?; + module_eval.into_future::<()>().await?; + Ok(module) + } +} diff --git a/stdlib/src/llrt/llrt_timers/lib.rs b/stdlib/src/llrt/llrt_timers/lib.rs new file mode 100644 index 00000000..b2d23560 --- /dev/null +++ b/stdlib/src/llrt/llrt_timers/lib.rs @@ -0,0 +1,557 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::{ + pin::{pin, Pin}, + ptr::NonNull, + rc::Rc, + sync::{ + atomic::{AtomicUsize, Ordering}, + Mutex, MutexGuard, + }, + time::Duration, +}; + +use crate::llrt_context::CtxExtension; +pub use crate::llrt_hooking::{invoke_async_hook, register_finalization_registry, HookType}; +use crate::llrt_utils::{ + module::{export_default, ModuleInfo}, + provider::ProviderType, +}; +use once_cell::sync::Lazy; +use rquickjs::{ + module::{Declarations, Exports, ModuleDef}, + prelude::{Func, Opt}, + qjs, Ctx, Exception, Function, Persistent, Result, Value, +}; +use tokio::{ + select, + sync::Notify, + time::{Instant, Sleep}, +}; + +static TIMER_ID: AtomicUsize = AtomicUsize::new(0); +static RT_TIMER_STATE: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); + +pub struct RuntimeTimerState { + timers: Vec, + rt: *mut qjs::JSRuntime, + running: bool, + deadline: Instant, + notify: Rc, +} +impl RuntimeTimerState { + fn new(rt: *mut qjs::JSRuntime) -> Self { + let deadline = Instant::now() + Duration::from_secs(86400 * 365 * 30); + Self { + timers: Default::default(), + rt, + deadline, + running: false, + notify: Default::default(), + } + } +} + +unsafe impl Send for RuntimeTimerState {} + +#[derive(Clone)] +pub struct Timeout { + callback: Option>>, + deadline: Instant, + raw_ctx: NonNull, + id: usize, + repeating: bool, + interval: u64, +} + +impl Default for Timeout { + fn default() -> Self { + Self { + callback: None, + deadline: Instant::now(), + raw_ctx: NonNull::dangling(), + id: 0, + repeating: false, + interval: 0, + } + } +} + +fn queue_microtask<'js>(_ctx: Ctx<'js>, cb: Function<'js>) -> Result<()> { + // SAFETY: Since it checks in advance whether it is an Function type, we can always get a pointer to the Function. + let uid = unsafe { qjs::JS_VALUE_GET_PTR(cb.as_raw()) } as usize; + register_finalization_registry(&_ctx, cb.clone().into_value(), uid)?; + invoke_async_hook(&_ctx, HookType::Init, ProviderType::Microtask, uid)?; + // NOTE: Defer simply registers a task in a microtask queue + // and is separate from the timing of when the actual callback runs. + // Therefore, asynchronous before/after hooks are not meaningful and will not be implemented. + + cb.defer::<()>(())?; + Ok(()) +} + +pub fn set_timeout_interval<'js>( + ctx: &Ctx<'js>, + cb: Function<'js>, + delay: u64, + provider_type: ProviderType, +) -> Result { + // SAFETY: Since it checks in advance whether it is an Function type, we can always get a pointer to the Function. + let uid = unsafe { qjs::JS_VALUE_GET_PTR(cb.as_raw()) } as usize; + + // NOTE: https://noncodersuccess.medium.com/understanding-setimmediate-vs-settimeout-in-node-js-6a3ef8fc02d4 + // If `setImmediate(fn)` and `setTimeout(fn, 0) are queued at the exact same time, + // `setImmediate(fn) takes precedence in Node.js, regardless of their execution order. + // This is due to the specifications of the Node.js event loop. + // The event loop specifications of LLRT are completely different from those of Node.js, + // but to make them the same, `setImmedaite()` is executed before any delay setting of `setTimeout()`. + let (repeating, deadline) = match provider_type { + ProviderType::Immediate => (false, Instant::now() - Duration::from_secs(600)), // before any setTimeout(fn, delay) + ProviderType::Timeout => (false, Instant::now() + Duration::from_millis(delay)), + ProviderType::Interval => (true, Instant::now() + Duration::from_millis(delay)), + _ => { + return Err(Exception::throw_type( + ctx, + "The specified provider type is not supported.", + )) + } + }; + + register_finalization_registry(ctx, cb.clone().into_value(), uid)?; + invoke_async_hook(ctx, HookType::Init, provider_type, uid)?; + + let id = TIMER_ID.fetch_add(1, Ordering::Relaxed); + + let callback = Persistent::::save(ctx, cb); + + let timeout = Timeout { + deadline, + callback: Some(callback), + raw_ctx: ctx.as_raw(), + id, + repeating, + interval: delay, + }; + + let rt_ptr = unsafe { qjs::JS_GetRuntime(ctx.as_raw().as_ptr()) }; + + let mut rt_timer = RT_TIMER_STATE.lock().unwrap(); + let state = get_timer_state(&mut rt_timer, rt_ptr); + state.timers.push(timeout); + let task_running = state.running; + if task_running { + if deadline < state.deadline { + state.deadline = deadline; + state.notify.notify_one(); + } + } else { + state.running = true; + let timer_abort = state.notify.clone(); + drop(rt_timer); + create_spawn_loop(rt_ptr, ctx, timer_abort, deadline)?; + } + + Ok(id) +} + +fn get_timer_state<'a>( + state_ref: &'a mut MutexGuard>, + rt: *mut qjs::JSRuntime, +) -> &'a mut RuntimeTimerState { + let rt_timers = state_ref.iter_mut().find(|state| state.rt == rt); + + //save a branch + unsafe { rt_timers.unwrap_unchecked() } +} + +fn clear_timeout_interval(ctx: Ctx<'_>, id: Opt) -> Result<()> { + if let Some(id) = id.0.and_then(|v| v.as_number()) { + let id = id as usize; + let rt = unsafe { qjs::JS_GetRuntime(ctx.as_raw().as_ptr()) }; + let mut rt_timers = RT_TIMER_STATE.lock().unwrap(); + + let state = get_timer_state(&mut rt_timers, rt); + if let Some(timeout) = state.timers.iter_mut().find(|t| t.id == id) { + let _ = timeout.callback.take(); + timeout.repeating = false; + timeout.deadline = Instant::now() - Duration::from_secs(1); + state.notify.notify_one() + } + } + + Ok(()) +} + +pub struct TimersModule; + +impl ModuleDef for TimersModule { + fn declare(declare: &Declarations) -> Result<()> { + declare.declare("setTimeout")?; + declare.declare("clearTimeout")?; + declare.declare("setInterval")?; + declare.declare("setImmediate")?; + declare.declare("clearInterval")?; + declare.declare("queueMicrotask")?; + declare.declare("default")?; + Ok(()) + } + + fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { + let globals = ctx.globals(); + + export_default(ctx, exports, |default| { + let functions = [ + "setTimeout", + "clearTimeout", + "setInterval", + "clearInterval", + "setImmediate", + "queueMicrotask", + ]; + for func_name in functions { + let function: Function = globals.get(func_name)?; + default.set(func_name, function)?; + } + Ok(()) + })?; + + Ok(()) + } +} + +impl From for ModuleInfo { + fn from(val: TimersModule) -> Self { + ModuleInfo { + name: "timers", + module: val, + } + } +} + +pub fn init(ctx: &Ctx<'_>) -> Result<()> { + let rt_ptr = unsafe { qjs::JS_GetRuntime(ctx.as_raw().as_ptr()) }; + + let mut rt_timers = RT_TIMER_STATE.lock().unwrap(); + rt_timers.push(RuntimeTimerState::new(rt_ptr)); + + let globals = ctx.globals(); + + globals.set( + "setTimeout", + Func::from(move |ctx, cb, delay: Opt| { + let delay = delay.unwrap_or(0.).max(0.) as u64; + set_timeout_interval(&ctx, cb, delay, ProviderType::Timeout) + }), + )?; + + globals.set( + "setInterval", + Func::from(move |ctx, cb, delay: Opt| { + let delay = delay.unwrap_or(0.).max(0.) as u64; + set_timeout_interval(&ctx, cb, delay, ProviderType::Interval) + }), + )?; + + globals.set("clearTimeout", Func::from(clear_timeout_interval))?; + + globals.set("clearInterval", Func::from(clear_timeout_interval))?; + + globals.set( + "setImmediate", + Func::from(move |ctx, cb| set_timeout_interval(&ctx, cb, 0, ProviderType::Immediate)), + )?; + + globals.set("queueMicrotask", Func::from(queue_microtask))?; + + Ok(()) +} + +#[inline(always)] +fn create_spawn_loop( + rt: *mut qjs::JSRuntime, + ctx: &Ctx<'_>, + timer_abort: Rc, + deadline: Instant, +) -> Result<()> { + ctx.spawn_exit_simple(async move { + let mut sleep = pin!(tokio::time::sleep_until(deadline)); + + let mut executing_timers: Vec> = Default::default(); + + loop { + select! { + _ = timer_abort.notified() => {} + _ = sleep.as_mut() => {} + } + + if !poll_timers(rt, &mut executing_timers, Some(&mut sleep), None)? { + break; + } + } + Ok(()) + }); + + Ok(()) +} + +pub struct ExecutingTimer( + Instant, + NonNull, + Persistent>, +); + +unsafe impl Send for ExecutingTimer {} + +pub fn poll_timers( + rt: *mut qjs::JSRuntime, + call_vec: &mut Vec>, + sleep: Option<&mut Pin<&mut Sleep>>, + deadline: Option<&mut Instant>, +) -> Result { + static MIN_SLEEP: Duration = Duration::from_millis(4); + static FAR_FUTURE: Duration = Duration::from_secs(84200 * 365 * 30); + + let mut rt_timers = RT_TIMER_STATE.lock().unwrap(); + let state = get_timer_state(&mut rt_timers, rt); + let now = Instant::now(); + + let mut had_items = false; + let mut lowest = now + FAR_FUTURE; + state.timers.retain_mut(|timeout| { + had_items = true; + if timeout.deadline < now { + let ctx = timeout.raw_ctx; + if let Some(cb) = timeout.callback.take() { + if !timeout.repeating { + call_vec.push(Some(ExecutingTimer(timeout.deadline, ctx, cb))); + return false; + } + timeout.deadline = now + Duration::from_millis(timeout.interval); + if timeout.deadline < lowest { + lowest = timeout.deadline; + } + call_vec.push(Some(ExecutingTimer(timeout.deadline, ctx, cb.clone()))); + timeout.callback.replace(cb); + } else { + return false; + } + } else if timeout.deadline < lowest { + lowest = timeout.deadline; + } + true + }); + + let has_items = !state.timers.is_empty(); + + if had_items { + if lowest - now < MIN_SLEEP { + lowest = now + MIN_SLEEP; + } + if let Some(sleep) = sleep { + sleep.as_mut().reset(lowest); + } + if let Some(deadline) = deadline { + *deadline = lowest; + } + state.deadline = lowest; + } + + drop(rt_timers); + + call_vec.sort_unstable_by_key(|v| v.as_ref().map(|v| v.0)); + + let mut is_first_time = true; + for item in call_vec.iter_mut() { + if let Some(ExecutingTimer(_, ctx, timeout)) = item.take() { + let ctx2 = unsafe { Ctx::from_raw(ctx) }; + + if is_first_time { + while ctx2.execute_pending_job() {} + is_first_time = false; + } + + if let Ok(timeout) = timeout.restore(&ctx2) { + // SAFETY: Since it checks in advance whether it is an Function type, we can always get a pointer to the Function. + let uid: usize = unsafe { qjs::JS_VALUE_GET_PTR(timeout.as_raw()) } as usize; + + invoke_async_hook(&ctx2, HookType::Before, ProviderType::None, uid)?; + + timeout.call::<_, ()>(())?; + + invoke_async_hook(&ctx2, HookType::After, ProviderType::None, uid)?; + } + + while ctx2.execute_pending_job() {} + } + } + call_vec.clear(); + + if !has_items { + let mut rt_timers = RT_TIMER_STATE.lock().unwrap(); + let state = get_timer_state(&mut rt_timers, rt); + let is_empty = state.timers.is_empty(); + state.running = !is_empty; + + return Ok(!is_empty); + } + Ok(true) +} + +#[cfg(test)] +mod tests { + use crate::llrt_test::{call_test, test_async_with, ModuleEvaluator}; + + use super::*; + + #[tokio::test] + async fn test_timers() { + test_async_with(|ctx| { + Box::pin(async move { + init(&ctx).unwrap(); + + // Assume we have a TimersModule that provides setTimeout, setImmediate, and setInterval + ModuleEvaluator::eval_rust::(ctx.clone(), "timers") + .await + .unwrap(); + + // Test setTimeout + let module = ModuleEvaluator::eval_js( + ctx.clone(), + "test_setTimeout", + r#" + import { setTimeout } from 'timers'; + export async function test() { + return new Promise((resolve) => { + setTimeout(() => resolve('timeout'), 100); + }); + } + "#, + ) + .await + .unwrap(); + let result = call_test::(&ctx, &module, ()).await; + assert_eq!(result, "timeout"); + + // Test setImmediate + let module = ModuleEvaluator::eval_js( + ctx.clone(), + "test_setImmediate", + r#" + import { setImmediate } from 'timers'; + export async function test() { + return new Promise((resolve) => { + setImmediate(() => resolve('immediate')); + }); + } + "#, + ) + .await + .unwrap(); + let result = call_test::(&ctx, &module, ()).await; + assert_eq!(result, "immediate"); + + // Test setInterval + let module = ModuleEvaluator::eval_js( + ctx.clone(), + "test_setInterval", + r#" + import { setInterval, clearInterval } from 'timers'; + export async function test() { + return new Promise((resolve) => { + let count = 0; + const intervalId = setInterval(() => { + count++; + if (count === 3) { + clearInterval(intervalId); + resolve(count); + } + }, 10); + }); + } + "#, + ) + .await + .unwrap(); + let result = call_test::(&ctx, &module, ()).await; + assert_eq!(result, 3); + + // Test nested timers + let module = ModuleEvaluator::eval_js( + ctx.clone(), + "test_nestedTimers", + r#" + import { setTimeout, setImmediate } from 'timers'; + export async function test() { + return new Promise((resolve) => { + setTimeout(() => { + setImmediate(() => { + setTimeout(() => { + resolve('nested'); + }, 10); + }); + }, 10); + }); + } + "#, + ) + .await + .unwrap(); + let result = call_test::(&ctx, &module, ()).await; + assert_eq!(result, "nested"); + + // Test canceling timeout + let module = ModuleEvaluator::eval_js( + ctx.clone(), + "test_cancelTimeout", + r#" + import { setTimeout, clearTimeout } from 'timers'; + export async function test() { + return new Promise((resolve) => { + const timeoutId = setTimeout(() => { + resolve('should not happen'); + }, 10); + clearTimeout(timeoutId); + setTimeout(() => resolve('canceled'), 20); + }); + } + "#, + ) + .await + .unwrap(); + let result = call_test::(&ctx, &module, ()).await; + assert_eq!(result, "canceled"); + + // Test multiple intervals + let module = ModuleEvaluator::eval_js( + ctx.clone(), + "test_multipleIntervals", + r#" + import { setInterval, clearInterval } from 'timers'; + export async function test() { + return new Promise((resolve) => { + let count1 = 0, count2 = 0; + const id1 = setInterval(() => { + count1++; + if (count1 === 2) clearInterval(id1); + }, 10); + const id2 = setInterval(() => { + count2++; + if (count2 === 3) { + clearInterval(id2); + resolve([count1, count2]); + } + }, 20); + }); + } + "#, + ) + .await + .unwrap(); + let result = call_test::, _>(&ctx, &module, ()).await; + assert_eq!(result, vec![2, 3]); + }) + }) + .await; + } +} diff --git a/stdlib/src/llrt/llrt_url/lib.rs b/stdlib/src/llrt/llrt_url/lib.rs new file mode 100644 index 00000000..36f4a094 --- /dev/null +++ b/stdlib/src/llrt/llrt_url/lib.rs @@ -0,0 +1,356 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#![allow(clippy::inherent_to_string)] +pub mod url_class; +pub mod url_search_params; + +use std::{path::PathBuf, str::FromStr}; + +use crate::llrt_utils::{ + module::{export_default, ModuleInfo}, + primordials::{BasePrimordials, Primordial}, + result::ResultExt, +}; +use rquickjs::{ + function::{Constructor, Func}, + module::{Declarations, Exports, ModuleDef}, + prelude::Opt, + Class, Coerced, Ctx, Exception, Result, Value, +}; +use url::{quirks, Url}; + +use self::url_class::{url_to_http_options, URL}; +use self::url_search_params::URLSearchParams; + +/// Returns whether the given scheme is a [special scheme](https://url.spec.whatwg.org/#special-scheme). +pub fn is_special_scheme(scheme: &str) -> bool { + matches!(scheme, "http" | "https" | "ftp" | "ws" | "wss" | "file") +} + +pub fn domain_to_unicode(domain: &str) -> String { + quirks::domain_to_unicode(domain) +} + +pub fn domain_to_ascii(domain: &str) -> String { + quirks::domain_to_ascii(domain) +} + +//options are ignored, no windows support yet +pub fn path_to_file_url<'js>(ctx: Ctx<'js>, path: String, _: Opt) -> Result> { + let url = Url::from_file_path(&path) + .map_err(|_| Exception::throw_type(&ctx, &["Path is not absolute: ", &path].concat()))?; + + URL::from_url(ctx, url) +} + +//options are ignored, no windows support yet +pub fn file_url_to_path<'js>(ctx: Ctx<'js>, url: Value<'js>) -> Result { + let url_string = if let Ok(url) = Class::::from_value(&url) { + url.borrow().to_string() + } else { + url.get::>()?.to_string() + }; + + let path = url_string.trim_start_matches("file://"); + + Ok(PathBuf::from_str(path) + .or_throw(&ctx)? + .to_string_lossy() + .to_string()) +} + +pub fn url_format<'js>(url: Class<'js, URL<'js>>, options: Opt>) -> Result { + let url = url.borrow(); + let mut string = url.protocol(); + string.push_str("//"); + + let mut include_fragment = true; + let mut unicode_encode = false; + let mut include_auth = true; + let mut include_search = true; + + // Parse options if provided + if let Some(options) = options.into_inner() { + if let Some(options) = options.as_object() { + if let Ok(value) = options.get("unicode") { + unicode_encode = value; + } + if let Ok(value) = options.get("auth") { + include_auth = value; + } + if let Ok(value) = options.get("fragment") { + include_fragment = value; + } + if let Ok(value) = options.get("search") { + include_search = value + } + } + } + + if include_auth { + let username = url.username(); + let password = url.password(); + if !username.is_empty() { + string.push_str(&username); + if !password.is_empty() { + string.push(':'); + string.push_str(&password); + } + string.push('@'); + } + } + + if unicode_encode { + string.push_str(&domain_to_unicode(&url.host())); + } else { + string.push_str(&url.host()); + } + + string.push_str(&url.pathname()); + + if include_search { + string.push_str(&url.search()); + } + + if include_fragment { + string.push_str(&url.hash()); + } + + Ok(string) +} + +/// Encode trailing space as `%20` in opaque paths before a setter runs. +/// +/// Used by [`URLSearchParams`] which mutates the shared [`Url`] directly. +pub fn convert_trailing_space(url: &mut Url) { + if is_special_scheme(url.scheme()) { + return; + } + + let path = url.path(); + let has_remaining = url.fragment().is_some() || url.query().is_some(); + + #[allow(clippy::manual_strip)] + if path.ends_with(' ') && has_remaining { + let new_path = [&path[..path.len() - 1], "%20"].concat(); + url.set_path(&new_path); + } +} + +/// Per WHATWG URL spec §4.5.3 ("URL serializer"), the `/.` path sentinel is +/// only inserted when a URL has no host AND its path starts with `//`. The +/// `url` crate inserts the sentinel during parsing and can leave it in the +/// serialization even after a host is set, breaking WPT `url-setters` +/// subtests like `.hostname = 'h'`. +/// +/// This strips the sentinel whenever the URL has a non-empty host and the +/// path begins with `/./`. +/// Per WHATWG URL spec §4.2, a file URL path segment matching `[A-Za-z]|` +/// followed by `/`, `\`, `?`, `#`, or end-of-path is a Windows drive letter. +/// Parsers normalize the `|` to `:`. The `url` crate doesn't perform this +/// rewrite itself, so we do it after parsing (WPT `url-constructor.any.js` +/// "Parsing: "). +/// When a `file://HOST/C:/...` string is parsed, the `url` crate drops +/// HOST (normalizing to `file:///C:/...`). Per WHATWG URL spec the host +/// must be preserved when non-empty (drive-letter state only applies when +/// host is null). Extract the host from the original source string and +/// re-set it on the parsed URL so downstream `join()` sees the host. +pub fn preserve_file_url_host(source: &str, mut url: Url) -> Url { + if url.scheme() != "file" { + return url; + } + if url.host_str().is_some_and(|h| !h.is_empty()) { + return url; + } + // Look for `file://HOST/...` in the original string. + let Some(rest) = source.strip_prefix("file://") else { + return url; + }; + let Some((host, _)) = rest.split_once('/') else { + return url; + }; + if host.is_empty() { + return url; + } + let _ = url.set_host(Some(host)); + url +} + +/// When resolving a relative URL against a file:// base whose first path +/// segment is a Windows drive letter (e.g. `file://h/C:/a/b`), the url crate +/// loses the host during `join`. Per WHATWG URL spec the host must be +/// preserved (WPT `url-constructor.any.js` "" base). +/// Patch the joined URL by restoring the base's host. +pub fn restore_file_url_host(base: &Url, joined: &mut Url) { + if base.scheme() != "file" || joined.scheme() != "file" { + return; + } + // Only when base had a host and joined has none / empty. + let Some(base_host) = base.host_str() else { + return; + }; + if base_host.is_empty() { + return; + } + if joined.host_str().is_some_and(|h| !h.is_empty()) { + return; + } + // Only when base's first path segment is a Windows drive letter — that's + // the code path that the url crate mishandles. + let base_path = base.path(); + let is_drive_letter_first_seg = base_path + .as_bytes() + .get(1) + .is_some_and(|b| b.is_ascii_alphabetic()) + && base_path.as_bytes().get(2) == Some(&b':') + && matches!(base_path.as_bytes().get(3), Some(&b'/') | None); + if !is_drive_letter_first_seg { + return; + } + let _ = joined.set_host(Some(base_host)); +} + +pub fn normalize_windows_drive_letter(url: &mut Url) { + if url.scheme() != "file" { + return; + } + let path = url.path(); + let bytes = path.as_bytes(); + // Expect path like "/|/..." — 4+ bytes, leading slash, letter, + // pipe, trailing slash. + if bytes.len() < 4 + || bytes[0] != b'/' + || !bytes[1].is_ascii_alphabetic() + || bytes[2] != b'|' + || bytes[3] != b'/' + { + return; + } + let new_path = ["/", &path[1..2], ":", &path[3..]].concat(); + url.set_path(&new_path); +} + +/// Per WHATWG URL spec, a non-special URL with an empty host can have its +/// path erased (WPT `url-setters.any.js`). The `url` crate keeps a trailing +/// `/` after the authority; reparse the serialization with it stripped when +/// the caller has explicitly set an empty pathname on such a URL. +pub fn erase_empty_host_path(url: &mut Url) { + if is_special_scheme(url.scheme()) { + return; + } + if url.path() != "/" { + return; + } + let serialized = url.as_str(); + // Serialized form must be `://` + `/` to qualify. (`scheme:/`, + // without authority, isn't eligible — the extra `/` is not a sentinel + // but a real path character.) + let Some(scheme_end) = serialized.find("://") else { + return; + }; + let authority_and_path = &serialized[scheme_end + 3..]; + // After "://": optional userinfo + host + port, then the path. If the + // path is just "/" and everything before is empty, the full + // authority_and_path is "/". + if authority_and_path != "/" { + return; + } + // Strip the trailing `/`. + let stripped = &serialized[..serialized.len() - 1]; + if let Ok(reparsed) = Url::parse(stripped) { + *url = reparsed; + } +} + +pub fn strip_path_sentinel(url: &mut Url) { + if is_special_scheme(url.scheme()) { + return; + } + // Path starting with `//` is what triggers the `/.` sentinel in the url + // crate's serialization — but the sentinel is only spec-correct when + // there's no authority. If the URL has `://` and its serialization still + // contains `/./` at the path boundary, reparse with it stripped. + if !url.path().starts_with("//") { + return; + } + let serialized = url.as_str(); + // Authority is present iff serialization contains "://". + let Some(auth_start) = serialized.find("://") else { + return; + }; + let after_auth = auth_start + 3; + // Look for next `/` that starts the path region. + let Some(path_start_rel) = serialized[after_auth..].find('/') else { + return; + }; + let path_idx = after_auth + path_start_rel; + if serialized[path_idx..].starts_with("/./") { + let stripped = [&serialized[..path_idx], &serialized[path_idx + 2..]].concat(); + if let Ok(reparsed) = Url::parse(&stripped) { + *url = reparsed; + } + } +} + +pub fn init(ctx: &Ctx<'_>) -> Result<()> { + let globals = ctx.globals(); + + Class::::define(&globals)?; + Class::::define(&globals)?; + + Ok(()) +} + +pub struct UrlModule; + +impl ModuleDef for UrlModule { + fn declare(declare: &Declarations) -> Result<()> { + declare.declare(stringify!(URL))?; + declare.declare(stringify!(URLSearchParams))?; + declare.declare("urlToHttpOptions")?; + declare.declare("domainToUnicode")?; + declare.declare("domainToASCII")?; + declare.declare("fileURLToPath")?; + declare.declare("pathToFileURL")?; + declare.declare("format")?; + declare.declare("default")?; + Ok(()) + } + + fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { + let globals = ctx.globals(); + BasePrimordials::init(ctx)?; + let url: Constructor = globals.get(stringify!(URL))?; + let url_search_params: Constructor = globals.get(stringify!(URLSearchParams))?; + + export_default(ctx, exports, |default| { + default.set(stringify!(URL), url)?; + default.set(stringify!(URLSearchParams), url_search_params)?; + default.set("urlToHttpOptions", Func::from(url_to_http_options))?; + default.set( + "domainToUnicode", + Func::from(|domain: String| domain_to_unicode(&domain)), + )?; + default.set( + "domainToASCII", + Func::from(|domain: String| domain_to_ascii(&domain)), + )?; + default.set("fileURLToPath", Func::from(file_url_to_path))?; + default.set("pathToFileURL", Func::from(path_to_file_url))?; + default.set("format", Func::from(url_format))?; + Ok(()) + })?; + + Ok(()) + } +} + +impl From for ModuleInfo { + fn from(val: UrlModule) -> Self { + ModuleInfo { + name: "url", + module: val, + } + } +} diff --git a/stdlib/src/llrt/llrt_url/url_class.rs b/stdlib/src/llrt/llrt_url/url_class.rs new file mode 100644 index 00000000..d7bb6229 --- /dev/null +++ b/stdlib/src/llrt/llrt_url/url_class.rs @@ -0,0 +1,347 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#![allow(clippy::uninlined_format_args)] + +use std::{cell::RefCell, rc::Rc}; + +use rquickjs::{ + atom::PredefinedAtom, class::Trace, function::Opt, Class, Coerced, Ctx, Exception, FromJs, + IntoJs, Null, Object, Result, Value, +}; +use url::{quirks, Url}; + +use super::url_search_params::URLSearchParams; + +/// Represents a JavaScript +/// [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) as defined +/// by the [WHATWG URL standard](https://url.spec.whatwg.org/). +#[derive(Clone, Trace, rquickjs::JsLifetime)] +#[rquickjs::class] +pub struct URL<'js> { + #[qjs(skip_trace)] + url: Rc>, + search_params: Class<'js, URLSearchParams>, +} + +#[rquickjs::methods(rename_all = "camelCase")] +impl<'js> URL<'js> { + #[qjs(constructor)] + pub fn new(ctx: Ctx<'js>, input: Value<'js>, base: Opt>) -> Result { + // USVString conversion per WHATWG URL spec: lone UTF-16 surrogates + // must be replaced with U+FFFD (not rejected) before the basic URL + // parser runs (WPT `url-origin.any.js` passes URLs containing lone + // surrogates and expects them to parse). + let input: Result = if input.is_string() { + crate::llrt_utils::bytes::get_lossy_string(input.clone()) + } else { + Coerced::::from_js(&ctx, input.clone()).map(|c| c.0) + }; + if let Some(base) = base.into_inner() { + if let Some(base) = base.as_string() { + if let Ok(base) = base.to_string() { + let base_url: Url = base + .parse() + .map_err(|_| Exception::throw_type(&ctx, "Invalid base URL"))?; + // Work around a url-crate normalization that loses the + // host when a file:// URL's path starts with a Windows + // drive letter (WPT url-constructor.any.js file-URL- + // with-host base cases). Extract the host manually + // from the original source string and preserve it. + let base_url = super::preserve_file_url_host(&base, base_url); + if let Ok(input) = input { + let mut joined = base_url + .join(input.as_str()) + .map_err(|_| Exception::throw_type(&ctx, "Invalid URL"))?; + super::restore_file_url_host(&base_url, &mut joined); + return Self::from_url(ctx, joined); + } + return Self::from_str(ctx, &base); + } + } + } + if let Ok(input) = input { + Self::from_str(ctx, input.as_str()) + } else { + Err(Exception::throw_message(&ctx, "Invalid URL")) + } + } + + #[qjs(get)] + pub fn hash(&self) -> String { + quirks::hash(&self.url.borrow()).to_string() + } + + #[qjs(set, rename = "hash")] + pub fn set_hash(&mut self, hash: String) -> String { + self.before_mutation(); + quirks::set_hash(&mut self.url.borrow_mut(), &hash); + hash + } + + #[qjs(get)] + pub fn host(&self) -> String { + quirks::host(&self.url.borrow()).to_string() + } + + #[qjs(set, rename = "host")] + pub fn set_host(&mut self, host: Coerced) -> String { + self.before_mutation(); + let _ = quirks::set_host(&mut self.url.borrow_mut(), &host); + host.0 + } + + #[qjs(get)] + pub fn hostname(&self) -> String { + quirks::hostname(&self.url.borrow()).to_string() + } + + #[qjs(set, rename = "hostname")] + pub fn set_hostname(&mut self, hostname: Coerced) -> String { + self.before_mutation(); + let _ = quirks::set_hostname(&mut self.url.borrow_mut(), hostname.as_str()); + super::strip_path_sentinel(&mut self.url.borrow_mut()); + hostname.0 + } + + #[qjs(get)] + pub fn href(&self) -> String { + quirks::href(&self.url.borrow()).to_string() + } + + #[qjs(set, rename = "href")] + pub fn set_href(&mut self, href: String) -> String { + self.before_mutation(); + let _ = quirks::set_href(&mut self.url.borrow_mut(), &href); + href + } + + #[qjs(get)] + pub fn origin(&self) -> String { + let url = self.url.borrow(); + // Per WHATWG URL spec §6.2, origin of a blob URL is computed by parsing + // the path as a URL. If the result's scheme is HTTP(S), return that + // URL's origin; otherwise, return an opaque (null) origin. The `url` + // crate returns the nested URL's origin even for non-HTTP schemes, + // breaking WPT `url-origin.any.js` on cases like `blob:ftp://...` and + // `blob:blob:https://...`. + if url.scheme() == "blob" { + return match url::Url::parse(url.path()) { + Ok(inner) if matches!(inner.scheme(), "http" | "https") => quirks::origin(&inner), + _ => "null".into(), + }; + } + quirks::origin(&url) + } + + #[qjs(get)] + pub fn password(&self) -> String { + quirks::password(&self.url.borrow()).to_string() + } + + #[qjs(set, rename = "password")] + pub fn set_password(&mut self, password: Coerced) -> String { + self.before_mutation(); + let _ = quirks::set_password(&mut self.url.borrow_mut(), &password); + password.0 + } + + #[qjs(get)] + pub fn pathname(&self) -> String { + quirks::pathname(&self.url.borrow()).to_string() + } + + #[qjs(set, rename = "pathname")] + pub fn set_pathname(&mut self, pathname: Coerced) -> String { + self.before_mutation(); + quirks::set_pathname(&mut self.url.borrow_mut(), pathname.as_str()); + // Per WHATWG URL spec, a non-special URL with an empty host can have + // its path erased (WPT `url-setters.any.js` "Non-special URLs with + // an empty host can have their paths erased"). The `url` crate + // forces a single `/` after the authority; strip it when the caller + // set an empty pathname on such a URL. + if pathname.0.is_empty() { + super::erase_empty_host_path(&mut self.url.borrow_mut()); + } + pathname.0 + } + + #[qjs(get)] + pub fn port(&self) -> String { + quirks::port(&self.url.borrow()).to_string() + } + + #[qjs(set, rename = "port")] + pub fn set_port(&mut self, ctx: Ctx<'js>, port: Value<'js>) -> Value<'js> { + if port.is_null() + || port.is_undefined() + || (port.is_int() && unsafe { port.as_int().unwrap_unchecked() } < 0) + { + return port; + } + if let Ok(port_string) = Coerced::::from_js(&ctx, port.clone()) { + self.before_mutation(); + // Per WHATWG URL spec, the port-state parser strips tab/LF/CR + // before reading. An empty STRIPPED value (but non-empty original) + // makes port parsing fail, which per spec means no-op (keep + // existing port). An empty ORIGINAL value, however, clears the + // port. + if port_string.is_empty() { + let _ = quirks::set_port(&mut self.url.borrow_mut(), ""); + } else { + let stripped: String = port_string + .chars() + .filter(|c| !matches!(c, '\t' | '\n' | '\r')) + .collect(); + if !stripped.is_empty() { + let _ = quirks::set_port(&mut self.url.borrow_mut(), &stripped); + } + // stripped is empty → parse failure per spec → no-op + } + } + port + } + + #[qjs(get)] + pub fn protocol(&self) -> String { + quirks::protocol(&self.url.borrow()).to_string() + } + + #[qjs(set, rename = "protocol")] + pub fn set_protocol(&mut self, protocol: Coerced) -> String { + self.before_mutation(); + let _ = quirks::set_protocol(&mut self.url.borrow_mut(), &protocol); + protocol.0 + } + + #[qjs(get)] + pub fn search(&self) -> String { + quirks::search(&self.url.borrow()).to_string() + } + + #[qjs(set, rename = "search")] + pub fn set_search(&mut self, search: Coerced) -> String { + self.before_mutation(); + quirks::set_search(&mut self.url.borrow_mut(), &search); + search.0 + } + + #[qjs(get)] + pub fn search_params(&self) -> &Value<'js> { + self.search_params.as_value() + } + + #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] + pub fn to_string_tag() -> &'static str { + stringify!(URL) + } + + #[qjs(get)] + pub fn username(&self) -> String { + quirks::username(&self.url.borrow()).to_string() + } + + #[qjs(set, rename = "username")] + pub fn set_username(&mut self, username: Coerced) -> String { + self.before_mutation(); + let _ = quirks::set_username(&mut self.url.borrow_mut(), &username); + username.0 + } + + #[qjs(static)] + pub fn can_parse(ctx: Ctx<'js>, input: Value<'js>, base: Opt>) -> bool { + Self::new(ctx, input, base).is_ok() + } + + #[qjs(static)] + pub fn parse(ctx: Ctx<'js>, input: Value<'js>, base: Opt>) -> Result> { + Self::new(ctx.clone(), input, base) + .map_or_else(|_| Null.into_js(&ctx), |instance| instance.into_js(&ctx)) + } + + #[qjs(rename = PredefinedAtom::ToJSON)] + pub fn to_json(&self) -> String { + self.to_string() + } + + pub fn to_string(&self) -> String { + self.href() + } +} + +impl<'js> URL<'js> { + pub fn from_str(ctx: Ctx<'js>, input: &str) -> Result { + let mut url: Url = input + .parse() + .map_err(|_| Exception::throw_type(&ctx, "Invalid URL"))?; + super::normalize_windows_drive_letter(&mut url); + super::convert_trailing_space(&mut url); + Self::build(ctx, url) + } + + pub fn from_url(ctx: Ctx<'js>, mut url: Url) -> Result { + super::normalize_windows_drive_letter(&mut url); + super::convert_trailing_space(&mut url); + Self::build(ctx, url) + } + + /// Validate that a string parses as a URL without constructing a JS + /// instance. Used by callers (e.g. `llrt_fetch`) that just need to know + /// whether a user-supplied string is a valid URL. + pub fn is_valid(input: &str) -> bool { + input.parse::().is_ok() + } + + fn build(ctx: Ctx<'js>, url: Url) -> Result { + let shared = Rc::new(RefCell::new(url)); + let search_params = Class::instance(ctx, URLSearchParams::from_url(&shared))?; + Ok(Self { + url: shared, + search_params, + }) + } + + fn before_mutation(&mut self) { + super::convert_trailing_space(&mut self.url.borrow_mut()); + } + + pub(crate) fn inner_url(&self) -> std::cell::Ref<'_, Url> { + self.url.borrow() + } +} + +pub fn url_to_http_options<'js>(ctx: Ctx<'js>, url: Class<'js, URL<'js>>) -> Result> { + let obj = Object::new(ctx)?; + let url = url.borrow(); + + let port = url.port(); + let username = url.username(); + let search = url.search(); + let hash = url.inner_url().fragment().unwrap_or("").to_string(); + + obj.set("protocol", url.protocol())?; + obj.set("hostname", url.hostname())?; + + if !hash.is_empty() { + obj.set("hash", hash)?; + } + + let pathname = url.pathname(); + let path = [pathname.as_str(), search.as_str()].concat(); + if !search.is_empty() { + obj.set("search", search)?; + } + obj.set("pathname", pathname)?; + obj.set("path", path)?; + obj.set("href", url.href())?; + + if !username.is_empty() { + obj.set("auth", [username, url.password()].join(":"))?; + } + + if !port.is_empty() { + obj.set("port", port)?; + } + + Ok(obj) +} diff --git a/stdlib/src/llrt/llrt_url/url_search_params.rs b/stdlib/src/llrt/llrt_url/url_search_params.rs new file mode 100644 index 00000000..198b4a48 --- /dev/null +++ b/stdlib/src/llrt/llrt_url/url_search_params.rs @@ -0,0 +1,1058 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::{ + cell::RefCell, + collections::{HashMap, HashSet}, + rc::Rc, +}; + +use crate::llrt_utils::{ + bytes::get_lossy_string, + class::{iterator_result, live_iterator, IterKind}, + primordials::{BasePrimordials, Primordial}, + string::get_coerced_defined_string, +}; +use rquickjs::{ + atom::PredefinedAtom, class::Trace, function::Opt, prelude::This, Array, Class, Coerced, Ctx, + Exception, FromJs, Function, IntoJs, Null, Object, Result, Symbol, Value, +}; +use url::Url; + +use super::convert_trailing_space; + +/// Represents `URLSearchParams` in the JavaScript context +/// +/// +/// +/// # Examples +/// +/// ```rust,ignore +/// // This is JavaScript +/// const params = new URLSearchParams(); +/// params.set("foo", "bar"); +/// ``` +#[derive(Clone, Trace, rquickjs::JsLifetime)] +#[rquickjs::class] +pub struct URLSearchParams { + // URL and URLSearchParams work together to manipulate URLs, so using a + // reference counter (Rc) allows them to have shared ownership of the + // undering Url, and a RefCell allows interior mutability. + #[qjs(skip_trace)] + pub url: Rc>, +} + +// URLSearchParams is designed to operate directly on the underlying Url to +// avoid maintaining derived state that can get out of sync. When it's used +// independently, it still needs a valid URL (http://example.com), but this +// doesn't have any effect on using URLSearchParams with URL as the params are +// stringified when added to a URL. +// +// ```js +// const params = new URLSearchParams("foo=bar"); +// const url = new URL("http://github.com"); +// url.search = params; // This works as expected +// ``` +#[rquickjs::methods(rename_all = "camelCase")] +impl<'js> URLSearchParams { + #[qjs(constructor)] + pub fn new(ctx: Ctx<'js>, init: Opt>) -> Result { + if let Some(init) = init.into_inner() { + if init.is_string() { + let string = get_lossy_string(init)?; + return Ok(Self::from_str(string)); + } else if init.is_array() { + return Self::from_array(&ctx, unsafe { init.into_array().unwrap_unchecked() }); + } else if init.is_object() { + return Self::from_object(&ctx, unsafe { init.into_object().unwrap_unchecked() }); + } + } + let url: Url = unsafe { "http://example.com".parse().unwrap_unchecked() }; + + Ok(URLSearchParams { + url: Rc::new(RefCell::new(url)), + }) + } + + // + // Properties + // + + #[qjs(get)] + pub fn size(&self) -> usize { + self.url.borrow().query_pairs().count() + } + + #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] + pub fn to_string_tag() -> &'static str { + stringify!(URLSearchParams) + } + + // + // Instance methods + // + + pub fn append(&mut self, key: Coerced, value: Coerced) { + convert_trailing_space(&mut self.url.borrow_mut()); + + self.url + .borrow_mut() + .query_pairs_mut() + .append_pair(key.as_str(), value.as_str()); + self.sync_query(); + } + + pub fn delete(&mut self, key: Coerced, value: Opt>) { + convert_trailing_space(&mut self.url.borrow_mut()); + + let key = key.0; + + let value = get_coerced_defined_string(&value.0); + + let new_pairs: Vec<_> = self + .url + .borrow() + .query_pairs() + .filter(|(k, v)| { + if let Some(value) = value.as_ref() { + return !(*k == key && *v == *value); + } + *k != key + }) + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(); + + if !new_pairs.is_empty() { + self.url + .borrow_mut() + .query_pairs_mut() + .clear() + .extend_pairs(new_pairs); + } else { + self.url.borrow_mut().set_query(None); + } + self.sync_query(); + } + + pub fn entries( + this: This>, + ctx: Ctx<'js>, + ) -> Result>> { + URLSearchParamsIter::new(&ctx, this.0, IterKind::Entries) + } + + pub fn for_each( + this: This>, + callback: Function<'js>, + ) -> Result<()> { + // Re-read each index so the callback's mutations are observed. + let mut index = 0; + loop { + let pair = this + .0 + .borrow() + .url + .borrow() + .query_pairs() + .nth(index) + .map(|(k, v)| (k.to_string(), v.to_string())); + let Some((k, v)) = pair else { + break; + }; + () = callback.call((v, k, this.0.clone()))?; + index += 1; + } + Ok(()) + } + + pub fn get(&mut self, ctx: Ctx<'js>, key: String) -> Result> { + match self + .url + .borrow() + .query_pairs() + .find(|(k, _)| *k == key) + .map(|(_, v)| v) + { + Some(value) => value.into_js(&ctx), + None => Null.into_js(&ctx), + } + } + + pub fn get_all(&mut self, key: String) -> Vec { + self.url + .borrow() + .query_pairs() + .filter_map(|(k, v)| if k == key { Some(v.to_string()) } else { None }) + .collect() + } + + pub fn has(&self, key: Coerced, value: Opt>) -> bool { + let value = get_coerced_defined_string(&value.0); + let key = key.0; + self.url.borrow().query_pairs().any(|(k, v)| { + if let Some(value) = value.as_ref() { + return *k == key && *v == *value; + } + *k == key + }) + } + + pub fn keys( + this: This>, + ctx: Ctx<'js>, + ) -> Result>> { + URLSearchParamsIter::new(&ctx, this.0, IterKind::Keys) + } + + pub fn set(&mut self, key: Coerced, value: Coerced) { + convert_trailing_space(&mut self.url.borrow_mut()); + + let key = key.0; + let value = value.0; + + // Use a HashSet just to filter duplicates + let mut uniques = HashSet::new(); + let mut new_query_pairs: Vec<(String, String)> = Vec::new(); + + for (k, v) in self.url.borrow().query_pairs() { + // Update the value for an existing key + let value = if k == key { + value.clone() + } else { + v.to_string() + }; + + let query_pair = (k.to_string(), value); + if uniques.insert(query_pair.clone()) { + new_query_pairs.push(query_pair); + } + } + + // Append a new key/value pair + let query_pair = (key, value); + if uniques.insert(query_pair.clone()) { + new_query_pairs.push(query_pair); + } + + self.url + .borrow_mut() + .query_pairs_mut() + .clear() + .extend_pairs(new_query_pairs); + self.sync_query(); + } + + pub fn sort(&mut self) { + let mut new_pairs: Vec<(String, String)> = + self.url.borrow().query_pairs().into_owned().collect(); + new_pairs.sort_by(|(a, _), (b, _)| { + // Spec requires sorting by UTF-16 code units + let a_utf16 = a.encode_utf16(); + let b_utf16 = b.encode_utf16(); + a_utf16.cmp(b_utf16) + }); + + if new_pairs.is_empty() { + self.url.borrow_mut().set_query(None); + } else { + self.url + .borrow_mut() + .query_pairs_mut() + .clear() + .extend_pairs(new_pairs); + } + self.sync_query(); + } + + pub fn to_string(&self) -> String { + // The Url create doesn't properly encode query params for all edge + // cases, so we need to construct the query string by percent-encoding + // each key/value + // TODO: This should probably be fixed in the Url crate + let url = self.url.borrow(); + url.query_pairs().fold( + String::with_capacity(url.query().map_or(0, |q| q.len())), + |mut acc, (key, value)| { + if !acc.is_empty() { + acc.push('&'); + } + url::form_urlencoded::byte_serialize(key.as_bytes()).for_each(|b| acc.push_str(b)); + acc.push('='); + url::form_urlencoded::byte_serialize(value.as_bytes()) + .for_each(|b| acc.push_str(b)); + acc + }, + ) + } + + pub fn values( + this: This>, + ctx: Ctx<'js>, + ) -> Result>> { + URLSearchParamsIter::new(&ctx, this.0, IterKind::Values) + } + + #[qjs(rename = PredefinedAtom::SymbolIterator)] + pub fn iterator( + this: This>, + ctx: Ctx<'js>, + ) -> Result>> { + URLSearchParamsIter::new(&ctx, this.0, IterKind::Entries) + } +} + +impl<'js> URLSearchParams { + fn read_entry(&self, index: usize, ctx: &Ctx<'js>) -> Result, Value<'js>)>> { + let pair = self + .url + .borrow() + .query_pairs() + .nth(index) + .map(|(k, v)| (k.to_string(), v.to_string())); + match pair { + Some((k, v)) => Ok(Some((k.into_js(ctx)?, v.into_js(ctx)?))), + None => Ok(None), + } + } + + /// Re-serialize the query string with proper percent-encoding. + /// The url crate doesn't encode commas, so we rebuild the query + /// using form_urlencoded::byte_serialize after each mutation. + fn sync_query(&self) { + let query = self.to_string(); + let mut url = self.url.borrow_mut(); + if query.is_empty() { + url.set_query(None); + } else { + url.set_query(Some(&query)); + } + } + + #[allow(clippy::should_implement_trait)] + pub fn from_str(query: String) -> Self { + let query = if !query.starts_with('?') { + ["?", &query].concat() + } else { + query + }; + let url = unsafe { + "http://example.com" + .parse::() + .unwrap_unchecked() + .join(&query) + .unwrap_unchecked() + }; + Self { + url: Rc::new(RefCell::new(url)), + } + } + + pub fn from_url(url: &Rc>) -> Self { + Self { + url: Rc::clone(url), + } + } + + pub fn from_array(ctx: &Ctx<'js>, array: Array<'js>) -> Result { + let mut url: Url = "http://example.com".parse().unwrap(); + let query_pairs: Vec<(String, String)> = array + .into_iter() + .map(|value| { + if let Ok(value) = value { + if let Some(pair) = value.as_array() { + if pair.len() == 2 { + let key_val: Value = pair.get(0)?; + let val_val: Value = pair.get(1)?; + let key = if key_val.is_string() { + get_lossy_string(key_val)? + } else { + Coerced::::from_js(ctx, key_val)?.0 + }; + let value = if val_val.is_string() { + get_lossy_string(val_val)? + } else { + Coerced::::from_js(ctx, val_val)?.0 + }; + return Ok((key, value)); + } + } + }; + Err(Exception::throw_type( + ctx, + "Invalid tuple: Each query pair must be an iterable [name, value] tuple", + )) + }) + .collect::>>()? + .into_iter() + .collect(); + + url.query_pairs_mut().extend_pairs(query_pairs); + + Ok(Self { + url: Rc::new(RefCell::new(url)), + }) + } + + pub fn from_object(ctx: &Ctx<'js>, object: Object<'js>) -> Result { + let iterator = Symbol::iterator(ctx.clone()); + if object.contains_key(iterator)? { + let query_pairs: Array = BasePrimordials::get(ctx)? + .function_array_from + .call((object,))?; + return Self::from_array(ctx, query_pairs); + } + + let mut url: Url = "http://example.com".parse().unwrap(); + let raw_pairs: Vec<(String, String)> = object + .keys::>() + .map(|key| { + let key = key?; + let key_string = if key.is_string() { + get_lossy_string(key.clone())? + } else { + Coerced::::from_js(ctx, key.clone())?.0 + }; + let value_val: Value = object.get(key)?; + let value = if value_val.is_string() { + get_lossy_string(value_val)? + } else { + Coerced::::from_js(ctx, value_val)?.0 + }; + Ok((key_string, value)) + }) + .collect::>>()?; + + // WebIDL record conversion: when multiple input keys normalise to the + // same string (e.g. two different lone surrogates both map to U+FFFD), + // the *last* value wins. Preserve original iteration order for keys + // that were only seen once. + let mut order: Vec = Vec::with_capacity(raw_pairs.len()); + let mut map: HashMap = HashMap::with_capacity(raw_pairs.len()); + for (k, v) in raw_pairs { + if !map.contains_key(&k) { + order.push(k.clone()); + } + map.insert(k, v); + } + let query_pairs: Vec<(String, String)> = order + .into_iter() + .map(|k| { + let v = map.remove(&k).unwrap_or_default(); + (k, v) + }) + .collect(); + + url.query_pairs_mut().extend_pairs(query_pairs); + + Ok(Self { + url: Rc::new(RefCell::new(url)), + }) + } +} + +/// Live iterator over a [`URLSearchParams`]. Re-reads on each `next()` so +/// mutations during iteration are observed. +#[derive(Trace, rquickjs::JsLifetime)] +#[rquickjs::class] +pub struct URLSearchParamsIter<'js> { + params: Class<'js, URLSearchParams>, + #[qjs(skip_trace)] + index: usize, + #[qjs(skip_trace)] + kind: IterKind, +} + +impl<'js> URLSearchParamsIter<'js> { + fn new( + ctx: &Ctx<'js>, + params: Class<'js, URLSearchParams>, + kind: IterKind, + ) -> Result> { + live_iterator( + ctx, + Self { + params, + index: 0, + kind, + }, + ) + } +} + +#[rquickjs::methods] +impl<'js> URLSearchParamsIter<'js> { + fn next(&mut self, ctx: Ctx<'js>) -> Result> { + let entry = self.params.borrow().read_entry(self.index, &ctx)?; + if entry.is_some() { + self.index += 1; + } + iterator_result(&ctx, self.kind, entry) + } + + #[qjs(rename = PredefinedAtom::SymbolIterator)] + fn iter(this: This>) -> Class<'js, Self> { + this.0 + } +} + +#[cfg(test)] +mod tests { + use crate::llrt_test::test_sync_with; + use rquickjs::{CatchResultExt, Class}; + + use super::*; + + fn setup(ctx: &rquickjs::Ctx) { + BasePrimordials::init(ctx).unwrap(); + Class::::define(&ctx.globals()).unwrap(); + } + + #[tokio::test] + async fn test_basic() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams(); + params.append('a', '1'); + params.append('b', '2'); + params.append('a', '3'); + params.append('b', '4'); + params.append('c', 8); + params.delete('a'); + params.delete('b', '2'); + params.toString() + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, "b=4&c=8"); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_iterate() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams(); + params.append('a', '1'); + params.append('b', '2'); + params.append('a', '3'); + let res = []; + for (const [name, value] of params) { + res.push(`${name}=${value}`); + } + res.join('&') + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, "a=1&b=2&a=3"); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_iterate_live_delete() { + // Deleting the current/later entry during iteration skips it. + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams("foo=0&baz=1&BAR=2"); + const keys = []; + for (const [name] of params) { + keys.push(name); + params.delete("baz"); + } + keys.join(",") + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, "foo,BAR"); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_iterate_live_append() { + // Appending during iteration causes the new pair to be reached. + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams("foo=0&baz=1"); + const keys = []; + for (const [name] of params) { + keys.push(name); + if (name === "baz") params.append("end", "9"); + } + keys.join(",") + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, "foo,baz,end"); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_iterate_keys_values_live() { + // keys()/values() must also be live index-based iterators. + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams("a=1&b=2&c=3"); + const k = [...params.keys()].join(","); + const v = [...params.values()].join(","); + `${k}|${v}` + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, "a,b,c|1,2,3"); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_iterate_entries() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams(); + params.append('a', '1'); + params.append('b', '2'); + params.append('a', '3'); + let res = []; + for (const [name, value] of params.entries()) { + res.push(`${name}=${value}`); + } + res.join('&') + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, "a=1&b=2&a=3"); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_iterate_keys() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams(); + params.append('a', '1'); + params.append('b', '2'); + params.append('a', '3'); + let res = []; + for (const name of params.keys()) { + res.push(name); + } + res.join('&') + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, "a&b&a"); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_iterate_values() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams(); + params.append('a', '1'); + params.append('b', '2'); + params.append('a', '3'); + let res = []; + for (const name of params.values()) { + res.push(name); + } + res.join('&') + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, "1&2&3"); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_new_string() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams('a=1&b=2&a=3'); + params.toString() + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, "a=1&b=2&a=3"); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_new_string_url() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams('https://google.com?a=1&b=2&a=3'); + params.toString() + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, "https%3A%2F%2Fgoogle.com%3Fa=1&b=2&a=3"); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_new_object() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams({'a': 1, 'b': 2}); + params.toString() + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, "a=1&b=2"); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_new_array() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams([['a', 1], ['b', 2], ['a', 3]]); + params.toString() + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, "a=1&b=2&a=3"); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_new_iterator() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams(); + params.append('a', '1'); + params.append('b', '2'); + params.append('a', '3'); + const params2 = new URLSearchParams(params.entries()); + params2.toString() + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, "a=1&b=2&a=3"); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_size() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams(); + params.append('a', '1'); + params.append('b', '2'); + params.append('a', '3'); + params.size + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, 3); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_set() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams(); + params.append('a', '1'); + params.append('b', '2'); + params.append('a', '3'); + params.set('a', '4'); + params.toString() + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, "a=4&b=2"); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_get() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams(); + params.append('a', '1'); + params.append('b', '2'); + params.append('a', '3'); + params.get('a') + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, "1"); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_get_missing() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams(); + params.append('a', '1'); + params.append('b', '2'); + params.append('a', '3'); + params.get('c') === null + "#, + ) + .catch(&ctx) + .unwrap(); + assert!(result); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_get_all() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams(); + params.append('a', '1'); + params.append('b', '2'); + params.append('a', '3'); + params.getAll('a').join('&') + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, "1&3"); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_get_all_missing() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams(); + params.append('a', '1'); + params.append('b', '2'); + params.append('a', '3'); + params.getAll('c').join('&') + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, ""); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_has() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams(); + params.append('a', '1'); + params.append('b', '2'); + params.append('a', '3'); + params.has('b') + "#, + ) + .catch(&ctx) + .unwrap(); + assert!(result); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_has_value() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams(); + params.append('a', '1'); + params.append('b', '2'); + params.append('a', '3'); + params.has('b', 5) + "#, + ) + .catch(&ctx) + .unwrap(); + assert!(!result); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_has_not() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams(); + params.append('a', '1'); + params.append('b', '2'); + params.append('a', '3'); + params.has('c') + "#, + ) + .catch(&ctx) + .unwrap(); + assert!(!result); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_sort() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams(); + params.append('a', '3'); + params.append('b', '2'); + params.append('a', '1'); + params.sort(); + params.toString() + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, "a=3&a=1&b=2"); + Ok(()) + }) + .await + } + + #[tokio::test] + async fn test_for_each() { + test_sync_with(|ctx| { + setup(&ctx); + let result = ctx + .eval::( + r#" + const params = new URLSearchParams(); + params.append('a', '3'); + params.append('b', '2'); + params.append('a', '1'); + let res = []; + params.forEach((value, name) => { + res.push(`${name}=${value}`); + }); + res.join('&') + "#, + ) + .catch(&ctx) + .unwrap(); + assert_eq!(result, "a=3&b=2&a=1"); + Ok(()) + }) + .await + } +} diff --git a/stdlib/src/llrt/llrt_utils/any_of.rs b/stdlib/src/llrt/llrt_utils/any_of.rs new file mode 100644 index 00000000..4dff62ce --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/any_of.rs @@ -0,0 +1,299 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use rquickjs::{ + class::{Trace, Tracer}, + Ctx, FromJs, IntoJs, JsLifetime, Result, Value, +}; + +macro_rules! define_any_of { + ($name:ident, $($variant:ident),+) => { + #[derive(Debug, Clone)] + pub enum $name<$($variant),+> { + $( + $variant($variant), + )+ + } + + define_any_of_from_js!($name, $($variant),+); + + impl<'js, $($variant: IntoJs<'js>),+> IntoJs<'js> for $name<$($variant),+> { + fn into_js(self, ctx: &Ctx<'js>) -> Result> { + match self { + $( + Self::$variant(val) => val.into_js(ctx), + )+ + } + } + } + + unsafe impl<'js, $($variant: JsLifetime<'js>),+> JsLifetime<'js> for $name<$($variant),+> { + type Changed<'to> = $name<$($variant::Changed<'to>),+>; + } + + impl<'js, $($variant: Trace<'js>),+> Trace<'js> for $name<$($variant),+> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + match self { + $( + Self::$variant(val) => val.trace(tracer), + )+ + } + } + } + + define_any_of_methods!($name, $($variant),+); + }; +} + +macro_rules! define_any_of_from_js { + ($name:ident, $first:ident, $($rest:ident),+) => { + impl<'js, $first: FromJs<'js>, $($rest: FromJs<'js>),+> FromJs<'js> for $name<$first, $($rest),+> { + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + define_any_of_from_js_impl!($name, ctx, value, $first, $($rest),+) + } + } + }; +} + +macro_rules! define_any_of_from_js_impl { + ($name:ident, $ctx:ident, $value:ident, $first:ident) => { + $first::from_js($ctx, $value).map($name::$first) + }; + + ($name:ident, $ctx:ident, $value:ident, $first:ident, $($rest:ident),+) => { + $first::from_js($ctx, $value.clone()).map($name::$first).or_else(|error| { + if error.is_from_js() { + define_any_of_from_js_impl!($name, $ctx, $value, $($rest),+) + } else { + Err(error) + } + }) + }; +} + +macro_rules! define_any_of_variant_methods { + ($variant:ident, $is_fn:ident, $as_fn:ident, $as_mut_fn:ident, $into_fn:ident) => { + #[allow(dead_code)] + pub fn $is_fn(&self) -> bool { + matches!(self, Self::$variant(_)) + } + + #[allow(dead_code)] + pub fn $as_fn(&self) -> Option<&$variant> { + match self { + Self::$variant(val) => Some(val), + _ => None, + } + } + + #[allow(dead_code)] + pub fn $as_mut_fn(&mut self) -> Option<&mut $variant> { + match self { + Self::$variant(val) => Some(val), + _ => None, + } + } + + #[allow(dead_code)] + pub fn $into_fn(self) -> std::result::Result<$variant, Self> { + match self { + Self::$variant(val) => Ok(val), + other => Err(other), + } + } + }; + + (A) => { + define_any_of_variant_methods!(A, is_a, as_a, as_a_mut, into_a); + }; + (B) => { + define_any_of_variant_methods!(B, is_b, as_b, as_b_mut, into_b); + }; + (C) => { + define_any_of_variant_methods!(C, is_c, as_c, as_c_mut, into_c); + }; + (D) => { + define_any_of_variant_methods!(D, is_d, as_d, as_d_mut, into_d); + }; + (E) => { + define_any_of_variant_methods!(E, is_e, as_e, as_e_mut, into_e); + }; + (F) => { + define_any_of_variant_methods!(F, is_f, as_f, as_f_mut, into_f); + }; + (G) => { + define_any_of_variant_methods!(G, is_g, as_g, as_g_mut, into_g); + }; + (H) => { + define_any_of_variant_methods!(H, is_h, as_h, as_h_mut, into_h); + }; +} + +macro_rules! define_any_of_methods { + ($name:ident, $($variant:ident),+) => { + impl<$($variant),+> $name<$($variant),+> { + $( + define_any_of_variant_methods!($variant); + )+ + } + }; +} + +define_any_of!(AnyOf2, A, B); +define_any_of!(AnyOf3, A, B, C); +define_any_of!(AnyOf4, A, B, C, D); +define_any_of!(AnyOf5, A, B, C, D, E); +define_any_of!(AnyOf6, A, B, C, D, E, F); +define_any_of!(AnyOf7, A, B, C, D, E, F, G); +define_any_of!(AnyOf8, A, B, C, D, E, F, G, H); + +#[cfg(test)] +mod tests { + use super::*; + use rquickjs::{Context, Runtime}; + + #[test] + fn test_any_of_string_number() { + let rt = Runtime::new().unwrap(); + let ctx = Context::full(&rt).unwrap(); + + ctx.with(|ctx| { + // Test string conversion + let val: Value = ctx.eval("'hello'").unwrap(); + let any: AnyOf2 = AnyOf2::from_js(&ctx, val).unwrap(); + assert!(any.is_a()); + assert_eq!(any.as_a().unwrap(), "hello"); + assert!(!any.is_b()); + assert!(any.as_b().is_none()); + + // Test number conversion + let val: Value = ctx.eval("42").unwrap(); + let any: AnyOf2 = AnyOf2::from_js(&ctx, val).unwrap(); + assert!(!any.is_a()); + assert!(any.is_b()); + assert_eq!(*any.as_b().unwrap(), 42); + }); + } + + #[test] + fn test_any_of_fallback() { + let rt = Runtime::new().unwrap(); + let ctx = Context::full(&rt).unwrap(); + + ctx.with(|ctx| { + // Test that it tries in order + let val: Value = ctx.eval("true").unwrap(); + let any: AnyOf3 = AnyOf3::from_js(&ctx, val).unwrap(); + assert!(any.is_c()); + assert!(*any.as_c().unwrap()); + assert!(!any.is_a()); + assert!(!any.is_b()); + }); + } + + #[test] + fn test_any_of_into_js() { + let rt = Runtime::new().unwrap(); + let ctx = Context::full(&rt).unwrap(); + + ctx.with(|ctx| { + let any: AnyOf2 = AnyOf2::A("test".to_string()); + let val: Value = any.into_js(&ctx).unwrap(); + let result: String = val.get().unwrap(); + assert_eq!(result, "test"); + + let any: AnyOf2 = AnyOf2::B(99); + let val: Value = any.into_js(&ctx).unwrap(); + let result: i32 = val.get().unwrap(); + assert_eq!(result, 99); + }); + } + + #[test] + fn test_any_of_methods() { + let rt = Runtime::new().unwrap(); + let ctx = Context::full(&rt).unwrap(); + + ctx.with(|ctx| { + // Test all methods for variant A + let val: Value = ctx.eval("'test'").unwrap(); + let any: AnyOf3 = AnyOf3::from_js(&ctx, val).unwrap(); + assert!(any.is_a()); + assert_eq!(any.as_a().unwrap(), "test"); + assert_eq!(any.into_a().unwrap(), "test"); + + // Test all methods for variant B + let val: Value = ctx.eval("42").unwrap(); + let any: AnyOf3 = AnyOf3::from_js(&ctx, val).unwrap(); + assert!(any.is_b()); + assert_eq!(*any.as_b().unwrap(), 42); + assert_eq!(any.into_b().unwrap(), 42); + + // Test all methods for variant C + let val: Value = ctx.eval("true").unwrap(); + let any: AnyOf3 = AnyOf3::from_js(&ctx, val).unwrap(); + assert!(any.is_c()); + assert!(*any.as_c().unwrap()); + assert!(any.into_c().unwrap()); + }); + } + + #[test] + fn test_any_of_mutable_methods() { + let rt = Runtime::new().unwrap(); + let ctx = Context::full(&rt).unwrap(); + + ctx.with(|ctx| { + let val: Value = ctx.eval("42").unwrap(); + let mut any: AnyOf4 = AnyOf4::from_js(&ctx, val).unwrap(); + + if let Some(n) = any.as_b_mut() { + *n = 100; + } + + assert_eq!(any.into_b().unwrap(), 100); + }); + } + + #[test] + fn test_any_of_error_propagation() { + use rquickjs::{Array, Object}; + + let rt = Runtime::new().unwrap(); + let ctx = Context::full(&rt).unwrap(); + + ctx.with(|ctx| { + // Test that conversion errors cause fallback to next type + let val: Value = ctx.eval("42").unwrap(); + let any: AnyOf2 = AnyOf2::from_js(&ctx, val).unwrap(); + assert!(any.is_b()); + + // Test that all types fail results in an error + let val: Value = ctx.eval("null").unwrap(); + let result: Result> = AnyOf2::from_js(&ctx, val); + assert!(result.is_err()); + }); + } + + #[test] + fn test_any_of_conversion_order() { + let rt = Runtime::new().unwrap(); + let ctx = Context::full(&rt).unwrap(); + + ctx.with(|ctx| { + // Test that conversion happens in order A, B, C, D, E + // Since 42 can be converted to f64, i32, etc., but String comes first and fails, + // it should try the next successful conversion + let val: Value = ctx.eval("42").unwrap(); + + // String should fail, so it tries i32 which succeeds + let any: AnyOf3 = AnyOf3::from_js(&ctx, val).unwrap(); + assert!(any.is_b()); + + // If we flip the order, f64 would be tried first (but both work) + let val: Value = ctx.eval("3.14").unwrap(); + let any: AnyOf3 = AnyOf3::from_js(&ctx, val).unwrap(); + assert!(any.is_b()); // f64 should succeed first + }); + } +} diff --git a/stdlib/src/llrt/llrt_utils/array_buffer.rs b/stdlib/src/llrt/llrt_utils/array_buffer.rs new file mode 100644 index 00000000..0c0ff8d2 --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/array_buffer.rs @@ -0,0 +1,122 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +//! Zero-copy `ArrayBuffer` helpers built on QuickJS-NG primitives. +//! +//! `rquickjs` doesn't yet ship safe wrappers for QuickJS-NG's +//! [immutable ArrayBuffer](https://tc39.es/proposal-immutable-arraybuffer/) +//! support, so we go through `rquickjs::qjs::*` directly. The two +//! capabilities exposed here are: +//! +//! * [`shared_array_buffer_view`] — create a fresh `ArrayBuffer` that +//! borrows the bytes of an existing one (no memcpy), kept alive via a +//! dup'd `JSValue` reference. The view is marked immutable, which is +//! both a correctness guarantee (consumer mutations can't leak into the +//! source) and a hard safety rail (the only QuickJS code path that +//! would lose our refcount handle is `.transfer()`, which immutability +//! blocks at the JS layer). +//! * [`set_immutable`] — flip the immutable flag on an existing +//! `ArrayBuffer` (used when the source is a freshly-allocated buffer +//! we own and want to seal before handing out). +//! +//! These are used by `Blob.stream()` / `Blob.slice()` and by fetch's +//! `Response.body` / `Request.body` getters to hand out aliased, +//! transfer-safe views into producer-owned storage. + +use std::ffi::c_void; + +use rquickjs::{qjs, ArrayBuffer, Ctx, Exception, Result, Value}; + +/// Mark an `ArrayBuffer` as immutable: subsequent writes through any +/// `Uint8Array` / `DataView` view silently fail (or `TypeError` in strict +/// mode), and `.transfer()` throws `TypeError: ArrayBuffer is immutable`. +/// +/// Calling this on an already-immutable buffer is a no-op. Calling it on +/// a detached buffer is a no-op (QuickJS returns -1 internally). The flag +/// is checked at write/transfer time, not at create time, so the buffer +/// can be initialised with bytes before being sealed. +pub fn set_immutable(ab: &ArrayBuffer<'_>) { + // Safety: the JSValue is owned by `ab`; we only flip a boolean flag + // on the underlying `JSArrayBuffer` struct. + unsafe { + qjs::JS_SetImmutableArrayBuffer(ab.as_value().as_raw(), true); + } +} + +/// Create a fresh, **immutable** `ArrayBuffer` that shares storage with +/// `source` at `[offset..offset+len]` without copying any bytes. The +/// returned buffer holds a dup'd reference to the source's `JSValue`, so +/// the backing allocation stays alive exactly as long as any view (or +/// transferred descendant of it) is reachable. +/// +/// Immutability is what makes this sound: +/// +/// * Writes through `Uint8Array` / `DataView` views silently no-op +/// (strict mode: `TypeError`) — aliased consumers can't corrupt the +/// source. +/// * `buffer.transfer()` throws `TypeError: ArrayBuffer is immutable` +/// — so a consumer can't detach the view and drop the `opaque` +/// pointer that keeps the source alive. Without this guard the +/// `free_func` would later fire with `opaque=NULL` (QuickJS strips +/// `opaque` on transfer; see `js_array_buffer_constructor3`) and +/// panic in `Box::from_raw(null)`. Because immutability blocks +/// transfer at the JS layer, that path is unreachable. +/// +/// If a future caller wants a *mutable* shared view, they need a +/// different cleanup strategy (ptr-keyed side table, upstream QuickJS +/// patch, or accepting a per-transfer leak). +pub fn shared_array_buffer_view<'js>( + ctx: &Ctx<'js>, + source: &ArrayBuffer<'js>, + offset: usize, + len: usize, +) -> Result> { + let raw = source + .as_raw() + .ok_or_else(|| Exception::throw_type(ctx, "cannot view a detached ArrayBuffer"))?; + debug_assert!( + offset.checked_add(len).is_some_and(|e| e <= raw.len), + "shared_array_buffer_view: slice out of range" + ); + let ptr = unsafe { raw.ptr.as_ptr().add(offset) }; + + // Dup the source's JSValue. The returned ArrayBuffer's free-callback + // (below) will drop this reference. + let ctx_ptr = ctx.as_raw().as_ptr(); + let rt = unsafe { qjs::JS_GetRuntime(ctx_ptr) }; + let source_val = unsafe { qjs::JS_DupValueRT(rt, source.as_value().as_raw()) }; + let opaque = Box::into_raw(Box::new(source_val)) as *mut c_void; + + extern "C" fn free_shared(rt: *mut qjs::JSRuntime, opaque: *mut c_void, _ptr: *mut c_void) { + // `opaque` is guaranteed non-null: the only QuickJS code path + // that loses it is `.transfer()`, which is blocked by the + // immutability flag we set below. + unsafe { + let boxed = Box::from_raw(opaque as *mut qjs::JSValue); + qjs::JS_FreeValueRT(rt, *boxed); + } + } + + let view = unsafe { + let val = qjs::JS_NewArrayBuffer( + ctx_ptr, + ptr, + len as _, + Some(free_shared), + opaque, + /*is_shared=*/ false, + ); + if qjs::JS_IsException(val) { + // QuickJS didn't take ownership of `opaque`; drop it ourselves. + let boxed = Box::from_raw(opaque as *mut qjs::JSValue); + qjs::JS_FreeValueRT(rt, *boxed); + return Err(ctx.throw(ctx.catch())); + } + let value = Value::from_raw(ctx.clone(), val); + ArrayBuffer::from_value(value) + .ok_or_else(|| Exception::throw_type(ctx, "expected ArrayBuffer"))? + }; + + set_immutable(&view); + Ok(view) +} diff --git a/stdlib/src/llrt/llrt_utils/bytearray_buffer.rs b/stdlib/src/llrt/llrt_utils/bytearray_buffer.rs new file mode 100644 index 00000000..b1f379b2 --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/bytearray_buffer.rs @@ -0,0 +1,227 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::{ + cmp::min, + collections::VecDeque, + sync::{ + atomic::{AtomicBool, AtomicUsize, Ordering}, + Arc, Mutex, + }, +}; + +use tokio::sync::{Notify, Semaphore}; + +#[derive(Clone)] +pub struct BytearrayBuffer { + inner: Arc>>, + max_capacity: Arc, + len: Arc, + notify: Arc, + closed: Arc, + write_semaphore: Arc, +} + +impl BytearrayBuffer { + pub fn new(capacity: usize) -> Self { + let queue = VecDeque::with_capacity(capacity); + let capacity = queue.capacity(); + Self { + inner: Arc::new(Mutex::new(queue)), + len: Arc::new(AtomicUsize::new(0)), + max_capacity: Arc::new(AtomicUsize::new(capacity)), + notify: Arc::new(Notify::new()), + closed: Arc::new(AtomicBool::new(false)), + write_semaphore: Arc::new(Semaphore::new(1)), + } + } + + pub fn len(&self) -> usize { + self.len.load(Ordering::Relaxed) + } + + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + #[allow(dead_code)] + pub fn write_forced(&self, item: &[u8]) { + let mut inner = self.inner.lock().unwrap(); + inner.extend(item); + let capacity = inner.capacity(); + self.len.fetch_add(item.len(), Ordering::Relaxed); + self.max_capacity.store(capacity, Ordering::Relaxed); + } + + pub async fn write(&self, item: &mut [u8]) -> usize { + let _ = self.write_semaphore.acquire().await.unwrap(); + let mut slice_index = 0; + loop { + let max_capacity = self.max_capacity.load(Ordering::Relaxed); + if self.closed.load(Ordering::Relaxed) { + return max_capacity; + } + + let len = self.len.load(Ordering::Relaxed); + + let available = max_capacity - len; + + if available > 0 { + let end_index = min(item.len() - 1, slice_index + available - 1); + let sub_slice = &item[slice_index..=end_index]; + let slice_length = sub_slice.len(); + slice_index += slice_length; + + self.inner.lock().unwrap().extend(sub_slice); + self.len.fetch_add(slice_length, Ordering::Relaxed); + + if slice_index == item.len() { + return max_capacity; + } + } + self.notify.notified().await; + } + } + + #[allow(dead_code)] + pub fn is_closed(&self) -> bool { + self.closed.load(Ordering::Relaxed) + } + + pub async fn close(&self) { + self.closed.store(true, Ordering::Relaxed); + self.notify.notify_one(); + //wait for write to finish + let _ = self.write_semaphore.acquire().await.unwrap(); + } + + pub async fn clear(&self) { + self.closed.store(false, Ordering::Relaxed); + self.notify.notify_one(); + //wait for write to finish + let _ = self.write_semaphore.acquire().await.unwrap(); + self.len.store(0, Ordering::Relaxed); + self.inner.lock().unwrap().clear(); + self.closed.store(false, Ordering::Relaxed); + } + + pub fn read(&self, desired_size: Option) -> Option> { + let mut inner = self.inner.lock().unwrap(); + let done = self.closed.load(Ordering::Relaxed); + + let items = if done { + Some(inner.drain(0..).collect()) + } else if let Some(desired_len) = desired_size { + let max_capacity = self.max_capacity.load(Ordering::Relaxed); + if desired_len > max_capacity { + let diff = desired_len - max_capacity; + inner.reserve(diff - 1); + let mut max_capacity = inner.capacity(); + if desired_len > max_capacity { + inner.reserve(desired_len - max_capacity); + max_capacity = inner.capacity(); + } + drop(inner); + self.max_capacity.store(max_capacity, Ordering::Relaxed); + self.notify.notify_one(); + return None; + } + + let len = self.len.load(Ordering::Relaxed); + if desired_len > len { + self.notify.notify_one(); + return None; + } + + Some(inner.drain(0..desired_len).collect()) + } else { + Some(inner.drain(0..).collect()) + }; + self.len.store(inner.len(), Ordering::Relaxed); + drop(inner); + self.notify.notify_one(); + items + } +} + +#[cfg(test)] +mod tests { + use super::BytearrayBuffer; + + #[tokio::test] + async fn clear_while_writing() { + let queue = BytearrayBuffer::new(8); + let queue2 = queue.clone(); + + tokio::task::spawn(async move { + let mut vec: Vec = (0..=255).collect(); + queue.write(&mut vec).await; + }); + + queue2.clear().await + } + + #[tokio::test] + async fn write_one_at_a_time() { + let queue = BytearrayBuffer::new(8); + let queue2 = queue.clone(); + let queue3 = queue.clone(); + + tokio::task::spawn(async move { + let mut vec: Vec = (0..=127).collect(); + queue.write(&mut vec).await; + }); + + tokio::task::spawn(async move { + let mut vec: Vec = (128..=255).collect(); + queue2.write(&mut vec).await; + }); + + let mut data = Vec::::new(); + + loop { + tokio::task::yield_now().await; + if let Some(bytes) = queue3.read(Some(256)) { + data.extend(bytes); + break; + } + } + + //assert that data in vec is increment from 0 to 255 + for i in 0..=255 { + assert_eq!(data[i as usize], i); + } + } + + #[tokio::test] + async fn queue() { + let queue = BytearrayBuffer::new(8); + let queue2 = queue.clone(); + + let write_task = tokio::task::spawn(async move { + for _ in 0..=255 { + let mut vec: Vec = (0..=255).collect(); + queue.write(&mut vec).await; + } + queue.close().await; + }); + + let mut data = Vec::::new(); + + loop { + let done = queue2.is_closed(); + + tokio::task::yield_now().await; + if let Some(bytes) = queue2.read(Some(9)) { + data.extend(bytes); + } + if done { + break; + } + } + + let _ = write_task.await; + + assert_eq!(data.len(), 256 * 256) + } +} diff --git a/stdlib/src/llrt/llrt_utils/bytes.rs b/stdlib/src/llrt/llrt_utils/bytes.rs new file mode 100644 index 00000000..8057f74a --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/bytes.rs @@ -0,0 +1,679 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::{rc::Rc, slice}; + +use half::f16; +use rquickjs::{ + atom::PredefinedAtom, + class::{Trace, Tracer}, + function::Constructor, + ArrayBuffer, Coerced, Ctx, Error, Exception, FromJs, IntoJs, JsLifetime, Object, Result, + TypedArray, U8Clamped, Value, +}; + +/// Convert a JS string to a `String`, replacing lone UTF-16 surrogates +/// with U+FFFD per WHATWG USVString. Use when ill-formed strings must +/// not fail. +// +// SAFETY (module-wide): QuickJS only emits valid WTF-8, so any run +// without 0xED is valid strict UTF-8. +pub fn get_lossy_string(string_value: Value) -> Result { + let js_str = string_value.into_string().ok_or_else(|| Error::FromJs { + from: "Value", + to: "JSString", + message: Some("Value is not a string".into()), + })?; + let cstr = js_str.to_cstring()?; + let bytes = unsafe { slice::from_raw_parts(cstr.as_ptr() as *const u8, cstr.len()) }; + + let first = match memchr::memchr(0xED, bytes) { + None => return Ok(unsafe { String::from_utf8_unchecked(bytes.to_vec()) }), + Some(idx) => idx, + }; + let mut result = String::with_capacity(bytes.len()); + result.push_str(unsafe { std::str::from_utf8_unchecked(&bytes[..first]) }); + qjs_substitute_into(&bytes[first..], &mut result); + Ok(result) +} + +fn qjs_substitute_into(bytes: &[u8], result: &mut String) { + let mut start = 0; + while start < bytes.len() { + let next_ed = match memchr::memchr(0xED, &bytes[start..]) { + None => { + result.push_str(unsafe { std::str::from_utf8_unchecked(&bytes[start..]) }); + return; + } + Some(rel) => start + rel, + }; + if next_ed > start { + result.push_str(unsafe { std::str::from_utf8_unchecked(&bytes[start..next_ed]) }); + } + if next_ed + 3 > bytes.len() { + replace_invalid_utf8_and_utf16_into(&bytes[next_ed..], result); + return; + } + let b1 = bytes[next_ed + 1]; + let b2 = bytes[next_ed + 2]; + if (b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 { + replace_invalid_utf8_and_utf16_into(&bytes[next_ed..], result); + return; + } + if (b1 & 0xE0) == 0xA0 { + result.push('\u{FFFD}'); + } else { + result.push_str(unsafe { std::str::from_utf8_unchecked(&bytes[next_ed..next_ed + 3]) }); + } + start = next_ed + 3; + } +} + +#[doc(hidden)] +pub fn replace_invalid_utf8_and_utf16(bytes: &[u8]) -> String { + let err = match simdutf8::compat::from_utf8(bytes) { + Ok(s) => return s.to_owned(), + Err(e) => e, + }; + let valid_up_to = err.valid_up_to(); + let mut result = String::with_capacity(bytes.len()); + result.push_str(unsafe { std::str::from_utf8_unchecked(&bytes[..valid_up_to]) }); + replace_invalid_utf8_and_utf16_into(&bytes[valid_up_to..], &mut result); + result +} + +fn replace_invalid_utf8_and_utf16_into(bytes: &[u8], result: &mut String) { + let mut i = 0; + + while i < bytes.len() { + let current = bytes[i]; + match current { + 0x00..=0x7F => { + result.push(current as char); + i += 1; + } + 0xC0..=0xDF if i + 1 < bytes.len() => { + let next = bytes[i + 1]; + if (next & 0xC0) == 0x80 { + let code_point = ((current as u32 & 0x1F) << 6) | (next as u32 & 0x3F); + result.push(char::from_u32(code_point).unwrap_or('\u{FFFD}')); + i += 2; + } else { + result.push('\u{FFFD}'); + i += 1; + } + } + 0xE0..=0xEF if i + 2 < bytes.len() => { + let next1 = bytes[i + 1]; + let next2 = bytes[i + 2]; + if (next1 & 0xC0) == 0x80 && (next2 & 0xC0) == 0x80 { + let code_point = ((current as u32 & 0x0F) << 12) + | ((next1 as u32 & 0x3F) << 6) + | (next2 as u32 & 0x3F); + result.push(char::from_u32(code_point).unwrap_or('\u{FFFD}')); + i += 3; + } else { + result.push('\u{FFFD}'); + i += 1; + } + } + 0xF0..=0xF7 if i + 3 < bytes.len() => { + let next1 = bytes[i + 1]; + let next2 = bytes[i + 2]; + let next3 = bytes[i + 3]; + if (next1 & 0xC0) == 0x80 && (next2 & 0xC0) == 0x80 && (next3 & 0xC0) == 0x80 { + let code_point = ((current as u32 & 0x07) << 18) + | ((next1 as u32 & 0x3F) << 12) + | ((next2 as u32 & 0x3F) << 6) + | (next3 as u32 & 0x3F); + result.push(char::from_u32(code_point).unwrap_or('\u{FFFD}')); + i += 4; + } else { + result.push('\u{FFFD}'); + i += 1; + } + } + _ => { + result.push('\u{FFFD}'); + i += 1; + } + } + } +} + +#[cfg(test)] +mod replace_invalid_utf8_tests { + use super::replace_invalid_utf8_and_utf16; + + fn cases() -> Vec<(&'static str, Vec, &'static str)> { + vec![ + ("empty", vec![], ""), + ("ascii", b"hello world".to_vec(), "hello world"), + ( + "ascii_with_control", + vec![b'a', 0x00, b'b', 0x7f, b'c'], + "a\u{0}b\u{7f}c", + ), + ("two_byte_latin1", vec![0xC3, 0xA9], "\u{00E9}"), + ("three_byte_cjk", vec![0xE4, 0xB8, 0x96], "\u{4e16}"), + ("four_byte_emoji", vec![0xF0, 0x9F, 0xA6, 0x80], "\u{1f980}"), + ("lone_high_surrogate", vec![0xED, 0xA0, 0xBD], "\u{FFFD}"), + ("lone_low_surrogate", vec![0xED, 0xB0, 0x80], "\u{FFFD}"), + ( + "surrogate_pair_in_wtf8", + vec![0xED, 0xA0, 0xBD, 0xED, 0xB2, 0xA9], + "\u{FFFD}\u{FFFD}", + ), + ("stray_continuation", vec![0x80], "\u{FFFD}"), + ("truncated_two_byte", vec![0xC3], "\u{FFFD}"), + ("truncated_three_byte", vec![0xE0, 0xA0], "\u{FFFD}\u{FFFD}"), + ( + "truncated_four_byte", + vec![0xF0, 0x9F, 0xA6], + "\u{FFFD}\u{FFFD}\u{FFFD}", + ), + ( + "two_byte_bad_continuation", + vec![0xC3, 0x20, b'a'], + "\u{FFFD} a", + ), + ( + "three_byte_bad_continuation", + vec![0xE4, 0xB8, 0x20, b'a'], + "\u{FFFD}\u{FFFD} a", + ), + ("high_byte_above_f7", vec![0xF8, b'a'], "\u{FFFD}a"), + ( + "mixed_valid_and_invalid", + { + let mut v = b"hello ".to_vec(); + v.extend_from_slice(&[0xED, 0xA0, 0xBD]); + v.extend_from_slice(" world".as_bytes()); + v + }, + "hello \u{FFFD} world", + ), + ( + "long_ascii", + b"the quick brown fox jumps over the lazy dog".repeat(20), + &*Box::leak( + "the quick brown fox jumps over the lazy dog" + .repeat(20) + .into_boxed_str(), + ), + ), + ] + } + + #[test] + fn matches_contract() { + for (name, input, expected) in cases() { + let got = replace_invalid_utf8_and_utf16(&input); + assert_eq!( + got, expected, + "case `{}`: got {:?}, expected {:?}", + name, got, expected + ); + } + } +} + +use crate::llrt_utils::{error_messages::ERROR_MSG_ARRAY_BUFFER_DETACHED, result::ResultExt}; + +#[derive(Clone, PartialEq)] +pub enum ObjectBytes<'js> { + U8Array(TypedArray<'js, u8>), + I8Array(TypedArray<'js, i8>), + U16Array(TypedArray<'js, u16>), + I16Array(TypedArray<'js, i16>), + U32Array(TypedArray<'js, u32>), + I32Array(TypedArray<'js, i32>), + U64Array(TypedArray<'js, u64>), + I64Array(TypedArray<'js, i64>), + F16Array(TypedArray<'js, f16>), + F32Array(TypedArray<'js, f32>), + F64Array(TypedArray<'js, f64>), + U8ClampedArray(TypedArray<'js, U8Clamped>), + DataView(ArrayBuffer<'js>, usize, usize), // buffer, offset, length + Vec(Vec), +} + +// Requires manual implementation because rquickjs hasn't implemented JsLifetime for f32 or f64 +unsafe impl<'js> JsLifetime<'js> for ObjectBytes<'js> { + type Changed<'to> = ObjectBytes<'to>; +} + +impl<'js> Trace<'js> for ObjectBytes<'js> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + match self { + ObjectBytes::U8Array(a) => a.trace(tracer), + ObjectBytes::I8Array(a) => a.trace(tracer), + ObjectBytes::U16Array(a) => a.trace(tracer), + ObjectBytes::I16Array(a) => a.trace(tracer), + ObjectBytes::U32Array(a) => a.trace(tracer), + ObjectBytes::I32Array(a) => a.trace(tracer), + ObjectBytes::U64Array(a) => a.trace(tracer), + ObjectBytes::I64Array(a) => a.trace(tracer), + ObjectBytes::F16Array(a) => a.trace(tracer), + ObjectBytes::F32Array(a) => a.trace(tracer), + ObjectBytes::F64Array(a) => a.trace(tracer), + ObjectBytes::U8ClampedArray(a) => a.trace(tracer), + ObjectBytes::DataView(ab, _, _) => ab.trace(tracer), + ObjectBytes::Vec(v) => v.trace(tracer), + } + } +} + +impl<'js> IntoJs<'js> for ObjectBytes<'js> { + fn into_js(self, ctx: &Ctx<'js>) -> Result> { + match self { + ObjectBytes::U8Array(a) => a.into_js(ctx), + ObjectBytes::I8Array(a) => a.into_js(ctx), + ObjectBytes::U16Array(a) => a.into_js(ctx), + ObjectBytes::I16Array(a) => a.into_js(ctx), + ObjectBytes::U32Array(a) => a.into_js(ctx), + ObjectBytes::I32Array(a) => a.into_js(ctx), + ObjectBytes::U64Array(a) => a.into_js(ctx), + ObjectBytes::I64Array(a) => a.into_js(ctx), + ObjectBytes::F16Array(a) => a.into_js(ctx), + ObjectBytes::F32Array(a) => a.into_js(ctx), + ObjectBytes::F64Array(a) => a.into_js(ctx), + ObjectBytes::U8ClampedArray(a) => a.into_js(ctx), + ObjectBytes::DataView(ab, _, _) => { + let ctor: Constructor = ctx.globals().get(PredefinedAtom::DataView)?; + ctor.construct((ab,)) + } + ObjectBytes::Vec(v) => v.into_js(ctx), + } + } +} + +impl<'js> TryFrom> for Vec { + type Error = Rc; + fn try_from(value: ObjectBytes<'js>) -> std::result::Result { + value.into_bytes_inner() + } +} + +impl<'a, 'js> TryFrom<&'a ObjectBytes<'js>> for &'a [u8] { + type Error = Rc; + fn try_from(value: &'a ObjectBytes<'js>) -> std::result::Result { + value.as_bytes_inner() + } +} + +impl<'js> FromJs<'js> for ObjectBytes<'js> { + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + Self::from_offset(ctx, &value, 0, None) + } +} + +impl<'js> ObjectBytes<'js> { + pub fn from(ctx: &Ctx<'js>, value: &Value<'js>) -> Result { + Self::from_offset(ctx, value, 0, None) + } + + pub fn from_offset( + ctx: &Ctx<'js>, + value: &Value<'js>, + offset: usize, + length: Option, + ) -> Result { + if value.is_undefined() { + return Ok(ObjectBytes::Vec(vec![])); + } + if let Some(bytes) = get_string_bytes(value, offset, length)? { + return Ok(ObjectBytes::Vec(bytes)); + } + if let Some(bytes) = get_array_bytes(value, offset, length)? { + return Ok(ObjectBytes::Vec(bytes)); + } + + if let Some(obj) = value.as_object() { + if let Some(bytes) = Self::from_array_buffer(obj)? { + return Ok(bytes); + } + } + + if let Some(bytes) = get_coerced_string_bytes(value, offset, length) { + return Ok(ObjectBytes::Vec(bytes)); + } + + Err(Exception::throw_message( + ctx, + "value must be typed DataView, Buffer, ArrayBuffer, Uint8Array or interpretable as string", + )) + } + + pub fn as_bytes(&self, ctx: &Ctx<'js>) -> Result<&[u8]> { + self.as_bytes_inner().or_throw(ctx) + } + + /// Returns the underlying bytes, or `None` if the buffer is detached or + /// the DataView range is invalid (including arithmetic overflow). Unlike + /// [`as_bytes`], does not raise a JS exception. + pub fn as_bytes_opt(&self) -> Option<&[u8]> { + self.as_bytes_inner().ok() + } + + fn as_bytes_inner(&self) -> std::result::Result<&[u8], Rc> { + match self { + ObjectBytes::U8Array(array) => array.as_bytes(), + ObjectBytes::I8Array(array) => array.as_bytes(), + ObjectBytes::U16Array(array) => array.as_bytes(), + ObjectBytes::I16Array(array) => array.as_bytes(), + ObjectBytes::U32Array(array) => array.as_bytes(), + ObjectBytes::I32Array(array) => array.as_bytes(), + ObjectBytes::U64Array(array) => array.as_bytes(), + ObjectBytes::I64Array(array) => array.as_bytes(), + ObjectBytes::F16Array(array) => array.as_bytes(), + ObjectBytes::F32Array(array) => array.as_bytes(), + ObjectBytes::F64Array(array) => array.as_bytes(), + ObjectBytes::U8ClampedArray(array) => array.as_bytes(), + ObjectBytes::DataView(ab, offset, length) => ab.as_bytes().and_then(|bytes| { + let end = offset.checked_add(*length)?; + bytes.get(*offset..end) + }), + ObjectBytes::Vec(bytes) => Some(bytes.as_ref()), + } + .ok_or(ERROR_MSG_ARRAY_BUFFER_DETACHED.into()) + } + + pub fn into_bytes(self, ctx: &Ctx<'_>) -> Result> { + self.into_bytes_inner().or_throw(ctx) + } + + fn into_bytes_inner(self) -> std::result::Result, Rc> { + if let ObjectBytes::Vec(bytes) = self { + return Ok(bytes); + } + Ok(self.as_bytes_inner()?.to_vec()) + } + + pub fn from_array_buffer(obj: &Object<'js>) -> Result>> { + //most common + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + return Ok(Some(ObjectBytes::U8Array(typed_array))); + } + //second most common + if let Some(array_buffer) = ArrayBuffer::from_object(obj.clone()) { + let len = array_buffer.len(); + return Ok(Some(ObjectBytes::DataView(array_buffer, 0, len))); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + return Ok(Some(ObjectBytes::I8Array(typed_array))); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + return Ok(Some(ObjectBytes::U16Array(typed_array))); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + return Ok(Some(ObjectBytes::I16Array(typed_array))); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + return Ok(Some(ObjectBytes::U32Array(typed_array))); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + return Ok(Some(ObjectBytes::I32Array(typed_array))); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + return Ok(Some(ObjectBytes::U64Array(typed_array))); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + return Ok(Some(ObjectBytes::I64Array(typed_array))); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + return Ok(Some(ObjectBytes::F16Array(typed_array))); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + return Ok(Some(ObjectBytes::F32Array(typed_array))); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + return Ok(Some(ObjectBytes::F64Array(typed_array))); + } + + if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { + return Ok(Some(ObjectBytes::U8ClampedArray(typed_array))); + } + + if let Ok(ab) = obj.get::<_, ArrayBuffer>("buffer") { + let offset: usize = obj.get("byteOffset").unwrap_or(0); + let length: usize = obj.get("byteLength").unwrap_or_else(|_| ab.len()); + return Ok(Some(ObjectBytes::DataView(ab, offset, length))); + } + + Ok(None) + } + + pub fn get_array_buffer(&self) -> Result, usize, usize)>> { + let buffer = match self { + ObjectBytes::U8Array(typed_array) => { + let byte_length = typed_array.len(); + ( + typed_array.arraybuffer()?, + byte_length, + typed_array.get("byteOffset")?, + ) + } + ObjectBytes::I8Array(typed_array) => { + let byte_length = typed_array.len(); + ( + typed_array.arraybuffer()?, + byte_length, + typed_array.get("byteOffset")?, + ) + } + ObjectBytes::U16Array(typed_array) => { + let byte_length = typed_array.len() * 2; + ( + typed_array.arraybuffer()?, + byte_length, + typed_array.get("byteOffset")?, + ) + } + ObjectBytes::I16Array(typed_array) => { + let byte_length = typed_array.len() * 2; + ( + typed_array.arraybuffer()?, + byte_length, + typed_array.get("byteOffset")?, + ) + } + ObjectBytes::U32Array(typed_array) => { + let byte_length = typed_array.len() * 4; + ( + typed_array.arraybuffer()?, + byte_length, + typed_array.get("byteOffset")?, + ) + } + ObjectBytes::I32Array(typed_array) => { + let byte_length = typed_array.len() * 4; + ( + typed_array.arraybuffer()?, + byte_length, + typed_array.get("byteOffset")?, + ) + } + ObjectBytes::U64Array(typed_array) => { + let byte_length = typed_array.len() * 8; + ( + typed_array.arraybuffer()?, + byte_length, + typed_array.get("byteOffset")?, + ) + } + ObjectBytes::I64Array(typed_array) => { + let byte_length = typed_array.len() * 8; + ( + typed_array.arraybuffer()?, + byte_length, + typed_array.get("byteOffset")?, + ) + } + ObjectBytes::F16Array(typed_array) => { + let byte_length = typed_array.len() * 2; + ( + typed_array.arraybuffer()?, + byte_length, + typed_array.get("byteOffset")?, + ) + } + ObjectBytes::F32Array(typed_array) => { + let byte_length = typed_array.len() * 4; + ( + typed_array.arraybuffer()?, + byte_length, + typed_array.get("byteOffset")?, + ) + } + ObjectBytes::F64Array(typed_array) => { + let byte_length = typed_array.len() * 8; + ( + typed_array.arraybuffer()?, + byte_length, + typed_array.get("byteOffset")?, + ) + } + ObjectBytes::U8ClampedArray(typed_array) => { + let byte_length = typed_array.len(); + ( + typed_array.arraybuffer()?, + byte_length, + typed_array.get("byteOffset")?, + ) + } + ObjectBytes::DataView(array_buffer, offset, length) => { + (array_buffer.clone(), *length, *offset) + } + _ => return Ok(None), + }; + + Ok(Some(buffer)) + } +} + +#[cfg(test)] +mod object_bytes_tests { + use super::{ObjectBytes, ERROR_MSG_ARRAY_BUFFER_DETACHED}; + use rquickjs::{ArrayBuffer, Context, Runtime}; + + #[test] + fn data_view_ranges_are_checked() { + let rt = Runtime::new().unwrap(); + let ctx = Context::full(&rt).unwrap(); + + ctx.with(|ctx| { + let buffer = ArrayBuffer::new_copy(ctx, [1_u8, 2, 3, 4]).unwrap(); + for (offset, length) in [(3, 2), (usize::MAX, 1)] { + let bytes = ObjectBytes::DataView(buffer.clone(), offset, length); + + assert_eq!( + bytes.as_bytes_inner().unwrap_err().as_ref(), + ERROR_MSG_ARRAY_BUFFER_DETACHED + ); + } + + let valid_bytes = ObjectBytes::DataView(buffer, 1, 2); + assert_eq!(valid_bytes.as_bytes_inner().unwrap(), &[2, 3]); + }); + } + + #[test] + fn data_view_detached_buffer_returns_error() { + let rt = Runtime::new().unwrap(); + let ctx = Context::full(&rt).unwrap(); + + ctx.with(|ctx| { + let mut buffer = ArrayBuffer::new_copy(ctx, [1_u8, 2, 3, 4]).unwrap(); + buffer.detach(); + let bytes = ObjectBytes::DataView(buffer, 0, 4); + + assert_eq!( + bytes.as_bytes_inner().unwrap_err().as_ref(), + ERROR_MSG_ARRAY_BUFFER_DETACHED + ); + }); + } +} + +pub fn get_start_end_indexes( + source_len: usize, + target_len: Option, + offset: usize, +) -> (usize, usize) { + if offset > source_len { + return (0, 0); + } + + let target_len = target_len.unwrap_or(source_len - offset); + + if offset + target_len > source_len { + return (offset, source_len); + } + + (offset, target_len + offset) +} + +pub fn get_array_bytes( + value: &Value<'_>, + offset: usize, + length: Option, +) -> Result>> { + if value.is_array() { + let array = value.as_array().unwrap(); + let (start, end) = get_start_end_indexes(array.len(), length, offset); + let size = end - start; + let mut bytes: Vec = Vec::with_capacity(size); + + for val in array.iter::().skip(start).take(size) { + let val: u8 = val?; + bytes.push(val); + } + + return Ok(Some(bytes)); + } + Ok(None) +} + +pub fn get_coerced_string_bytes( + value: &Value<'_>, + offset: usize, + length: Option, +) -> Option> { + if let Ok(val) = value.get::>() { + return Some(bytes_from_js_string(val.0, offset, length)); + }; + None +} + +fn bytes_from_js_string(string: String, offset: usize, length: Option) -> Vec { + let (start, end) = get_start_end_indexes(string.len(), length, offset); + string.as_bytes()[start..end].to_vec() +} + +#[inline] +pub fn get_string_bytes( + value: &Value<'_>, + offset: usize, + length: Option, +) -> Result>> { + if value.is_string() { + let string = get_lossy_string(value.clone())?; + return Ok(Some(bytes_from_js_string(string, offset, length))); + } + Ok(None) +} + +pub fn bytes_to_typed_array<'js>(ctx: Ctx<'js>, bytes: &[u8]) -> Result> { + TypedArray::::new(ctx.clone(), bytes).into_js(&ctx) +} diff --git a/stdlib/src/llrt/llrt_utils/class.rs b/stdlib/src/llrt/llrt_utils/class.rs new file mode 100644 index 00000000..72f13ad7 --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/class.rs @@ -0,0 +1,126 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use rquickjs::{ + atom::PredefinedAtom, class::JsClass, object::Accessor, object::Property, prelude::This, Array, + Class, Ctx, Function, Object, Result, Symbol, Value, +}; + +use super::{ + object::ObjectExt, + primordials::{BasePrimordials, Primordial}, + result::OptionExt, +}; + +pub static CUSTOM_INSPECT_SYMBOL_DESCRIPTION: &str = "llrt.inspect.custom"; + +/// Which view an iterator yields: `keys()`, `values()`, or `entries()`. +#[derive(Clone, Copy)] +pub enum IterKind { + Keys, + Values, + Entries, +} + +/// Wrap an entry into a `{ value, done }` iterator result. `None` means done. +pub fn iterator_result<'js>( + ctx: &Ctx<'js>, + kind: IterKind, + entry: Option<(Value<'js>, Value<'js>)>, +) -> Result> { + let obj = Object::new(ctx.clone())?; + match entry { + Some((key, value)) => { + obj.set(PredefinedAtom::Done, false)?; + match kind { + IterKind::Keys => obj.set(PredefinedAtom::Value, key)?, + IterKind::Values => obj.set(PredefinedAtom::Value, value)?, + IterKind::Entries => { + let entry = Array::new(ctx.clone())?; + entry.set(0, key)?; + entry.set(1, value)?; + obj.set(PredefinedAtom::Value, entry)?; + } + } + } + None => obj.set(PredefinedAtom::Done, true)?, + } + Ok(obj) +} + +/// Create a WebIDL iterator instance, wiring its prototype the first time: +/// the prototype inherits `%IteratorPrototype%` (so it's tagged +/// `[object Iterator]`) and `next` becomes enumerable. Idempotent — later +/// calls skip the setup — so callers just build iterators and never register +/// anything separately. +pub fn live_iterator<'js, C>(ctx: &Ctx<'js>, iter: C) -> Result> +where + C: JsClass<'js> + 'js, +{ + let instance = Class::::instance(ctx.clone(), iter)?; + if let Some(proto) = Class::::prototype(ctx)? { + let iterator_proto = &BasePrimordials::get(ctx)?.prototype_iterator; + if proto.get_prototype().as_ref() != Some(iterator_proto) { + proto.set_prototype(Some(iterator_proto))?; + let next_fn: Function = proto.get(PredefinedAtom::Next)?; + proto.prop( + PredefinedAtom::Next, + Property::from(next_fn) + .writable() + .enumerable() + .configurable(), + )?; + } + } + Ok(instance) +} + +pub fn get_class_name(value: &Value) -> Result> { + value + .get_optional::<_, Object>(PredefinedAtom::Constructor)? + .and_then_ok(|ctor| ctor.get_optional::<_, String>(PredefinedAtom::Name)) +} + +#[inline(always)] +pub fn get_class<'js, C>(provided: &Value<'js>) -> Result>> +where + C: JsClass<'js>, +{ + if provided + .as_object() + .map(|p| p.instance_of::()) + .unwrap_or_default() + { + return Ok(Some(Class::::from_value(provided)?)); + } + Ok(None) +} + +pub trait CustomInspectExtension<'js> { + fn define_with_custom_inspect(globals: &Object<'js>) -> Result<()>; +} + +pub trait CustomInspect<'js> +where + Self: JsClass<'js>, +{ + fn custom_inspect(&self, ctx: Ctx<'js>) -> Result>; +} + +impl<'js, C> CustomInspectExtension<'js> for Class<'js, C> +where + C: JsClass<'js> + CustomInspect<'js> + 'js, +{ + fn define_with_custom_inspect(globals: &Object<'js>) -> Result<()> { + Self::define(globals)?; + let custom_inspect_symbol = + Symbol::new_global(globals.ctx().clone(), CUSTOM_INSPECT_SYMBOL_DESCRIPTION)?; + if let Some(proto) = Class::::prototype(globals.ctx())? { + proto.prop( + custom_inspect_symbol, + Accessor::from(|this: This>, ctx| this.borrow().custom_inspect(ctx)), + )?; + } + Ok(()) + } +} diff --git a/stdlib/src/llrt/llrt_utils/clone.rs b/stdlib/src/llrt/llrt_utils/clone.rs new file mode 100644 index 00000000..50c0b00e --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/clone.rs @@ -0,0 +1,21 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use rquickjs::{class::JsClass, Class, Ctx, Object, Result, Value}; + +pub trait StructuredClone<'js>: JsClass<'js> { + fn structured_clone(&self, ctx: &Ctx<'js>) -> Result>; +} + +pub fn clone_platform_object<'js, T>( + ctx: &Ctx<'js>, + object: &Object<'js>, +) -> Result>> +where + T: StructuredClone<'js>, +{ + if let Some(class) = Class::::from_object(object) { + return Ok(Some(class.borrow().structured_clone(ctx)?)); + } + Ok(None) +} diff --git a/stdlib/src/llrt/llrt_utils/ctx.rs b/stdlib/src/llrt/llrt_utils/ctx.rs new file mode 100644 index 00000000..efbf1f31 --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/ctx.rs @@ -0,0 +1,18 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use rquickjs::{Ctx, Result}; + +pub trait CtxExt { + fn get_script_or_module_name(&self) -> Result; +} + +impl CtxExt for Ctx<'_> { + fn get_script_or_module_name(&self) -> Result { + if let Some(name) = self.script_or_module_name(0) { + name.to_string() + } else { + Ok(String::from(".")) + } + } +} diff --git a/stdlib/src/llrt/llrt_utils/error.rs b/stdlib/src/llrt/llrt_utils/error.rs new file mode 100644 index 00000000..915e3072 --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/error.rs @@ -0,0 +1,24 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use rquickjs::{CatchResultExt, CaughtError, Ctx, Error, IntoJs, Result, Value}; + +pub trait ErrorExtensions<'js> { + fn into_value(self, ctx: &Ctx<'js>) -> Result>; +} + +impl<'js> ErrorExtensions<'js> for Error { + fn into_value(self, ctx: &Ctx<'js>) -> Result> { + Err::<(), _>(self).catch(ctx).unwrap_err().into_value(ctx) + } +} + +impl<'js> ErrorExtensions<'js> for CaughtError<'js> { + fn into_value(self, ctx: &Ctx<'js>) -> Result> { + Ok(match self { + CaughtError::Error(err) => err.to_string().into_js(ctx)?, + CaughtError::Exception(ex) => ex.into_value(), + CaughtError::Value(val) => val, + }) + } +} diff --git a/stdlib/src/llrt/llrt_utils/error_messages.rs b/stdlib/src/llrt/llrt_utils/error_messages.rs new file mode 100644 index 00000000..60e4b56e --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/error_messages.rs @@ -0,0 +1,4 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +pub const ERROR_MSG_NOT_ARRAY_BUFFER: &str = "Not an ArrayBuffer"; +pub const ERROR_MSG_ARRAY_BUFFER_DETACHED: &str = "ArrayBuffer is detached"; +pub const ERROR_MSG_BROADCAST_LAGGED: &str = "Lagged too much behind"; diff --git a/stdlib/src/llrt/llrt_utils/fs.rs b/stdlib/src/llrt/llrt_utils/fs.rs new file mode 100644 index 00000000..3e349e0e --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/fs.rs @@ -0,0 +1,104 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::{fs::Metadata, io, path::PathBuf}; + +use tokio::fs::{self}; + +pub struct DirectoryWalker +where + T: Fn(&str) -> bool, +{ + stack: Vec<(PathBuf, Option)>, + filter: T, + recursive: bool, + eat_root: bool, +} + +impl DirectoryWalker +where + T: Fn(&str) -> bool, +{ + pub fn new(root: PathBuf, filter: T) -> Self { + Self { + stack: vec![(root, None)], + filter, + recursive: false, + eat_root: true, + } + } + + pub fn set_recursive(&mut self, recursive: bool) { + self.recursive = recursive; + } + + pub async fn walk(&mut self) -> io::Result> { + if self.eat_root { + self.eat_root = false; + let (dir, _) = self.stack.pop().unwrap(); + self.append_stack(&dir).await?; + } + if let Some((entry, metadata)) = self.stack.pop() { + let metadata = metadata.unwrap(); + if self.recursive && metadata.is_dir() { + self.append_stack(&entry).await?; + } + + Ok(Some((entry, metadata))) + } else { + Ok(None) + } + } + + pub fn walk_sync(&mut self) -> io::Result> { + if self.eat_root { + self.eat_root = false; + let (dir, _) = self.stack.pop().unwrap(); + self.append_stack_sync(&dir)?; + } + if let Some((entry, metadata)) = self.stack.pop() { + let metadata = metadata.unwrap(); + if self.recursive && metadata.is_dir() { + self.append_stack_sync(&entry)?; + } + + Ok(Some((entry, metadata))) + } else { + Ok(None) + } + } + + async fn append_stack(&mut self, dir: &PathBuf) -> io::Result<()> { + let mut stream = fs::read_dir(dir).await?; + + while let Some(entry) = stream.next_entry().await? { + let name = entry.file_name(); + let name = name.to_string_lossy(); + if !(self.filter)(name.as_ref()) { + continue; + } + let entry_path = entry.path(); + let metadata = fs::symlink_metadata(&entry_path).await?; + + self.stack.push((entry_path, Some(metadata))); + } + Ok(()) + } + + fn append_stack_sync(&mut self, dir: &PathBuf) -> io::Result<()> { + let dir = std::fs::read_dir(dir)?; + + for entry in dir.flatten() { + let name = entry.file_name(); + let name = name.to_string_lossy(); + if !(self.filter)(name.as_ref()) { + continue; + } + let entry_path = entry.path(); + let metadata = entry_path.symlink_metadata()?; + self.stack.push((entry_path, Some(metadata))) + } + + Ok(()) + } +} diff --git a/stdlib/src/llrt/llrt_utils/hash.rs b/stdlib/src/llrt/llrt_utils/hash.rs new file mode 100644 index 00000000..5b3d17d3 --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/hash.rs @@ -0,0 +1,9 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use std::hash::{DefaultHasher, Hash, Hasher}; + +#[inline] +pub fn default_hash(v: &T) -> usize { + let mut state = DefaultHasher::default(); + v.hash(&mut state); + state.finish() as usize +} diff --git a/stdlib/src/llrt/llrt_utils/io.rs b/stdlib/src/llrt/llrt_utils/io.rs new file mode 100644 index 00000000..baefaea8 --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/io.rs @@ -0,0 +1,31 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +macro_rules! define_extension { + ($base:ident, $file:ident, $ext:expr) => { + #[allow(dead_code)] + pub const $base: &str = $ext; + #[allow(dead_code)] + pub const $file: &str = concat!(".", $ext); + }; +} + +define_extension!(BYTECODE_EXT, BYTECODE_FILE_EXT, "lrt"); + +macro_rules! define_supported_extensions { + // Accepts a list of supported extensions and a single additional constant extension + ($constant_ext:ident, $($ext:literal),*) => { + // Define the array of extensions as a constant + pub const SUPPORTED_EXTENSIONS: &[&str] = &[$($ext),*, $constant_ext]; + + pub const JS_EXTENSIONS: &[&str] = &[$($ext),*]; + + // Define the function `is_supported_ext` using a match statement + pub fn is_supported_ext(ext: &str) -> bool { + matches!(ext, $($ext)|* | $constant_ext) + } + }; +} + +define_supported_extensions!(BYTECODE_FILE_EXT, ".js", ".mjs", ".cjs"); diff --git a/stdlib/src/llrt/llrt_utils/latch.rs b/stdlib/src/llrt/llrt_utils/latch.rs new file mode 100644 index 00000000..1fdda570 --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/latch.rs @@ -0,0 +1,31 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::sync::atomic::{AtomicUsize, Ordering}; + +use tokio::sync::Notify; + +#[derive(Default)] +pub struct Latch { + count: AtomicUsize, + notify: Notify, +} + +impl Latch { + pub fn increment(&self) { + self.count.fetch_add(1, Ordering::Relaxed); + } + + pub fn decrement(&self) { + let previous = self.count.fetch_sub(1, Ordering::Relaxed); + if previous == 1 { + self.notify.notify_waiters(); + } + } + + pub async fn wait(&self) { + if self.count.load(Ordering::Relaxed) > 0 { + self.notify.notified().await; + } + } +} diff --git a/stdlib/src/llrt/llrt_utils/lib.rs b/stdlib/src/llrt/llrt_utils/lib.rs new file mode 100644 index 00000000..32b92a2b --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/lib.rs @@ -0,0 +1,37 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +pub mod any_of; +pub mod array_buffer; +#[cfg(any())] +pub mod bytearray_buffer; +pub mod bytes; +pub mod class; +pub mod clone; +pub mod ctx; +pub mod error; +pub mod error_messages; +#[cfg(any())] +pub mod fs; +pub mod hash; +pub mod io; +pub mod latch; +pub mod macros; +pub mod mc_oneshot; +pub mod module; +pub mod object; +pub mod option; +pub mod primordials; +pub mod provider; +pub mod result; +pub mod reuse_list; +pub mod string; +pub mod sysinfo; +pub mod time; + +pub mod signals; + +pub const VERSION: &str = "0.9.0-beta"; + +// Macro exports move to the combined crate root. +pub(crate) use crate::{count_members, iterable_enum, str_enum}; diff --git a/stdlib/src/llrt/llrt_utils/macros.rs b/stdlib/src/llrt/llrt_utils/macros.rs new file mode 100644 index 00000000..85a8c0d3 --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/macros.rs @@ -0,0 +1,56 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#[macro_export] +macro_rules! count_members { + () => (0); + ($head:tt $(,$tail:tt)*) => (1 + count_members!($($tail),*)); +} + +#[macro_export] +macro_rules! iterable_enum { + ($name:ident, $($variant:ident),*) => { + impl $name { + const VARIANTS: &'static [$name] = &[$($name::$variant,)*]; + pub fn iter() -> std::slice::Iter<'static, $name> { + Self::VARIANTS.iter() + } + + #[allow(dead_code)] + fn _ensure_all_variants(s: Self) { + match s { + $($name::$variant => {},)* + } + } + } + }; +} + +#[macro_export] +macro_rules! str_enum { + ($name:ident, $($variant:ident => $str:expr),*) => { + impl $name { + pub fn as_str(&self) -> &'static str { + match self { + $($name::$variant => $str,)* + } + } + } + + impl AsRef for $name { + fn as_ref(&self) -> &str { + self.as_str() + } + } + + impl TryFrom<&str> for $name { + type Error = String; + fn try_from(s: &str) -> std::result::Result { + match s { + $($str => Ok($name::$variant),)* + _ => Err(["'", s, "' not available"].concat()) + } + } + } + }; +} diff --git a/stdlib/src/llrt/llrt_utils/mc_oneshot.rs b/stdlib/src/llrt/llrt_utils/mc_oneshot.rs new file mode 100644 index 00000000..dac3be5c --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/mc_oneshot.rs @@ -0,0 +1,119 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, RwLock, +}; + +use rquickjs::{ + class::{Trace, Tracer}, + Value, +}; +use std::ops::Deref; +use tokio::sync::Notify; + +#[derive(Debug)] +pub struct Shared { + is_sent: AtomicBool, + value: RwLock>, + notify: Notify, +} + +#[derive(Clone, Debug)] +pub struct Sender(Arc>); + +impl Deref for Sender { + type Target = Arc>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl<'js> Trace<'js> for Sender> { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + if let Ok(v) = self.value.read() { + if let Some(v) = v.as_ref() { + tracer.mark(v) + } + } + } +} + +impl Sender { + pub fn send(&self, value: T) { + if !self.is_sent.load(Ordering::Relaxed) { + self.value.write().unwrap().replace(value); + self.is_sent.store(true, Ordering::Release); + self.notify.notify_waiters(); + } + } + + pub fn subscribe(&self) -> Receiver { + Receiver(Arc::clone(&self.0)) + } +} + +#[derive(Clone, Debug)] +pub struct Receiver(Arc>); + +impl Deref for Receiver { + type Target = Arc>; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Receiver { + pub async fn recv(&self) -> T { + if !self.is_sent.load(Ordering::Acquire) { + self.notify.notified().await; + } + self.value.read().unwrap().clone().unwrap() + } +} + +pub fn channel() -> (Sender, Receiver) { + let shared = Arc::new(Shared { + is_sent: AtomicBool::new(false), + value: RwLock::new(None), + notify: Notify::new(), + }); + + (Sender(Arc::clone(&shared)), Receiver(shared)) +} + +#[cfg(test)] +mod tests { + use tokio::join; + + #[tokio::test] + async fn test() { + let (tx, rx1) = super::channel::(); + + let rx2 = tx.subscribe(); + let rx3 = tx.subscribe(); + + let a = tokio::spawn(async move { + let val = rx1.recv().await; //wait for value to become false + assert!(val) + }); + + let b = tokio::spawn(async move { + let val = rx2.recv().await; //wait for value to become false + assert!(val) + }); + + tokio::time::sleep(std::time::Duration::from_millis(10)).await; + + tx.send(true); + + let val = rx3.recv().await; + assert!(val); + + let (a, b) = join!(a, b); + a.unwrap(); + b.unwrap(); + } +} diff --git a/stdlib/src/llrt/llrt_utils/module.rs b/stdlib/src/llrt/llrt_utils/module.rs new file mode 100644 index 00000000..950922d4 --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/module.rs @@ -0,0 +1,30 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use rquickjs::{ + module::{Exports, ModuleDef}, + Ctx, Object, Result, Value, +}; + +pub struct ModuleInfo { + pub name: &'static str, + pub module: T, +} + +pub fn export_default<'js, F>(ctx: &Ctx<'js>, exports: &Exports<'js>, f: F) -> Result<()> +where + F: FnOnce(&Object<'js>) -> Result<()>, +{ + let default = Object::new(ctx.clone())?; + f(&default)?; + + for name in default.keys::() { + let name = name?; + let value: Value = default.get(&name)?; + exports.export(name, value)?; + } + + exports.export("default", default)?; + + Ok(()) +} diff --git a/stdlib/src/llrt/llrt_utils/object.rs b/stdlib/src/llrt/llrt_utils/object.rs new file mode 100644 index 00000000..25495967 --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/object.rs @@ -0,0 +1,171 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use std::collections::BTreeMap; + +use rquickjs::{ + atom::PredefinedAtom, + function::{Constructor, IntoJsFunc}, + object::Property, + prelude::Func, + Array, Coerced, Ctx, Error, Exception, FromJs, IntoAtom, IntoJs, Object, Result, Undefined, + Value, +}; + +use crate::llrt_utils::primordials::{BasePrimordials, Primordial}; + +pub trait ObjectExt<'js> { + fn get_optional + Clone, V: FromJs<'js>>(&self, k: K) -> Result>; + fn get_required, V: FromJs<'js>>( + &self, + k: K, + object_name: &'static str, + ) -> Result; + fn into_object_or_throw(self, ctx: &Ctx<'js>, object_name: &'static str) + -> Result>; +} + +impl<'js> ObjectExt<'js> for Object<'js> { + fn get_optional + Clone, V: FromJs<'js> + Sized>( + &self, + k: K, + ) -> Result> { + self.get::>(k) + } + + fn get_required, V: FromJs<'js>>( + &self, + k: K, + object_name: &'static str, + ) -> Result { + let k = k.as_ref(); + self.get::<&str, Option>(k)?.ok_or_else(|| { + Exception::throw_type( + self.ctx(), + &[object_name, " '", k, "' property required"].concat(), + ) + }) + } + + fn into_object_or_throw(self, _: &Ctx<'js>, _: &'static str) -> Result> { + Ok(self) + } +} + +impl<'js> ObjectExt<'js> for Value<'js> { + fn get_optional + Clone, V: FromJs<'js>>(&self, k: K) -> Result> { + if let Some(obj) = self.as_object() { + return obj.get_optional(k); + } + Ok(None) + } + + fn get_required, V: FromJs<'js>>( + &self, + k: K, + object_name: &'static str, + ) -> Result { + self.as_object() + .ok_or_else(|| not_a_object_error(self.ctx(), object_name))? + .get_required(k, object_name) + } + + fn into_object_or_throw( + self, + ctx: &Ctx<'js>, + object_name: &'static str, + ) -> Result> { + self.into_object() + .ok_or_else(|| not_a_object_error(ctx, object_name)) + } +} + +pub fn not_a_object_error(ctx: &Ctx<'_>, object_name: &str) -> Error { + Exception::throw_type(ctx, &[object_name, " is not an object"].concat()) +} + +pub struct Proxy<'js> { + target: Value<'js>, + options: Object<'js>, +} + +impl<'js> IntoJs<'js> for Proxy<'js> { + fn into_js(self, ctx: &Ctx<'js>) -> Result> { + BasePrimordials::get(ctx)? + .constructor_proxy + .construct::<_, Value>((self.target, self.options)) + } +} + +impl<'js> Proxy<'js> { + pub fn new(ctx: Ctx<'js>) -> Result { + let options = Object::new(ctx.clone())?; + Ok(Self { + target: Undefined.into_value(ctx), + options, + }) + } + + pub fn with_target(ctx: Ctx<'js>, target: Value<'js>) -> Result { + let options = Object::new(ctx)?; + Ok(Self { target, options }) + } + + pub fn setter(&self, setter: Func) -> Result<()> + where + T: IntoJsFunc<'js, P> + 'js, + { + self.options.set(PredefinedAtom::Setter, setter)?; + Ok(()) + } + + pub fn getter(&self, getter: Func) -> Result<()> + where + T: IntoJsFunc<'js, P> + 'js, + { + self.options.set(PredefinedAtom::Getter, getter)?; + Ok(()) + } +} + +pub fn array_to_btree_map<'js>( + ctx: &Ctx<'js>, + array: Array<'js>, +) -> Result>> { + let value = object_from_entries(ctx, array)?; + let value = value.into_value(); + BTreeMap::from_js(ctx, value) +} + +pub fn object_from_entries<'js>(ctx: &Ctx<'js>, array: Array<'js>) -> Result> { + let obj = Object::new(ctx.clone())?; + for value in array.into_iter().flatten() { + if let Some(entry) = value.as_array() { + if let Ok(key) = entry.get::(0) { + if let Ok(value) = entry.get::(1) { + let _ = obj.set(key, value); //ignore result of failed + } + } + } + } + Ok(obj) +} + +/// Build a constructor that behaves like `class Name extends Parent` +pub fn define_subclass<'js, F, P>( + ctx: &Ctx<'js>, + name: &str, + parent: &Constructor<'js>, + construct: F, +) -> Result> +where + F: IntoJsFunc<'js, P> + 'js, +{ + let parent_proto: Object = parent.get(PredefinedAtom::Prototype)?; + let proto = Object::new(ctx.clone())?; + proto.set_prototype(Some(&parent_proto))?; + let constructor = Constructor::new_prototype(ctx, proto, construct)?; + constructor.set_prototype(parent.as_object())?; + constructor.prop(PredefinedAtom::Name, Property::from(name).configurable())?; + Ok(constructor) +} diff --git a/stdlib/src/llrt/llrt_utils/option.rs b/stdlib/src/llrt/llrt_utils/option.rs new file mode 100644 index 00000000..95994fbb --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/option.rs @@ -0,0 +1,102 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use rquickjs::{ + class::{Trace, Tracer}, + function::{FromParam, ParamRequirement, ParamsAccessor}, + Ctx, FromJs, IntoJs, JsLifetime, Result, Type, Value, +}; + +/// Helper type for treating an undefined value as None, without treating null as None +#[derive(Clone)] +pub struct Undefined(pub Option); + +impl<'js, T: FromJs<'js>> FromJs<'js> for Undefined { + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + if value.type_of() == Type::Undefined { + Ok(Self(None)) + } else { + Ok(Self(Some(FromJs::from_js(ctx, value)?))) + } + } +} + +impl<'js, T: IntoJs<'js>> IntoJs<'js> for Undefined { + fn into_js(self, ctx: &Ctx<'js>) -> Result> { + match self.0 { + None => Ok(Value::new_undefined(ctx.clone())), + Some(val) => val.into_js(ctx), + } + } +} + +impl Default for Undefined { + fn default() -> Self { + Self(None) + } +} + +unsafe impl<'js, T: JsLifetime<'js>> JsLifetime<'js> for Undefined { + type Changed<'to> = Undefined>; +} + +impl<'js, T: Trace<'js>> Trace<'js> for Undefined { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + self.0.trace(tracer) + } +} + +/// Helper type for converting an None into null instead of undefined. +/// Needed while rquickjs::function::Null has no IntoJs implementation +#[derive(Clone)] +pub struct Null(pub Option); + +impl<'js, T: FromJs<'js>> FromJs<'js> for Null { + fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { + if value.type_of() == Type::Null { + Ok(Self(None)) + } else { + Ok(Self(Some(FromJs::from_js(ctx, value)?))) + } + } +} + +impl<'js, T: IntoJs<'js>> IntoJs<'js> for Null { + fn into_js(self, ctx: &Ctx<'js>) -> Result> { + match self.0 { + None => Ok(Value::new_null(ctx.clone())), + Some(val) => val.into_js(ctx), + } + } +} + +unsafe impl<'js, T: JsLifetime<'js>> JsLifetime<'js> for Null { + type Changed<'to> = Null>; +} + +impl<'js, T: Trace<'js>> Trace<'js> for Null { + fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { + self.0.trace(tracer) + } +} + +/// Helper type for accepting no value, or null, but considering undefined as a value +pub struct NullableOpt(pub Option); + +impl<'js, T: FromJs<'js>> FromParam<'js> for NullableOpt { + fn param_requirement() -> ParamRequirement { + ParamRequirement::optional() + } + + fn from_param<'a>(params: &mut ParamsAccessor<'a, 'js>) -> Result { + if !params.is_empty() { + let arg = params.arg(); + if arg.is_null() { + Ok(NullableOpt(None)) + } else { + let ctx = params.ctx().clone(); + Ok(NullableOpt(Some(T::from_js(&ctx, arg)?))) + } + } else { + Ok(NullableOpt(None)) + } + } +} diff --git a/stdlib/src/llrt/llrt_utils/primordials.rs b/stdlib/src/llrt/llrt_utils/primordials.rs new file mode 100644 index 00000000..dd50dc59 --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/primordials.rs @@ -0,0 +1,166 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use std::any::type_name; + +use rquickjs::{ + atom::PredefinedAtom, function::Constructor, runtime::UserDataGuard, Ctx, Exception, Function, + JsLifetime, Object, Result, +}; + +use crate::llrt_utils::result::ResultExt; + +#[derive(JsLifetime)] +pub struct BasePrimordials<'js> { + // Constructors + pub constructor_map: Constructor<'js>, + pub constructor_set: Constructor<'js>, + pub constructor_date: Constructor<'js>, + pub constructor_error: Constructor<'js>, + pub constructor_type_error: Constructor<'js>, + pub constructor_range_error: Constructor<'js>, + pub constructor_regexp: Constructor<'js>, + pub constructor_uint8array: Constructor<'js>, + pub constructor_array_buffer: Constructor<'js>, + pub constructor_proxy: Constructor<'js>, + pub constructor_object: Constructor<'js>, + pub constructor_bool: Constructor<'js>, + pub constructor_number: Constructor<'js>, + pub constructor_string: Constructor<'js>, + + // Prototypes + pub prototype_object: Object<'js>, + pub prototype_date: Object<'js>, + pub prototype_regexp: Object<'js>, + pub prototype_set: Object<'js>, + pub prototype_map: Object<'js>, + pub prototype_error: Object<'js>, + + // Functions + pub function_array_from: Function<'js>, + pub function_array_buffer_is_view: Function<'js>, + pub function_get_own_property_descriptor: Function<'js>, + pub function_reflect_own_keys: Function<'js>, + pub function_parse_int: Function<'js>, + pub function_parse_float: Function<'js>, + pub prototype_iterator: Object<'js>, +} + +pub trait Primordial<'js> +where + Self: Sized + JsLifetime<'js>, +{ + fn get<'a>(ctx: &'a Ctx<'js>) -> Result> { + let userdata = ctx.userdata::().or_throw_msg( + ctx, + &[ + "Userdata of ", + type_name::(), + " not initialized. Call init(&ctx) on this type.", + ] + .concat(), + )?; + + Ok(userdata) + } + + fn init<'a>(ctx: &'a Ctx<'js>) -> Result<()> { + if ctx.userdata::().is_none() { + let primoridals = Self::new(ctx)?; + let _ = ctx.store_userdata(primoridals); + } + + Ok(()) + } + fn new(ctx: &Ctx<'js>) -> Result; +} + +impl<'js> Primordial<'js> for BasePrimordials<'js> { + fn new(ctx: &Ctx<'js>) -> Result { + let globals = ctx.globals(); + + let constructor_object: Constructor = globals.get(PredefinedAtom::Object)?; + let prototype_object: Object = constructor_object.get(PredefinedAtom::Prototype)?; + + let constructor_proxy: Constructor = globals.get(PredefinedAtom::Proxy)?; + + let function_get_own_property_descriptor: Function = + constructor_object.get(PredefinedAtom::GetOwnPropertyDescriptor)?; + + let constructor_date: Constructor = globals.get(PredefinedAtom::Date)?; + let prototype_date: Object = constructor_date.get(PredefinedAtom::Prototype)?; + + let constructor_map: Constructor = globals.get(PredefinedAtom::Map)?; + let prototype_map: Object = constructor_map.get(PredefinedAtom::Prototype)?; + + let constructor_set: Constructor = globals.get(PredefinedAtom::Set)?; + let prototype_set: Object = constructor_set.get(PredefinedAtom::Prototype)?; + + let constructor_regexp: Constructor = globals.get(PredefinedAtom::RegExp)?; + let prototype_regexp: Object = constructor_regexp.get(PredefinedAtom::Prototype)?; + + let constructor_uint8array: Constructor = globals.get(PredefinedAtom::Uint8Array)?; + let constructor_arraybuffer: Constructor = globals.get(PredefinedAtom::ArrayBuffer)?; + + let constructor_error: Constructor = globals.get(PredefinedAtom::Error)?; + let constructor_type_error: Constructor = ctx.globals().get(PredefinedAtom::TypeError)?; + let constructor_range_error: Constructor = ctx.globals().get(PredefinedAtom::RangeError)?; + let prototype_error: Object = constructor_error.get(PredefinedAtom::Prototype)?; + + let constructor_array: Object = globals.get(PredefinedAtom::Array)?; + let function_array_from: Function = constructor_array.get(PredefinedAtom::From)?; + + let constructor_array_buffer: Object = globals.get(PredefinedAtom::ArrayBuffer)?; + let function_array_buffer_is_view: Function = constructor_array_buffer.get("isView")?; + + let constructor_bool: Constructor = globals.get(PredefinedAtom::Boolean)?; + + let constructor_number: Constructor = globals.get(PredefinedAtom::Number)?; + let function_parse_float: Function = constructor_number.get("parseFloat")?; + let function_parse_int: Function = constructor_number.get("parseInt")?; + + let constructor_string: Constructor = globals.get(PredefinedAtom::String)?; + + let reflect: Object = globals.get("Reflect")?; + let function_reflect_own_keys: Function = reflect.get("ownKeys")?; + + // Walk to %IteratorPrototype% via an array iterator. + let array = rquickjs::Array::new(ctx.clone())?; + let iter_fn: Function = array + .as_object() + .get(rquickjs::atom::PredefinedAtom::SymbolIterator)?; + let array_iter: Object = iter_fn.call((rquickjs::function::This(array),))?; + let prototype_iterator = array_iter + .get_prototype() + .and_then(|p| p.get_prototype()) + .ok_or_else(|| Exception::throw_internal(ctx, "missing %IteratorPrototype%"))?; + + Ok(Self { + constructor_map, + constructor_set, + constructor_date, + constructor_proxy, + constructor_error, + constructor_type_error, + constructor_range_error, + constructor_regexp, + constructor_uint8array, + constructor_array_buffer: constructor_arraybuffer, + constructor_object, + constructor_bool, + constructor_number, + constructor_string, + prototype_object, + prototype_date, + prototype_regexp, + prototype_set, + prototype_map, + prototype_error, + function_array_from, + function_array_buffer_is_view, + function_get_own_property_descriptor, + function_reflect_own_keys, + function_parse_float, + function_parse_int, + prototype_iterator, + }) + } +} diff --git a/stdlib/src/llrt/llrt_utils/provider.rs b/stdlib/src/llrt/llrt_utils/provider.rs new file mode 100644 index 00000000..db99b88d --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/provider.rs @@ -0,0 +1,25 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#[derive(PartialEq)] +pub enum ProviderType { + None, + Resource(String), // Custom asynchronous resource + // Userland provider types + Immediate, // [Immediate] Processing by setImmediate() + Interval, // [Interval] Timer by setInterval() + MessagePort, // [MessagePort] Port for worker_threads + Microtask, // [Microtask] Processing by queueMicrotask() + TickObject, // [TickObject] Processing by process.nextTick() + Timeout, // [Timeout] Timer by setTimeout() + // Internal provider types + FsReqCallback, // [FSREQCALLBACK] Callback for file system operations + GetAddrInfoReqWrap, // [GETADDRINFOREQWRAP] When resolving DNS (dns.lookup(), etc.) + GetNameInfoReqWrap, // [GETNAMEINFOREQWRAP] DNS reverse lookup + PipeWrap, // [PIPEWRAP] Pipe connection + StatWatcher, // [STATWACHER] File monitoring such as fs.watch() + TcpWrap, // [TCPWRAP] TCP socket wrap (net.Socket, etc.) + TimerWrap, // [TIMERWRAP] Internal timer wrap (low level) + TlsWrap, // [TLSWRAP] TLS socket (HTTPS, etc.) + UdpWrap, // [UDPWRAP] UDP socket wrap (dgram module) +} diff --git a/stdlib/src/llrt/llrt_utils/result.rs b/stdlib/src/llrt/llrt_utils/result.rs new file mode 100644 index 00000000..91364ce9 --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/result.rs @@ -0,0 +1,105 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +#![allow(clippy::uninlined_format_args)] + +use std::{fmt::Write, result::Result as StdResult}; + +use rquickjs::{Ctx, Exception, Result}; + +pub trait ResultExt { + fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> Result; + fn or_throw_range(self, ctx: &Ctx, msg: &str) -> Result; + fn or_throw_type(self, ctx: &Ctx, msg: &str) -> Result; + fn or_throw(self, ctx: &Ctx) -> Result; +} + +pub trait OptionExt { + fn and_then_ok(self, f: F) -> StdResult, E> + where + F: FnOnce(T) -> StdResult, E>; + + fn unwrap_or_else_ok(self, f: F) -> StdResult + where + F: FnOnce() -> StdResult; +} + +impl ResultExt for StdResult { + fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> Result { + self.map_err(|e| { + let mut message = String::with_capacity(100); + message.push_str(msg); + message.push_str(". "); + write!(message, "{}", e).unwrap(); + Exception::throw_message(ctx, &message) + }) + } + + fn or_throw_range(self, ctx: &Ctx, msg: &str) -> Result { + self.map_err(|e| { + let mut message = String::with_capacity(100); + if !message.is_empty() { + message.push_str(msg); + message.push_str(". "); + } + write!(message, "{}", e).unwrap(); + Exception::throw_range(ctx, &message) + }) + } + + fn or_throw_type(self, ctx: &Ctx, msg: &str) -> Result { + self.map_err(|e| { + let mut message = String::with_capacity(100); + if !msg.is_empty() { + message.push_str(msg); + message.push_str(". "); + } + write!(message, "{}", e).unwrap(); + Exception::throw_type(ctx, &message) + }) + } + + fn or_throw(self, ctx: &Ctx) -> Result { + self.map_err(|err| Exception::throw_message(ctx, &err.to_string())) + } +} + +impl ResultExt for Option { + fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> Result { + self.ok_or_else(|| Exception::throw_message(ctx, msg)) + } + + fn or_throw_range(self, ctx: &Ctx, msg: &str) -> Result { + self.ok_or_else(|| Exception::throw_range(ctx, msg)) + } + + fn or_throw_type(self, ctx: &Ctx, msg: &str) -> Result { + self.ok_or_else(|| Exception::throw_type(ctx, msg)) + } + + fn or_throw(self, ctx: &Ctx) -> Result { + self.ok_or_else(|| Exception::throw_message(ctx, "Value is not present")) + } +} + +impl OptionExt for Option { + fn and_then_ok(self, f: F) -> StdResult, E> + where + F: FnOnce(T) -> StdResult, E>, + { + match self { + Some(v) => f(v), + None => Ok(None), + } + } + + fn unwrap_or_else_ok(self, f: F) -> StdResult + where + F: FnOnce() -> StdResult, + { + match self { + Some(v) => Ok(v), + None => f(), + } + } +} diff --git a/stdlib/src/llrt/llrt_utils/reuse_list.rs b/stdlib/src/llrt/llrt_utils/reuse_list.rs new file mode 100644 index 00000000..624126db --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/reuse_list.rs @@ -0,0 +1,338 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use std::fmt::Debug; + +#[derive(Default, Clone)] +pub struct ReuseList { + items: Vec>, + slots: Vec, + last_slot_idx: usize, + len: usize, + slot_size: usize, +} + +impl Debug for ReuseList { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ReuseList") + .field("items", &self.items) + .field("slots", &self.slots) + .finish() + } +} + +impl ReuseList { + pub fn new() -> Self { + Self::with_capacity(0) + } + + //is empty + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + //create a with capacity + pub fn with_capacity(capacity: usize) -> Self { + Self { + items: Vec::with_capacity(capacity), + slots: Vec::with_capacity(capacity >> 2), + last_slot_idx: 0, + len: 0, + slot_size: 0, + } + } + + pub fn append(&mut self, item: T) -> usize { + if self.slot_size > 0 { + //reuse empty slot if valid + let slot = self.slots[self.last_slot_idx - 1]; + if slot > 0 { + self.items[slot - 1] = Some(item); + self.slots[self.last_slot_idx - 1] = 0; + if self.last_slot_idx > 1 { + self.last_slot_idx -= 1; + } + + self.len += 1; + return slot - 1; + } + } + //no valid empty slots, append to end + self.items.push(Some(item)); + self.len += 1; + self.items.len() - 1 + } + + pub fn remove(&mut self, index: usize) -> Option { + if index >= self.items.len() { + return None; + } + + let item = self.items[index].take(); + if item.is_some() { + if self.slot_size > 0 && self.slots[self.last_slot_idx - 1] == 0 { + self.slots[self.last_slot_idx - 1] = index + 1; + } else { + self.slots.push(index + 1); + self.last_slot_idx += 1; + self.slot_size += 1; + } + self.len -= 1; + } + item + } + + pub fn get(&self, index: usize) -> Option<&T> { + if index >= self.items.len() { + None + } else { + self.items[index].as_ref() + } + } + + pub fn get_mut(&mut self, index: usize) -> Option<&mut T> { + if index >= self.items.len() { + None + } else { + self.items[index].as_mut() + } + } + + pub fn capacity(&self) -> usize { + self.items.capacity() + } + + pub fn len(&self) -> usize { + self.len + } + + pub fn iter(&self) -> impl Iterator { + self.items.iter().filter_map(|x| x.as_ref()) + } + + pub fn iter_mut(&mut self) -> impl Iterator { + self.items.iter_mut().filter_map(|x| x.as_mut()) + } + + //implement clear + pub fn clear(&mut self) { + self.items.clear(); + self.slots.clear(); + self.last_slot_idx = 0; + self.len = 0; + self.slot_size = 0; + } + + pub fn optimize(&mut self) { + let mut new_items = Vec::with_capacity(self.len); + + for item in self.items.iter_mut() { + let a = item.take(); + if a.is_some() { + new_items.push(a); + } + } + self.items = new_items; + self.slots.clear(); + self.last_slot_idx = 0; + self.slot_size = 0; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_new() { + let list: ReuseList = ReuseList::new(); + assert_eq!(list.len(), 0); + assert_eq!(list.capacity(), 0); + assert_eq!(list.items.len(), 0); + assert_eq!(list.slots.len(), 0); + } + + #[test] + fn test_with_capacity() { + let list: ReuseList = ReuseList::with_capacity(10); + assert_eq!(list.len(), 0); + assert_eq!(list.capacity(), 10); + assert_eq!(list.items.len(), 0); + assert_eq!(list.slots.len(), 0); + } + + #[test] + fn test_append() { + let mut list = ReuseList::new(); + assert_eq!(list.append(1), 0); + assert_eq!(list.append(2), 1); + assert_eq!(list.append(3), 2); + assert_eq!(list.len(), 3); + + let items: Vec = list.iter().cloned().collect(); + assert_eq!(items, vec![1, 2, 3]); + assert_eq!(list.items, vec![Some(1), Some(2), Some(3)]); + assert_eq!(list.slots, vec![]); + } + + #[test] + fn test_remove() { + let mut list = ReuseList::new(); + list.append(1); + list.append(2); + list.append(3); + + assert_eq!(list.remove(1), Some(2)); + assert_eq!(list.len(), 2); + + let items: Vec = list.iter().cloned().collect(); + assert_eq!(items, vec![1, 3]); + assert_eq!(list.items, vec![Some(1), None, Some(3)]); + assert_eq!(list.slots, vec![2]); + + assert_eq!(list.remove(5), None); + } + + #[test] + fn test_reuse_slots() { + let mut list = ReuseList::new(); + list.append(1); + list.append(2); + list.append(3); + + list.remove(1); // Remove 2 + assert_eq!(list.append(4), 1); // Should reuse index 1 + + let items: Vec = list.iter().cloned().collect(); + assert_eq!(items, vec![1, 4, 3]); + assert_eq!(list.items, vec![Some(1), Some(4), Some(3)]); + assert_eq!(list.slots, vec![0]); + } + + #[test] + fn test_get() { + let mut list = ReuseList::new(); + list.append(1); + list.append(2); + + assert_eq!(list.get(0), Some(&1)); + assert_eq!(list.get(1), Some(&2)); + assert_eq!(list.get(2), None); + assert_eq!(list.items, vec![Some(1), Some(2)]); + assert_eq!(list.slots, vec![]); + } + + #[test] + fn test_get_mut() { + let mut list = ReuseList::new(); + list.append(1); + list.append(2); + + if let Some(value) = list.get_mut(0) { + *value = 10; + } + + assert_eq!(list.get(0), Some(&10)); + assert_eq!(list.items, vec![Some(10), Some(2)]); + assert_eq!(list.slots, vec![]); + } + + #[test] + fn test_iter() { + let mut list = ReuseList::new(); + list.append(1); + list.append(2); + list.append(3); + list.remove(1); + + let items: Vec = list.iter().cloned().collect(); + assert_eq!(items, vec![1, 3]); + assert_eq!(list.items, vec![Some(1), None, Some(3)]); + assert_eq!(list.slots, vec![2]); + } + + #[test] + fn test_iter_mut() { + let mut list = ReuseList::new(); + list.append(1); + list.append(2); + list.append(3); + + for item in list.iter_mut() { + *item *= 2; + } + + let items: Vec = list.iter().cloned().collect(); + assert_eq!(items, vec![2, 4, 6]); + assert_eq!(list.items, vec![Some(2), Some(4), Some(6)]); + assert_eq!(list.slots, vec![]); + } + + #[test] + fn test_multiple_removes() { + let mut list = ReuseList::new(); + for i in 0..5 { + list.append(i); + } + + list.remove(1); + list.remove(3); + + let items: Vec = list.iter().cloned().collect(); + assert_eq!(items, vec![0, 2, 4]); + assert_eq!(list.items, vec![Some(0), None, Some(2), None, Some(4)]); + assert_eq!(list.slots, vec![2, 4]); + + // Test reuse of both slots + list.append(10); + list.append(11); + + let items: Vec = list.iter().cloned().collect(); + assert_eq!(items, vec![0, 11, 2, 10, 4]); + assert_eq!( + list.items, + vec![Some(0), Some(11), Some(2), Some(10), Some(4)] + ); + assert_eq!(list.slots, vec![0, 0]); + + list.remove(0); + + let items: Vec = list.iter().cloned().collect(); + assert_eq!(items, vec![11, 2, 10, 4]); + assert_eq!(list.items, vec![None, Some(11), Some(2), Some(10), Some(4)]); + assert_eq!(list.slots, vec![1, 0]); + + list.append(20); + + let items: Vec = list.iter().cloned().collect(); + assert_eq!(items, vec![20, 11, 2, 10, 4]); + assert_eq!( + list.items, + vec![Some(20), Some(11), Some(2), Some(10), Some(4)] + ); + assert_eq!(list.slots, vec![0, 0]); + + //remove all items + list.clear(); + } + + #[test] + fn test_optimize() { + let mut list = ReuseList::new(); + list.append(1); + list.append(2); + list.append(3); + list.remove(1); + + assert_eq!(list.items, vec![Some(1), None, Some(3)]); + assert_eq!(list.slots, vec![2]); + + list.optimize(); + + assert_eq!(list.items, vec![Some(1), Some(3)]); + assert_eq!(list.slots, vec![]); + assert_eq!(list.last_slot_idx, 0); + assert_eq!(list.slot_size, 0); + + let items: Vec = list.iter().cloned().collect(); + assert_eq!(items, vec![1, 3]); + } +} diff --git a/stdlib/src/llrt/llrt_utils/signals.rs b/stdlib/src/llrt/llrt_utils/signals.rs new file mode 100644 index 00000000..47a1bb8d --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/signals.rs @@ -0,0 +1,150 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use rquickjs::{prelude::Opt, Ctx, Exception, Result, Value}; + +use crate::llrt_utils::result::ResultExt; +use std::io; + +#[cfg(unix)] +macro_rules! generate_signal_from_str_fn { + ($($signal:path),*) => { + pub fn signal_from_str(signal: &str) -> Option { + let signal = ["libc::", signal].concat(); + match signal.as_str() { + $(stringify!($signal) => Some($signal),)* + _ => None, + } + } + + pub fn signal_str_from_i32(signal: i32) -> Option<&'static str> { + $(if signal == $signal { + return Some(&stringify!($signal)[6..]); + })* + None + } + }; +} + +#[cfg(unix)] +generate_signal_from_str_fn!( + libc::SIGHUP, + libc::SIGINT, + libc::SIGQUIT, + libc::SIGILL, + libc::SIGABRT, + libc::SIGFPE, + libc::SIGKILL, + libc::SIGSEGV, + libc::SIGPIPE, + libc::SIGALRM, + libc::SIGTERM +); + +#[cfg(not(unix))] +static WINDOWS_SIGTERM: i32 = -1; + +pub fn parse_signal(signal: Option>) -> Result { + let Some(val) = signal else { + #[cfg(unix)] + return Ok(libc::SIGTERM); + #[cfg(not(unix))] + return Ok(WINDOWS_SIGTERM); + }; + + if let Some(num) = val.as_number() { + let sig = num as i32; + #[cfg(unix)] + return Ok(sig); + // On Windows: 0 checks existence, anything else kills + #[cfg(not(unix))] + return Ok(if sig == 0 { 0 } else { WINDOWS_SIGTERM }); + } + + if let Some(str_val) = val.as_string() { + let s = str_val.to_string()?; + + #[cfg(unix)] + let mapped_sig = signal_from_str(&s); + + #[cfg(not(unix))] + let mapped_sig = match s.as_str() { + "SIGINT" | "SIGTERM" | "SIGKILL" | "SIGQUIT" | "SIGHUP" | "SIGUSR1" => { + Some(WINDOWS_SIGTERM) + } + _ => None, + }; + + return match mapped_sig { + Some(sig) => Ok(sig), + None => Err(Exception::throw_type( + val.ctx(), + &format!("Unknown signal: {}", s), + )), + }; + } + + Err(Exception::throw_type(val.ctx(), "Invalid signal type")) +} + +#[cfg(unix)] +pub fn kill_process_raw(pid: u32, signal: i32) -> io::Result<()> { + // libc::kill returns 0 on success, -1 on error + // SAFETY: kill is a safe system call as long as the signal value is valid, which is ensured by parse_signal + if unsafe { libc::kill(pid as i32, signal) } == 0 { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } +} + +#[cfg(windows)] +pub fn kill_process_raw(pid: u32, signal: i32) -> io::Result<()> { + use windows_sys::Win32::Foundation::CloseHandle; + use windows_sys::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE}; + + // SAFETY: OpenProcess is safe to call with valid parameters, and PROCESS_TERMINATE is a valid access right + let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid) }; + if handle == std::ptr::null_mut() { + return Err(io::Error::last_os_error()); + } + + let result = if signal == 0 { + Ok(()) + } else { + // SAFETY: TerminateProcess is safe to call with a valid process handle obtained from OpenProcess + if unsafe { TerminateProcess(handle, 1) } != 0 { + Ok(()) + } else { + Err(io::Error::last_os_error()) + } + }; + + // SAFETY: CloseHandle is safe to call with a valid handle obtained from OpenProcess + unsafe { CloseHandle(handle) }; + result +} + +pub fn kill(ctx: &Ctx<'_>, pid: u32, signal: Opt>) -> Result { + let signal = parse_signal(signal.0)?; + + kill_process_raw(pid, signal) + .map(|_| true) + .or_else(|e| { + // Handle "Process Not Found" / "Existence Check" logic + // If signal is 0 (check existence) and we hit a specific error, return Ok(false). + + #[cfg(unix)] + let is_not_found = e.raw_os_error() == Some(libc::ESRCH); // Error 3 + + #[cfg(windows)] + let is_not_found = true; // On Windows, any OpenProcess failure during check implies "not found" (or not accessible) + + if signal == 0 && is_not_found { + Ok(false) + } else { + Err(e) + } + }) + .or_throw(ctx) +} diff --git a/stdlib/src/llrt/llrt_utils/string.rs b/stdlib/src/llrt/llrt_utils/string.rs new file mode 100644 index 00000000..d066cbe9 --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/string.rs @@ -0,0 +1,27 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use rquickjs::{Coerced, Result, Value}; + +#[inline] +pub fn get_string(value: &Value<'_>) -> Result> { + if let Some(val) = value.as_string() { + let string = val.to_string()?; + return Ok(Some(string)); + } + Ok(None) +} + +pub fn get_coerced_string(value: &Value<'_>) -> Option { + if let Ok(val) = value.get::>() { + return Some(val.0); + }; + None +} + +pub fn get_coerced_defined_string<'js>(value: &Option>) -> Option { + if let Some(value) = value { + if !value.is_undefined() { + return get_coerced_string(value); + } + }; + None +} diff --git a/stdlib/src/llrt/llrt_utils/sysinfo.rs b/stdlib/src/llrt/llrt_utils/sysinfo.rs new file mode 100644 index 00000000..f10c3e65 --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/sysinfo.rs @@ -0,0 +1,14 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +#[cfg(target_os = "macos")] +pub const PLATFORM: &str = "darwin"; +#[cfg(target_os = "windows")] +pub const PLATFORM: &str = "win32"; +#[cfg(not(any(target_os = "macos", target_os = "windows")))] +pub const PLATFORM: &str = std::env::consts::OS; + +#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] +pub const ARCH: &str = "x64"; +#[cfg(target_arch = "aarch64")] +pub const ARCH: &str = "arm64"; +#[cfg(not(any(target_arch = "x86_64", target_arch = "x86", target_arch = "aarch64")))] +pub const ARCH: &str = std::env::consts::ARCH; diff --git a/stdlib/src/llrt/llrt_utils/time.rs b/stdlib/src/llrt/llrt_utils/time.rs new file mode 100644 index 00000000..2998cd07 --- /dev/null +++ b/stdlib/src/llrt/llrt_utils/time.rs @@ -0,0 +1,48 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +use std::{ + sync::atomic::{AtomicU64, Ordering}, + time::SystemTime, +}; + +static TIME_ORIGIN: AtomicU64 = AtomicU64::new(0); + +/// Get the current time in nanoseconds. +/// +/// # Safety +/// - Good until the year 2554 +/// - Always use a checked substraction since this can return 0 +pub fn now_nanos() -> u64 { + SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_nanos() as u64 +} + +/// Get the current time in millis. +/// +/// # Safety +/// - Good until the year 2554 +/// - Always use a checked substraction since this can return 0 +pub fn now_millis() -> i64 { + SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap_or_default() + .as_millis() as i64 +} + +/// Get the origin time in nanoseconds. +/// +/// # Safety +/// - Good until the year 2554 +/// - Always use a checked substraction since this can return 0 +pub fn origin_nanos() -> u64 { + TIME_ORIGIN.load(Ordering::Relaxed) +} + +// For accuracy reasons, this function should be executed when the vm is initialized +pub fn init() { + if TIME_ORIGIN.load(Ordering::Relaxed) == 0 { + let time_origin = now_nanos(); + TIME_ORIGIN.store(time_origin, Ordering::Relaxed) + } +} diff --git a/stdlib/src/llrt/llrt_zlib/brotli.rs b/stdlib/src/llrt/llrt_zlib/brotli.rs new file mode 100644 index 00000000..2d74a48e --- /dev/null +++ b/stdlib/src/llrt/llrt_zlib/brotli.rs @@ -0,0 +1,50 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use crate::llrt_buffer::Buffer; +use crate::llrt_context::CtxExtension; +use crate::llrt_utils::{bytes::ObjectBytes, result::ResultExt}; +use rquickjs::{ + prelude::{Opt, Rest}, + Ctx, Error, Exception, Function, IntoJs, Null, Result, Value, +}; + +use super::{define_cb_function, define_sync_function, max_output_length, read_to_end_limited}; + +enum BrotliCommand { + Compress, + Decompress, +} + +fn brotli_converter<'js>( + ctx: Ctx<'js>, + bytes: ObjectBytes<'js>, + options: Opt>, + command: BrotliCommand, +) -> Result> { + let src = bytes.as_bytes(&ctx)?; + let limit = max_output_length(&options)?; + + let dst = match command { + BrotliCommand::Compress => read_to_end_limited( + &ctx, + crate::llrt_compression::brotli::encoder(src), + limit, + src.len(), + )?, + BrotliCommand::Decompress => read_to_end_limited( + &ctx, + crate::llrt_compression::brotli::decoder(src), + limit, + src.len(), + )?, + }; + + Buffer(dst).into_js(&ctx) +} + +define_cb_function!(br_comp, brotli_converter, BrotliCommand::Compress); +define_sync_function!(br_comp_sync, brotli_converter, BrotliCommand::Compress); + +define_cb_function!(br_decomp, brotli_converter, BrotliCommand::Decompress); +define_sync_function!(br_decomp_sync, brotli_converter, BrotliCommand::Decompress); diff --git a/stdlib/src/llrt/llrt_zlib/lib.rs b/stdlib/src/llrt/llrt_zlib/lib.rs new file mode 100644 index 00000000..91352f33 --- /dev/null +++ b/stdlib/src/llrt/llrt_zlib/lib.rs @@ -0,0 +1,212 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use crate::llrt_utils::module::{export_default, ModuleInfo}; +use rquickjs::{ + function::Func, + module::{Declarations, Exports, ModuleDef}, + Ctx, Result, +}; + +mod brotli; +mod zlib; +mod zstd; + +use std::io::Read; + +use crate::llrt_utils::object::ObjectExt; +use rquickjs::{prelude::Opt, Exception, Value}; + +/// Reads the `maxOutputLength` option, which `node:zlib` uses to cap the output +/// of the convenience methods. +pub(crate) fn max_output_length<'js>(options: &Opt>) -> Result> { + match options.0.as_ref() { + Some(options) => options.get_optional::<_, usize>("maxOutputLength"), + None => Ok(None), + } +} + +/// Drains `reader` into a buffer, rejecting output longer than `limit` bytes +/// with the same `RangeError` Node.js raises for `maxOutputLength`. +/// +/// Reading stops one byte past the limit, so an over-long result is detected +/// without decompressing (or allocating) the rest of the payload. +pub(crate) fn read_to_end_limited( + ctx: &Ctx<'_>, + reader: R, + limit: Option, + capacity: usize, +) -> Result> { + let Some(limit) = limit else { + let mut dst = Vec::with_capacity(capacity); + let mut reader = reader; + reader.read_to_end(&mut dst)?; + return Ok(dst); + }; + + let cutoff = limit.saturating_add(1); + let mut dst = Vec::with_capacity(capacity.min(cutoff)); + reader.take(cutoff as u64).read_to_end(&mut dst)?; + + if dst.len() > limit { + return Err(Exception::throw_range( + ctx, + &[ + "Cannot create a Buffer larger than ", + &limit.to_string(), + " bytes", + ] + .concat(), + )); + } + + Ok(dst) +} + +use self::brotli::{br_comp, br_comp_sync, br_decomp, br_decomp_sync}; +use self::zlib::{ + deflate, deflate_raw, deflate_raw_sync, deflate_sync, gunzip, gunzip_sync, gzip, gzip_sync, + inflate, inflate_raw, inflate_raw_sync, inflate_sync, +}; +use self::zstd::{zstd_comp, zstd_comp_sync, zstd_decomp, zstd_decomp_sync}; + +#[macro_export] +macro_rules! define_sync_function { + ($fn_name:ident, $converter:expr, $command:expr) => { + pub(crate) fn $fn_name<'js>( + ctx: Ctx<'js>, + value: ObjectBytes<'js>, + options: Opt>, + ) -> Result> { + $converter(ctx.clone(), value, options, $command) + } + }; +} + +#[macro_export] +macro_rules! define_cb_function { + ($fn_name:ident, $converter:expr, $command:expr) => { + pub(crate) fn $fn_name<'js>( + ctx: Ctx<'js>, + value: ObjectBytes<'js>, + args: Rest>, + ) -> Result<()> { + let mut args_iter = args.0.into_iter().rev(); + let cb: Function = args_iter + .next() + .and_then(|v| v.into_function()) + .or_throw_msg(&ctx, "Callback parameter is not a function")?; + let options = match args_iter.next() { + Some(v) => Opt(Some(v)), + None => Opt(None), + }; + + ctx.clone().spawn_exit(async move { + match $converter(ctx.clone(), value, options, $command) { + Ok(obj) => { + () = cb.call((Null.into_js(&ctx), obj))?; + Ok::<_, Error>(()) + } + Err(err) => { + // `Error::Exception` is only a marker; the thrown value + // (and therefore the real message) lives in ctx.catch(). + let err = if matches!(err, Error::Exception) { + ctx.catch() + } else { + Exception::from_message(ctx.clone(), &err.to_string())?.into_value() + }; + () = cb.call((err,))?; + Ok(()) + } + } + })?; + Ok(()) + } + }; +} +pub struct ZlibModule; + +impl ModuleDef for ZlibModule { + fn declare(declare: &Declarations) -> Result<()> { + declare.declare("deflate")?; + declare.declare("deflateSync")?; + + declare.declare("deflateRaw")?; + declare.declare("deflateRawSync")?; + + declare.declare("gzip")?; + declare.declare("gzipSync")?; + + declare.declare("inflate")?; + declare.declare("inflateSync")?; + + declare.declare("inflateRaw")?; + declare.declare("inflateRawSync")?; + + declare.declare("gunzip")?; + declare.declare("gunzipSync")?; + + declare.declare("brotliCompress")?; + declare.declare("brotliCompressSync")?; + + declare.declare("brotliDecompress")?; + declare.declare("brotliDecompressSync")?; + + declare.declare("zstdCompress")?; + declare.declare("zstdCompressSync")?; + + declare.declare("zstdDecompress")?; + declare.declare("zstdDecompressSync")?; + + declare.declare("default")?; + Ok(()) + } + + fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { + export_default(ctx, exports, |default| { + default.set("deflate", Func::from(deflate))?; + default.set("deflateSync", Func::from(deflate_sync))?; + + default.set("deflateRaw", Func::from(deflate_raw))?; + default.set("deflateRawSync", Func::from(deflate_raw_sync))?; + + default.set("gzip", Func::from(gzip))?; + default.set("gzipSync", Func::from(gzip_sync))?; + + default.set("inflate", Func::from(inflate))?; + default.set("inflateSync", Func::from(inflate_sync))?; + + default.set("inflateRaw", Func::from(inflate_raw))?; + default.set("inflateRawSync", Func::from(inflate_raw_sync))?; + + default.set("gunzip", Func::from(gunzip))?; + default.set("gunzipSync", Func::from(gunzip_sync))?; + + default.set("brotliCompress", Func::from(br_comp))?; + default.set("brotliCompressSync", Func::from(br_comp_sync))?; + + default.set("brotliDecompress", Func::from(br_decomp))?; + default.set("brotliDecompressSync", Func::from(br_decomp_sync))?; + + default.set("zstdCompress", Func::from(zstd_comp))?; + default.set("zstdCompressSync", Func::from(zstd_comp_sync))?; + + default.set("zstdDecompress", Func::from(zstd_decomp))?; + default.set("zstdDecompressSync", Func::from(zstd_decomp_sync))?; + + Ok(()) + }) + } +} + +impl From for ModuleInfo { + fn from(val: ZlibModule) -> Self { + ModuleInfo { + name: "zlib", + module: val, + } + } +} + +// Macro exports move to the combined crate root. +pub(crate) use crate::{define_cb_function, define_sync_function}; diff --git a/stdlib/src/llrt/llrt_zlib/zlib.rs b/stdlib/src/llrt/llrt_zlib/zlib.rs new file mode 100644 index 00000000..a4a08a04 --- /dev/null +++ b/stdlib/src/llrt/llrt_zlib/zlib.rs @@ -0,0 +1,97 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use crate::llrt_buffer::Buffer; +use crate::llrt_context::CtxExtension; +use crate::llrt_utils::{bytes::ObjectBytes, object::ObjectExt, result::ResultExt}; +use rquickjs::{ + prelude::{Opt, Rest}, + Ctx, Error, Exception, Function, IntoJs, Null, Result, Value, +}; + +use super::{define_cb_function, define_sync_function, max_output_length, read_to_end_limited}; + +enum ZlibCommand { + Deflate, + DeflateRaw, + Gzip, + Inflate, + InflateRaw, + Gunzip, +} + +fn zlib_converter<'js>( + ctx: Ctx<'js>, + bytes: ObjectBytes<'js>, + options: Opt>, + command: ZlibCommand, +) -> Result> { + let src = bytes.as_bytes(&ctx)?; + + let mut level = crate::llrt_compression::zlib::Compression::default(); + if let Some(options) = options.0.as_ref() { + if let Some(opt) = options.get_optional("level")? { + level = crate::llrt_compression::zlib::Compression::new(opt); + } + } + let limit = max_output_length(&options)?; + + let dst = match command { + ZlibCommand::Deflate => read_to_end_limited( + &ctx, + crate::llrt_compression::zlib::encoder(src, level), + limit, + src.len(), + )?, + ZlibCommand::DeflateRaw => read_to_end_limited( + &ctx, + crate::llrt_compression::deflate::encoder(src, level), + limit, + src.len(), + )?, + ZlibCommand::Gzip => read_to_end_limited( + &ctx, + crate::llrt_compression::gz::encoder(src, level), + limit, + src.len(), + )?, + ZlibCommand::Inflate => read_to_end_limited( + &ctx, + crate::llrt_compression::zlib::decoder(src), + limit, + src.len(), + )?, + ZlibCommand::InflateRaw => read_to_end_limited( + &ctx, + crate::llrt_compression::deflate::decoder(src), + limit, + src.len(), + )?, + ZlibCommand::Gunzip => read_to_end_limited( + &ctx, + crate::llrt_compression::gz::decoder(src), + limit, + src.len(), + )?, + }; + + Buffer(dst).into_js(&ctx) +} + +define_cb_function!(deflate, zlib_converter, ZlibCommand::Deflate); +define_sync_function!(deflate_sync, zlib_converter, ZlibCommand::Deflate); + +define_cb_function!(deflate_raw, zlib_converter, ZlibCommand::DeflateRaw); +define_sync_function!(deflate_raw_sync, zlib_converter, ZlibCommand::DeflateRaw); + +define_cb_function!(gzip, zlib_converter, ZlibCommand::Gzip); +define_sync_function!(gzip_sync, zlib_converter, ZlibCommand::Gzip); + +define_cb_function!(inflate, zlib_converter, ZlibCommand::Inflate); +define_sync_function!(inflate_sync, zlib_converter, ZlibCommand::Inflate); + +define_cb_function!(inflate_raw, zlib_converter, ZlibCommand::InflateRaw); +define_sync_function!(inflate_raw_sync, zlib_converter, ZlibCommand::InflateRaw); + +define_cb_function!(gunzip, zlib_converter, ZlibCommand::Gunzip); +define_sync_function!(gunzip_sync, zlib_converter, ZlibCommand::Gunzip); diff --git a/stdlib/src/llrt/llrt_zlib/zstd.rs b/stdlib/src/llrt/llrt_zlib/zstd.rs new file mode 100644 index 00000000..66677f46 --- /dev/null +++ b/stdlib/src/llrt/llrt_zlib/zstd.rs @@ -0,0 +1,57 @@ +// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 +use crate::llrt_buffer::Buffer; +use crate::llrt_context::CtxExtension; +use crate::llrt_utils::{bytes::ObjectBytes, object::ObjectExt, result::ResultExt}; +use rquickjs::{ + prelude::{Opt, Rest}, + Ctx, Error, Exception, Function, IntoJs, Null, Result, Value, +}; + +use super::{define_cb_function, define_sync_function, max_output_length, read_to_end_limited}; + +enum ZstdCommand { + Compress, + Decompress, +} + +fn zstd_converter<'js>( + ctx: Ctx<'js>, + bytes: ObjectBytes<'js>, + options: Opt>, + command: ZstdCommand, +) -> Result> { + let src = bytes.as_bytes(&ctx)?; + + let mut level = crate::llrt_compression::zstd::DEFAULT_COMPRESSION_LEVEL; + if let Some(options) = options.0.as_ref() { + if let Some(opt) = options.get_optional("level")? { + level = opt; + } + } + let limit = max_output_length(&options)?; + + let dst = match command { + ZstdCommand::Compress => read_to_end_limited( + &ctx, + crate::llrt_compression::zstd::encoder(src, level)?, + limit, + src.len(), + )?, + ZstdCommand::Decompress => read_to_end_limited( + &ctx, + crate::llrt_compression::zstd::decoder(src)?, + limit, + src.len(), + )?, + }; + + Buffer(dst).into_js(&ctx) +} + +define_cb_function!(zstd_comp, zstd_converter, ZstdCommand::Compress); +define_sync_function!(zstd_comp_sync, zstd_converter, ZstdCommand::Compress); + +define_cb_function!(zstd_decomp, zstd_converter, ZstdCommand::Decompress); +define_sync_function!(zstd_decomp_sync, zstd_converter, ZstdCommand::Decompress); diff --git a/stdlib/tests/modules.rs b/stdlib/tests/modules.rs new file mode 100644 index 00000000..7a380a59 --- /dev/null +++ b/stdlib/tests/modules.rs @@ -0,0 +1,77 @@ +use quickjs_jit_stdlib as stdlib; +use rquickjs::{CatchResultExt, Context, Module, Runtime}; + +fn evaluate(source: &str) { + let runtime = Runtime::new().unwrap(); + runtime.set_loader(stdlib::resolver(), stdlib::loader()); + let context = Context::full(&runtime).unwrap(); + context.with(|ctx| { + stdlib::init(&ctx).unwrap(); + Module::evaluate(ctx.clone(), "consumer", source) + .unwrap() + .finish::<()>() + .catch(&ctx) + .unwrap(); + }); +} + +#[test] +fn modules_share_buffer_url_and_quickjs_types() { + evaluate( + r#" + import { Buffer as ImportedBuffer } from 'buffer'; + import path from 'path'; + import { URL as ImportedURL } from 'url'; + if (ImportedBuffer !== Buffer || ImportedURL !== URL) throw Error('different globals'); + if (Buffer.from('hello').toString('base64') !== 'aGVsbG8=') throw Error('buffer'); + if (path.normalize('./file') !== 'file') throw Error('path'); + if (new URL('/child?q=1', 'https://example.com/base').hostname !== 'example.com') throw Error('url'); + "#, + ); +} + +#[test] +fn crypto_hash_and_compression_roundtrip() { + evaluate( + r#" + import { createHash } from 'crypto'; + import { gzipSync, gunzipSync } from 'zlib'; + const digest = createHash('sha256').update('abc').digest('hex'); + if (digest !== 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad') throw Error(digest); + const input = Buffer.from('redistributed standard library'); + if (gunzipSync(gzipSync(input)).toString() !== input.toString()) throw Error('compression'); + "#, + ); +} + +#[test] +fn module_definitions_can_be_composed_by_a_host() { + let _: rquickjs::loader::ModuleLoader = rquickjs::loader::ModuleLoader::default() + .with_module("buffer", stdlib::buffer::BufferModule) + .with_module("crypto", stdlib::crypto::CryptoModule) + .with_module("path", stdlib::path::PathModule) + .with_module("url", stdlib::url::UrlModule) + .with_module("zlib", stdlib::zlib::ZlibModule); +} + +#[tokio::test] +async fn async_blob_crypto_and_compression_use_the_host_executor() { + let runtime = rquickjs::AsyncRuntime::new().unwrap(); + runtime + .set_loader(stdlib::resolver(), stdlib::loader()) + .await; + let context = rquickjs::AsyncContext::full(&runtime).await.unwrap(); + context.async_with(async |ctx| { + stdlib::init(&ctx).unwrap(); + Module::evaluate(ctx.clone(), "async-consumer", r#" + import { gzip, gunzip } from 'zlib'; + const text = 'async standard modules'; + if (await new Blob([text]).text() !== text) throw Error('Blob.text'); + const digest = await crypto.subtle.digest('SHA-256', Buffer.from('abc')); + if (Buffer.from(digest).toString('hex') !== 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad') throw Error('subtle.digest'); + const compressed = await new Promise((resolve, reject) => gzip(Buffer.from(text), (error, value) => error ? reject(error) : resolve(value))); + const plain = await new Promise((resolve, reject) => gunzip(compressed, (error, value) => error ? reject(error) : resolve(value))); + if (plain.toString() !== text) throw Error('async gzip'); + "#).unwrap().into_future::<()>().await.catch(&ctx).unwrap(); + }).await; +} From bfa699706ad35f352e28c0185b97b5d0989a4ecb Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 7 Sep 2026 16:06:57 +0800 Subject: [PATCH 2/3] chore: normalize redistributed notice whitespace Co-authored-by: Codex --- scripts/import-stdlib.py | 2 +- stdlib/NOTICE-LLRT | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/import-stdlib.py b/scripts/import-stdlib.py index c0399931..c28ed459 100644 --- a/scripts/import-stdlib.py +++ b/scripts/import-stdlib.py @@ -134,7 +134,7 @@ def dependencies(data): output.write_text(text) shutil.copy2(root / 'LICENSE', dest / 'LICENSE-APACHE') -shutil.copy2(root / 'NOTICE', dest / 'NOTICE-LLRT') +(dest / 'NOTICE-LLRT').write_text('\n'.join(line.rstrip() for line in (root / 'NOTICE').read_text().splitlines()) + '\n') (dest / 'UPSTREAM.json').write_text(json.dumps(provenance, indent=2, sort_keys=True) + '\n') def value(v): diff --git a/stdlib/NOTICE-LLRT b/stdlib/NOTICE-LLRT index 7104457d..349c8395 100644 --- a/stdlib/NOTICE-LLRT +++ b/stdlib/NOTICE-LLRT @@ -1,2 +1,2 @@ LLRT -Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. \ No newline at end of file +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. From 605da483611a3548edb8c33fdff602e3f5f42076 Mon Sep 17 00:00:00 2001 From: Jason Lee Date: Mon, 7 Sep 2026 16:26:38 +0800 Subject: [PATCH 3/3] refactor: use external LLRT dependencies for stdlib Replace vendored sources with a thin facade and verify host-owned JIT compatibility. Keep stdlib Git-only until compatible registry dependencies are available. Co-authored-by: Codex --- .github/workflows/stdlib.yml | 16 +- CHANGELOG.md | 2 +- README.md | 8 +- docs/superpowers/plans/2026-09-07-stdlib.md | 44 - scripts/check-stdlib-jit.py | 55 + scripts/check-stdlib-package.py | 39 - scripts/import-stdlib.py | 172 -- stdlib/Cargo.toml | 68 +- stdlib/LICENSE-APACHE | 202 -- stdlib/NOTICE | 11 - stdlib/NOTICE-LLRT | 2 - stdlib/README.md | 82 +- stdlib/UPSTREAM.json | 197 -- stdlib/src/lib.rs | 80 +- .../src/llrt/llrt_abort/abort_controller.rs | 62 - stdlib/src/llrt/llrt_abort/abort_signal.rs | 281 --- stdlib/src/llrt/llrt_abort/lib.rs | 26 - .../llrt_async_hooks/finalization_registry.rs | 65 - stdlib/src/llrt/llrt_async_hooks/lib.rs | 335 --- .../src/llrt/llrt_buffer/array_buffer_view.rs | 159 -- stdlib/src/llrt/llrt_buffer/blob.rs | 436 ---- stdlib/src/llrt/llrt_buffer/buffer.rs | 1039 -------- stdlib/src/llrt/llrt_buffer/file.rs | 129 - stdlib/src/llrt/llrt_buffer/lib.rs | 102 - stdlib/src/llrt/llrt_compression/lib.rs | 99 - stdlib/src/llrt/llrt_compression/streaming.rs | 98 - stdlib/src/llrt/llrt_context/lib.rs | 93 - stdlib/src/llrt/llrt_crypto/crc32.rs | 72 - stdlib/src/llrt/llrt_crypto/hash.rs | 217 -- stdlib/src/llrt/llrt_crypto/lib.rs | 385 --- .../src/llrt/llrt_crypto/provider/graviola.rs | 571 ----- stdlib/src/llrt/llrt_crypto/provider/mod.rs | 1268 ---------- .../src/llrt/llrt_crypto/provider/openssl.rs | 1319 ---------- stdlib/src/llrt/llrt_crypto/provider/ring.rs | 544 ----- .../llrt_crypto/provider/rust/aes_variants.rs | 285 --- .../src/llrt/llrt_crypto/provider/rust/mod.rs | 1654 ------------- .../src/llrt/llrt_crypto/subtle/crypto_key.rs | 165 -- .../llrt_crypto/subtle/derive_algorithm.rs | 77 - .../llrt/llrt_crypto/subtle/derive_bits.rs | 184 -- .../llrt/llrt_crypto/subtle/derive_keys.rs | 89 - stdlib/src/llrt/llrt_crypto/subtle/digest.rs | 59 - .../src/llrt/llrt_crypto/subtle/encryption.rs | 269 -- .../subtle/encryption_algorithm.rs | 120 - .../src/llrt/llrt_crypto/subtle/export_key.rs | 229 -- .../llrt/llrt_crypto/subtle/generate_key.rs | 137 -- .../src/llrt/llrt_crypto/subtle/import_key.rs | 76 - .../llrt/llrt_crypto/subtle/key_algorithm.rs | 1609 ------------ stdlib/src/llrt/llrt_crypto/subtle/mod.rs | 183 -- stdlib/src/llrt/llrt_crypto/subtle/sign.rs | 129 - .../llrt/llrt_crypto/subtle/sign_algorithm.rs | 58 - stdlib/src/llrt/llrt_crypto/subtle/stubs.rs | 64 - stdlib/src/llrt/llrt_crypto/subtle/util.rs | 87 - stdlib/src/llrt/llrt_crypto/subtle/verify.rs | 155 -- .../src/llrt/llrt_crypto/subtle/wrapping.rs | 93 - stdlib/src/llrt/llrt_encoding/lib.rs | 254 -- stdlib/src/llrt/llrt_events/custom_event.rs | 40 - stdlib/src/llrt/llrt_events/event.rs | 62 - stdlib/src/llrt/llrt_events/event_target.rs | 44 - stdlib/src/llrt/llrt_events/lib.rs | 580 ----- stdlib/src/llrt/llrt_exceptions/lib.rs | 464 ---- stdlib/src/llrt/llrt_hooking/lib.rs | 88 - stdlib/src/llrt/llrt_json/escape.rs | 341 --- stdlib/src/llrt/llrt_json/lib.rs | 233 -- stdlib/src/llrt/llrt_json/parse.rs | 105 - stdlib/src/llrt/llrt_json/stringify.rs | 552 ----- stdlib/src/llrt/llrt_path/lib.rs | 906 ------- stdlib/src/llrt/llrt_stream_web/lib.rs | 181 -- .../queuing_strategy/byte_length.rs | 36 - .../llrt_stream_web/queuing_strategy/count.rs | 36 - .../llrt_stream_web/queuing_strategy/mod.rs | 231 -- .../llrt_stream_web/queuing_strategy/tests.rs | 159 -- .../llrt_stream_web/readable/byob_reader.rs | 624 ----- .../readable/byte_controller.rs | 2169 ----------------- .../llrt_stream_web/readable/controller.rs | 200 -- .../readable/default_controller.rs | 960 -------- .../readable/default_reader.rs | 540 ---- .../llrt/llrt_stream_web/readable/iterator.rs | 698 ------ .../src/llrt/llrt_stream_web/readable/mod.rs | 31 - .../llrt/llrt_stream_web/readable/objects.rs | 459 ---- .../llrt/llrt_stream_web/readable/reader.rs | 404 --- .../readable/stream/algorithms.rs | 281 --- .../llrt_stream_web/readable/stream/mod.rs | 1117 --------- .../llrt_stream_web/readable/stream/pipe.rs | 700 ------ .../llrt_stream_web/readable/stream/source.rs | 36 - .../llrt_stream_web/readable/stream/tee.rs | 1713 ------------- .../llrt_stream_web/readable_writable_pair.rs | 25 - .../llrt_stream_web/transform/controller.rs | 308 --- .../src/llrt/llrt_stream_web/transform/mod.rs | 9 - .../llrt/llrt_stream_web/transform/stream.rs | 352 --- .../llrt/llrt_stream_web/transform/tests.rs | 440 ---- .../llrt_stream_web/transform/transformer.rs | 44 - stdlib/src/llrt/llrt_stream_web/utils/mod.rs | 58 - .../src/llrt/llrt_stream_web/utils/promise.rs | 260 -- .../src/llrt/llrt_stream_web/utils/queue.rs | 103 - .../writable/default_controller.rs | 871 ------- .../writable/default_writer.rs | 497 ---- .../src/llrt/llrt_stream_web/writable/mod.rs | 17 - .../llrt/llrt_stream_web/writable/objects.rs | 162 -- .../llrt_stream_web/writable/stream/mod.rs | 772 ------ .../llrt_stream_web/writable/stream/sink.rs | 35 - .../llrt/llrt_stream_web/writable/writer.rs | 79 - stdlib/src/llrt/llrt_test/lib.rs | 149 -- stdlib/src/llrt/llrt_timers/lib.rs | 557 ----- stdlib/src/llrt/llrt_url/lib.rs | 356 --- stdlib/src/llrt/llrt_url/url_class.rs | 347 --- stdlib/src/llrt/llrt_url/url_search_params.rs | 1058 -------- stdlib/src/llrt/llrt_utils/any_of.rs | 299 --- stdlib/src/llrt/llrt_utils/array_buffer.rs | 122 - .../src/llrt/llrt_utils/bytearray_buffer.rs | 227 -- stdlib/src/llrt/llrt_utils/bytes.rs | 679 ------ stdlib/src/llrt/llrt_utils/class.rs | 126 - stdlib/src/llrt/llrt_utils/clone.rs | 21 - stdlib/src/llrt/llrt_utils/ctx.rs | 18 - stdlib/src/llrt/llrt_utils/error.rs | 24 - stdlib/src/llrt/llrt_utils/error_messages.rs | 4 - stdlib/src/llrt/llrt_utils/fs.rs | 104 - stdlib/src/llrt/llrt_utils/hash.rs | 9 - stdlib/src/llrt/llrt_utils/io.rs | 31 - stdlib/src/llrt/llrt_utils/latch.rs | 31 - stdlib/src/llrt/llrt_utils/lib.rs | 37 - stdlib/src/llrt/llrt_utils/macros.rs | 56 - stdlib/src/llrt/llrt_utils/mc_oneshot.rs | 119 - stdlib/src/llrt/llrt_utils/module.rs | 30 - stdlib/src/llrt/llrt_utils/object.rs | 171 -- stdlib/src/llrt/llrt_utils/option.rs | 102 - stdlib/src/llrt/llrt_utils/primordials.rs | 166 -- stdlib/src/llrt/llrt_utils/provider.rs | 25 - stdlib/src/llrt/llrt_utils/result.rs | 105 - stdlib/src/llrt/llrt_utils/reuse_list.rs | 338 --- stdlib/src/llrt/llrt_utils/signals.rs | 150 -- stdlib/src/llrt/llrt_utils/string.rs | 27 - stdlib/src/llrt/llrt_utils/sysinfo.rs | 14 - stdlib/src/llrt/llrt_utils/time.rs | 48 - stdlib/src/llrt/llrt_zlib/brotli.rs | 50 - stdlib/src/llrt/llrt_zlib/lib.rs | 212 -- stdlib/src/llrt/llrt_zlib/zlib.rs | 97 - stdlib/src/llrt/llrt_zlib/zstd.rs | 57 - stdlib/tests/modules.rs | 2 +- 138 files changed, 120 insertions(+), 38124 deletions(-) delete mode 100644 docs/superpowers/plans/2026-09-07-stdlib.md create mode 100644 scripts/check-stdlib-jit.py delete mode 100644 scripts/check-stdlib-package.py delete mode 100644 scripts/import-stdlib.py delete mode 100644 stdlib/LICENSE-APACHE delete mode 100644 stdlib/NOTICE delete mode 100644 stdlib/NOTICE-LLRT delete mode 100644 stdlib/UPSTREAM.json delete mode 100644 stdlib/src/llrt/llrt_abort/abort_controller.rs delete mode 100644 stdlib/src/llrt/llrt_abort/abort_signal.rs delete mode 100644 stdlib/src/llrt/llrt_abort/lib.rs delete mode 100644 stdlib/src/llrt/llrt_async_hooks/finalization_registry.rs delete mode 100644 stdlib/src/llrt/llrt_async_hooks/lib.rs delete mode 100644 stdlib/src/llrt/llrt_buffer/array_buffer_view.rs delete mode 100644 stdlib/src/llrt/llrt_buffer/blob.rs delete mode 100644 stdlib/src/llrt/llrt_buffer/buffer.rs delete mode 100644 stdlib/src/llrt/llrt_buffer/file.rs delete mode 100644 stdlib/src/llrt/llrt_buffer/lib.rs delete mode 100644 stdlib/src/llrt/llrt_compression/lib.rs delete mode 100644 stdlib/src/llrt/llrt_compression/streaming.rs delete mode 100644 stdlib/src/llrt/llrt_context/lib.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/crc32.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/hash.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/lib.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/provider/graviola.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/provider/mod.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/provider/openssl.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/provider/ring.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/provider/rust/aes_variants.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/provider/rust/mod.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/subtle/crypto_key.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/subtle/derive_algorithm.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/subtle/derive_bits.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/subtle/derive_keys.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/subtle/digest.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/subtle/encryption.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/subtle/encryption_algorithm.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/subtle/export_key.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/subtle/generate_key.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/subtle/import_key.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/subtle/key_algorithm.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/subtle/mod.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/subtle/sign.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/subtle/sign_algorithm.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/subtle/stubs.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/subtle/util.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/subtle/verify.rs delete mode 100644 stdlib/src/llrt/llrt_crypto/subtle/wrapping.rs delete mode 100644 stdlib/src/llrt/llrt_encoding/lib.rs delete mode 100644 stdlib/src/llrt/llrt_events/custom_event.rs delete mode 100644 stdlib/src/llrt/llrt_events/event.rs delete mode 100644 stdlib/src/llrt/llrt_events/event_target.rs delete mode 100644 stdlib/src/llrt/llrt_events/lib.rs delete mode 100644 stdlib/src/llrt/llrt_exceptions/lib.rs delete mode 100644 stdlib/src/llrt/llrt_hooking/lib.rs delete mode 100644 stdlib/src/llrt/llrt_json/escape.rs delete mode 100644 stdlib/src/llrt/llrt_json/lib.rs delete mode 100644 stdlib/src/llrt/llrt_json/parse.rs delete mode 100644 stdlib/src/llrt/llrt_json/stringify.rs delete mode 100644 stdlib/src/llrt/llrt_path/lib.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/lib.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/queuing_strategy/byte_length.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/queuing_strategy/count.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/queuing_strategy/mod.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/queuing_strategy/tests.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/readable/byob_reader.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/readable/byte_controller.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/readable/controller.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/readable/default_controller.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/readable/default_reader.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/readable/iterator.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/readable/mod.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/readable/objects.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/readable/reader.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/readable/stream/algorithms.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/readable/stream/mod.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/readable/stream/pipe.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/readable/stream/source.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/readable/stream/tee.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/readable_writable_pair.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/transform/controller.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/transform/mod.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/transform/stream.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/transform/tests.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/transform/transformer.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/utils/mod.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/utils/promise.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/utils/queue.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/writable/default_controller.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/writable/default_writer.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/writable/mod.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/writable/objects.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/writable/stream/mod.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/writable/stream/sink.rs delete mode 100644 stdlib/src/llrt/llrt_stream_web/writable/writer.rs delete mode 100644 stdlib/src/llrt/llrt_test/lib.rs delete mode 100644 stdlib/src/llrt/llrt_timers/lib.rs delete mode 100644 stdlib/src/llrt/llrt_url/lib.rs delete mode 100644 stdlib/src/llrt/llrt_url/url_class.rs delete mode 100644 stdlib/src/llrt/llrt_url/url_search_params.rs delete mode 100644 stdlib/src/llrt/llrt_utils/any_of.rs delete mode 100644 stdlib/src/llrt/llrt_utils/array_buffer.rs delete mode 100644 stdlib/src/llrt/llrt_utils/bytearray_buffer.rs delete mode 100644 stdlib/src/llrt/llrt_utils/bytes.rs delete mode 100644 stdlib/src/llrt/llrt_utils/class.rs delete mode 100644 stdlib/src/llrt/llrt_utils/clone.rs delete mode 100644 stdlib/src/llrt/llrt_utils/ctx.rs delete mode 100644 stdlib/src/llrt/llrt_utils/error.rs delete mode 100644 stdlib/src/llrt/llrt_utils/error_messages.rs delete mode 100644 stdlib/src/llrt/llrt_utils/fs.rs delete mode 100644 stdlib/src/llrt/llrt_utils/hash.rs delete mode 100644 stdlib/src/llrt/llrt_utils/io.rs delete mode 100644 stdlib/src/llrt/llrt_utils/latch.rs delete mode 100644 stdlib/src/llrt/llrt_utils/lib.rs delete mode 100644 stdlib/src/llrt/llrt_utils/macros.rs delete mode 100644 stdlib/src/llrt/llrt_utils/mc_oneshot.rs delete mode 100644 stdlib/src/llrt/llrt_utils/module.rs delete mode 100644 stdlib/src/llrt/llrt_utils/object.rs delete mode 100644 stdlib/src/llrt/llrt_utils/option.rs delete mode 100644 stdlib/src/llrt/llrt_utils/primordials.rs delete mode 100644 stdlib/src/llrt/llrt_utils/provider.rs delete mode 100644 stdlib/src/llrt/llrt_utils/result.rs delete mode 100644 stdlib/src/llrt/llrt_utils/reuse_list.rs delete mode 100644 stdlib/src/llrt/llrt_utils/signals.rs delete mode 100644 stdlib/src/llrt/llrt_utils/string.rs delete mode 100644 stdlib/src/llrt/llrt_utils/sysinfo.rs delete mode 100644 stdlib/src/llrt/llrt_utils/time.rs delete mode 100644 stdlib/src/llrt/llrt_zlib/brotli.rs delete mode 100644 stdlib/src/llrt/llrt_zlib/lib.rs delete mode 100644 stdlib/src/llrt/llrt_zlib/zlib.rs delete mode 100644 stdlib/src/llrt/llrt_zlib/zstd.rs diff --git a/.github/workflows/stdlib.yml b/.github/workflows/stdlib.yml index 4fc6d2f3..8ff674cb 100644 --- a/.github/workflows/stdlib.yml +++ b/.github/workflows/stdlib.yml @@ -19,20 +19,10 @@ jobs: toolchain: stable - run: cargo test -p quickjs-jit-stdlib - run: cargo test -p quickjs-jit-stdlib --all-features - - package: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 + - uses: actions/setup-python@v6 with: - submodules: true - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: stable - - name: Verify the redistributed package and local binding releases - run: cargo package -p quickjs-jit-sys -p quickjs-jit-core -p quickjs-jit-macro -p quickjs-jit -p quickjs-jit-stdlib - - name: Check published dependency boundary - run: python3 scripts/check-stdlib-package.py target/package/quickjs-jit-stdlib-*.crate + python-version: '3.11' + - run: python scripts/check-stdlib-jit.py msrv: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index 82d64096..464ef8b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add `quickjs-jit-stdlib`, a single-package redistribution of the LLRT standard modules used by GPUI Shell, with bundled sources, provenance, and no LLRT package dependencies. +- Add `quickjs-jit-stdlib`, a thin facade over external LLRT standard modules, with host-owned JIT compatibility and no bundled LLRT sources. - Add pre-generated bindings for `riscv64gc-unknown-linux-gnu` and `riscv64a23-unknown-linux-gnu` - JIT M2: Tier 1 and Tier 2 now support the remaining comparison, bitwise, shift, `%`, unary diff --git a/README.md b/README.md index b4c5f6e3..43171b66 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,10 @@ and use Cargo source patches from that revision for both `quickjs-jit-core` and `quickjs-jit-sys`. Do not combine the published 0.12.2 runtime with 0.12.3 core or sys crates. -The optional [`quickjs-jit-stdlib`](stdlib/README.md) package redistributes -LLRT-derived Buffer, Crypto, Path, URL and Zlib modules in one crate. Its LLRT -implementation dependencies are bundled as source modules; applications do not -need LLRT packages or a Cargo compatibility patch. Stdlib requires Rust 1.89+. +The optional [`quickjs-jit-stdlib`](stdlib/README.md) package provides a thin +facade over external LLRT Buffer, Crypto, Path, URL and Zlib dependencies. +It contains no LLRT implementation sources. JIT hosts retain their application-owned +`rquickjs` compatibility patch. Stdlib requires Rust 1.89+ and is Git-only for now. [![github](https://img.shields.io/badge/github-longbridge/rquickjs-8da0cb.svg?style=for-the-badge&logo=github)](https://github.com/longbridge/rquickjs) [![crates](https://img.shields.io/crates/v/quickjs-jit.svg?style=for-the-badge&color=fc8d62&logo=rust)](https://crates.io/crates/quickjs-jit) diff --git a/docs/superpowers/plans/2026-09-07-stdlib.md b/docs/superpowers/plans/2026-09-07-stdlib.md deleted file mode 100644 index ebfa6fc2..00000000 --- a/docs/superpowers/plans/2026-09-07-stdlib.md +++ /dev/null @@ -1,44 +0,0 @@ -# Single-package LLRT redistribution - -The approved design is one publishable `quickjs-jit-stdlib` package containing -LLRT source modules, not a facade over separately published or Git LLRT crates. -Work is isolated in `.worktrees/stdlib`, branch `quickjs-jit-stdlib`, based on -v0.12.7; existing work in the main checkout and GPUI Kit remains untouched. - -Scope: GPUI Shell's buffer, crypto, path, URL and zlib modules, and their full -runtime dependency closure at LLRT 7b95c82a9b15e7ddfb2778eca4b5a63111e74f51. -Use its RustCrypto and compression-rust selections; no new JavaScript APIs or -backend selection system. All bindings use the distribution's quickjs-jit. -Expose the five module namespaces and module definitions from one Rust crate. -Retain copyright headers, Apache-2.0 license, provenance and a reproducible -importer. Rewrite former crate paths and exported macro paths into this crate. - -Implementation and validation: -- [x] Add consumer tests for modules, Buffer/URL globals, crypto hashing and - compression roundtrips; establish the missing-package failure. -- [x] Import runtime dependency closure as modules and merge active registry - dependencies, including target-specific settings. Retain upstream unit - tests with a local test helper where practical. -- [x] Implement registration/global initialization, document host-owned async - scheduling and the fact this is a subset of LLRT, not all Node APIs. -- [x] Run focused consumer and retained unit tests. Check features/type identity. -- [x] Inspect and extract the package, validate in an external consumer without - LLRT patches or Git dependencies. Any unpublished quickjs-jit release - prerequisite must be reported rather than publishing dependencies. -- [x] Document the GPUI Shell import migration and inspect final diffs. - -Acceptance: one distributable stdlib crate, no LLRT package dependency, no -rquickjs compatibility facade, functioning existing module behavior, clean -build from redistributed sources, no dependency on paths inside LLRT checkout. - - -Verification completed on Apple Silicon: -- 119 retained LLRT unit tests and 4 consumer integration tests pass with parallel enabled. -- README doctest passes; one upstream URLSearchParams doctest remains intentionally ignored. -- `cargo +1.89 check -p quickjs-jit-stdlib` passes. -- Cargo packages and verifies all four binding packages and stdlib together through its temporary registry. -- The final stdlib archive has only registry dependencies, no original rquickjs/LLRT packages, no nested Cargo manifests and all license/provenance files. -- Four tests pass in an external consumer assembled from extracted archives. Only the not-yet-published binding releases use temporary source substitutions. -- Importer idempotence, Rust formatting and whitespace checks pass. -- Linux/Windows and MSRV coverage are configured in `.github/workflows/stdlib.yml`; remote CI has not been run in this session. -- Main checkouts and Shell integration were left for their existing work; migration instructions are in stdlib/README.md. diff --git a/scripts/check-stdlib-jit.py b/scripts/check-stdlib-jit.py new file mode 100644 index 00000000..4919712d --- /dev/null +++ b/scripts/check-stdlib-jit.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python3 +"""Test the facade from an external JIT host with an application-owned patch.""" +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import tomllib + +root = Path(__file__).resolve().parents[1] +bindings = tomllib.loads((root / "Cargo.toml").read_text()) +version = bindings["package"]["version"] + +with tempfile.TemporaryDirectory(prefix="stdlib-jit-host-") as directory: + host = Path(directory) + compat = host / "compat" + (compat / "src").mkdir(parents=True) + # The fixture belongs to this temporary application, never to the library. + features = {name: [f"upstream/{name}"] for name in bindings["features"]} + (compat / "Cargo.toml").write_text( + '[package]\nname = "rquickjs"\n' + f'version = "{version}"\nedition = "2021"\npublish = false\n' + '[dependencies]\nupstream = { package = "quickjs-jit", ' + f'path = {json.dumps(str(root))}, default-features = false }}\n' + '[features]\n' + + ''.join(f'{name} = {json.dumps(values)}\n' for name, values in features.items()) + ) + (compat / "src/lib.rs").write_text('#![no_std]\npub use upstream::*;\n') + (host / "tests").mkdir() + shutil.copyfile(root / "stdlib/tests/modules.rs", host / "tests/modules.rs") + (host / "Cargo.toml").write_text( + '[package]\nname = "stdlib-jit-host"\nversion = "0.0.0"\n' + 'edition = "2021"\npublish = false\n[workspace]\n' + '[dependencies]\nrquickjs = { package = "quickjs-jit", ' + f'path = {json.dumps(str(root))}, features = ["futures", "loader", "macro", "half"] }}\n' + f'quickjs-jit-stdlib = {{ path = {json.dumps(str(root / "stdlib"))} }}\n' + 'tokio = { version = "1", features = ["macros", "rt", "time"] }\n' + '[features]\nparallel = ["quickjs-jit-stdlib/parallel", "rquickjs/parallel"]\n' + '[patch.crates-io]\nrquickjs = { path = "compat" }\n' + ) + env = dict(os.environ) + env.setdefault("CARGO_TARGET_DIR", str(root / "target/stdlib-jit-host")) + metadata = json.loads(subprocess.check_output( + ["cargo", "metadata", "--format-version", "1"], cwd=host, env=env + )) + packages = metadata["packages"] + assert not any(p["name"] in {"rquickjs-core", "rquickjs-sys", "rquickjs-macro"} + for p in packages), "Host resolved original bindings alongside JIT" + assert sum(p["name"] == "quickjs-jit-sys" for p in packages) == 1 + llrt = [p for p in packages if p["name"].startswith("llrt_")] + assert llrt and all((p["source"] or "").startswith("git+https://github.com/awslabs/llrt") + for p in llrt), "LLRT must remain an external Git dependency" + for flags in ([], ["--all-features"]): + subprocess.run(["cargo", "test", *flags], cwd=host, env=env, check=True) diff --git a/scripts/check-stdlib-package.py b/scripts/check-stdlib-package.py deleted file mode 100644 index 64e2dc67..00000000 --- a/scripts/check-stdlib-package.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 -"""Check that a Cargo archive is one self-contained LLRT redistribution.""" -import sys -import tarfile -if sys.version_info < (3, 11): - raise SystemExit("Python 3.11 or newer is required") -import tomllib - -with tarfile.open(sys.argv[1], 'r:gz') as archive: - names = archive.getnames() - root = names[0].split('/')[0] - def read(name): - return archive.extractfile(f'{root}/{name}').read() - manifest = tomllib.loads(read('Cargo.toml').decode()) - assert manifest['package']['name'] == 'quickjs-jit-stdlib' - assert not manifest.get('patch'), 'consumer patches are not a publication strategy' - sections = [manifest] - sections.extend(manifest.get('target', {}).values()) - for section in sections: - for kind in ['dependencies', 'dev-dependencies', 'build-dependencies']: - for name, spec in section.get(kind, {}).items(): - spec = {'version': spec} if isinstance(spec, str) else spec - package = spec.get('package', name) - assert not package.startswith('llrt_'), (kind, package) - assert not {'git', 'path', 'registry'} & spec.keys(), (kind, package, spec) - assert spec.get('version'), (kind, package) - assert manifest['dependencies']['rquickjs']['package'] == 'quickjs-jit' - for name in names: - assert not (name.endswith('/Cargo.toml') and name != f'{root}/Cargo.toml'), name - lock = tomllib.loads(read('Cargo.lock').decode()) - for package in lock['package']: - assert not package['name'].startswith('llrt_'), package - assert package['name'] not in {'rquickjs', 'rquickjs-core', 'rquickjs-sys', 'rquickjs-macro'}, package - assert not package.get('source', '').startswith('git+'), package - for required in ['LICENSE-APACHE', 'NOTICE', 'NOTICE-LLRT', 'UPSTREAM.json']: - assert read(required), required - for module in ['buffer', 'crypto', 'path', 'url', 'zlib']: - assert read(f'src/llrt/llrt_{module}/lib.rs'), module -print('PASS: one stdlib archive, registry-only dependencies, no LLRT packages or patches') diff --git a/scripts/import-stdlib.py b/scripts/import-stdlib.py deleted file mode 100644 index c28ed459..00000000 --- a/scripts/import-stdlib.py +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env python3 -"""Import the pinned LLRT runtime closure into one distributable Rust crate. - -Usage: python3 scripts/import-stdlib.py /path/to/llrt -The source checkout must match REVISION. This script never downloads code. -""" -import copy -import hashlib -import json -from pathlib import Path -import re -import shutil -import subprocess -import sys -if sys.version_info < (3, 11): - raise SystemExit("Python 3.11 or newer is required") -import tomllib - -REVISION = '7b95c82a9b15e7ddfb2778eca4b5a63111e74f51' -ROOTS = {'llrt_buffer': [], 'llrt_crypto': ['crypto-rust'], 'llrt_path': [], - 'llrt_url': [], 'llrt_zlib': ['compression-rust']} -root = Path(sys.argv[1]).resolve() -if subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip() != REVISION: - raise SystemExit('LLRT checkout must be at ' + REVISION) -subprocess.run(['git', '-C', str(root), 'diff', '--exit-code', 'HEAD', '--', 'libs', 'modules'], check=True, stdout=subprocess.DEVNULL) -dest = Path(__file__).resolve().parents[1] / 'stdlib' -crates = {} -for p in root.glob('*/*/Cargo.toml'): - data = tomllib.loads(p.read_text()) - if 'package' in data: - crates[data['package']['name']] = (p.parent, data) - -def dependencies(data): - result = [(None, k, v) for k, v in data.get('dependencies', {}).items()] - for target, section in data.get('target', {}).items(): - result += [(target, k, v) for k, v in section.get('dependencies', {}).items()] - return [(t, k, {'version': v} if isinstance(v, str) else v) for t, k, v in result] - -features = {name: set(fs) for name, fs in ROOTS.items()} -optional = {} -extra = {} -for name in ROOTS: - if name != 'llrt_zlib': features[name].add('default') -changed = True -while changed: - before = repr((features, optional, extra)) - for name, enabled in list(features.items()): - data = crates[name][1] - optional.setdefault(name, set()) - extra.setdefault(name, {}) - for feature in list(enabled): - for child in data.get('features', {}).get(feature, []): - if '/' in child: - dep, feat = child.split('/', 1) - conditional = dep.endswith('?') - dep = dep.rstrip('?') - if conditional and dep not in optional[name]: continue - optional[name].add(dep) - extra[name].setdefault(dep, set()).add(feat) - elif child.startswith('dep:'): - optional[name].add(child[4:]) - elif child in data.get('features', {}): - enabled.add(child) - else: - optional[name].add(child) - for _, dep, spec in dependencies(data): - if spec.get('optional') and dep not in optional[name]: continue - if dep.startswith('llrt_'): - fs = features.setdefault(dep, set()) - fs.update(spec.get('features', [])) - fs.update(extra[name].get(dep, set())) - if spec.get('default-features', True): fs.add('default') - changed = before != repr((features, optional, extra)) - -# Merge only selected external runtime dependencies. Optional backend selection -# is resolved during import: this package ships Shell's existing backend choices. -merged = {} -for name in features: - for target, dep, spec in dependencies(crates[name][1]): - if dep.startswith('llrt_'): continue - if spec.get('optional') and dep not in optional[name]: continue - spec = copy.deepcopy(spec) - spec.pop('optional', None) - spec.pop('path', None) - spec['features'] = sorted(set(spec.get('features', [])) | extra[name].get(dep, set())) - key = (target, dep) - if key in merged: - prior = merged[key] - if prior['version'] != spec['version'] and dep != 'rquickjs': - raise SystemExit(f'incompatible versions for {dep}: {prior} / {spec}') - prior['features'] = sorted(set(prior['features']) | set(spec['features'])) - prior['default-features'] = prior.get('default-features', True) or spec.get('default-features', True) - else: merged[key] = spec -binding = merged[(None, 'rquickjs')] -binding.update(package='quickjs-jit', version='=0.12.7', path='..') -binding['features'] = sorted(set(binding['features']) | {'std', 'loader'}) - -vendor = dest / 'src' / 'llrt' -if vendor.exists(): shutil.rmtree(vendor) -vendor.mkdir(parents=True) -provenance = {'repository': 'https://github.com/awslabs/llrt', 'revision': REVISION, - 'roots': ROOTS, 'features': {n: sorted(f) for n, f in sorted(features.items())}, 'files': {}} -# Keep the upstream unit tests and their helper in the same crate. -names = sorted(features) + ['llrt_test'] -macro_names = {} -for name in names: - src = crates[name][0] / 'src' - macro_names[name] = [] - for p in src.rglob('*.rs'): - text = p.read_text() - macro_names[name] += re.findall(r'#\[macro_export\]\s*macro_rules!\s+(\w+)', text) -for name in names: - src = crates[name][0] / 'src' - for p in src.rglob('*'): - if not p.is_file(): continue - output = vendor / name / p.relative_to(src) - output.parent.mkdir(parents=True, exist_ok=True) - raw = p.read_bytes() - provenance['files'][str(p.relative_to(root))] = hashlib.sha256(raw).hexdigest() - if p.suffix != '.rs': output.write_bytes(raw); continue - text = raw.decode() - text = text.replace('env!("CARGO_PKG_VERSION")', json.dumps(crates[name][1]['package']['version'])) - # Former crate-local paths are now local to the imported module. - text = re.sub(r'\bcrate::', f'crate::{name}::', text) - for other in names: - text = re.sub(r'(? { - signal: Class<'js, AbortSignal<'js>>, -} - -unsafe impl<'js> JsLifetime<'js> for AbortController<'js> { - type Changed<'to> = AbortController<'to>; -} - -#[rquickjs::methods] -impl<'js> AbortController<'js> { - #[qjs(constructor)] - pub fn new(ctx: Ctx<'js>) -> Result { - let signal = AbortSignal::new(); - - let abort_controller = Self { - signal: Class::instance(ctx, signal)?, - }; - Ok(abort_controller) - } - - #[qjs(get)] - pub fn signal(&self) -> Class<'js, AbortSignal<'js>> { - self.signal.clone() - } - - #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] - pub fn to_string_tag() -> &'static str { - stringify!(AbortController) - } - - pub fn abort( - ctx: Ctx<'js>, - this: This>, - reason: Opt>, - ) -> Result<()> { - let instance = this.0.borrow(); - let signal = instance.signal.clone(); - let mut signal_borrow = signal.borrow_mut(); - if signal_borrow.aborted { - //only once - return Ok(()); - } - signal_borrow.set_reason(reason); - drop(signal_borrow); - AbortSignal::send_aborted(This(signal), ctx)?; - - Ok(()) - } -} diff --git a/stdlib/src/llrt/llrt_abort/abort_signal.rs b/stdlib/src/llrt/llrt_abort/abort_signal.rs deleted file mode 100644 index bfce649c..00000000 --- a/stdlib/src/llrt/llrt_abort/abort_signal.rs +++ /dev/null @@ -1,281 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::sync::{Arc, RwLock}; - -use crate::llrt_events::{Emitter, EventEmitter, EventList}; -use crate::llrt_exceptions::{DOMException, DOMExceptionName}; -use crate::llrt_utils::mc_oneshot; -use rquickjs::{ - atom::PredefinedAtom, - class::{Trace, Tracer}, - function::OnceFn, - prelude::{Opt, This}, - Array, Class, Ctx, Error, Exception, Function, JsLifetime, Result, Undefined, Value, -}; - -#[derive(Clone)] -#[rquickjs::class] -pub struct AbortSignal<'js> { - emitter: EventEmitter<'js>, - pub aborted: bool, - reason: Option>, - pub sender: mc_oneshot::Sender>, -} - -unsafe impl<'js> JsLifetime<'js> for AbortSignal<'js> { - type Changed<'to> = AbortSignal<'to>; -} - -impl<'js> Trace<'js> for AbortSignal<'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - if let Some(reason) = &self.reason { - tracer.mark(reason); - } - self.emitter.trace(tracer); - self.sender.trace(tracer); - } -} - -impl<'js> Emitter<'js> for AbortSignal<'js> { - fn get_event_list(&self) -> Arc>> { - self.emitter.get_event_list() - } -} - -#[rquickjs::methods(rename_all = "camelCase")] -impl<'js> AbortSignal<'js> { - #[qjs(constructor)] - pub fn new() -> Self { - let (sender, _) = mc_oneshot::channel::>(); - Self { - emitter: EventEmitter::new(), - aborted: false, - reason: None, - sender, - } - } - - #[qjs(get, rename = "onabort")] - pub fn get_on_abort(&self) -> Option> { - Self::get_listeners_str(self, "abort").first().cloned() - } - - #[qjs(set, rename = "onabort")] - pub fn set_on_abort( - this: This>, - ctx: Ctx<'js>, - listener: Function<'js>, - ) -> Result<()> { - Self::add_event_listener_str(this.0, &ctx, "abort", listener, false, false)?; - Ok(()) - } - - pub fn remove_on_abort( - this: This>, - ctx: Ctx<'js>, - listener: Function<'js>, - ) -> Result<()> { - Self::remove_event_listener_str(this.0, &ctx, "abort", listener)?; - Ok(()) - } - - pub fn throw_if_aborted(&self, ctx: Ctx<'js>) -> Result<()> { - if self.aborted { - return Err(ctx.throw( - self.reason - .clone() - .unwrap_or_else(|| Undefined.into_value(ctx.clone())), - )); - } - Ok(()) - } - - #[qjs(static)] - pub fn any(ctx: Ctx<'js>, signals: Array<'js>) -> Result> { - let mut new_signal = AbortSignal::new(); - - let mut signal_instances = Vec::with_capacity(signals.len()); - - for signal in signals.iter() { - let signal: Value = signal?; - let signal: Class = Class::from_value(&signal) - .map_err(|_| Exception::throw_type(&ctx, "Value is not an AbortSignal instance"))?; - let signal_borrow = signal.borrow(); - if signal_borrow.aborted { - new_signal.aborted = true; - new_signal.reason.clone_from(&signal_borrow.reason); - let new_signal = Class::instance(ctx, new_signal)?; - return Ok(new_signal); - } else { - drop(signal_borrow); - signal_instances.push(signal); - } - } - - let new_signal_instance = Class::instance(ctx.clone(), new_signal)?; - for signal in signal_instances { - let signal_instance_2 = new_signal_instance.clone(); - Self::add_event_listener_str( - signal, - &ctx, - "abort", - Function::new( - ctx.clone(), - OnceFn::from(|ctx, signal| { - struct Args<'js>(Ctx<'js>, This>>); - let Args(ctx, signal) = Args(ctx, signal); - let mut borrow = signal_instance_2.borrow_mut(); - borrow.aborted = true; - borrow.reason.clone_from(&signal.borrow().reason); - drop(borrow); - Self::send_aborted(This(signal_instance_2), ctx) - }), - )?, - false, - true, - )?; - } - - Ok(new_signal_instance) - } - - #[qjs(get)] - pub fn aborted(&self) -> bool { - self.aborted - } - - #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] - pub fn to_string_tag() -> &'static str { - stringify!(AbortSignal) - } - - #[qjs(get)] - pub fn reason(&self) -> Option> { - self.reason.clone() - } - - #[qjs(set, rename = "reason")] - pub fn set_reason(&mut self, reason: Opt>) { - match reason.0 { - Some(new_reason) if !new_reason.is_undefined() => self.reason.replace(new_reason), - _ => self.reason.take(), - }; - } - - #[qjs(skip)] - pub fn send_aborted(this: This>, ctx: Ctx<'js>) -> Result<()> { - let mut borrow = this.borrow_mut(); - borrow.aborted = true; - let reason = get_reason_or_dom_exception( - &ctx, - borrow.reason.as_ref(), - DOMExceptionName::AbortError, - )?; - borrow.reason = Some(reason.clone()); - borrow.sender.send(reason); - drop(borrow); - Self::emit_str(this.0, &ctx, "abort", vec![], false)?; - Ok(()) - } - - #[qjs(static)] - pub fn abort(ctx: Ctx<'js>, reason: Opt>) -> Result> { - let mut signal = Self::new(); - signal.set_reason(reason); - let instance = Class::instance(ctx.clone(), signal)?; - Self::send_aborted(This(instance.clone()), ctx)?; - Ok(instance) - } - - #[qjs(static)] - pub fn timeout(ctx: Ctx<'js>, milliseconds: u64) -> Result> { - let timeout_error = - get_reason_or_dom_exception(&ctx, None, DOMExceptionName::TimeoutError)?; - - let signal = Self::new(); - let signal_instance = Class::instance(ctx.clone(), signal)?; - let signal_instance2 = signal_instance.clone(); - - let cb = Function::new( - ctx.clone(), - OnceFn::from(move |ctx| { - let mut borrow = signal_instance.borrow_mut(); - borrow.set_reason(Opt(Some(timeout_error))); - drop(borrow); - Self::send_aborted(This(signal_instance), ctx)?; - Ok::<_, Error>(()) - }), - )?; - - #[cfg(all())] - { - crate::llrt_timers::set_timeout_interval( - &ctx, - cb, - milliseconds, - crate::llrt_utils::provider::ProviderType::Timeout, - )?; - } - #[cfg(all(not(all()), any()))] - { - use crate::llrt_utils::ctx::CtxExtension; - ctx.clone().spawn_exit_simple(async move { - tokio::time::sleep(std::time::Duration::from_millis(milliseconds)).await; - cb.call::<_, ()>(())?; - Ok(()) - }); - } - #[cfg(all(not(any()), not(all())))] - { - compile_error!("Either the `sleep-tokio` or `sleep-timers` feature must be enabled") - } - - Ok(signal_instance2) - } -} - -fn get_reason_or_dom_exception<'js>( - ctx: &Ctx<'js>, - reason: Option<&Value<'js>>, - name: DOMExceptionName, -) -> Result> { - let reason = if let Some(reason) = reason { - reason.clone() - } else { - let ex = DOMException::new_with_name(ctx, name, String::new())?; - Class::instance(ctx.clone(), ex)?.into_value() - }; - Ok(reason) -} - -#[cfg(test)] -mod tests { - use std::time::Duration; - - use crate::llrt_test::test_async_with; - - use super::*; - - #[cfg(all())] - #[tokio::test] - async fn test_abort_signal() { - test_async_with(|ctx| { - crate::llrt_abort::init(&ctx).unwrap(); - crate::llrt_timers::init(&ctx).unwrap(); - Box::pin(async move { - let signal = AbortSignal::timeout(ctx, 5).unwrap(); - - assert!(!signal.borrow().aborted()); - - tokio::time::sleep(Duration::from_millis(50)).await; - - assert!(signal.borrow().aborted()); - let reason = signal.borrow().reason().unwrap(); - let reason = Class::::from_value(&reason).unwrap(); - assert_eq!(reason.borrow().name(), "TimeoutError"); - }) - }) - .await; - } -} diff --git a/stdlib/src/llrt/llrt_abort/lib.rs b/stdlib/src/llrt/llrt_abort/lib.rs deleted file mode 100644 index b379a331..00000000 --- a/stdlib/src/llrt/llrt_abort/lib.rs +++ /dev/null @@ -1,26 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -#![allow(clippy::new_without_default)] -use crate::llrt_events::Emitter; -use crate::llrt_utils::primordials::{BasePrimordials, Primordial}; -use rquickjs::{Class, Ctx, Result}; - -pub use self::{abort_controller::AbortController, abort_signal::AbortSignal}; - -mod abort_controller; -mod abort_signal; - -pub fn init(ctx: &Ctx<'_>) -> Result<()> { - let globals = ctx.globals(); - - BasePrimordials::init(ctx)?; - - Class::::define(&globals)?; - Class::::define(&globals)?; - - AbortSignal::add_event_emitter_prototype(ctx)?; - AbortSignal::add_event_target_prototype(ctx)?; - - Ok(()) -} diff --git a/stdlib/src/llrt/llrt_async_hooks/finalization_registry.rs b/stdlib/src/llrt/llrt_async_hooks/finalization_registry.rs deleted file mode 100644 index a52c6f56..00000000 --- a/stdlib/src/llrt/llrt_async_hooks/finalization_registry.rs +++ /dev/null @@ -1,65 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::cell::RefCell; - -use crate::llrt_utils::result::ResultExt; -use rquickjs::{prelude::Func, Ctx, Result, Value}; -use tracing::trace; - -use super::{remove_id_map, update_current_id, AsyncHookState}; - -pub(crate) fn init_finalization_registry(ctx: &Ctx<'_>) -> Result<()> { - let global = ctx.globals(); - - global.set( - "__invokeFinalizationHook", - Func::from(invoke_finalization_hook), - )?; - - let _: () = ctx.eval( - r#" - globalThis.asyncFinalizationRegistry = (() => { - const registry = new FinalizationRegistry(__invokeFinalizationHook); - return { - register(target, heldValue) { - registry.register(target, heldValue); - } - }; - })(); - "#, - )?; - - global.remove("__invokeFinalizationHook")?; - - Ok(()) -} - -fn invoke_finalization_hook<'js>(ctx: Ctx<'js>, uid: Value<'js>) -> Result<()> { - let bind_state = ctx.userdata::>().or_throw(&ctx)?; - let state = bind_state.borrow(); - if state.hooks.is_empty() { - return Ok(()); - } - - let uid = uid.as_number().unwrap() as usize; - - let current_id = remove_id_map(&ctx, uid)?; - if current_id.0 == 0 { - return Ok(()); - } - - update_current_id(&ctx, current_id)?; - trace!("Destroy[{}](async_id, trigger_id): {:?}", uid, current_id); - - for hook in &state.hooks { - if *hook.enabled.as_ref().borrow() { - if let Some(func) = &hook.destroy { - let _ = func - .call::<_, ()>((current_id.0,)) - .or_else(|_| func.call::<_, ()>(())); - } - } - } - Ok(()) -} diff --git a/stdlib/src/llrt/llrt_async_hooks/lib.rs b/stdlib/src/llrt/llrt_async_hooks/lib.rs deleted file mode 100644 index ff24e5a8..00000000 --- a/stdlib/src/llrt/llrt_async_hooks/lib.rs +++ /dev/null @@ -1,335 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::{cell::RefCell, collections::HashMap, marker::PhantomData, rc::Rc}; - -use crate::llrt_hooking::register_finalization_registry; -use crate::llrt_utils::{ - module::{export_default, ModuleInfo}, - result::ResultExt, -}; -use rquickjs::{ - module::{Declarations, Exports, ModuleDef}, - prelude::Func, - promise::PromiseHookType, - qjs, - runtime::PromiseHook, - Ctx, Function, JsLifetime, Object, Result, Value, -}; -use tracing::trace; - -mod finalization_registry; - -use crate::llrt_async_hooks::finalization_registry::init_finalization_registry; - -struct Hook<'js> { - enabled: Rc>, - init: Option>, - before: Option>, - after: Option>, - promise_resolve: Option>, - destroy: Option>, -} - -struct AsyncHookState<'js> { - hooks: Vec>, -} - -impl Default for AsyncHookState<'_> { - fn default() -> Self { - Self::new() - } -} - -impl AsyncHookState<'_> { - fn new() -> Self { - Self { hooks: Vec::new() } - } -} - -unsafe impl<'js> JsLifetime<'js> for AsyncHookState<'js> { - type Changed<'to> = AsyncHookState<'to>; -} - -struct AsyncHookIds<'js> { - next_async_id: u64, - id_map: HashMap, // (execution_async_id, trigger_async_id) - current_id: (u64, u64), // (execution_async_id, trigger_async_id) - _marker: PhantomData<&'js ()>, -} - -impl Default for AsyncHookIds<'_> { - fn default() -> Self { - Self::new() - } -} - -impl AsyncHookIds<'_> { - fn new() -> Self { - Self { - next_async_id: 1, - id_map: HashMap::new(), - current_id: (1, 1), - _marker: PhantomData, - } - } -} - -unsafe impl<'js> JsLifetime<'js> for AsyncHookIds<'js> { - type Changed<'to> = AsyncHookIds<'to>; -} - -fn create_hook<'js>(ctx: Ctx<'js>, hooks_obj: Object<'js>) -> Result> { - let init = hooks_obj.get::<_, Function>("init").ok(); - let before = hooks_obj.get::<_, Function>("before").ok(); - let after = hooks_obj.get::<_, Function>("after").ok(); - let promise_resolve = hooks_obj.get::<_, Function>("promiseResolve").ok(); - let destroy = hooks_obj.get::<_, Function>("destroy").ok(); - let enabled = Rc::new(RefCell::new(false)); - - let hook = Hook { - enabled: enabled.clone(), - init, - before, - after, - promise_resolve, - destroy, - }; - - let binding = ctx.userdata::>().or_throw(&ctx)?; - let mut state = binding.borrow_mut(); - state.hooks.push(hook); - - let obj = Object::new(ctx.clone())?; - { - let enabled_clone = enabled.clone(); - obj.set( - "enable", - Function::new(ctx.clone(), move || -> Result<()> { - *enabled_clone.borrow_mut() = true; - Ok(()) - }), - )?; - } - { - let enabled_clone = enabled.clone(); - obj.set( - "disable", - Function::new(ctx.clone(), move || -> Result<()> { - *enabled_clone.borrow_mut() = false; - Ok(()) - }), - )?; - } - - Ok(obj.into()) -} - -fn current_id() -> u64 { - // NOTE: This method is now obsolete. Therefore, it does not return a valid value. - // But we will define it because it is used by cls-hooked. - 0 -} - -fn execution_async_id(ctx: Ctx<'_>) -> Result { - let bind_ids = ctx.userdata::>().or_throw(&ctx)?; - let ids = bind_ids.borrow(); - Ok(ids.current_id.0) -} - -fn trigger_async_id(ctx: Ctx<'_>) -> Result { - let bind_ids = ctx.userdata::>().or_throw(&ctx)?; - let ids = bind_ids.borrow(); - Ok(ids.current_id.1) -} - -pub struct AsyncHooksModule; - -impl ModuleDef for AsyncHooksModule { - fn declare(declare: &Declarations) -> Result<()> { - declare.declare("createHook")?; - declare.declare("currentId")?; - declare.declare("executionAsyncId")?; - declare.declare("triggerAsyncId")?; - declare.declare("default")?; - - Ok(()) - } - - fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { - export_default(ctx, exports, |default| { - default.set("createHook", Func::from(create_hook))?; - default.set("currentId", Func::from(current_id))?; - default.set("executionAsyncId", Func::from(execution_async_id))?; - default.set("triggerAsyncId", Func::from(trigger_async_id))?; - - Ok(()) - })?; - - Ok(()) - } -} - -impl From for ModuleInfo { - fn from(val: AsyncHooksModule) -> Self { - ModuleInfo { - name: "async_hooks", - module: val, - } - } -} - -pub fn init(ctx: &Ctx<'_>) -> Result<()> { - let global = ctx.globals(); - - let _ = ctx.store_userdata(RefCell::new(AsyncHookState::default())); - let _ = ctx.store_userdata(RefCell::new(AsyncHookIds::default())); - - global.set( - "invokeAsyncHook", - Func::from( - move |ctx: Ctx<'_>, type_: String, async_type: String, uid: usize| { - let type_ = match type_.as_ref() { - "init" => PromiseHookType::Init, - "before" => PromiseHookType::Before, - "after" => PromiseHookType::After, - "resolve" => PromiseHookType::Resolve, - _ => return, - }; - - let _ = invoke_async_hook(&ctx, type_, async_type.as_ref(), uid, None); - }, - ), - )?; - - init_finalization_registry(ctx)?; - - Ok(()) -} - -pub fn promise_hook_tracker() -> PromiseHook { - Box::new( - |ctx: Ctx<'_>, type_: PromiseHookType, promise: Value<'_>, parent: Value<'_>| { - // SAFETY: Since it checks in advance whether it is an Object type, we can always get a pointer to the object. - let object = promise - .as_object() - .map(|v| unsafe { qjs::JS_VALUE_GET_PTR(v.as_raw()) } as usize) - .unwrap(); - let parent = parent - .as_object() - .map(|v| unsafe { qjs::JS_VALUE_GET_PTR(v.as_raw()) } as usize); - - if type_ == PromiseHookType::Init { - let _ = register_finalization_registry(&ctx, promise, object); - } - - let _ = invoke_async_hook(&ctx, type_, "PROMISE", object, parent); - }, - ) -} - -fn invoke_async_hook( - ctx: &Ctx<'_>, - type_: PromiseHookType, - async_type: &str, - object: usize, - parent: Option, -) -> Result<()> { - let bind_state = ctx.userdata::>().or_throw(ctx)?; - let state = bind_state.borrow(); - - if state.hooks.is_empty() { - return Ok(()); - } - - match type_ { - PromiseHookType::Init => { - let current_id = insert_id_map(ctx, object, parent, async_type == "PROMISE")?; - trace!("Init(async_id, trigger_id): {:?}", current_id); - update_current_id(ctx, current_id)?; - - for hook in &state.hooks { - if *hook.enabled.as_ref().borrow() { - if let Some(func) = &hook.init { - let _ = func - .call::<_, ()>((current_id.0, async_type, current_id.1)) - .or_else(|_| func.call::<_, ()>((current_id.0, async_type))) - .or_else(|_| func.call::<_, ()>((current_id.0,))) - .or_else(|_| func.call::<_, ()>(())); - } - } - } - } - PromiseHookType::Before | PromiseHookType::After | PromiseHookType::Resolve => { - let current_id = get_id_map(ctx, object)?; - if current_id.0 == 0 { - return Ok(()); - } - - let _type = match type_ { - PromiseHookType::Before => "Before", - PromiseHookType::After => "After", - PromiseHookType::Resolve => "Resolve", - _ => unreachable!(), - }; - trace!("{}(async_id, trigger_id): {:?}", _type, current_id); - update_current_id(ctx, current_id)?; - - for hook in &state.hooks { - if *hook.enabled.as_ref().borrow() { - if let Some(func) = match type_ { - PromiseHookType::Before => &hook.before, - PromiseHookType::After => &hook.after, - PromiseHookType::Resolve => &hook.promise_resolve, - _ => unreachable!(), - } { - let _ = func - .call::<_, ()>((current_id.0,)) - .or_else(|_| func.call::<_, ()>(())); - } - } - } - } - } - Ok(()) -} - -fn insert_id_map( - ctx: &Ctx<'_>, - target: usize, - parent: Option, - is_promise: bool, -) -> Result<(u64, u64)> { - let bind_ids = ctx.userdata::>().or_throw(ctx)?; - let mut ids = bind_ids.borrow_mut(); - ids.next_async_id = ids.next_async_id.wrapping_add(1); - let async_id = ids.next_async_id; - let trigger_id = parent - .and_then(|tid| ids.id_map.get(&tid)) - .map(|id| id.0) - .unwrap_or(if is_promise { 1 } else { ids.current_id.1 }); - ids.id_map.insert(target, (async_id, trigger_id)); - Ok((async_id, trigger_id)) -} - -fn get_id_map(ctx: &Ctx<'_>, target: usize) -> Result<(u64, u64)> { - let bind_ids = ctx.userdata::>().or_throw(ctx)?; - let ids = bind_ids.borrow(); - Ok(*ids.id_map.get(&target).unwrap_or(&(0, 0))) -} - -fn remove_id_map(ctx: &Ctx<'_>, target: usize) -> Result<(u64, u64)> { - let bind_ids = ctx.userdata::>().or_throw(ctx)?; - let mut ids = bind_ids.borrow_mut(); - Ok(ids - .id_map - .remove_entry(&target) - .map(|(_, (async_id, trigger_id))| (async_id, trigger_id)) - .unwrap_or((0, 0))) -} - -fn update_current_id(ctx: &Ctx<'_>, id: (u64, u64)) -> Result<()> { - let bind_ids = ctx.userdata::>().or_throw(ctx)?; - bind_ids.borrow_mut().current_id = id; - Ok(()) -} diff --git a/stdlib/src/llrt/llrt_buffer/array_buffer_view.rs b/stdlib/src/llrt/llrt_buffer/array_buffer_view.rs deleted file mode 100644 index 2d0baa91..00000000 --- a/stdlib/src/llrt/llrt_buffer/array_buffer_view.rs +++ /dev/null @@ -1,159 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -#![allow(clippy::uninlined_format_args)] - -use std::ptr::NonNull; - -use rquickjs::{ArrayBuffer, Ctx, Error, FromJs, IntoJs, Object, Result, TypedArray, Value}; - -use crate::llrt_buffer::Buffer; - -pub struct ArrayBufferView<'js> { - value: Value<'js>, - buffer: Option, -} - -struct RawArrayBuffer { - len: usize, - ptr: NonNull, -} - -impl RawArrayBuffer { - pub fn new(len: usize, ptr: NonNull) -> Self { - Self { len, ptr } - } -} - -impl<'js> IntoJs<'js> for ArrayBufferView<'js> { - fn into_js(self, _ctx: &Ctx<'js>) -> Result> { - Ok(self.value) - } -} - -impl<'js> FromJs<'js> for ArrayBufferView<'js> { - fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let ty_name = value.type_name(); - let obj = Object::from_value(value.clone()) - .map_err(|_| Error::new_from_js(ty_name, "ArrayBufferView"))?; - - if let Some(array_buffer) = ArrayBuffer::from_object(obj.clone()) { - let buffer = array_buffer - .as_raw() - .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); - return Ok(ArrayBufferView { value, buffer }); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - let buffer = typed_array - .as_raw() - .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); - return Ok(ArrayBufferView { value, buffer }); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - let buffer = typed_array - .as_raw() - .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); - return Ok(ArrayBufferView { value, buffer }); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - let buffer = typed_array - .as_raw() - .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); - return Ok(ArrayBufferView { value, buffer }); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - let buffer = typed_array - .as_raw() - .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); - return Ok(ArrayBufferView { value, buffer }); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - let buffer = typed_array - .as_raw() - .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); - return Ok(ArrayBufferView { value, buffer }); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - let buffer = typed_array - .as_raw() - .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); - return Ok(ArrayBufferView { value, buffer }); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - let buffer = typed_array - .as_raw() - .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); - return Ok(ArrayBufferView { value, buffer }); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - let buffer = typed_array - .as_raw() - .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); - return Ok(ArrayBufferView { value, buffer }); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - let buffer = typed_array - .as_raw() - .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); - return Ok(ArrayBufferView { value, buffer }); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - let buffer = typed_array - .as_raw() - .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); - return Ok(ArrayBufferView { value, buffer }); - } - - if let Ok(array_buffer) = obj.get::<_, ArrayBuffer>("buffer") { - let buffer = array_buffer - .as_raw() - .map(|raw| RawArrayBuffer::new(raw.len, raw.ptr)); - return Ok(ArrayBufferView { value, buffer }); - } - - Err(Error::new_from_js(ty_name, "ArrayBufferView")) - } -} - -impl<'js> ArrayBufferView<'js> { - pub fn from_buffer(ctx: &Ctx<'js>, buffer: Buffer) -> Result { - let value = buffer.into_js(ctx)?; - Self::from_js(ctx, value) - } - - pub fn len(&self) -> usize { - self.buffer.as_ref().map(|b| b.len).unwrap_or(0) - } - - #[allow(dead_code)] - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - pub fn as_bytes(&self) -> Option<&[u8]> { - self.buffer - .as_ref() - .map(|b| unsafe { std::slice::from_raw_parts(b.ptr.as_ptr(), b.len) }) - } - - /// Mutable buffer for the view. - /// - /// # Safety - /// This is only safe if you have a lock on the runtime. - /// Do not pass it directly to other threads. - pub fn as_bytes_mut(&mut self) -> Option<&mut [u8]> { - self.buffer - .as_ref() - .map(|b| unsafe { std::slice::from_raw_parts_mut(b.ptr.as_ptr(), b.len) }) - } -} diff --git a/stdlib/src/llrt/llrt_buffer/blob.rs b/stdlib/src/llrt/llrt_buffer/blob.rs deleted file mode 100644 index c77165d3..00000000 --- a/stdlib/src/llrt/llrt_buffer/blob.rs +++ /dev/null @@ -1,436 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::ops::RangeInclusive; - -use crate::llrt_stream_web::{ - readable_byte_stream_controller_close_stream, - readable_byte_stream_controller_enqueue_bytes_borrowed, utils::promise::PromisePrimordials, - CancelAlgorithm, PullAlgorithm, ReadableStream, ReadableStreamControllerClass, -}; -use crate::llrt_utils::{ - array_buffer::shared_array_buffer_view, - bytes::{get_lossy_string, ObjectBytes}, - object::not_a_object_error, - primordials::Primordial, - result::ResultExt, - string::get_coerced_defined_string, -}; -use rquickjs::{ - atom::PredefinedAtom, class::Trace, function::Opt, prelude::This, Array, ArrayBuffer, Class, - Coerced, Ctx, Exception, FromJs, IntoJs, JsIterator, Result, TypedArray, Value, -}; - -use super::file::File; - -struct ArrayPartsIter<'js> { - array: Array<'js>, - index: usize, -} - -impl<'js> ArrayPartsIter<'js> { - fn new(array: Array<'js>) -> Self { - Self { array, index: 0 } - } -} - -impl<'js> Iterator for ArrayPartsIter<'js> { - type Item = Result>; - - fn next(&mut self) -> Option { - let len: usize = match self.array.as_object().get(PredefinedAtom::Length) { - Ok(v) => v, - Err(e) => return Some(Err(e)), - }; - if self.index >= len { - return None; - } - let result = self.array.get(self.index); - self.index += 1; - Some(result) - } -} - -enum EndingType { - Native, - Transparent, -} - -#[cfg(windows)] -const LINE_ENDING: &[u8] = b"\r\n"; -#[cfg(not(windows))] -const LINE_ENDING: &[u8] = b"\n"; - -#[rquickjs::class] -#[derive(Trace, Clone, rquickjs::JsLifetime)] -pub struct Blob<'js> { - /// Bytes live in a JS-owned `ArrayBuffer` so `.arrayBuffer()` / `.bytes()` - /// / `.stream()` can hand out refcount-bumped views without copying. - data: ArrayBuffer<'js>, - mime_type: String, -} - -fn normalize_type(mut mime_type: String) -> String { - static INVALID_RANGE: RangeInclusive = 0x0020..=0x007E; - - let bytes = unsafe { mime_type.as_bytes_mut() }; - for byte in bytes { - if !INVALID_RANGE.contains(byte) { - return String::new(); - } - byte.make_ascii_lowercase(); - } - mime_type -} - -#[rquickjs::methods] -impl<'js> Blob<'js> { - #[qjs(constructor)] - pub fn new( - ctx: Ctx<'js>, - this: This>, - parts: Opt>, - options: Opt>, - ) -> Result { - if this.as_function().is_none() { - return Err(Exception::throw_type( - &ctx, - "Failed to construct 'Blob': Please use the 'new' operator", - )); - } - - Self::from_parts(ctx, parts, options) - } - - #[qjs(get)] - pub fn size(&self) -> usize { - self.data.len() - } - - #[qjs(get, rename = "type")] - pub fn mime_type(&self) -> String { - self.mime_type.clone() - } - - pub async fn text(&self) -> String { - String::from_utf8_lossy(self.as_bytes()).to_string() - } - - #[qjs(rename = "arrayBuffer")] - pub async fn array_buffer(&self, ctx: Ctx<'js>) -> Result> { - //should be mutable according to spec, thus copy is required - ArrayBuffer::new_copy(ctx, self.as_bytes()) - } - - pub async fn bytes(&self, ctx: Ctx<'js>) -> Result> { - //should be mutable according to spec, thus copy is required - let ab = ArrayBuffer::new_copy(ctx, self.as_bytes())?; - TypedArray::::from_arraybuffer(ab).map(|t| t.into_value()) - } - - pub fn slice( - &self, - ctx: Ctx<'js>, - start: Opt>, - end: Opt>, - content_type: Opt>, - ) -> Result> { - let start = start.0.and_then(|v| v.as_number()).map(clamp_long_long); - let end = end.0.and_then(|v| v.as_number()).map(clamp_long_long); - Self::slice_blob(self, &ctx, start, end, content_type.0) - } - - pub fn stream(&self, ctx: Ctx<'js>) -> Result> { - let data = self.data.clone(); - let pull = PullAlgorithm::from_fn_once( - move |ctx: Ctx<'js>, controller: ReadableStreamControllerClass<'js>| { - let ctrl = match controller { - ReadableStreamControllerClass::ReadableStreamByteController(c) => c, - _ => return Err(Exception::throw_type(&ctx, "Expected byte controller")), - }; - let len = data.len(); - if len != 0 { - let view = shared_array_buffer_view(&ctx, &data, 0, len)?; - readable_byte_stream_controller_enqueue_bytes_borrowed( - ctx.clone(), - ctrl.clone(), - view, - )?; - } - readable_byte_stream_controller_close_stream(ctx.clone(), ctrl)?; - Ok(PromisePrimordials::get(&ctx)? - .promise_resolved_with_undefined - .clone()) - }, - ); - // Byte-source stream so callers can use `getReader({ mode: 'byob' })`. - // Matches spec: Blob.stream() returns a `type: "bytes"` ReadableStream. - let stream = ReadableStream::from_byte_pull_algorithm( - ctx, - pull, - CancelAlgorithm::ReturnPromiseUndefined, - )?; - Ok(stream.into_value()) - } - - #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] - pub fn to_string_tag() -> &'static str { - stringify!(Blob) - } - - #[qjs(static, rename = PredefinedAtom::SymbolHasInstance)] - pub fn has_instance(value: Value<'js>) -> bool { - if let Some(obj) = value.as_object() { - return obj.instance_of::() || obj.instance_of::(); - } - false - } - - #[qjs(skip)] - pub fn slice_blob( - &self, - ctx: &Ctx<'js>, - start: Option, - end: Option, - content_type: Option>, - ) -> Result> { - let bytes = self.as_bytes(); - let len = bytes.len(); - let start = start.unwrap_or_default(); - let start = if start < 0 { - (len as isize + start).max(0) as usize - } else { - len.min(start as usize) - }; - let end = end.unwrap_or(len as isize); - let end = if end < 0 { - (len as isize + end).max(0) as usize - } else { - len.min(end as usize) - }; - let data = shared_array_buffer_view(ctx, &self.data, start, end.saturating_sub(start))?; - let mime_type = get_coerced_defined_string(&content_type); - let mime_type = mime_type.map(normalize_type).unwrap_or_default(); - Ok(Blob { mime_type, data }) - } -} - -impl<'js> Blob<'js> { - pub fn from_bytes(ctx: &Ctx<'js>, data: Vec, content_type: Option) -> Result { - let mime_type = content_type.map(normalize_type).unwrap_or_default(); - let data = ArrayBuffer::new(ctx.clone(), data)?; - Ok(Self { mime_type, data }) - } - - pub fn from_parts( - ctx: Ctx<'js>, - parts: Opt>, - options: Opt>, - ) -> Result { - if let Some(options) = options.0.as_ref() { - if !options.is_null() && !options.is_undefined() && options.as_object().is_none() { - return Err(not_a_object_error(&ctx, "options")); - } - } - - let mut endings = EndingType::Transparent; - if let Some(options) = options.0.as_ref() { - if let Some(opts) = options.as_object() { - if opts.contains_key("endings")? { - if let Some(parsed) = parse_endings(&ctx, opts.get("endings")?)? { - endings = parsed; - } - } - } - } - - let bytes = if let Some(parts) = parts.0 { - bytes_from_parts(&ctx, parts, endings)? - } else { - Vec::new() - }; - - let mut mime_type = String::new(); - if let Some(options) = options.0.as_ref() { - if let Some(opts) = options.as_object() { - if let Some(x) = opts.get::<_, Option>>("type")? { - mime_type = normalize_type(x.to_string()); - } - } - } - - // Transfer Vec ownership to JS — QuickJS calls the drop callback when - // the ArrayBuffer is GC'd, so no extra Rust-side copy. - let data = ArrayBuffer::new(ctx, bytes)?; - - Ok(Self { data, mime_type }) - } - - pub fn get_bytes(&self) -> Vec { - self.as_bytes().to_vec() - } - - /// Zero-copy access to the underlying `ArrayBuffer`. Cloning the handle is - /// cheap (it's a JS-refcount bump); no bytes are copied. Useful for - /// consumers that want to pass the Blob body on to hyper via - /// `ObjectBytes::DataView` without the `get_bytes()` allocation. - pub fn array_buffer_ref(&self) -> ArrayBuffer<'js> { - self.data.clone() - } - - /// Borrow the underlying bytes directly. Returns `&[]` if the ArrayBuffer - /// has been detached (shouldn't happen in normal blob flow). - pub fn as_bytes(&self) -> &[u8] { - self.data.as_bytes().unwrap_or(&[]) - } -} - -fn bytes_from_parts<'js>( - ctx: &Ctx<'js>, - parts: Value<'js>, - endings: EndingType, -) -> Result> { - if parts.is_undefined() { - return Ok(Vec::new()); - } - - if let Some(array) = parts.clone().into_array() { - return process_parts(ctx, ArrayPartsIter::new(array), endings); - } - - process_parts(ctx, JsIterator::from_js(ctx, parts)?, endings) -} - -fn process_parts<'js, I>(ctx: &Ctx<'js>, iter: I, endings: EndingType) -> Result> -where - I: IntoIterator>>, -{ - let mut data = Vec::new(); - for elem in iter { - let elem = elem?; - if let Some(arr) = elem.as_array() { - let string = array_to_string(arr)?; - data.extend_from_slice(string.as_bytes()); - continue; - } - if let Some(object) = elem.as_object() { - if let Some(x) = Class::::from_object(object) { - data.extend_from_slice(x.borrow().as_bytes()); - continue; - } - if let Some(x) = Class::::from_object(object) { - let file = x.borrow(); - let end = Some(file.size().try_into().or_throw(ctx)?); - let mime_type = Some(file.mime_type().into_js(ctx)?); - let sub = file.slice(ctx.clone(), Opt(Some(0)), Opt(end), Opt(mime_type))?; - data.extend_from_slice(sub.as_bytes()); - continue; - } - if let Ok(x) = ObjectBytes::from(ctx, object) { - data.extend_from_slice(x.as_bytes(ctx).map_err(|_| { - Exception::throw_type(ctx, "Cannot create a blob with detached buffer") - })?); - continue; - } - if let Some(x) = ArrayBuffer::from_object(object.clone()) { - data.extend_from_slice(x.as_bytes().ok_or_else(|| { - Exception::throw_type(ctx, "Cannot create a blob with detached buffer") - })?); - continue; - } - } - - let string = if elem.is_string() { - get_lossy_string(elem)? - } else { - Coerced::::from_js(ctx, elem)?.0 - }; - if let EndingType::Transparent = endings { - data.extend_from_slice(string.as_bytes()); - } else { - let len = string.len(); - data.reserve(len); - - let bytes = string.as_bytes(); - let mut iter = bytes.iter(); - - let mut start = 0usize; - let mut i = 0usize; - let line_ending_is_n = LINE_ENDING[0] == b'\n'; - - while let Some(byte) = iter.next() { - if byte == &b'\r' { - if let Some(next_byte) = iter.next() { - data.extend(&bytes[start..i]); - i += 1; - start = i + 1; - if next_byte != &b'\n' { - data.extend([b'\r', *next_byte]); - } else { - data.extend(LINE_ENDING); - } - } - } else if byte == &b'\n' && !line_ending_is_n { - data.extend(&bytes[start..i]); - data.extend(LINE_ENDING); - start = i + 1; - }; - i += 1; - } - - if start < len { - data.extend(&bytes[start..len]); - } - } - } - Ok(data) -} - -fn parse_endings<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> Result> { - if value.is_undefined() { - return Ok(None); - } - let endings = match Coerced::::from_js(ctx, value)?.0.as_str() { - "transparent" => Some(EndingType::Transparent), - "native" => Some(EndingType::Native), - _ => { - return Err(Exception::throw_type( - ctx, - r#"expected 'endings' to be either 'transparent' or 'native'"#, - )); - } - }; - Ok(endings) -} - -fn array_to_string(array: &Array) -> Result { - let mut itoa_buffer = itoa::Buffer::new(); - let mut ryu_buffer = ryu::Buffer::new(); - - let parts = array - .clone() - .into_iter() - .map(|value| { - let value = value?; - if let Some(string) = value.as_string() { - Ok(string.to_string()?) - } else if let Some(number) = value.as_int() { - Ok(itoa_buffer.format(number).to_string()) - } else if let Some(number) = value.as_float() { - Ok(ryu_buffer.format(number).to_string()) - } else { - Ok(String::new()) - } - }) - .collect::>>()?; - - Ok(parts.join(",")) -} - -fn clamp_long_long(value: f64) -> isize { - if value.is_nan() { - return 0; - } - let rounded = value.round_ties_even(); - rounded.clamp(isize::MIN as f64, isize::MAX as f64) as isize -} diff --git a/stdlib/src/llrt/llrt_buffer/buffer.rs b/stdlib/src/llrt/llrt_buffer/buffer.rs deleted file mode 100644 index 48371c0d..00000000 --- a/stdlib/src/llrt/llrt_buffer/buffer.rs +++ /dev/null @@ -1,1039 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::{mem::MaybeUninit, slice}; - -use crate::llrt_encoding::Encoder; -use crate::llrt_utils::{ - bytes::{get_array_bytes, get_start_end_indexes, ObjectBytes}, - error_messages::{ERROR_MSG_ARRAY_BUFFER_DETACHED, ERROR_MSG_NOT_ARRAY_BUFFER}, - iterable_enum, - primordials::Primordial, - result::ResultExt, - string::{get_coerced_string, get_string}, -}; -use rquickjs::{ - atom::PredefinedAtom, - function::{Constructor, Opt}, - prelude::{Func, Rest, This}, - Array, ArrayBuffer, Ctx, Exception, Function, IntoJs, JsLifetime, Object, Result, TypedArray, - Value, -}; - -#[derive(JsLifetime)] -pub struct BufferPrimordials<'js> { - constructor: Constructor<'js>, -} - -impl<'js> Primordial<'js> for BufferPrimordials<'js> { - fn new(ctx: &Ctx<'js>) -> Result - where - Self: Sized, - { - let constructor: Constructor = ctx.globals().get(stringify!(Buffer))?; - - Ok(Self { constructor }) - } -} - -pub struct Buffer(pub Vec); - -fn resolve_view_bytes<'js>( - ctx: &Ctx<'js>, - array_buffer: ArrayBuffer<'js>, - byte_length: usize, - byte_offset: usize, -) -> Result<&'js mut [u8]> { - let raw = array_buffer - .as_raw() - .ok_or(ERROR_MSG_ARRAY_BUFFER_DETACHED) - .or_throw(ctx)?; - - if byte_offset > raw.len || byte_length > raw.len - byte_offset { - return Err(Exception::throw_range( - ctx, - "The value of \"byteOffset\" is out of range", - )); - } - - // SAFETY: bounds checked above. - Ok(unsafe { slice::from_raw_parts_mut(raw.ptr.as_ptr().add(byte_offset), byte_length) }) -} - -impl<'js> IntoJs<'js> for Buffer { - fn into_js(self, ctx: &Ctx<'js>) -> Result> { - let array_buffer = ArrayBuffer::new(ctx.clone(), self.0)?; - Self::from_array_buffer(ctx, array_buffer) - } -} - -impl<'js> Buffer { - pub fn alloc(length: usize) -> Self { - Self(vec![0; length]) - } - - pub fn to_string(&self, ctx: &Ctx<'js>, encoding: &str) -> Result { - Encoder::from_str(encoding) - .and_then(|enc| enc.encode_to_string(self.0.as_ref(), true)) - .or_throw(ctx) - } - - fn from_array_buffer(ctx: &Ctx<'js>, buffer: ArrayBuffer<'js>) -> Result> { - BufferPrimordials::get(ctx)? - .constructor - .construct((buffer,)) - } - - fn from_array_buffer_offset_length( - ctx: &Ctx<'js>, - array_buffer: ArrayBuffer<'js>, - offset: usize, - length: usize, - ) -> Result> { - BufferPrimordials::get(ctx)? - .constructor - .construct((array_buffer, offset, length)) - } - - fn from_encoding( - ctx: &Ctx<'js>, - mut bytes: Vec, - encoding: Option, - ) -> Result> { - if let Some(encoding) = encoding { - let encoder = Encoder::from_str(&encoding).or_throw(ctx)?; - bytes = encoder.decode(bytes).or_throw(ctx)?; - } - Buffer(bytes).into_js(ctx) - } - - fn from_string_encoding( - ctx: &Ctx<'js>, - string: String, - encoding: Option, - ) -> Result> { - let bytes = if let Some(encoding) = encoding { - let encoder = Encoder::from_str(&encoding).or_throw(ctx)?; - encoder.decode_from_string(string).or_throw(ctx)? - } else { - string.into_bytes() - }; - Buffer(bytes).into_js(ctx) - } -} - -// Static Methods -fn alloc<'js>( - ctx: Ctx<'js>, - length: usize, - fill: Opt>, - encoding: Opt, -) -> Result> { - if let Some(value) = fill.0 { - if let Some(value) = value.as_string() { - let string = value.to_string()?; - - if let Some(encoding) = encoding.0 { - let encoder = Encoder::from_str(&encoding).or_throw(&ctx)?; - let bytes = encoder.decode_from_string(string).or_throw(&ctx)?; - return alloc_byte_ref(&ctx, &bytes, length); - } - - let byte_ref = string.as_bytes(); - - return alloc_byte_ref(&ctx, byte_ref, length); - } - if let Some(value) = value.as_int() { - let bytes = vec![value as u8; length]; - return Buffer(bytes).into_js(&ctx); - } - if let Some(obj) = value.as_object() { - if let Some(ob) = ObjectBytes::from_array_buffer(obj)? { - let bytes = ob.as_bytes(&ctx)?; - return alloc_byte_ref(&ctx, bytes, length); - } - } - } - - Buffer(vec![0; length]).into_js(&ctx) -} - -fn alloc_byte_ref<'js>(ctx: &Ctx<'js>, byte_ref: &[u8], length: usize) -> Result> { - let mut bytes = vec![0; length]; - let byte_ref_length = byte_ref.len(); - for i in 0..length { - bytes[i] = byte_ref[i % byte_ref_length]; - } - Buffer(bytes).into_js(ctx) -} - -fn alloc_unsafe(ctx: Ctx<'_>, size: usize) -> Result> { - let mut bytes: Vec> = Vec::with_capacity(size); - unsafe { - bytes.set_len(size); - } - - Buffer(maybeuninit_to_u8(bytes)).into_js(&ctx) -} - -fn maybeuninit_to_u8(vec: Vec>) -> Vec { - let len = vec.len(); - let capacity = vec.capacity(); - let ptr = vec.as_ptr() as *mut u8; - - std::mem::forget(vec); - - // This conversion is safe because MaybeUninit has the same memory layout as u8, meaning the underlying bytes are identical. - // Since Vec and Vec share the same memory representation, a simple reinterpretation of the pointer is valid. - // Additionally, Vec::from_raw_parts correctly reconstructs the vector using the original length and capacity, ensuring that memory ownership remains consistent. - // The call to std::mem::forget(vec) prevents the original Vec from being dropped, avoiding double frees or memory corruption. - // However, this conversion is only safe if all elements of MaybeUninit are properly initialized. - // If any uninitialized values exist, reading them as u8 would lead to undefined behavior. - unsafe { Vec::from_raw_parts(ptr, len, capacity) } -} - -fn alloc_unsafe_slow(ctx: Ctx<'_>, size: usize) -> Result> { - let layout = std::alloc::Layout::array::(size).or_throw(&ctx)?; - - let bytes = unsafe { - let ptr = std::alloc::alloc(layout); - if ptr.is_null() { - return Err(Exception::throw_internal(&ctx, "Memory allocation failed")); - } - Vec::from_raw_parts(ptr, size, size) - }; - Buffer(bytes).into_js(&ctx) -} - -fn byte_length<'js>(ctx: Ctx<'js>, value: Value<'js>, encoding: Opt) -> Result { - //slow path - if let Some(encoding) = encoding.0 { - let encoder = Encoder::from_str(&encoding).or_throw(&ctx)?; - let a = ObjectBytes::from(&ctx, &value)?; - let bytes = a.as_bytes(&ctx)?; - return Ok(encoder.decode(bytes).or_throw(&ctx)?.len()); - } - //fast path - if let Some(val) = value.as_string() { - return Ok(val.to_string()?.len()); - } - - if value.is_array() { - let array = value.as_array().unwrap(); - - for val in array.iter::() { - val.or_throw_msg(&ctx, "array value is not u8")?; - } - - return Ok(array.len()); - } - - if let Some(obj) = value.as_object() { - if let Some(ob) = ObjectBytes::from_array_buffer(obj)? { - return Ok(ob.as_bytes(&ctx)?.len()); - } - } - - Err(Exception::throw_message( - &ctx, - "value must be typed DataView, Buffer, ArrayBuffer, Uint8Array or string", - )) -} - -fn concat<'js>(ctx: Ctx<'js>, list: Array<'js>, max_length: Opt) -> Result> { - let mut bytes = Vec::new(); - let mut total_length = 0; - let mut length; - for value in list.iter::() { - let typed_array = TypedArray::::from_object(value?)?; - let bytes_ref: &[u8] = typed_array.as_ref(); - - length = bytes_ref.len(); - - if length == 0 { - continue; - } - - if let Some(max_length) = max_length.0 { - total_length += length; - if total_length > max_length { - let diff = max_length - (total_length - length); - bytes.extend_from_slice(&bytes_ref[0..diff]); - break; - } - } - bytes.extend_from_slice(bytes_ref); - } - - Buffer(bytes).into_js(&ctx) -} - -fn from<'js>( - ctx: Ctx<'js>, - value: Value<'js>, - offset_or_encoding: Opt>, - length: Opt, -) -> Result> { - let mut encoding: Option = None; - let mut offset = 0; - - if let Some(offset_or_encoding) = offset_or_encoding.0 { - if offset_or_encoding.is_string() { - encoding = Some(offset_or_encoding.get()?); - } else if offset_or_encoding.is_number() { - offset = offset_or_encoding.get()?; - } - } - - // WARN: This is currently bugged for strings that can't be converted to utf8 - // See https://github.com/quickjs-ng/quickjs/issues/992 - if let Some(string) = get_string(&value)? { - return Buffer::from_string_encoding(&ctx, string, encoding)?.into_js(&ctx); - } - if let Some(bytes) = get_array_bytes(&value, offset, length.0)? { - return Buffer::from_encoding(&ctx, bytes, encoding)?.into_js(&ctx); - } - - if let Some(obj) = value.as_object() { - if let Some(ab_bytes) = ObjectBytes::from_array_buffer(obj)? { - let bytes = ab_bytes.as_bytes(&ctx)?; - let (start, end) = get_start_end_indexes(bytes.len(), length.0, offset); - - //buffers from buffer should be copied - if obj - .get::<_, Option>(PredefinedAtom::Meta)? - .as_deref() - == Some(stringify!(Buffer)) - || encoding.is_some() - { - let bytes = bytes.into(); - return Buffer::from_encoding(&ctx, bytes, encoding)?.into_js(&ctx); - } else { - let (array_buffer, _, source_offset) = ab_bytes.get_array_buffer()?.unwrap(); //we know it's an array buffer - return Buffer::from_array_buffer_offset_length( - &ctx, - array_buffer, - start + source_offset, - end - start, - ); - } - } - } - - if let Some(string) = get_coerced_string(&value) { - return Buffer::from_string_encoding(&ctx, string, encoding)?.into_js(&ctx); - } - - Err(Exception::throw_message( - &ctx, - "value must be typed DataView, Buffer, ArrayBuffer, Uint8Array or interpretable as string", - )) -} - -fn is_buffer<'js>(ctx: Ctx<'js>, value: Value<'js>) -> Result { - if let Some(object) = value.as_object() { - let constructor = BufferPrimordials::get(&ctx)?; - return Ok(object.is_instance_of(&constructor.constructor)); - } - - Ok(false) -} - -fn is_encoding(value: Value) -> Result { - if let Some(js_string) = value.as_string() { - let std_string = js_string.to_string()?; - return Ok(Encoder::from_str(std_string.as_str()).is_ok()); - } - - Ok(false) -} - -// Prototype Methods -fn copy<'js>( - this: This>, - ctx: Ctx<'js>, - target: ObjectBytes<'js>, - args: Rest, -) -> Result { - let mut args_iter = args.0.into_iter(); - let target_start = args_iter.next().unwrap_or_default(); - let source_start = args_iter.next().unwrap_or_default(); - let source_end = args_iter.next().unwrap_or_else(|| this.0.len()); - - let source_bytes = ObjectBytes::from(&ctx, this.0.as_inner())?; - let source_bytes = source_bytes.as_bytes(&ctx)?; - - if source_start > source_bytes.len() { - return Err(Exception::throw_range( - &ctx, - "The value of \"sourceStart\" is out of range", - )); - } - - // sourceEnd is clamped (not an error), unlike sourceStart above. - let source_end = source_end.min(source_bytes.len()); - - let mut copyable_length = 0; - - if source_start >= source_end { - return Ok(copyable_length); - } - - if let Some((array_buffer, target_byte_length, target_byte_offset)) = - target.get_array_buffer()? - { - let target_bytes = - resolve_view_bytes(&ctx, array_buffer, target_byte_length, target_byte_offset)?; - - if target_start <= target_bytes.len() { - copyable_length = (source_end - source_start).min(target_bytes.len() - target_start); - - target_bytes[target_start..target_start + copyable_length] - .copy_from_slice(&source_bytes[source_start..source_start + copyable_length]); - } - } - - Ok(copyable_length) -} - -fn subarray<'js>( - this: This>, - ctx: Ctx<'js>, - start: Opt, - end: Opt, -) -> Result> { - let view = TypedArray::::from_object(this.0.clone())?; - - let array_buffer = view.arraybuffer()?; - let view_offset = this.0.get::<_, isize>("byteOffset")?; - let view_length = this.0.get::<_, isize>("byteLength")?; - - let start_index = start.map_or(0, |s| { - if s < 0 { - (view_length + s).max(0) - } else { - s.min(view_length) - } - }); - - let end_index = end.map_or(view_length, |e| { - if e < 0 { - (view_length + e).max(0) - } else { - e.min(view_length) - } - }); - - let length = (end_index - start_index).max(0) as usize; - let new_offset = (view_offset + start_index).max(0) as usize; - - Buffer::from_array_buffer_offset_length(&ctx, array_buffer, new_offset, length) -} - -fn to_string( - this: This>, - ctx: Ctx, - encoding: Opt, - start: Opt, - end: Opt, -) -> Result { - let typed_array = TypedArray::::from_object(this.0)?; - let bytes: &[u8] = typed_array.as_ref(); - - let start = start - .0 - .map(|s| s.max(0) as usize) - .unwrap_or(0) - .min(bytes.len()); - let end = end - .0 - .map(|e| e.max(0) as usize) - .unwrap_or(bytes.len()) - .min(bytes.len()); - let bytes = &bytes[start..end]; - - let encoder = Encoder::from_optional_str(encoding.as_deref()).or_throw(&ctx)?; - encoder.encode_to_string(bytes, true).or_throw(&ctx) -} - -fn write<'js>( - this: This>, - ctx: Ctx<'js>, - string: String, - args: Rest>, -) -> Result { - let (offset, length, encoding) = get_write_parameters(&ctx, &args, this.0.len())?; - - let target = ObjectBytes::from(&ctx, this.0.as_inner())?; - - let mut writable_length = 0; - - if let Some((array_buffer, target_byte_length, target_byte_offset)) = - target.get_array_buffer()? - { - let target_bytes = - resolve_view_bytes(&ctx, array_buffer, target_byte_length, target_byte_offset)?; - - let encoder = Encoder::from_str(&encoding).or_throw(&ctx)?; - - if encoder.as_label() == "utf-8" { - let (source_slice, valid_length) = safe_byte_slice(&string, length.min(string.len())); - writable_length = valid_length; - target_bytes[offset..offset + writable_length].copy_from_slice(source_slice); - } else { - let decode_bytes = encoder.decode_from_string(string).or_throw(&ctx)?; - writable_length = length.min(decode_bytes.len()); - target_bytes[offset..offset + writable_length] - .copy_from_slice(&decode_bytes[..writable_length]); - }; - } - - Ok(writable_length) -} - -fn get_write_parameters<'js>( - ctx: &Ctx<'js>, - args: &Rest>, - len: usize, -) -> Result<(usize, usize, String)> { - let mut offset = 0; - let mut length = len; - let mut encoding = "utf8".to_owned(); - - if let Some(v1) = args.0.first() { - if let Some(s) = v1.as_string() { - return Ok((0, len, s.to_string()?)); - } - offset = v1.as_int().unwrap_or(0) as usize; - if offset > len { - return Err(Exception::throw_range( - ctx, - "The value of \"offset\" is out of range", - )); - } - length = len - offset; - } - - if let Some(v2) = args.0.get(1) { - if let Some(s) = v2.as_string() { - return Ok((offset, len - offset, s.to_string()?)); - } - length = v2 - .as_int() - .map_or(len - offset, |l| (l as usize).min(len - offset)); - } - - if let Some(v3) = args.0.get(2) { - if let Some(s) = v3.as_string() { - encoding = s.to_string()?; - } - } - - Ok((offset, length, encoding)) -} - -fn safe_byte_slice(s: &str, end: usize) -> (&[u8], usize) { - let bytes = s.as_bytes(); - - if bytes.len() <= end { - return (bytes, bytes.len()); - } - - let valid_end = s - .char_indices() - .map(|(i, _)| i) - .rfind(|&i| i <= end) - .unwrap_or(0); - - (&bytes[0..valid_end], valid_end) -} - -#[derive(Clone, Copy)] -pub enum Endian { - Little, - Big, -} - -#[derive(Clone, Copy, PartialEq, Eq)] -pub enum NumberKind { - Int8, - UInt8, - Int16, - UInt16, - Int32, - UInt32, - Float32, - Float64, - BigInt, - BigUInt, -} - -impl NumberKind { - pub fn bits(&self) -> u8 { - match self { - NumberKind::Int8 => 8, - NumberKind::UInt8 => 8, - NumberKind::Int16 => 16, - NumberKind::UInt16 => 16, - NumberKind::Int32 => 32, - NumberKind::UInt32 => 32, - NumberKind::Float32 => 32, - NumberKind::Float64 => 64, - NumberKind::BigInt => 64, - NumberKind::BigUInt => 64, - } - } - - pub fn is_signed(&self) -> bool { - matches!( - self, - NumberKind::Int8 | NumberKind::Int16 | NumberKind::Int32 - ) - } - - pub fn prototype(&self) -> &'static [(Endian, &'static str, Option<&'static str>)] { - match self { - NumberKind::Int8 => &[(Endian::Little, "Int8", None)], - NumberKind::UInt8 => &[(Endian::Little, "UInt8", Some("Uint8"))], - NumberKind::Int16 => &[ - (Endian::Little, "Int16LE", None), - (Endian::Big, "Int16BE", None), - ], - NumberKind::UInt16 => &[ - (Endian::Little, "UInt16LE", Some("Uint16LE")), - (Endian::Big, "UInt16BE", Some("Uint16BE")), - ], - NumberKind::Int32 => &[ - (Endian::Little, "Int32LE", None), - (Endian::Big, "Int32BE", None), - ], - NumberKind::UInt32 => &[ - (Endian::Little, "UInt32LE", Some("Uint32LE")), - (Endian::Big, "UInt32BE", Some("Uint32BE")), - ], - NumberKind::Float32 => &[ - (Endian::Little, "FloatLE", None), - (Endian::Big, "FloatBE", None), - ], - NumberKind::Float64 => &[ - (Endian::Little, "DoubleLE", None), - (Endian::Big, "DoubleBE", None), - ], - NumberKind::BigInt => &[ - (Endian::Little, "BigInt64LE", None), - (Endian::Big, "BigInt64BE", None), - ], - NumberKind::BigUInt => &[ - (Endian::Little, "BigUInt64LE", Some("BigUint64LE")), - (Endian::Big, "BigUInt64BE", Some("BigUint64BE")), - ], - } - } -} - -iterable_enum!( - NumberKind, Int8, UInt8, Int16, UInt16, Int32, UInt32, Float32, Float64, BigInt, BigUInt -); - -#[allow(clippy::too_many_arguments)] -fn write_buf<'js>( - this: &This>, - ctx: &Ctx<'js>, - value: &Value<'js>, - offset: &Opt, - endian: Endian, - kind: NumberKind, -) -> Result { - let offset = offset.0.unwrap_or_default(); - - // Extract and convert value - let (byte_count, bytes) = match kind { - NumberKind::BigInt => { - let Some(bigint) = value.as_big_int() else { - return Err(Exception::throw_type(ctx, "Expected BigInt")); - }; - let (byte_count, val) = (8, bigint.clone().to_i64().or_throw(ctx)? as u64); - (byte_count, endian_bytes(val, endian)) - } - NumberKind::BigUInt => { - return Err(Exception::throw_type(ctx, "Uint64 is not supported")); - } - NumberKind::Float32 => { - let Some(float_val) = value.as_float() else { - return Err(Exception::throw_type(ctx, "Expected number")); - }; - match endian { - Endian::Big => (4, (float_val as f32).to_bits().to_be_bytes().to_vec()), - Endian::Little => (4, (float_val as f32).to_bits().to_le_bytes().to_vec()), - } - } - NumberKind::Float64 => { - let Some(float_val) = value.as_float() else { - return Err(Exception::throw_type(ctx, "Expected number")); - }; - match endian { - Endian::Big => (8, float_val.to_bits().to_be_bytes().to_vec()), - Endian::Little => (8, float_val.to_bits().to_le_bytes().to_vec()), - } - } - NumberKind::Int8 - | NumberKind::UInt8 - | NumberKind::Int16 - | NumberKind::UInt16 - | NumberKind::Int32 - | NumberKind::UInt32 => { - let Some(int_val) = value.as_number() else { - return Err(Exception::throw_type(ctx, "Expected number")); - }; - let int_val = int_val as i64; - let bit_mask = (1i64 << kind.bits()) - 1; - let max_val = if kind.is_signed() { - (1i64 << (kind.bits() - 1)) - 1 - } else { - bit_mask - }; - let min_val = if kind.is_signed() { -max_val - 1 } else { 0 }; - - if int_val < min_val || int_val > max_val { - return Err(Exception::throw_range(ctx, "Value out of range")); - } - - let masked = int_val & bit_mask; - ( - (kind.bits() / 8) as usize, - shifted_bytes(masked as u64, kind.bits(), endian), - ) - } - }; - - if offset >= this.0.len() || offset + byte_count > this.0.len() { - return Err(Exception::throw_range( - ctx, - "The specified offset is out of range", - )); - } - - let target = ObjectBytes::from(ctx, this.0.as_inner())?; - let mut writable_length = 0; - - if let Some((array_buffer, target_byte_length, target_byte_offset)) = - target.get_array_buffer()? - { - let target_bytes = - resolve_view_bytes(ctx, array_buffer, target_byte_length, target_byte_offset)?; - - writable_length = offset + bytes.len(); - target_bytes[offset..writable_length].copy_from_slice(&bytes); - } - - Ok(writable_length) -} - -fn read_buf<'js>( - this: &This>, - ctx: &Ctx<'js>, - offset: &Opt, - endian: Endian, - kind: NumberKind, -) -> Result> { - // Retrieve the array buffer - let target = ObjectBytes::from(ctx, this.0.as_inner())?; - let Some((array_buffer, target_byte_length, target_byte_offset)) = target.get_array_buffer()? - else { - return Err(Exception::throw_message(ctx, ERROR_MSG_NOT_ARRAY_BUFFER)); - }; - let target_bytes = - resolve_view_bytes(ctx, array_buffer, target_byte_length, target_byte_offset)?; - - // Enforce the bounds - let start = offset.0.unwrap_or_default(); - let end = start + (kind.bits() / 8) as usize; - if end > target_bytes.len() { - return Err(Exception::throw_range( - ctx, - "The value of \"offset\" is out of range", - )); - } - - let bytes = &target_bytes[start..end]; - - let value = match kind { - NumberKind::BigInt => { - let value = match endian { - Endian::Big => i64::from_be_bytes(bytes.try_into().unwrap()), - Endian::Little => i64::from_le_bytes(bytes.try_into().unwrap()), - }; - Value::new_big_int(ctx.clone(), value)? - } - NumberKind::BigUInt => { - return Err(Exception::throw_type(ctx, "Uint64 is not supported")); - } - NumberKind::Float32 => { - let value = match endian { - Endian::Big => f32::from_be_bytes(bytes.try_into().unwrap()), - Endian::Little => f32::from_le_bytes(bytes.try_into().unwrap()), - }; - Value::new_float(ctx.clone(), value as f64) - } - NumberKind::Float64 => { - let value = match endian { - Endian::Big => f64::from_be_bytes(bytes.try_into().unwrap()), - Endian::Little => f64::from_le_bytes(bytes.try_into().unwrap()), - }; - Value::new_float(ctx.clone(), value) - } - NumberKind::Int8 => { - let value = match endian { - Endian::Big => i8::from_be_bytes(bytes.try_into().unwrap()), - Endian::Little => i8::from_le_bytes(bytes.try_into().unwrap()), - }; - Value::new_int(ctx.clone(), value as i32) - } - NumberKind::UInt8 => { - let value = match endian { - Endian::Big => u8::from_be_bytes(bytes.try_into().unwrap()), - Endian::Little => u8::from_le_bytes(bytes.try_into().unwrap()), - }; - Value::new_int(ctx.clone(), value as i32) - } - NumberKind::Int16 => { - let value = match endian { - Endian::Big => i16::from_be_bytes(bytes.try_into().unwrap()), - Endian::Little => i16::from_le_bytes(bytes.try_into().unwrap()), - }; - Value::new_int(ctx.clone(), value as i32) - } - NumberKind::UInt16 => { - let value = match endian { - Endian::Big => u16::from_be_bytes(bytes.try_into().unwrap()), - Endian::Little => u16::from_le_bytes(bytes.try_into().unwrap()), - }; - Value::new_int(ctx.clone(), value as i32) - } - NumberKind::Int32 => { - let value = match endian { - Endian::Big => i32::from_be_bytes(bytes.try_into().unwrap()), - Endian::Little => i32::from_le_bytes(bytes.try_into().unwrap()), - }; - Value::new_int(ctx.clone(), value) - } - NumberKind::UInt32 => { - let value = match endian { - Endian::Big => u32::from_be_bytes(bytes.try_into().unwrap()), - Endian::Little => u32::from_le_bytes(bytes.try_into().unwrap()), - }; - Value::new_float(ctx.clone(), value as f64) - } - }; - Ok(value) -} - -// Pure mathematical byte generation -fn endian_bytes(mut val: u64, endian: Endian) -> Vec { - let mut bytes = vec![0u8; 8]; - - #[allow(clippy::needless_range_loop)] - for i in 0..8 { - bytes[i] = match endian { - Endian::Big => (val >> (56 - i * 8)) as u8, - Endian::Little => (val >> (i * 8)) as u8, - }; - // Clear processed bits - match endian { - Endian::Big => val &= !(0xFF << ((7 - i) * 8)), - Endian::Little => val &= !(0xFF << (i * 8)), - } - } - bytes -} - -fn shifted_bytes(mut val: u64, bits: u8, endian: Endian) -> Vec { - let byte_count = (bits / 8) as usize; - let mut bytes = vec![0u8; byte_count]; - - #[allow(clippy::needless_range_loop)] - for i in 0..byte_count { - let shift = match endian { - Endian::Big => (byte_count - 1 - i) * 8, - Endian::Little => i * 8, - }; - bytes[i] = (val >> shift) as u8; - val &= !(0xFF << shift); // Clear processed bits - } - bytes -} - -pub(crate) fn set_prototype<'js>(ctx: &Ctx<'js>, constructor: Object<'js>) -> Result<()> { - let _ = &constructor.set("alloc", Func::from(alloc))?; - let _ = &constructor.set("allocUnsafe", Func::from(alloc_unsafe))?; - let _ = &constructor.set("allocUnsafeSlow", Func::from(alloc_unsafe_slow))?; - let _ = &constructor.set("byteLength", Func::from(byte_length))?; - let _ = &constructor.set("concat", Func::from(concat))?; - let _ = &constructor.set(PredefinedAtom::From, Func::from(from))?; - let _ = &constructor.set("isBuffer", Func::from(is_buffer))?; - let _ = &constructor.set("isEncoding", Func::from(is_encoding))?; - - let prototype: &Object = &constructor.get(PredefinedAtom::Prototype)?; - prototype.set("copy", Func::from(copy))?; - prototype.set("subarray", Func::from(subarray))?; - prototype.set(PredefinedAtom::ToString, Func::from(to_string))?; - prototype.set("write", Func::from(write))?; - - // Set all write and read methods - for kind in NumberKind::iter() { - for (endian, name, alias) in kind.prototype() { - let write_func = Function::new(ctx.clone(), |t, c, v, o| { - write_buf(&t, &c, &v, &o, *endian, *kind) - })?; - let read_func = - Function::new(ctx.clone(), |t, c, o| read_buf(&t, &c, &o, *endian, *kind))?; - if let Some(alias) = alias { - prototype.set(["write", alias].concat(), write_func.clone())?; - prototype.set(["read", alias].concat(), read_func.clone())?; - } - prototype.set(["write", name].concat(), write_func)?; - prototype.set(["read", name].concat(), read_func)?; - } - } - - //not assessable from js - prototype.prop(PredefinedAtom::Meta, stringify!(Buffer))?; - - ctx.globals().set(stringify!(Buffer), constructor)?; - - Ok(()) -} - -#[cfg(test)] -mod tests { - use crate::llrt_test::{call_test, test_async_with, ModuleEvaluator}; - - use crate::llrt_buffer::BufferModule; - - #[tokio::test] - async fn test_subarray() { - test_async_with(|ctx| { - Box::pin(async move { - crate::llrt_buffer::init(&ctx).unwrap(); - ModuleEvaluator::eval_rust::(ctx.clone(), "buffer") - .await - .unwrap(); - - let data = "hello world".to_string().into_bytes(); - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test", - r#" - import { Buffer } from 'buffer'; - - export async function test(data) { - let buffer = Buffer.from(data); - let sub = buffer.subarray(6, 11); // "world" part - return sub.toString(); - } - "#, - ) - .await - .unwrap(); - let result = call_test::(&ctx, &module, (data,)).await; - assert_eq!(result, "world"); - }) - }) - .await; - } - - #[tokio::test] - async fn test_subarray_partial() { - test_async_with(|ctx| { - Box::pin(async move { - crate::llrt_buffer::init(&ctx).unwrap(); - ModuleEvaluator::eval_rust::(ctx.clone(), "buffer") - .await - .unwrap(); - - let data = "hello world".to_string().into_bytes(); - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test", - r#" - import { Buffer } from 'buffer'; - - export async function test(data) { - let buffer = Buffer.from(data); - let sub = buffer.subarray(0, 5); // "hello" part - return sub.toString(); - } - "#, - ) - .await - .unwrap(); - let result = call_test::(&ctx, &module, (data,)).await; - assert_eq!(result, "hello"); - }) - }) - .await; - } - - #[tokio::test] - async fn test_subarray_out_of_bounds() { - test_async_with(|ctx| { - Box::pin(async move { - crate::llrt_buffer::init(&ctx).unwrap(); - ModuleEvaluator::eval_rust::(ctx.clone(), "buffer") - .await - .unwrap(); - - let data = "hello world".to_string().into_bytes(); - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test", - r#" - import { Buffer } from 'buffer'; - - export async function test(data) { - let buffer = Buffer.from(data); - let sub = buffer.subarray(6, 20); // "world" part but goes out of bounds - return sub.toString(); - } - "#, - ) - .await - .unwrap(); - let result = call_test::(&ctx, &module, (data,)).await; - assert_eq!(result, "world"); - }) - }) - .await; - } - - #[tokio::test] - async fn test_read_int_32_be() { - test_async_with(|ctx| { - Box::pin(async move { - crate::llrt_buffer::init(&ctx).unwrap(); - ModuleEvaluator::eval_rust::(ctx.clone(), "buffer") - .await - .unwrap(); - - let data = "hello world".to_string().into_bytes(); - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test", - r#" - import { Buffer } from 'buffer'; - - export async function test(data) { - const buf = Buffer.from([1, 2, 3, 4, 0, 0, 0, 0]); - return buf.readInt32BE(); - } - "#, - ) - .await - .unwrap(); - let result = call_test::(&ctx, &module, (data,)).await; - assert_eq!(result, 0x01020304); - }) - }) - .await; - } -} diff --git a/stdlib/src/llrt/llrt_buffer/file.rs b/stdlib/src/llrt/llrt_buffer/file.rs deleted file mode 100644 index 75629940..00000000 --- a/stdlib/src/llrt/llrt_buffer/file.rs +++ /dev/null @@ -1,129 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use crate::llrt_utils::time; -use rquickjs::{ - atom::PredefinedAtom, class::Trace, function::Opt, ArrayBuffer, Coerced, Ctx, Exception, - IntoJs, Object, Result, Value, -}; - -use super::blob::Blob; - -#[rquickjs::class] -#[derive(Trace, Clone, rquickjs::JsLifetime)] -pub struct File<'js> { - blob: Blob<'js>, - filename: String, - last_modified: i64, -} - -#[rquickjs::methods] -impl<'js> File<'js> { - #[qjs(constructor)] - fn new( - ctx: Ctx<'js>, - data: Value<'js>, - filename: Coerced, - options: Opt>, - ) -> Result { - let mut last_modified = time::now_millis(); - - if let Some(ref opts) = options.0 { - if opts.is_bool() || opts.is_float() || opts.is_int() || opts.is_string() { - return Err(Exception::throw_type(&ctx, "Invalid options")); - } - - if let Some(v) = opts.as_object() { - if let Some(x) = v.get::<_, Option>>("lastModified")? { - last_modified = x.0; - } - } - } - - let blob = Blob::from_parts(ctx, Opt(Some(data)), options)?; - - Ok(Self { - blob, - filename: filename.0, - last_modified, - }) - } - - #[qjs(get)] - pub fn size(&self) -> usize { - self.blob.size() - } - - #[qjs(get)] - pub fn name(&self) -> String { - self.filename.clone() - } - - #[qjs(get, rename = "type")] - pub fn mime_type(&self) -> String { - self.blob.mime_type() - } - - #[qjs(get, rename = "lastModified")] - pub fn last_modified(&self) -> i64 { - self.last_modified - } - - pub fn slice( - &self, - ctx: Ctx<'js>, - start: Opt, - end: Opt, - content_type: Opt>, - ) -> Result> { - self.blob.slice_blob(&ctx, start.0, end.0, content_type.0) - } - - pub async fn text(&mut self) -> String { - self.blob.text().await - } - - #[qjs(rename = "arrayBuffer")] - pub async fn array_buffer(&self, ctx: Ctx<'js>) -> Result> { - self.blob.array_buffer(ctx).await - } - - pub async fn bytes(&self, ctx: Ctx<'js>) -> Result> { - self.blob.bytes(ctx).await - } - - #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] - pub fn to_string_tag() -> &'static str { - stringify!(File) - } -} - -impl<'js> File<'js> { - pub fn from_bytes( - ctx: &Ctx<'js>, - data: Vec, - filename: String, - mime_type: Option, - ) -> Result { - let options = Opt(Some({ - let obj = Object::new(ctx.clone())?; - obj.set("type", mime_type.clone().unwrap_or("".into()).into_js(ctx)?)?; - obj.into_js(ctx)? - })); - let blob = Blob::from_parts(ctx.clone(), Opt(Some(data.into_js(ctx)?)), options)?; - - Ok(Self { - blob, - filename, - last_modified: time::now_millis(), - }) - } - - pub fn get_blob(&self) -> Blob<'js> { - self.blob.clone() - } - - pub fn set_filename(&mut self, filename: String) { - self.filename = filename; - } -} diff --git a/stdlib/src/llrt/llrt_buffer/lib.rs b/stdlib/src/llrt/llrt_buffer/lib.rs deleted file mode 100644 index b491ba60..00000000 --- a/stdlib/src/llrt/llrt_buffer/lib.rs +++ /dev/null @@ -1,102 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use crate::llrt_utils::{ - module::{export_default, ModuleInfo}, - object::define_subclass, - primordials::{BasePrimordials, Primordial}, -}; -use rquickjs::{ - function::{Args, Constructor, Rest}, - module::{Declarations, Exports, ModuleDef}, - Class, Ctx, Function, IntoJs, Object, Result, Value, -}; - -pub use self::array_buffer_view::*; -pub use self::blob::*; -pub use self::buffer::*; -pub use self::file::*; - -mod array_buffer_view; -mod blob; -mod buffer; -mod file; - -pub struct BufferModule; - -impl ModuleDef for BufferModule { - fn declare(declare: &Declarations) -> Result<()> { - declare.declare(stringify!(Buffer))?; - declare.declare("atob")?; - declare.declare("btoa")?; - declare.declare("constants")?; - declare.declare("default")?; - Ok(()) - } - - fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { - let globals = ctx.globals(); - let buf: Constructor = globals.get(stringify!(Buffer))?; - - let constants = Object::new(ctx.clone())?; - constants.set("MAX_LENGTH", u32::MAX)?; // For QuickJS - constants.set("MAX_STRING_LENGTH", (1 << 30) - 1)?; // For QuickJS - - let atob: Function = ctx.globals().get("atob")?; - let btoa: Function = ctx.globals().get("btoa")?; - - export_default(ctx, exports, |default| { - default.set(stringify!(Buffer), buf)?; - default.set("atob", atob.into_js(ctx)?)?; - default.set("btoa", btoa.into_js(ctx)?)?; - default.set("constants", constants)?; - Ok(()) - })?; - - Ok(()) - } -} - -impl From for ModuleInfo { - fn from(val: BufferModule) -> Self { - ModuleInfo { - name: "buffer", - module: val, - } - } -} - -pub fn init<'js>(ctx: &Ctx<'js>) -> Result<()> { - let globals = ctx.globals(); - BasePrimordials::init(ctx)?; - - // Buffer extends the native Uint8Array: it forwards construction to the - // Uint8Array constructor and inherits its static and prototype members. - let uint8array = BasePrimordials::get(ctx)?.constructor_uint8array.clone(); - let buffer_ctor = define_subclass( - ctx, - stringify!(Buffer), - &uint8array, - |ctx: Ctx<'js>, args: Rest>| { - let uint8array = &BasePrimordials::get(&ctx)?.constructor_uint8array; - let mut ctor_args = Args::new(ctx.clone(), args.0.len()); - ctor_args.push_args(args.0)?; - ctor_args.construct::(uint8array) - }, - )?; - let buffer: Object = buffer_ctor.into_value().into_object().unwrap(); - set_prototype(ctx, buffer)?; - - BufferPrimordials::init(ctx)?; - - // Blob - Class::::define(&globals)?; - - // File - Class::::define(&globals)?; - - //init primordials - let _ = BufferPrimordials::get(ctx)?; - - Ok(()) -} diff --git a/stdlib/src/llrt/llrt_compression/lib.rs b/stdlib/src/llrt/llrt_compression/lib.rs deleted file mode 100644 index f8b3091d..00000000 --- a/stdlib/src/llrt/llrt_compression/lib.rs +++ /dev/null @@ -1,99 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -pub mod streaming; - -#[cfg(any(any(), all()))] -pub mod zstd { - use std::io::{BufReader, Read, Result}; - - use zstd::stream::read::{Decoder as ZstdDecoder, Encoder as ZstdEncoder}; - pub use zstd::DEFAULT_COMPRESSION_LEVEL; - - pub fn encoder(r: R, level: i32) -> Result>> { - ZstdEncoder::new(r, level) - } - - pub fn decoder(r: R) -> Result>> { - ZstdDecoder::new(r) - } -} - -#[cfg(any(any(), all()))] -pub mod deflate { - use std::io::Read; - - use flate2::read::{DeflateDecoder, DeflateEncoder}; - pub use flate2::Compression; - - pub fn encoder(r: R, level: Compression) -> DeflateEncoder { - DeflateEncoder::new(r, level) - } - - pub fn decoder(r: R) -> DeflateDecoder { - DeflateDecoder::new(r) - } -} - -#[cfg(any(any(), all()))] -pub mod gz { - use std::io::Read; - - use flate2::read::{GzDecoder, GzEncoder}; - pub use flate2::Compression; - - pub fn encoder(r: R, level: Compression) -> GzEncoder { - GzEncoder::new(r, level) - } - - pub fn decoder(r: R) -> GzDecoder { - GzDecoder::new(r) - } -} - -#[cfg(any(any(), all()))] -pub mod zlib { - use std::io::Read; - - use flate2::read::{ZlibDecoder, ZlibEncoder}; - pub use flate2::Compression; - - pub fn encoder(r: R, level: Compression) -> ZlibEncoder { - ZlibEncoder::new(r, level) - } - - pub fn decoder(r: R) -> ZlibDecoder { - ZlibDecoder::new(r) - } -} - -#[cfg(any())] -pub mod brotli { - use std::io::BufRead; - - use brotlic::{CompressorReader as BrotliEncoder, DecompressorReader as BrotliDecoder}; - - pub fn encoder(r: R) -> BrotliEncoder { - BrotliEncoder::new(r) - } - - pub fn decoder(r: R) -> BrotliDecoder { - BrotliDecoder::new(r) - } -} - -#[cfg(all(not(any()), all()))] -pub mod brotli { - use std::io::Read; - - use brotli::{CompressorReader as BrotliEncoder, Decompressor as BrotliDecoder}; - - pub fn encoder(r: R) -> BrotliEncoder { - BrotliEncoder::new(r, 8_096, 11, 22) - } - - pub fn decoder(r: R) -> BrotliDecoder { - BrotliDecoder::new(r, 8_096) - } -} diff --git a/stdlib/src/llrt/llrt_compression/streaming.rs b/stdlib/src/llrt/llrt_compression/streaming.rs deleted file mode 100644 index e8cafe34..00000000 --- a/stdlib/src/llrt/llrt_compression/streaming.rs +++ /dev/null @@ -1,98 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::io::{self, Write}; - -#[cfg(all(not(any()), all()))] -use brotli as brotlic; - -/// Streaming decompressor that maintains state across chunks -pub enum StreamingDecoder { - #[cfg(any(any(), all()))] - Gzip(flate2::write::GzDecoder>), - #[cfg(any(any(), all()))] - Deflate(flate2::write::ZlibDecoder>), - #[cfg(any(any(), all()))] - Zstd(zstd::stream::write::Decoder<'static, Vec>), - #[cfg(any(any(), all()))] - Brotli(brotlic::DecompressorWriter>), - Identity, -} - -impl StreamingDecoder { - pub fn new(encoding: &str) -> io::Result { - match encoding { - #[cfg(any(any(), all()))] - "gzip" => Ok(Self::Gzip(flate2::write::GzDecoder::new(Vec::new()))), - #[cfg(any(any(), all()))] - "deflate" => Ok(Self::Deflate(flate2::write::ZlibDecoder::new(Vec::new()))), - #[cfg(any(any(), all()))] - "zstd" => Ok(Self::Zstd(zstd::stream::write::Decoder::new(Vec::new())?)), - #[cfg(any())] - "br" => Ok(Self::Brotli(brotlic::DecompressorWriter::new(Vec::new()))), - #[cfg(all(not(any()), all()))] - "br" => Ok(Self::Brotli(brotlic::DecompressorWriter::new( - Vec::new(), - 8_096, - ))), - "" | "identity" => Ok(Self::Identity), - _ => Err(io::Error::new( - io::ErrorKind::InvalidInput, - format!("unsupported encoding: {}", encoding), - )), - } - } - - /// Decompress a chunk of data, returning the decompressed output - pub fn decompress_chunk(&mut self, input: &[u8]) -> io::Result> { - match self { - Self::Identity => Ok(input.to_vec()), - #[cfg(any(any(), all()))] - Self::Gzip(decoder) => { - decoder.write_all(input)?; - decoder.flush()?; - Ok(std::mem::take(decoder.get_mut())) - } - #[cfg(any(any(), all()))] - Self::Deflate(decoder) => { - decoder.write_all(input)?; - decoder.flush()?; - Ok(std::mem::take(decoder.get_mut())) - } - #[cfg(any(any(), all()))] - Self::Zstd(decoder) => { - decoder.write_all(input)?; - decoder.flush()?; - Ok(std::mem::take(decoder.get_mut())) - } - #[cfg(any(any(), all()))] - Self::Brotli(decoder) => { - decoder.write_all(input)?; - decoder.flush()?; - Ok(std::mem::take(decoder.get_mut())) - } - } - } - - /// Finish decompression and return any remaining data - pub fn finish(self) -> io::Result> { - match self { - Self::Identity => Ok(Vec::new()), - #[cfg(any(any(), all()))] - Self::Gzip(decoder) => decoder.finish(), - #[cfg(any(any(), all()))] - Self::Deflate(decoder) => decoder.finish(), - #[cfg(any(any(), all()))] - Self::Zstd(decoder) => Ok(decoder.into_inner()), - #[cfg(any())] - Self::Brotli(decoder) => decoder - .into_inner() - .map_err(|e| io::Error::other(e.to_string())), - #[cfg(all(not(any()), all()))] - Self::Brotli(decoder) => decoder - .into_inner() - .map_err(|_| io::Error::other("brotli decompression failed")), - } - } -} diff --git a/stdlib/src/llrt/llrt_context/lib.rs b/stdlib/src/llrt/llrt_context/lib.rs deleted file mode 100644 index c35a4731..00000000 --- a/stdlib/src/llrt/llrt_context/lib.rs +++ /dev/null @@ -1,93 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::future::Future; -use std::sync::OnceLock; - -use crate::llrt_utils::primordials::{BasePrimordials, Primordial}; -use rquickjs::{atom::PredefinedAtom, CatchResultExt, CaughtError, Ctx, Object, Result}; -use tokio::sync::oneshot::{self, Receiver}; -use tracing::trace; - -#[allow(clippy::type_complexity)] -static ERROR_HANDLER: OnceLock Fn(&Ctx<'js>, CaughtError<'js>) + Sync + Send>> = - OnceLock::new(); - -pub trait CtxExtension<'js> { - /// Despite naming, this will not necessarily exit the parent process. - /// It depends on the handler set by `set_spawn_error_handler`. - fn spawn_exit(&self, future: F) -> Result> - where - F: Future> + 'js, - R: 'js; - - fn spawn_exit_simple(&self, future: F) - where - F: Future> + 'js; -} - -impl<'js> CtxExtension<'js> for Ctx<'js> { - fn spawn_exit(&self, future: F) -> Result> - where - F: Future> + 'js, - R: 'js, - { - let ctx = self.clone(); - - let primordials = BasePrimordials::get(self)?; - let type_error: Object = primordials.constructor_type_error.construct(())?; - let stack: Option = type_error.get(PredefinedAtom::Stack).ok(); - - let (join_channel_tx, join_channel_rx) = oneshot::channel(); - - self.spawn(async move { - match future.await.catch(&ctx) { - Ok(res) => { - //result here doesn't matter if receiver has dropped - let _ = join_channel_tx.send(res); - } - Err(err) => handle_spawn_error(&ctx, err, stack), - } - }); - Ok(join_channel_rx) - } - - /// Same as above but fire & forget and without a forced stack trace collection - fn spawn_exit_simple(&self, future: F) - where - F: Future> + 'js, - { - let ctx = self.clone(); - self.spawn(async move { - if let Err(err) = future.await.catch(&ctx) { - handle_spawn_error(&ctx, err, None) - } - }); - } -} - -fn handle_spawn_error<'js>(ctx: &Ctx<'js>, err: CaughtError<'js>, stack: Option) { - let error_handler = if let Some(handler) = ERROR_HANDLER.get() { - handler - } else { - trace!("Future error: {:?}", err); - return; - }; - if let CaughtError::Exception(err) = err { - if err.stack().is_none() { - if let Some(stack) = stack { - err.set(PredefinedAtom::Stack, stack).unwrap(); - } - } - error_handler(ctx, CaughtError::Exception(err)); - } else { - error_handler(ctx, err); - } -} - -pub fn set_spawn_error_handler(handler: F) -where - F: for<'js> Fn(&Ctx<'js>, CaughtError<'js>) + Sync + Send + 'static, -{ - _ = ERROR_HANDLER.set(Box::new(handler)); -} diff --git a/stdlib/src/llrt/llrt_crypto/crc32.rs b/stdlib/src/llrt/llrt_crypto/crc32.rs deleted file mode 100644 index a9fc8eb2..00000000 --- a/stdlib/src/llrt/llrt_crypto/crc32.rs +++ /dev/null @@ -1,72 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::hash::Hasher; - -use crate::llrt_utils::bytes::ObjectBytes; -use crc32c::Crc32cHasher; -use rquickjs::{prelude::This, Class, Ctx, Result}; - -#[rquickjs::class] -#[derive(rquickjs::class::Trace, rquickjs::JsLifetime)] -pub struct Crc32c { - #[qjs(skip_trace)] - hasher: crc32c::Crc32cHasher, -} - -#[rquickjs::methods] -impl Crc32c { - #[qjs(constructor)] - fn new() -> Self { - Self { - hasher: Crc32cHasher::default(), - } - } - - #[qjs(rename = "digest")] - fn crc32c_digest(&self) -> u64 { - self.hasher.finish() - } - - #[qjs(rename = "update")] - fn crc32c_update<'js>( - this: This>, - ctx: Ctx<'js>, - bytes: ObjectBytes<'js>, - ) -> Result> { - this.0.borrow_mut().hasher.write(bytes.as_bytes(&ctx)?); - Ok(this.0) - } -} - -#[rquickjs::class] -#[derive(rquickjs::class::Trace, rquickjs::JsLifetime)] -pub struct Crc32 { - #[qjs(skip_trace)] - hasher: crc32fast::Hasher, -} - -#[rquickjs::methods] -impl Crc32 { - #[qjs(constructor)] - fn new() -> Self { - Self { - hasher: crc32fast::Hasher::new(), - } - } - - #[qjs(rename = "digest")] - fn crc32_digest(&self) -> u64 { - self.hasher.finish() - } - - #[qjs(rename = "update")] - fn crc32_update<'js>( - this: This>, - ctx: Ctx<'js>, - bytes: ObjectBytes<'js>, - ) -> Result> { - this.0.borrow_mut().hasher.write(bytes.as_bytes(&ctx)?); - Ok(this.0) - } -} diff --git a/stdlib/src/llrt/llrt_crypto/hash.rs b/stdlib/src/llrt/llrt_crypto/hash.rs deleted file mode 100644 index c0ffbd71..00000000 --- a/stdlib/src/llrt/llrt_crypto/hash.rs +++ /dev/null @@ -1,217 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use crate::llrt_buffer::Buffer; -use crate::llrt_utils::{bytes::ObjectBytes, iterable_enum, result::ResultExt}; -use rquickjs::{ - class::Trace, function::Opt, prelude::This, Class, Ctx, IntoJs, JsLifetime, Result, Value, -}; - -use super::encoded_bytes; -use crate::llrt_crypto::provider::{CryptoError, CryptoProvider, HmacProvider, SimpleDigest}; -use crate::llrt_crypto::CRYPTO_PROVIDER; - -#[derive(Debug, Clone, Copy)] -pub enum HashAlgorithm { - Md5, - Sha1, - Sha256, - Sha384, - Sha512, -} - -iterable_enum!(HashAlgorithm, Md5, Sha1, Sha256, Sha384, Sha512); - -impl TryFrom<&str> for HashAlgorithm { - type Error = String; - fn try_from(s: &str) -> std::result::Result { - Ok(match s.to_ascii_uppercase().as_str() { - "MD5" => HashAlgorithm::Md5, - "MD-5" => HashAlgorithm::Md5, - "SHA1" => HashAlgorithm::Sha1, - "SHA-1" => HashAlgorithm::Sha1, - "SHA256" => HashAlgorithm::Sha256, - "SHA-256" => HashAlgorithm::Sha256, - "SHA384" => HashAlgorithm::Sha384, - "SHA-384" => HashAlgorithm::Sha384, - "SHA512" => HashAlgorithm::Sha512, - "SHA-512" => HashAlgorithm::Sha512, - _ => return Err(["'", s, "' not available"].concat()), - }) - } -} - -impl HashAlgorithm { - pub fn class_name(&self) -> &'static str { - match self { - HashAlgorithm::Md5 => "Md5", - HashAlgorithm::Sha1 => "Sha1", - HashAlgorithm::Sha256 => "Sha256", - HashAlgorithm::Sha384 => "Sha384", - HashAlgorithm::Sha512 => "Sha512", - } - } - - pub fn as_str(&self) -> &'static str { - match self { - HashAlgorithm::Md5 => "MD5", - HashAlgorithm::Sha1 => "SHA-1", - HashAlgorithm::Sha256 => "SHA-256", - HashAlgorithm::Sha384 => "SHA-384", - HashAlgorithm::Sha512 => "SHA-512", - } - } - - pub fn as_numeric_str(&self) -> &'static str { - match self { - HashAlgorithm::Md5 => "md5", - HashAlgorithm::Sha1 => "1", - HashAlgorithm::Sha256 => "256", - HashAlgorithm::Sha384 => "384", - HashAlgorithm::Sha512 => "512", - } - } - - pub fn digest_len(&self) -> usize { - match self { - HashAlgorithm::Md5 => 16, - HashAlgorithm::Sha1 => 20, - HashAlgorithm::Sha256 => 32, - HashAlgorithm::Sha384 => 48, - HashAlgorithm::Sha512 => 64, - } - } - - pub fn block_len(&self) -> usize { - match self { - HashAlgorithm::Md5 => 64, - HashAlgorithm::Sha1 => 64, - HashAlgorithm::Sha256 => 64, - HashAlgorithm::Sha384 => 128, - HashAlgorithm::Sha512 => 128, - } - } - - pub(super) fn from_strict_str(s: &str) -> std::result::Result { - Ok(match s { - "SHA-1" => HashAlgorithm::Sha1, - "SHA-256" => HashAlgorithm::Sha256, - "SHA-384" => HashAlgorithm::Sha384, - "SHA-512" => HashAlgorithm::Sha512, - _ => return Err(CryptoError::UnsupportedAlgorithm), - }) - } -} - -type ProviderDigest = ::Digest; -type ProviderHmac = ::Hmac; - -#[derive(Trace, JsLifetime)] -#[rquickjs::class] -pub struct Hash { - #[qjs(skip_trace)] - digest: Option, - #[qjs(skip_trace)] - hmac: Option, -} - -impl Hash { - pub fn new(ctx: Ctx<'_>, algorithm: String) -> Result { - let algorithm = HashAlgorithm::try_from(algorithm.as_str()).or_throw(&ctx)?; - Ok(Self { - digest: Some(CRYPTO_PROVIDER.digest(algorithm)), - hmac: None, - }) - } - - pub fn new_hmac<'js>( - ctx: Ctx<'js>, - algorithm: String, - secret: ObjectBytes<'js>, - ) -> Result { - let algorithm = HashAlgorithm::try_from(algorithm.as_str()).or_throw(&ctx)?; - let key = secret.as_bytes(&ctx)?; - Ok(Self { - digest: None, - hmac: Some(CRYPTO_PROVIDER.hmac(algorithm, key)), - }) - } - - fn do_update(&mut self, data: &[u8]) { - if let Some(ref mut d) = self.digest { - d.update(data); - } else if let Some(ref mut h) = self.hmac { - h.update(data); - } - } - - fn do_finalize(&mut self) -> Option> { - if let Some(d) = self.digest.take() { - Some(d.finalize()) - } else { - self.hmac.take().map(|h| h.finalize()) - } - } -} - -#[rquickjs::methods] -impl Hash { - #[qjs(rename = "digest")] - fn hash_digest<'js>(&mut self, ctx: Ctx<'js>, encoding: Opt) -> Result> { - let result = self - .do_finalize() - .ok_or_else(|| rquickjs::Exception::throw_message(&ctx, "Digest already called"))?; - - let Some(encoding) = encoding.0 else { - return Buffer(result).into_js(&ctx); - }; - - match encoded_bytes(&ctx, &result, &encoding)? { - Some(encoded) => Ok(encoded), - None => Buffer(result).into_js(&ctx), - } - } - - #[qjs(rename = "update")] - fn hash_update<'js>( - this: This>, - ctx: Ctx<'js>, - bytes: ObjectBytes<'js>, - ) -> Result> { - let bytes = bytes.as_bytes(&ctx)?; - this.0.borrow_mut().do_update(bytes); - Ok(this.0) - } -} - -#[derive(Trace, JsLifetime)] -#[rquickjs::class] -pub struct Hmac { - #[qjs(skip_trace)] - hash: Hash, -} - -impl Hmac { - pub fn new<'js>(ctx: Ctx<'js>, algorithm: String, key_value: ObjectBytes<'js>) -> Result { - Ok(Self { - hash: Hash::new_hmac(ctx, algorithm, key_value)?, - }) - } -} - -#[rquickjs::methods] -impl Hmac { - fn digest<'js>(&mut self, ctx: Ctx<'js>, encoding: Opt) -> Result> { - self.hash.hash_digest(ctx, encoding) - } - - fn update<'js>( - this: This>, - ctx: Ctx<'js>, - bytes: ObjectBytes<'js>, - ) -> Result> { - let bytes = bytes.as_bytes(&ctx)?; - this.0.borrow_mut().hash.do_update(bytes); - Ok(this.0) - } -} diff --git a/stdlib/src/llrt/llrt_crypto/lib.rs b/stdlib/src/llrt/llrt_crypto/lib.rs deleted file mode 100644 index faab2edc..00000000 --- a/stdlib/src/llrt/llrt_crypto/lib.rs +++ /dev/null @@ -1,385 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -// Compile-time checks for conflicting crypto features -#[cfg(all(all(), any()))] -compile_error!("Features `crypto-rust` and `crypto-openssl` are mutually exclusive"); - -#[cfg(all(all(), any()))] -compile_error!("Features `crypto-rust` and `crypto-ring` are mutually exclusive"); - -#[cfg(all(all(), any()))] -compile_error!("Features `crypto-rust` and `crypto-graviola` are mutually exclusive"); - -#[cfg(all(any(), any()))] -compile_error!("Features `crypto-openssl` and `crypto-ring` are mutually exclusive"); - -#[cfg(all(any(), any()))] -compile_error!("Features `crypto-openssl` and `crypto-graviola` are mutually exclusive"); - -#[cfg(all(any(), any()))] -compile_error!("Features `crypto-ring` and `crypto-graviola` are mutually exclusive"); - -mod crc32; -mod hash; -mod subtle; - -mod provider; - -use std::slice; - -use crate::llrt_buffer::Buffer; -use crate::llrt_context::CtxExtension; -use crate::llrt_encoding::{bytes_to_b64_string, bytes_to_hex_string}; -use crate::llrt_exceptions::DOMException; -use crate::llrt_utils::{ - bytes::{get_start_end_indexes, ObjectBytes}, - error::ErrorExtensions, - error_messages::{ERROR_MSG_ARRAY_BUFFER_DETACHED, ERROR_MSG_NOT_ARRAY_BUFFER}, - module::{export_default, ModuleInfo}, - result::ResultExt, -}; -use once_cell::sync::Lazy; -use rand::RngExt; -use rquickjs::prelude::Async; -use rquickjs::{ - atom::PredefinedAtom, - function::{Constructor, Opt}, - module::{Declarations, Exports, ModuleDef}, - prelude::{Func, Rest}, - Class, Ctx, Error, Exception, Function, IntoJs, Null, Object, Result, Value, -}; -pub use subtle::CryptoKey; -use subtle::{ - subtle_decrypt, subtle_derive_bits, subtle_derive_key, subtle_digest, subtle_encrypt, - subtle_export_key, subtle_generate_key, subtle_import_key, subtle_sign, subtle_unwrap_key, - subtle_verify, subtle_wrap_key, SubtleCrypto, -}; - -use self::{ - crc32::{Crc32, Crc32c}, - hash::{Hash, HashAlgorithm, Hmac}, -}; - -static CRYPTO_PROVIDER: Lazy = - Lazy::new(|| provider::DefaultProvider {}); - -fn encoded_bytes<'js>(ctx: &Ctx<'js>, bytes: &[u8], encoding: &str) -> Result>> { - match encoding { - "hex" => { - let hex = bytes_to_hex_string(bytes); - let hex = rquickjs::String::from_str(ctx.clone(), &hex)?; - Ok(Some(Value::from_string(hex))) - } - "base64" => { - let b64 = bytes_to_b64_string(bytes); - let b64 = rquickjs::String::from_str(ctx.clone(), &b64)?; - Ok(Some(Value::from_string(b64))) - } - _ => Ok(None), - } -} - -#[inline] -pub fn random_byte_array(length: usize) -> Vec { - let mut vec = vec![0u8; length]; - rand::rng().fill(&mut vec[..]); - vec -} - -fn get_random_bytes(ctx: Ctx, length: usize) -> Result { - let random_bytes = random_byte_array(length); - Buffer(random_bytes).into_js(&ctx) -} - -fn get_random_int(first: i64, second: Opt) -> Result { - let mut rng = rand::rng(); - let random_number = match second.0 { - Some(max) => rng.random_range(first..max), - None => rng.random_range(0..first), - }; - - Ok(random_number) -} - -fn random_fill<'js>(ctx: Ctx<'js>, obj: Object<'js>, args: Rest>) -> Result<()> { - let args_iter = args.0.into_iter(); - let mut args_iter = args_iter.rev(); - - let callback: Function = args_iter - .next() - .and_then(|v| v.into_function()) - .or_throw_msg(&ctx, "Callback required")?; - let size = args_iter - .next() - .and_then(|arg| arg.as_int()) - .map(|i| i as usize); - let offset = args_iter - .next() - .and_then(|arg| arg.as_int()) - .map(|i| i as usize); - - ctx.clone().spawn_exit(async move { - if let Err(err) = random_fill_sync(ctx.clone(), obj.clone(), Opt(offset), Opt(size)) { - let err = err.into_value(&ctx)?; - () = callback.call((err,))?; - - return Ok(()); - } - () = callback.call((Null.into_js(&ctx), obj))?; - Ok::<_, Error>(()) - })?; - Ok(()) -} - -fn random_fill_sync<'js>( - ctx: Ctx<'js>, - obj: Object<'js>, - offset: Opt, - size: Opt, -) -> Result> { - let offset = offset.unwrap_or(0); - - if let Some(object_bytes) = ObjectBytes::from_array_buffer(&obj)? { - let (array_buffer, source_length, source_offset) = object_bytes - .get_array_buffer()? - .expect(ERROR_MSG_NOT_ARRAY_BUFFER); - let raw = array_buffer - .as_raw() - .ok_or(ERROR_MSG_ARRAY_BUFFER_DETACHED) - .or_throw(&ctx)?; - - if offset > source_length { - return Err(Exception::throw_range( - &ctx, - "The value of \"offset\" is out of range", - )); - } - if let Some(size) = size.0 { - if offset + size > source_length { - return Err(Exception::throw_range( - &ctx, - "The value of \"size + offset\" is out of range", - )); - } - } - - let (start, end) = get_start_end_indexes(source_length, size.0, offset); - - // SAFETY: source_offset..+source_length stays in the backing buffer; - // start/end are clamped to it above. - let bytes = unsafe { - slice::from_raw_parts_mut(raw.ptr.as_ptr().add(source_offset), source_length) - }; - - rand::rng().fill(&mut bytes[start..end]); - } - - Ok(obj) -} - -fn get_random_values<'js>(ctx: Ctx<'js>, obj: Object<'js>) -> Result> { - if let Some(object_bytes) = ObjectBytes::from_array_buffer(&obj)? { - if matches!( - object_bytes, - ObjectBytes::F64Array(_) - | ObjectBytes::F32Array(_) - | ObjectBytes::F16Array(_) - | ObjectBytes::DataView(_, _, _) - ) { - return Err(DOMException::type_mismatch_error( - &ctx, - "getRandomValues requires an integer TypedArray", - )); - } - - let (array_buffer, source_length, source_offset) = object_bytes - .get_array_buffer()? - .expect(ERROR_MSG_NOT_ARRAY_BUFFER); - let raw = array_buffer - .as_raw() - .ok_or(ERROR_MSG_ARRAY_BUFFER_DETACHED) - .or_throw(&ctx)?; - - if source_length > 0x10000 { - return Err(DOMException::quota_exceeded_error( - &ctx, - "The requested length exceeds 65,536 bytes", - )); - } - - let bytes = unsafe { - std::slice::from_raw_parts_mut(raw.ptr.as_ptr().add(source_offset), source_length) - }; - - rand::rng().fill(bytes) - } - - Ok(obj) -} - -fn uuidv4() -> String { - let uuid = rand::random::() & 0xFFFFFFFFFFFF4FFFBFFFFFFFFFFFFFFF | 0x40008000000000000000; - - static HEX_CHARS: &[u8; 16] = b"0123456789abcdef"; - let bytes = uuid.to_be_bytes(); - - let mut buf = [0u8; 36]; - - // Precomputed positions for 32 hex digits (excluding hyphens) - static HEX_POS: [usize; 32] = [ - 0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 14, 15, 16, 17, 19, 20, 21, 22, 24, 25, 26, 27, 28, - 29, 30, 31, 32, 33, 34, 35, - ]; - - // Map each byte to its hex representation - let mut hex_idx = 0; - for &byte in &bytes[..] { - let high = HEX_CHARS[(byte >> 4) as usize]; - let low = HEX_CHARS[(byte & 0x0f) as usize]; - - buf[HEX_POS[hex_idx]] = high; - buf[HEX_POS[hex_idx + 1]] = low; - hex_idx += 2; - } - - // Insert hyphens at standard positions - buf[8] = b'-'; - buf[13] = b'-'; - buf[18] = b'-'; - buf[23] = b'-'; - - // SAFETY: The buffer only contains valid UTF-8 characters (hex digits and hyphens) - // that were explicitly set from the HEX_CHARS array and hyphen literals - unsafe { String::from_utf8_unchecked(buf.to_vec()) } -} - -#[rquickjs::class] -#[derive(rquickjs::JsLifetime, rquickjs::class::Trace)] -struct Crypto {} - -#[rquickjs::methods] -impl Crypto { - #[qjs(constructor)] - pub fn new(ctx: Ctx<'_>) -> Result { - Err(Exception::throw_type(&ctx, "Illegal constructor")) - } - - #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] - pub fn to_string_tag() -> &'static str { - stringify!(Crypto) - } -} - -pub fn init(ctx: &Ctx<'_>) -> Result<()> { - let globals = ctx.globals(); - - Class::::define(&globals)?; - let crypto = Class::instance(ctx.clone(), Crypto {})?; - - crypto.set("createHash", Func::from(Hash::new))?; - crypto.set("createHmac", Func::from(Hmac::new))?; - crypto.set("randomBytes", Func::from(get_random_bytes))?; - crypto.set("randomInt", Func::from(get_random_int))?; - crypto.set("randomUUID", Func::from(uuidv4))?; - crypto.set("randomFillSync", Func::from(random_fill_sync))?; - crypto.set("randomFill", Func::from(random_fill))?; - crypto.set("getRandomValues", Func::from(get_random_values))?; - - Class::::define(&globals)?; - Class::::define(&globals)?; - - let subtle = Class::instance(ctx.clone(), SubtleCrypto {})?; - subtle.set("decrypt", Func::from(Async(subtle_decrypt)))?; - subtle.set("deriveKey", Func::from(Async(subtle_derive_key)))?; - subtle.set("deriveBits", Func::from(Async(subtle_derive_bits)))?; - subtle.set("digest", Func::from(Async(subtle_digest)))?; - subtle.set("encrypt", Func::from(Async(subtle_encrypt)))?; - subtle.set("exportKey", Func::from(Async(subtle_export_key)))?; - subtle.set("generateKey", Func::from(Async(subtle_generate_key)))?; - subtle.set("importKey", Func::from(Async(subtle_import_key)))?; - subtle.set("sign", Func::from(Async(subtle_sign)))?; - subtle.set("verify", Func::from(Async(subtle_verify)))?; - subtle.set("wrapKey", Func::from(Async(subtle_wrap_key)))?; - subtle.set("unwrapKey", Func::from(Async(subtle_unwrap_key)))?; - crypto.set("subtle", subtle)?; - - globals.set("crypto", crypto)?; - - Ok(()) -} - -pub struct CryptoModule; - -impl ModuleDef for CryptoModule { - fn declare(declare: &Declarations) -> Result<()> { - declare.declare("createHash")?; - declare.declare("createHmac")?; - declare.declare("Crc32")?; - declare.declare("Crc32c")?; - declare.declare("randomBytes")?; - declare.declare("randomUUID")?; - declare.declare("randomInt")?; - declare.declare("randomFillSync")?; - declare.declare("randomFill")?; - declare.declare("getRandomValues")?; - - for algorithm in HashAlgorithm::iter() { - declare.declare(algorithm.class_name())?; - } - - declare.declare("crypto")?; - declare.declare("webcrypto")?; - declare.declare("default")?; - - Ok(()) - } - - fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { - export_default(ctx, exports, |default| { - for algorithm in HashAlgorithm::iter() { - let class_name: &str = algorithm.class_name(); - let algo_name = String::from(algorithm.as_str()); - - let ctor = Constructor::new_class::( - ctx.clone(), - move |ctx: Ctx<'js>, secret: Opt>| match secret.0 { - Some(secret) => Hash::new_hmac(ctx, algo_name.clone(), secret), - None => Hash::new(ctx, algo_name.clone()), - }, - )?; - - default.set(class_name, ctor)?; - } - - let crypto: Object = ctx.globals().get("crypto")?; - - Class::::define(default)?; - Class::::define(default)?; - - default.set("createHash", Func::from(Hash::new))?; - default.set("createHmac", Func::from(Hmac::new))?; - default.set("randomBytes", Func::from(get_random_bytes))?; - default.set("randomInt", Func::from(get_random_int))?; - default.set("randomUUID", Func::from(uuidv4))?; - default.set("randomFillSync", Func::from(random_fill_sync))?; - default.set("randomFill", Func::from(random_fill))?; - default.set("getRandomValues", Func::from(get_random_values))?; - default.set("crypto", crypto.clone())?; - default.set("webcrypto", crypto)?; - Ok(()) - })?; - - Ok(()) - } -} - -impl From for ModuleInfo { - fn from(val: CryptoModule) -> Self { - ModuleInfo { - name: "crypto", - module: val, - } - } -} diff --git a/stdlib/src/llrt/llrt_crypto/provider/graviola.rs b/stdlib/src/llrt/llrt_crypto/provider/graviola.rs deleted file mode 100644 index c7b56047..00000000 --- a/stdlib/src/llrt/llrt_crypto/provider/graviola.rs +++ /dev/null @@ -1,571 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Graviola crypto provider - a high-performance crypto library using formally verified assembler. -//! -//! Supported: SHA256/384/512, HMAC, AES-GCM -//! Not supported: Most other operations due to API limitations - -use graviola::{ - aead::AesGcm, - hashing::{hmac::Hmac, Hash, HashContext, Sha256, Sha384, Sha512}, -}; - -use crate::llrt_crypto::hash::HashAlgorithm; -use crate::llrt_crypto::provider::{ - AesMode, CryptoError, CryptoProvider, HmacProvider, SimpleDigest, -}; -use crate::llrt_crypto::subtle::EllipticCurve; - -pub struct GraviolaProvider; - -pub enum GraviolaDigest { - Sha256(::Context), - Sha384(::Context), - Sha512(::Context), -} - -impl SimpleDigest for GraviolaDigest { - fn update(&mut self, data: &[u8]) { - match self { - GraviolaDigest::Sha256(h) => h.update(data), - GraviolaDigest::Sha384(h) => h.update(data), - GraviolaDigest::Sha512(h) => h.update(data), - } - } - - fn finalize(self) -> Vec { - match self { - GraviolaDigest::Sha256(h) => h.finish().as_ref().to_vec(), - GraviolaDigest::Sha384(h) => h.finish().as_ref().to_vec(), - GraviolaDigest::Sha512(h) => h.finish().as_ref().to_vec(), - } - } -} - -pub enum GraviolaHmac { - Sha256(Hmac), - Sha384(Hmac), - Sha512(Hmac), -} - -impl HmacProvider for GraviolaHmac { - fn update(&mut self, data: &[u8]) { - match self { - GraviolaHmac::Sha256(h) => h.update(data), - GraviolaHmac::Sha384(h) => h.update(data), - GraviolaHmac::Sha512(h) => h.update(data), - } - } - - fn finalize(self) -> Vec { - match self { - GraviolaHmac::Sha256(h) => h.finish().as_ref().to_vec(), - GraviolaHmac::Sha384(h) => h.finish().as_ref().to_vec(), - GraviolaHmac::Sha512(h) => h.finish().as_ref().to_vec(), - } - } -} - -impl CryptoProvider for GraviolaProvider { - type Digest = GraviolaDigest; - type Hmac = GraviolaHmac; - - fn digest(&self, algorithm: HashAlgorithm) -> Self::Digest { - match algorithm { - HashAlgorithm::Sha256 => GraviolaDigest::Sha256(Sha256::new()), - HashAlgorithm::Sha384 => GraviolaDigest::Sha384(Sha384::new()), - HashAlgorithm::Sha512 => GraviolaDigest::Sha512(Sha512::new()), - _ => panic!("Unsupported digest algorithm for Graviola"), - } - } - - fn hmac(&self, algorithm: HashAlgorithm, key: &[u8]) -> Self::Hmac { - match algorithm { - HashAlgorithm::Sha256 => GraviolaHmac::Sha256(Hmac::::new(key)), - HashAlgorithm::Sha384 => GraviolaHmac::Sha384(Hmac::::new(key)), - HashAlgorithm::Sha512 => GraviolaHmac::Sha512(Hmac::::new(key)), - _ => panic!("Unsupported HMAC algorithm for Graviola"), - } - } - - fn ecdsa_sign( - &self, - _curve: EllipticCurve, - _private_key_der: &[u8], - _digest: &[u8], - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn ecdsa_verify( - &self, - _curve: EllipticCurve, - _public_key_sec1: &[u8], - _signature: &[u8], - _digest: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn ed25519_sign(&self, _private_key_der: &[u8], _data: &[u8]) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn ed25519_verify( - &self, - _public_key_bytes: &[u8], - _signature: &[u8], - _data: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn rsa_pss_sign( - &self, - _private_key_der: &[u8], - _digest: &[u8], - _salt_length: usize, - _hash_alg: HashAlgorithm, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn rsa_pss_verify( - &self, - _public_key_der: &[u8], - _signature: &[u8], - _digest: &[u8], - _salt_length: usize, - _hash_alg: HashAlgorithm, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn rsa_pkcs1v15_sign( - &self, - _private_key_der: &[u8], - _digest: &[u8], - _hash_alg: HashAlgorithm, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn rsa_pkcs1v15_verify( - &self, - _public_key_der: &[u8], - _signature: &[u8], - _digest: &[u8], - _hash_alg: HashAlgorithm, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn rsa_oaep_encrypt( - &self, - _public_key_der: &[u8], - _data: &[u8], - _hash_alg: HashAlgorithm, - _label: Option<&[u8]>, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn rsa_oaep_decrypt( - &self, - _private_key_der: &[u8], - _data: &[u8], - _hash_alg: HashAlgorithm, - _label: Option<&[u8]>, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn ecdh_derive_bits( - &self, - _curve: EllipticCurve, - _private_key_der: &[u8], - _public_key_sec1: &[u8], - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn x25519_derive_bits( - &self, - _private_key: &[u8], - _public_key: &[u8], - ) -> Result, CryptoError> { - // Graviola doesn't expose from_bytes for X25519 PrivateKey - Err(CryptoError::UnsupportedAlgorithm) - } - - fn aes_encrypt( - &self, - mode: AesMode, - key: &[u8], - iv: &[u8], - data: &[u8], - additional_data: Option<&[u8]>, - ) -> Result, CryptoError> { - match mode { - AesMode::Gcm { .. } => { - let nonce: [u8; 12] = iv.try_into().map_err(|_| CryptoError::InvalidData(None))?; - if !matches!(key.len(), 16 | 32) { - return Err(CryptoError::InvalidKey(None)); - } - let aead = AesGcm::new(key); - let aad = additional_data.unwrap_or(&[]); - let mut ciphertext = data.to_vec(); - let mut tag = [0u8; 16]; - aead.encrypt(&nonce, aad, &mut ciphertext, &mut tag); - ciphertext.extend_from_slice(&tag); - Ok(ciphertext) - } - _ => Err(CryptoError::UnsupportedAlgorithm), - } - } - - fn aes_decrypt( - &self, - mode: AesMode, - key: &[u8], - iv: &[u8], - data: &[u8], - additional_data: Option<&[u8]>, - ) -> Result, CryptoError> { - match mode { - AesMode::Gcm { .. } => { - let nonce: [u8; 12] = iv.try_into().map_err(|_| CryptoError::InvalidData(None))?; - if !matches!(key.len(), 16 | 32) { - return Err(CryptoError::InvalidKey(None)); - } - if data.len() < 16 { - return Err(CryptoError::InvalidData(None)); - } - let aead = AesGcm::new(key); - let aad = additional_data.unwrap_or(&[]); - let (ciphertext, tag) = data.split_at(data.len() - 16); - let tag: [u8; 16] = tag.try_into().unwrap(); - let mut plaintext = ciphertext.to_vec(); - aead.decrypt(&nonce, aad, &mut plaintext, &tag) - .map_err(|_| CryptoError::DecryptionFailed(None))?; - Ok(plaintext) - } - _ => Err(CryptoError::UnsupportedAlgorithm), - } - } - - fn aes_kw_wrap(&self, _kek: &[u8], _key: &[u8]) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn aes_kw_unwrap(&self, _kek: &[u8], _wrapped_key: &[u8]) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn hkdf_derive_key( - &self, - _key: &[u8], - _salt: &[u8], - _info: &[u8], - _length: usize, - _hash_alg: HashAlgorithm, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn pbkdf2_derive_key( - &self, - _password: &[u8], - _salt: &[u8], - _iterations: u32, - _length: usize, - _hash_alg: HashAlgorithm, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn generate_aes_key(&self, length_bits: u16) -> Result, CryptoError> { - if !matches!(length_bits, 128 | 256) { - return Err(CryptoError::InvalidLength); - } - Ok(crate::llrt_crypto::random_byte_array( - (length_bits / 8) as usize, - )) - } - - fn generate_hmac_key( - &self, - hash_alg: HashAlgorithm, - length_bits: u16, - ) -> Result, CryptoError> { - let length_bytes = if length_bits == 0 { - match hash_alg { - HashAlgorithm::Sha256 => 64, - HashAlgorithm::Sha384 | HashAlgorithm::Sha512 => 128, - _ => return Err(CryptoError::UnsupportedAlgorithm), - } - } else { - (length_bits / 8) as usize - }; - Ok(crate::llrt_crypto::random_byte_array(length_bytes)) - } - - fn generate_ec_key(&self, _curve: EllipticCurve) -> Result<(Vec, Vec), CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn generate_ed25519_key(&self) -> Result<(Vec, Vec), CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn generate_x25519_key(&self) -> Result<(Vec, Vec), CryptoError> { - // Graviola doesn't expose as_bytes for X25519 PrivateKey - Err(CryptoError::UnsupportedAlgorithm) - } - - fn generate_rsa_key( - &self, - _modulus_length: u32, - _public_exponent: &[u8], - ) -> Result<(Vec, Vec), CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn import_rsa_public_key_pkcs1( - &self, - _der: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_rsa_private_key_pkcs1( - &self, - _der: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_rsa_public_key_spki( - &self, - _der: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_rsa_private_key_pkcs8( - &self, - _der: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_rsa_public_key_pkcs1(&self, _key_data: &[u8]) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_rsa_public_key_spki(&self, _key_data: &[u8]) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_rsa_private_key_pkcs8(&self, _key_data: &[u8]) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_ec_public_key_sec1( - &self, - _data: &[u8], - _curve: EllipticCurve, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_ec_public_key_spki( - &self, - _der: &[u8], - _curve: EllipticCurve, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_ec_private_key_pkcs8( - &self, - _der: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_ec_private_key_sec1( - &self, - _data: &[u8], - _curve: EllipticCurve, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_ec_public_key_sec1( - &self, - _key_data: &[u8], - _curve: EllipticCurve, - _is_private: bool, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_ec_public_key_spki( - &self, - _key_data: &[u8], - _curve: EllipticCurve, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_ec_private_key_pkcs8( - &self, - _key_data: &[u8], - _curve: EllipticCurve, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_okp_public_key_raw( - &self, - _data: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_okp_public_key_spki( - &self, - _der: &[u8], - _expected_oid: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_okp_private_key_pkcs8( - &self, - _der: &[u8], - _expected_oid: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_okp_public_key_raw( - &self, - _key_data: &[u8], - _is_private: bool, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_okp_public_key_spki( - &self, - _key_data: &[u8], - _oid: &[u8], - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_okp_private_key_pkcs8( - &self, - _key_data: &[u8], - _oid: &[u8], - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_rsa_jwk( - &self, - _jwk: super::RsaJwkImport<'_>, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_rsa_jwk( - &self, - _key_data: &[u8], - _is_private: bool, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_ec_jwk( - &self, - _jwk: super::EcJwkImport<'_>, - _curve: EllipticCurve, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_ec_jwk( - &self, - _key_data: &[u8], - _curve: EllipticCurve, - _is_private: bool, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_okp_jwk( - &self, - _jwk: super::OkpJwkImport<'_>, - _is_ed25519: bool, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_okp_jwk( - &self, - _key_data: &[u8], - _is_private: bool, - _is_ed25519: bool, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } -} - -// Hybrid types for graviola-rust: Graviola for SHA256/384/512, RustCrypto for MD5/SHA1 -#[cfg(any())] -pub enum GraviolaRustDigest { - Graviola(GraviolaDigest), - Rust(super::rust::RustDigest), -} - -#[cfg(any())] -impl GraviolaRustDigest { - pub fn new(algorithm: HashAlgorithm) -> Self { - match algorithm { - HashAlgorithm::Sha256 | HashAlgorithm::Sha384 | HashAlgorithm::Sha512 => { - Self::Graviola(GraviolaProvider.digest(algorithm)) - } - _ => Self::Rust(super::rust::RustCryptoProvider.digest(algorithm)), - } - } -} - -#[cfg(any())] -impl SimpleDigest for GraviolaRustDigest { - fn update(&mut self, data: &[u8]) { - match self { - Self::Graviola(d) => d.update(data), - Self::Rust(d) => d.update(data), - } - } - fn finalize(self) -> Vec { - match self { - Self::Graviola(d) => d.finalize(), - Self::Rust(d) => d.finalize(), - } - } -} - -#[cfg(any())] -pub enum GraviolaRustHmac { - Graviola(GraviolaHmac), - Rust(super::rust::RustHmac), -} - -#[cfg(any())] -impl GraviolaRustHmac { - pub fn new(algorithm: HashAlgorithm, key: &[u8]) -> Self { - match algorithm { - HashAlgorithm::Sha256 | HashAlgorithm::Sha384 | HashAlgorithm::Sha512 => { - Self::Graviola(GraviolaProvider.hmac(algorithm, key)) - } - _ => Self::Rust(super::rust::RustCryptoProvider.hmac(algorithm, key)), - } - } -} - -#[cfg(any())] -impl HmacProvider for GraviolaRustHmac { - fn update(&mut self, data: &[u8]) { - match self { - Self::Graviola(h) => h.update(data), - Self::Rust(h) => h.update(data), - } - } - fn finalize(self) -> Vec { - match self { - Self::Graviola(h) => h.finalize(), - Self::Rust(h) => h.finalize(), - } - } -} diff --git a/stdlib/src/llrt/llrt_crypto/provider/mod.rs b/stdlib/src/llrt/llrt_crypto/provider/mod.rs deleted file mode 100644 index cff8c5e9..00000000 --- a/stdlib/src/llrt/llrt_crypto/provider/mod.rs +++ /dev/null @@ -1,1268 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -// Ensure only one crypto provider is selected -#[cfg(all(all(), any()))] -compile_error!("Features `crypto-rust` and `crypto-openssl` are mutually exclusive"); - -#[cfg(all(all(), any()))] -compile_error!("Features `crypto-rust` and `crypto-ring` are mutually exclusive"); - -#[cfg(all(all(), any()))] -compile_error!("Features `crypto-rust` and `crypto-graviola` are mutually exclusive"); - -#[cfg(all(any(), any()))] -compile_error!("Features `crypto-ring` and `crypto-openssl` are mutually exclusive"); - -#[cfg(all(any(), any()))] -compile_error!("Features `crypto-ring` and `crypto-graviola` are mutually exclusive"); - -#[cfg(all(any(), any()))] -compile_error!("Features `crypto-openssl` and `crypto-graviola` are mutually exclusive"); - -#[cfg(all(any(), any()))] -compile_error!("Features `crypto-ring-rust` and `crypto-graviola-rust` are mutually exclusive"); - -#[cfg(any(any(), any()))] -mod graviola; - -#[cfg(any())] -mod openssl; - -#[cfg(any(any(), any()))] -mod ring; - -#[cfg(all())] -mod rust; - -use crate::llrt_crypto::hash::HashAlgorithm; -use crate::llrt_crypto::subtle::EllipticCurve; - -#[derive(Debug)] -#[allow(dead_code)] -pub struct RsaImportResult { - pub key_data: Vec, - pub modulus_length: u32, - pub public_exponent: Vec, - pub is_private: bool, -} - -#[derive(Debug)] -#[allow(dead_code)] -pub struct EcImportResult { - pub key_data: Vec, - pub is_private: bool, -} - -#[derive(Debug)] -#[allow(dead_code)] -pub struct OkpImportResult { - pub key_data: Vec, - pub is_private: bool, -} - -/// RSA JWK components for import (all values are raw bytes, not base64) -#[derive(Debug)] -#[allow(dead_code)] -pub struct RsaJwkImport<'a> { - pub n: &'a [u8], // modulus - pub e: &'a [u8], // public exponent - pub d: Option<&'a [u8]>, // private exponent - pub p: Option<&'a [u8]>, // first prime - pub q: Option<&'a [u8]>, // second prime - pub dp: Option<&'a [u8]>, // first factor CRT exponent - pub dq: Option<&'a [u8]>, // second factor CRT exponent - pub qi: Option<&'a [u8]>, // first CRT coefficient -} - -/// RSA JWK components for export -#[derive(Debug)] -#[allow(dead_code)] -pub struct RsaJwkExport { - pub n: Vec, - pub e: Vec, - pub d: Option>, - pub p: Option>, - pub q: Option>, - pub dp: Option>, - pub dq: Option>, - pub qi: Option>, -} - -/// EC JWK components for import (all values are raw bytes) -#[derive(Debug)] -#[allow(dead_code)] -pub struct EcJwkImport<'a> { - pub x: &'a [u8], - pub y: &'a [u8], - pub d: Option<&'a [u8]>, -} - -/// EC JWK components for export -#[derive(Debug)] -#[allow(dead_code)] -pub struct EcJwkExport { - pub x: Vec, - pub y: Vec, - pub d: Option>, -} - -/// OKP (Ed25519/X25519) JWK components for import -#[derive(Debug)] -#[allow(dead_code)] -pub struct OkpJwkImport<'a> { - pub x: &'a [u8], // public key - pub d: Option<&'a [u8]>, // private key -} - -/// OKP JWK components for export -#[derive(Debug)] -#[allow(dead_code)] -pub struct OkpJwkExport { - pub x: Vec, - pub d: Option>, -} - -pub trait SimpleDigest: Send { - fn update(&mut self, data: &[u8]); - fn finalize(self) -> Vec - where - Self: Sized; -} - -#[derive(Debug, Clone, Copy)] -#[allow(dead_code)] -pub enum AesMode { - Ctr { counter_length: u32 }, - Cbc, - Gcm { tag_length: u8 }, -} - -#[allow(dead_code)] -pub trait CryptoProvider { - type Digest: SimpleDigest; - type Hmac: HmacProvider; - - // Digest operations - fn digest(&self, algorithm: HashAlgorithm) -> Self::Digest; - - // HMAC operations - fn hmac(&self, algorithm: HashAlgorithm, key: &[u8]) -> Self::Hmac; - - // ECDSA operations - fn ecdsa_sign( - &self, - curve: EllipticCurve, - private_key_der: &[u8], - digest: &[u8], - ) -> Result, CryptoError>; - fn ecdsa_verify( - &self, - curve: EllipticCurve, - public_key_sec1: &[u8], - signature: &[u8], - digest: &[u8], - ) -> Result; - - // EdDSA operations - fn ed25519_sign(&self, private_key_der: &[u8], data: &[u8]) -> Result, CryptoError>; - fn ed25519_verify( - &self, - public_key_bytes: &[u8], - signature: &[u8], - data: &[u8], - ) -> Result; - - // RSA operations - fn rsa_pss_sign( - &self, - private_key_der: &[u8], - digest: &[u8], - salt_length: usize, - hash_alg: HashAlgorithm, - ) -> Result, CryptoError>; - fn rsa_pss_verify( - &self, - public_key_der: &[u8], - signature: &[u8], - digest: &[u8], - salt_length: usize, - hash_alg: HashAlgorithm, - ) -> Result; - fn rsa_pkcs1v15_sign( - &self, - private_key_der: &[u8], - digest: &[u8], - hash_alg: HashAlgorithm, - ) -> Result, CryptoError>; - fn rsa_pkcs1v15_verify( - &self, - public_key_der: &[u8], - signature: &[u8], - digest: &[u8], - hash_alg: HashAlgorithm, - ) -> Result; - fn rsa_oaep_encrypt( - &self, - public_key_der: &[u8], - data: &[u8], - hash_alg: HashAlgorithm, - label: Option<&[u8]>, - ) -> Result, CryptoError>; - fn rsa_oaep_decrypt( - &self, - private_key_der: &[u8], - data: &[u8], - hash_alg: HashAlgorithm, - label: Option<&[u8]>, - ) -> Result, CryptoError>; - - // ECDH operations - fn ecdh_derive_bits( - &self, - curve: EllipticCurve, - private_key_der: &[u8], - public_key_sec1: &[u8], - ) -> Result, CryptoError>; - - // X25519 operations - fn x25519_derive_bits( - &self, - private_key: &[u8], - public_key: &[u8], - ) -> Result, CryptoError>; - - // AES operations - fn aes_encrypt( - &self, - mode: AesMode, - key: &[u8], - iv: &[u8], - data: &[u8], - additional_data: Option<&[u8]>, - ) -> Result, CryptoError>; - fn aes_decrypt( - &self, - mode: AesMode, - key: &[u8], - iv: &[u8], - data: &[u8], - additional_data: Option<&[u8]>, - ) -> Result, CryptoError>; - - // AES-KW operations - fn aes_kw_wrap(&self, kek: &[u8], key: &[u8]) -> Result, CryptoError>; - fn aes_kw_unwrap(&self, kek: &[u8], wrapped_key: &[u8]) -> Result, CryptoError>; - - // KDF operations - fn hkdf_derive_key( - &self, - key: &[u8], - salt: &[u8], - info: &[u8], - length: usize, - hash_alg: HashAlgorithm, - ) -> Result, CryptoError>; - fn pbkdf2_derive_key( - &self, - password: &[u8], - salt: &[u8], - iterations: u32, - length: usize, - hash_alg: HashAlgorithm, - ) -> Result, CryptoError>; - - fn generate_aes_key(&self, length_bits: u16) -> Result, CryptoError>; - fn generate_hmac_key( - &self, - hash_alg: HashAlgorithm, - length_bits: u16, - ) -> Result, CryptoError>; - fn generate_ec_key(&self, curve: EllipticCurve) -> Result<(Vec, Vec), CryptoError>; // (private, public) - fn generate_ed25519_key(&self) -> Result<(Vec, Vec), CryptoError>; - fn generate_x25519_key(&self) -> Result<(Vec, Vec), CryptoError>; - fn generate_rsa_key( - &self, - modulus_length: u32, - public_exponent: &[u8], - ) -> Result<(Vec, Vec), CryptoError>; - - // RSA key import from DER formats - fn import_rsa_public_key_pkcs1(&self, der: &[u8]) -> Result; - fn import_rsa_private_key_pkcs1(&self, der: &[u8]) -> Result; - fn import_rsa_public_key_spki(&self, der: &[u8]) -> Result; - fn import_rsa_private_key_pkcs8(&self, der: &[u8]) -> Result; - - // RSA key export to DER formats - fn export_rsa_public_key_pkcs1(&self, key_data: &[u8]) -> Result, CryptoError>; - fn export_rsa_public_key_spki(&self, key_data: &[u8]) -> Result, CryptoError>; - fn export_rsa_private_key_pkcs8(&self, key_data: &[u8]) -> Result, CryptoError>; - - // EC key import from DER formats - fn import_ec_public_key_sec1( - &self, - data: &[u8], - curve: EllipticCurve, - ) -> Result; - fn import_ec_public_key_spki( - &self, - der: &[u8], - curve: EllipticCurve, - ) -> Result; - fn import_ec_private_key_pkcs8(&self, der: &[u8]) -> Result; - fn import_ec_private_key_sec1( - &self, - data: &[u8], - curve: EllipticCurve, - ) -> Result; - - // EC key export - fn export_ec_public_key_sec1( - &self, - key_data: &[u8], - curve: EllipticCurve, - is_private: bool, - ) -> Result, CryptoError>; - fn export_ec_public_key_spki( - &self, - key_data: &[u8], - curve: EllipticCurve, - ) -> Result, CryptoError>; - fn export_ec_private_key_pkcs8( - &self, - key_data: &[u8], - curve: EllipticCurve, - ) -> Result, CryptoError>; - - // OKP (Ed25519/X25519) key import - fn import_okp_public_key_raw(&self, data: &[u8]) -> Result; - fn import_okp_public_key_spki( - &self, - der: &[u8], - expected_oid: &[u8], - ) -> Result; - fn import_okp_private_key_pkcs8( - &self, - der: &[u8], - expected_oid: &[u8], - ) -> Result; - - // OKP key export - fn export_okp_public_key_raw( - &self, - key_data: &[u8], - is_private: bool, - ) -> Result, CryptoError>; - fn export_okp_public_key_spki( - &self, - key_data: &[u8], - oid: &[u8], - ) -> Result, CryptoError>; - fn export_okp_private_key_pkcs8( - &self, - key_data: &[u8], - oid: &[u8], - ) -> Result, CryptoError>; - - // JWK import/export - fn import_rsa_jwk(&self, jwk: RsaJwkImport<'_>) -> Result; - fn export_rsa_jwk( - &self, - key_data: &[u8], - is_private: bool, - ) -> Result; - fn import_ec_jwk( - &self, - jwk: EcJwkImport<'_>, - curve: EllipticCurve, - ) -> Result; - fn export_ec_jwk( - &self, - key_data: &[u8], - curve: EllipticCurve, - is_private: bool, - ) -> Result; - - // OKP JWK import/export - fn import_okp_jwk( - &self, - jwk: OkpJwkImport<'_>, - is_ed25519: bool, - ) -> Result; - fn export_okp_jwk( - &self, - key_data: &[u8], - is_private: bool, - is_ed25519: bool, - ) -> Result; -} - -pub trait HmacProvider: Send { - fn update(&mut self, data: &[u8]); - fn finalize(self) -> Vec - where - Self: Sized; -} - -#[derive(Debug)] -#[allow(dead_code)] -pub enum CryptoError { - InvalidKey(Option>), - InvalidData(Option>), - InvalidSignature(Option>), - InvalidLength, - SigningFailed(Option>), - VerificationFailed, - OperationFailed(Option>), - UnsupportedAlgorithm, - DerivationFailed(Option>), - EncryptionFailed(Option>), - DecryptionFailed(Option>), - InvalidAccess(Option>), -} - -impl std::fmt::Display for CryptoError { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - CryptoError::InvalidKey(None) => write!(f, "Invalid key"), - CryptoError::InvalidKey(Some(msg)) => write!(f, "Invalid key: {}", msg), - CryptoError::InvalidData(None) => write!(f, "Invalid data"), - CryptoError::InvalidData(Some(msg)) => write!(f, "Invalid data: {}", msg), - CryptoError::InvalidSignature(None) => write!(f, "Invalid signature"), - CryptoError::InvalidSignature(Some(msg)) => write!(f, "Invalid signature: {}", msg), - CryptoError::InvalidLength => write!(f, "Invalid length"), - CryptoError::SigningFailed(None) => write!(f, "Signing failed"), - CryptoError::SigningFailed(Some(msg)) => write!(f, "Signing failed: {}", msg), - CryptoError::VerificationFailed => write!(f, "Verification failed"), - CryptoError::OperationFailed(None) => write!(f, "Operation failed"), - CryptoError::OperationFailed(Some(msg)) => write!(f, "Operation failed: {}", msg), - CryptoError::UnsupportedAlgorithm => write!(f, "Unsupported algorithm"), - CryptoError::DerivationFailed(None) => write!(f, "Derivation failed"), - CryptoError::DerivationFailed(Some(msg)) => write!(f, "Derivation failed: {}", msg), - CryptoError::EncryptionFailed(None) => write!(f, "Encryption failed"), - CryptoError::EncryptionFailed(Some(msg)) => write!(f, "Encryption failed: {}", msg), - CryptoError::DecryptionFailed(None) => write!(f, "Decryption failed"), - CryptoError::DecryptionFailed(Some(msg)) => write!(f, "Decryption failed: {}", msg), - CryptoError::InvalidAccess(None) => write!(f, "Invalid access"), - CryptoError::InvalidAccess(Some(msg)) => write!(f, "Invalid access: {}", msg), - } - } -} - -impl std::error::Error for CryptoError {} - -pub fn parse_rsa_public_exponent(public_exponent: &[u8]) -> Result { - match public_exponent { - [0x01, 0x00, 0x01] => Ok(65537), - [0x03] => Ok(3), - bytes if bytes.ends_with(&[0x03]) && bytes[..bytes.len() - 1].iter().all(|&b| b == 0) => { - Ok(3) - } - _ => Err(CryptoError::OperationFailed(None)), - } -} - -#[cfg(any())] -pub type DefaultProvider = openssl::OpenSslProvider; - -#[cfg(all())] -pub type DefaultProvider = rust::RustCryptoProvider; - -#[cfg(any())] -pub type DefaultProvider = ring::RingProvider; - -#[cfg(any())] -pub type DefaultProvider = RingRustProvider; - -#[cfg(all(any(), not(any())))] -pub type DefaultProvider = graviola::GraviolaProvider; - -#[cfg(any())] -pub type DefaultProvider = GraviolaRustProvider; - -// Macro to generate hybrid providers that delegate to RustCrypto -#[cfg(any(any(), any()))] -macro_rules! impl_hybrid_provider { - ($name:ident, $digest:ty, $hmac:ty, $digest_fn:expr, $hmac_fn:expr, $aes_encrypt:expr, $aes_decrypt:expr) => { - pub struct $name; - impl CryptoProvider for $name { - type Digest = $digest; - type Hmac = $hmac; - fn digest(&self, alg: HashAlgorithm) -> Self::Digest { - $digest_fn(alg) - } - fn hmac(&self, alg: HashAlgorithm, key: &[u8]) -> Self::Hmac { - $hmac_fn(alg, key) - } - fn ecdsa_sign( - &self, - c: EllipticCurve, - k: &[u8], - d: &[u8], - ) -> Result, CryptoError> { - rust::RustCryptoProvider.ecdsa_sign(c, k, d) - } - fn ecdsa_verify( - &self, - c: EllipticCurve, - k: &[u8], - s: &[u8], - d: &[u8], - ) -> Result { - rust::RustCryptoProvider.ecdsa_verify(c, k, s, d) - } - fn ed25519_sign(&self, k: &[u8], d: &[u8]) -> Result, CryptoError> { - rust::RustCryptoProvider.ed25519_sign(k, d) - } - fn ed25519_verify(&self, k: &[u8], s: &[u8], d: &[u8]) -> Result { - rust::RustCryptoProvider.ed25519_verify(k, s, d) - } - fn rsa_pss_sign( - &self, - k: &[u8], - d: &[u8], - s: usize, - a: HashAlgorithm, - ) -> Result, CryptoError> { - rust::RustCryptoProvider.rsa_pss_sign(k, d, s, a) - } - fn rsa_pss_verify( - &self, - k: &[u8], - s: &[u8], - d: &[u8], - sl: usize, - a: HashAlgorithm, - ) -> Result { - rust::RustCryptoProvider.rsa_pss_verify(k, s, d, sl, a) - } - fn rsa_pkcs1v15_sign( - &self, - k: &[u8], - d: &[u8], - a: HashAlgorithm, - ) -> Result, CryptoError> { - rust::RustCryptoProvider.rsa_pkcs1v15_sign(k, d, a) - } - fn rsa_pkcs1v15_verify( - &self, - k: &[u8], - s: &[u8], - d: &[u8], - a: HashAlgorithm, - ) -> Result { - rust::RustCryptoProvider.rsa_pkcs1v15_verify(k, s, d, a) - } - fn rsa_oaep_encrypt( - &self, - k: &[u8], - d: &[u8], - a: HashAlgorithm, - l: Option<&[u8]>, - ) -> Result, CryptoError> { - rust::RustCryptoProvider.rsa_oaep_encrypt(k, d, a, l) - } - fn rsa_oaep_decrypt( - &self, - k: &[u8], - d: &[u8], - a: HashAlgorithm, - l: Option<&[u8]>, - ) -> Result, CryptoError> { - rust::RustCryptoProvider.rsa_oaep_decrypt(k, d, a, l) - } - fn ecdh_derive_bits( - &self, - c: EllipticCurve, - pk: &[u8], - pubk: &[u8], - ) -> Result, CryptoError> { - rust::RustCryptoProvider.ecdh_derive_bits(c, pk, pubk) - } - fn x25519_derive_bits(&self, pk: &[u8], pubk: &[u8]) -> Result, CryptoError> { - rust::RustCryptoProvider.x25519_derive_bits(pk, pubk) - } - fn aes_encrypt( - &self, - m: AesMode, - k: &[u8], - iv: &[u8], - d: &[u8], - aad: Option<&[u8]>, - ) -> Result, CryptoError> { - $aes_encrypt(m, k, iv, d, aad) - } - fn aes_decrypt( - &self, - m: AesMode, - k: &[u8], - iv: &[u8], - d: &[u8], - aad: Option<&[u8]>, - ) -> Result, CryptoError> { - $aes_decrypt(m, k, iv, d, aad) - } - fn aes_kw_wrap(&self, kek: &[u8], k: &[u8]) -> Result, CryptoError> { - rust::RustCryptoProvider.aes_kw_wrap(kek, k) - } - fn aes_kw_unwrap(&self, kek: &[u8], w: &[u8]) -> Result, CryptoError> { - rust::RustCryptoProvider.aes_kw_unwrap(kek, w) - } - fn hkdf_derive_key( - &self, - k: &[u8], - s: &[u8], - i: &[u8], - l: usize, - a: HashAlgorithm, - ) -> Result, CryptoError> { - rust::RustCryptoProvider.hkdf_derive_key(k, s, i, l, a) - } - fn pbkdf2_derive_key( - &self, - p: &[u8], - s: &[u8], - i: u32, - l: usize, - a: HashAlgorithm, - ) -> Result, CryptoError> { - rust::RustCryptoProvider.pbkdf2_derive_key(p, s, i, l, a) - } - fn generate_aes_key(&self, b: u16) -> Result, CryptoError> { - rust::RustCryptoProvider.generate_aes_key(b) - } - fn generate_hmac_key(&self, a: HashAlgorithm, b: u16) -> Result, CryptoError> { - rust::RustCryptoProvider.generate_hmac_key(a, b) - } - fn generate_ec_key(&self, c: EllipticCurve) -> Result<(Vec, Vec), CryptoError> { - rust::RustCryptoProvider.generate_ec_key(c) - } - fn generate_ed25519_key(&self) -> Result<(Vec, Vec), CryptoError> { - rust::RustCryptoProvider.generate_ed25519_key() - } - fn generate_x25519_key(&self) -> Result<(Vec, Vec), CryptoError> { - rust::RustCryptoProvider.generate_x25519_key() - } - fn generate_rsa_key( - &self, - b: u32, - e: &[u8], - ) -> Result<(Vec, Vec), CryptoError> { - rust::RustCryptoProvider.generate_rsa_key(b, e) - } - fn import_rsa_public_key_pkcs1( - &self, - d: &[u8], - ) -> Result { - rust::RustCryptoProvider.import_rsa_public_key_pkcs1(d) - } - fn import_rsa_private_key_pkcs1( - &self, - d: &[u8], - ) -> Result { - rust::RustCryptoProvider.import_rsa_private_key_pkcs1(d) - } - fn import_rsa_public_key_spki(&self, d: &[u8]) -> Result { - rust::RustCryptoProvider.import_rsa_public_key_spki(d) - } - fn import_rsa_private_key_pkcs8( - &self, - d: &[u8], - ) -> Result { - rust::RustCryptoProvider.import_rsa_private_key_pkcs8(d) - } - fn export_rsa_public_key_pkcs1(&self, d: &[u8]) -> Result, CryptoError> { - rust::RustCryptoProvider.export_rsa_public_key_pkcs1(d) - } - fn export_rsa_public_key_spki(&self, d: &[u8]) -> Result, CryptoError> { - rust::RustCryptoProvider.export_rsa_public_key_spki(d) - } - fn export_rsa_private_key_pkcs8(&self, d: &[u8]) -> Result, CryptoError> { - rust::RustCryptoProvider.export_rsa_private_key_pkcs8(d) - } - fn import_ec_public_key_sec1( - &self, - d: &[u8], - c: EllipticCurve, - ) -> Result { - rust::RustCryptoProvider.import_ec_public_key_sec1(d, c) - } - fn import_ec_public_key_spki( - &self, - d: &[u8], - c: EllipticCurve, - ) -> Result { - rust::RustCryptoProvider.import_ec_public_key_spki(d, c) - } - fn import_ec_private_key_pkcs8(&self, d: &[u8]) -> Result { - rust::RustCryptoProvider.import_ec_private_key_pkcs8(d) - } - fn import_ec_private_key_sec1( - &self, - d: &[u8], - c: EllipticCurve, - ) -> Result { - rust::RustCryptoProvider.import_ec_private_key_sec1(d, c) - } - fn export_ec_public_key_sec1( - &self, - d: &[u8], - c: EllipticCurve, - p: bool, - ) -> Result, CryptoError> { - rust::RustCryptoProvider.export_ec_public_key_sec1(d, c, p) - } - fn export_ec_public_key_spki( - &self, - d: &[u8], - c: EllipticCurve, - ) -> Result, CryptoError> { - rust::RustCryptoProvider.export_ec_public_key_spki(d, c) - } - fn export_ec_private_key_pkcs8( - &self, - d: &[u8], - c: EllipticCurve, - ) -> Result, CryptoError> { - rust::RustCryptoProvider.export_ec_private_key_pkcs8(d, c) - } - fn import_okp_public_key_raw(&self, d: &[u8]) -> Result { - rust::RustCryptoProvider.import_okp_public_key_raw(d) - } - fn import_okp_public_key_spki( - &self, - d: &[u8], - o: &[u8], - ) -> Result { - rust::RustCryptoProvider.import_okp_public_key_spki(d, o) - } - fn import_okp_private_key_pkcs8( - &self, - d: &[u8], - o: &[u8], - ) -> Result { - rust::RustCryptoProvider.import_okp_private_key_pkcs8(d, o) - } - fn export_okp_public_key_raw(&self, d: &[u8], p: bool) -> Result, CryptoError> { - rust::RustCryptoProvider.export_okp_public_key_raw(d, p) - } - fn export_okp_public_key_spki( - &self, - d: &[u8], - o: &[u8], - ) -> Result, CryptoError> { - rust::RustCryptoProvider.export_okp_public_key_spki(d, o) - } - fn export_okp_private_key_pkcs8( - &self, - d: &[u8], - o: &[u8], - ) -> Result, CryptoError> { - rust::RustCryptoProvider.export_okp_private_key_pkcs8(d, o) - } - fn import_rsa_jwk(&self, j: RsaJwkImport<'_>) -> Result { - rust::RustCryptoProvider.import_rsa_jwk(j) - } - fn export_rsa_jwk(&self, d: &[u8], p: bool) -> Result { - rust::RustCryptoProvider.export_rsa_jwk(d, p) - } - fn import_ec_jwk( - &self, - j: EcJwkImport<'_>, - c: EllipticCurve, - ) -> Result { - rust::RustCryptoProvider.import_ec_jwk(j, c) - } - fn export_ec_jwk( - &self, - d: &[u8], - c: EllipticCurve, - p: bool, - ) -> Result { - rust::RustCryptoProvider.export_ec_jwk(d, c, p) - } - fn import_okp_jwk( - &self, - j: OkpJwkImport<'_>, - is_ed25519: bool, - ) -> Result { - rust::RustCryptoProvider.import_okp_jwk(j, is_ed25519) - } - fn export_okp_jwk( - &self, - d: &[u8], - is_private: bool, - is_ed25519: bool, - ) -> Result { - rust::RustCryptoProvider.export_okp_jwk(d, is_private, is_ed25519) - } - } - }; -} - -#[cfg(any())] -impl_hybrid_provider!( - RingRustProvider, - ring::RingDigestType, - ring::RingHmacType, - |a| ring::RingProvider.digest(a), - |a, k| ring::RingProvider.hmac(a, k), - |m, k, iv, d, aad| rust::RustCryptoProvider.aes_encrypt(m, k, iv, d, aad), - |m, k, iv, d, aad| rust::RustCryptoProvider.aes_decrypt(m, k, iv, d, aad) -); - -#[cfg(any())] -fn graviola_aes_supported() -> bool { - #[cfg(target_arch = "aarch64")] - { - std::arch::is_aarch64_feature_detected!("aes") - } - #[cfg(target_arch = "x86_64")] - { - std::arch::is_x86_feature_detected!("aes") - } - #[cfg(not(any(target_arch = "aarch64", target_arch = "x86_64")))] - { - false - } -} - -#[cfg(any())] -impl_hybrid_provider!( - GraviolaRustProvider, - graviola::GraviolaRustDigest, - graviola::GraviolaRustHmac, - graviola::GraviolaRustDigest::new, - graviola::GraviolaRustHmac::new, - |m: AesMode, k: &[u8], iv: &[u8], d: &[u8], aad: Option<&[u8]>| { - if graviola_aes_supported() - && matches!(m, AesMode::Gcm { .. }) - && matches!(k.len(), 16 | 32) - { - graviola::GraviolaProvider.aes_encrypt(m, k, iv, d, aad) - } else { - rust::RustCryptoProvider.aes_encrypt(m, k, iv, d, aad) - } - }, - |m: AesMode, k: &[u8], iv: &[u8], d: &[u8], aad: Option<&[u8]>| { - if graviola_aes_supported() - && matches!(m, AesMode::Gcm { .. }) - && matches!(k.len(), 16 | 32) - { - graviola::GraviolaProvider.aes_decrypt(m, k, iv, d, aad) - } else { - rust::RustCryptoProvider.aes_decrypt(m, k, iv, d, aad) - } - } -); - -#[cfg(test)] -mod tests { - use super::*; - - fn provider() -> impl CryptoProvider { - #[cfg(all())] - return rust::RustCryptoProvider; - #[cfg(any())] - return RingRustProvider; - #[cfg(any())] - return GraviolaRustProvider; - #[cfg(any())] - return openssl::OpenSslProvider; - #[cfg(any())] - return ring::RingProvider; - #[cfg(all(any(), not(any())))] - return graviola::GraviolaProvider; - } - - fn to_hex(bytes: &[u8]) -> String { - bytes.iter().map(|b| format!("{:02x}", b)).collect() - } - - // SHA digest tests - #[test] - fn test_sha256_digest() { - let p = provider(); - let mut digest = p.digest(HashAlgorithm::Sha256); - digest.update(b"hello world"); - let result = digest.finalize(); - assert_eq!(result.len(), 32); - assert_eq!( - to_hex(&result), - "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9" - ); - } - - #[test] - fn test_sha384_digest() { - let p = provider(); - let mut digest = p.digest(HashAlgorithm::Sha384); - digest.update(b"hello world"); - let result = digest.finalize(); - assert_eq!(result.len(), 48); - } - - #[test] - fn test_sha512_digest() { - let p = provider(); - let mut digest = p.digest(HashAlgorithm::Sha512); - digest.update(b"hello world"); - let result = digest.finalize(); - assert_eq!(result.len(), 64); - } - - // HMAC tests - #[test] - fn test_hmac_sha256() { - let p = provider(); - let key = b"secret key"; - let mut hmac = p.hmac(HashAlgorithm::Sha256, key); - hmac.update(b"hello world"); - let result = hmac.finalize(); - assert_eq!(result.len(), 32); - } - - // AES-GCM tests - only for providers that support AES - #[cfg(any(all(), any(), any(), any()))] - #[test] - fn test_aes_gcm_128_roundtrip() { - let p = provider(); - let key = [0u8; 16]; - let iv = [0u8; 12]; - let plaintext = b"hello world"; - let aad = b"additional data"; - - let ciphertext = p - .aes_encrypt( - AesMode::Gcm { tag_length: 128 }, - &key, - &iv, - plaintext, - Some(aad), - ) - .unwrap(); - - assert_eq!(ciphertext.len(), plaintext.len() + 16); // plaintext + tag - - let decrypted = p - .aes_decrypt( - AesMode::Gcm { tag_length: 128 }, - &key, - &iv, - &ciphertext, - Some(aad), - ) - .unwrap(); - - assert_eq!(decrypted, plaintext); - } - - #[cfg(any(all(), any(), any(), any()))] - #[test] - fn test_aes_gcm_256_roundtrip() { - let p = provider(); - let key = [0u8; 32]; - let iv = [0u8; 12]; - let plaintext = b"hello world"; - - let ciphertext = p - .aes_encrypt(AesMode::Gcm { tag_length: 128 }, &key, &iv, plaintext, None) - .unwrap(); - - let decrypted = p - .aes_decrypt( - AesMode::Gcm { tag_length: 128 }, - &key, - &iv, - &ciphertext, - None, - ) - .unwrap(); - - assert_eq!(decrypted, plaintext); - } - - #[cfg(any(all(), any(), any(), any()))] - #[test] - fn test_aes_gcm_wrong_key_fails() { - let p = provider(); - let key = [0u8; 16]; - let wrong_key = [1u8; 16]; - let iv = [0u8; 12]; - let plaintext = b"hello world"; - - let ciphertext = p - .aes_encrypt(AesMode::Gcm { tag_length: 128 }, &key, &iv, plaintext, None) - .unwrap(); - - let result = p.aes_decrypt( - AesMode::Gcm { tag_length: 128 }, - &wrong_key, - &iv, - &ciphertext, - None, - ); - - assert!(result.is_err()); - } - - // Key generation tests - only for providers that support key generation - #[cfg(any(all(), any(), any(), any()))] - #[test] - fn test_generate_aes_key_128() { - let p = provider(); - let key = p.generate_aes_key(128).unwrap(); - assert_eq!(key.len(), 16); - } - - #[cfg(any(all(), any(), any(), any()))] - #[test] - fn test_generate_aes_key_256() { - let p = provider(); - let key = p.generate_aes_key(256).unwrap(); - assert_eq!(key.len(), 32); - } - - #[cfg(any(all(), any(), any(), any()))] - #[test] - fn test_generate_hmac_key() { - let p = provider(); - let key = p.generate_hmac_key(HashAlgorithm::Sha256, 256).unwrap(); - assert_eq!(key.len(), 32); - } - - // Tests that require full crypto support - #[cfg(any(all(), any(), any(), any()))] - mod full_provider_tests { - use super::*; - - #[test] - fn test_aes_cbc_roundtrip() { - let p = provider(); - let key = [0u8; 16]; - let iv = [0u8; 16]; - let plaintext = b"hello world12345"; // 16 bytes for block alignment - - let ciphertext = p - .aes_encrypt(AesMode::Cbc, &key, &iv, plaintext, None) - .unwrap(); - - let decrypted = p - .aes_decrypt(AesMode::Cbc, &key, &iv, &ciphertext, None) - .unwrap(); - - assert_eq!(decrypted, plaintext); - } - - #[test] - fn test_aes_ctr_roundtrip() { - let p = provider(); - let key = [0u8; 16]; - let iv = [0u8; 16]; - let plaintext = b"hello world"; - - let ciphertext = p - .aes_encrypt( - AesMode::Ctr { counter_length: 64 }, - &key, - &iv, - plaintext, - None, - ) - .unwrap(); - - let decrypted = p - .aes_decrypt( - AesMode::Ctr { counter_length: 64 }, - &key, - &iv, - &ciphertext, - None, - ) - .unwrap(); - - assert_eq!(decrypted, plaintext); - } - - #[test] - fn test_aes_kw_roundtrip() { - let p = provider(); - let kek = [0u8; 16]; - let key_to_wrap = [1u8; 16]; - - let wrapped = p.aes_kw_wrap(&kek, &key_to_wrap).unwrap(); - let unwrapped = p.aes_kw_unwrap(&kek, &wrapped).unwrap(); - - assert_eq!(unwrapped, key_to_wrap); - } - - #[test] - fn test_hkdf_derive() { - let p = provider(); - let ikm = b"input key material"; - let salt = b"salt"; - let info = b"info"; - - let derived = p - .hkdf_derive_key(ikm, salt, info, 32, HashAlgorithm::Sha256) - .unwrap(); - - assert_eq!(derived.len(), 32); - } - - #[test] - fn test_pbkdf2_derive() { - let p = provider(); - let password = b"password"; - let salt = b"salt"; - - let derived = p - .pbkdf2_derive_key(password, salt, 1000, 32, HashAlgorithm::Sha256) - .unwrap(); - - assert_eq!(derived.len(), 32); - } - - #[test] - fn test_ec_p256_sign_verify() { - let p = provider(); - let (private_key, public_key) = p.generate_ec_key(EllipticCurve::P256).unwrap(); - - // Create a digest to sign - let mut digest = p.digest(HashAlgorithm::Sha256); - digest.update(b"message to sign"); - let hash = digest.finalize(); - - let signature = p - .ecdsa_sign(EllipticCurve::P256, &private_key, &hash) - .unwrap(); - - let valid = p - .ecdsa_verify(EllipticCurve::P256, &public_key, &signature, &hash) - .unwrap(); - - assert!(valid); - } - - #[test] - fn test_ec_p384_sign_verify() { - let p = provider(); - let (private_key, public_key) = p.generate_ec_key(EllipticCurve::P384).unwrap(); - - let mut digest = p.digest(HashAlgorithm::Sha384); - digest.update(b"message to sign"); - let hash = digest.finalize(); - - let signature = p - .ecdsa_sign(EllipticCurve::P384, &private_key, &hash) - .unwrap(); - - let valid = p - .ecdsa_verify(EllipticCurve::P384, &public_key, &signature, &hash) - .unwrap(); - - assert!(valid); - } - - #[test] - fn test_ed25519_sign_verify() { - let p = provider(); - let (private_key, public_key) = p.generate_ed25519_key().unwrap(); - - let message = b"message to sign"; - let signature = p.ed25519_sign(&private_key, message).unwrap(); - - let valid = p.ed25519_verify(&public_key, &signature, message).unwrap(); - - assert!(valid); - } - - #[test] - fn test_x25519_key_exchange() { - let p = provider(); - let (alice_private, alice_public) = p.generate_x25519_key().unwrap(); - let (bob_private, bob_public) = p.generate_x25519_key().unwrap(); - - let alice_shared = p.x25519_derive_bits(&alice_private, &bob_public).unwrap(); - let bob_shared = p.x25519_derive_bits(&bob_private, &alice_public).unwrap(); - - assert_eq!(alice_shared, bob_shared); - assert_eq!(alice_shared.len(), 32); - } - - #[test] - fn test_ecdh_p256_key_exchange() { - let p = provider(); - let (alice_private, alice_public) = p.generate_ec_key(EllipticCurve::P256).unwrap(); - let (bob_private, bob_public) = p.generate_ec_key(EllipticCurve::P256).unwrap(); - - let alice_shared = p - .ecdh_derive_bits(EllipticCurve::P256, &alice_private, &bob_public) - .unwrap(); - let bob_shared = p - .ecdh_derive_bits(EllipticCurve::P256, &bob_private, &alice_public) - .unwrap(); - - assert_eq!(alice_shared, bob_shared); - } - - #[test] - fn test_rsa_pss_sign_verify() { - let p = provider(); - let (private_key, public_key) = p.generate_rsa_key(2048, &[1, 0, 1]).unwrap(); - - let mut digest = p.digest(HashAlgorithm::Sha256); - digest.update(b"message to sign"); - let hash = digest.finalize(); - - let signature = p - .rsa_pss_sign(&private_key, &hash, 32, HashAlgorithm::Sha256) - .unwrap(); - - let valid = p - .rsa_pss_verify(&public_key, &signature, &hash, 32, HashAlgorithm::Sha256) - .unwrap(); - - assert!(valid); - } - - #[test] - fn test_rsa_pkcs1v15_sign_verify() { - let p = provider(); - let (private_key, public_key) = p.generate_rsa_key(2048, &[1, 0, 1]).unwrap(); - - let mut digest = p.digest(HashAlgorithm::Sha256); - digest.update(b"message to sign"); - let hash = digest.finalize(); - - let signature = p - .rsa_pkcs1v15_sign(&private_key, &hash, HashAlgorithm::Sha256) - .unwrap(); - - let valid = p - .rsa_pkcs1v15_verify(&public_key, &signature, &hash, HashAlgorithm::Sha256) - .unwrap(); - - assert!(valid); - } - - #[test] - fn test_rsa_oaep_encrypt_decrypt() { - let p = provider(); - let (private_key, public_key) = p.generate_rsa_key(2048, &[1, 0, 1]).unwrap(); - - let plaintext = b"secret message"; - - let ciphertext = p - .rsa_oaep_encrypt(&public_key, plaintext, HashAlgorithm::Sha256, None) - .unwrap(); - - let decrypted = p - .rsa_oaep_decrypt(&private_key, &ciphertext, HashAlgorithm::Sha256, None) - .unwrap(); - - assert_eq!(decrypted, plaintext); - } - } -} diff --git a/stdlib/src/llrt/llrt_crypto/provider/openssl.rs b/stdlib/src/llrt/llrt_crypto/provider/openssl.rs deleted file mode 100644 index f223c662..00000000 --- a/stdlib/src/llrt/llrt_crypto/provider/openssl.rs +++ /dev/null @@ -1,1319 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! OpenSSL crypto provider - uses OpenSSL for cryptographic operations. - -use openssl::bn::BigNum; -use openssl::derive::Deriver; -use openssl::ec::{EcGroup, EcKey}; -use openssl::ecdsa::EcdsaSig; -use openssl::hash::{Hasher, MessageDigest}; -use openssl::md::Md; -use openssl::nid::Nid; -use openssl::pkey::{Id, PKey}; -use openssl::pkey_ctx::PkeyCtx; -use openssl::rand::rand_bytes; -use openssl::rsa::{Padding, Rsa}; -use openssl::sign::{Signer, Verifier}; -use openssl::symm::{self, Cipher}; - -use crate::llrt_crypto::hash::HashAlgorithm; -use crate::llrt_crypto::provider::{ - AesMode, CryptoError, CryptoProvider, HmacProvider, SimpleDigest, -}; -use crate::llrt_crypto::subtle::EllipticCurve; - -pub struct OpenSslProvider; - -pub enum OpenSslDigest { - Md5(Hasher), - Sha1(Hasher), - Sha256(Hasher), - Sha384(Hasher), - Sha512(Hasher), -} - -impl SimpleDigest for OpenSslDigest { - fn update(&mut self, data: &[u8]) { - match self { - OpenSslDigest::Md5(h) - | OpenSslDigest::Sha1(h) - | OpenSslDigest::Sha256(h) - | OpenSslDigest::Sha384(h) - | OpenSslDigest::Sha512(h) => { - let _ = h.update(data); - } - } - } - - fn finalize(mut self) -> Vec { - match self { - OpenSslDigest::Md5(ref mut h) - | OpenSslDigest::Sha1(ref mut h) - | OpenSslDigest::Sha256(ref mut h) - | OpenSslDigest::Sha384(ref mut h) - | OpenSslDigest::Sha512(ref mut h) => { - h.finish().map(|d| d.to_vec()).unwrap_or_default() - } - } - } -} - -pub struct OpenSslHmac { - signer: Signer<'static>, -} - -impl HmacProvider for OpenSslHmac { - fn update(&mut self, data: &[u8]) { - let _ = self.signer.update(data); - } - - fn finalize(self) -> Vec { - self.signer.sign_to_vec().unwrap_or_default() - } -} - -fn get_message_digest(alg: HashAlgorithm) -> MessageDigest { - match alg { - HashAlgorithm::Md5 => MessageDigest::md5(), - HashAlgorithm::Sha1 => MessageDigest::sha1(), - HashAlgorithm::Sha256 => MessageDigest::sha256(), - HashAlgorithm::Sha384 => MessageDigest::sha384(), - HashAlgorithm::Sha512 => MessageDigest::sha512(), - } -} - -fn get_md(alg: HashAlgorithm) -> &'static openssl::md::MdRef { - match alg { - HashAlgorithm::Md5 => Md::md5(), - HashAlgorithm::Sha1 => Md::sha1(), - HashAlgorithm::Sha256 => Md::sha256(), - HashAlgorithm::Sha384 => Md::sha384(), - HashAlgorithm::Sha512 => Md::sha512(), - } -} - -fn curve_to_nid(curve: EllipticCurve) -> Nid { - match curve { - EllipticCurve::P256 => Nid::X9_62_PRIME256V1, - EllipticCurve::P384 => Nid::SECP384R1, - EllipticCurve::P521 => Nid::SECP521R1, - } -} - -fn get_ec_group(curve: EllipticCurve) -> Result { - let nid = curve_to_nid(curve); - EcGroup::from_curve_name(nid) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into()))) -} - -impl CryptoProvider for OpenSslProvider { - type Digest = OpenSslDigest; - type Hmac = OpenSslHmac; - - fn digest(&self, algorithm: HashAlgorithm) -> Self::Digest { - let md = get_message_digest(algorithm); - let hasher = Hasher::new(md).expect("Failed to create hasher"); - match algorithm { - HashAlgorithm::Md5 => OpenSslDigest::Md5(hasher), - HashAlgorithm::Sha1 => OpenSslDigest::Sha1(hasher), - HashAlgorithm::Sha256 => OpenSslDigest::Sha256(hasher), - HashAlgorithm::Sha384 => OpenSslDigest::Sha384(hasher), - HashAlgorithm::Sha512 => OpenSslDigest::Sha512(hasher), - } - } - - fn hmac(&self, algorithm: HashAlgorithm, key: &[u8]) -> Self::Hmac { - let md = get_message_digest(algorithm); - let pkey = PKey::hmac(key).expect("Failed to create HMAC key"); - let signer = unsafe { - std::mem::transmute::, Signer<'static>>( - Signer::new(md, &pkey).expect("Failed to create signer"), - ) - }; - OpenSslHmac { signer } - } - - fn ecdsa_sign( - &self, - curve: EllipticCurve, - private_key_der: &[u8], - digest: &[u8], - ) -> Result, CryptoError> { - let group = get_ec_group(curve)?; - let ec_key = EcKey::private_key_from_der(private_key_der) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let sig = EcdsaSig::sign(digest, &ec_key) - .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; - let r = sig.r().to_vec(); - let s = sig.s().to_vec(); - let coord_len = (group.degree() as usize).div_ceil(8); - let mut result = vec![0u8; coord_len * 2]; - result[coord_len - r.len()..coord_len].copy_from_slice(&r); - result[coord_len * 2 - s.len()..].copy_from_slice(&s); - Ok(result) - } - - fn ecdsa_verify( - &self, - curve: EllipticCurve, - public_key_sec1: &[u8], - signature: &[u8], - digest: &[u8], - ) -> Result { - let group = get_ec_group(curve)?; - let ec_key = EcKey::public_key_from_der(public_key_sec1).or_else(|_| { - let point = openssl::ec::EcPoint::from_bytes( - &group, - public_key_sec1, - &mut openssl::bn::BigNumContext::new().unwrap(), - ) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - EcKey::from_public_key(&group, &point) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) - })?; - let coord_len = signature.len() / 2; - let r = BigNum::from_slice(&signature[..coord_len]) - .map_err(|e| CryptoError::InvalidSignature(Some(e.to_string().into())))?; - let s = BigNum::from_slice(&signature[coord_len..]) - .map_err(|e| CryptoError::InvalidSignature(Some(e.to_string().into())))?; - let sig = EcdsaSig::from_private_components(r, s) - .map_err(|e| CryptoError::InvalidSignature(Some(e.to_string().into())))?; - Ok(sig.verify(digest, &ec_key).unwrap_or(false)) - } - - fn ed25519_sign(&self, private_key_der: &[u8], data: &[u8]) -> Result, CryptoError> { - let pkey = PKey::private_key_from_der(private_key_der) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let mut signer = Signer::new_without_digest(&pkey) - .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; - signer - .sign_oneshot_to_vec(data) - .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into()))) - } - - fn ed25519_verify( - &self, - public_key_bytes: &[u8], - signature: &[u8], - data: &[u8], - ) -> Result { - let pkey = PKey::public_key_from_raw_bytes(public_key_bytes, Id::ED25519) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let mut verifier = Verifier::new_without_digest(&pkey) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - Ok(verifier.verify_oneshot(signature, data).unwrap_or(false)) - } - - fn rsa_pss_sign( - &self, - private_key_der: &[u8], - digest: &[u8], - salt_length: usize, - hash_alg: HashAlgorithm, - ) -> Result, CryptoError> { - let rsa = Rsa::private_key_from_der(private_key_der) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let pkey = - PKey::from_rsa(rsa).map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let md = get_message_digest(hash_alg); - let mut signer = Signer::new(md, &pkey) - .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; - signer - .set_rsa_padding(Padding::PKCS1_PSS) - .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; - signer - .set_rsa_pss_saltlen(openssl::sign::RsaPssSaltlen::custom(salt_length as i32)) - .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; - signer - .set_rsa_mgf1_md(md) - .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; - signer - .update(digest) - .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; - signer - .sign_to_vec() - .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into()))) - } - - fn rsa_pss_verify( - &self, - public_key_der: &[u8], - signature: &[u8], - digest: &[u8], - salt_length: usize, - hash_alg: HashAlgorithm, - ) -> Result { - let rsa = Rsa::public_key_from_der(public_key_der) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let pkey = - PKey::from_rsa(rsa).map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let md = get_message_digest(hash_alg); - let mut verifier = Verifier::new(md, &pkey) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - verifier - .set_rsa_padding(Padding::PKCS1_PSS) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - verifier - .set_rsa_pss_saltlen(openssl::sign::RsaPssSaltlen::custom(salt_length as i32)) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - verifier - .set_rsa_mgf1_md(md) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - verifier - .update(digest) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - Ok(verifier.verify(signature).unwrap_or(false)) - } - - fn rsa_pkcs1v15_sign( - &self, - private_key_der: &[u8], - digest: &[u8], - hash_alg: HashAlgorithm, - ) -> Result, CryptoError> { - let rsa = Rsa::private_key_from_der(private_key_der) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let pkey = - PKey::from_rsa(rsa).map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let md = get_message_digest(hash_alg); - let mut signer = Signer::new(md, &pkey) - .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; - signer - .set_rsa_padding(Padding::PKCS1) - .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; - signer - .update(digest) - .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into())))?; - signer - .sign_to_vec() - .map_err(|e| CryptoError::SigningFailed(Some(e.to_string().into()))) - } - - fn rsa_pkcs1v15_verify( - &self, - public_key_der: &[u8], - signature: &[u8], - digest: &[u8], - hash_alg: HashAlgorithm, - ) -> Result { - let rsa = Rsa::public_key_from_der(public_key_der) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let pkey = - PKey::from_rsa(rsa).map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let md = get_message_digest(hash_alg); - let mut verifier = Verifier::new(md, &pkey) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - verifier - .set_rsa_padding(Padding::PKCS1) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - verifier - .update(digest) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - Ok(verifier.verify(signature).unwrap_or(false)) - } - - fn rsa_oaep_encrypt( - &self, - public_key_der: &[u8], - data: &[u8], - hash_alg: HashAlgorithm, - label: Option<&[u8]>, - ) -> Result, CryptoError> { - let rsa = Rsa::public_key_from_der(public_key_der) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let pkey = - PKey::from_rsa(rsa).map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let mut ctx = PkeyCtx::new(&pkey) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - ctx.encrypt_init() - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - ctx.set_rsa_padding(Padding::PKCS1_OAEP) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - ctx.set_rsa_oaep_md(get_md(hash_alg)) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - ctx.set_rsa_mgf1_md(get_md(hash_alg)) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - if let Some(lbl) = label { - ctx.set_rsa_oaep_label(lbl) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - } - let mut out = vec![0u8; pkey.size()]; - let len = ctx - .encrypt(data, Some(&mut out)) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - out.truncate(len); - Ok(out) - } - - fn rsa_oaep_decrypt( - &self, - private_key_der: &[u8], - data: &[u8], - hash_alg: HashAlgorithm, - label: Option<&[u8]>, - ) -> Result, CryptoError> { - let rsa = Rsa::private_key_from_der(private_key_der) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let pkey = - PKey::from_rsa(rsa).map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let mut ctx = PkeyCtx::new(&pkey) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - ctx.decrypt_init() - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - ctx.set_rsa_padding(Padding::PKCS1_OAEP) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - ctx.set_rsa_oaep_md(get_md(hash_alg)) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - ctx.set_rsa_mgf1_md(get_md(hash_alg)) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - if let Some(lbl) = label { - ctx.set_rsa_oaep_label(lbl) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - } - let mut out = vec![0u8; pkey.size()]; - let len = ctx - .decrypt(data, Some(&mut out)) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - out.truncate(len); - Ok(out) - } - - fn ecdh_derive_bits( - &self, - curve: EllipticCurve, - private_key_der: &[u8], - public_key_sec1: &[u8], - ) -> Result, CryptoError> { - let group = get_ec_group(curve)?; - let private_ec = EcKey::private_key_from_der(private_key_der) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let private_pkey = PKey::from_ec_key(private_ec) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let public_ec = EcKey::public_key_from_der(public_key_sec1).or_else(|_| { - let point = openssl::ec::EcPoint::from_bytes( - &group, - public_key_sec1, - &mut openssl::bn::BigNumContext::new().unwrap(), - ) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - EcKey::from_public_key(&group, &point) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) - })?; - let public_pkey = PKey::from_ec_key(public_ec) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let mut deriver = Deriver::new(&private_pkey) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - deriver - .set_peer(&public_pkey) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - deriver - .derive_to_vec() - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into()))) - } - - fn x25519_derive_bits( - &self, - private_key: &[u8], - public_key: &[u8], - ) -> Result, CryptoError> { - let private_pkey = PKey::private_key_from_raw_bytes(private_key, Id::X25519) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let public_pkey = PKey::public_key_from_raw_bytes(public_key, Id::X25519) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let mut deriver = Deriver::new(&private_pkey) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - deriver - .set_peer(&public_pkey) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - deriver - .derive_to_vec() - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into()))) - } - - fn aes_encrypt( - &self, - mode: AesMode, - key: &[u8], - iv: &[u8], - data: &[u8], - additional_data: Option<&[u8]>, - ) -> Result, CryptoError> { - match mode { - AesMode::Cbc => { - let cipher = match key.len() { - 16 => Cipher::aes_128_cbc(), - 24 => Cipher::aes_192_cbc(), - 32 => Cipher::aes_256_cbc(), - _ => { - return Err(CryptoError::InvalidKey(Some( - "Invalid AES key length".into(), - ))) - } - }; - symm::encrypt(cipher, key, Some(iv), data) - .map_err(|e| CryptoError::EncryptionFailed(Some(e.to_string().into()))) - } - AesMode::Ctr { .. } => { - let cipher = match key.len() { - 16 => Cipher::aes_128_ctr(), - 24 => Cipher::aes_192_ctr(), - 32 => Cipher::aes_256_ctr(), - _ => { - return Err(CryptoError::InvalidKey(Some( - "Invalid AES key length".into(), - ))) - } - }; - symm::encrypt(cipher, key, Some(iv), data) - .map_err(|e| CryptoError::EncryptionFailed(Some(e.to_string().into()))) - } - AesMode::Gcm { tag_length } => { - let cipher = match key.len() { - 16 => Cipher::aes_128_gcm(), - 24 => Cipher::aes_192_gcm(), - 32 => Cipher::aes_256_gcm(), - _ => { - return Err(CryptoError::InvalidKey(Some( - "Invalid AES key length".into(), - ))) - } - }; - let tag_len = (tag_length / 8) as usize; - let mut tag = vec![0u8; tag_len]; - let ciphertext = symm::encrypt_aead( - cipher, - key, - Some(iv), - additional_data.unwrap_or(&[]), - data, - &mut tag, - ) - .map_err(|e| CryptoError::EncryptionFailed(Some(e.to_string().into())))?; - let mut result = ciphertext; - result.extend_from_slice(&tag); - Ok(result) - } - } - } - - fn aes_decrypt( - &self, - mode: AesMode, - key: &[u8], - iv: &[u8], - data: &[u8], - additional_data: Option<&[u8]>, - ) -> Result, CryptoError> { - match mode { - AesMode::Cbc => { - let cipher = match key.len() { - 16 => Cipher::aes_128_cbc(), - 24 => Cipher::aes_192_cbc(), - 32 => Cipher::aes_256_cbc(), - _ => { - return Err(CryptoError::InvalidKey(Some( - "Invalid AES key length".into(), - ))) - } - }; - symm::decrypt(cipher, key, Some(iv), data) - .map_err(|e| CryptoError::DecryptionFailed(Some(e.to_string().into()))) - } - AesMode::Ctr { .. } => { - let cipher = match key.len() { - 16 => Cipher::aes_128_ctr(), - 24 => Cipher::aes_192_ctr(), - 32 => Cipher::aes_256_ctr(), - _ => { - return Err(CryptoError::InvalidKey(Some( - "Invalid AES key length".into(), - ))) - } - }; - symm::decrypt(cipher, key, Some(iv), data) - .map_err(|e| CryptoError::DecryptionFailed(Some(e.to_string().into()))) - } - AesMode::Gcm { tag_length } => { - let cipher = match key.len() { - 16 => Cipher::aes_128_gcm(), - 24 => Cipher::aes_192_gcm(), - 32 => Cipher::aes_256_gcm(), - _ => { - return Err(CryptoError::InvalidKey(Some( - "Invalid AES key length".into(), - ))) - } - }; - let tag_len = (tag_length / 8) as usize; - if data.len() < tag_len { - return Err(CryptoError::InvalidData(Some( - "Data too short for GCM tag".into(), - ))); - } - let (ciphertext, tag) = data.split_at(data.len() - tag_len); - symm::decrypt_aead( - cipher, - key, - Some(iv), - additional_data.unwrap_or(&[]), - ciphertext, - tag, - ) - .map_err(|e| CryptoError::DecryptionFailed(Some(e.to_string().into()))) - } - } - } - - fn aes_kw_wrap(&self, kek: &[u8], key: &[u8]) -> Result, CryptoError> { - use openssl::aes::{wrap_key, AesKey}; - let aes_key = AesKey::new_encrypt(kek).map_err(|_| CryptoError::InvalidKey(None))?; - let mut out = vec![0u8; key.len() + 8]; - wrap_key(&aes_key, None, &mut out, key).map_err(|_| CryptoError::OperationFailed(None))?; - Ok(out) - } - - fn aes_kw_unwrap(&self, kek: &[u8], wrapped_key: &[u8]) -> Result, CryptoError> { - use openssl::aes::{unwrap_key, AesKey}; - let aes_key = AesKey::new_decrypt(kek).map_err(|_| CryptoError::InvalidKey(None))?; - let mut out = vec![0u8; wrapped_key.len() - 8]; - unwrap_key(&aes_key, None, &mut out, wrapped_key) - .map_err(|_| CryptoError::OperationFailed(None))?; - Ok(out) - } - - fn hkdf_derive_key( - &self, - key: &[u8], - salt: &[u8], - info: &[u8], - length: usize, - hash_alg: HashAlgorithm, - ) -> Result, CryptoError> { - use openssl::pkey_ctx::HkdfMode; - let md = get_md(hash_alg); - let mut ctx = PkeyCtx::new_id(Id::HKDF) - .map_err(|e| CryptoError::DerivationFailed(Some(e.to_string().into())))?; - ctx.derive_init() - .map_err(|e| CryptoError::DerivationFailed(Some(e.to_string().into())))?; - ctx.set_hkdf_md(md) - .map_err(|e| CryptoError::DerivationFailed(Some(e.to_string().into())))?; - ctx.set_hkdf_mode(HkdfMode::EXTRACT_THEN_EXPAND) - .map_err(|e| CryptoError::DerivationFailed(Some(e.to_string().into())))?; - ctx.set_hkdf_key(key) - .map_err(|e| CryptoError::DerivationFailed(Some(e.to_string().into())))?; - if !salt.is_empty() { - ctx.set_hkdf_salt(salt) - .map_err(|e| CryptoError::DerivationFailed(Some(e.to_string().into())))?; - } - if !info.is_empty() { - ctx.add_hkdf_info(info) - .map_err(|e| CryptoError::DerivationFailed(Some(e.to_string().into())))?; - } - let mut out = vec![0u8; length]; - ctx.derive(Some(&mut out)) - .map_err(|e| CryptoError::DerivationFailed(Some(e.to_string().into())))?; - Ok(out) - } - - fn pbkdf2_derive_key( - &self, - password: &[u8], - salt: &[u8], - iterations: u32, - length: usize, - hash_alg: HashAlgorithm, - ) -> Result, CryptoError> { - let md = get_message_digest(hash_alg); - let mut out = vec![0u8; length]; - openssl::pkcs5::pbkdf2_hmac(password, salt, iterations as usize, md, &mut out) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - Ok(out) - } - - fn generate_aes_key(&self, length_bits: u16) -> Result, CryptoError> { - let length_bytes = (length_bits / 8) as usize; - let mut key = vec![0u8; length_bytes]; - rand_bytes(&mut key) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - Ok(key) - } - - fn generate_hmac_key( - &self, - hash_alg: HashAlgorithm, - length_bits: u16, - ) -> Result, CryptoError> { - let length_bytes = if length_bits == 0 { - match hash_alg { - HashAlgorithm::Md5 => 16, - HashAlgorithm::Sha1 => 20, - HashAlgorithm::Sha256 => 32, - HashAlgorithm::Sha384 => 48, - HashAlgorithm::Sha512 => 64, - } - } else { - (length_bits / 8) as usize - }; - let mut key = vec![0u8; length_bytes]; - rand_bytes(&mut key) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - Ok(key) - } - - fn generate_ec_key(&self, curve: EllipticCurve) -> Result<(Vec, Vec), CryptoError> { - let group = get_ec_group(curve)?; - let ec_key = EcKey::generate(&group) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - let pkey = PKey::from_ec_key(ec_key.clone()) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - // Return PKCS#8 DER for private key (consistent with RustCrypto) - let private_der = pkey - .private_key_to_der() - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - // Return SEC1 uncompressed point for public key (consistent with RustCrypto) - let mut bn_ctx = openssl::bn::BigNumContext::new() - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - let public_sec1 = ec_key - .public_key() - .to_bytes( - &group, - openssl::ec::PointConversionForm::UNCOMPRESSED, - &mut bn_ctx, - ) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - Ok((private_der, public_sec1)) - } - - fn generate_ed25519_key(&self) -> Result<(Vec, Vec), CryptoError> { - let pkey = PKey::generate_ed25519() - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - let private_der = pkey - .private_key_to_der() - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - let public_raw = pkey - .raw_public_key() - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - Ok((private_der, public_raw)) - } - - fn generate_x25519_key(&self) -> Result<(Vec, Vec), CryptoError> { - let pkey = PKey::generate_x25519() - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - let private_raw = pkey - .raw_private_key() - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - let public_raw = pkey - .raw_public_key() - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - Ok((private_raw, public_raw)) - } - - fn generate_rsa_key( - &self, - modulus_length: u32, - public_exponent: &[u8], - ) -> Result<(Vec, Vec), CryptoError> { - let exp = BigNum::from_slice(public_exponent) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let rsa = Rsa::generate_with_e(modulus_length, &exp) - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - let private_der = rsa - .private_key_to_der() - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - let public_der = rsa - .public_key_to_der() - .map_err(|e| CryptoError::OperationFailed(Some(e.to_string().into())))?; - Ok((private_der, public_der)) - } - - fn import_rsa_public_key_pkcs1( - &self, - der: &[u8], - ) -> Result { - let rsa = Rsa::public_key_from_der(der) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let modulus_length = rsa.n().num_bits() as u32; - let public_exponent = rsa.e().to_vec(); - let key_data = rsa - .public_key_to_der() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - Ok(super::RsaImportResult { - key_data, - modulus_length, - public_exponent, - is_private: false, - }) - } - - fn import_rsa_private_key_pkcs1( - &self, - der: &[u8], - ) -> Result { - let rsa = Rsa::private_key_from_der(der) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let modulus_length = rsa.n().num_bits() as u32; - let public_exponent = rsa.e().to_vec(); - let key_data = rsa - .private_key_to_der() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - Ok(super::RsaImportResult { - key_data, - modulus_length, - public_exponent, - is_private: true, - }) - } - - fn import_rsa_public_key_spki( - &self, - der: &[u8], - ) -> Result { - let pkey = PKey::public_key_from_der(der) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let rsa = pkey - .rsa() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let modulus_length = rsa.n().num_bits() as u32; - let public_exponent = rsa.e().to_vec(); - let key_data = rsa - .public_key_to_der() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - Ok(super::RsaImportResult { - key_data, - modulus_length, - public_exponent, - is_private: false, - }) - } - - fn import_rsa_private_key_pkcs8( - &self, - der: &[u8], - ) -> Result { - let pkey = PKey::private_key_from_der(der) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let rsa = pkey - .rsa() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let modulus_length = rsa.n().num_bits() as u32; - let public_exponent = rsa.e().to_vec(); - let key_data = rsa - .private_key_to_der() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - Ok(super::RsaImportResult { - key_data, - modulus_length, - public_exponent, - is_private: true, - }) - } - - fn export_rsa_public_key_pkcs1(&self, key_data: &[u8]) -> Result, CryptoError> { - let rsa = Rsa::public_key_from_der(key_data) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - rsa.public_key_to_der() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) - } - - fn export_rsa_public_key_spki(&self, key_data: &[u8]) -> Result, CryptoError> { - let rsa = Rsa::public_key_from_der(key_data) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let pkey = - PKey::from_rsa(rsa).map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - pkey.public_key_to_der() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) - } - - fn export_rsa_private_key_pkcs8(&self, key_data: &[u8]) -> Result, CryptoError> { - let rsa = Rsa::private_key_from_der(key_data) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let pkey = - PKey::from_rsa(rsa).map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - pkey.private_key_to_der() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) - } - - fn import_ec_public_key_sec1( - &self, - data: &[u8], - curve: EllipticCurve, - ) -> Result { - let nid = curve_to_nid(curve); - let group = EcGroup::from_curve_name(nid) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let mut ctx = openssl::bn::BigNumContext::new() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let point = openssl::ec::EcPoint::from_bytes(&group, data, &mut ctx) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let ec_key = EcKey::from_public_key(&group, &point) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let pkey = PKey::from_ec_key(ec_key) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - Ok(super::EcImportResult { - key_data: pkey - .public_key_to_der() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?, - is_private: false, - }) - } - - fn import_ec_public_key_spki( - &self, - der: &[u8], - _curve: EllipticCurve, - ) -> Result { - let pkey = PKey::public_key_from_der(der) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - Ok(super::EcImportResult { - key_data: pkey - .public_key_to_der() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?, - is_private: false, - }) - } - - fn import_ec_private_key_pkcs8( - &self, - der: &[u8], - ) -> Result { - let pkey = PKey::private_key_from_der(der) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - Ok(super::EcImportResult { - key_data: pkey - .private_key_to_der() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?, - is_private: true, - }) - } - - fn import_ec_private_key_sec1( - &self, - data: &[u8], - curve: EllipticCurve, - ) -> Result { - let nid = curve_to_nid(curve); - let group = EcGroup::from_curve_name(nid) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let bn = BigNum::from_slice(data) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let generator = group - .generator_opt() - .ok_or_else(|| CryptoError::InvalidKey(Some("EC group has no generator".into())))?; - let ec_key = EcKey::from_private_components(&group, &bn, generator) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let pkey = PKey::from_ec_key(ec_key) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - Ok(super::EcImportResult { - key_data: pkey - .private_key_to_der() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?, - is_private: true, - }) - } - - fn export_ec_public_key_sec1( - &self, - key_data: &[u8], - _curve: EllipticCurve, - is_private: bool, - ) -> Result, CryptoError> { - let mut ctx = openssl::bn::BigNumContext::new() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - if is_private { - let ec_key = PKey::private_key_from_der(key_data) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))? - .ec_key() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - ec_key - .public_key() - .to_bytes( - ec_key.group(), - openssl::ec::PointConversionForm::UNCOMPRESSED, - &mut ctx, - ) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) - } else { - let ec_key = PKey::public_key_from_der(key_data) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))? - .ec_key() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - ec_key - .public_key() - .to_bytes( - ec_key.group(), - openssl::ec::PointConversionForm::UNCOMPRESSED, - &mut ctx, - ) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) - } - } - - fn export_ec_public_key_spki( - &self, - key_data: &[u8], - _curve: EllipticCurve, - ) -> Result, CryptoError> { - let pkey = PKey::public_key_from_der(key_data) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - pkey.public_key_to_der() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) - } - - fn export_ec_private_key_pkcs8( - &self, - key_data: &[u8], - _curve: EllipticCurve, - ) -> Result, CryptoError> { - let pkey = PKey::private_key_from_der(key_data) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - pkey.private_key_to_der() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) - } - - fn import_okp_public_key_raw( - &self, - data: &[u8], - ) -> Result { - if data.len() != 32 { - return Err(CryptoError::InvalidKey(None)); - } - Ok(super::OkpImportResult { - key_data: data.to_vec(), - is_private: false, - }) - } - - fn import_okp_public_key_spki( - &self, - der: &[u8], - _expected_oid: &[u8], - ) -> Result { - let pkey = PKey::public_key_from_der(der) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let raw = pkey - .raw_public_key() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - Ok(super::OkpImportResult { - key_data: raw, - is_private: false, - }) - } - - fn import_okp_private_key_pkcs8( - &self, - der: &[u8], - _expected_oid: &[u8], - ) -> Result { - Ok(super::OkpImportResult { - key_data: der.to_vec(), - is_private: true, - }) - } - - fn export_okp_public_key_raw( - &self, - key_data: &[u8], - is_private: bool, - ) -> Result, CryptoError> { - if is_private { - let pkey = PKey::private_key_from_der(key_data) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - pkey.raw_public_key() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) - } else { - Ok(key_data.to_vec()) - } - } - - fn export_okp_public_key_spki( - &self, - key_data: &[u8], - _oid: &[u8], - ) -> Result, CryptoError> { - // key_data is raw public key, need to wrap in SPKI - let pkey = PKey::public_key_from_raw_bytes(key_data, Id::ED25519) - .or_else(|_| PKey::public_key_from_raw_bytes(key_data, Id::X25519)) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - pkey.public_key_to_der() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into()))) - } - - fn export_okp_private_key_pkcs8( - &self, - key_data: &[u8], - _oid: &[u8], - ) -> Result, CryptoError> { - // key_data is already PKCS8 - Ok(key_data.to_vec()) - } - - fn import_rsa_jwk( - &self, - jwk: super::RsaJwkImport<'_>, - ) -> Result { - let n = BigNum::from_slice(jwk.n) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let e = BigNum::from_slice(jwk.e) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let modulus_length = n.num_bits() as u32; - let pub_exp_bytes = jwk.e.to_vec(); - - if let ( - Some(d_bytes), - Some(p_bytes), - Some(q_bytes), - Some(dp_bytes), - Some(dq_bytes), - Some(qi_bytes), - ) = (jwk.d, jwk.p, jwk.q, jwk.dp, jwk.dq, jwk.qi) - { - let d = BigNum::from_slice(d_bytes) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let p = BigNum::from_slice(p_bytes) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let q = BigNum::from_slice(q_bytes) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let dp = BigNum::from_slice(dp_bytes) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let dq = BigNum::from_slice(dq_bytes) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let qi = BigNum::from_slice(qi_bytes) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - - let rsa = Rsa::from_private_components(n, e, d, p, q, dp, dq, qi) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let pkey = PKey::from_rsa(rsa) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - Ok(super::RsaImportResult { - key_data: pkey - .private_key_to_der() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?, - modulus_length, - public_exponent: pub_exp_bytes, - is_private: true, - }) - } else { - let rsa = Rsa::from_public_components(n, e) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let pkey = PKey::from_rsa(rsa) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - Ok(super::RsaImportResult { - key_data: pkey - .public_key_to_der() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?, - modulus_length, - public_exponent: pub_exp_bytes, - is_private: false, - }) - } - } - - fn export_rsa_jwk( - &self, - key_data: &[u8], - is_private: bool, - ) -> Result { - if is_private { - let pkey = PKey::private_key_from_der(key_data) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let rsa = pkey - .rsa() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - Ok(super::RsaJwkExport { - n: rsa.n().to_vec(), - e: rsa.e().to_vec(), - d: Some(rsa.d().to_vec()), - p: rsa.p().map(|v| v.to_vec()), - q: rsa.q().map(|v| v.to_vec()), - dp: rsa.dmp1().map(|v| v.to_vec()), - dq: rsa.dmq1().map(|v| v.to_vec()), - qi: rsa.iqmp().map(|v| v.to_vec()), - }) - } else { - let pkey = PKey::public_key_from_der(key_data) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let rsa = pkey - .rsa() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - Ok(super::RsaJwkExport { - n: rsa.n().to_vec(), - e: rsa.e().to_vec(), - d: None, - p: None, - q: None, - dp: None, - dq: None, - qi: None, - }) - } - } - - fn import_ec_jwk( - &self, - jwk: super::EcJwkImport<'_>, - curve: EllipticCurve, - ) -> Result { - let nid = curve_to_nid(curve); - let group = EcGroup::from_curve_name(nid) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let x = BigNum::from_slice(jwk.x) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let y = BigNum::from_slice(jwk.y) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let pub_key = EcKey::from_public_key_affine_coordinates(&group, &x, &y) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - - if let Some(d_bytes) = jwk.d { - let d = BigNum::from_slice(d_bytes) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let priv_key = EcKey::from_private_components(&group, &d, pub_key.public_key()) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let pkey = PKey::from_ec_key(priv_key) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - Ok(super::EcImportResult { - key_data: pkey - .private_key_to_der() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?, - is_private: true, - }) - } else { - // Return SEC1 uncompressed point for public key (consistent with generate_ec_key) - let mut ctx = openssl::bn::BigNumContext::new() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let sec1 = pub_key - .public_key() - .to_bytes( - &group, - openssl::ec::PointConversionForm::UNCOMPRESSED, - &mut ctx, - ) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - Ok(super::EcImportResult { - key_data: sec1, - is_private: false, - }) - } - } - - fn export_ec_jwk( - &self, - key_data: &[u8], - curve: EllipticCurve, - is_private: bool, - ) -> Result { - let nid = curve_to_nid(curve); - let group = EcGroup::from_curve_name(nid) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let mut ctx = openssl::bn::BigNumContext::new() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - - if is_private { - let pkey = PKey::private_key_from_der(key_data) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let ec_key = pkey - .ec_key() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let mut x = - BigNum::new().map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let mut y = - BigNum::new().map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - ec_key - .public_key() - .affine_coordinates(&group, &mut x, &mut y, &mut ctx) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - Ok(super::EcJwkExport { - x: x.to_vec(), - y: y.to_vec(), - d: Some(ec_key.private_key().to_vec()), - }) - } else { - // key_data is SEC1 uncompressed point (0x04 || x || y) - let coord_len = match curve { - EllipticCurve::P256 => 32, - EllipticCurve::P384 => 48, - EllipticCurve::P521 => 66, - }; - if key_data.len() != 1 + 2 * coord_len || key_data[0] != 0x04 { - return Err(CryptoError::InvalidKey(None)); - } - let x = key_data[1..1 + coord_len].to_vec(); - let y = key_data[1 + coord_len..].to_vec(); - Ok(super::EcJwkExport { x, y, d: None }) - } - } - - fn import_okp_jwk( - &self, - jwk: super::OkpJwkImport<'_>, - is_ed25519: bool, - ) -> Result { - let id = if is_ed25519 { Id::ED25519 } else { Id::X25519 }; - if let Some(d) = jwk.d { - // Private key - let pkey = PKey::private_key_from_raw_bytes(d, id) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - if is_ed25519 { - // Ed25519: return PKCS8 DER - Ok(super::OkpImportResult { - key_data: pkey - .private_key_to_der() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?, - is_private: true, - }) - } else { - // X25519: return raw bytes - Ok(super::OkpImportResult { - key_data: d.to_vec(), - is_private: true, - }) - } - } else { - // Public key - store raw bytes - Ok(super::OkpImportResult { - key_data: jwk.x.to_vec(), - is_private: false, - }) - } - } - - fn export_okp_jwk( - &self, - key_data: &[u8], - is_private: bool, - is_ed25519: bool, - ) -> Result { - if is_private { - if is_ed25519 { - // Ed25519: key_data is PKCS8 DER - let pkey = PKey::private_key_from_der(key_data) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let d = pkey - .raw_private_key() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let x = pkey - .raw_public_key() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - Ok(super::OkpJwkExport { x, d: Some(d) }) - } else { - // X25519: key_data is raw 32-byte secret - let pkey = PKey::private_key_from_raw_bytes(key_data, Id::X25519) - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - let x = pkey - .raw_public_key() - .map_err(|e| CryptoError::InvalidKey(Some(e.to_string().into())))?; - Ok(super::OkpJwkExport { - x, - d: Some(key_data.to_vec()), - }) - } - } else { - // Public key - key_data is raw bytes - Ok(super::OkpJwkExport { - x: key_data.to_vec(), - d: None, - }) - } - } -} diff --git a/stdlib/src/llrt/llrt_crypto/provider/ring.rs b/stdlib/src/llrt/llrt_crypto/provider/ring.rs deleted file mode 100644 index c1e58227..00000000 --- a/stdlib/src/llrt/llrt_crypto/provider/ring.rs +++ /dev/null @@ -1,544 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use crate::llrt_crypto::hash::HashAlgorithm; -use crate::llrt_crypto::provider::{ - AesMode, CryptoError, CryptoProvider, HmacProvider, SimpleDigest, -}; -use crate::llrt_crypto::subtle::EllipticCurve; -use md5::{Digest, Md5 as Md5Hasher}; -use ring::{digest, hmac}; - -pub struct RingProvider; - -pub enum RingDigestType { - Sha1(RingDigest), - Sha256(RingDigest), - Sha384(RingDigest), - Sha512(RingDigest), - Md5(RingMd5), -} - -pub enum RingHmacType { - Sha1(RingHmacSha1), - Sha256(RingHmacSha256), - Sha384(RingHmacSha384), - Sha512(RingHmacSha512), -} - -impl SimpleDigest for RingDigestType { - fn update(&mut self, data: &[u8]) { - match self { - RingDigestType::Sha1(d) => d.update(data), - RingDigestType::Sha256(d) => d.update(data), - RingDigestType::Sha384(d) => d.update(data), - RingDigestType::Sha512(d) => d.update(data), - RingDigestType::Md5(d) => d.update(data), - } - } - - fn finalize(self) -> Vec { - match self { - RingDigestType::Sha1(d) => d.finalize(), - RingDigestType::Sha256(d) => d.finalize(), - RingDigestType::Sha384(d) => d.finalize(), - RingDigestType::Sha512(d) => d.finalize(), - RingDigestType::Md5(d) => d.finalize(), - } - } -} - -impl HmacProvider for RingHmacType { - fn update(&mut self, data: &[u8]) { - match self { - RingHmacType::Sha1(h) => h.update(data), - RingHmacType::Sha256(h) => h.update(data), - RingHmacType::Sha384(h) => h.update(data), - RingHmacType::Sha512(h) => h.update(data), - } - } - - fn finalize(self) -> Vec { - match self { - RingHmacType::Sha1(h) => h.finalize(), - RingHmacType::Sha256(h) => h.finalize(), - RingHmacType::Sha384(h) => h.finalize(), - RingHmacType::Sha512(h) => h.finalize(), - } - } -} - -// Simple wrapper for Ring digest -pub struct RingDigest { - algorithm: &'static digest::Algorithm, - data: Vec, -} - -impl RingDigest { - fn new(algorithm: &'static digest::Algorithm) -> Self { - Self { - algorithm, - data: Vec::new(), - } - } -} - -impl SimpleDigest for RingDigest { - fn update(&mut self, data: &[u8]) { - self.data.extend_from_slice(data); - } - - fn finalize(self) -> Vec { - digest::digest(self.algorithm, &self.data).as_ref().to_vec() - } -} - -// MD5 wrapper -pub struct RingMd5(Md5Hasher); - -impl SimpleDigest for RingMd5 { - fn update(&mut self, data: &[u8]) { - Digest::update(&mut self.0, data); - } - - fn finalize(self) -> Vec { - self.0.finalize().to_vec() - } -} - -// HMAC implementations -pub struct RingHmacSha1(hmac::Context); -pub struct RingHmacSha256(hmac::Context); -pub struct RingHmacSha384(hmac::Context); -pub struct RingHmacSha512(hmac::Context); - -impl HmacProvider for RingHmacSha1 { - fn update(&mut self, data: &[u8]) { - self.0.update(data); - } - fn finalize(self) -> Vec { - self.0.sign().as_ref().to_vec() - } -} -impl HmacProvider for RingHmacSha256 { - fn update(&mut self, data: &[u8]) { - self.0.update(data); - } - fn finalize(self) -> Vec { - self.0.sign().as_ref().to_vec() - } -} -impl HmacProvider for RingHmacSha384 { - fn update(&mut self, data: &[u8]) { - self.0.update(data); - } - fn finalize(self) -> Vec { - self.0.sign().as_ref().to_vec() - } -} -impl HmacProvider for RingHmacSha512 { - fn update(&mut self, data: &[u8]) { - self.0.update(data); - } - fn finalize(self) -> Vec { - self.0.sign().as_ref().to_vec() - } -} - -impl CryptoProvider for RingProvider { - type Digest = RingDigestType; - type Hmac = RingHmacType; - - fn digest(&self, algorithm: HashAlgorithm) -> Self::Digest { - match algorithm { - HashAlgorithm::Md5 => RingDigestType::Md5(RingMd5(Md5Hasher::new())), - HashAlgorithm::Sha1 => { - RingDigestType::Sha1(RingDigest::new(&digest::SHA1_FOR_LEGACY_USE_ONLY)) - } - HashAlgorithm::Sha256 => RingDigestType::Sha256(RingDigest::new(&digest::SHA256)), - HashAlgorithm::Sha384 => RingDigestType::Sha384(RingDigest::new(&digest::SHA384)), - HashAlgorithm::Sha512 => RingDigestType::Sha512(RingDigest::new(&digest::SHA512)), - } - } - - fn hmac(&self, algorithm: HashAlgorithm, key: &[u8]) -> Self::Hmac { - match algorithm { - HashAlgorithm::Md5 => { - panic!("HMAC-MD5 not supported by Ring provider"); - } - HashAlgorithm::Sha1 => RingHmacType::Sha1(RingHmacSha1(hmac::Context::with_key( - &hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, key), - ))), - HashAlgorithm::Sha256 => RingHmacType::Sha256(RingHmacSha256(hmac::Context::with_key( - &hmac::Key::new(hmac::HMAC_SHA256, key), - ))), - HashAlgorithm::Sha384 => RingHmacType::Sha384(RingHmacSha384(hmac::Context::with_key( - &hmac::Key::new(hmac::HMAC_SHA384, key), - ))), - HashAlgorithm::Sha512 => RingHmacType::Sha512(RingHmacSha512(hmac::Context::with_key( - &hmac::Key::new(hmac::HMAC_SHA512, key), - ))), - } - } - - fn ecdsa_sign( - &self, - _curve: EllipticCurve, - _private_key_der: &[u8], - _digest: &[u8], - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn ecdsa_verify( - &self, - _curve: EllipticCurve, - _public_key_sec1: &[u8], - _signature: &[u8], - _digest: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn ed25519_sign(&self, _private_key_der: &[u8], _data: &[u8]) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn ed25519_verify( - &self, - _public_key_bytes: &[u8], - _signature: &[u8], - _data: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn rsa_pss_sign( - &self, - _private_key_der: &[u8], - _digest: &[u8], - _salt_length: usize, - _hash_alg: HashAlgorithm, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn rsa_pss_verify( - &self, - _public_key_der: &[u8], - _signature: &[u8], - _digest: &[u8], - _salt_length: usize, - _hash_alg: HashAlgorithm, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn rsa_pkcs1v15_sign( - &self, - _private_key_der: &[u8], - _digest: &[u8], - _hash_alg: HashAlgorithm, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn rsa_pkcs1v15_verify( - &self, - _public_key_der: &[u8], - _signature: &[u8], - _digest: &[u8], - _hash_alg: HashAlgorithm, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn rsa_oaep_encrypt( - &self, - _public_key_der: &[u8], - _data: &[u8], - _hash_alg: HashAlgorithm, - _label: Option<&[u8]>, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn rsa_oaep_decrypt( - &self, - _private_key_der: &[u8], - _data: &[u8], - _hash_alg: HashAlgorithm, - _label: Option<&[u8]>, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn ecdh_derive_bits( - &self, - _curve: EllipticCurve, - _private_key_der: &[u8], - _public_key_sec1: &[u8], - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn x25519_derive_bits( - &self, - _private_key: &[u8], - _public_key: &[u8], - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn aes_encrypt( - &self, - _mode: AesMode, - _key: &[u8], - _iv: &[u8], - _data: &[u8], - _additional_data: Option<&[u8]>, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn aes_decrypt( - &self, - _mode: AesMode, - _key: &[u8], - _iv: &[u8], - _data: &[u8], - _additional_data: Option<&[u8]>, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn aes_kw_wrap(&self, _kek: &[u8], _key: &[u8]) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn aes_kw_unwrap(&self, _kek: &[u8], _wrapped_key: &[u8]) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn hkdf_derive_key( - &self, - _key: &[u8], - _salt: &[u8], - _info: &[u8], - _length: usize, - _hash_alg: HashAlgorithm, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn pbkdf2_derive_key( - &self, - _password: &[u8], - _salt: &[u8], - _iterations: u32, - _length: usize, - _hash_alg: HashAlgorithm, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn generate_aes_key(&self, _length_bits: u16) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn generate_hmac_key( - &self, - _hash_alg: HashAlgorithm, - _length_bits: u16, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn generate_ec_key(&self, _curve: EllipticCurve) -> Result<(Vec, Vec), CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn generate_ed25519_key(&self) -> Result<(Vec, Vec), CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn generate_x25519_key(&self) -> Result<(Vec, Vec), CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn generate_rsa_key( - &self, - _modulus_length: u32, - _public_exponent: &[u8], - ) -> Result<(Vec, Vec), CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - - fn import_rsa_public_key_pkcs1( - &self, - _der: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_rsa_private_key_pkcs1( - &self, - _der: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_rsa_public_key_spki( - &self, - _der: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_rsa_private_key_pkcs8( - &self, - _der: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_rsa_public_key_pkcs1(&self, _key_data: &[u8]) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_rsa_public_key_spki(&self, _key_data: &[u8]) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_rsa_private_key_pkcs8(&self, _key_data: &[u8]) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_ec_public_key_sec1( - &self, - _data: &[u8], - _curve: EllipticCurve, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_ec_public_key_spki( - &self, - _der: &[u8], - _curve: EllipticCurve, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_ec_private_key_pkcs8( - &self, - _der: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_ec_private_key_sec1( - &self, - _data: &[u8], - _curve: EllipticCurve, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_ec_public_key_sec1( - &self, - _key_data: &[u8], - _curve: EllipticCurve, - _is_private: bool, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_ec_public_key_spki( - &self, - _key_data: &[u8], - _curve: EllipticCurve, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_ec_private_key_pkcs8( - &self, - _key_data: &[u8], - _curve: EllipticCurve, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_okp_public_key_raw( - &self, - _data: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_okp_public_key_spki( - &self, - _der: &[u8], - _expected_oid: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_okp_private_key_pkcs8( - &self, - _der: &[u8], - _expected_oid: &[u8], - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_okp_public_key_raw( - &self, - _key_data: &[u8], - _is_private: bool, - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_okp_public_key_spki( - &self, - _key_data: &[u8], - _oid: &[u8], - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_okp_private_key_pkcs8( - &self, - _key_data: &[u8], - _oid: &[u8], - ) -> Result, CryptoError> { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_rsa_jwk( - &self, - _jwk: super::RsaJwkImport<'_>, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_rsa_jwk( - &self, - _key_data: &[u8], - _is_private: bool, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_ec_jwk( - &self, - _jwk: super::EcJwkImport<'_>, - _curve: EllipticCurve, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_ec_jwk( - &self, - _key_data: &[u8], - _curve: EllipticCurve, - _is_private: bool, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn import_okp_jwk( - &self, - _jwk: super::OkpJwkImport<'_>, - _is_ed25519: bool, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } - fn export_okp_jwk( - &self, - _key_data: &[u8], - _is_private: bool, - _is_ed25519: bool, - ) -> Result { - Err(CryptoError::UnsupportedAlgorithm) - } -} diff --git a/stdlib/src/llrt/llrt_crypto/provider/rust/aes_variants.rs b/stdlib/src/llrt/llrt_crypto/provider/rust/aes_variants.rs deleted file mode 100644 index 482957b7..00000000 --- a/stdlib/src/llrt/llrt_crypto/provider/rust/aes_variants.rs +++ /dev/null @@ -1,285 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! AES cipher variant types for SubtleCrypto operations. -//! Only available when the `_rustcrypto` feature is enabled. - -use aes::cipher::BlockModeDecrypt; -use aes::cipher::BlockModeEncrypt; - -use aes::{ - cipher::{ - block_padding::{Error as PaddingError, Pkcs7}, - consts::{U12, U13, U14, U15, U16, U4, U8}, - InvalidLength, KeyIvInit, StreamCipher, StreamCipherError, - }, - Aes128, Aes192, Aes256, -}; -use aes_gcm::{ - aead::{Aead, Payload}, - AesGcm, KeyInit, Nonce, -}; -use ctr::{Ctr128BE, Ctr32BE, Ctr64BE}; - -#[allow(dead_code)] -pub enum AesCbcEncVariant { - Aes128(cbc::Encryptor), - Aes192(cbc::Encryptor), - Aes256(cbc::Encryptor), -} - -#[allow(dead_code)] -impl AesCbcEncVariant { - pub fn new(key_len: u16, key: &[u8], iv: &[u8]) -> std::result::Result { - let variant: AesCbcEncVariant = match key_len { - 128 => Self::Aes128(cbc::Encryptor::new_from_slices(key, iv)?), - 192 => Self::Aes192(cbc::Encryptor::new_from_slices(key, iv)?), - 256 => Self::Aes256(cbc::Encryptor::new_from_slices(key, iv)?), - _ => return Err(InvalidLength), - }; - - Ok(variant) - } - - pub fn encrypt(self, data: &[u8]) -> Vec { - match self { - Self::Aes128(v) => v.encrypt_padded_vec::(data), - Self::Aes192(v) => v.encrypt_padded_vec::(data), - Self::Aes256(v) => v.encrypt_padded_vec::(data), - } - } -} - -#[allow(dead_code)] -pub enum AesCbcDecVariant { - Aes128(cbc::Decryptor), - Aes192(cbc::Decryptor), - Aes256(cbc::Decryptor), -} - -#[allow(dead_code)] -impl AesCbcDecVariant { - pub fn new(key_len: u16, key: &[u8], iv: &[u8]) -> std::result::Result { - let variant: AesCbcDecVariant = match key_len { - 128 => Self::Aes128(cbc::Decryptor::new_from_slices(key, iv)?), - 192 => Self::Aes192(cbc::Decryptor::new_from_slices(key, iv)?), - 256 => Self::Aes256(cbc::Decryptor::new_from_slices(key, iv)?), - _ => return Err(InvalidLength), - }; - - Ok(variant) - } - - pub fn decrypt(self, data: &[u8]) -> std::result::Result, PaddingError> { - Ok(match self { - Self::Aes128(v) => v.decrypt_padded_vec::(data)?, - Self::Aes192(v) => v.decrypt_padded_vec::(data)?, - Self::Aes256(v) => v.decrypt_padded_vec::(data)?, - }) - } -} - -#[allow(dead_code)] -pub enum AesCtrVariant { - Aes128Ctr32(Ctr32BE), - Aes128Ctr64(Ctr64BE), - Aes128Ctr128(Ctr128BE), - Aes192Ctr32(Ctr32BE), - Aes192Ctr64(Ctr64BE), - Aes192Ctr128(Ctr128BE), - Aes256Ctr32(Ctr32BE), - Aes256Ctr64(Ctr64BE), - Aes256Ctr128(Ctr128BE), -} - -#[allow(dead_code)] -impl AesCtrVariant { - pub fn new( - key_len: u16, - encryption_length: u32, - key: &[u8], - counter: &[u8], - ) -> std::result::Result { - let variant: AesCtrVariant = match (key_len, encryption_length) { - (128, 32) => Self::Aes128Ctr32(Ctr32BE::new_from_slices(key, counter)?), - (128, 64) => Self::Aes128Ctr64(Ctr64BE::new_from_slices(key, counter)?), - (128, 128) => Self::Aes128Ctr128(Ctr128BE::new_from_slices(key, counter)?), - (192, 32) => Self::Aes192Ctr32(Ctr32BE::new_from_slices(key, counter)?), - (192, 64) => Self::Aes192Ctr64(Ctr64BE::new_from_slices(key, counter)?), - (192, 128) => Self::Aes192Ctr128(Ctr128BE::new_from_slices(key, counter)?), - (256, 32) => Self::Aes256Ctr32(Ctr32BE::new_from_slices(key, counter)?), - (256, 64) => Self::Aes256Ctr64(Ctr64BE::new_from_slices(key, counter)?), - (256, 128) => Self::Aes256Ctr128(Ctr128BE::new_from_slices(key, counter)?), - _ => return Err(InvalidLength), - }; - - Ok(variant) - } - - pub fn encrypt(&mut self, data: &[u8]) -> std::result::Result, StreamCipherError> { - let mut ciphertext = data.to_vec(); - match self { - Self::Aes128Ctr32(v) => v.try_apply_keystream(&mut ciphertext)?, - Self::Aes128Ctr64(v) => v.try_apply_keystream(&mut ciphertext)?, - Self::Aes128Ctr128(v) => v.try_apply_keystream(&mut ciphertext)?, - Self::Aes192Ctr32(v) => v.try_apply_keystream(&mut ciphertext)?, - Self::Aes192Ctr64(v) => v.try_apply_keystream(&mut ciphertext)?, - Self::Aes192Ctr128(v) => v.try_apply_keystream(&mut ciphertext)?, - Self::Aes256Ctr32(v) => v.try_apply_keystream(&mut ciphertext)?, - Self::Aes256Ctr64(v) => v.try_apply_keystream(&mut ciphertext)?, - Self::Aes256Ctr128(v) => v.try_apply_keystream(&mut ciphertext)?, - } - Ok(ciphertext) - } - - pub fn decrypt(&mut self, data: &[u8]) -> std::result::Result, StreamCipherError> { - let mut ciphertext = data.to_vec(); - match self { - Self::Aes128Ctr32(v) => v.try_apply_keystream(&mut ciphertext)?, - Self::Aes128Ctr64(v) => v.try_apply_keystream(&mut ciphertext)?, - Self::Aes128Ctr128(v) => v.try_apply_keystream(&mut ciphertext)?, - Self::Aes192Ctr32(v) => v.try_apply_keystream(&mut ciphertext)?, - Self::Aes192Ctr64(v) => v.try_apply_keystream(&mut ciphertext)?, - Self::Aes192Ctr128(v) => v.try_apply_keystream(&mut ciphertext)?, - Self::Aes256Ctr32(v) => v.try_apply_keystream(&mut ciphertext)?, - Self::Aes256Ctr64(v) => v.try_apply_keystream(&mut ciphertext)?, - Self::Aes256Ctr128(v) => v.try_apply_keystream(&mut ciphertext)?, - } - Ok(ciphertext) - } -} - -pub enum AesGcmVariant { - Aes128Gcm32(AesGcm), - Aes192Gcm32(AesGcm), - Aes256Gcm32(AesGcm), - Aes128Gcm64(AesGcm), - Aes192Gcm64(AesGcm), - Aes256Gcm64(AesGcm), - Aes128Gcm96(AesGcm), - Aes192Gcm96(AesGcm), - Aes256Gcm96(AesGcm), - Aes128Gcm104(AesGcm), - Aes192Gcm104(AesGcm), - Aes256Gcm104(AesGcm), - Aes128Gcm112(AesGcm), - Aes192Gcm112(AesGcm), - Aes256Gcm112(AesGcm), - Aes128Gcm120(AesGcm), - Aes192Gcm120(AesGcm), - Aes256Gcm120(AesGcm), - Aes128Gcm128(AesGcm), - Aes192Gcm128(AesGcm), - Aes256Gcm128(AesGcm), -} - -#[allow(dead_code)] -impl AesGcmVariant { - pub fn new( - key_len: u16, - tag_length: u8, - key: &[u8], - ) -> std::result::Result { - let variant = match (key_len, tag_length) { - (128, 32) => Self::Aes128Gcm32(AesGcm::new_from_slice(key)?), - (192, 32) => Self::Aes192Gcm32(AesGcm::new_from_slice(key)?), - (256, 32) => Self::Aes256Gcm32(AesGcm::new_from_slice(key)?), - (128, 64) => Self::Aes128Gcm64(AesGcm::new_from_slice(key)?), - (192, 64) => Self::Aes192Gcm64(AesGcm::new_from_slice(key)?), - (256, 64) => Self::Aes256Gcm64(AesGcm::new_from_slice(key)?), - (128, 96) => Self::Aes128Gcm96(AesGcm::new_from_slice(key)?), - (192, 96) => Self::Aes192Gcm96(AesGcm::new_from_slice(key)?), - (256, 96) => Self::Aes256Gcm96(AesGcm::new_from_slice(key)?), - (128, 104) => Self::Aes128Gcm104(AesGcm::new_from_slice(key)?), - (192, 104) => Self::Aes192Gcm104(AesGcm::new_from_slice(key)?), - (256, 104) => Self::Aes256Gcm104(AesGcm::new_from_slice(key)?), - (128, 112) => Self::Aes128Gcm112(AesGcm::new_from_slice(key)?), - (192, 112) => Self::Aes192Gcm112(AesGcm::new_from_slice(key)?), - (256, 112) => Self::Aes256Gcm112(AesGcm::new_from_slice(key)?), - (128, 120) => Self::Aes128Gcm120(AesGcm::new_from_slice(key)?), - (192, 120) => Self::Aes192Gcm120(AesGcm::new_from_slice(key)?), - (256, 120) => Self::Aes256Gcm120(AesGcm::new_from_slice(key)?), - (128, 128) => Self::Aes128Gcm128(AesGcm::new_from_slice(key)?), - (192, 128) => Self::Aes192Gcm128(AesGcm::new_from_slice(key)?), - (256, 128) => Self::Aes256Gcm128(AesGcm::new_from_slice(key)?), - _ => return Err(InvalidLength), - }; - - Ok(variant) - } - - pub fn encrypt( - &self, - nonce: &[u8], - msg: &[u8], - aad: Option<&[u8]>, - ) -> std::result::Result, aes_gcm::Error> { - let plaintext: Payload = Payload { - msg, - aad: aad.unwrap_or_default(), - }; - let nonce: &ctr::cipher::Array<_, _> = - &Nonce::::try_from(nonce).map_err(|_| aes_gcm::Error)?; - match self { - Self::Aes128Gcm32(v) => v.encrypt(nonce, plaintext), - Self::Aes192Gcm32(v) => v.encrypt(nonce, plaintext), - Self::Aes256Gcm32(v) => v.encrypt(nonce, plaintext), - Self::Aes128Gcm64(v) => v.encrypt(nonce, plaintext), - Self::Aes192Gcm64(v) => v.encrypt(nonce, plaintext), - Self::Aes256Gcm64(v) => v.encrypt(nonce, plaintext), - Self::Aes128Gcm96(v) => v.encrypt(nonce, plaintext), - Self::Aes192Gcm96(v) => v.encrypt(nonce, plaintext), - Self::Aes256Gcm96(v) => v.encrypt(nonce, plaintext), - Self::Aes128Gcm104(v) => v.encrypt(nonce, plaintext), - Self::Aes192Gcm104(v) => v.encrypt(nonce, plaintext), - Self::Aes256Gcm104(v) => v.encrypt(nonce, plaintext), - Self::Aes128Gcm112(v) => v.encrypt(nonce, plaintext), - Self::Aes192Gcm112(v) => v.encrypt(nonce, plaintext), - Self::Aes256Gcm112(v) => v.encrypt(nonce, plaintext), - Self::Aes128Gcm120(v) => v.encrypt(nonce, plaintext), - Self::Aes192Gcm120(v) => v.encrypt(nonce, plaintext), - Self::Aes256Gcm120(v) => v.encrypt(nonce, plaintext), - Self::Aes128Gcm128(v) => v.encrypt(nonce, plaintext), - Self::Aes192Gcm128(v) => v.encrypt(nonce, plaintext), - Self::Aes256Gcm128(v) => v.encrypt(nonce, plaintext), - } - } - - pub fn decrypt( - &self, - nonce: &[u8], - msg: &[u8], - aad: Option<&[u8]>, - ) -> std::result::Result, aes_gcm::Error> { - let ciphertext: Payload = Payload { - msg, - aad: aad.unwrap_or_default(), - }; - let nonce: &ctr::cipher::Array<_, _> = - &Nonce::::try_from(nonce).map_err(|_| aes_gcm::Error)?; - match self { - Self::Aes128Gcm32(v) => v.decrypt(nonce, ciphertext), - Self::Aes192Gcm32(v) => v.decrypt(nonce, ciphertext), - Self::Aes256Gcm32(v) => v.decrypt(nonce, ciphertext), - Self::Aes128Gcm64(v) => v.decrypt(nonce, ciphertext), - Self::Aes192Gcm64(v) => v.decrypt(nonce, ciphertext), - Self::Aes256Gcm64(v) => v.decrypt(nonce, ciphertext), - Self::Aes128Gcm96(v) => v.decrypt(nonce, ciphertext), - Self::Aes192Gcm96(v) => v.decrypt(nonce, ciphertext), - Self::Aes256Gcm96(v) => v.decrypt(nonce, ciphertext), - Self::Aes128Gcm104(v) => v.decrypt(nonce, ciphertext), - Self::Aes192Gcm104(v) => v.decrypt(nonce, ciphertext), - Self::Aes256Gcm104(v) => v.decrypt(nonce, ciphertext), - Self::Aes128Gcm112(v) => v.decrypt(nonce, ciphertext), - Self::Aes192Gcm112(v) => v.decrypt(nonce, ciphertext), - Self::Aes256Gcm112(v) => v.decrypt(nonce, ciphertext), - Self::Aes128Gcm120(v) => v.decrypt(nonce, ciphertext), - Self::Aes192Gcm120(v) => v.decrypt(nonce, ciphertext), - Self::Aes256Gcm120(v) => v.decrypt(nonce, ciphertext), - Self::Aes128Gcm128(v) => v.decrypt(nonce, ciphertext), - Self::Aes192Gcm128(v) => v.decrypt(nonce, ciphertext), - Self::Aes256Gcm128(v) => v.decrypt(nonce, ciphertext), - } - } -} diff --git a/stdlib/src/llrt/llrt_crypto/provider/rust/mod.rs b/stdlib/src/llrt/llrt_crypto/provider/rust/mod.rs deleted file mode 100644 index 8cfce0be..00000000 --- a/stdlib/src/llrt/llrt_crypto/provider/rust/mod.rs +++ /dev/null @@ -1,1654 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -mod aes_variants; - -use std::num::NonZeroU32; - -use aes::cipher::{ - block_padding::Pkcs7, BlockModeDecrypt, BlockModeEncrypt, KeyIvInit, StreamCipher, - StreamCipherError, -}; -use aes_gcm::{ - aead::{Aead, Payload}, - KeyInit, Nonce, -}; -use aes_kw::{KwAes128, KwAes192, KwAes256}; -use cbc::{Decryptor, Encryptor}; -use ctr::{cipher::Array, Ctr128BE, Ctr32BE, Ctr64BE}; -use der::{ - asn1::{BitStringRef, OctetString, OctetStringRef}, - Decode, Encode, -}; -use ecdsa::signature::hazmat::PrehashVerifier; -use ed25519_dalek::{Signature, Signer, VerifyingKey}; -use elliptic_curve::{consts::U12, sec1::ToSec1Point, Generate}; -use hkdf::Hkdf; -use hmac::{Hmac as HmacImpl, Mac}; -use p256::{ - ecdsa::{ - Signature as P256Signature, SigningKey as P256SigningKey, VerifyingKey as P256VerifyingKey, - }, - SecretKey as P256SecretKey, -}; -use p384::{ - ecdsa::{ - Signature as P384Signature, SigningKey as P384SigningKey, VerifyingKey as P384VerifyingKey, - }, - SecretKey as P384SecretKey, -}; -use p521::{ - ecdsa::{ - Signature as P521Signature, SigningKey as P521SigningKey, VerifyingKey as P521VerifyingKey, - }, - SecretKey as P521SecretKey, -}; -use pbkdf2::pbkdf2; -use pkcs8::{DecodePrivateKey, EncodePrivateKey}; -use rsa::pkcs1::{ - DecodeRsaPrivateKey, DecodeRsaPublicKey, EncodeRsaPrivateKey, EncodeRsaPublicKey, -}; -use rsa::signature::hazmat::PrehashSigner; -use rsa::{ - pss::Pss, - sha2::{Digest, Sha256, Sha384, Sha512}, - BoxedUint, Oaep, Pkcs1v15Sign, RsaPrivateKey, RsaPublicKey, -}; -use sha1::Sha1; - -use crate::llrt_crypto::{ - hash::HashAlgorithm, - provider::{ - parse_rsa_public_exponent, AesMode, CryptoError, CryptoProvider, HmacProvider, SimpleDigest, - }, - random_byte_array, - subtle::EllipticCurve, -}; - -use aes_variants::AesGcmVariant; - -impl From for CryptoError { - fn from(_: aes::cipher::InvalidLength) -> Self { - CryptoError::InvalidLength - } -} - -impl From for CryptoError { - fn from(_: StreamCipherError) -> Self { - CryptoError::OperationFailed(None) - } -} - -// Digest implementation using sha2/md5 crates -pub enum RustDigest { - Md5(md5::Md5), - Sha1(Sha1), - Sha256(Sha256), - Sha384(Sha384), - Sha512(Sha512), -} - -impl SimpleDigest for RustDigest { - fn update(&mut self, data: &[u8]) { - match self { - RustDigest::Md5(h) => Digest::update(h, data), - RustDigest::Sha1(h) => Digest::update(h, data), - RustDigest::Sha256(h) => Digest::update(h, data), - RustDigest::Sha384(h) => Digest::update(h, data), - RustDigest::Sha512(h) => Digest::update(h, data), - } - } - - fn finalize(self) -> Vec { - match self { - RustDigest::Md5(h) => h.finalize().to_vec(), - RustDigest::Sha1(h) => h.finalize().to_vec(), - RustDigest::Sha256(h) => h.finalize().to_vec(), - RustDigest::Sha384(h) => h.finalize().to_vec(), - RustDigest::Sha512(h) => h.finalize().to_vec(), - } - } -} - -// HMAC implementation using hmac crate -pub enum RustHmac { - Sha1(HmacImpl), - Sha256(HmacImpl), - Sha384(HmacImpl), - Sha512(HmacImpl), -} - -impl HmacProvider for RustHmac { - fn update(&mut self, data: &[u8]) { - match self { - RustHmac::Sha1(h) => Mac::update(h, data), - RustHmac::Sha256(h) => Mac::update(h, data), - RustHmac::Sha384(h) => Mac::update(h, data), - RustHmac::Sha512(h) => Mac::update(h, data), - } - } - - fn finalize(self) -> Vec { - match self { - RustHmac::Sha1(h) => h.finalize().into_bytes().to_vec(), - RustHmac::Sha256(h) => h.finalize().into_bytes().to_vec(), - RustHmac::Sha384(h) => h.finalize().into_bytes().to_vec(), - RustHmac::Sha512(h) => h.finalize().into_bytes().to_vec(), - } - } -} - -// Main Crypto Provider -#[derive(Default)] -pub struct RustCryptoProvider; - -impl CryptoProvider for RustCryptoProvider { - type Digest = RustDigest; - type Hmac = RustHmac; - - fn digest(&self, algorithm: HashAlgorithm) -> Self::Digest { - match algorithm { - HashAlgorithm::Md5 => RustDigest::Md5(md5::Md5::new()), - HashAlgorithm::Sha1 => RustDigest::Sha1(Sha1::new()), - HashAlgorithm::Sha256 => RustDigest::Sha256(Sha256::new()), - HashAlgorithm::Sha384 => RustDigest::Sha384(Sha384::new()), - HashAlgorithm::Sha512 => RustDigest::Sha512(Sha512::new()), - } - } - - fn hmac(&self, algorithm: HashAlgorithm, key: &[u8]) -> Self::Hmac { - match algorithm { - HashAlgorithm::Md5 => panic!("HMAC-MD5 not supported"), - HashAlgorithm::Sha1 => RustHmac::Sha1(HmacImpl::::new_from_slice(key).unwrap()), - HashAlgorithm::Sha256 => { - RustHmac::Sha256(HmacImpl::::new_from_slice(key).unwrap()) - } - HashAlgorithm::Sha384 => { - RustHmac::Sha384(HmacImpl::::new_from_slice(key).unwrap()) - } - HashAlgorithm::Sha512 => { - RustHmac::Sha512(HmacImpl::::new_from_slice(key).unwrap()) - } - } - } - - fn ecdsa_sign( - &self, - curve: EllipticCurve, - private_key_der: &[u8], - digest: &[u8], - ) -> Result, CryptoError> { - match curve { - EllipticCurve::P256 => { - let secret_key = P256SecretKey::from_pkcs8_der(private_key_der) - .map_err(|_| CryptoError::InvalidKey(None))?; - let signing_key = P256SigningKey::from(secret_key); - let signature: p256::ecdsa::Signature = signing_key - .sign_prehash(digest) - .map_err(|_| CryptoError::SigningFailed(None))?; - Ok(signature.to_bytes().to_vec()) - } - EllipticCurve::P384 => { - let secret_key = P384SecretKey::from_pkcs8_der(private_key_der) - .map_err(|_| CryptoError::InvalidKey(None))?; - let signing_key = P384SigningKey::from(secret_key); - let signature: p384::ecdsa::Signature = signing_key - .sign_prehash(digest) - .map_err(|_| CryptoError::SigningFailed(None))?; - Ok(signature.to_bytes().to_vec()) - } - EllipticCurve::P521 => { - let secret_key = P521SecretKey::from_pkcs8_der(private_key_der) - .map_err(|_| CryptoError::InvalidKey(None))?; - let signing_key = P521SigningKey::from(secret_key); - let signature: p521::ecdsa::Signature = signing_key - .sign_prehash(digest) - .map_err(|_| CryptoError::SigningFailed(None))?; - Ok(signature.to_bytes().to_vec()) - } - } - } - - fn ecdsa_verify( - &self, - curve: EllipticCurve, - public_key_sec1: &[u8], - signature: &[u8], - digest: &[u8], - ) -> Result { - match curve { - EllipticCurve::P256 => { - let verifying_key = P256VerifyingKey::from_sec1_bytes(public_key_sec1) - .map_err(|_| CryptoError::InvalidKey(None))?; - let sig = P256Signature::from_slice(signature) - .map_err(|_| CryptoError::InvalidSignature(None))?; - Ok(verifying_key.verify_prehash(digest, &sig).is_ok()) - } - EllipticCurve::P384 => { - let verifying_key = P384VerifyingKey::from_sec1_bytes(public_key_sec1) - .map_err(|_| CryptoError::InvalidKey(None))?; - let sig = P384Signature::from_slice(signature) - .map_err(|_| CryptoError::InvalidSignature(None))?; - Ok(verifying_key.verify_prehash(digest, &sig).is_ok()) - } - EllipticCurve::P521 => { - let verifying_key = P521VerifyingKey::from_sec1_bytes(public_key_sec1) - .map_err(|_| CryptoError::InvalidKey(None))?; - let sig = P521Signature::from_slice(signature) - .map_err(|_| CryptoError::InvalidSignature(None))?; - Ok(verifying_key.verify_prehash(digest, &sig).is_ok()) - } - } - } - - fn ed25519_sign(&self, private_key_der: &[u8], data: &[u8]) -> Result, CryptoError> { - let signing_key = ed25519_dalek::SigningKey::from_pkcs8_der(private_key_der) - .map_err(|_| CryptoError::InvalidKey(None))?; - let signature = signing_key - .try_sign(data) - .map_err(|_| CryptoError::InvalidSignature(None))?; - Ok(signature.to_bytes().to_vec()) - } - - fn ed25519_verify( - &self, - public_key_bytes: &[u8], - signature: &[u8], - data: &[u8], - ) -> Result { - let public_key = VerifyingKey::from_bytes( - public_key_bytes - .try_into() - .map_err(|_| CryptoError::InvalidKey(None))?, - ) - .map_err(|_| CryptoError::InvalidKey(None))?; - let signature = Signature::from_bytes( - signature - .try_into() - .map_err(|_| CryptoError::InvalidSignature(None))?, - ); - Ok(public_key.verify_strict(data, &signature).is_ok()) - } - - fn rsa_pss_sign( - &self, - private_key_der: &[u8], - digest: &[u8], - salt_length: usize, - hash_alg: HashAlgorithm, - ) -> Result, CryptoError> { - let mut rng = rand::rng(); - let private_key = RsaPrivateKey::from_pkcs1_der(private_key_der) - .map_err(|_| CryptoError::InvalidKey(None))?; - - match hash_alg { - HashAlgorithm::Sha1 => private_key - .sign_with_rng(&mut rng, Pss::::new_with_salt(salt_length), digest) - .map_err(|_| CryptoError::SigningFailed(None)), - HashAlgorithm::Sha256 => private_key - .sign_with_rng(&mut rng, Pss::::new_with_salt(salt_length), digest) - .map_err(|_| CryptoError::SigningFailed(None)), - HashAlgorithm::Sha384 => private_key - .sign_with_rng(&mut rng, Pss::::new_with_salt(salt_length), digest) - .map_err(|_| CryptoError::SigningFailed(None)), - HashAlgorithm::Sha512 => private_key - .sign_with_rng(&mut rng, Pss::::new_with_salt(salt_length), digest) - .map_err(|_| CryptoError::SigningFailed(None)), - _ => Err(CryptoError::UnsupportedAlgorithm), - } - } - - fn rsa_pss_verify( - &self, - public_key_der: &[u8], - signature: &[u8], - digest: &[u8], - salt_length: usize, - hash_alg: HashAlgorithm, - ) -> Result { - let public_key = RsaPublicKey::from_pkcs1_der(public_key_der) - .map_err(|_| CryptoError::InvalidKey(None))?; - - match hash_alg { - HashAlgorithm::Sha1 => Ok(public_key - .verify(Pss::::new_with_salt(salt_length), digest, signature) - .is_ok()), - HashAlgorithm::Sha256 => Ok(public_key - .verify(Pss::::new_with_salt(salt_length), digest, signature) - .is_ok()), - HashAlgorithm::Sha384 => Ok(public_key - .verify(Pss::::new_with_salt(salt_length), digest, signature) - .is_ok()), - HashAlgorithm::Sha512 => Ok(public_key - .verify(Pss::::new_with_salt(salt_length), digest, signature) - .is_ok()), - _ => Err(CryptoError::UnsupportedAlgorithm), - } - } - - fn rsa_pkcs1v15_sign( - &self, - private_key_der: &[u8], - digest: &[u8], - hash_alg: HashAlgorithm, - ) -> Result, CryptoError> { - let mut rng = rand::rng(); - let private_key = RsaPrivateKey::from_pkcs1_der(private_key_der) - .map_err(|_| CryptoError::InvalidKey(None))?; - - match hash_alg { - HashAlgorithm::Sha1 => private_key - .sign_with_rng(&mut rng, Pkcs1v15Sign::new::(), digest) - .map_err(|_| CryptoError::SigningFailed(None)), - HashAlgorithm::Sha256 => private_key - .sign_with_rng(&mut rng, Pkcs1v15Sign::new::(), digest) - .map_err(|_| CryptoError::SigningFailed(None)), - HashAlgorithm::Sha384 => private_key - .sign_with_rng(&mut rng, Pkcs1v15Sign::new::(), digest) - .map_err(|_| CryptoError::SigningFailed(None)), - HashAlgorithm::Sha512 => private_key - .sign_with_rng(&mut rng, Pkcs1v15Sign::new::(), digest) - .map_err(|_| CryptoError::SigningFailed(None)), - _ => Err(CryptoError::UnsupportedAlgorithm), - } - } - - fn rsa_pkcs1v15_verify( - &self, - public_key_der: &[u8], - signature: &[u8], - digest: &[u8], - hash_alg: HashAlgorithm, - ) -> Result { - let public_key = RsaPublicKey::from_pkcs1_der(public_key_der) - .map_err(|_| CryptoError::InvalidKey(None))?; - - match hash_alg { - HashAlgorithm::Sha1 => Ok(public_key - .verify(Pkcs1v15Sign::new::(), digest, signature) - .is_ok()), - HashAlgorithm::Sha256 => Ok(public_key - .verify(Pkcs1v15Sign::new::(), digest, signature) - .is_ok()), - HashAlgorithm::Sha384 => Ok(public_key - .verify(Pkcs1v15Sign::new::(), digest, signature) - .is_ok()), - HashAlgorithm::Sha512 => Ok(public_key - .verify(Pkcs1v15Sign::new::(), digest, signature) - .is_ok()), - _ => Err(CryptoError::UnsupportedAlgorithm), - } - } - - fn rsa_oaep_encrypt( - &self, - public_key_der: &[u8], - data: &[u8], - hash_alg: HashAlgorithm, - label: Option<&[u8]>, - ) -> Result, CryptoError> { - let mut rng = rand::rng(); - let public_key = RsaPublicKey::from_pkcs1_der(public_key_der) - .map_err(|_| CryptoError::InvalidKey(None))?; - - match hash_alg { - HashAlgorithm::Sha1 => { - let mut padding = Oaep::::new(); - if let Some(l) = label { - if !l.is_empty() { - padding.label = Some(l.into()); - } - } - public_key - .encrypt(&mut rng, padding, data) - .map_err(|_| CryptoError::EncryptionFailed(None)) - } - HashAlgorithm::Sha256 => { - let mut padding = Oaep::::new(); - if let Some(l) = label { - if !l.is_empty() { - padding.label = Some(l.into()); - } - } - public_key - .encrypt(&mut rng, padding, data) - .map_err(|_| CryptoError::EncryptionFailed(None)) - } - HashAlgorithm::Sha384 => { - let mut padding = Oaep::::new(); - if let Some(l) = label { - if !l.is_empty() { - padding.label = Some(l.into()); - } - } - public_key - .encrypt(&mut rng, padding, data) - .map_err(|_| CryptoError::EncryptionFailed(None)) - } - HashAlgorithm::Sha512 => { - let mut padding = Oaep::::new(); - if let Some(l) = label { - if !l.is_empty() { - padding.label = Some(l.into()); - } - } - public_key - .encrypt(&mut rng, padding, data) - .map_err(|_| CryptoError::EncryptionFailed(None)) - } - _ => Err(CryptoError::UnsupportedAlgorithm), - } - } - - fn rsa_oaep_decrypt( - &self, - private_key_der: &[u8], - data: &[u8], - hash_alg: HashAlgorithm, - label: Option<&[u8]>, - ) -> Result, CryptoError> { - let private_key = RsaPrivateKey::from_pkcs1_der(private_key_der) - .map_err(|_| CryptoError::InvalidKey(None))?; - - match hash_alg { - HashAlgorithm::Sha1 => { - let mut padding = Oaep::::new(); - if let Some(l) = label { - if !l.is_empty() { - padding.label = Some(l.into()); - } - } - private_key - .decrypt(padding, data) - .map_err(|_| CryptoError::DecryptionFailed(None)) - } - HashAlgorithm::Sha256 => { - let mut padding = Oaep::::new(); - if let Some(l) = label { - if !l.is_empty() { - padding.label = Some(l.into()); - } - } - private_key - .decrypt(padding, data) - .map_err(|_| CryptoError::DecryptionFailed(None)) - } - HashAlgorithm::Sha384 => { - let mut padding = Oaep::::new(); - if let Some(l) = label { - if !l.is_empty() { - padding.label = Some(l.into()); - } - } - private_key - .decrypt(padding, data) - .map_err(|_| CryptoError::DecryptionFailed(None)) - } - HashAlgorithm::Sha512 => { - let mut padding = Oaep::::new(); - if let Some(l) = label { - if !l.is_empty() { - padding.label = Some(l.into()); - } - } - private_key - .decrypt(padding, data) - .map_err(|_| CryptoError::DecryptionFailed(None)) - } - _ => Err(CryptoError::UnsupportedAlgorithm), - } - } - - fn ecdh_derive_bits( - &self, - curve: EllipticCurve, - private_key_der: &[u8], - public_key_sec1: &[u8], - ) -> Result, CryptoError> { - match curve { - EllipticCurve::P256 => { - let secret_key = P256SecretKey::from_pkcs8_der(private_key_der) - .map_err(|_| CryptoError::InvalidKey(None))?; - let public_key = p256::PublicKey::from_sec1_bytes(public_key_sec1) - .map_err(|_| CryptoError::InvalidKey(None))?; - let shared_secret = p256::elliptic_curve::ecdh::diffie_hellman( - secret_key.to_nonzero_scalar(), - public_key.as_affine(), - ); - Ok(shared_secret.raw_secret_bytes().to_vec()) - } - EllipticCurve::P384 => { - let secret_key = P384SecretKey::from_pkcs8_der(private_key_der) - .map_err(|_| CryptoError::InvalidKey(None))?; - let public_key = p384::PublicKey::from_sec1_bytes(public_key_sec1) - .map_err(|_| CryptoError::InvalidKey(None))?; - let shared_secret = p384::elliptic_curve::ecdh::diffie_hellman( - secret_key.to_nonzero_scalar(), - public_key.as_affine(), - ); - Ok(shared_secret.raw_secret_bytes().to_vec()) - } - EllipticCurve::P521 => { - let secret_key = P521SecretKey::from_pkcs8_der(private_key_der) - .map_err(|_| CryptoError::InvalidKey(None))?; - let public_key = p521::PublicKey::from_sec1_bytes(public_key_sec1) - .map_err(|_| CryptoError::InvalidKey(None))?; - let shared_secret = p521::elliptic_curve::ecdh::diffie_hellman( - secret_key.to_nonzero_scalar(), - public_key.as_affine(), - ); - Ok(shared_secret.raw_secret_bytes().to_vec()) - } - } - } - - fn x25519_derive_bits( - &self, - private_key: &[u8], - public_key: &[u8], - ) -> Result, CryptoError> { - let private_array: [u8; 32] = private_key - .try_into() - .map_err(|_| CryptoError::InvalidKey(None))?; - let public_array: [u8; 32] = public_key - .try_into() - .map_err(|_| CryptoError::InvalidKey(None))?; - - let secret_key = x25519_dalek::StaticSecret::from(private_array); - let public_key = x25519_dalek::PublicKey::from(public_array); - let shared_secret = secret_key.diffie_hellman(&public_key); - - if shared_secret.as_bytes().iter().all(|b| *b == 0) { - return Err(CryptoError::OperationFailed(None)); - } - - Ok(shared_secret.as_bytes().to_vec()) - } - - fn aes_encrypt( - &self, - mode: AesMode, - key: &[u8], - iv: &[u8], - data: &[u8], - additional_data: Option<&[u8]>, - ) -> Result, CryptoError> { - match mode { - AesMode::Cbc => match key.len() { - 16 => { - let encryptor = Encryptor::::new_from_slices(key, iv)?; - Ok(encryptor.encrypt_padded_vec::(data)) - } - 24 => { - let encryptor = Encryptor::::new_from_slices(key, iv)?; - Ok(encryptor.encrypt_padded_vec::(data)) - } - 32 => { - let encryptor = Encryptor::::new_from_slices(key, iv)?; - Ok(encryptor.encrypt_padded_vec::(data)) - } - _ => Err(CryptoError::InvalidKey(None)), - }, - AesMode::Ctr { counter_length } => { - let mut ciphertext = data.to_vec(); - match (key.len(), counter_length) { - (16, 32) => { - let mut cipher = Ctr32BE::::new_from_slices(key, iv)?; - cipher.try_apply_keystream(&mut ciphertext)?; - } - (16, 64) => { - let mut cipher = Ctr64BE::::new_from_slices(key, iv)?; - cipher.try_apply_keystream(&mut ciphertext)?; - } - (16, 128) => { - let mut cipher = Ctr128BE::::new_from_slices(key, iv)?; - cipher.try_apply_keystream(&mut ciphertext)?; - } - (24, 32) => { - let mut cipher = Ctr32BE::::new_from_slices(key, iv)?; - cipher.try_apply_keystream(&mut ciphertext)?; - } - (24, 64) => { - let mut cipher = Ctr64BE::::new_from_slices(key, iv)?; - cipher.try_apply_keystream(&mut ciphertext)?; - } - (24, 128) => { - let mut cipher = Ctr128BE::::new_from_slices(key, iv)?; - cipher.try_apply_keystream(&mut ciphertext)?; - } - (32, 32) => { - let mut cipher = Ctr32BE::::new_from_slices(key, iv)?; - cipher.try_apply_keystream(&mut ciphertext)?; - } - (32, 64) => { - let mut cipher = Ctr64BE::::new_from_slices(key, iv)?; - cipher.try_apply_keystream(&mut ciphertext)?; - } - (32, 128) => { - let mut cipher = Ctr128BE::::new_from_slices(key, iv)?; - cipher.try_apply_keystream(&mut ciphertext)?; - } - _ => return Err(CryptoError::InvalidKey(None)), - } - Ok(ciphertext) - } - AesMode::Gcm { tag_length } => { - let variant = AesGcmVariant::new((key.len() * 8) as u16, tag_length, key)?; - let nonce: &Array<_, _> = - &Nonce::::try_from(iv).map_err(|_| CryptoError::InvalidData(None))?; - - let plaintext = Payload { - msg: data, - aad: additional_data.unwrap_or_default(), - }; - - match variant { - AesGcmVariant::Aes128Gcm32(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes192Gcm32(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes256Gcm32(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes128Gcm64(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes192Gcm64(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes256Gcm64(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes128Gcm96(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes192Gcm96(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes256Gcm96(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes128Gcm104(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes192Gcm104(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes256Gcm104(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes128Gcm112(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes192Gcm112(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes256Gcm112(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes128Gcm120(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes192Gcm120(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes256Gcm120(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes128Gcm128(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes192Gcm128(v) => v.encrypt(nonce, plaintext), - AesGcmVariant::Aes256Gcm128(v) => v.encrypt(nonce, plaintext), - } - .map_err(|_| CryptoError::EncryptionFailed(None)) - } - } - } - - fn aes_decrypt( - &self, - mode: AesMode, - key: &[u8], - iv: &[u8], - data: &[u8], - additional_data: Option<&[u8]>, - ) -> Result, CryptoError> { - match mode { - AesMode::Cbc => match key.len() { - 16 => { - let decryptor = Decryptor::::new_from_slices(key, iv)?; - decryptor - .decrypt_padded_vec::(data) - .map_err(|_| CryptoError::DecryptionFailed(None)) - } - 24 => { - let decryptor = Decryptor::::new_from_slices(key, iv)?; - decryptor - .decrypt_padded_vec::(data) - .map_err(|_| CryptoError::DecryptionFailed(None)) - } - 32 => { - let decryptor = Decryptor::::new_from_slices(key, iv)?; - decryptor - .decrypt_padded_vec::(data) - .map_err(|_| CryptoError::DecryptionFailed(None)) - } - _ => Err(CryptoError::InvalidKey(None)), - }, - AesMode::Ctr { .. } => { - // CTR decryption is the same as encryption - self.aes_encrypt(mode, key, iv, data, additional_data) - } - AesMode::Gcm { tag_length } => { - let variant = AesGcmVariant::new((key.len() * 8) as u16, tag_length, key)?; - let nonce: &Array<_, _> = - &Nonce::::try_from(iv).map_err(|_| CryptoError::InvalidData(None))?; - - let ciphertext = Payload { - msg: data, - aad: additional_data.unwrap_or_default(), - }; - - match variant { - AesGcmVariant::Aes128Gcm32(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes192Gcm32(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes256Gcm32(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes128Gcm64(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes192Gcm64(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes256Gcm64(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes128Gcm96(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes192Gcm96(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes256Gcm96(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes128Gcm104(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes192Gcm104(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes256Gcm104(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes128Gcm112(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes192Gcm112(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes256Gcm112(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes128Gcm120(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes192Gcm120(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes256Gcm120(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes128Gcm128(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes192Gcm128(v) => v.decrypt(nonce, ciphertext), - AesGcmVariant::Aes256Gcm128(v) => v.decrypt(nonce, ciphertext), - } - .map_err(|_| CryptoError::DecryptionFailed(None)) - } - } - } - - fn aes_kw_wrap(&self, kek: &[u8], key: &[u8]) -> Result, CryptoError> { - match kek.len() { - 16 => { - let kw = - KwAes128::new_from_slice(kek).map_err(|_| CryptoError::InvalidKey(None))?; - let mut buf = vec![0u8; key.len() + 8]; - let result = kw - .wrap_key(key, &mut buf) - .map_err(|_| CryptoError::OperationFailed(None))?; - Ok(result.to_vec()) - } - 24 => { - let kw = - KwAes192::new_from_slice(kek).map_err(|_| CryptoError::InvalidKey(None))?; - let mut buf = vec![0u8; key.len() + 8]; - let result = kw - .wrap_key(key, &mut buf) - .map_err(|_| CryptoError::OperationFailed(None))?; - Ok(result.to_vec()) - } - 32 => { - let kw = - KwAes256::new_from_slice(kek).map_err(|_| CryptoError::InvalidKey(None))?; - let mut buf = vec![0u8; key.len() + 8]; - let result = kw - .wrap_key(key, &mut buf) - .map_err(|_| CryptoError::OperationFailed(None))?; - Ok(result.to_vec()) - } - _ => Err(CryptoError::InvalidKey(None)), - } - } - - fn aes_kw_unwrap(&self, kek: &[u8], wrapped_key: &[u8]) -> Result, CryptoError> { - match kek.len() { - 16 => { - let kw = - KwAes128::new_from_slice(kek).map_err(|_| CryptoError::InvalidKey(None))?; - let mut buf = vec![0u8; wrapped_key.len()]; - let result = kw - .unwrap_key(wrapped_key, &mut buf) - .map_err(|_| CryptoError::OperationFailed(None))?; - Ok(result.to_vec()) - } - 24 => { - let kw = - KwAes192::new_from_slice(kek).map_err(|_| CryptoError::InvalidKey(None))?; - let mut buf = vec![0u8; wrapped_key.len()]; - let result = kw - .unwrap_key(wrapped_key, &mut buf) - .map_err(|_| CryptoError::OperationFailed(None))?; - Ok(result.to_vec()) - } - 32 => { - let kw = - KwAes256::new_from_slice(kek).map_err(|_| CryptoError::InvalidKey(None))?; - let mut buf = vec![0u8; wrapped_key.len()]; - let result = kw - .unwrap_key(wrapped_key, &mut buf) - .map_err(|_| CryptoError::OperationFailed(None))?; - Ok(result.to_vec()) - } - _ => Err(CryptoError::InvalidKey(None)), - } - } - - fn hkdf_derive_key( - &self, - key: &[u8], - salt: &[u8], - info: &[u8], - length: usize, - hash_alg: HashAlgorithm, - ) -> Result, CryptoError> { - let mut out = vec![0u8; length]; - - match hash_alg { - HashAlgorithm::Sha1 => { - let prk = Hkdf::::new(Some(salt), key); - prk.expand(info, &mut out) - } - HashAlgorithm::Sha256 => { - let prk = Hkdf::::new(Some(salt), key); - prk.expand(info, &mut out) - } - HashAlgorithm::Sha384 => { - let prk = Hkdf::::new(Some(salt), key); - prk.expand(info, &mut out) - } - HashAlgorithm::Sha512 => { - let prk = Hkdf::::new(Some(salt), key); - prk.expand(info, &mut out) - } - _ => return Err(CryptoError::UnsupportedAlgorithm), - } - .map_err(|_| CryptoError::DerivationFailed(None))?; - Ok(out) - } - - fn pbkdf2_derive_key( - &self, - password: &[u8], - salt: &[u8], - iterations: u32, - length: usize, - hash_alg: HashAlgorithm, - ) -> Result, CryptoError> { - let mut out = vec![0; length]; - let iterations = NonZeroU32::new(iterations).ok_or(CryptoError::InvalidData(None))?; - match hash_alg { - HashAlgorithm::Sha1 => { - pbkdf2::>(password, salt, iterations.get(), &mut out) - } - HashAlgorithm::Sha256 => { - pbkdf2::>(password, salt, iterations.get(), &mut out) - } - HashAlgorithm::Sha384 => { - pbkdf2::>(password, salt, iterations.get(), &mut out) - } - HashAlgorithm::Sha512 => { - pbkdf2::>(password, salt, iterations.get(), &mut out) - } - _ => return Err(CryptoError::UnsupportedAlgorithm), - } - .map_err(|_| CryptoError::InvalidLength)?; - Ok(out) - } - - fn generate_aes_key(&self, length_bits: u16) -> Result, CryptoError> { - let length_bytes = (length_bits / 8) as usize; - if !matches!(length_bits, 128 | 192 | 256) { - return Err(CryptoError::InvalidLength); - } - Ok(random_byte_array(length_bytes)) - } - - fn generate_hmac_key( - &self, - hash_alg: HashAlgorithm, - length_bits: u16, - ) -> Result, CryptoError> { - let length_bytes = if length_bits == 0 { - hash_alg.block_len() - } else { - (length_bits / 8) as usize - }; - - if length_bytes > 128 { - return Err(CryptoError::InvalidLength); - } - - Ok(random_byte_array(length_bytes)) - } - - fn generate_ec_key(&self, curve: EllipticCurve) -> Result<(Vec, Vec), CryptoError> { - let mut rng = rand::rng(); - - match curve { - EllipticCurve::P256 => { - let key = P256SecretKey::try_generate_from_rng(&mut rng) - .map_err(|_| CryptoError::OperationFailed(None))?; - let pkcs8 = key - .to_pkcs8_der() - .map_err(|_| CryptoError::OperationFailed(None))?; - let private_key = pkcs8.as_bytes().to_vec(); - let public_key = key.public_key().to_sec1_bytes().to_vec(); - Ok((private_key, public_key)) - } - EllipticCurve::P384 => { - let key = P384SecretKey::try_generate_from_rng(&mut rng) - .map_err(|_| CryptoError::OperationFailed(None))?; - let pkcs8 = key - .to_pkcs8_der() - .map_err(|_| CryptoError::OperationFailed(None))?; - let private_key = pkcs8.as_bytes().to_vec(); - let public_key = key.public_key().to_sec1_bytes().to_vec(); - Ok((private_key, public_key)) - } - EllipticCurve::P521 => { - let key = P521SecretKey::try_generate_from_rng(&mut rng) - .map_err(|_| CryptoError::OperationFailed(None))?; - let pkcs8 = key - .to_pkcs8_der() - .map_err(|_| CryptoError::OperationFailed(None))?; - let private_key = pkcs8.as_bytes().to_vec(); - let public_key = key.public_key().to_sec1_bytes().to_vec(); - Ok((private_key, public_key)) - } - } - } - - fn generate_ed25519_key(&self) -> Result<(Vec, Vec), CryptoError> { - let mut rng = rand::rng(); - let private_key = ed25519_dalek::SigningKey::generate(&mut rng) - .to_pkcs8_der() - .map_err(|_| CryptoError::OperationFailed(None))? - .as_bytes() - .to_vec(); - let signing_key = ed25519_dalek::SigningKey::from_pkcs8_der(&private_key) - .map_err(|_| CryptoError::OperationFailed(None))?; - let public_key = signing_key.verifying_key().to_bytes().to_vec(); - Ok((private_key, public_key)) - } - - fn generate_x25519_key(&self) -> Result<(Vec, Vec), CryptoError> { - let mut rng = rand::rng(); - let secret_key = x25519_dalek::StaticSecret::random_from_rng(&mut rng); - let private_key = secret_key.as_bytes().to_vec(); - let public_key = x25519_dalek::PublicKey::from(&secret_key) - .as_bytes() - .to_vec(); - Ok((private_key, public_key)) - } - - fn generate_rsa_key( - &self, - modulus_length: u32, - public_exponent: &[u8], - ) -> Result<(Vec, Vec), CryptoError> { - let exponent = parse_rsa_public_exponent(public_exponent)?; - - let exp = BoxedUint::from(exponent); - let mut rng = rand::rng(); - let rsa_private_key = RsaPrivateKey::new_with_exp(&mut rng, modulus_length as usize, exp) - .map_err(|_| CryptoError::OperationFailed(None))?; - - let public_key = rsa_private_key - .to_public_key() - .to_pkcs1_der() - .map_err(|_| CryptoError::OperationFailed(None))?; - let private_key = rsa_private_key - .to_pkcs1_der() - .map_err(|_| CryptoError::OperationFailed(None))?; - - Ok(( - private_key.as_bytes().to_vec(), - public_key.as_bytes().to_vec(), - )) - } - - fn import_rsa_public_key_pkcs1( - &self, - der: &[u8], - ) -> Result { - use der::Decode; - let public_key = - rsa::pkcs1::RsaPublicKey::from_der(der).map_err(|_| CryptoError::InvalidKey(None))?; - let modulus_length = public_key.modulus.as_bytes().len() * 8; - let public_exponent = public_key.public_exponent.as_bytes().to_vec(); - let key_data = public_key - .to_der() - .map_err(|_| CryptoError::InvalidKey(None))?; - Ok(super::RsaImportResult { - key_data, - modulus_length: modulus_length as u32, - public_exponent, - is_private: false, - }) - } - - fn import_rsa_private_key_pkcs1( - &self, - der: &[u8], - ) -> Result { - use der::Decode; - let private_key = - rsa::pkcs1::RsaPrivateKey::from_der(der).map_err(|_| CryptoError::InvalidKey(None))?; - let modulus_length = private_key.modulus.as_bytes().len() * 8; - let public_exponent = private_key.public_exponent.as_bytes().to_vec(); - let key_data = private_key - .to_der() - .map_err(|_| CryptoError::InvalidKey(None))?; - Ok(super::RsaImportResult { - key_data, - modulus_length: modulus_length as u32, - public_exponent, - is_private: true, - }) - } - - fn import_rsa_public_key_spki( - &self, - der: &[u8], - ) -> Result { - use der::Decode; - let spki = spki::SubjectPublicKeyInfoRef::try_from(der) - .map_err(|_| CryptoError::InvalidKey(None))?; - let public_key = rsa::pkcs1::RsaPublicKey::from_der(spki.subject_public_key.raw_bytes()) - .map_err(|_| CryptoError::InvalidKey(None))?; - let modulus_length = public_key.modulus.as_bytes().len() * 8; - let public_exponent = public_key.public_exponent.as_bytes().to_vec(); - let key_data = public_key - .to_der() - .map_err(|_| CryptoError::InvalidKey(None))?; - Ok(super::RsaImportResult { - key_data, - modulus_length: modulus_length as u32, - public_exponent, - is_private: false, - }) - } - - fn import_rsa_private_key_pkcs8( - &self, - der: &[u8], - ) -> Result { - use der::Decode; - let pk_info = - pkcs8::PrivateKeyInfoRef::from_der(der).map_err(|_| CryptoError::InvalidKey(None))?; - let private_key = rsa::pkcs1::RsaPrivateKey::from_der(pk_info.private_key.as_bytes()) - .map_err(|_| CryptoError::InvalidKey(None))?; - let modulus_length = private_key.modulus.as_bytes().len() * 8; - let public_exponent = private_key.public_exponent.as_bytes().to_vec(); - let key_data = pk_info.private_key.as_bytes().to_vec(); - Ok(super::RsaImportResult { - key_data, - modulus_length: modulus_length as u32, - public_exponent, - is_private: true, - }) - } - - fn export_rsa_public_key_pkcs1(&self, key_data: &[u8]) -> Result, CryptoError> { - // key_data is already PKCS1 DER - Ok(key_data.to_vec()) - } - - fn export_rsa_public_key_spki(&self, key_data: &[u8]) -> Result, CryptoError> { - use der::{Decode, Encode}; - let public_key = rsa::pkcs1::RsaPublicKey::from_der(key_data) - .map_err(|_| CryptoError::InvalidKey(None))?; - let spki = spki::SubjectPublicKeyInfo { - algorithm: spki::AlgorithmIdentifier:: { - oid: const_oid::db::rfc5912::RSA_ENCRYPTION, - parameters: Some(der::asn1::Null.into()), - }, - subject_public_key: spki::der::asn1::BitString::from_bytes( - &public_key - .to_der() - .map_err(|_| CryptoError::InvalidKey(None))?, - ) - .map_err(|_| CryptoError::InvalidKey(None))?, - }; - spki.to_der().map_err(|_| CryptoError::InvalidKey(None)) - } - - fn export_rsa_private_key_pkcs8(&self, key_data: &[u8]) -> Result, CryptoError> { - let private_key = - RsaPrivateKey::from_pkcs1_der(key_data).map_err(|_| CryptoError::InvalidKey(None))?; - private_key - .to_pkcs8_der() - .map(|doc| doc.as_bytes().to_vec()) - .map_err(|_| CryptoError::InvalidKey(None)) - } - - fn import_ec_public_key_sec1( - &self, - data: &[u8], - curve: EllipticCurve, - ) -> Result { - let key_data = match curve { - EllipticCurve::P256 => { - let public_key = p256::PublicKey::from_sec1_bytes(data) - .map_err(|_| CryptoError::InvalidKey(None))?; - public_key.to_sec1_point(false).as_bytes().to_vec() - } - EllipticCurve::P384 => { - let public_key = p384::PublicKey::from_sec1_bytes(data) - .map_err(|_| CryptoError::InvalidKey(None))?; - public_key.to_sec1_point(false).as_bytes().to_vec() - } - EllipticCurve::P521 => { - let public_key = p521::PublicKey::from_sec1_bytes(data) - .map_err(|_| CryptoError::InvalidKey(None))?; - public_key.to_sec1_point(false).as_bytes().to_vec() - } - }; - - Ok(super::EcImportResult { - key_data, - is_private: false, - }) - } - - fn import_ec_public_key_spki( - &self, - der: &[u8], - curve: EllipticCurve, - ) -> Result { - let spki = spki::SubjectPublicKeyInfoRef::try_from(der) - .map_err(|_| CryptoError::InvalidKey(None))?; - let point = spki.subject_public_key.raw_bytes(); - self.import_ec_public_key_sec1(point, curve) - } - - fn import_ec_private_key_pkcs8( - &self, - der: &[u8], - ) -> Result { - Ok(super::EcImportResult { - key_data: der.to_vec(), - is_private: true, - }) - } - - fn import_ec_private_key_sec1( - &self, - data: &[u8], - curve: EllipticCurve, - ) -> Result { - // Convert SEC1 private key to PKCS8 - let pkcs8_der = match curve { - EllipticCurve::P256 => { - let key = - P256SecretKey::from_slice(data).map_err(|_| CryptoError::InvalidKey(None))?; - key.to_pkcs8_der() - .map_err(|_| CryptoError::InvalidKey(None))? - .as_bytes() - .to_vec() - } - EllipticCurve::P384 => { - let key = - P384SecretKey::from_slice(data).map_err(|_| CryptoError::InvalidKey(None))?; - key.to_pkcs8_der() - .map_err(|_| CryptoError::InvalidKey(None))? - .as_bytes() - .to_vec() - } - EllipticCurve::P521 => { - let key = - P521SecretKey::from_slice(data).map_err(|_| CryptoError::InvalidKey(None))?; - key.to_pkcs8_der() - .map_err(|_| CryptoError::InvalidKey(None))? - .as_bytes() - .to_vec() - } - }; - Ok(super::EcImportResult { - key_data: pkcs8_der, - is_private: true, - }) - } - - fn export_ec_public_key_sec1( - &self, - key_data: &[u8], - curve: EllipticCurve, - is_private: bool, - ) -> Result, CryptoError> { - if is_private { - // Extract public key from PKCS8 private key - match curve { - EllipticCurve::P256 => { - let key = P256SecretKey::from_pkcs8_der(key_data) - .map_err(|_| CryptoError::InvalidKey(None))?; - Ok(key.public_key().to_sec1_point(false).as_bytes().to_vec()) - } - EllipticCurve::P384 => { - let key = P384SecretKey::from_pkcs8_der(key_data) - .map_err(|_| CryptoError::InvalidKey(None))?; - Ok(key.public_key().to_sec1_point(false).as_bytes().to_vec()) - } - EllipticCurve::P521 => { - let key = P521SecretKey::from_pkcs8_der(key_data) - .map_err(|_| CryptoError::InvalidKey(None))?; - Ok(key.public_key().to_sec1_point(false).as_bytes().to_vec()) - } - } - } else { - // key_data is already SEC1 encoded - Ok(key_data.to_vec()) - } - } - - fn export_ec_public_key_spki( - &self, - key_data: &[u8], - curve: EllipticCurve, - ) -> Result, CryptoError> { - use der::Encode; - use elliptic_curve::pkcs8::AssociatedOid; - let curve_oid = match curve { - EllipticCurve::P256 => p256::NistP256::OID, - EllipticCurve::P384 => p384::NistP384::OID, - EllipticCurve::P521 => p521::NistP521::OID, - }; - let spki = spki::SubjectPublicKeyInfo { - algorithm: spki::AlgorithmIdentifier:: { - oid: elliptic_curve::ALGORITHM_OID, - parameters: Some(curve_oid), - }, - subject_public_key: spki::der::asn1::BitString::from_bytes(key_data) - .map_err(|_| CryptoError::InvalidKey(None))?, - }; - spki.to_der().map_err(|_| CryptoError::InvalidKey(None)) - } - - fn export_ec_private_key_pkcs8( - &self, - key_data: &[u8], - _curve: EllipticCurve, - ) -> Result, CryptoError> { - // key_data is already PKCS8 - Ok(key_data.to_vec()) - } - - fn import_okp_public_key_raw( - &self, - data: &[u8], - ) -> Result { - if data.len() != 32 { - return Err(CryptoError::InvalidLength); - } - Ok(super::OkpImportResult { - key_data: data.to_vec(), - is_private: false, - }) - } - - fn import_okp_public_key_spki( - &self, - der: &[u8], - _expected_oid: &[u8], - ) -> Result { - let spki = spki::SubjectPublicKeyInfoRef::try_from(der) - .map_err(|_| CryptoError::InvalidKey(None))?; - Ok(super::OkpImportResult { - key_data: spki.subject_public_key.raw_bytes().to_vec(), - is_private: false, - }) - } - - fn import_okp_private_key_pkcs8( - &self, - der: &[u8], - _expected_oid: &[u8], - ) -> Result { - Ok(super::OkpImportResult { - key_data: der.to_vec(), - is_private: true, - }) - } - - fn export_okp_public_key_raw( - &self, - key_data: &[u8], - is_private: bool, - ) -> Result, CryptoError> { - if is_private { - // Extract public key from PKCS8 - for X25519/Ed25519 - use der::Decode; - let pk_info = pkcs8::PrivateKeyInfoRef::from_der(key_data) - .map_err(|_| CryptoError::InvalidKey(None))?; - // The private key is wrapped in an OCTET STRING, skip the tag+length (2 bytes) - let private_key_bytes = pk_info.private_key.as_bytes(); - let seed = if private_key_bytes.len() > 2 && private_key_bytes[0] == 0x04 { - &private_key_bytes[2..] - } else { - private_key_bytes - }; - let bytes: [u8; 32] = seed.try_into().map_err(|_| CryptoError::InvalidKey(None))?; - let secret = x25519_dalek::StaticSecret::from(bytes); - let public = x25519_dalek::PublicKey::from(&secret); - Ok(public.as_bytes().to_vec()) - } else { - Ok(key_data.to_vec()) - } - } - - fn export_okp_public_key_spki( - &self, - key_data: &[u8], - oid: &[u8], - ) -> Result, CryptoError> { - use der::Encode; - let oid = const_oid::ObjectIdentifier::from_bytes(oid) - .map_err(|_| CryptoError::InvalidKey(None))?; - let spki = spki::SubjectPublicKeyInfo { - algorithm: spki::AlgorithmIdentifierOwned { - oid, - parameters: None, - }, - subject_public_key: spki::der::asn1::BitString::from_bytes(key_data) - .map_err(|_| CryptoError::InvalidKey(None))?, - }; - spki.to_der().map_err(|_| CryptoError::InvalidKey(None)) - } - - fn export_okp_private_key_pkcs8( - &self, - key_data: &[u8], - oid: &[u8], - ) -> Result, CryptoError> { - // Ed25519: key_data is already PKCS#8. - if oid == const_oid::db::rfc8410::ID_ED_25519.as_bytes() { - return Ok(key_data.to_vec()); - } - // X25519: key_data is the raw 32-byte private scalar. - if oid == const_oid::db::rfc8410::ID_X_25519.as_bytes() { - if key_data.len() != 32 { - return Err(CryptoError::InvalidKey(None)); - } - - // RFC 8410 requires the privateKey field to contain - // an encoded OCTET STRING containing the 32-byte scalar. - let inner = OctetStringRef::new(key_data).map_err(|_| CryptoError::InvalidKey(None))?; - let inner_der = inner.to_der().map_err(|_| CryptoError::InvalidKey(None))?; - let pk_info = pkcs8::PrivateKeyInfoRef { - algorithm: spki::AlgorithmIdentifier { - oid: const_oid::db::rfc8410::ID_X_25519, - parameters: None, - }, - private_key: OctetStringRef::new(&inner_der) - .map_err(|_| CryptoError::InvalidKey(None))?, - public_key: None, - }; - return pk_info.to_der().map_err(|_| CryptoError::InvalidKey(None)); - } - Err(CryptoError::InvalidKey(None)) - } - - fn import_rsa_jwk( - &self, - jwk: super::RsaJwkImport<'_>, - ) -> Result { - use der::{asn1::UintRef, Encode}; - let modulus = UintRef::new(jwk.n).map_err(|_| CryptoError::InvalidKey(None))?; - let public_exponent = UintRef::new(jwk.e).map_err(|_| CryptoError::InvalidKey(None))?; - let modulus_length = (modulus.as_bytes().len() * 8) as u32; - let pub_exp_bytes = public_exponent.as_bytes().to_vec(); - - if let (Some(d), Some(p), Some(q), Some(dp), Some(dq), Some(qi)) = - (jwk.d, jwk.p, jwk.q, jwk.dp, jwk.dq, jwk.qi) - { - let private_key = rsa::pkcs1::RsaPrivateKey { - modulus, - public_exponent, - private_exponent: UintRef::new(d).map_err(|_| CryptoError::InvalidKey(None))?, - prime1: UintRef::new(p).map_err(|_| CryptoError::InvalidKey(None))?, - prime2: UintRef::new(q).map_err(|_| CryptoError::InvalidKey(None))?, - exponent1: UintRef::new(dp).map_err(|_| CryptoError::InvalidKey(None))?, - exponent2: UintRef::new(dq).map_err(|_| CryptoError::InvalidKey(None))?, - coefficient: UintRef::new(qi).map_err(|_| CryptoError::InvalidKey(None))?, - other_prime_infos: None, - }; - Ok(super::RsaImportResult { - key_data: private_key - .to_der() - .map_err(|_| CryptoError::InvalidKey(None))?, - modulus_length, - public_exponent: pub_exp_bytes, - is_private: true, - }) - } else { - let public_key = rsa::pkcs1::RsaPublicKey { - modulus, - public_exponent, - }; - Ok(super::RsaImportResult { - key_data: public_key - .to_der() - .map_err(|_| CryptoError::InvalidKey(None))?, - modulus_length, - public_exponent: pub_exp_bytes, - is_private: false, - }) - } - } - - fn export_rsa_jwk( - &self, - key_data: &[u8], - is_private: bool, - ) -> Result { - use der::Decode; - if is_private { - let key = rsa::pkcs1::RsaPrivateKey::from_der(key_data) - .map_err(|_| CryptoError::InvalidKey(None))?; - Ok(super::RsaJwkExport { - n: key.modulus.as_bytes().to_vec(), - e: key.public_exponent.as_bytes().to_vec(), - d: Some(key.private_exponent.as_bytes().to_vec()), - p: Some(key.prime1.as_bytes().to_vec()), - q: Some(key.prime2.as_bytes().to_vec()), - dp: Some(key.exponent1.as_bytes().to_vec()), - dq: Some(key.exponent2.as_bytes().to_vec()), - qi: Some(key.coefficient.as_bytes().to_vec()), - }) - } else { - let key = rsa::pkcs1::RsaPublicKey::from_der(key_data) - .map_err(|_| CryptoError::InvalidKey(None))?; - Ok(super::RsaJwkExport { - n: key.modulus.as_bytes().to_vec(), - e: key.public_exponent.as_bytes().to_vec(), - d: None, - p: None, - q: None, - dp: None, - dq: None, - qi: None, - }) - } - } - - fn import_ec_jwk( - &self, - jwk: super::EcJwkImport<'_>, - curve: EllipticCurve, - ) -> Result { - if let Some(d) = jwk.d { - // Private key - convert to PKCS8 - let pkcs8_der = match curve { - EllipticCurve::P256 => { - let key = - P256SecretKey::from_slice(d).map_err(|_| CryptoError::InvalidKey(None))?; - key.to_pkcs8_der() - .map_err(|_| CryptoError::InvalidKey(None))? - .as_bytes() - .to_vec() - } - EllipticCurve::P384 => { - let key = - P384SecretKey::from_slice(d).map_err(|_| CryptoError::InvalidKey(None))?; - key.to_pkcs8_der() - .map_err(|_| CryptoError::InvalidKey(None))? - .as_bytes() - .to_vec() - } - EllipticCurve::P521 => { - let key = - P521SecretKey::from_slice(d).map_err(|_| CryptoError::InvalidKey(None))?; - key.to_pkcs8_der() - .map_err(|_| CryptoError::InvalidKey(None))? - .as_bytes() - .to_vec() - } - }; - Ok(super::EcImportResult { - key_data: pkcs8_der, - is_private: true, - }) - } else { - // Public key - encode as SEC1 uncompressed point - let mut point = Vec::with_capacity(1 + jwk.x.len() + jwk.y.len()); - point.push(0x04); // uncompressed - point.extend_from_slice(jwk.x); - point.extend_from_slice(jwk.y); - Ok(super::EcImportResult { - key_data: point, - is_private: false, - }) - } - } - - fn export_ec_jwk( - &self, - key_data: &[u8], - curve: EllipticCurve, - is_private: bool, - ) -> Result { - let coord_len = match curve { - EllipticCurve::P256 => 32, - EllipticCurve::P384 => 48, - EllipticCurve::P521 => 66, - }; - if is_private { - // key_data is PKCS8 - use elliptic_curve's SecretKey to parse it - let (x, y, d) = match curve { - EllipticCurve::P256 => { - let sk = P256SecretKey::from_pkcs8_der(key_data) - .map_err(|_| CryptoError::InvalidKey(None))?; - let pk = sk.public_key(); - let pt = pk.to_sec1_point(false); - ( - pt.x().unwrap().to_vec(), - pt.y().unwrap().to_vec(), - sk.to_bytes().to_vec(), - ) - } - EllipticCurve::P384 => { - let sk = P384SecretKey::from_pkcs8_der(key_data) - .map_err(|_| CryptoError::InvalidKey(None))?; - let pk = sk.public_key(); - let pt = pk.to_sec1_point(false); - ( - pt.x().unwrap().to_vec(), - pt.y().unwrap().to_vec(), - sk.to_bytes().to_vec(), - ) - } - EllipticCurve::P521 => { - let sk = P521SecretKey::from_pkcs8_der(key_data) - .map_err(|_| CryptoError::InvalidKey(None))?; - let pk = sk.public_key(); - let pt = pk.to_sec1_point(false); - ( - pt.x().unwrap().to_vec(), - pt.y().unwrap().to_vec(), - sk.to_bytes().to_vec(), - ) - } - }; - Ok(super::EcJwkExport { x, y, d: Some(d) }) - } else { - // key_data is SEC1 uncompressed point (0x04 || x || y) - if key_data.len() != 1 + 2 * coord_len || key_data[0] != 0x04 { - return Err(CryptoError::InvalidKey(None)); - } - let x = key_data[1..1 + coord_len].to_vec(); - let y = key_data[1 + coord_len..].to_vec(); - Ok(super::EcJwkExport { x, y, d: None }) - } - } - - fn import_okp_jwk( - &self, - jwk: super::OkpJwkImport<'_>, - is_ed25519: bool, - ) -> Result { - if let Some(d) = jwk.d { - // Private key - for Ed25519 we need PKCS8, for X25519 we store raw - if is_ed25519 { - // Ed25519: construct PKCS8 from raw private key - let pk_info = pkcs8::PrivateKeyInfoRef { - algorithm: spki::AlgorithmIdentifier { - oid: const_oid::db::rfc8410::ID_ED_25519, - parameters: None, - }, - private_key: OctetStringRef::new(d) - .map_err(|_| CryptoError::InvalidKey(None))?, - public_key: Some( - BitStringRef::from_bytes(jwk.x) - .map_err(|_| CryptoError::InvalidKey(None))?, - ), - }; - let der = pk_info - .to_der() - .map_err(|_| CryptoError::InvalidKey(None))?; - Ok(super::OkpImportResult { - key_data: der, - is_private: true, - }) - } else { - // X25519: store raw 32-byte secret - Ok(super::OkpImportResult { - key_data: d.to_vec(), - is_private: true, - }) - } - } else { - // Public key - store raw bytes - Ok(super::OkpImportResult { - key_data: jwk.x.to_vec(), - is_private: false, - }) - } - } - - fn export_okp_jwk( - &self, - key_data: &[u8], - is_private: bool, - is_ed25519: bool, - ) -> Result { - if is_private { - if is_ed25519 { - // Ed25519: key_data is complete PKCS#8 DER. - let pk_info = pkcs8::PrivateKeyInfoRef::from_der(key_data) - .map_err(|_| CryptoError::InvalidKey(None))?; - let d = OctetString::from_der(pk_info.private_key.as_bytes()) - .map_err(|_| CryptoError::InvalidKey(None))? - .as_bytes() - .to_vec(); - - if d.len() != 32 { - return Err(CryptoError::InvalidKey(None)); - } - - let x = pk_info - .public_key - .ok_or(CryptoError::InvalidKey(None))? - .raw_bytes() - .to_vec(); - - if x.len() != 32 { - return Err(CryptoError::InvalidKey(None)); - } - - Ok(super::OkpJwkExport { x, d: Some(d) }) - } else { - // X25519: key_data is raw 32-byte secret - let secret = x25519_dalek::StaticSecret::from( - <[u8; 32]>::try_from(key_data).map_err(|_| CryptoError::InvalidKey(None))?, - ); - let public = x25519_dalek::PublicKey::from(&secret); - Ok(super::OkpJwkExport { - x: public.as_bytes().to_vec(), - d: Some(key_data.to_vec()), - }) - } - } else { - // Public key - key_data is raw bytes - Ok(super::OkpJwkExport { - x: key_data.to_vec(), - d: None, - }) - } - } -} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/crypto_key.rs b/stdlib/src/llrt/llrt_crypto/subtle/crypto_key.rs deleted file mode 100644 index fc3d751d..00000000 --- a/stdlib/src/llrt/llrt_crypto/subtle/crypto_key.rs +++ /dev/null @@ -1,165 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::rc::Rc; - -use crate::llrt_utils::{clone::StructuredClone, str_enum}; -use rquickjs::{ - atom::PredefinedAtom, - class::{Trace, Tracer}, - Class, Ctx, Exception, IntoJs, Object, Result, Value, -}; - -use crate::llrt_crypto::provider::CryptoError; - -use super::key_algorithm::KeyAlgorithm; - -#[derive(PartialEq, Clone, Copy)] -pub enum KeyKind { - Secret, - Private, - Public, -} - -str_enum!(KeyKind,Secret => "secret", Private => "private", Public => "public"); - -#[rquickjs::class] -#[derive(rquickjs::JsLifetime)] -pub struct CryptoKey<'js> { - pub kind: KeyKind, - pub extractable: bool, - pub algorithm: KeyAlgorithm, - pub name: Box, - pub usages: Vec, - pub handle: Rc<[u8]>, - algorithm_cache: Option>, - usages_cache: Option>, -} - -impl<'js> CryptoKey<'js> { - pub fn new( - kind: KeyKind, - name: N, - extractable: bool, - algorithm: KeyAlgorithm, - usages: Vec, - handle: H, - ) -> Self - where - N: Into>, - H: Into>, - { - Self { - kind, - extractable, - algorithm, - name: name.into(), - usages, - handle: handle.into(), - algorithm_cache: None, - usages_cache: None, - } - } -} - -impl<'js> Trace<'js> for CryptoKey<'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - if let Some(cached) = &self.algorithm_cache { - cached.trace(tracer); - } - if let Some(cached) = &self.usages_cache { - cached.trace(tracer); - } - } -} - -impl<'js> StructuredClone<'js> for CryptoKey<'js> { - fn structured_clone(&self, ctx: &Ctx<'js>) -> Result> { - Ok(Class::instance( - ctx.clone(), - CryptoKey { - kind: self.kind, - extractable: self.extractable, - algorithm: self.algorithm.clone(), - name: self.name.clone(), - usages: self.usages.clone(), - handle: self.handle.clone(), - algorithm_cache: None, - usages_cache: None, - }, - )? - .into_value()) - } -} - -#[rquickjs::methods] -impl<'js> CryptoKey<'js> { - #[qjs(constructor)] - fn constructor(ctx: Ctx<'_>) -> Result { - Err(Exception::throw_type(&ctx, "Illegal constructor")) - } - - #[qjs(get, rename = "type")] - pub fn get_type(&self) -> &str { - self.kind.as_str() - } - - #[qjs(get)] - pub fn extractable(&self) -> bool { - self.extractable - } - - #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] - pub fn to_string_tag() -> &'static str { - stringify!(CryptoKey) - } - - #[qjs(get)] - pub fn algorithm(&mut self, ctx: Ctx<'js>) -> Result> { - if let Some(cached) = &self.algorithm_cache { - return Ok(cached.clone().into_value()); - } - let obj = self.algorithm.as_object(&ctx, self.name.as_ref())?; - self.algorithm_cache = Some(obj.clone()); - Ok(obj.into_value()) - } - - #[qjs(get)] - pub fn usages(&mut self, ctx: Ctx<'js>) -> Result> { - if let Some(cached) = &self.usages_cache { - return Ok(cached.clone()); - } - let arr = self.usages.clone().into_js(&ctx)?; - self.usages_cache = Some(arr.clone()); - Ok(arr) - } -} - -impl<'js> CryptoKey<'js> { - pub fn check_validity(&self, usage: &str) -> std::result::Result<(), CryptoError> { - for key in self.usages.iter() { - if key == usage { - return Ok(()); - } - } - Err(CryptoError::InvalidAccess(Some( - [ - "CryptoKey with '", - self.name.as_ref(), - "', doesn't support '", - usage, - "'", - ] - .concat() - .into(), - ))) - } - - pub fn check_kind(&self, expected: KeyKind) -> std::result::Result<(), CryptoError> { - if self.kind != expected { - return Err(CryptoError::InvalidAccess(Some("Invalid key type".into()))); - } - - Ok(()) - } -} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/derive_algorithm.rs b/stdlib/src/llrt/llrt_crypto/subtle/derive_algorithm.rs deleted file mode 100644 index bde1715f..00000000 --- a/stdlib/src/llrt/llrt_crypto/subtle/derive_algorithm.rs +++ /dev/null @@ -1,77 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::rc::Rc; - -use crate::llrt_utils::object::ObjectExt; -use rquickjs::{Class, Ctx, FromJs, Result, Value}; - -use super::{ - algorithm_invalid_access_error, algorithm_mismatch_error, algorithm_not_supported_error, - crypto_key::{CryptoKey, KeyKind}, - key_algorithm::{EcAlgorithm, KeyAlgorithm, KeyDerivation}, - normalize_algorithm_name, - util::ResultDomExt, - EllipticCurve, -}; - -#[derive(Debug)] -pub enum DeriveAlgorithm { - X25519 { - public_key: Rc<[u8]>, - }, - Ecdh { - curve: EllipticCurve, - ec_algorithm: EcAlgorithm, - public_key: Rc<[u8]>, - }, - Derive(KeyDerivation), -} - -impl<'js> FromJs<'js> for DeriveAlgorithm { - fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let obj = value.into_object_or_throw(ctx, "algorithm")?; - - let name: String = obj.get_required("name", "algorithm")?; - let name = normalize_algorithm_name(&name); - - Ok(match name.as_str() { - "X25519" => { - let public_key: Class = obj.get_required("public", "algorithm")?; - let public_key = public_key.borrow(); - - public_key.check_kind(KeyKind::Public).or_throw_dom(ctx)?; - - if !matches!(public_key.algorithm, KeyAlgorithm::X25519) { - return algorithm_invalid_access_error(ctx, &name); - } - - DeriveAlgorithm::X25519 { - public_key: public_key.handle.clone(), - } - } - "ECDH" => { - let public_key: Class = obj.get_required("public", "algorithm")?; - let public_key = public_key.borrow(); - - public_key.check_kind(KeyKind::Public).or_throw_dom(ctx)?; - - if let KeyAlgorithm::Ec { - curve, algorithm, .. - } = &public_key.algorithm - { - DeriveAlgorithm::Ecdh { - curve: *curve, - ec_algorithm: algorithm.clone(), - public_key: public_key.handle.clone(), - } - } else { - return algorithm_mismatch_error(ctx, &name); - } - } - "HKDF" => DeriveAlgorithm::Derive(KeyDerivation::for_hkdf_object(ctx, obj)?), - "PBKDF2" => DeriveAlgorithm::Derive(KeyDerivation::for_pbkf2_object(&ctx, obj)?), - _ => return algorithm_not_supported_error(ctx), - }) - } -} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/derive_bits.rs b/stdlib/src/llrt/llrt_crypto/subtle/derive_bits.rs deleted file mode 100644 index e371a27c..00000000 --- a/stdlib/src/llrt/llrt_crypto/subtle/derive_bits.rs +++ /dev/null @@ -1,184 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::future::Future; - -use crate::llrt_exceptions::DOMException; -use crate::llrt_utils::result::ResultExt; -use rquickjs::{prelude::Opt, ArrayBuffer, Class, Ctx, FromJs, Result, Value}; - -use crate::llrt_crypto::{provider::CryptoProvider, CRYPTO_PROVIDER}; - -use super::{ - algorithm_invalid_access_error, algorithm_mismatch_error, - crypto_key::{CryptoKey, KeyKind}, - derive_algorithm::DeriveAlgorithm, - key_algorithm::{EcAlgorithm, KeyAlgorithm, KeyDerivation}, - util::ResultDomExt, - EllipticCurve, -}; - -pub fn subtle_derive_bits<'js>( - ctx: Ctx<'js>, - algorithm: Value<'js>, - base_key: Class<'js, CryptoKey<'js>>, - length: Opt>, -) -> impl Future>> + 'js { - let prepared = DeriveAlgorithm::from_js(&ctx, algorithm); - - async move { - let algorithm = prepared?; - - let base_key = base_key.borrow(); - base_key.check_validity("deriveBits").or_throw_dom(&ctx)?; - - let length = parse_derive_bits_length(&ctx, length)?; - let bytes = derive_bits(&ctx, &algorithm, &base_key, length)?; - - ArrayBuffer::new(ctx, bytes) - } -} - -pub(super) fn derive_bits( - ctx: &Ctx<'_>, - algorithm: &DeriveAlgorithm, - base_key: &CryptoKey, - length: DeriveBitsLength, -) -> Result> { - match algorithm { - DeriveAlgorithm::Ecdh { - curve, - ec_algorithm, - public_key, - } => { - if !matches!(ec_algorithm, EcAlgorithm::Ecdh) { - return algorithm_invalid_access_error(ctx, "ECDH"); - } - if let KeyAlgorithm::Ec { - curve: base_key_curve, - algorithm, - } = &base_key.algorithm - { - if curve == base_key_curve - && base_key.kind == KeyKind::Private - && matches!(algorithm, EcAlgorithm::Ecdh) - { - let length = match length { - DeriveBitsLength::Default => match curve { - EllipticCurve::P256 => 256, - EllipticCurve::P384 => 384, - EllipticCurve::P521 => 528, - }, - DeriveBitsLength::Specified(length) => length, - }; - let bytes = CRYPTO_PROVIDER - .ecdh_derive_bits(*curve, &base_key.handle, public_key) - .or_throw_dom(ctx)?; - return truncate_derived_bits(ctx, bytes, length); - } - - return Err(DOMException::invalid_access_error( - ctx, - "ECDH curve must be same as baseKey", - )); - } - algorithm_mismatch_error(ctx, "ECDH") - } - DeriveAlgorithm::X25519 { public_key } => { - if !matches!(base_key.algorithm, KeyAlgorithm::X25519) { - return algorithm_mismatch_error(ctx, "X25519"); - } - let length = match length { - DeriveBitsLength::Default => 256, - DeriveBitsLength::Specified(length) if length <= 256 => length, - DeriveBitsLength::Specified(_) => { - return Err(DOMException::operation_error(ctx, "Invalid length")); - } - }; - let bytes = CRYPTO_PROVIDER - .x25519_derive_bits(&base_key.handle, public_key) - .or_throw_dom(ctx)?; - - truncate_derived_bits(ctx, bytes, length) - } - DeriveAlgorithm::Derive(KeyDerivation::Hkdf { hash, salt, info }) => { - if !matches!(base_key.algorithm, KeyAlgorithm::HkdfImport) { - return algorithm_invalid_access_error(ctx, "HKDF"); - } - let length = match length { - DeriveBitsLength::Specified(length) if length % 8 == 0 => length, - _ => { - return Err(DOMException::operation_error(ctx, "Invalid length")); - } - }; - let out_length = (length / 8).try_into().or_throw(ctx)?; - CRYPTO_PROVIDER - .hkdf_derive_key(&base_key.handle, salt, info, out_length, *hash) - .or_throw(ctx) - } - DeriveAlgorithm::Derive(KeyDerivation::Pbkdf2 { - hash, - salt, - iterations, - }) => { - if !matches!(base_key.algorithm, KeyAlgorithm::Pbkdf2Import) { - return algorithm_invalid_access_error(ctx, "PBKDF2"); - } - let length = match length { - DeriveBitsLength::Specified(length) if length % 8 == 0 => length, - _ => { - return Err(DOMException::operation_error(ctx, "Invalid length")); - } - }; - let out_length = (length / 8).try_into().or_throw(ctx)?; - CRYPTO_PROVIDER - .pbkdf2_derive_key(&base_key.handle, salt, *iterations, out_length, *hash) - .or_throw(ctx) - } - } -} - -fn truncate_derived_bits(ctx: &Ctx<'_>, mut bytes: Vec, length: u32) -> Result> { - let max_bits = (bytes.len() * 8) as u32; - - if length > max_bits { - return Err(DOMException::operation_error( - ctx, - "Requested length exceeds derived secret size", - )); - } - - let byte_length = length.div_ceil(8) as usize; - bytes.truncate(byte_length); - - let remainder = (length % 8) as u8; - if remainder != 0 { - let mask = 0xff << (8 - remainder); - if let Some(last) = bytes.last_mut() { - *last &= mask; - } - } - - Ok(bytes) -} - -pub(super) enum DeriveBitsLength { - Default, - Specified(u32), -} - -fn parse_derive_bits_length<'js>( - ctx: &Ctx<'js>, - length: Opt>, -) -> Result { - match length.0 { - None => Ok(DeriveBitsLength::Default), - Some(value) if value.is_null() || value.is_undefined() => Ok(DeriveBitsLength::Default), - Some(value) => { - let length = u32::from_js(ctx, value) - .map_err(|_| DOMException::operation_error(ctx, "Invalid length"))?; - - Ok(DeriveBitsLength::Specified(length)) - } - } -} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/derive_keys.rs b/stdlib/src/llrt/llrt_crypto/subtle/derive_keys.rs deleted file mode 100644 index c6b67a5b..00000000 --- a/stdlib/src/llrt/llrt_crypto/subtle/derive_keys.rs +++ /dev/null @@ -1,89 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::future::Future; - -use rquickjs::{Array, Class, Ctx, FromJs, Result, Value}; - -use super::{ - algorithm_not_supported_error, - crypto_key::{CryptoKey, KeyKind}, - derive_algorithm::DeriveAlgorithm, - derive_bits::{derive_bits, DeriveBitsLength}, - key_algorithm::{KeyAlgorithm, KeyAlgorithmMode, KeyAlgorithmWithUsages}, - util::ResultDomExt, -}; - -pub fn subtle_derive_key<'js>( - ctx: Ctx<'js>, - algorithm: Value<'js>, - base_key: Class<'js, CryptoKey<'js>>, - derived_key_algorithm: Value<'js>, - extractable: bool, - key_usages: Array<'js>, -) -> impl Future>>> + 'js { - let prepared = prepare_derive_key(&ctx, algorithm, derived_key_algorithm, key_usages); - - async move { - let (algorithm, key_algorithm) = prepared?; - - derive_key(&ctx, &algorithm, &base_key, extractable, key_algorithm) - } -} - -fn prepare_derive_key<'js>( - ctx: &Ctx<'js>, - algorithm: Value<'js>, - derived_key_algorithm: Value<'js>, - key_usages: Array<'js>, -) -> Result<(DeriveAlgorithm, KeyAlgorithmWithUsages)> { - let algorithm = DeriveAlgorithm::from_js(ctx, algorithm)?; - - let key_algorithm = KeyAlgorithm::from_js( - ctx, - KeyAlgorithmMode::Derive, - derived_key_algorithm, - key_usages, - )?; - - Ok((algorithm, key_algorithm)) -} - -fn derive_key<'js>( - ctx: &Ctx<'js>, - algorithm: &DeriveAlgorithm, - base_key: &Class<'js, CryptoKey<'js>>, - extractable: bool, - key_algorithm: KeyAlgorithmWithUsages, -) -> Result>> { - let length = match &key_algorithm.algorithm { - KeyAlgorithm::Aes { length, .. } => *length, - KeyAlgorithm::Hmac { length, .. } => *length, - KeyAlgorithm::Derive { .. } => 0, - _ => { - return algorithm_not_supported_error(ctx); - } - }; - - let base_key = base_key.borrow(); - - base_key.check_validity("deriveKey").or_throw_dom(ctx)?; - - let bytes = derive_bits( - ctx, - algorithm, - &base_key, - DeriveBitsLength::Specified(length as u32), - )?; - - let key = CryptoKey::new( - KeyKind::Secret, - key_algorithm.name, - extractable, - key_algorithm.algorithm, - key_algorithm.public_usages, - bytes, - ); - - Class::instance(ctx.clone(), key) -} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/digest.rs b/stdlib/src/llrt/llrt_crypto/subtle/digest.rs deleted file mode 100644 index 137cde8f..00000000 --- a/stdlib/src/llrt/llrt_crypto/subtle/digest.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use std::future::Future; - -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use crate::llrt_utils::{bytes::ObjectBytes, object::ObjectExt, result::ResultExt}; -use rquickjs::{ArrayBuffer, Ctx, Result, Value}; - -use crate::llrt_crypto::{ - hash::HashAlgorithm, - provider::{CryptoProvider, SimpleDigest}, - CRYPTO_PROVIDER, -}; - -use super::algorithm_not_supported_error; - -pub fn subtle_digest<'js>( - ctx: Ctx<'js>, - algorithm: Value<'js>, - data: ObjectBytes<'js>, -) -> impl Future>> + 'js { - // Snapshot inputs synchronously so mutating/detaching the buffer after the call can't affect the result (WPT digest.https.any.js). - let prepared = prepare_digest(&ctx, algorithm, data); - - async move { - let (hash_algorithm, input) = prepared?; - let bytes = digest(&hash_algorithm, &input); - ArrayBuffer::new(ctx, bytes) - } -} - -fn prepare_digest<'js>( - ctx: &Ctx<'js>, - algorithm: Value<'js>, - data: ObjectBytes<'js>, -) -> Result<(HashAlgorithm, Vec)> { - let algorithm = if let Some(s) = algorithm.as_string() { - s.to_string().or_throw(ctx)? - } else if let Some(name) = algorithm.get_optional::<_, String>("name")? { - name - } else { - return Err(rquickjs::Exception::throw_type( - ctx, - "Algorithm 'name' property required", - )); - }; - let hash_algorithm = match HashAlgorithm::try_from(algorithm.as_str()) { - Ok(h) => h, - Err(_) => return algorithm_not_supported_error(ctx), - }; - let input = data.as_bytes_opt().map(<[u8]>::to_vec).unwrap_or_default(); - Ok((hash_algorithm, input)) -} - -pub fn digest(hash_algorithm: &HashAlgorithm, data: &[u8]) -> Vec { - let mut hasher = CRYPTO_PROVIDER.digest(*hash_algorithm); - hasher.update(data); - hasher.finalize() -} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/encryption.rs b/stdlib/src/llrt/llrt_crypto/subtle/encryption.rs deleted file mode 100644 index a66a374a..00000000 --- a/stdlib/src/llrt/llrt_crypto/subtle/encryption.rs +++ /dev/null @@ -1,269 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::{borrow::Cow, future::Future}; - -use crate::llrt_exceptions::DOMException; -use crate::llrt_utils::{bytes::ObjectBytes, result::ResultExt}; -use rquickjs::{ArrayBuffer, Class, Ctx, Exception, FromJs, Result, Value}; - -use crate::llrt_crypto::{ - provider::{AesMode, CryptoProvider}, - CRYPTO_PROVIDER, -}; - -use super::{ - algorithm_mismatch_error, - encryption_algorithm::EncryptionAlgorithm, - key_algorithm::{AesAlgorithm, KeyAlgorithm}, - util::ResultDomExt, - CryptoKey, EncryptionMode, -}; - -pub fn subtle_decrypt<'js>( - ctx: Ctx<'js>, - algorithm: Value<'js>, - key: Class<'js, CryptoKey<'js>>, - data: ObjectBytes<'js>, -) -> impl Future>> + 'js { - let prepared = prepare_encrypt_decrypt(&ctx, algorithm, key, data); - - async move { - let (algorithm, key, input) = prepared?; - - let key = key.borrow(); - key.check_validity("decrypt").or_throw_dom(&ctx)?; - - let bytes = encrypt_decrypt( - &ctx, - &algorithm, - &key, - &input, - EncryptionMode::Encryption, - EncryptionOperation::Decrypt, - )?; - ArrayBuffer::new(ctx, bytes) - } -} - -pub fn subtle_encrypt<'js>( - ctx: Ctx<'js>, - algorithm: Value<'js>, - key: Class<'js, CryptoKey<'js>>, - data: ObjectBytes<'js>, -) -> impl Future>> + 'js { - let prepared = prepare_encrypt_decrypt(&ctx, algorithm, key, data); - - async move { - let (algorithm, key, input) = prepared?; - - let key = key.borrow(); - key.check_validity("encrypt").or_throw_dom(&ctx)?; - - let bytes = encrypt_decrypt( - &ctx, - &algorithm, - &key, - &input, - EncryptionMode::Encryption, - EncryptionOperation::Encrypt, - )?; - ArrayBuffer::new(ctx, bytes) - } -} - -fn prepare_encrypt_decrypt<'js>( - ctx: &Ctx<'js>, - algorithm: Value<'js>, - key: Class<'js, CryptoKey<'js>>, - data: ObjectBytes<'js>, -) -> Result<(EncryptionAlgorithm, Class<'js, CryptoKey<'js>>, Vec)> { - let algorithm = EncryptionAlgorithm::from_js(ctx, algorithm)?; - let input = data.as_bytes_opt().map(<[u8]>::to_vec).unwrap_or_default(); - Ok((algorithm, key, input)) -} - -pub enum EncryptionOperation { - Encrypt, - Decrypt, -} - -pub fn encrypt_decrypt( - ctx: &Ctx<'_>, - algorithm: &EncryptionAlgorithm, - key: &CryptoKey, - data: &[u8], - mode: EncryptionMode, - operation: EncryptionOperation, -) -> Result> { - let handle = key.handle.as_ref(); - let bytes = match algorithm { - EncryptionAlgorithm::AesCbc { iv } => { - validate_aes_length(ctx, key, handle, AesAlgorithm::Cbc)?; - - match operation { - EncryptionOperation::Encrypt => CRYPTO_PROVIDER - .aes_encrypt(AesMode::Cbc, handle, iv, data, None) - .or_throw_dom(ctx)?, - EncryptionOperation::Decrypt => CRYPTO_PROVIDER - .aes_decrypt(AesMode::Cbc, handle, iv, data, None) - .or_throw_dom(ctx)?, - } - } - EncryptionAlgorithm::AesCtr { - counter, - length: encryption_length, - } => { - validate_aes_length(ctx, key, handle, AesAlgorithm::Ctr)?; - match operation { - EncryptionOperation::Encrypt => CRYPTO_PROVIDER - .aes_encrypt( - AesMode::Ctr { - counter_length: *encryption_length, - }, - handle, - counter, - data, - None, - ) - .or_throw_dom(ctx)?, - EncryptionOperation::Decrypt => CRYPTO_PROVIDER - .aes_decrypt( - AesMode::Ctr { - counter_length: *encryption_length, - }, - handle, - counter, - data, - None, - ) - .or_throw_dom(ctx)?, - } - } - EncryptionAlgorithm::AesGcm { - iv, - tag_length, - additional_data, - } => { - validate_aes_length(ctx, key, handle, AesAlgorithm::Gcm)?; - let aad = additional_data.as_deref(); - - match operation { - EncryptionOperation::Encrypt => CRYPTO_PROVIDER - .aes_encrypt( - AesMode::Gcm { - tag_length: *tag_length, - }, - handle, - iv, - data, - aad, - ) - .or_throw_dom(ctx)?, - EncryptionOperation::Decrypt => { - let tag_len = (*tag_length as usize) / 8; - if data.len() < tag_len { - return Err(DOMException::operation_error( - ctx, - "Invalid ciphertext length", - )); - } - // Pass the full data (ciphertext + tag) to the decrypt function - CRYPTO_PROVIDER - .aes_decrypt( - AesMode::Gcm { - tag_length: *tag_length, - }, - handle, - iv, - data, - aad, - ) - .or_throw_dom(ctx)? - } - } - } - EncryptionAlgorithm::AesKw => { - let padding = match mode { - EncryptionMode::Encryption => { - return Err(Exception::throw_message( - ctx, - "AES-KW can only be used for wrapping keys", - )); - } - EncryptionMode::Wrapping(padding) => padding, - }; - - match operation { - EncryptionOperation::Encrypt => { - // Pad data to multiple of 8 bytes if needed - let mut padded_data = Cow::Borrowed(data); - if !data.len().is_multiple_of(8) { - let pad_len = 8 - (data.len() % 8); - let mut padded = data.to_vec(); - padded.extend(std::iter::repeat_n(padding, pad_len)); - padded_data = Cow::Owned(padded) - } - CRYPTO_PROVIDER - .aes_kw_wrap(handle, &padded_data) - .or_throw_dom(ctx)? - } - EncryptionOperation::Decrypt => { - let unwrapped = CRYPTO_PROVIDER.aes_kw_unwrap(handle, data).or_throw(ctx)?; - // Remove padding if present - if padding != 0 { - let trimmed: Vec = unwrapped - .into_iter() - .rev() - .skip_while(|&b| b == padding) - .collect::>() - .into_iter() - .rev() - .collect(); - trimmed - } else { - unwrapped - } - } - } - } - EncryptionAlgorithm::RsaOaep { label } => { - let hash = match &key.algorithm { - KeyAlgorithm::Rsa { hash, .. } => hash, - _ => return algorithm_mismatch_error(ctx, "RSA-OAEP"), - }; - - match operation { - EncryptionOperation::Encrypt => CRYPTO_PROVIDER - .rsa_oaep_encrypt(handle, data, *hash, label.as_deref()) - .or_throw_dom(ctx)?, - EncryptionOperation::Decrypt => CRYPTO_PROVIDER - .rsa_oaep_decrypt(handle, data, *hash, label.as_deref()) - .or_throw_dom(ctx)?, - } - } - }; - Ok(bytes) -} - -pub fn validate_aes_length( - ctx: &Ctx<'_>, - key: &CryptoKey, - handle: &[u8], - expected_algorithm: AesAlgorithm, -) -> Result<()> { - match &key.algorithm { - KeyAlgorithm::Aes { algorithm, length } if *algorithm == expected_algorithm => { - if *length != handle.len() as u16 * 8 { - return Err(DOMException::operation_error(ctx, "Invalid AES key length")); - } - Ok(()) - } - KeyAlgorithm::Aes { .. } => Err(DOMException::invalid_access_error( - ctx, - "AES algorithm mismatch", - )), - - _ => algorithm_mismatch_error(ctx, "AES"), - } -} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/encryption_algorithm.rs b/stdlib/src/llrt/llrt_crypto/subtle/encryption_algorithm.rs deleted file mode 100644 index ce1c580c..00000000 --- a/stdlib/src/llrt/llrt_crypto/subtle/encryption_algorithm.rs +++ /dev/null @@ -1,120 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use crate::llrt_exceptions::DOMException; -use crate::llrt_utils::{bytes::ObjectBytes, object::ObjectExt}; -use rquickjs::{Ctx, Exception, FromJs, Result, Value}; - -use super::{algorithm_not_supported_error, normalize_algorithm_name, to_name_and_maybe_object}; - -#[derive(Debug)] -pub enum EncryptionAlgorithm { - AesCbc { - iv: Box<[u8]>, - }, - AesCtr { - counter: Box<[u8]>, - length: u32, - }, - AesGcm { - iv: Box<[u8]>, - tag_length: u8, - additional_data: Option>, - }, - RsaOaep { - label: Option>, - }, - AesKw, -} - -impl<'js> FromJs<'js> for EncryptionAlgorithm { - fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let (name, obj) = to_name_and_maybe_object(ctx, value)?; - let name = normalize_algorithm_name(&name); - - match name.as_str() { - "AES-CBC" => { - let obj = obj?; - let iv = obj - .get_required::<_, ObjectBytes>("iv", "algorithm")? - .into_bytes(ctx)? - .into_boxed_slice(); - - if iv.len() != 16 { - return Err(DOMException::operation_error( - ctx, - "invalid length of iv. Currently supported 16 bytes", - )); - } - - Ok(EncryptionAlgorithm::AesCbc { iv }) - } - "AES-CTR" => { - let obj = obj?; - let counter = obj - .get_required::<_, ObjectBytes>("counter", "algorithm")? - .into_bytes(ctx)? - .into_boxed_slice(); - - let length = obj.get_required::<_, u32>("length", "algorithm")?; - - if !matches!(length, 32 | 64 | 128) { - return Err(DOMException::operation_error( - ctx, - "invalid counter length. Currently supported 32/64/128 bits", - )); - } - - Ok(EncryptionAlgorithm::AesCtr { counter, length }) - } - "AES-GCM" => { - let obj = obj?; - let iv = obj - .get_required::<_, ObjectBytes>("iv", "algorithm")? - .into_bytes(ctx)? - .into_boxed_slice(); - - //FIXME only 12? 96 maybe recommended? - if iv.len() != 12 { - return Err(Exception::throw_type( - ctx, - "invalid length of iv. Currently supported 12 bytes", - )); - } - - let additional_data = obj - .get_optional::<_, ObjectBytes>("additionalData")? - .map(|v| v.into_bytes(ctx)) - .transpose()? - .map(|vec| vec.into_boxed_slice()); - - let tag_length = obj.get_optional::<_, u8>("tagLength")?.unwrap_or(128); - - //ensure tag length is supported using a match statement 32, 64, 96, 104, 112, 120, or 128 - if !matches!(tag_length, 32 | 64 | 96 | 104 | 112 | 120 | 128) { - return Err(DOMException::operation_error(ctx, "Invalid tagLength")); - } - - Ok(EncryptionAlgorithm::AesGcm { - iv, - additional_data, - tag_length, - }) - } - "RSA-OAEP" => { - let label = if let Ok(obj) = obj { - obj.get_optional::<_, ObjectBytes>("label")? - .map(|bytes| bytes.into_bytes(ctx)) - .transpose()? - .map(|vec| vec.into_boxed_slice()) - } else { - None - }; - - Ok(EncryptionAlgorithm::RsaOaep { label }) - } - "AES-KW" => Ok(EncryptionAlgorithm::AesKw), - _ => algorithm_not_supported_error(ctx), - } - } -} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/export_key.rs b/stdlib/src/llrt/llrt_crypto/subtle/export_key.rs deleted file mode 100644 index cf0f9802..00000000 --- a/stdlib/src/llrt/llrt_crypto/subtle/export_key.rs +++ /dev/null @@ -1,229 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Unified key export implementation using CryptoProvider trait. - -use crate::llrt_encoding::bytes_to_b64_url_safe_string; -use rquickjs::{ArrayBuffer, Class, Ctx, Exception, Object, Result}; - -use crate::llrt_crypto::provider::CryptoProvider; -use crate::llrt_crypto::CRYPTO_PROVIDER; - -use super::{ - crypto_key::KeyKind, - key_algorithm::{KeyAlgorithm, KeyFormat}, - util::ResultDomExt, - CryptoKey, -}; - -pub fn algorithm_export_error(ctx: &Ctx<'_>, algorithm: &str, format: &str) -> Result { - Err(Exception::throw_message( - ctx, - &["Export of ", algorithm, " as ", format, " is not supported"].concat(), - )) -} - -pub enum ExportOutput<'js> { - Bytes(Vec), - Object(Object<'js>), -} - -pub async fn subtle_export_key<'js>( - ctx: Ctx<'js>, - format: KeyFormat, - key: Class<'js, CryptoKey<'js>>, -) -> Result> { - let key = key.borrow(); - let export = export_key(&ctx, format, &key)?; - Ok(match export { - ExportOutput::Bytes(bytes) => ArrayBuffer::new(ctx, bytes)?.into_object(), - ExportOutput::Object(object) => object, - }) -} - -pub fn export_key<'js>( - ctx: &Ctx<'js>, - format: KeyFormat, - key: &CryptoKey, -) -> Result> { - if !key.extractable { - return Err(Exception::throw_type( - ctx, - "The CryptoKey is non extractable", - )); - } - let bytes = match format { - KeyFormat::Jwk => return Ok(ExportOutput::Object(export_jwk(ctx, key)?)), - KeyFormat::Raw => export_raw(ctx, key), - KeyFormat::Spki => export_spki(ctx, key), - KeyFormat::Pkcs8 => export_pkcs8(ctx, key), - }?; - Ok(ExportOutput::Bytes(bytes)) -} - -fn export_raw(ctx: &Ctx<'_>, key: &CryptoKey) -> Result> { - if key.kind == KeyKind::Private { - return Err(Exception::throw_type( - ctx, - "Private Crypto keys can't be exported as raw format", - )); - } - match &key.algorithm { - KeyAlgorithm::Aes { .. } | KeyAlgorithm::Hmac { .. } => Ok(key.handle.to_vec()), - KeyAlgorithm::Ec { curve, .. } => CRYPTO_PROVIDER - .export_ec_public_key_sec1(&key.handle, *curve, false) - .or_throw_dom(ctx), - KeyAlgorithm::Ed25519 => CRYPTO_PROVIDER - .export_okp_public_key_raw(&key.handle, false) - .or_throw_dom(ctx), - KeyAlgorithm::X25519 => CRYPTO_PROVIDER - .export_okp_public_key_raw(&key.handle, false) - .or_throw_dom(ctx), - KeyAlgorithm::Rsa { .. } => CRYPTO_PROVIDER - .export_rsa_public_key_pkcs1(&key.handle) - .or_throw_dom(ctx), - _ => algorithm_export_error(ctx, &key.name, "raw"), - } -} - -fn export_pkcs8(ctx: &Ctx<'_>, key: &CryptoKey) -> Result> { - if key.kind != KeyKind::Private { - return Err(Exception::throw_type( - ctx, - "Public or Secret Crypto keys can't be exported as pkcs8 format", - )); - } - match &key.algorithm { - KeyAlgorithm::Ec { curve, .. } => CRYPTO_PROVIDER - .export_ec_private_key_pkcs8(&key.handle, *curve) - .or_throw_dom(ctx), - KeyAlgorithm::Ed25519 => CRYPTO_PROVIDER - .export_okp_private_key_pkcs8( - &key.handle, - const_oid::db::rfc8410::ID_ED_25519.as_bytes(), - ) - .or_throw_dom(ctx), - KeyAlgorithm::X25519 => CRYPTO_PROVIDER - .export_okp_private_key_pkcs8( - &key.handle, - const_oid::db::rfc8410::ID_X_25519.as_bytes(), - ) - .or_throw_dom(ctx), - KeyAlgorithm::Rsa { .. } => CRYPTO_PROVIDER - .export_rsa_private_key_pkcs8(&key.handle) - .or_throw_dom(ctx), - _ => algorithm_export_error(ctx, &key.name, "pkcs8"), - } -} - -fn export_spki(ctx: &Ctx<'_>, key: &CryptoKey) -> Result> { - if key.kind != KeyKind::Public { - return Err(Exception::throw_type( - ctx, - "Private or Secret Crypto keys can't be exported as spki format", - )); - } - match &key.algorithm { - KeyAlgorithm::Ec { curve, .. } => CRYPTO_PROVIDER - .export_ec_public_key_spki(&key.handle, *curve) - .or_throw_dom(ctx), - KeyAlgorithm::Ed25519 => CRYPTO_PROVIDER - .export_okp_public_key_spki(&key.handle, const_oid::db::rfc8410::ID_ED_25519.as_bytes()) - .or_throw_dom(ctx), - KeyAlgorithm::X25519 => CRYPTO_PROVIDER - .export_okp_public_key_spki(&key.handle, const_oid::db::rfc8410::ID_X_25519.as_bytes()) - .or_throw_dom(ctx), - KeyAlgorithm::Rsa { .. } => CRYPTO_PROVIDER - .export_rsa_public_key_spki(&key.handle) - .or_throw_dom(ctx), - _ => algorithm_export_error(ctx, &key.name, "spki"), - } -} - -fn export_jwk<'js>(ctx: &Ctx<'js>, key: &CryptoKey) -> Result> { - let obj = Object::new(ctx.clone())?; - obj.set("key_ops", key.usages.clone())?; - obj.set("ext", true)?; - - match &key.algorithm { - KeyAlgorithm::Aes { length, .. } => { - let prefix = match length { - 128 => "A128", - 192 => "A192", - 256 => "A256", - _ => unreachable!(), - }; - let suffix = &key.name[("AES-".len())..]; - obj.set("kty", "oct")?; - obj.set("k", bytes_to_b64_url_safe_string(&key.handle))?; - obj.set("alg", [prefix, suffix].concat())?; - } - KeyAlgorithm::Hmac { hash, .. } => { - obj.set("kty", "oct")?; - obj.set("alg", ["HS", &hash.as_str()[4..]].concat())?; - obj.set("k", bytes_to_b64_url_safe_string(&key.handle))?; - } - KeyAlgorithm::Ec { curve, .. } => { - let jwk = CRYPTO_PROVIDER - .export_ec_jwk(&key.handle, *curve, key.kind == KeyKind::Private) - .or_throw_dom(ctx)?; - obj.set("kty", "EC")?; - obj.set("crv", curve.as_str())?; - obj.set("x", bytes_to_b64_url_safe_string(&jwk.x))?; - obj.set("y", bytes_to_b64_url_safe_string(&jwk.y))?; - if let Some(d) = jwk.d { - obj.set("d", bytes_to_b64_url_safe_string(&d))?; - } - } - KeyAlgorithm::Ed25519 => { - let jwk = CRYPTO_PROVIDER - .export_okp_jwk(&key.handle, key.kind == KeyKind::Private, true) - .or_throw_dom(ctx)?; - obj.set("kty", "OKP")?; - obj.set("crv", "Ed25519")?; - obj.set("x", bytes_to_b64_url_safe_string(&jwk.x))?; - obj.set("alg", "Ed25519")?; - if let Some(d) = jwk.d { - obj.set("d", bytes_to_b64_url_safe_string(&d))?; - } - } - KeyAlgorithm::X25519 => { - let jwk = CRYPTO_PROVIDER - .export_okp_jwk(&key.handle, key.kind == KeyKind::Private, false) - .or_throw_dom(ctx)?; - obj.set("kty", "OKP")?; - obj.set("crv", "X25519")?; - obj.set("x", bytes_to_b64_url_safe_string(&jwk.x))?; - if let Some(d) = jwk.d { - obj.set("d", bytes_to_b64_url_safe_string(&d))?; - } - } - KeyAlgorithm::Rsa { hash, .. } => { - let jwk = CRYPTO_PROVIDER - .export_rsa_jwk(&key.handle, key.kind == KeyKind::Private) - .or_throw_dom(ctx)?; - let alg_suffix = hash.as_numeric_str(); - let alg_prefix = match key.name.as_ref() { - "RSASSA-PKCS1-v1_5" => "RS", - "RSA-PSS" => "PS", - "RSA-OAEP" => "RSA-OAEP-", - _ => unreachable!(), - }; - obj.set("kty", "RSA")?; - obj.set("n", bytes_to_b64_url_safe_string(&jwk.n))?; - obj.set("e", bytes_to_b64_url_safe_string(&jwk.e))?; - obj.set("alg", [alg_prefix, alg_suffix].concat())?; - if let Some(d) = jwk.d { - obj.set("d", bytes_to_b64_url_safe_string(&d))?; - obj.set("p", bytes_to_b64_url_safe_string(&jwk.p.unwrap()))?; - obj.set("q", bytes_to_b64_url_safe_string(&jwk.q.unwrap()))?; - obj.set("dp", bytes_to_b64_url_safe_string(&jwk.dp.unwrap()))?; - obj.set("dq", bytes_to_b64_url_safe_string(&jwk.dq.unwrap()))?; - obj.set("qi", bytes_to_b64_url_safe_string(&jwk.qi.unwrap()))?; - } - } - _ => return algorithm_export_error(ctx, &key.name, "jwk"), - } - Ok(obj) -} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/generate_key.rs b/stdlib/src/llrt/llrt_crypto/subtle/generate_key.rs deleted file mode 100644 index 223fe4c5..00000000 --- a/stdlib/src/llrt/llrt_crypto/subtle/generate_key.rs +++ /dev/null @@ -1,137 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use crate::llrt_exceptions::DOMException; -use rquickjs::{object::Property, Array, Class, Ctx, Object, Result, Value}; - -use crate::llrt_crypto::{hash::HashAlgorithm, provider::CryptoProvider, CRYPTO_PROVIDER}; - -use super::{ - algorithm_not_supported_error, - crypto_key::{CryptoKey, KeyKind}, - key_algorithm::{KeyAlgorithm, KeyAlgorithmMode, KeyAlgorithmWithUsages}, - util::ResultDomExt, -}; - -pub async fn subtle_generate_key<'js>( - ctx: Ctx<'js>, - algorithm: Value<'js>, - extractable: Value<'js>, - key_usages: Array<'js>, -) -> Result> { - let KeyAlgorithmWithUsages { - name, - algorithm: key_algorithm, - private_usages, - public_usages, - } = KeyAlgorithm::from_js(&ctx, KeyAlgorithmMode::Generate, algorithm, key_usages)?; - - let (private_key, public_or_secret_key) = generate_key(&ctx, &key_algorithm)?; - - let Some(extractable) = extractable.as_bool() else { - return Err(DOMException::not_supported_error(&ctx, "Invalid parameter")); - }; - - if matches!( - key_algorithm, - KeyAlgorithm::Aes { .. } | KeyAlgorithm::Hmac { .. } - ) { - return Ok(Class::instance( - ctx, - CryptoKey::new( - KeyKind::Secret, - name, - extractable, - key_algorithm, - public_usages, - public_or_secret_key, - ), - )? - .into_value()); - } - - let private_key = Class::instance( - ctx.clone(), - CryptoKey::new( - KeyKind::Private, - name.clone(), - extractable, - key_algorithm.clone(), - private_usages, - private_key, - ), - )?; - - let public_key = Class::instance( - ctx.clone(), - CryptoKey::new( - KeyKind::Public, - name, - true, - key_algorithm, - public_usages, - public_or_secret_key, - ), - )?; - - let key_pair = Object::new(ctx.clone())?; - key_pair.prop("privateKey", Property::from(private_key).enumerable())?; - key_pair.prop("publicKey", Property::from(public_key).enumerable())?; - Ok(key_pair.into_value()) -} - -fn generate_key(ctx: &Ctx<'_>, algorithm: &KeyAlgorithm) -> Result<(Vec, Vec)> { - match algorithm { - KeyAlgorithm::Aes { length, .. } => { - // Default to AES-256 - let key = CRYPTO_PROVIDER - .generate_aes_key(*length) - .or_throw_dom_with_msg(ctx, "AES key generation failed")?; - Ok((vec![], key)) - } - KeyAlgorithm::Hmac { hash, length } => { - let key = CRYPTO_PROVIDER - .generate_hmac_key(*hash, *length) - .or_throw_dom_with_msg(ctx, "HMAC key generation failed")?; - Ok((vec![], key)) - } - KeyAlgorithm::Ec { curve, .. } => CRYPTO_PROVIDER - .generate_ec_key(*curve) - .or_throw_dom_with_msg(ctx, "EC key generation failed"), - KeyAlgorithm::Ed25519 => CRYPTO_PROVIDER - .generate_ed25519_key() - .or_throw_dom_with_msg(ctx, "Ed25519 key generation failed"), - KeyAlgorithm::X25519 => CRYPTO_PROVIDER - .generate_x25519_key() - .or_throw_dom_with_msg(ctx, "X25519 key generation failed"), - KeyAlgorithm::Rsa { - modulus_length, - public_exponent, - .. - } => CRYPTO_PROVIDER - .generate_rsa_key(*modulus_length, public_exponent.as_ref()) - .or_throw_dom_with_msg(ctx, "RSA key generation failed"), - _ => algorithm_not_supported_error(ctx), - } -} - -#[allow(dead_code)] -fn generate_symmetric_key(_ctx: &Ctx<'_>, length: usize) -> Result> { - Ok(crate::llrt_crypto::random_byte_array(length)) -} - -#[allow(dead_code)] -pub fn get_hash_length(ctx: &Ctx, hash: &HashAlgorithm, length: u16) -> Result { - if length == 0 { - return Ok(hash.block_len()); - } - - if !length.is_multiple_of(8) || (length / 8) as usize > 128 { - return Err(DOMException::not_supported_error( - ctx, - "Invalid HMAC key length", - )); - } - - Ok((length / 8) as usize) -} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/import_key.rs b/stdlib/src/llrt/llrt_crypto/subtle/import_key.rs deleted file mode 100644 index 403d4c68..00000000 --- a/stdlib/src/llrt/llrt_crypto/subtle/import_key.rs +++ /dev/null @@ -1,76 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use crate::llrt_exceptions::DOMException; -use crate::llrt_utils::{bytes::ObjectBytes, object::ObjectExt}; -use rquickjs::{Array, Class, Ctx, FromJs, Result, Value}; - -use super::{ - crypto_key::{CryptoKey, KeyKind}, - key_algorithm::{ - KeyAlgorithm, KeyAlgorithmMode, KeyAlgorithmWithUsages, KeyFormat, KeyFormatData, - }, -}; - -pub async fn subtle_import_key<'js>( - ctx: Ctx<'js>, - format: KeyFormat, - key_data: Value<'js>, - algorithm: Value<'js>, - extractable: bool, - key_usages: Array<'js>, -) -> Result>> { - let format = match format { - KeyFormat::Raw => KeyFormatData::Raw(ObjectBytes::from_js(&ctx, key_data)?), - KeyFormat::Pkcs8 => KeyFormatData::Pkcs8(ObjectBytes::from_js(&ctx, key_data)?), - KeyFormat::Spki => KeyFormatData::Spki(ObjectBytes::from_js(&ctx, key_data)?), - KeyFormat::Jwk => KeyFormatData::Jwk(key_data.into_object_or_throw(&ctx, "keyData")?), - }; - - import_key(ctx, format, algorithm, extractable, key_usages) -} - -pub fn import_key<'js>( - ctx: Ctx<'js>, - format: KeyFormatData<'js>, - algorithm: Value<'js>, - extractable: bool, - key_usages: Array<'js>, -) -> Result>> { - if extractable { - if let KeyFormatData::Jwk(jwk) = &format { - if matches!(jwk.get_optional::<_, bool>("ext")?, Some(false)) { - return Err(DOMException::data_error(&ctx, "JWK is not extractable")); - } - } - } - - let mut kind = KeyKind::Public; - let mut data = Vec::new(); - - let KeyAlgorithmWithUsages { - name, - algorithm: key_algorithm, - public_usages, - private_usages, - } = KeyAlgorithm::from_js( - &ctx, - KeyAlgorithmMode::Import { - kind: &mut kind, - data: &mut data, - format, - }, - algorithm, - key_usages, - )?; - - let usages = match kind { - KeyKind::Public | KeyKind::Secret => public_usages, - KeyKind::Private => private_usages, - }; - - Class::instance( - ctx, - CryptoKey::new(kind, name, extractable, key_algorithm, usages, data), - ) -} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/key_algorithm.rs b/stdlib/src/llrt/llrt_crypto/subtle/key_algorithm.rs deleted file mode 100644 index 32ec9ed0..00000000 --- a/stdlib/src/llrt/llrt_crypto/subtle/key_algorithm.rs +++ /dev/null @@ -1,1609 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -#![allow(clippy::uninlined_format_args)] - -use std::rc::Rc; - -#[cfg(all())] -use crate::llrt_encoding::bytes_from_b64_url_safe; -use crate::llrt_exceptions::DOMException; -use crate::llrt_utils::{bytes::ObjectBytes, object::ObjectExt, result::ResultExt, str_enum}; -#[cfg(all())] -use der::{ - asn1::{BitStringRef, OctetString, OctetStringRef}, - Decode, Encode, -}; -#[cfg(all())] -use ed25519_dalek::SigningKey; -#[cfg(all())] -use pkcs8::PrivateKeyInfoRef; -use rquickjs::{ - atom::PredefinedAtom, Array, Ctx, Exception, FromJs, Object, Result, TypedArray, Value, -}; -#[cfg(all())] -use spki::{AlgorithmIdentifier, ObjectIdentifier}; -#[cfg(all())] -use x25519_dalek::{PublicKey, StaticSecret}; - -use crate::llrt_crypto::{hash::HashAlgorithm, provider::parse_rsa_public_exponent}; - -#[cfg(all())] -use super::{algorithm_mismatch_error, util::DataError}; -use super::{ - algorithm_not_supported_error, - crypto_key::KeyKind, - normalize_algorithm_name, to_name_and_maybe_object, - util::{NotSupportedError, ResultDomExt}, - EllipticCurve, -}; - -#[derive(Clone, Copy, PartialEq)] -pub enum KeyUsage { - //7 values, can be max 255 (u8) 0b11111111 - Encrypt, - Decrypt, - WrapKey, - UnwrapKey, - Sign, - Verify, - DeriveKey, - DeriveBits, -} - -impl TryFrom<&str> for KeyUsage { - type Error = String; - - fn try_from(s: &str) -> std::result::Result { - Ok(match s { - "encrypt" => KeyUsage::Encrypt, - "decrypt" => KeyUsage::Decrypt, - "wrapKey" => KeyUsage::WrapKey, - "unwrapKey" => KeyUsage::UnwrapKey, - "sign" => KeyUsage::Sign, - "verify" => KeyUsage::Verify, - "deriveKey" => KeyUsage::DeriveKey, - "deriveBits" => KeyUsage::DeriveBits, - _ => return Err(["Invalid key usage: ", s].concat()), - }) - } -} - -impl KeyUsage { - fn classify_and_check_usages<'js>( - ctx: &Ctx<'js>, - key_usage_algorithm: KeyUsageAlgorithm, - key_usages: &Array<'js>, - private_usages: &mut Vec, - public_usages: &mut Vec, - kind: Option<&KeyKind>, - ) -> Result<()> { - let (mut private_usages_mask, mut public_usages_mask) = key_usage_algorithm.masks(); - - match kind { - Some(KeyKind::Private) => public_usages_mask = 0, - Some(KeyKind::Secret) | Some(KeyKind::Public) => private_usages_mask = 0, - None => {} - }; - - let allowed_usages = private_usages_mask | public_usages_mask; - - let mut generated_public_usages = Vec::with_capacity(4); - let mut generated_private_usages = Vec::with_capacity(4); - - let mut has_any_usages = false; - - for usage in key_usages.iter::() { - has_any_usages = true; - let value = usage?; - let usage = KeyUsage::try_from(value.as_str()).or_throw(ctx)?; - let usage = usage.mask(); - if allowed_usages & usage != usage { - return Err(Exception::throw_syntax( - ctx, - &["Invalid key usage '", &value, "'"].concat(), - )); - } - - if private_usages_mask == public_usages_mask { - generated_private_usages.push(value.clone()); - generated_public_usages.push(value); - } else if private_usages_mask & usage == usage { - generated_private_usages.push(value); - } else if public_usages_mask & usage == usage { - generated_public_usages.push(value); - } - } - - *private_usages = generated_private_usages; - *public_usages = generated_public_usages; - - if !has_any_usages - && key_usage_algorithm.requires_non_empty_usages() - && !matches!(kind, Some(KeyKind::Public)) - { - return Err(Exception::throw_syntax(ctx, "Key usages empty")); - } - - if private_usages != public_usages { - let valid_usage = match kind { - Some(KeyKind::Secret) | Some(KeyKind::Public) => { - private_usages.is_empty() && !public_usages.is_empty() - } - Some(KeyKind::Private) => !private_usages.is_empty() && public_usages.is_empty(), - None => true, - }; - - if !valid_usage { - return Err(Exception::throw_syntax(ctx, "Invalid key usage")); - } - } - - Ok(()) - } - - const fn mask(self) -> u16 { - 1 << self as u16 - } -} - -#[repr(u16)] -#[derive(Clone, Copy)] -pub enum KeyUsageAlgorithm { - //single mask algorithms (symmetric) - AesKw = KeyUsage::WrapKey.mask() | KeyUsage::UnwrapKey.mask(), - //all non-KW AES - Symmetric = (KeyUsage::Encrypt.mask()) - | (KeyUsage::Decrypt.mask()) - | (KeyUsage::WrapKey.mask()) - | (KeyUsage::UnwrapKey.mask()), - - Hmac = (KeyUsage::Sign.mask()) | (KeyUsage::Verify.mask()), - - // asymmetric derive algorithms - use high bits as private usages - // ECDH/X25519 - DeriveAsymmetric = ((KeyUsage::DeriveKey.mask() | KeyUsage::DeriveBits.mask()) << 8), - - // HKDF/PBKDF2 - DeriveSymmetric = KeyUsage::DeriveKey.mask() | KeyUsage::DeriveBits.mask(), - - RsaOaep = ((KeyUsage::Decrypt.mask() | KeyUsage::UnwrapKey.mask()) << 8) //private - | KeyUsage::Encrypt.mask() | KeyUsage::WrapKey.mask(), //public - - //ECDSA, ED25519, all non-OEAP RSA - Sign = (KeyUsage::Sign.mask() << 8) //private - | KeyUsage::Verify.mask(), //public -} -impl KeyUsageAlgorithm { - fn masks(&self) -> (u16, u16) { - let value = *self as u16; - let private_mask = value >> 8; - let public_mask = value & 0xFF; - (private_mask, public_mask) - } - - fn requires_non_empty_usages(self) -> bool { - matches!( - self, - Self::Symmetric - | Self::AesKw - | Self::Hmac - | Self::DeriveAsymmetric - | Self::DeriveSymmetric - | Self::Sign - | Self::RsaOaep - ) - } -} - -#[derive(Debug, Clone)] -pub enum KeyDerivation { - Hkdf { - hash: HashAlgorithm, - salt: Box<[u8]>, - info: Box<[u8]>, - }, - Pbkdf2 { - hash: HashAlgorithm, - salt: Box<[u8]>, - iterations: u32, - }, -} - -impl KeyDerivation { - pub fn for_hkdf_object<'js>(ctx: &Ctx<'js>, obj: Object<'js>) -> Result { - let hash = extract_sha_hash(ctx, &obj)?; - - let salt = obj - .get_required::<_, ObjectBytes>("salt", "algorithm")? - .into_bytes(ctx)? - .into_boxed_slice(); - - let info = obj - .get_required::<_, ObjectBytes>("info", "algorithm")? - .into_bytes(ctx)? - .into_boxed_slice(); - - Ok(KeyDerivation::Hkdf { hash, salt, info }) - } - - pub fn for_pbkf2_object<'js>(ctx: &&Ctx<'js>, obj: Object<'js>) -> Result { - let hash = extract_sha_hash(ctx, &obj)?; - - let salt = obj - .get_required::<_, ObjectBytes>("salt", "algorithm")? - .into_bytes(ctx)? - .into_boxed_slice(); - - let iterations = obj.get_required("iterations", "algorithm")?; - Ok(KeyDerivation::Pbkdf2 { - hash, - salt, - iterations, - }) - } -} - -#[derive(Debug, Clone)] -pub enum EcAlgorithm { - Ecdh, - Ecdsa, -} - -#[derive(PartialEq, Debug, Clone)] -pub enum AesAlgorithm { - Cbc, - Ctr, - Gcm, - Kw, -} - -#[derive(Debug, Clone)] -pub enum KeyAlgorithm { - Aes { - length: u16, - algorithm: AesAlgorithm, - }, - Ec { - curve: EllipticCurve, - algorithm: EcAlgorithm, - }, - X25519, - Ed25519, - Hmac { - hash: HashAlgorithm, - length: u16, - }, - Rsa { - modulus_length: u32, - public_exponent: Rc>, - hash: HashAlgorithm, - }, - Derive(KeyDerivation), - HkdfImport, - Pbkdf2Import, -} - -pub enum KeyFormat { - Jwk, - Raw, - Spki, - Pkcs8, -} - -str_enum!(KeyFormat, Jwk => "jwk", Raw => "raw", Spki => "spki", Pkcs8 => "pkcs8"); - -impl<'js> FromJs<'js> for KeyFormat { - fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { - if let Some(string) = value.as_string() { - let string = string.to_string()?; - match string.as_str() { - "jwk" => return Ok(KeyFormat::Jwk), - "raw" => return Ok(KeyFormat::Raw), - "spki" => return Ok(KeyFormat::Spki), - "pkcs8" => return Ok(KeyFormat::Pkcs8), - _ => {} - }; - } - Err(DOMException::not_supported_error( - ctx, - "Key import/export format must be 'jwk','raw','spki' or 'pkcs8'", - )) - } -} - -#[derive(PartialEq)] -pub enum KeyFormatData<'js> { - Jwk(Object<'js>), - Raw(ObjectBytes<'js>), - Spki(ObjectBytes<'js>), - Pkcs8(ObjectBytes<'js>), -} - -#[derive(PartialEq)] -pub enum KeyAlgorithmMode<'a, 'js> { - Import { - format: KeyFormatData<'js>, - kind: &'a mut KeyKind, - data: &'a mut Vec, - }, - Generate, - Derive, -} - -pub struct KeyAlgorithmWithUsages { - pub name: String, - pub algorithm: KeyAlgorithm, - pub public_usages: Vec, - pub private_usages: Vec, -} - -fn from_ed25519<'js>( - ctx: &Ctx<'js>, - mode: KeyAlgorithmMode<'_, 'js>, - algorithm_name: &str, - usages: &Array<'js>, - private_usages: &mut Vec, - public_usages: &mut Vec, -) -> Result { - #[cfg(all())] - #[inline] - fn import<'js>( - ctx: &Ctx<'js>, - mode: KeyAlgorithmMode<'_, 'js>, - algorithm_name: &str, - ) -> Result> { - if let KeyAlgorithmMode::Import { format, kind, data } = mode { - import_okp_key( - ctx, - format, - kind, - data, - const_oid::db::rfc8410::ID_ED_25519, - algorithm_name, - true, - )?; - Ok(Some(*kind)) - } else { - Ok(None) - } - } - - #[cfg(not(all()))] - #[inline] - fn import<'js>( - _ctx: &Ctx<'js>, - _mode: KeyAlgorithmMode<'_, 'js>, - _algorithm_name: &str, - ) -> Result> { - Ok(None) - } - - let key_kind = import(ctx, mode, algorithm_name)?; - KeyUsage::classify_and_check_usages( - ctx, - KeyUsageAlgorithm::Sign, - usages, - private_usages, - public_usages, - key_kind.as_ref(), - )?; - Ok(KeyAlgorithm::Ed25519) -} - -fn from_x25519<'js>( - ctx: &Ctx<'js>, - mode: KeyAlgorithmMode<'_, 'js>, - algorithm_name: &str, - usages: &Array<'js>, - private_usages: &mut Vec, - public_usages: &mut Vec, -) -> Result { - #[cfg(all())] - #[inline] - fn import<'js>( - ctx: &Ctx<'js>, - mode: KeyAlgorithmMode<'_, 'js>, - algorithm_name: &str, - ) -> Result> { - if let KeyAlgorithmMode::Import { format, kind, data } = mode { - import_okp_key( - ctx, - format, - kind, - data, - const_oid::db::rfc8410::ID_X_25519, - algorithm_name, - false, - )?; - Ok(Some(*kind)) - } else { - Ok(None) - } - } - - #[cfg(not(all()))] - #[inline] - fn import<'js>( - _ctx: &Ctx<'js>, - _mode: KeyAlgorithmMode<'_, 'js>, - _algorithm_name: &str, - ) -> Result> { - Ok(None) - } - - let key_kind = import(ctx, mode, algorithm_name)?; - KeyUsage::classify_and_check_usages( - ctx, - KeyUsageAlgorithm::DeriveAsymmetric, - usages, - private_usages, - public_usages, - key_kind.as_ref(), - )?; - Ok(KeyAlgorithm::X25519) -} - -fn from_aes<'js>( - ctx: &Ctx<'js>, - mode: KeyAlgorithmMode<'_, 'js>, - obj: Result>, - algorithm_name: &str, - usages: &Array<'js>, - private_usages: &mut Vec, - public_usages: &mut Vec, -) -> Result { - #[cfg(all())] - #[inline] - fn import<'js>( - ctx: &Ctx<'js>, - mode: KeyAlgorithmMode<'_, 'js>, - obj: Result>, - algorithm_name: &str, - ) -> Result<(u16, Option)> { - if let KeyAlgorithmMode::Import { data, format, kind } = mode { - let length = - import_symmetric_key(ctx, format, kind, data, algorithm_name, None)? as u16; - Ok((length, Some(*kind))) - } else { - let length: u16 = obj?.get_required("length", "algorithm")?; - Ok((length, None)) - } - } - - #[cfg(not(all()))] - #[inline] - fn import<'js>( - _ctx: &Ctx<'js>, - _mode: KeyAlgorithmMode<'_, 'js>, - obj: Result>, - _algorithm_name: &str, - ) -> Result<(u16, Option)> { - let length: u16 = obj?.get_required("length", "algorithm")?; - Ok((length, None)) - } - - let (length, key_kind) = import(ctx, mode, obj, algorithm_name)?; - - if !matches!(length, 128 | 192 | 256) { - return Err(DOMException::operation_error( - ctx, - format!( - "Algorithm 'length' must be one of: 128, 192, or 256 = {}", - length - ), - )); - } - - let algorithm = match algorithm_name { - "AES-CBC" => AesAlgorithm::Cbc, - "AES-CTR" => AesAlgorithm::Ctr, - "AES-GCM" => AesAlgorithm::Gcm, - "AES-KW" => AesAlgorithm::Kw, - _ => return Err(DOMException::operation_error(ctx, "Invalid algorithm name")), - }; - - KeyUsage::classify_and_check_usages( - ctx, - if algorithm == AesAlgorithm::Kw { - KeyUsageAlgorithm::AesKw - } else { - KeyUsageAlgorithm::Symmetric - }, - usages, - private_usages, - public_usages, - key_kind.as_ref(), - )?; - - Ok(KeyAlgorithm::Aes { length, algorithm }) -} - -fn from_hmac<'js>( - ctx: &Ctx<'js>, - mode: KeyAlgorithmMode<'_, 'js>, - obj: Result>, - algorithm_name: &str, - usages: &Array<'js>, - private_usages: &mut Vec, - public_usages: &mut Vec, -) -> Result { - let obj = obj?; - let hash = extract_sha_hash(ctx, &obj)?; - if !matches!( - hash, - HashAlgorithm::Sha1 | HashAlgorithm::Sha256 | HashAlgorithm::Sha384 | HashAlgorithm::Sha512 - ) { - return Err(DOMException::not_supported_error( - ctx, - "Unsupported HMAC hash algorithm", - )); - } - let mut length = match obj.get_optional::<_, u16>("length")? { - Some(length) => length, - None => match mode { - KeyAlgorithmMode::Import { .. } => 0, - _ => (hash.block_len() * 8) as u16, - }, - }; - - #[cfg(all())] - #[inline] - fn import<'js>( - ctx: &Ctx<'js>, - mode: KeyAlgorithmMode<'_, 'js>, - algorithm_name: &str, - hash: &HashAlgorithm, - length: &mut u16, - ) -> Result> { - if let KeyAlgorithmMode::Import { data, format, kind } = mode { - let data_length = - import_symmetric_key(ctx, format, kind, data, algorithm_name, Some(hash))?; - if *length == 0 { - *length = data_length as u16; - } - Ok(Some(*kind)) - } else { - Ok(None) - } - } - - #[cfg(not(all()))] - #[inline] - fn import<'js>( - _ctx: &Ctx<'js>, - _mode: KeyAlgorithmMode<'_, 'js>, - _algorithm_name: &str, - _hash: &HashAlgorithm, - _length: &mut u16, - ) -> Result> { - Ok(None) - } - - let key_kind = import(ctx, mode, algorithm_name, &hash, &mut length)?; - - KeyUsage::classify_and_check_usages( - ctx, - KeyUsageAlgorithm::Hmac, - usages, - private_usages, - public_usages, - key_kind.as_ref(), - )?; - - Ok(KeyAlgorithm::Hmac { hash, length }) -} - -fn from_rsa<'js>( - ctx: &Ctx<'js>, - mode: KeyAlgorithmMode<'_, 'js>, - obj: Result>, - algorithm_name: &str, - usages: &Array<'js>, - private_usages: &mut Vec, - public_usages: &mut Vec, -) -> Result { - let obj = obj?; - let hash = extract_sha_hash(ctx, &obj)?; - let is_generate = mode == KeyAlgorithmMode::Generate; - - #[cfg(all())] - #[inline] - fn import<'js>( - ctx: &Ctx<'js>, - mode: KeyAlgorithmMode<'_, 'js>, - obj: &Object<'js>, - algorithm_name: &str, - hash: &HashAlgorithm, - ) -> Result<(u32, Box<[u8]>, Option)> { - if let KeyAlgorithmMode::Import { format, kind, data } = mode { - let (mod_length, exp) = import_rsa_key(ctx, format, kind, data, algorithm_name, hash)?; - Ok((mod_length, exp, Some(*kind))) - } else { - let modulus_length = obj.get_required("modulusLength", "algorithm")?; - let public_exponent: TypedArray = - obj.get_required("publicExponent", "algorithm")?; - let public_exponent = public_exponent - .as_bytes() - .ok_or_else(|| { - DOMException::not_supported_error(ctx, "Array buffer has been detached") - })? - .to_owned() - .into_boxed_slice(); - Ok((modulus_length, public_exponent, None)) - } - } - - #[cfg(not(all()))] - #[inline] - fn import<'js>( - ctx: &Ctx<'js>, - _mode: KeyAlgorithmMode<'_, 'js>, - obj: &Object<'js>, - _algorithm_name: &str, - _hash: &HashAlgorithm, - ) -> Result<(u32, Box<[u8]>, Option)> { - let modulus_length = obj.get_required("modulusLength", "algorithm")?; - let public_exponent: TypedArray = obj.get_required("publicExponent", "algorithm")?; - let public_exponent = public_exponent - .as_bytes() - .ok_or_else(|| { - DOMException::not_supported_error(ctx, "Array buffer has been detached") - })? - .to_owned() - .into_boxed_slice(); - Ok((modulus_length, public_exponent, None)) - } - - let (modulus_length, public_exponent, key_kind) = - import(ctx, mode, &obj, algorithm_name, &hash)?; - - if is_generate { - parse_rsa_public_exponent(&public_exponent).or_throw_dom(ctx)?; - } - - KeyUsage::classify_and_check_usages( - ctx, - if algorithm_name == "RSA-OAEP" { - KeyUsageAlgorithm::RsaOaep - } else { - KeyUsageAlgorithm::Sign - }, - usages, - private_usages, - public_usages, - key_kind.as_ref(), - )?; - - Ok(KeyAlgorithm::Rsa { - modulus_length, - public_exponent: Rc::new(public_exponent), - hash, - }) -} - -fn from_hkdf<'js>( - ctx: &Ctx<'js>, - mode: KeyAlgorithmMode<'_, 'js>, - obj: Result>, - algorithm_name: &str, - usages: &Array<'js>, - private_usages: &mut Vec, - public_usages: &mut Vec, -) -> Result { - #[cfg(all())] - #[inline] - fn import<'js>( - ctx: &Ctx<'js>, - mode: KeyAlgorithmMode<'_, 'js>, - obj: Result>, - algorithm_name: &str, - ) -> Result<(KeyAlgorithm, Option)> { - match mode { - KeyAlgorithmMode::Import { format, kind, data } => { - import_derive_key(ctx, format, kind, data, algorithm_name)?; - Ok((KeyAlgorithm::HkdfImport, Some(*kind))) - } - KeyAlgorithmMode::Derive => { - let obj = obj?; - Ok(( - KeyAlgorithm::Derive(KeyDerivation::for_hkdf_object(ctx, obj)?), - None, - )) - } - _ => algorithm_not_supported_error(ctx), - } - } - - #[cfg(not(all()))] - #[inline] - fn import<'js>( - ctx: &Ctx<'js>, - mode: KeyAlgorithmMode<'_, 'js>, - obj: Result>, - _algorithm_name: &str, - ) -> Result<(KeyAlgorithm, Option)> { - match mode { - KeyAlgorithmMode::Derive => { - let obj = obj?; - Ok(( - KeyAlgorithm::Derive(KeyDerivation::for_hkdf_object(ctx, obj)?), - None, - )) - } - _ => algorithm_not_supported_error(ctx), - } - } - - let (algorithm, key_kind) = import(ctx, mode, obj, algorithm_name)?; - - KeyUsage::classify_and_check_usages( - ctx, - KeyUsageAlgorithm::DeriveSymmetric, - usages, - private_usages, - public_usages, - key_kind.as_ref(), - )?; - - Ok(algorithm) -} - -fn from_pbkdf2<'js>( - ctx: &Ctx<'js>, - mode: KeyAlgorithmMode<'_, 'js>, - obj: Result>, - algorithm_name: &str, - usages: &Array<'js>, - private_usages: &mut Vec, - public_usages: &mut Vec, -) -> Result { - #[cfg(all())] - #[inline] - fn import<'js>( - ctx: &Ctx<'js>, - mode: KeyAlgorithmMode<'_, 'js>, - obj: Result>, - algorithm_name: &str, - ) -> Result<(KeyAlgorithm, Option)> { - match mode { - KeyAlgorithmMode::Import { format, kind, data } => { - import_derive_key(ctx, format, kind, data, algorithm_name)?; - Ok((KeyAlgorithm::Pbkdf2Import, Some(*kind))) - } - KeyAlgorithmMode::Derive => { - let obj = obj?; - Ok(( - KeyAlgorithm::Derive(KeyDerivation::for_pbkf2_object(&ctx, obj)?), - None, - )) - } - _ => algorithm_not_supported_error(ctx), - } - } - - #[cfg(not(all()))] - #[inline] - fn import<'js>( - ctx: &Ctx<'js>, - mode: KeyAlgorithmMode<'_, 'js>, - obj: Result>, - _algorithm_name: &str, - ) -> Result<(KeyAlgorithm, Option)> { - match mode { - KeyAlgorithmMode::Derive => { - let obj = obj?; - Ok(( - KeyAlgorithm::Derive(KeyDerivation::for_pbkf2_object(&ctx, obj)?), - None, - )) - } - _ => algorithm_not_supported_error(ctx), - } - } - - let (algorithm, key_kind) = import(ctx, mode, obj, algorithm_name)?; - - KeyUsage::classify_and_check_usages( - ctx, - KeyUsageAlgorithm::DeriveSymmetric, - usages, - private_usages, - public_usages, - key_kind.as_ref(), - )?; - - Ok(algorithm) -} - -impl KeyAlgorithm { - pub fn from_js<'js>( - ctx: &Ctx<'js>, - mode: KeyAlgorithmMode<'_, 'js>, - value: Value<'js>, - usages: Array<'js>, - ) -> Result { - // When _subtle-full is not enabled, Import mode is not supported - #[cfg(not(all()))] - if matches!(mode, KeyAlgorithmMode::Import { .. }) { - return Err(DOMException::not_supported_error( - ctx, - "Key import is not supported with this crypto provider", - )); - } - - let (name, obj) = to_name_and_maybe_object(ctx, value)?; - let name = normalize_algorithm_name(&name); - let mut public_usages = vec![]; - let mut private_usages = vec![]; - let algorithm_name = name.as_ref(); - let algorithm = match algorithm_name { - "Ed25519" => from_ed25519( - ctx, - mode, - algorithm_name, - &usages, - &mut private_usages, - &mut public_usages, - )?, - "X25519" => from_x25519( - ctx, - mode, - algorithm_name, - &usages, - &mut private_usages, - &mut public_usages, - )?, - "AES-CBC" | "AES-CTR" | "AES-GCM" | "AES-KW" => from_aes( - ctx, - mode, - obj, - algorithm_name, - &usages, - &mut private_usages, - &mut public_usages, - )?, - "ECDH" => Self::from_ec( - ctx, - mode, - obj, - algorithm_name, - EcAlgorithm::Ecdh, - &usages, - &mut private_usages, - &mut public_usages, - KeyUsageAlgorithm::DeriveAsymmetric, - )?, - "ECDSA" => Self::from_ec( - ctx, - mode, - obj, - algorithm_name, - EcAlgorithm::Ecdsa, - &usages, - &mut private_usages, - &mut public_usages, - KeyUsageAlgorithm::Sign, - )?, - "HMAC" => from_hmac( - ctx, - mode, - obj, - algorithm_name, - &usages, - &mut private_usages, - &mut public_usages, - )?, - "RSA-OAEP" | "RSA-PSS" | "RSASSA-PKCS1-v1_5" => from_rsa( - ctx, - mode, - obj, - algorithm_name, - &usages, - &mut private_usages, - &mut public_usages, - )?, - "HKDF" => from_hkdf( - ctx, - mode, - obj, - algorithm_name, - &usages, - &mut private_usages, - &mut public_usages, - )?, - "PBKDF2" => from_pbkdf2( - ctx, - mode, - obj, - algorithm_name, - &usages, - &mut private_usages, - &mut public_usages, - )?, - _ => return algorithm_not_supported_error(ctx), - }; - - Ok(KeyAlgorithmWithUsages { - name, - algorithm, - public_usages, - private_usages, - }) - } - - pub fn as_object<'js, T: AsRef>(&self, ctx: &Ctx<'js>, name: T) -> Result> { - let obj = Object::new(ctx.clone())?; - obj.set(PredefinedAtom::Name, name.as_ref())?; - match self { - KeyAlgorithm::Aes { length, .. } => { - obj.set(PredefinedAtom::Length, length)?; - } - KeyAlgorithm::Ec { curve, .. } => { - obj.set("namedCurve", curve.as_str())?; - } - - KeyAlgorithm::Hmac { hash, length } => { - let hash_obj = create_hash_object(ctx, hash)?; - obj.set("hash", hash_obj)?; - - obj.set(PredefinedAtom::Length, length)?; - } - KeyAlgorithm::Rsa { - modulus_length, - public_exponent, - hash, - } => { - let public_exponent = public_exponent.as_ref().to_vec(); - let array = TypedArray::new(ctx.clone(), public_exponent)?; - - let hash_obj = create_hash_object(ctx, hash)?; - obj.set("hash", hash_obj)?; - - obj.set("modulusLength", modulus_length)?; - obj.set("publicExponent", array)?; - } - KeyAlgorithm::Derive(KeyDerivation::Hkdf { hash, salt, info }) => { - let salt = TypedArray::::new(ctx.clone(), salt.to_vec())?; - let info = TypedArray::::new(ctx.clone(), info.to_vec())?; - - obj.set("hash", hash.as_str())?; - obj.set("salt", salt)?; - obj.set("info", info)?; - } - KeyAlgorithm::Derive(KeyDerivation::Pbkdf2 { - hash, - salt, - iterations, - }) => { - let salt = TypedArray::::new(ctx.clone(), salt.to_vec())?; - obj.set("hash", hash.as_str())?; - obj.set("salt", salt)?; - obj.set("iterations", iterations)?; - } - _ => {} - }; - Ok(obj) - } - - #[allow(clippy::too_many_arguments)] - fn from_ec<'js>( - ctx: &Ctx<'js>, - #[allow(unused_variables)] mode: KeyAlgorithmMode<'_, 'js>, - obj: Result>, - #[allow(unused_variables)] algorithm_name: &str, - algorithm: EcAlgorithm, - key_usages: &Array<'js>, - private_usages: &mut Vec, - public_usages: &mut Vec, - key_usage_algorithm: KeyUsageAlgorithm, - ) -> Result { - let obj = obj?; - let curve_name: String = obj.get_required("namedCurve", "algorithm")?; - let curve = EllipticCurve::try_from(curve_name.as_str()) - .map_err(NotSupportedError) - .or_throw_dom(ctx)?; - - #[cfg(all())] - let key_kind = if let KeyAlgorithmMode::Import { format, kind, data } = mode { - import_ec_key(ctx, format, kind, data, algorithm_name, &curve, &curve_name)?; - Some(kind) - } else { - None - }; - #[cfg(not(all()))] - let key_kind: Option<&KeyKind> = None; - - KeyUsage::classify_and_check_usages( - ctx, - key_usage_algorithm, - key_usages, - private_usages, - public_usages, - key_kind.as_deref(), - )?; - - Ok(KeyAlgorithm::Ec { curve, algorithm }) - } -} - -#[cfg(all())] -fn import_derive_key<'js>( - ctx: &Ctx<'js>, - format: KeyFormatData<'js>, - kind: &mut KeyKind, - data: &mut Vec, - algorithm_name: &str, -) -> Result<()> { - if let KeyFormatData::Raw(object_bytes) = format { - *data = object_bytes.into_bytes(ctx)?; - *kind = KeyKind::Secret; - } else { - return Err(DOMException::not_supported_error( - ctx, - [algorithm_name, " only supports 'raw' import format"].concat(), - )); - } - - Ok(()) -} - -#[cfg(all())] -fn import_rsa_key<'js>( - ctx: &Ctx<'js>, - format: KeyFormatData<'js>, - kind: &mut KeyKind, - data: &mut Vec, - algorithm_name: &str, - hash: &HashAlgorithm, -) -> Result<(u32, Box<[u8]>)> { - use crate::llrt_crypto::{ - provider::{CryptoProvider, RsaJwkImport}, - CRYPTO_PROVIDER, - }; - - let validate_oid = |other_oid: const_oid::ObjectIdentifier| -> Result<()> { - if other_oid != const_oid::db::rfc5912::RSA_ENCRYPTION { - return algorithm_mismatch_error(ctx, algorithm_name); - } - Ok(()) - }; - - let (modulus_length, public_exponent) = match format { - KeyFormatData::Jwk(object) => { - validate_jwk_kty(ctx, &object, "RSA")?; - - if let Some(alg) = object.get_optional::<_, String>("alg")? { - let numeric_hash_str = match algorithm_name { - "RSASSA-PKCS1-v1_5" => alg.strip_prefix("RS"), - "RSA-PSS" => alg.strip_prefix("PS"), - "RSA-OAEP" => alg.strip_prefix("RSA-OAEP-"), - _ => None, - }; - let Some(numeric_hash_str) = numeric_hash_str else { - return algorithm_mismatch_error(ctx, algorithm_name); - }; - if numeric_hash_str != hash.as_numeric_str() { - return hash_mismatch_error(ctx, hash); - } - } - - let n_bytes = get_jwk_required_bytes(ctx, &object, "n")?; - let e_bytes = get_jwk_required_bytes(ctx, &object, "e")?; - - let d_bytes = get_jwk_optional_bytes(ctx, &object, "d")?; - - let result = if let Some(ref d_bytes) = d_bytes { - let p_bytes = get_jwk_required_bytes(ctx, &object, "p")?; - let q_bytes = get_jwk_required_bytes(ctx, &object, "q")?; - let dp_bytes = get_jwk_required_bytes(ctx, &object, "dp")?; - let dq_bytes = get_jwk_required_bytes(ctx, &object, "dq")?; - let qi_bytes = get_jwk_required_bytes(ctx, &object, "qi")?; - - let jwk = RsaJwkImport { - n: &n_bytes, - e: &e_bytes, - d: Some(d_bytes), - p: Some(&p_bytes), - q: Some(&q_bytes), - dp: Some(&dp_bytes), - dq: Some(&dq_bytes), - qi: Some(&qi_bytes), - }; - CRYPTO_PROVIDER.import_rsa_jwk(jwk).or_throw_dom(ctx)? - } else { - let jwk = RsaJwkImport { - n: &n_bytes, - e: &e_bytes, - d: None, - p: None, - q: None, - dp: None, - dq: None, - qi: None, - }; - CRYPTO_PROVIDER.import_rsa_jwk(jwk).or_throw_dom(ctx)? - }; - - *data = result.key_data; - *kind = if result.is_private { - KeyKind::Private - } else { - KeyKind::Public - }; - (result.modulus_length as usize, result.public_exponent) - } - KeyFormatData::Raw(object_bytes) => { - let result = CRYPTO_PROVIDER - .import_rsa_public_key_pkcs1(object_bytes.as_bytes(ctx)?) - .or_throw_dom(ctx)?; - *data = result.key_data; - *kind = KeyKind::Public; - (result.modulus_length as usize, result.public_exponent) - } - KeyFormatData::Pkcs8(object_bytes) => { - let pk_info = PrivateKeyInfoRef::from_der(object_bytes.as_bytes(ctx)?).or_throw(ctx)?; - validate_oid(pk_info.algorithm.oid)?; - let result = CRYPTO_PROVIDER - .import_rsa_private_key_pkcs8(object_bytes.as_bytes(ctx)?) - .or_throw_dom(ctx)?; - *data = result.key_data; - *kind = KeyKind::Private; - (result.modulus_length as usize, result.public_exponent) - } - KeyFormatData::Spki(object_bytes) => { - let pk_info = spki::SubjectPublicKeyInfoRef::try_from(object_bytes.as_bytes(ctx)?) - .or_throw(ctx)?; - validate_oid(pk_info.algorithm.oid)?; - let result = CRYPTO_PROVIDER - .import_rsa_public_key_spki(object_bytes.as_bytes(ctx)?) - .or_throw_dom(ctx)?; - *data = result.key_data; - *kind = KeyKind::Public; - (result.modulus_length as usize, result.public_exponent) - } - }; - - let public_exponent = public_exponent.into_boxed_slice(); - Ok((modulus_length as u32, public_exponent)) -} - -#[cfg(all())] -fn import_symmetric_key<'js>( - ctx: &Ctx<'js>, - format: KeyFormatData<'js>, - kind: &mut KeyKind, - data: &mut Vec, - algorithm_name: &str, - hash: Option<&HashAlgorithm>, -) -> Result { - *kind = KeyKind::Secret; - - match format { - KeyFormatData::Jwk(object) => { - validate_jwk_kty(ctx, &object, "oct")?; - - let k: String = get_jwk_required_string(ctx, &object, "k")?; - let alg: String = get_jwk_required_string(ctx, &object, "alg")?; - - let prefix = &alg[..1]; - - match (prefix, hash) { - //HMAC - HS256, HS512 etc - ("H", Some(hash)) => { - if &alg[2..] != hash.as_numeric_str() { - return hash_mismatch_error(ctx, hash); - } - } - //AES - A256KW, A256GCM, A256CRT, A512CBC etc - ("A", None) => { - //extract AES-{suffix} - let aes_variant = &alg[4..]; - - if !algorithm_name.ends_with(aes_variant) { - return algorithm_mismatch_error(ctx, algorithm_name); - } - } - _ => return algorithm_mismatch_error(ctx, algorithm_name), - } - - *data = bytes_from_b64_url_safe(k.as_bytes()).or_throw(ctx)?; - Ok(data.len() * 8) - } - KeyFormatData::Raw(object_bytes) => { - let bytes = object_bytes.into_bytes(ctx)?; - - *data = bytes; - Ok(data.len() * 8) - } - _ => algorithm_mismatch_error(ctx, algorithm_name), - } -} - -// EC algorithm OID for validation -#[cfg(all())] -const EC_ALGORITHM_OID: const_oid::ObjectIdentifier = - const_oid::ObjectIdentifier::new_unwrap("1.2.840.10045.2.1"); - -#[cfg(all())] -fn import_ec_key<'js>( - ctx: &Ctx<'js>, - format: KeyFormatData<'js>, - kind: &mut KeyKind, - data: &mut Vec, - algorithm_name: &str, - curve: &EllipticCurve, - curve_name: &str, -) -> Result<()> { - use crate::llrt_crypto::{ - provider::{CryptoProvider, EcJwkImport}, - CRYPTO_PROVIDER, - }; - - let validate_oid = |other_oid: const_oid::ObjectIdentifier| -> Result<()> { - if other_oid != EC_ALGORITHM_OID { - return algorithm_mismatch_error(ctx, algorithm_name); - } - Ok(()) - }; - - // Get expected coordinate length for the curve - let coord_len = match curve { - EllipticCurve::P256 => 32, - EllipticCurve::P384 => 48, - EllipticCurve::P521 => 66, - }; - - match format { - KeyFormatData::Jwk(object) => { - validate_jwk_kty(ctx, &object, "EC")?; - - validate_jwk_use(ctx, &object, true)?; - - validate_jwk_crv(ctx, &object, curve_name)?; - - let x_bytes = get_jwk_required_bytes(ctx, &object, "x")?; - validate_jwk_bytes_len(ctx, algorithm_name, "x coordinate", &x_bytes, coord_len)?; - - let y_bytes = get_jwk_required_bytes(ctx, &object, "y")?; - validate_jwk_bytes_len(ctx, algorithm_name, "y coordinate", &y_bytes, coord_len)?; - - let d_bytes = get_jwk_optional_bytes(ctx, &object, "d")?; - - if let Some(ref d_bytes) = d_bytes { - validate_jwk_bytes_len(ctx, algorithm_name, "private key", d_bytes, coord_len)?; - } - - let jwk = EcJwkImport { - x: &x_bytes, - y: &y_bytes, - d: d_bytes.as_deref(), - }; - - let result = CRYPTO_PROVIDER - .import_ec_jwk(jwk, *curve) - .or_throw_dom(ctx)?; - *data = result.key_data; - *kind = if result.is_private { - KeyKind::Private - } else { - KeyKind::Public - }; - } - KeyFormatData::Raw(object_bytes) => { - let bytes = object_bytes.as_bytes(ctx)?; - let result = CRYPTO_PROVIDER - .import_ec_public_key_sec1(bytes, *curve) - .or_throw_dom(ctx)?; - *data = result.key_data; - *kind = KeyKind::Public; - } - KeyFormatData::Spki(object_bytes) => { - let spki = spki::SubjectPublicKeyInfoRef::try_from(object_bytes.as_bytes(ctx)?) - .or_throw_data_error(ctx)?; - validate_oid(spki.algorithm.oid)?; - let result = CRYPTO_PROVIDER - .import_ec_public_key_spki(object_bytes.as_bytes(ctx)?, *curve) - .or_throw_dom(ctx)?; - *data = result.key_data; - *kind = KeyKind::Public; - } - KeyFormatData::Pkcs8(object_bytes) => { - let pkcs8 = PrivateKeyInfoRef::try_from(object_bytes.as_bytes(ctx)?) - .or_throw_data_error(ctx)?; - validate_oid(pkcs8.algorithm.oid)?; - let result = CRYPTO_PROVIDER - .import_ec_private_key_pkcs8(object_bytes.as_bytes(ctx)?) - .or_throw_dom(ctx)?; - *data = result.key_data; - *kind = KeyKind::Private; - } - }; - Ok(()) -} - -#[cfg(all())] -fn import_okp_key<'js>( - ctx: &Ctx<'js>, - format: KeyFormatData<'js>, - kind: &mut KeyKind, - data: &mut Vec, - oid: ObjectIdentifier, - algorithm_name: &str, - is_ed25519: bool, -) -> Result<()> { - let validate_oid = |other_oid: const_oid::ObjectIdentifier| -> Result<()> { - if other_oid != oid { - return algorithm_mismatch_error(ctx, algorithm_name); - } - Ok(()) - }; - - match format { - KeyFormatData::Jwk(object) => { - validate_jwk_kty(ctx, &object, "OKP")?; - - validate_jwk_crv(ctx, &object, algorithm_name)?; - - if is_ed25519 { - validate_jwk_alg(ctx, &object)?; - } - - validate_jwk_use(ctx, &object, is_ed25519)?; - - let public_key = get_jwk_required_bytes(ctx, &object, "x")?; - validate_jwk_bytes_len(ctx, algorithm_name, "public key", &public_key, 32)?; - - let private_key = get_jwk_optional_bytes(ctx, &object, "d")?; - - if let Some(private_key) = private_key { - validate_jwk_bytes_len(ctx, algorithm_name, "private key", &private_key, 32)?; - - validate_okp_jwk_key_pair(ctx, &private_key, &public_key, is_ed25519)?; - - if is_ed25519 { - // Ed25519 internal representation is the complete PKCS#8 DER. - let inner = OctetStringRef::new(private_key.as_slice()).or_throw(ctx)?; - let inner_der = inner.to_der().or_throw(ctx)?; - let pk_info = PrivateKeyInfoRef { - algorithm: AlgorithmIdentifier { - oid, - parameters: None, - }, - private_key: OctetStringRef::new(&inner_der).or_throw(ctx)?, - public_key: Some(BitStringRef::from_bytes(&public_key).or_throw(ctx)?), - }; - *data = pk_info.to_der().or_throw(ctx)?; - } else { - // X25519 internal representation is raw 32-byte scalar. - *data = private_key; - } - *kind = KeyKind::Private; - } else { - *data = public_key; - *kind = KeyKind::Public; - } - } - KeyFormatData::Raw(object_bytes) => { - let bytes = object_bytes.into_bytes(ctx)?; - if bytes.len() != 32 { - return Err(DOMException::data_error( - ctx, - [algorithm_name, " keys must be 32 bytes long"].concat(), - )); - } - *data = bytes; - *kind = KeyKind::Public; - } - KeyFormatData::Spki(object_bytes) => { - let spki = spki::SubjectPublicKeyInfoRef::try_from(object_bytes.as_bytes(ctx)?) - .or_throw_data_error(ctx)?; - validate_oid(spki.algorithm.oid)?; - - let public_key = spki.subject_public_key.raw_bytes(); - if public_key.len() != 32 { - return Err(DOMException::data_error( - ctx, - [algorithm_name, " public key must be 32 bytes"].concat(), - )); - } - - *data = public_key.to_vec(); - *kind = KeyKind::Public; - } - KeyFormatData::Pkcs8(object_bytes) => { - let bytes = object_bytes.into_bytes(ctx)?; - let pkcs8 = PrivateKeyInfoRef::try_from(bytes.as_slice()).or_throw_data_error(ctx)?; - validate_oid(pkcs8.algorithm.oid)?; - if is_ed25519 { - // Ed25519 internal representation is the complete PKCS#8 DER. - *data = bytes; - } else { - // X25519 internal representation is the inner OCTET STRING. - *data = OctetString::from_der(pkcs8.private_key.as_bytes()) - .or_throw(ctx)? - .as_bytes() - .to_vec(); - if data.len() != 32 { - return Err(DOMException::data_error( - ctx, - [algorithm_name, " private key must be 32 bytes"].concat(), - )); - } - } - *kind = KeyKind::Private; - } - } - - Ok(()) -} - -#[cfg(all())] -fn get_jwk_required_string<'js>( - ctx: &Ctx<'js>, - object: &Object<'js>, - name: &str, -) -> Result { - object - .get_required(name, "keyData") - .or_throw_data_error(ctx) -} - -#[cfg(all())] -fn get_jwk_required_bytes<'js>( - ctx: &Ctx<'js>, - object: &Object<'js>, - name: &str, -) -> Result> { - let value = get_jwk_required_string(ctx, object, name)?; - bytes_from_b64_url_safe(value.as_bytes()).or_throw_data_error(ctx) -} - -#[cfg(all())] -fn get_jwk_optional_bytes<'js>( - ctx: &Ctx<'js>, - object: &Object<'js>, - name: &str, -) -> Result>> { - let value = object.get_optional::<_, String>(name)?; - value - .map(|value| bytes_from_b64_url_safe(value.as_bytes()).or_throw_data_error(ctx)) - .transpose() -} - -#[cfg(all())] -fn validate_jwk_kty<'js>(ctx: &Ctx<'js>, object: &Object<'js>, expected: &str) -> Result<()> { - let kty = get_jwk_required_string(ctx, object, "kty")?; - if kty != expected { - return Err(DOMException::data_error( - ctx, - ["JWK 'kty' parameter must be '", expected, "'"].concat(), - )); - } - Ok(()) -} - -#[cfg(all())] -fn validate_jwk_crv<'js>(ctx: &Ctx<'js>, object: &Object<'js>, expected: &str) -> Result<()> { - let crv = get_jwk_required_string(ctx, object, "crv")?; - if crv != expected { - return Err(DOMException::data_error( - ctx, - ["JWK 'crv' parameter must be '", expected, "'"].concat(), - )); - } - Ok(()) -} - -#[cfg(all())] -fn validate_jwk_use(ctx: &Ctx<'_>, object: &Object<'_>, is_ed25519: bool) -> Result<()> { - if let Some(use_) = object.get_optional::<_, String>("use")? { - let expected = if is_ed25519 { "sig" } else { "enc" }; - if use_ != expected { - return Err(DOMException::data_error( - ctx, - "JWK 'use' parameter is invalid", - )); - } - } - Ok(()) -} - -#[cfg(all())] -fn validate_jwk_alg(ctx: &Ctx<'_>, object: &Object<'_>) -> Result<()> { - if let Some(alg) = object.get_optional::<_, String>("alg")? { - if alg != "Ed25519" && alg != "EdDSA" { - return Err(DOMException::data_error( - ctx, - "JWK 'alg' parameter is invalid", - )); - } - } - Ok(()) -} - -#[cfg(all())] -fn validate_jwk_bytes_len( - ctx: &Ctx<'_>, - algorithm_name: &str, - field: &str, - bytes: &[u8], - expected: usize, -) -> Result<()> { - if bytes.len() != expected { - return Err(DOMException::data_error( - ctx, - [algorithm_name, " JWK ", field, " has invalid length"].concat(), - )); - } - Ok(()) -} - -#[cfg(all())] -fn validate_okp_jwk_key_pair<'js>( - ctx: &Ctx<'js>, - private_key: &[u8], - public_key: &[u8], - is_ed25519: bool, -) -> Result<()> { - let derived_public_key = if is_ed25519 { - let secret_key: [u8; 32] = private_key.try_into().or_throw_data_error(ctx)?; - SigningKey::from_bytes(&secret_key) - .verifying_key() - .to_bytes() - .to_vec() - } else { - let secret_key: [u8; 32] = private_key.try_into().or_throw_data_error(ctx)?; - let secret = StaticSecret::from(secret_key); - PublicKey::from(&secret).as_bytes().to_vec() - }; - if derived_public_key.as_slice() != public_key { - return Err(DOMException::data_error(ctx, "JWK key pair is invalid")); - } - Ok(()) -} - -pub fn extract_sha_hash<'js>(ctx: &Ctx<'js>, obj: &Object<'js>) -> Result { - let hash: Value = obj.get_required("hash", "algorithm")?; - let hash = if let Some(string) = hash.as_string() { - string.to_string() - } else if let Some(obj) = hash.into_object() { - obj.get_required("name", "hash") - } else { - return Err(DOMException::not_supported_error( - ctx, - "hash must be a string or an object", - )); - }?; - let hash = normalize_algorithm_name(&hash); - HashAlgorithm::from_strict_str(hash.as_str()).or_throw_dom(ctx) -} - -fn create_hash_object<'js>(ctx: &Ctx<'js>, hash: &HashAlgorithm) -> Result> { - let hash_obj = Object::new(ctx.clone())?; - hash_obj.set(PredefinedAtom::Name, hash.as_str())?; - Ok(hash_obj) -} - -#[cfg(all())] -pub fn hash_mismatch_error(ctx: &Ctx<'_>, hash: &HashAlgorithm) -> Result { - Err(DOMException::type_mismatch_error( - ctx, - ["Algorithm hash expected to be ", hash.as_str()].concat(), - )) -} - -#[cfg(all())] -trait DataErrorResultExt { - fn or_throw_data_error(self, ctx: &Ctx<'_>) -> Result; -} - -#[cfg(all())] -impl DataErrorResultExt for std::result::Result -where - E: std::fmt::Display, -{ - fn or_throw_data_error(self, ctx: &Ctx<'_>) -> Result { - self.map_err(|e| DataError(e.to_string())).or_throw_dom(ctx) - } -} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/mod.rs b/stdlib/src/llrt/llrt_crypto/subtle/mod.rs deleted file mode 100644 index 42eb6e9a..00000000 --- a/stdlib/src/llrt/llrt_crypto/subtle/mod.rs +++ /dev/null @@ -1,183 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -mod crypto_key; -mod derive_algorithm; -mod derive_bits; -mod derive_keys; -mod digest; -mod encryption; -mod encryption_algorithm; -#[cfg(all())] -mod export_key; -mod generate_key; -#[cfg(all())] -mod import_key; -#[cfg(all())] -mod key_algorithm; -mod sign; -mod sign_algorithm; -mod util; -mod verify; -#[cfg(all())] -mod wrapping; - -pub use crypto_key::CryptoKey; -pub use derive_bits::subtle_derive_bits; -pub use derive_keys::subtle_derive_key; -pub use digest::subtle_digest; -pub use encryption::subtle_decrypt; -pub use encryption::subtle_encrypt; -#[cfg(all())] -pub use export_key::subtle_export_key; -pub use generate_key::subtle_generate_key; -#[cfg(all())] -pub use import_key::subtle_import_key; -#[cfg(all())] -use key_algorithm::KeyAlgorithm; -pub use sign::subtle_sign; -pub use verify::subtle_verify; -#[cfg(all())] -pub use wrapping::subtle_unwrap_key; -#[cfg(all())] -pub use wrapping::subtle_wrap_key; - -// Stub implementations for limited crypto providers (no _subtle-full) -#[cfg(not(all()))] -mod key_algorithm; -#[cfg(not(all()))] -use key_algorithm::KeyAlgorithm; - -use crate::llrt_exceptions::DOMException; -use crate::llrt_utils::{object::ObjectExt, str_enum}; -use rquickjs::{atom::PredefinedAtom, Ctx, Error, Exception, Object, Result, Value}; - -use crate::llrt_crypto::provider::{CryptoProvider, SimpleDigest}; - -use crate::llrt_crypto::hash::HashAlgorithm; - -#[rquickjs::class] -#[derive(rquickjs::JsLifetime, rquickjs::class::Trace)] -pub struct SubtleCrypto {} - -#[rquickjs::methods] -impl SubtleCrypto { - #[qjs(constructor)] - pub fn new(ctx: Ctx<'_>) -> Result { - Err(Exception::throw_type(&ctx, "Illegal constructor")) - } - - #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] - pub fn to_string_tag() -> &'static str { - stringify!(SubtleCrypto) - } -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum EllipticCurve { - P256, - P384, - P521, -} - -str_enum!(EllipticCurve,P256 => "P-256", P384 => "P-384", P521 => "P-521"); - -pub enum EncryptionMode { - Encryption, - #[allow(dead_code)] - Wrapping(u8), //padding byte -} - -pub fn rsa_hash_digest<'a>( - ctx: &Ctx<'_>, - key: &'a CryptoKey, - data: &'a [u8], - algorithm_name: &str, -) -> Result<(&'a HashAlgorithm, Vec)> { - let hash = match &key.algorithm { - KeyAlgorithm::Rsa { hash, .. } => hash, - _ => return algorithm_mismatch_error(ctx, algorithm_name), - }; - if !matches!( - hash, - HashAlgorithm::Sha1 | HashAlgorithm::Sha256 | HashAlgorithm::Sha384 | HashAlgorithm::Sha512 - ) { - return Err(Exception::throw_message( - ctx, - "Only SHA-1, SHA-256, SHA-384 or SHA-512 is supported for RSA", - )); - } - - let mut hasher = crate::llrt_crypto::CRYPTO_PROVIDER.digest(*hash); - hasher.update(data); - let digest = hasher.finalize(); - - Ok((hash, digest)) -} - -pub fn to_name_and_maybe_object<'js>( - ctx: &Ctx<'js>, - value: Value<'js>, -) -> Result<(String, Result>)> { - let obj; - let name = if let Some(string) = value.as_string() { - obj = Err(Error::new_from_js_message( - "string", - "object", - "algorithm is not an object", - )); - string.to_string()? - } else if let Some(object) = value.into_object() { - let name = object.get_required("name", "algorithm")?; - obj = Ok(object); - name - } else { - return Err(Exception::throw_message( - ctx, - "algorithm must be a string or an object", - )); - }; - Ok((name, obj)) -} - -pub fn normalize_algorithm_name(name: &str) -> String { - let name = name.to_ascii_uppercase(); - match name.as_str() { - "ED25519" => "Ed25519".to_string(), - "RSASSA-PKCS1-V1_5" => "RSASSA-PKCS1-v1_5".to_string(), - _ => name, - } -} - -pub fn algorithm_mismatch_error(ctx: &Ctx<'_>, expected_algorithm: &str) -> Result { - Err(DOMException::type_mismatch_error( - ctx, - ["Key algorithm must be ", expected_algorithm].concat(), - )) -} - -pub fn algorithm_not_supported_error(ctx: &Ctx<'_>) -> Result { - Err(DOMException::not_supported_error( - ctx, - "Algorithm not supported", - )) -} - -pub fn algorithm_invalid_access_error(ctx: &Ctx<'_>, expected_algorithm: &str) -> Result { - Err(DOMException::invalid_access_error( - ctx, - ["Key algorithm must be ", expected_algorithm].concat(), - )) -} - -// Stub implementations for providers without _subtle-full -#[cfg(not(all()))] -mod stubs; -#[cfg(not(all()))] -pub use stubs::subtle_export_key; -#[cfg(not(all()))] -pub use stubs::subtle_import_key; -#[cfg(not(all()))] -pub use stubs::subtle_unwrap_key; -#[cfg(not(all()))] -pub use stubs::subtle_wrap_key; diff --git a/stdlib/src/llrt/llrt_crypto/subtle/sign.rs b/stdlib/src/llrt/llrt_crypto/subtle/sign.rs deleted file mode 100644 index 81660a1d..00000000 --- a/stdlib/src/llrt/llrt_crypto/subtle/sign.rs +++ /dev/null @@ -1,129 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::future::Future; - -use crate::llrt_crypto::provider::{CryptoProvider, HmacProvider}; -use crate::llrt_utils::bytes::ObjectBytes; -use rquickjs::{ArrayBuffer, Class, Ctx, FromJs, Result, Value}; - -use crate::llrt_crypto::CRYPTO_PROVIDER; - -use super::{ - algorithm_invalid_access_error, - crypto_key::{CryptoKey, KeyKind}, - key_algorithm::KeyAlgorithm, - rsa_hash_digest, - sign_algorithm::SigningAlgorithm, - util::ResultDomExt, -}; - -pub fn subtle_sign<'js>( - ctx: Ctx<'js>, - algorithm: Value<'js>, - key: Class<'js, CryptoKey<'js>>, - data: ObjectBytes<'js>, -) -> impl Future>> + 'js { - // Keep preparation outside the async block: Rust async function bodies are deferred until - // polled, while WebCrypto requires call-time algorithm normalization and input snapshotting. - // Retaining the Result lets preparation failures reject the rquickjs-created Promise. - let prepared = prepare_sign(&ctx, algorithm, key, data); - - async move { - let (algorithm, key, data) = prepared?; - let key = key.borrow(); - if key.name.as_ref() != algorithm.name() { - return algorithm_invalid_access_error(&ctx, algorithm.name()); - } - key.check_validity("sign").or_throw_dom(&ctx)?; - let expected_kind = match &algorithm { - SigningAlgorithm::Hmac => KeyKind::Secret, - _ => KeyKind::Private, - }; - key.check_kind(expected_kind).or_throw_dom(&ctx)?; - - let bytes = sign(&ctx, &algorithm, &key, &data)?; - ArrayBuffer::new(ctx, bytes) - } -} - -fn prepare_sign<'js>( - ctx: &Ctx<'js>, - algorithm: Value<'js>, - key: Class<'js, CryptoKey<'js>>, - data: ObjectBytes<'js>, -) -> Result<(SigningAlgorithm, Class<'js, CryptoKey<'js>>, Vec)> { - let algorithm = SigningAlgorithm::from_js(ctx, algorithm)?; - let data = data.as_bytes_opt().unwrap_or_default().to_vec(); - Ok((algorithm, key, data)) -} - -fn sign( - ctx: &Ctx<'_>, - algorithm: &SigningAlgorithm, - key: &CryptoKey, - data: &[u8], -) -> Result> { - let handle = key.handle.as_ref(); - Ok(match algorithm { - SigningAlgorithm::Ecdsa { hash } => { - let curve = match &key.algorithm { - KeyAlgorithm::Ec { curve, .. } => curve, - _ => return algorithm_invalid_access_error(ctx, "ECDSA"), - }; - - let digest = crate::llrt_crypto::subtle::digest::digest(hash, data); - - crate::llrt_crypto::CRYPTO_PROVIDER - .ecdsa_sign(*curve, handle, &digest) - .or_throw_dom(ctx)? - } - SigningAlgorithm::Ed25519 => { - if !matches!(&key.algorithm, KeyAlgorithm::Ed25519) { - return algorithm_invalid_access_error(ctx, "Ed25519"); - } - crate::llrt_crypto::CRYPTO_PROVIDER - .ed25519_sign(handle, data) - .or_throw_dom(ctx)? - } - SigningAlgorithm::Hmac => { - let hash = if let KeyAlgorithm::Hmac { hash, .. } = &key.algorithm { - hash - } else { - return algorithm_invalid_access_error(ctx, "HMAC"); - }; - - let mut hmac = CRYPTO_PROVIDER.hmac(*hash, handle); - hmac.update(data); - hmac.finalize() - } - SigningAlgorithm::RsaPss { salt_length } => { - let (hash, digest) = rsa_hash_digest(ctx, key, data, "RSA-PSS")?; - crate::llrt_crypto::CRYPTO_PROVIDER - .rsa_pss_sign(&key.handle, digest.as_ref(), *salt_length as usize, *hash) - .or_throw_dom(ctx)? - } - SigningAlgorithm::RsassaPkcs1v15 => { - let (hash, digest) = rsa_hash_digest(ctx, key, data, "RSASSA-PKCS1-v1_5")?; - crate::llrt_crypto::CRYPTO_PROVIDER - .rsa_pkcs1v15_sign(&key.handle, digest.as_ref(), *hash) - .or_throw_dom(ctx)? - } - }) -} - -// // Helper function for RSA signing -// fn rsa_sign( -// ctx: &Ctx<'_>, -// key: &CryptoKey, -// algorithm_name: &str, -// data: &[u8], -// sign_fn: F, -// ) -> Result> -// where -// F: FnOnce(&HashAlgorithm, &[u8], &rsa::RsaPrivateKey) -> Result>, -// { -// let (hash, digest) = rsa_hash_digest(ctx, key, data, algorithm_name)?; - -// sign_fn(hash, digest.as_ref()) -// } diff --git a/stdlib/src/llrt/llrt_crypto/subtle/sign_algorithm.rs b/stdlib/src/llrt/llrt_crypto/subtle/sign_algorithm.rs deleted file mode 100644 index c48dfb19..00000000 --- a/stdlib/src/llrt/llrt_crypto/subtle/sign_algorithm.rs +++ /dev/null @@ -1,58 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use crate::llrt_utils::object::ObjectExt; -use rquickjs::{Ctx, FromJs, Result, Value}; - -use crate::llrt_crypto::hash::HashAlgorithm; - -use super::{ - algorithm_not_supported_error, key_algorithm::extract_sha_hash, normalize_algorithm_name, - to_name_and_maybe_object, -}; - -#[derive(Debug)] -pub enum SigningAlgorithm { - Ecdsa { hash: HashAlgorithm }, - Ed25519, - RsaPss { salt_length: u32 }, - RsassaPkcs1v15, - Hmac, -} - -impl<'js> FromJs<'js> for SigningAlgorithm { - fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let (name, obj) = to_name_and_maybe_object(ctx, value)?; - let name = normalize_algorithm_name(&name); - - let algorithm = match name.as_str() { - "Ed25519" => SigningAlgorithm::Ed25519, - "HMAC" => SigningAlgorithm::Hmac, - "RSASSA-PKCS1-v1_5" => SigningAlgorithm::RsassaPkcs1v15, - "ECDSA" => { - let obj = obj?; - let hash = extract_sha_hash(ctx, &obj)?; - SigningAlgorithm::Ecdsa { hash } - } - "RSA-PSS" => { - let salt_length = obj?.get_required("saltLength", "algorithm")?; - - SigningAlgorithm::RsaPss { salt_length } - } - _ => return algorithm_not_supported_error(ctx), - }; - Ok(algorithm) - } -} - -impl SigningAlgorithm { - pub fn name(&self) -> &'static str { - match self { - SigningAlgorithm::Ecdsa { .. } => "ECDSA", - SigningAlgorithm::Ed25519 => "Ed25519", - SigningAlgorithm::RsaPss { .. } => "RSA-PSS", - SigningAlgorithm::RsassaPkcs1v15 => "RSASSA-PKCS1-v1_5", - SigningAlgorithm::Hmac => "HMAC", - } - } -} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/stubs.rs b/stdlib/src/llrt/llrt_crypto/subtle/stubs.rs deleted file mode 100644 index e54d224e..00000000 --- a/stdlib/src/llrt/llrt_crypto/subtle/stubs.rs +++ /dev/null @@ -1,64 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -//! Stub implementations for SubtleCrypto operations when `_rustcrypto` feature is disabled. -//! These return errors indicating the operation is not supported. - -use rquickjs::{Ctx, Exception, Object, Result, Value}; - -use super::{crypto_key::CryptoKey, encryption_algorithm, key_algorithm}; - -pub async fn subtle_export_key<'js>( - ctx: Ctx<'js>, - _format: key_algorithm::KeyFormat, - _key: rquickjs::Class<'js, CryptoKey<'js>>, -) -> Result> { - Err(Exception::throw_message( - &ctx, - "exportKey is not supported with this crypto provider", - )) -} - -pub async fn subtle_import_key<'js>( - ctx: Ctx<'js>, - _format: key_algorithm::KeyFormat, - _key_data: Value<'js>, - _algorithm: Value<'js>, - _extractable: bool, - _key_usages: rquickjs::Array<'js>, -) -> Result>> { - Err(Exception::throw_message( - &ctx, - "importKey is not supported with this crypto provider", - )) -} - -pub async fn subtle_wrap_key<'js>( - ctx: Ctx<'js>, - _format: key_algorithm::KeyFormat, - _key: rquickjs::Class<'js, CryptoKey<'js>>, - _wrapping_key: rquickjs::Class<'js, CryptoKey<'js>>, - _wrap_algo: encryption_algorithm::EncryptionAlgorithm, -) -> Result> { - Err(Exception::throw_message( - &ctx, - "wrapKey is not supported with this crypto provider", - )) -} - -pub async fn subtle_unwrap_key<'js>( - _format: key_algorithm::KeyFormat, - wrapped_key: rquickjs::ArrayBuffer<'js>, - _unwrapping_key: rquickjs::Class<'js, CryptoKey<'js>>, - _unwrap_algo: encryption_algorithm::EncryptionAlgorithm, - _unwrapped_key_algo: Value<'js>, - _extractable: bool, - _key_usages: rquickjs::Array<'js>, -) -> Result>> { - let ctx = wrapped_key.ctx().clone(); - Err(Exception::throw_message( - &ctx, - "unwrapKey is not supported with this crypto provider", - )) -} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/util.rs b/stdlib/src/llrt/llrt_crypto/subtle/util.rs deleted file mode 100644 index 00769d2c..00000000 --- a/stdlib/src/llrt/llrt_crypto/subtle/util.rs +++ /dev/null @@ -1,87 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::{fmt::Display, result::Result as StdResult}; - -use crate::llrt_exceptions::DOMException; -use rquickjs::{Ctx, Error, Result}; - -use crate::llrt_crypto::provider::CryptoError; - -pub trait IntoDomException { - fn into_dom_exception(self, ctx: &Ctx, msg: &str) -> Error; -} - -impl IntoDomException for CryptoError { - fn into_dom_exception(self, ctx: &Ctx, msg: &str) -> Error { - let message = with_message(&self, msg); - match self { - CryptoError::UnsupportedAlgorithm => DOMException::not_supported_error(ctx, message), - CryptoError::InvalidLength - | CryptoError::InvalidKey(_) - | CryptoError::InvalidData(_) - | CryptoError::InvalidSignature(_) => DOMException::data_error(ctx, message), - CryptoError::SigningFailed(_) - | CryptoError::VerificationFailed - | CryptoError::OperationFailed(_) - | CryptoError::DerivationFailed(_) - | CryptoError::EncryptionFailed(_) - | CryptoError::DecryptionFailed(_) => DOMException::operation_error(ctx, message), - CryptoError::InvalidAccess(_) => DOMException::invalid_access_error(ctx, message), - } - } -} - -pub trait ResultDomExt { - fn or_throw_dom(self, ctx: &Ctx) -> Result; - fn or_throw_dom_with_msg(self, ctx: &Ctx, msg: &str) -> Result; -} - -impl ResultDomExt for StdResult { - fn or_throw_dom(self, ctx: &Ctx) -> Result { - self.map_err(|e| e.into_dom_exception(ctx, "")) - } - fn or_throw_dom_with_msg(self, ctx: &Ctx, msg: &str) -> Result { - self.map_err(|e| e.into_dom_exception(ctx, msg)) - } -} - -impl ResultDomExt for Option { - fn or_throw_dom(self, ctx: &Ctx) -> Result { - self.ok_or_else(|| DOMException::not_supported_error(ctx, "Value is not present")) - } - fn or_throw_dom_with_msg(self, ctx: &Ctx, msg: &str) -> Result { - let message = if msg.is_empty() { - "Value is not present" - } else { - msg - }; - self.ok_or_else(|| DOMException::not_supported_error(ctx, message)) - } -} - -pub struct NotSupportedError(pub E); - -impl IntoDomException for NotSupportedError { - fn into_dom_exception(self, ctx: &Ctx, msg: &str) -> Error { - DOMException::not_supported_error(ctx, with_message(self.0, msg)) - } -} - -#[allow(dead_code)] -pub struct DataError(pub E); - -#[allow(dead_code)] -impl IntoDomException for DataError { - fn into_dom_exception(self, ctx: &Ctx, msg: &str) -> Error { - DOMException::data_error(ctx, with_message(self.0, msg)) - } -} - -fn with_message(err: E, msg: &str) -> String { - if msg.is_empty() { - err.to_string() - } else { - [msg, ": ", &err.to_string()].concat() - } -} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/verify.rs b/stdlib/src/llrt/llrt_crypto/subtle/verify.rs deleted file mode 100644 index 2e84550e..00000000 --- a/stdlib/src/llrt/llrt_crypto/subtle/verify.rs +++ /dev/null @@ -1,155 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::future::Future; - -use crate::llrt_crypto::provider::{CryptoError, CryptoProvider, HmacProvider}; -use crate::llrt_utils::bytes::ObjectBytes; -use rquickjs::{Class, Ctx, FromJs, Result, Value}; - -use crate::llrt_crypto::CRYPTO_PROVIDER; - -use super::{ - algorithm_invalid_access_error, - crypto_key::{CryptoKey, KeyKind}, - digest, - key_algorithm::KeyAlgorithm, - rsa_hash_digest, - sign_algorithm::SigningAlgorithm, - util::ResultDomExt, -}; - -pub fn subtle_verify<'js>( - ctx: Ctx<'js>, - algorithm: Value<'js>, - key: Class<'js, CryptoKey<'js>>, - signature: ObjectBytes<'js>, - data: ObjectBytes<'js>, -) -> impl Future> + 'js { - // Keep preparation outside the async block: Rust async function bodies are deferred until - // polled, while WebCrypto requires call-time algorithm normalization and input snapshotting. - // Retaining the Result lets preparation failures reject the rquickjs-created Promise. - let prepared = prepare_verify(&ctx, algorithm, key, signature, data); - - async move { - let PreparedVerify { - algorithm, - key, - signature, - data, - } = prepared?; - let key = key.borrow(); - if key.name.as_ref() != algorithm.name() { - return algorithm_invalid_access_error(&ctx, algorithm.name()); - } - key.check_validity("verify").or_throw_dom(&ctx)?; - let expected_kind = match &algorithm { - SigningAlgorithm::Hmac => KeyKind::Secret, - _ => KeyKind::Public, - }; - key.check_kind(expected_kind).or_throw_dom(&ctx)?; - - verify(&ctx, &algorithm, &key, &signature, &data) - } -} - -struct PreparedVerify<'js> { - algorithm: SigningAlgorithm, - key: Class<'js, CryptoKey<'js>>, - signature: Vec, - data: Vec, -} - -fn prepare_verify<'js>( - ctx: &Ctx<'js>, - algorithm: Value<'js>, - key: Class<'js, CryptoKey<'js>>, - signature: ObjectBytes<'js>, - data: ObjectBytes<'js>, -) -> Result> { - let algorithm = SigningAlgorithm::from_js(ctx, algorithm)?; - let signature = signature.as_bytes_opt().unwrap_or_default().to_vec(); - let data = data.as_bytes_opt().unwrap_or_default().to_vec(); - Ok(PreparedVerify { - algorithm, - key, - signature, - data, - }) -} - -fn verify( - ctx: &Ctx<'_>, - algorithm: &SigningAlgorithm, - key: &CryptoKey, - signature: &[u8], - data: &[u8], -) -> Result { - let handle = key.handle.as_ref(); - Ok(match algorithm { - SigningAlgorithm::Ecdsa { hash } => { - let curve = match &key.algorithm { - KeyAlgorithm::Ec { curve, .. } => curve, - _ => return algorithm_invalid_access_error(ctx, "ECDSA"), - }; - - let digest = digest::digest(hash, data); - - crate::llrt_crypto::CRYPTO_PROVIDER - .ecdsa_verify(*curve, handle, signature, &digest) - .into_verification(ctx)? - } - SigningAlgorithm::Ed25519 => { - if !matches!(&key.algorithm, KeyAlgorithm::Ed25519) { - return algorithm_invalid_access_error(ctx, "Ed25519"); - } - - crate::llrt_crypto::CRYPTO_PROVIDER - .ed25519_verify(handle, signature, data) - .into_verification(ctx)? - } - SigningAlgorithm::Hmac => { - let hash = match &key.algorithm { - KeyAlgorithm::Hmac { hash, .. } => hash, - _ => return algorithm_invalid_access_error(ctx, "HMAC"), - }; - - let mut hmac = CRYPTO_PROVIDER.hmac(*hash, handle); - hmac.update(data); - let computed_signature = hmac.finalize(); - - computed_signature == signature - } - SigningAlgorithm::RsaPss { salt_length } => { - let (hash, digest) = rsa_hash_digest(ctx, key, data, "RSA-PSS")?; - crate::llrt_crypto::CRYPTO_PROVIDER - .rsa_pss_verify( - &key.handle, - signature, - digest.as_ref(), - *salt_length as usize, - *hash, - ) - .into_verification(ctx)? - } - SigningAlgorithm::RsassaPkcs1v15 => { - let (hash, digest) = rsa_hash_digest(ctx, key, data, "RSASSA-PKCS1-v1_5")?; - crate::llrt_crypto::CRYPTO_PROVIDER - .rsa_pkcs1v15_verify(&key.handle, signature, digest.as_ref(), *hash) - .into_verification(ctx)? - } - }) -} - -trait VerificationResultExt { - fn into_verification(self, ctx: &Ctx<'_>) -> Result; -} - -impl VerificationResultExt for std::result::Result { - fn into_verification(self, ctx: &Ctx<'_>) -> Result { - match self { - Err(CryptoError::InvalidSignature(_)) => Ok(false), - result => result.or_throw_dom(ctx), - } - } -} diff --git a/stdlib/src/llrt/llrt_crypto/subtle/wrapping.rs b/stdlib/src/llrt/llrt_crypto/subtle/wrapping.rs deleted file mode 100644 index 0dfb61a2..00000000 --- a/stdlib/src/llrt/llrt_crypto/subtle/wrapping.rs +++ /dev/null @@ -1,93 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use crate::llrt_json::{parse::json_parse, stringify::json_stringify}; -use crate::llrt_utils::{bytes::ObjectBytes, object::ObjectExt, result::ResultExt}; -use rquickjs::{Array, ArrayBuffer, Class, Ctx, Result, Value}; - -use super::{ - crypto_key::CryptoKey, - encryption::{self, encrypt_decrypt}, - encryption_algorithm::EncryptionAlgorithm, - export_key::{export_key, ExportOutput}, - import_key::import_key, - key_algorithm::{KeyFormat, KeyFormatData}, - EncryptionMode, -}; - -pub async fn subtle_wrap_key<'js>( - ctx: Ctx<'js>, - format: KeyFormat, - key: Class<'js, CryptoKey<'js>>, - wrapping_key: Class<'js, CryptoKey<'js>>, - wrap_algo: EncryptionAlgorithm, -) -> Result> { - let key = key.borrow(); - - let export = export_key(&ctx, format, &key)?; - - let (bytes, padding) = match export { - ExportOutput::Bytes(bytes) => (bytes, 0), - ExportOutput::Object(value) => { - let json = json_stringify(&ctx, value.into_value())?.unwrap(); - (json.into_bytes(), b' ') - } - }; - - let wrapping_key = wrapping_key.borrow(); - wrapping_key.check_validity("wrapKey").or_throw(&ctx)?; - - let bytes = encrypt_decrypt( - &ctx, - &wrap_algo, - &wrapping_key, - &bytes, - EncryptionMode::Wrapping(padding), - encryption::EncryptionOperation::Encrypt, - )?; - - ArrayBuffer::new(ctx, bytes) -} - -//cant take more than 7 args -pub async fn subtle_unwrap_key<'js>( - format: KeyFormat, - wrapped_key: Value<'js>, - unwrapping_key: Class<'js, CryptoKey<'js>>, - unwrap_algo: EncryptionAlgorithm, - unwrapped_key_algo: Value<'js>, - extractable: bool, - key_usages: Array<'js>, -) -> Result>> { - let unwrapping_key = unwrapping_key.borrow(); - let ctx = wrapped_key.ctx().clone(); - unwrapping_key.check_validity("unwrapKey").or_throw(&ctx)?; - - let bytes = ObjectBytes::from(&ctx, &wrapped_key)?; - let bytes = bytes.as_bytes(&ctx)?; - - let padding = match format { - KeyFormat::Jwk => b' ', - _ => 0, - }; - - let bytes = encrypt_decrypt( - &ctx, - &unwrap_algo, - &unwrapping_key, - bytes, - EncryptionMode::Wrapping(padding), - encryption::EncryptionOperation::Decrypt, - )?; - - let key_format = match format { - KeyFormat::Jwk => { - KeyFormatData::Jwk(json_parse(&ctx, bytes)?.into_object_or_throw(&ctx, "wrappedKey")?) - } - KeyFormat::Raw => KeyFormatData::Raw(ObjectBytes::Vec(bytes)), - KeyFormat::Spki => KeyFormatData::Spki(ObjectBytes::Vec(bytes)), - KeyFormat::Pkcs8 => KeyFormatData::Pkcs8(ObjectBytes::Vec(bytes)), - }; - - import_key(ctx, key_format, unwrapped_key_algo, extractable, key_usages) -} diff --git a/stdlib/src/llrt/llrt_encoding/lib.rs b/stdlib/src/llrt/llrt_encoding/lib.rs deleted file mode 100644 index 26d56f7c..00000000 --- a/stdlib/src/llrt/llrt_encoding/lib.rs +++ /dev/null @@ -1,254 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::borrow::Cow; - -use hex_simd::AsciiCase; - -#[derive(Clone, PartialEq)] -pub enum Encoder { - Hex, - Base64, - Windows1252, - Utf8, - Utf16le, - Utf16be, -} - -const ENCODING_MAP: phf::Map<&'static str, Encoder> = phf::phf_map! { - "buffer" => Encoder::Utf8, - "hex" => Encoder::Hex, - "base64" => Encoder::Base64, - "unicode-1-1-utf-8" => Encoder::Utf8, - "unicode11utf8" => Encoder::Utf8, - "unicode20utf8" => Encoder::Utf8, - "utf-8" => Encoder::Utf8, - "utf8" => Encoder::Utf8, - "x-unicode20utf8" => Encoder::Utf8, - "csunicode" => Encoder::Utf16le, - "iso-10646-ucs-2" => Encoder::Utf16le, - "ucs-2" => Encoder::Utf16le, - "ucs2" => Encoder::Utf16le, - "unicode" => Encoder::Utf16le, - "unicodefeff" => Encoder::Utf16le, - "utf-16" => Encoder::Utf16le, - "utf-16le" => Encoder::Utf16le, - "utf16le" => Encoder::Utf16le, - "unicodefffe" => Encoder::Utf16be, - "utf-16be" => Encoder::Utf16be, - "ansi_x3.4-1968" => Encoder::Windows1252, - "ascii" => Encoder::Windows1252, - "cp1252" => Encoder::Windows1252, - "cp819" => Encoder::Windows1252, - "csisolatin1" => Encoder::Windows1252, - "ibm819" => Encoder::Windows1252, - "iso-8859-1" => Encoder::Windows1252, - "iso-ir-100" => Encoder::Windows1252, - "iso8859-1" => Encoder::Windows1252, - "iso88591" => Encoder::Windows1252, - "iso_8859-1" => Encoder::Windows1252, - "iso_8859-1:1987" => Encoder::Windows1252, - "l1" => Encoder::Windows1252, - "latin1" => Encoder::Windows1252, - "us-ascii" => Encoder::Windows1252, - "windows-1252" => Encoder::Windows1252, - "x-cp1252" => Encoder::Windows1252, -}; - -impl Encoder { - pub fn from_optional_str(encoding: Option<&str>) -> Result { - match encoding { - Some(label) if !label.is_empty() => Self::from_str(label), - _ => Ok(Self::Utf8), - } - } - - #[allow(clippy::should_implement_trait)] - pub fn from_str(encoding: &str) -> Result { - ENCODING_MAP - .get(encoding.trim_ascii().to_ascii_lowercase().as_str()) - .cloned() - .ok_or_else(|| ["The \"", encoding, "\" encoding is not supported"].concat()) - } - - pub fn encode_to_string(&self, bytes: &[u8], lossy: bool) -> Result { - match self { - Self::Hex => Ok(bytes_to_hex_string(bytes)), - Self::Base64 => Ok(bytes_to_b64_string(bytes)), - Self::Utf8 | Self::Windows1252 => bytes_to_utf8_string(bytes, lossy), - Self::Utf16le => bytes_to_utf16_string(bytes, Endian::Little, lossy), - Self::Utf16be => bytes_to_utf16_string(bytes, Endian::Big, lossy), - } - } - - #[allow(dead_code)] - pub fn encode(&self, bytes: &[u8]) -> Result, String> { - match self { - Self::Hex => Ok(bytes_to_hex(bytes)), - Self::Base64 => Ok(bytes_to_b64(bytes)), - Self::Utf8 | Self::Windows1252 | Self::Utf16le | Self::Utf16be => Ok(bytes.to_vec()), - } - } - - pub fn decode<'a, T: Into>>(&self, bytes: T) -> Result, String> { - match self { - Self::Hex => bytes_from_hex(bytes), - Self::Base64 => bytes_from_b64(bytes), - Self::Utf8 | Self::Windows1252 | Self::Utf16le | Self::Utf16be => { - Ok(bytes.into().into()) - } - } - } - - pub fn decode_from_string(&self, string: String) -> Result, String> { - match self { - Self::Hex => bytes_from_hex(string.into_bytes()), - Self::Base64 => bytes_from_b64(string.into_bytes()), - Self::Utf8 | Self::Windows1252 => Ok(string.into_bytes()), - Self::Utf16le => Ok(string - .encode_utf16() - .flat_map(|utf16| utf16.to_le_bytes()) - .collect::>()), - Self::Utf16be => Ok(string - .encode_utf16() - .flat_map(|utf16| utf16.to_be_bytes()) - .collect::>()), - } - } - - pub fn as_label(&self) -> &str { - match self { - Self::Hex => "hex", - Self::Base64 => "base64", - Self::Windows1252 => "windows-1252", - Self::Utf8 => "utf-8", - Self::Utf16le => "utf-16le", - Self::Utf16be => "utf-16be", - } - } -} - -pub fn bytes_to_hex(bytes: &[u8]) -> Vec { - hex_simd::encode_type(bytes, AsciiCase::Lower) -} - -pub fn bytes_from_hex<'a, T: Into>>(hex_bytes: T) -> Result, String> { - hex_simd::decode_to_vec(hex_bytes.into()).map_err(|err| err.to_string()) -} - -pub fn bytes_from_b64<'a, T: Into>>(base64_bytes: T) -> Result, String> { - let bytes: Cow<'a, [u8]> = base64_bytes.into(); - - //need to collect since memchr2_iter is borrowing bytes. This is fine since we're unlikely to contain url safe base64 - let url_safe_byte_positions: Vec = memchr::memchr2_iter(b'-', b'_', &bytes).collect(); - - if url_safe_byte_positions.is_empty() { - return base64_simd::forgiving_decode_to_vec(&bytes).map_err(|e| e.to_string()); - } - - //doesn't allocate for already owned data - let mut bytes = bytes.into_owned(); - for pos in url_safe_byte_positions { - bytes[pos] = match bytes[pos] { - b'-' => b'+', - b'_' => b'/', - _ => unreachable!(), - }; - } - base64_simd::forgiving_decode_to_vec(&bytes).map_err(|e| e.to_string()) -} - -/// Strict standard-base64 decode (single SIMD pass): rejects url-safe chars, -/// whitespace and bad padding, matching @smithy/util-base64 semantics. -pub fn bytes_from_b64_strict(bytes: &[u8]) -> Result, String> { - base64_simd::STANDARD - .decode_to_vec(bytes) - .map_err(|e| e.to_string()) -} - -pub fn bytes_to_b64_string(bytes: &[u8]) -> String { - base64_simd::STANDARD.encode_to_string(bytes) -} - -pub fn bytes_to_b64_url_safe_string(bytes: &[u8]) -> String { - base64_simd::URL_SAFE_NO_PAD.encode_to_string(bytes) -} - -pub fn bytes_from_b64_url_safe(bytes: &[u8]) -> Result, String> { - base64_simd::URL_SAFE_NO_PAD - .decode_to_vec(bytes) - .map_err(|e| e.to_string()) -} - -pub fn bytes_to_b64(bytes: &[u8]) -> Vec { - base64_simd::STANDARD.encode_type(bytes) -} - -pub fn bytes_to_hex_string(bytes: &[u8]) -> String { - hex_simd::encode_to_string(bytes, AsciiCase::Lower) -} - -pub fn bytes_to_utf8_string(bytes: &[u8], lossy: bool) -> Result { - if lossy { - Ok(String::from_utf8_lossy(bytes).to_string()) - } else { - String::from_utf8(bytes.to_vec()).map_err(|e| e.to_string()) - } -} - -#[derive(Clone, Copy)] -pub enum Endian { - Little, - Big, -} - -pub fn bytes_to_utf16_string(bytes: &[u8], endian: Endian, lossy: bool) -> Result { - if !lossy && !bytes.len().is_multiple_of(2) { - return Err("Input byte slice length must be even".to_string()); - } - - let data16: Vec = match endian { - Endian::Little => bytes - .as_chunks::<2>() - .0 - .iter() - .copied() - .map(u16::from_le_bytes) - .collect(), - Endian::Big => bytes - .as_chunks::<2>() - .0 - .iter() - .copied() - .map(u16::from_be_bytes) - .collect(), - }; - - let mut result = if lossy { - String::from_utf16_lossy(&data16) - } else { - String::from_utf16(&data16).map_err(|e| e.to_string())? - }; - - // Odd trailing byte in lossy mode produces a replacement character - if lossy && !bytes.len().is_multiple_of(2) { - result.push('\u{FFFD}'); - } - - Ok(result) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn b64_strict_matches_smithy_semantics() { - // canonical decodes - assert_eq!(bytes_from_b64_strict(b"SGVsbG8=").unwrap(), b"Hello"); - // url-safe, whitespace, bad-padding are rejected (like @smithy/util-base64) - assert!(bytes_from_b64_strict(b"-_8=").is_err()); - assert!(bytes_from_b64_strict(b"SGVs bG8=").is_err()); - assert!(bytes_from_b64_strict(b"SGVsbG8").is_err()); - } -} diff --git a/stdlib/src/llrt/llrt_events/custom_event.rs b/stdlib/src/llrt/llrt_events/custom_event.rs deleted file mode 100644 index 4d24be74..00000000 --- a/stdlib/src/llrt/llrt_events/custom_event.rs +++ /dev/null @@ -1,40 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use rquickjs::{prelude::Opt, Ctx, IntoJs, Null, Result, Value}; - -use crate::llrt_utils::object::ObjectExt; - -#[rquickjs::class] -#[derive(rquickjs::class::Trace, rquickjs::JsLifetime)] -pub struct CustomEvent<'js> { - event_type: String, - detail: Option>, -} - -#[rquickjs::methods] -impl<'js> CustomEvent<'js> { - #[qjs(constructor)] - pub fn new(event_type: String, options: Opt>) -> Result { - let mut detail = None; - if let Some(options) = options.0 { - if let Some(opt) = options.get_optional("detail")? { - detail = opt; - } - } - Ok(Self { event_type, detail }) - } - - #[qjs(get)] - pub fn detail(&self, ctx: Ctx<'js>) -> Result> { - if let Some(detail) = &self.detail { - return Ok(detail.clone()); - } - Null.into_js(&ctx) - } - - #[qjs(get, rename = "type")] - pub fn event_type(&self) -> String { - self.event_type.clone() - } -} diff --git a/stdlib/src/llrt/llrt_events/event.rs b/stdlib/src/llrt/llrt_events/event.rs deleted file mode 100644 index 2e81873f..00000000 --- a/stdlib/src/llrt/llrt_events/event.rs +++ /dev/null @@ -1,62 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use rquickjs::{prelude::Opt, Result, Value}; - -use crate::llrt_utils::object::ObjectExt; - -#[rquickjs::class] -#[derive(rquickjs::class::Trace, rquickjs::JsLifetime)] -pub struct Event { - event_type: String, - bubbles: bool, - cancelable: bool, - composed: bool, -} - -#[rquickjs::methods] -impl Event { - #[qjs(constructor)] - pub fn new(event_type: String, options: Opt>) -> Result { - let mut bubbles = false; - let mut cancelable = false; - let mut composed = false; - if let Some(options) = options.0 { - if let Some(opt) = options.get_optional("bubbles")? { - bubbles = opt; - } - if let Some(opt) = options.get_optional("cancelable")? { - cancelable = opt; - } - if let Some(opt) = options.get_optional("composed")? { - composed = opt; - } - } - Ok(Self { - event_type, - bubbles, - cancelable, - composed, - }) - } - - #[qjs(get)] - pub fn bubbles(&self) -> bool { - self.bubbles - } - - #[qjs(get)] - pub fn cancelable(&self) -> bool { - self.cancelable - } - - #[qjs(get)] - pub fn composed(&self) -> bool { - self.composed - } - - #[qjs(get, rename = "type")] - pub fn event_type(&self) -> String { - self.event_type.clone() - } -} diff --git a/stdlib/src/llrt/llrt_events/event_target.rs b/stdlib/src/llrt/llrt_events/event_target.rs deleted file mode 100644 index c4405a2c..00000000 --- a/stdlib/src/llrt/llrt_events/event_target.rs +++ /dev/null @@ -1,44 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::sync::{Arc, RwLock}; - -use rquickjs::{ - class::{Trace, Tracer}, - JsLifetime, -}; - -use super::{Emitter, EventList, Events}; - -#[rquickjs::class] -#[derive(Clone)] -pub struct EventTarget<'js> { - pub events: Events<'js>, -} - -unsafe impl<'js> JsLifetime<'js> for EventTarget<'js> { - type Changed<'to> = EventTarget<'to>; -} - -impl<'js> Emitter<'js> for EventTarget<'js> { - fn get_event_list(&self) -> Arc>> { - self.events.clone() - } -} - -impl<'js> Trace<'js> for EventTarget<'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - self.trace_event_emitter(tracer); - } -} - -#[rquickjs::methods] -impl<'js> EventTarget<'js> { - #[qjs(constructor)] - pub fn new() -> Self { - Self { - #[allow(clippy::arc_with_non_send_sync)] - events: Arc::new(RwLock::new(Vec::new())), - } - } -} diff --git a/stdlib/src/llrt/llrt_events/lib.rs b/stdlib/src/llrt/llrt_events/lib.rs deleted file mode 100644 index d56445ed..00000000 --- a/stdlib/src/llrt/llrt_events/lib.rs +++ /dev/null @@ -1,580 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -#![allow( - clippy::mutable_key_type, - clippy::for_kv_map, - clippy::new_without_default -)] -use std::{ - rc::Rc, - sync::{Arc, RwLock}, -}; - -use crate::llrt_utils::{ - error::ErrorExtensions, module::ModuleInfo, object::ObjectExt, result::ResultExt, -}; -use rquickjs::{ - class::{JsClass, Trace, Tracer}, - module::{Declarations, Exports, ModuleDef}, - prelude::{Func, Opt, Rest, This}, - CatchResultExt, Class, Ctx, Function, JsLifetime, Object, Result, String as JsString, Symbol, - Value, -}; -use tracing::trace; - -use self::{custom_event::CustomEvent, event::Event, event_target::EventTarget}; - -pub mod custom_event; -pub mod event; -pub mod event_target; - -#[derive(Clone, Debug)] -pub enum EventKey<'js> { - Symbol(Symbol<'js>), - String(Rc), -} - -impl<'js> EventKey<'js> { - fn from_value(ctx: &Ctx, value: Value<'js>) -> Result { - if value.is_string() { - let key: String = value.get()?; - Ok(EventKey::String(key.into())) - } else { - let sym = value.into_symbol().ok_or("Not a symbol").or_throw(ctx)?; - Ok(EventKey::Symbol(sym)) - } - } -} - -impl Eq for EventKey<'_> {} - -impl PartialEq for EventKey<'_> { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (EventKey::Symbol(symbol1), EventKey::Symbol(symbol2)) => symbol1 == symbol2, - (EventKey::String(str1), EventKey::String(str2)) => str1 == str2, - _ => false, - } - } -} - -pub struct EventItem<'js> { - callback: Function<'js>, - once: bool, -} - -pub type EventList<'js> = Vec<(EventKey<'js>, Vec>)>; -pub type Events<'js> = Arc>>; - -/// Get the hidden symbol used to store the event list on JS objects. -fn events_symbol<'js>(ctx: &Ctx<'js>) -> Result> { - Symbol::new_global(ctx.clone(), "__ee") -} - -/// Convert a Class into an Object for use with Emitter methods. -fn class_to_obj<'js, C: JsClass<'js>>(class: Class<'js, C>) -> Result> { - Object::from_value(class.into_value()) -} - -/// Resolve the event list from a JS object. For native Emitter classes, -/// reads from the native struct. For plain JS objects (e.g. stream.js Readable), -/// lazily creates and stores a native EventEmitter as a hidden property. -#[allow(clippy::arc_with_non_send_sync)] -pub fn resolve_events<'js>(ctx: &Ctx<'js>, obj: &Object<'js>) -> Result> { - // Try native EventEmitter first - if let Some(class) = Class::::from_object(obj) { - return Ok(class.borrow().events.clone()); - } - let sym = events_symbol(ctx)?; - // Check for hidden property - if let Some(ee) = obj.get::<_, Option>>>(sym.clone())? { - return Ok(ee.borrow().events.clone()); - } - // Create and store a new one - let events: Events<'js> = Arc::new(RwLock::new(Vec::new())); - let ee = Class::instance( - ctx.clone(), - EventEmitter { - events: events.clone(), - }, - )?; - obj.set(sym, ee)?; - Ok(events) -} - -#[rquickjs::class] -#[derive(Clone)] -pub struct EventEmitter<'js> { - pub events: Events<'js>, -} - -unsafe impl<'js> JsLifetime<'js> for EventEmitter<'js> { - type Changed<'to> = EventEmitter<'to>; -} - -impl<'js> Emitter<'js> for EventEmitter<'js> { - fn get_event_list(&self) -> Arc>> { - self.events.clone() - } -} - -impl<'js> Trace<'js> for EventEmitter<'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - self.trace_event_emitter(tracer); - } -} - -#[rquickjs::methods] -impl<'js> EventEmitter<'js> { - #[qjs(constructor)] - pub fn new() -> Self { - Self { - #[allow(clippy::arc_with_non_send_sync)] - events: Arc::new(RwLock::new(Vec::new())), - } - } -} - -pub trait EmitError<'js> { - fn emit_error(self, id: &'static str, ctx: &Ctx<'js>, this: Class<'js, C>) -> Result - where - C: Emitter<'js>; -} - -impl<'js, T> EmitError<'js> for Result { - fn emit_error(self, id: &'static str, ctx: &Ctx<'js>, this: Class<'js, C>) -> Result - where - C: Emitter<'js>, - { - if let Err(err) = self.catch(ctx) { - trace!("Error caught in: {}", id); - if this.borrow().has_listener_str("error") { - let error_value = err.into_value(ctx)?; - C::emit_str(this, ctx, "error", vec![error_value], false)?; - return Ok(true); - } - return Err(err.throw(ctx)); - } - Ok(false) - } -} - -pub trait Emitter<'js> -where - Self: JsClass<'js> + Sized + 'js, -{ - fn get_event_list(&self) -> Arc>>; - - fn on_event_changed(&mut self, _event: EventKey<'js>, _added: bool) -> Result<()> { - Ok(()) - } - - /// Resolve the event list from a `this` object. For native classes, - /// extracts from the class data. For plain JS objects, uses the hidden property. - fn resolve_events_from(ctx: &Ctx<'js>, this: &Object<'js>) -> Result> { - if let Some(class) = Class::::from_object(this) { - return Ok(class.borrow().get_event_list()); - } - resolve_events(ctx, this) - } - - fn add_event_emitter_prototype(ctx: &Ctx<'js>) -> Result> { - let proto = Class::::prototype(ctx)? - .or_throw_msg(ctx, "Prototype for EventEmitter not found")?; - - let on = Function::new(ctx.clone(), Self::on)?; - let off = Function::new(ctx.clone(), Self::remove_event_listener)?; - - proto.set("once", Func::from(Self::once))?; - proto.set("on", on.clone())?; - proto.set("emit", Func::from(Self::emit))?; - proto.set("prependListener", Func::from(Self::prepend_listener))?; - proto.set( - "prependOnceListener", - Func::from(Self::prepend_once_listener), - )?; - proto.set("off", off.clone())?; - proto.set("eventNames", Func::from(Self::event_names))?; - proto.set("addListener", on)?; - proto.set("removeListener", off)?; - proto.set("listenerCount", Func::from(Self::listener_count))?; - proto.set("removeAllListeners", Func::from(Self::remove_all_listeners))?; - - Ok(proto) - } - - fn add_event_target_prototype(ctx: &Ctx<'js>) -> Result> { - let proto = Class::::prototype(ctx)? - .or_throw_msg(ctx, "Prototype for EventTarget not found")?; - - let on = Function::new(ctx.clone(), Self::evt_add_event_listener)?; - let off = Function::new(ctx.clone(), Self::remove_event_listener)?; - - proto.set("dispatchEvent", Func::from(Self::evt_dispatch_event))?; - proto.set("addEventListener", on)?; - proto.set("removeEventListener", off)?; - - Ok(proto) - } - - fn trace_event_emitter<'a>(&self, tracer: Tracer<'a, 'js>) { - let events = self.get_event_list(); - let events = events.read().unwrap(); - for (key, items) in events.iter() { - if let EventKey::Symbol(sym) = &key { - tracer.mark(sym); - } - - for item in items { - tracer.mark(&item.callback); - } - } - } - - fn remove_event_listener_str( - this: Class<'js, Self>, - ctx: &Ctx<'js>, - event: &str, - listener: Function<'js>, - ) -> Result> { - let event = to_event(ctx, event)?; - Self::remove_event_listener(This(class_to_obj(this)?), ctx.clone(), event, listener) - } - - fn remove_event_listener( - this: This>, - ctx: Ctx<'js>, - event: Value<'js>, - listener: Function<'js>, - ) -> Result> { - let events = Self::resolve_events_from(&ctx, &this)?; - let mut events = events.write().or_throw(&ctx)?; - - let key = EventKey::from_value(&ctx, event)?; - if let Some(index) = events.iter_mut().position(|(k, _)| k == &key) { - let items = &mut events[index].1; - if let Some(pos) = items.iter().position(|item| item.callback == listener) { - items.remove(pos); - if items.is_empty() { - events.remove(index); - } - } - }; - - Ok(this.0) - } - - fn add_event_listener_str( - this: Class<'js, Self>, - ctx: &Ctx<'js>, - event: &str, - listener: Function<'js>, - prepend: bool, - once: bool, - ) -> Result> { - let event = to_event(ctx, event)?; - Self::add_event_listener( - This(class_to_obj(this)?), - ctx.clone(), - event, - listener, - prepend, - once, - ) - } - - fn once( - this: This>, - ctx: Ctx<'js>, - event: Value<'js>, - listener: Function<'js>, - ) -> Result> { - Self::add_event_listener(this, ctx, event, listener, false, true) - } - - fn on( - this: This>, - ctx: Ctx<'js>, - event: Value<'js>, - listener: Function<'js>, - ) -> Result> { - Self::add_event_listener(this, ctx, event, listener, false, false) - } - - fn prepend_listener( - this: This>, - ctx: Ctx<'js>, - event: Value<'js>, - listener: Function<'js>, - ) -> Result> { - Self::add_event_listener(this, ctx, event, listener, true, false) - } - - fn prepend_once_listener( - this: This>, - ctx: Ctx<'js>, - event: Value<'js>, - listener: Function<'js>, - ) -> Result> { - Self::add_event_listener(this, ctx, event, listener, true, true) - } - - fn evt_add_event_listener( - this: This>, - ctx: Ctx<'js>, - event: Value<'js>, - listener: Function<'js>, - options: Opt>, - ) -> Result> { - let mut once = false; - if let Some(opt) = options.0 { - if let Some(once_opt) = opt.get("once")? { - once = once_opt; - } - } - Self::add_event_listener(this, ctx, event, listener, false, once) - } - - fn add_event_listener( - this: This>, - ctx: Ctx<'js>, - event: Value<'js>, - listener: Function<'js>, - prepend: bool, - once: bool, - ) -> Result> { - let events = Self::resolve_events_from(&ctx, &this)?; - let mut events = events.write().or_throw(&ctx)?; - let key = EventKey::from_value(&ctx, event)?; - let mut is_new = false; - - let items = match events.iter_mut().find(|(k, _)| k == &key) { - Some((_, entry_items)) => entry_items, - None => { - is_new = true; - events.push((key.clone(), Vec::new())); - &mut events.last_mut().unwrap().1 - } - }; - - let item = EventItem { - callback: listener, - once, - }; - if !prepend { - items.push(item); - } else { - items.insert(0, item); - } - if is_new { - if let Some(class) = Class::::from_object(&this.0) { - class.borrow_mut().on_event_changed(key, true)?; - } - } - Ok(this.0) - } - - fn has_listener_str(&self, event: &str) -> bool { - let key = EventKey::String(event.into()); - has_key(self.get_event_list(), key) - } - - #[allow(dead_code)] - fn has_listener(&self, ctx: Ctx<'js>, event: Value<'js>) -> Result { - let key = EventKey::from_value(&ctx, event)?; - Ok(has_key(self.get_event_list(), key)) - } - - #[allow(dead_code)] - fn get_listeners(&self, ctx: &Ctx<'js>, event: Value<'js>) -> Result>> { - let key = EventKey::from_value(ctx, event)?; - Ok(find_all_listeners(self.get_event_list(), key)) - } - - fn get_listeners_str(&self, event: &str) -> Vec> { - let key = EventKey::String(event.into()); - find_all_listeners(self.get_event_list(), key) - } - - fn do_emit( - event: Value<'js>, - this: This>, - ctx: &Ctx<'js>, - args: Rest>, - defer: bool, - ) -> Result { - let events = Self::resolve_events_from(ctx, &this)?; - let mut events = events.write().or_throw(ctx)?; - let key = EventKey::from_value(ctx, event)?; - - if let Some(index) = events.iter_mut().position(|(k, _)| k == &key) { - let items = &mut events[index].1; - let mut callbacks = Vec::with_capacity(items.len()); - items.retain(|item: &EventItem<'_>| { - callbacks.push(item.callback.clone()); - !item.once - }); - if items.is_empty() { - events.remove(index); - if let Some(class) = Class::::from_object(&this.0) { - class.borrow_mut().on_event_changed(key, false)?; - } - } - drop(events); - for callback in callbacks { - let args: Vec> = args.iter().map(|arg| arg.to_owned()).collect(); - let args = Rest(args); - let this_val = This(this.0.clone().into_value()); - if defer { - callback.defer((this_val, args))?; - } else { - callback.call::<_, ()>((this_val, args))?; - } - } - Ok(true) - } else { - Ok(false) - } - } - - fn emit_str( - this: Class<'js, Self>, - ctx: &Ctx<'js>, - event: &str, - args: Vec>, - defer: bool, - ) -> Result<()> { - let event = to_event(ctx, event)?; - Self::do_emit(event, This(class_to_obj(this)?), ctx, args.into(), defer)?; - Ok(()) - } - - fn emit( - this: This>, - ctx: Ctx<'js>, - event: Value<'js>, - args: Rest>, - ) -> Result { - Self::do_emit(event, this, &ctx, args, false) - } - - fn evt_dispatch_event( - this: This>, - ctx: Ctx<'js>, - event: Value<'js>, - ) -> Result { - let event_type = event.get_optional("type")?.unwrap(); - Self::do_emit(event_type, this, &ctx, Rest(vec![event]), false) - } - - fn event_names(this: This>, ctx: Ctx<'js>) -> Result>> { - let events = Self::resolve_events_from(&ctx, &this)?; - let events = events.read().or_throw(&ctx)?; - - let mut names = Vec::with_capacity(events.len()); - for (key, _entry) in events.iter() { - let value = match key { - EventKey::Symbol(symbol) => symbol.clone().into_value(), - EventKey::String(str) => JsString::from_str(ctx.clone(), str)?.into(), - }; - - names.push(value) - } - - Ok(names) - } - - fn listener_count(this: This>, ctx: Ctx<'js>, event: Value<'js>) -> Result { - let events = Self::resolve_events_from(&ctx, &this)?; - let key = EventKey::from_value(&ctx, event)?; - let events = events.read().or_throw(&ctx)?; - Ok(events - .iter() - .find(|(k, _)| k == &key) - .map_or(0, |(_, items)| items.len())) - } - - fn remove_all_listeners( - this: This>, - ctx: Ctx<'js>, - event: Opt>, - ) -> Result> { - let events = Self::resolve_events_from(&ctx, &this)?; - let mut events = events.write().or_throw(&ctx)?; - match event.0 { - Some(event) if !event.is_undefined() => { - let key = EventKey::from_value(&ctx, event)?; - events.retain(|(k, _)| k != &key); - } - _ => events.clear(), - } - Ok(this.0) - } -} - -fn find_all_listeners<'js>( - events: Arc>>, - key: EventKey<'js>, -) -> Vec> { - let events = events.read().unwrap(); - let items = events.iter().find(|(k, _)| k == &key); - if let Some((_, callbacks)) = items { - callbacks.iter().map(|item| item.callback.clone()).collect() - } else { - vec![] - } -} - -fn has_key<'js>(event_list: Arc>>, key: EventKey<'js>) -> bool { - event_list.read().unwrap().iter().any(|(k, _)| k == &key) -} - -fn to_event<'js>(ctx: &Ctx<'js>, event: &str) -> Result> { - let event = JsString::from_str(ctx.clone(), event)?; - Ok(event.into_value()) -} - -pub struct EventsModule; - -impl ModuleDef for EventsModule { - fn declare(declare: &Declarations) -> Result<()> { - declare.declare(stringify!(EventEmitter))?; - declare.declare("default")?; - - Ok(()) - } - - fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { - let ctor = Class::::create_constructor(ctx)? - .expect("Can't create EventEmitter constructor"); - ctor.set(stringify!(EventEmitter), ctor.clone())?; - exports.export(stringify!(EventEmitter), ctor.clone())?; - exports.export("default", ctor)?; - - EventEmitter::add_event_emitter_prototype(ctx)?; - - Ok(()) - } -} - -impl From for ModuleInfo { - fn from(val: EventsModule) -> Self { - ModuleInfo { - name: "events", - module: val, - } - } -} - -pub fn init(ctx: &Ctx<'_>) -> Result<()> { - let globals = ctx.globals(); - - Class::::define(&globals)?; - Class::::define(&globals)?; - Class::::define(&globals)?; - - EventTarget::add_event_target_prototype(ctx)?; - - Ok(()) -} diff --git a/stdlib/src/llrt/llrt_exceptions/lib.rs b/stdlib/src/llrt/llrt_exceptions/lib.rs deleted file mode 100644 index bb27e895..00000000 --- a/stdlib/src/llrt/llrt_exceptions/lib.rs +++ /dev/null @@ -1,464 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use core::fmt; -use std::fmt::Debug; - -use crate::llrt_utils::{ - object::define_subclass, - option::Undefined, - primordials::{BasePrimordials, Primordial}, -}; -use rquickjs::{ - atom::PredefinedAtom, - class::{ - impl_::{CloneTrait, CloneWrapper}, - JsClass, Trace, - }, - function::{Constructor, Opt}, - object::{Accessor, Property}, - prelude::{Func, This}, - qjs, Class, Coerced, Ctx, Error, Exception, FromJs, IntoJs, JsLifetime, Object, Result, Value, -}; - -#[derive(JsLifetime)] -struct ExceptionPrimordials<'js> { - constructor_dom_exception: Constructor<'js>, - constructor_quota_exceeded_error: Constructor<'js>, -} - -impl<'js> Primordial<'js> for ExceptionPrimordials<'js> { - fn new(ctx: &Ctx<'js>) -> Result { - let globals = ctx.globals(); - Ok(Self { - constructor_dom_exception: globals.get(DOMException::NAME)?, - constructor_quota_exceeded_error: globals.get("QuotaExceededError")?, - }) - } -} - -#[derive(Trace, JsLifetime, Debug)] -pub struct DOMException { - name: String, - message: String, - stack: String, - code: u8, -} - -impl fmt::Display for DOMException { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.debug_struct("DOMException") - .field("name", &self.name()) - .field("message", &self.message()) - .field("stack", &self.stack) - .finish() - } -} - -fn add_constants(obj: &Object<'_>) -> Result<()> { - const CONSTANTS: [(&str, u8); 25] = [ - ("INDEX_SIZE_ERR", 1), - ("DOMSTRING_SIZE_ERR", 2), - ("HIERARCHY_REQUEST_ERR", 3), - ("WRONG_DOCUMENT_ERR", 4), - ("INVALID_CHARACTER_ERR", 5), - ("NO_DATA_ALLOWED_ERR", 6), - ("NO_MODIFICATION_ALLOWED_ERR", 7), - ("NOT_FOUND_ERR", 8), - ("NOT_SUPPORTED_ERR", 9), - ("INUSE_ATTRIBUTE_ERR", 10), - ("INVALID_STATE_ERR", 11), - ("SYNTAX_ERR", 12), - ("INVALID_MODIFICATION_ERR", 13), - ("NAMESPACE_ERR", 14), - ("INVALID_ACCESS_ERR", 15), - ("VALIDATION_ERR", 16), - ("TYPE_MISMATCH_ERR", 17), - ("SECURITY_ERR", 18), - ("NETWORK_ERR", 19), - ("ABORT_ERR", 20), - ("URL_MISMATCH_ERR", 21), - ("QUOTA_EXCEEDED_ERR", 22), - ("TIMEOUT_ERR", 23), - ("INVALID_NODE_TYPE_ERR", 24), - ("DATA_CLONE_ERR", 25), - ]; - - for (key, value) in CONSTANTS { - obj.prop(key, Property::from(value).enumerable())?; - } - - Ok(()) -} - -impl<'js> JsClass<'js> for DOMException { - const NAME: &'static str = "DOMException"; - type Mutable = rquickjs::class::Writable; - fn prototype(ctx: &Ctx<'js>) -> rquickjs::Result>> { - use rquickjs::class::impl_::{MethodImpl, MethodImplementor}; - let proto = Object::new(ctx.clone())?; - let implementor = MethodImpl::::new(); - implementor.implement(&proto)?; - add_constants(&proto)?; - - Ok(Some(proto)) - } - fn constructor(ctx: &Ctx<'js>) -> Result>> { - use rquickjs::class::impl_::{ConstructorCreate, ConstructorCreator}; - let implementor = ConstructorCreate::::new(); - let constructor = implementor - .create_constructor(ctx)? - .expect("DOMException must have a constructor"); - add_constants(&constructor)?; - - Ok(Some(constructor)) - } -} - -impl<'js> IntoJs<'js> for DOMException { - fn into_js(self, ctx: &rquickjs::Ctx<'js>) -> Result> { - let cls = Class::::instance(ctx.clone(), self)?; - IntoJs::into_js(cls, ctx) - } -} - -impl<'js> FromJs<'js> for DOMException -where - for<'a> CloneWrapper<'a, Self>: CloneTrait, -{ - fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let value = Class::::from_js(ctx, value)?; - let borrow = value.try_borrow()?; - Ok(CloneWrapper(&*borrow).wrap_clone()) - } -} - -#[rquickjs::methods] -impl DOMException { - #[qjs(constructor)] - pub fn new<'js>( - ctx: Ctx<'js>, - this: This>, - message: Opt>>, - name: Opt>>, - ) -> Result { - // When called with `new`, rquickjs passes the constructor function - // as `this`. Without `new` this is undefined or the global object. - if this.0.as_function().is_none() { - return Err(Exception::throw_type( - &ctx, - "Cannot call the DOMException constructor without 'new'", - )); - } - - let message = match message.0 { - Some(Undefined(Some(message))) => message.0, - _ => String::new(), - }; - - let name = match name.0 { - Some(Undefined(Some(message))) => DOMExceptionName::from(message.0), - _ => DOMExceptionName::Error, - }; - - Self::new_with_name(&ctx, name, message) - } - - #[qjs(skip)] - pub fn new_with_name(ctx: &Ctx<'_>, name: DOMExceptionName, message: String) -> Result { - let primordials = BasePrimordials::get(ctx)?; - - let new: Object = primordials - .constructor_error - .construct((message.clone(),))?; - - Ok(Self { - name: name.as_str().to_string(), - code: name.code(), - message, - stack: new.get::<_, String>(PredefinedAtom::Stack)?, - }) - } - - #[qjs(get, enumerable, configurable)] - fn message(&self) -> &str { - self.message.as_str() - } - - #[qjs(get, enumerable, configurable)] - pub fn name(&self) -> &str { - self.name.as_str() - } - - #[qjs(get, enumerable, configurable)] - pub fn code(&self) -> u8 { - self.code - } - - #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] - pub fn to_string_tag() -> &'static str { - stringify!(DOMException) - } -} - -impl<'js> DOMException { - fn create( - ctx: &Ctx<'js>, - name: DOMExceptionName, - message: impl Into, - ) -> Result> { - let primordials = ExceptionPrimordials::get(ctx)?; - let ctor = match name { - DOMExceptionName::QuotaExceededError => &primordials.constructor_quota_exceeded_error, - _ => &primordials.constructor_dom_exception, - }; - ctor.construct((message.into(), name.as_str())) - } - - fn throw_value(ctx: &Ctx<'js>, value: Value<'js>) -> Error { - unsafe { - let dup = qjs::JS_DupValue(ctx.as_raw().as_ptr(), value.as_raw()); - qjs::JS_Throw(ctx.as_raw().as_ptr(), dup); - } - Error::Exception - } - - fn create_error(ctx: &Ctx<'js>, name: DOMExceptionName, message: impl Into) -> Error { - let value = Self::create(ctx, name, message).expect("failed to create DOMException"); - Self::throw_value(ctx, value) - } - - pub fn not_supported_error(ctx: &Ctx<'js>, message: impl Into) -> Error { - Self::create_error(ctx, DOMExceptionName::NotSupportedError, message) - } - - pub fn type_mismatch_error(ctx: &Ctx<'js>, message: impl Into) -> Error { - Self::create_error(ctx, DOMExceptionName::TypeMismatchError, message) - } - - pub fn operation_error(ctx: &Ctx<'js>, message: impl Into) -> Error { - Self::create_error(ctx, DOMExceptionName::OperationError, message) - } - - pub fn quota_exceeded_error(ctx: &Ctx<'js>, message: impl Into) -> Error { - Self::create_error(ctx, DOMExceptionName::QuotaExceededError, message) - } - - pub fn data_error(ctx: &Ctx<'js>, message: impl Into) -> Error { - Self::create_error(ctx, DOMExceptionName::DataError, message) - } - - pub fn invalid_access_error(ctx: &Ctx<'js>, message: impl Into) -> Error { - Self::create_error(ctx, DOMExceptionName::InvalidAccessError, message) - } - - fn define_quota_exceeded_error(ctx: &Ctx<'js>) -> Result<()> { - let dom_exception: Constructor = ctx.globals().get(Self::NAME)?; - let quota_exceeded_error = define_subclass( - ctx, - "QuotaExceededError", - &dom_exception, - |ctx, message: Opt>>| { - let message = match message.0 { - Some(Undefined(Some(m))) => m.0, - _ => String::new(), - }; - Self::new_with_name(&ctx, DOMExceptionName::QuotaExceededError, message) - }, - )?; - let null = Value::new_null(ctx.clone()); - let proto: Object = quota_exceeded_error.get(PredefinedAtom::Prototype)?; - proto.prop( - "requested", - Property::from(null.clone()).enumerable().configurable(), - )?; - proto.prop("quota", Property::from(null).enumerable().configurable())?; - ctx.globals().prop( - "QuotaExceededError", - Property::from(quota_exceeded_error) - .writable() - .configurable(), - ) - } -} - -macro_rules! create_dom_exception { - ($name:ident, $($variant:ident),+ $(,)?) => { - #[derive(Debug)] - pub enum $name { - $( - $variant, - )+ - Other(String), - } - - impl $name { - pub fn as_str(&self) -> &str { - match self { - $( - Self::$variant => stringify!($variant), - )+ - Self::Other(value) => value, - } - } - } - - impl From for $name { - fn from(value: String) -> Self { - match value.as_str() { - $( - stringify!($variant) => Self::$variant, - )+ - _ => Self::Other(value), - } - } - } - }; -} - -// https://webidl.spec.whatwg.org/#dfn-error-names-table -create_dom_exception!( - DOMExceptionName, - IndexSizeError, - HierarchyRequestError, - WrongDocumentError, - InvalidCharacterError, - NoModificationAllowedError, - NotFoundError, - NotSupportedError, - InUseAttributeError, - InvalidStateError, - SyntaxError, - InvalidModificationError, - NamespaceError, - InvalidAccessError, - TypeMismatchError, - SecurityError, - NetworkError, - AbortError, - URLMismatchError, - QuotaExceededError, - TimeoutError, - InvalidNodeTypeError, - DataCloneError, - EncodingError, - NotReadableError, - UnknownError, - ConstraintError, - DataError, - TransactionInactiveError, - ReadOnlyError, - VersionError, - OperationError, - NotAllowedError, - Error, -); - -impl DOMExceptionName { - fn code(&self) -> u8 { - match self { - DOMExceptionName::IndexSizeError => 1, - DOMExceptionName::HierarchyRequestError => 3, - DOMExceptionName::WrongDocumentError => 4, - DOMExceptionName::InvalidCharacterError => 5, - DOMExceptionName::NoModificationAllowedError => 7, - DOMExceptionName::NotFoundError => 8, - DOMExceptionName::NotSupportedError => 9, - DOMExceptionName::InUseAttributeError => 10, - DOMExceptionName::InvalidStateError => 11, - DOMExceptionName::SyntaxError => 12, - DOMExceptionName::InvalidModificationError => 13, - DOMExceptionName::NamespaceError => 14, - DOMExceptionName::InvalidAccessError => 15, - DOMExceptionName::TypeMismatchError => 17, - DOMExceptionName::SecurityError => 18, - DOMExceptionName::NetworkError => 19, - DOMExceptionName::AbortError => 20, - DOMExceptionName::URLMismatchError => 21, - DOMExceptionName::QuotaExceededError => 22, - DOMExceptionName::TimeoutError => 23, - DOMExceptionName::InvalidNodeTypeError => 24, - DOMExceptionName::DataCloneError => 25, - _ => 0, - } - } -} - -pub fn init(ctx: &Ctx<'_>) -> Result<()> { - let globals = ctx.globals(); - - BasePrimordials::init(ctx)?; - - if let Some(constructor) = Class::::create_constructor(ctx)? { - // the wpt tests expect this particular property descriptor - globals.prop( - DOMException::NAME, - Property::from(constructor).writable().configurable(), - )?; - } - - let dom_ex_proto = Class::::prototype(ctx)?.unwrap(); - dom_ex_proto.set_prototype(Some(&BasePrimordials::get(ctx)?.prototype_error))?; - - DOMException::define_quota_exceeded_error(ctx)?; - ExceptionPrimordials::init(ctx)?; - - // `Error.isError(v)` only returns `true` for objects with QuickJS's - // `[[ErrorData]]` internal slot (class id `JS_CLASS_ERROR`). There is - // no public rquickjs API to tag a class-derived instance with that - // slot, so we replace `Error.isError` with a version that also - // recognizes `DOMException` instances (and its subclasses) via - // `instanceof`. - BasePrimordials::get(ctx)? - .constructor_error - .set("isError", Func::from(is_error))?; - - define_error_stack_accessor(ctx)?; - - Ok(()) -} - -// https://tc39.es/proposal-error-stack-accessor/ moves `stack` to an accessor -// on `Error.prototype`, so DOMException inherits it instead of exposing its own. -// QuickJS still gives plain Error instances an own `stack` data property, which -// shadows this accessor, so the getter only runs for DOMException instances. -fn define_error_stack_accessor<'js>(ctx: &Ctx<'js>) -> Result<()> { - let prototype_error = BasePrimordials::get(ctx)?.prototype_error.clone(); - prototype_error.prop( - PredefinedAtom::Stack, - Accessor::new( - |this: This>| -> Result { - let stack = Class::::from_value(&this.0) - .ok() - .map(|cls| cls.borrow().stack.clone()); - Ok(stack.unwrap_or_default()) - }, - |ctx: Ctx<'js>, this: This>, value: Value<'js>| -> Result<()> { - // SetterThatIgnoresPrototypeProperties: never install on the - // home object itself. - let Some(obj) = this.0.as_object() else { - return Ok(()); - }; - if *obj == BasePrimordials::get(&ctx)?.prototype_error { - return Ok(()); - } - obj.prop( - PredefinedAtom::Stack, - Property::from(value).writable().enumerable().configurable(), - ) - }, - ) - .configurable(), - ) -} - -fn is_error<'js>(ctx: Ctx<'js>, value: Value<'js>) -> Result { - if value.is_error() { - return Ok(true); - } - let Some(obj) = value.as_object() else { - return Ok(false); - }; - let dom_exception: Value = ctx.globals().get(DOMException::NAME)?; - Ok(obj.is_instance_of(&dom_exception)) -} diff --git a/stdlib/src/llrt/llrt_hooking/lib.rs b/stdlib/src/llrt/llrt_hooking/lib.rs deleted file mode 100644 index ec560b66..00000000 --- a/stdlib/src/llrt/llrt_hooking/lib.rs +++ /dev/null @@ -1,88 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::env; - -use crate::llrt_utils::{object::ObjectExt, provider::ProviderType}; -use once_cell::sync::Lazy; -use rquickjs::{Ctx, Exception, Function, Result, Value}; - -pub static HOOKING_MODE: Lazy = - Lazy::new(|| env::var("LLRT_ASYNC_HOOKS").as_deref() == Ok("1")); - -#[derive(PartialEq)] -pub enum HookType { - Init, - Before, - After, -} - -pub fn invoke_async_hook( - ctx: &Ctx<'_>, - hook_type: HookType, - provider_type: ProviderType, - uid: usize, -) -> Result<()> { - if !HOOKING_MODE.to_owned() { - return Ok(()); - } - - let hook_ = match hook_type { - HookType::Init => "init", - HookType::Before => "before", - HookType::After => "after", - }; - - let provider_ = match provider_type { - ProviderType::None if hook_type != HookType::Init => "", - ProviderType::None => { - return Err(Exception::throw_type( - ctx, - "Asynchronous types cannot be omitted in init hooks.", - )) - } - ProviderType::Resource(s) => &["Resource(", &s, ")"].concat(), - // Userland provider types - ProviderType::Immediate => "Immediate", - ProviderType::Interval => "Interval", - ProviderType::MessagePort => "MessagePort", - ProviderType::Microtask => "Microtask", - ProviderType::TickObject => "TickObject", - ProviderType::Timeout => "Timeout", - // Internal provider types - ProviderType::FsReqCallback => "FSREQCALLBACK", - ProviderType::GetAddrInfoReqWrap => "GETADDRINFOREQWRAP", - ProviderType::GetNameInfoReqWrap => "GETNAMEINFOREQWRAP", - ProviderType::PipeWrap => "PIPEWRAP", - ProviderType::StatWatcher => "STATWACHER", - ProviderType::TcpWrap => "TCPWRAP", - ProviderType::TimerWrap => "TIMERWRAP", - ProviderType::TlsWrap => "TLSWRAP", - ProviderType::UdpWrap => "UDPWRAP", - }; - - let invoke_async_hook = ctx - .globals() - .get_optional::<_, Function>("invokeAsyncHook")?; - if let Some(func) = &invoke_async_hook { - func.call::<_, ()>((hook_, provider_, uid))?; - } - Ok(()) -} - -pub fn register_finalization_registry<'js>( - ctx: &Ctx<'js>, - target: Value<'js>, - uid: usize, -) -> Result<()> { - if !HOOKING_MODE.to_owned() { - return Ok(()); - } - - if let Ok(register) = - ctx.eval::, &str>("globalThis.asyncFinalizationRegistry.register") - { - let _ = register.call::<_, ()>((target, uid)); - } - Ok(()) -} diff --git a/stdlib/src/llrt/llrt_json/escape.rs b/stdlib/src/llrt/llrt_json/escape.rs deleted file mode 100644 index 9950d338..00000000 --- a/stdlib/src/llrt/llrt_json/escape.rs +++ /dev/null @@ -1,341 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -static JSON_ESCAPE_CHARS: [u8; 256] = [ - 0u8, 1u8, 2u8, 3u8, 4u8, 5u8, 6u8, 7u8, 8u8, 9u8, 10u8, 11u8, 12u8, 13u8, 14u8, 15u8, 16u8, - 17u8, 18u8, 19u8, 20u8, 21u8, 22u8, 23u8, 24u8, 25u8, 26u8, 27u8, 28u8, 29u8, 30u8, 31u8, 34u8, - 34u8, 32u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, - 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, - 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, - 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 33u8, 34u8, 34u8, 34u8, 34u8, - 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, - 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, - 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, - 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, - 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, - 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, - 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, - 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, - 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, - 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, 34u8, -]; -static JSON_ESCAPE_QUOTES: [&str; 34usize] = [ - "\\u0000", "\\u0001", "\\u0002", "\\u0003", "\\u0004", "\\u0005", "\\u0006", "\\u0007", "\\b", - "\\t", "\\n", "\\u000b", "\\f", "\\r", "\\u000e", "\\u000f", "\\u0010", "\\u0011", "\\u0012", - "\\u0013", "\\u0014", "\\u0015", "\\u0016", "\\u0017", "\\u0018", "\\u0019", "\\u001a", - "\\u001b", "\\u001c", "\\u001d", "\\u001e", "\\u001f", "\\\"", "\\\\", -]; - -const ESCAPE_LEN: usize = 34; - -#[cold] -#[inline(always)] -fn write_surrogate_escape(result: &mut String, bytes: &[u8], i: usize) -> usize { - let code_point = ((bytes[i] as u16 & 0x0F) << 12) - | ((bytes[i + 1] as u16 & 0x3F) << 6) - | (bytes[i + 2] as u16 & 0x3F); - - result.push_str("\\u"); - let hex = [ - (code_point >> 12) as u8, - ((code_point >> 8) & 0xF) as u8, - ((code_point >> 4) & 0xF) as u8, - (code_point & 0xF) as u8, - ]; - for h in hex { - result.push(if h < 10 { - (b'0' + h) as char - } else { - (b'a' + h - 10) as char - }); - } - 3 -} - -#[allow(dead_code)] -pub fn escape_json(bytes: &[u8]) -> String { - let mut result = String::new(); - escape_json_string(&mut result, bytes); - result -} - -#[inline(always)] -fn process_byte( - result: &mut String, - bytes: &[u8], - byte: u8, - i: &mut usize, - start: &mut usize, - len: usize, -) { - // Fast path for simple escapes ({<32, 34, 92}); 0xED is filtered out here - // because JSON_ESCAPE_CHARS[0xED] == ESCAPE_LEN. - let c = JSON_ESCAPE_CHARS[byte as usize] as usize; - if c < ESCAPE_LEN { - // SAFETY: c < JSON_ESCAPE_QUOTES.len(); start <= i <= bytes.len(). - let esc = unsafe { JSON_ESCAPE_QUOTES.get_unchecked(c) }.as_bytes(); - let pending = unsafe { bytes.get_unchecked(*start..*i) }; - // Branch-free flush: one reserve + two memcpys (pending may be empty). - unsafe { - let vec = result.as_mut_vec(); - let total = pending.len() + esc.len(); - vec.reserve(total); - let cur = vec.len(); - let dst = vec.as_mut_ptr().add(cur); - std::ptr::copy_nonoverlapping(pending.as_ptr(), dst, pending.len()); - std::ptr::copy_nonoverlapping(esc.as_ptr(), dst.add(pending.len()), esc.len()); - vec.set_len(cur + total); - } - *i += 1; - *start = *i; - return; - } - - // WTF-8 lone surrogate (0xED A0..BF 80..BF) -> \uXXXX. Otherwise pass through. - if byte == 0xED && *i + 2 < len && (bytes[*i + 1] & 0xF0) >= 0xA0 { - if *start < *i { - // SAFETY: start <= i <= len; bytes through i are valid UTF-8/WTF-8. - result.push_str(unsafe { - std::str::from_utf8_unchecked(bytes.get_unchecked(*start..*i)) - }); - } - *i += write_surrogate_escape(result, bytes, *i); - *start = *i; - return; - } - *i += 1; -} - -/// SWAR escape-byte detector: sets the high bit of each byte in the returned -/// u64 for any input byte matching `< 32 || == 34 || == 92 || == 0xED`. May -/// produce false positives (caller's `process_byte` re-validates via the -/// escape table). Little-endian load so byte k -> bit (k*8); recover via -/// `trailing_zeros() / 8`. -#[inline(always)] -fn chunk_escape_mask(chunk: &[u8; 8]) -> u64 { - const ONES: u64 = 0x0101_0101_0101_0101; - const HIGH: u64 = 0x8080_8080_8080_8080; - let x = u64::from_le_bytes(*chunk); - let lt32 = x.wrapping_sub(0x20 * ONES) & !x; - let eq34 = { - let y = x ^ (0x22 * ONES); - y.wrapping_sub(ONES) & !y - }; - let eq92 = { - let y = x ^ (0x5C * ONES); - y.wrapping_sub(ONES) & !y - }; - let eqed = { - let y = x ^ (0xED * ONES); - y.wrapping_sub(ONES) & !y - }; - (lt32 | eq34 | eq92 | eqed) & HIGH -} - -/// Append a JSON-escaped form of `bytes` to `result`. -/// -/// Accepts UTF-8 or WTF-8 (QuickJS uses WTF-8 for JS strings with lone -/// surrogates). Scans 64 bytes at a time as 8x 8-byte SWAR masks; clean -/// strides are skipped without copying, dirty halves jump byte-to-byte via -/// `trailing_zeros`. The trailing <64 bytes are swept the same way and the -/// final <8 fall through to `process_byte`. -#[inline(always)] -pub fn escape_json_string_simple(result: &mut String, bytes: &[u8]) { - let len = bytes.len(); - let mut start = 0; - let mut i = 0; - // Headroom: small strings can expand up to 6x (all-control to \uXXXX); - // larger inputs see <25% density in practice. No-op when `result` is - // already pre-sized (common stringify-accumulator case). - let headroom = if len < 128 { - len * 5 + 16 - } else { - len / 4 + 16 - }; - result.reserve(len + headroom); - - let (chunks64, tail) = bytes.as_chunks::<64>(); - - let mut base = 0usize; - for chunk64 in chunks64 { - // Hand-unrolled to keep 8 independent SWAR dependency chains visible; - // LLVM doesn't reliably do this from a fixed-size array loop. - macro_rules! mask_at { - ($off:expr) => { - chunk_escape_mask((&chunk64[$off..$off + 8]).try_into().unwrap()) - }; - } - let m_0 = mask_at!(0); - let m_1 = mask_at!(8); - let m_2 = mask_at!(16); - let m_3 = mask_at!(24); - let m_4 = mask_at!(32); - let m_5 = mask_at!(40); - let m_6 = mask_at!(48); - let m_7 = mask_at!(56); - if (m_0 | m_1 | m_2 | m_3 | m_4 | m_5 | m_6 | m_7) == 0 { - i = base + 64; - } else { - macro_rules! dispatch { - ($off:expr, $mask:expr) => { - process_dirty_half(result, bytes, base + $off, $mask, &mut i, &mut start, len) - }; - } - dispatch!(0, m_0); - dispatch!(8, m_1); - dispatch!(16, m_2); - dispatch!(24, m_3); - dispatch!(32, m_4); - dispatch!(40, m_5); - dispatch!(48, m_6); - dispatch!(56, m_7); - } - base += 64; - } - - // 0..=63-byte tail: SWAR-sweep 8-byte sub-chunks, then byte-by-byte for <8. - let (sub_chunks, _sub_tail) = tail.as_chunks::<8>(); - for (k, sub) in sub_chunks.iter().enumerate() { - let mask = chunk_escape_mask(sub); - process_dirty_half(result, bytes, base + k * 8, mask, &mut i, &mut start, len); - } - - while i < len { - process_byte(result, bytes, bytes[i], &mut i, &mut start, len); - } - - if start < len { - result.push_str(unsafe { std::str::from_utf8_unchecked(&bytes[start..len]) }); - } -} - -#[inline(always)] -fn process_dirty_half( - result: &mut String, - bytes: &[u8], - half_start: usize, - mask: u64, - i: &mut usize, - start: &mut usize, - len: usize, -) { - let half_end = half_start + 8; - if mask == 0 { - *i = (*i).max(half_end); - return; - } - // A surrogate from the previous half may have consumed up to 2 bytes - // into this one; drop mask bits for those positions. - let mut m = mask & (!0u64 << ((*i - half_start) * 8)); - // Single-bit fast path skips the loop's mask-clearing shift. - if m.count_ones() == 1 { - *i = half_start + (m.trailing_zeros() as usize) / 8; - process_byte(result, bytes, bytes[*i], i, start, len); - *i = (*i).max(half_end); - return; - } - while m != 0 { - *i = half_start + (m.trailing_zeros() as usize) / 8; - process_byte(result, bytes, bytes[*i], i, start, len); - // checked_shl handles consumed >= 8 (shift >= 64) by zeroing m. - let consumed = *i - half_start; - m &= (!0u64).checked_shl((consumed as u32) * 8).unwrap_or(0); - } - *i = (*i).max(half_end); -} - -pub fn escape_json_string(result: &mut String, bytes: &[u8]) { - escape_json_string_simple(result, bytes); -} - -#[cfg(test)] -mod tests { - use crate::llrt_json::escape::escape_json; - - #[test] - fn escape_json_simple() { - assert_eq!(escape_json(b"Hello, World!"), "Hello, World!"); - } - - #[test] - fn escape_json_quotes() { - assert_eq!(escape_json(b"\"quoted\""), "\\\"quoted\\\""); - } - - #[test] - fn escape_json_backslash() { - assert_eq!(escape_json(b"back\\slash"), "back\\\\slash"); - } - - #[test] - fn escape_json_newline() { - assert_eq!(escape_json(b"line\nbreak"), "line\\nbreak"); - } - - #[test] - fn escape_json_tab() { - assert_eq!(escape_json(b"tab\tcharacter"), "tab\\tcharacter"); - } - - #[test] - fn escape_json_unicode() { - assert_eq!( - escape_json("unicode: \u{1F609}".as_bytes()), - "unicode: \u{1F609}" - ); - } - - #[test] - fn escape_json_special_characters() { - assert_eq!( - escape_json(b"!@#$%^&*()_+-=[]{}|;':,.<>?/"), - "!@#$%^&*()_+-=[]{}|;':,.<>?/" - ); - } - - #[test] - fn escape_json_mixed_characters() { - assert_eq!( - escape_json(b"123\"\"45678901\"234567"), - "123\\\"\\\"45678901\\\"234567" - ); - } - - // WTF-8 lone surrogate sequences — emitted by QuickJS when a String contains - // lone surrogate code points (e.g. from JSON.stringify("\uD800")). These must - // be escaped as `\uXXXX` even though they're not valid UTF-8. - #[test] - fn escape_json_lone_surrogate() { - // U+D800 in WTF-8 is 0xED 0xA0 0x80. - assert_eq!(escape_json(&[0xED, 0xA0, 0x80]), "\\ud800"); - } - - #[test] - fn escape_json_lone_surrogate_with_context() { - // Make sure surrogates at different alignments (within, across chunk - // boundaries) are handled correctly. - let mut input = b"abcdefg".to_vec(); // 7 bytes before surrogate - input.extend_from_slice(&[0xED, 0xBF, 0xBF]); // U+DFFF - input.extend_from_slice(b"xyz"); - assert_eq!(escape_json(&input), "abcdefg\\udfffxyz"); - } - - #[test] - fn escape_json_surrogate_at_chunk_boundary() { - // Surrogate starts at byte index 6, spans past the 8-byte chunk boundary. - let mut input = b"abcdef".to_vec(); // 6 bytes - input.extend_from_slice(&[0xED, 0xA0, 0x80]); // U+D800, ends at index 9 - input.extend_from_slice(b"xyz123456789"); - let expected = "abcdef\\ud800xyz123456789"; - assert_eq!(escape_json(&input), expected); - } - - #[test] - fn escape_json_korean_passthrough() { - // Valid Korean Hangul (U+D6C8 "훈") is encoded 0xED 0x9B 0x88 — the - // second byte has high nibble 0x90 < 0xA0 so it must NOT be escaped. - let s = "훈훈훈"; - assert_eq!(escape_json(s.as_bytes()), s); - } -} diff --git a/stdlib/src/llrt/llrt_json/lib.rs b/stdlib/src/llrt/llrt_json/lib.rs deleted file mode 100644 index c4269943..00000000 --- a/stdlib/src/llrt/llrt_json/lib.rs +++ /dev/null @@ -1,233 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::cmp::min; - -use rquickjs::{ - atom::PredefinedAtom, function::Opt, prelude::Func, Ctx, IntoJs, Object, Result, Value, -}; - -pub mod escape; -pub mod parse; -pub mod stringify; - -use crate::llrt_json::parse::json_parse_string; -use crate::llrt_json::stringify::json_stringify_replacer_space; - -pub fn redefine_static_methods(ctx: &Ctx<'_>) -> Result<()> { - let globals = ctx.globals(); - let json_module: Object = globals.get(PredefinedAtom::JSON)?; - json_module.set("parse", Func::from(json_parse_string))?; - json_module.set( - "stringify", - Func::from(|ctx, value, replacer, space| { - struct StringifyArgs<'js>(Ctx<'js>, Value<'js>, Opt>, Opt>); - let StringifyArgs(ctx, value, replacer, space) = - StringifyArgs(ctx, value, replacer, space); - - let mut space_value = None; - let mut replacer_value = None; - - if let Some(replacer) = replacer.0 { - if let Some(space) = space.0 { - if let Some(space) = space.as_string() { - let mut space = space.clone().to_string()?; - space.truncate(20); - space_value = Some(space); - } - if let Some(number) = space.as_int() { - if number > 0 { - space_value = Some(" ".repeat(min(10, number as usize))); - } - } - } - replacer_value = Some(replacer); - } - - json_stringify_replacer_space(&ctx, value, replacer_value, space_value) - .map(|v| v.into_js(&ctx))? - }), - )?; - Ok(()) -} - -#[cfg(test)] -mod tests { - use crate::llrt_test::test_sync_with; - use rquickjs::{prelude::Func, Array, CatchResultExt, IntoJs, Null, Object, Undefined, Value}; - - use crate::llrt_json::{ - parse::{json_parse, json_parse_string}, - stringify::{json_stringify, json_stringify_replacer_space}, - }; - - static JSON: &str = r#"{"organization":{"name":"TechCorp","founding_year":2000,"departments":[{"name":"Engineering","head":{"name":"Alice Smith","title":"VP of Engineering","contact":{"email":"alice.smith@techcorp.com","phone":"+1 (555) 123-4567"}},"employees":[{"id":101,"name":"Bob Johnson","position":"Software Engineer","contact":{"email":"bob.johnson@techcorp.com","phone":"+1 (555) 234-5678"},"projects":[{"project_id":"P001","name":"Project A","status":"In Progress","description":"Developing a revolutionary software solution for clients.","start_date":"2023-01-15","end_date":null,"team":[{"id":201,"name":"Sara Davis","role":"UI/UX Designer"},{"id":202,"name":"Charlie Brown","role":"Quality Assurance Engineer"}]},{"project_id":"P002","name":"Project B","status":"Completed","description":"Upgrading existing systems to enhance performance.","start_date":"2022-05-01","end_date":"2022-11-30","team":[{"id":203,"name":"Emily White","role":"Systems Architect"},{"id":204,"name":"James Green","role":"Database Administrator"}]}]},{"id":102,"name":"Carol Williams","position":"Senior Software Engineer","contact":{"email":"carol.williams@techcorp.com","phone":"+1 (555) 345-6789"},"projects":[{"project_id":"P001","name":"Project A","status":"In Progress","description":"Working on the backend development of Project A.","start_date":"2023-01-15","end_date":null,"team":[{"id":205,"name":"Alex Turner","role":"DevOps Engineer"},{"id":206,"name":"Mia Garcia","role":"Software Developer"}]},{"project_id":"P003","name":"Project C","status":"Planning","description":"Researching and planning for a future project.","start_date":null,"end_date":null,"team":[]}]}]},{"name":"Marketing","head":{"name":"David Brown","title":"VP of Marketing","contact":{"email":"david.brown@techcorp.com","phone":"+1 (555) 456-7890"}},"employees":[{"id":201,"name":"Eva Miller","position":"Marketing Specialist","contact":{"email":"eva.miller@techcorp.com","phone":"+1 (555) 567-8901"},"campaigns":[{"campaign_id":"C001","name":"Product Launch","status":"Upcoming","description":"Planning for the launch of a new product line.","start_date":"2023-03-01","end_date":null,"team":[{"id":301,"name":"Oliver Martinez","role":"Graphic Designer"},{"id":302,"name":"Sophie Johnson","role":"Content Writer"}]},{"campaign_id":"C002","name":"Brand Awareness","status":"Ongoing","description":"Executing strategies to increase brand visibility.","start_date":"2022-11-15","end_date":"2023-01-31","team":[{"id":303,"name":"Liam Taylor","role":"Social Media Manager"},{"id":304,"name":"Ava Clark","role":"Marketing Analyst"}]}]}]}]}}"#; - - #[tokio::test] - async fn json_parser() { - test_sync_with(|ctx| { - let json_data = [ - r#"{"aa\"\"aaaaaaaaaaaaaaaa":"a","b":"bbb"}"#, - r#"{"a":"aaaaaaaaaaaaaaaaaa","b":"bbb"}"#, - r#"{"a":["a","a","aaaa","a"],"b":"b"}"#, - r#"{"type":"Buffer","data":[10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10,10]}"#, - r#"{"a":[{"object2":{"key1":"value1","key2":123,"key3":false,"nestedObject":{"nestedKey":"nestedValue"}},"string":"Hello, World!","emptyObj":{},"emptyArr":[],"number":42,"boolean":true,"nullValue":null,"array":[1,2,3,"four",5.5,true,null],"object":{"key1":"value1","key2":123,"key3":false,"nestedObject":{"nestedKey":"nestedValue"}}}]}"#, - JSON, - ]; - - for json_str in json_data { - let json = json_str.to_string(); - let json2 = json.clone(); - - let value = json_parse(&ctx, json2)?; - let new_json = json_stringify_replacer_space(&ctx, value.clone(),None,Some(" ".into()))?.unwrap(); - let builtin_json = ctx.json_stringify_replacer_space(value,Null," ".to_string())?.unwrap().to_string()?; - assert_eq!(new_json, builtin_json); - } - - Ok(()) - }) - .await; - } - - #[tokio::test] - async fn json_parse_non_string() { - test_sync_with(|ctx| { - ctx.globals().set("parse", Func::from(json_parse_string))?; - - let result = ctx.eval::<(), _>("parse({})").catch(&ctx); - - if let Err(err) = result { - assert_eq!( - err.to_string(), - "Error: \"[object Object]\" not valid JSON at index 1 ('o')\n at (eval_script:1:1)\n" - ); - } else { - panic!("expected error") - } - - Ok(()) - }) - .await; - } - - #[tokio::test] - async fn json_stringify_undefined() { - test_sync_with(|ctx| { - let stringified = json_stringify(&ctx, Undefined.into_js(&ctx)?)?; - let stringified_2 = ctx - .json_stringify(Undefined)? - .map(|v| v.to_string().unwrap()); - assert_eq!(stringified, stringified_2); - - let obj: Value = ctx.eval( - r#"let obj = { value: undefined, array: [undefined, null, 1, true, "hello", { [Symbol("sym")]: 1, [undefined]: 2}] };obj;"#, - )?; - - let stringified = json_stringify(&ctx, obj.clone())?; - let stringified_2 = ctx - .json_stringify(obj)? - .map(|v| v.to_string().unwrap()); - assert_eq!(stringified, stringified_2); - - Ok(()) - }) - .await; - } - - #[tokio::test] - async fn json_stringify_objects() { - test_sync_with(|ctx| { - let date: Value = ctx.eval("let obj = { date: new Date(0) };obj;")?; - let stringified = json_stringify(&ctx, date.clone())?.unwrap(); - let stringified_2 = ctx.json_stringify(date)?.unwrap().to_string()?; - assert_eq!(stringified, stringified_2); - Ok(()) - }) - .await; - } - - #[tokio::test] - async fn huge_numbers() { - test_sync_with(|ctx| { - - let big_int_value = json_parse(&ctx, b"99999999999999999999999999999999999999999999999999999999999999999999999999999999999")?; - - let stringified = json_stringify(&ctx, big_int_value.clone())?.unwrap(); - let stringified_2 = ctx.json_stringify(big_int_value)?.unwrap().to_string()?.replace("e+", "e"); - assert_eq!(stringified, stringified_2); - - let big_int_value: Value = ctx.eval("999999999999")?; - let stringified = json_stringify(&ctx, big_int_value.clone())?.unwrap(); - let stringified_2 = ctx.json_stringify(big_int_value)?.unwrap().to_string()?; - assert_eq!(stringified, stringified_2); - - Ok(()) - }) - .await; - } - - #[tokio::test] - async fn json_circular_ref() { - test_sync_with(|ctx| { - let obj1 = Object::new(ctx.clone())?; - let obj2 = Object::new(ctx.clone())?; - let obj3 = Object::new(ctx.clone())?; - let obj4 = Object::new(ctx.clone())?; - obj4.set("key", "value")?; - obj3.set("sub2", obj4.clone())?; - obj2.set("sub1", obj3)?; - obj1.set("root1", obj2.clone())?; - obj1.set("root2", obj2.clone())?; - obj1.set("root3", obj2.clone())?; - - let value = obj1.clone().into_value(); - - let stringified = json_stringify(&ctx, value.clone())?.unwrap(); - let stringified_2 = ctx.json_stringify(value.clone())?.unwrap().to_string()?; - assert_eq!(stringified, stringified_2); - - obj4.set("recursive", obj1.clone())?; - - let stringified = json_stringify(&ctx, value.clone()); - - if let Err(error_message) = stringified.catch(&ctx) { - let error_str = error_message.to_string(); - assert_eq!( - "Error: Circular reference detected at: \"...root1.sub1.sub2.recursive\"\n", - error_str - ) - } else { - panic!("Expected an error, but got Ok"); - } - - let array1 = Array::new(ctx.clone())?; - let array2 = Array::new(ctx.clone())?; - let array3 = Array::new(ctx.clone())?; - - let obj5 = Object::new(ctx.clone())?; - obj5.set("key", obj1.clone())?; - array3.set(2, obj5)?; - array2.set(1, array3)?; - array1.set(0, array2)?; - - obj4.remove("recursive")?; - obj1.set("recursiveArray", array1)?; - - let stringified = json_stringify(&ctx, value.clone()); - - if let Err(error_message) = stringified.catch(&ctx) { - let error_str = error_message.to_string(); - assert_eq!( - "Error: Circular reference detected at: \"...recursiveArray[0][1][2].key\"\n", - error_str - ) - } else { - panic!("Expected an error, but got Ok"); - } - - Ok(()) - }) - .await; - } -} diff --git a/stdlib/src/llrt/llrt_json/parse.rs b/stdlib/src/llrt/llrt_json/parse.rs deleted file mode 100644 index 4ee78fcd..00000000 --- a/stdlib/src/llrt/llrt_json/parse.rs +++ /dev/null @@ -1,105 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use crate::llrt_utils::bytes::ObjectBytes; -use rquickjs::{Array, Ctx, Exception, IntoJs, Null, Object, Result, Undefined, Value}; -use simd_json::{Node, StaticNode}; - -pub fn json_parse_string<'js>(ctx: Ctx<'js>, bytes: ObjectBytes<'js>) -> Result> { - let bytes = bytes.as_bytes(&ctx)?; - json_parse(&ctx, bytes) -} - -pub fn json_parse<'js, T: Into>>(ctx: &Ctx<'js>, json: T) -> Result> { - let mut json: Vec = json.into(); - let tape = match simd_json::to_tape(&mut json) { - Ok(tape) => tape, - Err(err) => { - // simd_json is strict about lone / unpaired surrogate escapes - // (`\uXXXX` where XXXX is a surrogate code point). Fall back to - // QuickJS's native `JSON.parse`, which is more permissive and is - // needed for spec compliance with content that round-trips - // through `JSON.stringify` of strings containing lone surrogates. - if err.character() == Some('u') { - if let Ok(value) = ctx.json_parse(json.as_slice()) { - return Ok(value); - } - } - let mut itoa = itoa::Buffer::new(); - let mut error_msg = String::with_capacity(256); - let json_length = json.len(); - if json_length < 128 { - error_msg.reserve(json_length); - error_msg.push('\"'); - error_msg.push_str(&std::string::String::from_utf8_lossy(&json)); - error_msg.push_str("\" "); - } - - error_msg.push_str("not valid JSON at index "); - error_msg.push_str(itoa.format(err.index())); - if let Some(char) = err.character() { - error_msg.push_str(" ('"); - error_msg.push(char); - error_msg.push_str("')"); - } - return Err(Exception::throw_syntax(ctx, &error_msg)); - } - }; - let tape = tape.0; - - if let Some(first) = tape.first() { - return match first { - Node::String(value) => value.into_js(ctx), - Node::Static(node) => static_node_to_value(ctx, *node), - _ => parse_node(ctx, &tape, 0).map(|(value, _)| value), - }; - } - - Undefined.into_js(ctx) -} - -#[inline(always)] -fn static_node_to_value<'js>(ctx: &Ctx<'js>, node: StaticNode) -> Result> { - match node { - StaticNode::I64(value) => value.into_js(ctx), - StaticNode::U64(value) => value.into_js(ctx), - StaticNode::F64(value) => value.into_js(ctx), - StaticNode::Bool(value) => value.into_js(ctx), - StaticNode::Null => Null.into_js(ctx), - } -} - -fn parse_node<'js>(ctx: &Ctx<'js>, tape: &[Node], index: usize) -> Result<(Value<'js>, usize)> { - match tape[index] { - Node::String(value) => Ok((value.into_js(ctx)?, index + 1)), - Node::Static(node) => Ok((static_node_to_value(ctx, node)?, index + 1)), - Node::Object { len, .. } => { - let js_object = Object::new(ctx.clone())?; - let mut current_index = index + 1; - - for _ in 0..len { - if let Node::String(key) = tape[current_index] { - current_index += 1; - let (value, new_index) = parse_node(ctx, tape, current_index)?; - current_index = new_index; - js_object.set(key, value)?; - } - } - - Ok((js_object.into_value(), current_index)) - } - Node::Array { len, .. } => { - let js_array = Array::new(ctx.clone())?; - let mut current_index = index + 1; - - for i in 0..len { - let (value, new_index) = parse_node(ctx, tape, current_index)?; - current_index = new_index; - js_array.set(i, value)?; - } - - Ok((js_array.into_value(), current_index)) - } - } -} diff --git a/stdlib/src/llrt/llrt_json/stringify.rs b/stdlib/src/llrt/llrt_json/stringify.rs deleted file mode 100644 index 7c6bb201..00000000 --- a/stdlib/src/llrt/llrt_json/stringify.rs +++ /dev/null @@ -1,552 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::{collections::HashSet, rc::Rc}; - -use rquickjs::{ - atom::PredefinedAtom, function::This, qjs, Ctx, Exception, Function, Object, Result, Type, - Value, -}; - -use crate::llrt_json::escape::escape_json_string; - -const CIRCULAR_REF_DETECTION_DEPTH: usize = 20; - -struct StringifyContext<'a, 'js> { - ctx: &'a Ctx<'js>, - result: &'a mut String, - value: &'a Value<'js>, - depth: usize, - indentation: Option<&'a str>, - key: Option<&'a str>, - index: Option, - parent: Option<&'a Object<'js>>, - ancestors: &'a mut Vec<(usize, Rc)>, - replacer_fn: Option<&'a Function<'js>>, - include_keys_replacer: Option<&'a HashSet>, - itoa_buffer: &'a mut itoa::Buffer, - ryu_buffer: &'a mut ryu::Buffer, -} - -#[allow(dead_code)] -pub fn json_stringify<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> Result> { - json_stringify_replacer_space(ctx, value, None, None) -} - -#[allow(dead_code)] -pub fn json_stringify_replacer<'js>( - ctx: &Ctx<'js>, - value: Value<'js>, - replacer: Option>, -) -> Result> { - json_stringify_replacer_space(ctx, value, replacer, None) -} - -pub fn json_stringify_replacer_space<'js>( - ctx: &Ctx<'js>, - value: Value<'js>, - replacer: Option>, - indentation: Option, -) -> Result> { - let mut result = String::with_capacity(128); - let mut replacer_fn = None; - let mut include_keys_replacer = None; - - let tmp_function; - - let mut itoa_buffer = itoa::Buffer::new(); - let mut ryu_buffer = ryu::Buffer::new(); - - if let Some(replacer) = replacer { - if let Some(function) = replacer.as_function() { - tmp_function = function.clone(); - replacer_fn = Some(&tmp_function); - } else if let Some(array) = replacer.as_array() { - let mut filter = HashSet::with_capacity(array.len()); - for value in array.clone().into_iter() { - let value = value?; - if let Some(string) = value.as_string() { - filter.insert(string.to_string()?); - } else if let Some(number) = value.as_int() { - filter.insert(itoa_buffer.format(number).to_string()); - } else if let Some(number) = value.as_float() { - filter.insert(ryu_buffer.format(number).to_string()); - } - } - include_keys_replacer = Some(filter); - } - } - - let indentation = indentation.as_deref(); - let include_keys_replacer = include_keys_replacer.as_ref(); - - let mut ancestors = Vec::with_capacity(10); - - let mut context = StringifyContext { - ctx, - result: &mut result, - value: &value, - depth: 0, - indentation: None, - key: None, - index: None, - parent: None, - ancestors: &mut ancestors, - replacer_fn, - include_keys_replacer, - itoa_buffer: &mut itoa_buffer, - ryu_buffer: &mut ryu_buffer, - }; - - match write_primitive(&mut context, false)? { - PrimitiveStatus::Written => { - return Ok(Some(result)); - } - PrimitiveStatus::Ignored => { - return Ok(None); - } - _ => {} - } - - context.depth += 1; - context.indentation = indentation; - iterate(&mut context, None)?; - Ok(Some(result)) -} - -#[inline(always)] -#[cold] -fn write_indentation(result: &mut String, indentation: Option<&str>, depth: usize) { - if let Some(indentation) = indentation { - result.push('\n'); - result.push_str(&indentation.repeat(depth - 1)); - } -} - -#[inline(always)] -#[cold] -fn run_to_json<'js>( - context: &mut StringifyContext<'_, 'js>, - js_object: &Object<'js>, -) -> Result<()> { - let to_json = js_object.get::<_, Function>(PredefinedAtom::ToJSON)?; - let val: Value = to_json.call((This(js_object.clone()),))?; - - //only preserve indentation if we're returning nested data - let indentation = context.indentation.and_then(|indentation| { - matches!( - val.type_of(), - Type::Object | Type::Array | Type::Exception | Type::Proxy - ) - .then_some(indentation) - }); - - append_value( - &mut StringifyContext { - ctx: context.ctx, - result: context.result, - value: &val, - depth: context.depth, - indentation, - key: None, - index: None, - parent: Some(js_object), - ancestors: context.ancestors, - replacer_fn: context.replacer_fn, - include_keys_replacer: context.include_keys_replacer, - itoa_buffer: context.itoa_buffer, - ryu_buffer: context.ryu_buffer, - }, - false, - )?; - Ok(()) -} - -#[derive(PartialEq)] -enum PrimitiveStatus<'js> { - Written, - Ignored, - Iterate(Option>), -} - -#[inline(always)] -#[cold] -fn run_replacer<'js>( - context: &mut StringifyContext<'_, 'js>, - replacer_fn: &Function<'js>, - add_comma: bool, -) -> Result> { - let key = context.key; - let index = context.index; - let value = context.value; - let parent = if let Some(parent) = context.parent { - parent.clone() - } else { - let parent = Object::new(context.ctx.clone())?; - parent.set("", value.clone())?; - parent - }; - let new_value: Value = replacer_fn.call(( - This(parent), - get_key_or_index(context.itoa_buffer, key, index), - value, - ))?; - - write_primitive2(context, add_comma, Some(new_value)) -} - -fn write_primitive<'js>( - context: &mut StringifyContext<'_, 'js>, - add_comma: bool, -) -> Result> { - if let Some(replacer_fn) = context.replacer_fn { - return run_replacer(context, replacer_fn, add_comma); - } - - write_primitive2(context, add_comma, None) -} - -fn write_primitive2<'js>( - context: &mut StringifyContext<'_, 'js>, - add_comma: bool, - new_value: Option>, -) -> Result> { - let key = context.key; - let index = context.index; - let include_keys_replacer = context.include_keys_replacer; - let indentation = context.indentation; - let depth = context.depth; - - let value = new_value.as_ref().unwrap_or(context.value); - - let type_of = value.type_of(); - - if context.index.is_none() - && matches!( - type_of, - Type::Symbol | Type::Undefined | Type::Function | Type::Constructor - ) - { - return Ok(PrimitiveStatus::Ignored); - } - - if matches!(type_of, Type::BigInt) { - return Err(Exception::throw_type( - context.ctx, - "Do not know how to serialize a BigInt", - )); - } - - if let Some(include_keys_replacer) = include_keys_replacer { - let key = get_key_or_index(context.itoa_buffer, key, index); - if !include_keys_replacer.contains(key) { - return Ok(PrimitiveStatus::Ignored); - } - }; - - if let Some(indentation) = indentation { - write_indented_separator(context.result, key, add_comma, indentation, depth); - } else { - write_sep(context.result, add_comma, false); - if let Some(key) = key { - write_key(context.result, key, false); - } - } - - match type_of { - Type::Null | Type::Undefined => context.result.push_str("null"), - Type::Bool => { - let bool_str = if unsafe { value.as_bool().unwrap_unchecked() } { - "true" - } else { - "false" - }; - context.result.push_str(bool_str); - } - Type::Int => context.result.push_str( - context - .itoa_buffer - .format(unsafe { value.as_int().unwrap_unchecked() }), - ), - Type::Float => { - let float_value = unsafe { value.as_float().unwrap_unchecked() }; - const EXP_MASK: u64 = 0x7ff0000000000000; - let bits = float_value.to_bits(); - if bits & EXP_MASK == EXP_MASK { - context.result.push_str("null"); - } else { - let str = context.ryu_buffer.format_finite(float_value); - - let bytes = str.as_bytes(); - let len = bytes.len(); - - context.result.push_str(str); - - if &bytes[len - 2..] == b".0" { - let len = context.result.len(); - unsafe { context.result.as_mut_vec().set_len(len - 2) } - } - } - } - Type::String => { - let js_string = unsafe { value.as_string().unwrap_unchecked() }.clone(); - write_string(context.result, js_string.to_cstring()?.as_str()); - } - _ => return Ok(PrimitiveStatus::Iterate(new_value)), - } - Ok(PrimitiveStatus::Written) -} - -#[inline(always)] -#[cold] -fn write_indented_separator( - result: &mut String, - key: Option<&str>, - add_comma: bool, - indentation: &str, - depth: usize, -) { - write_sep(result, add_comma, true); - result.push_str(&indentation.repeat(depth)); - if let Some(key) = key { - write_key(result, key, true); - } -} - -#[cold] -fn detect_circular_reference( - ctx: &Ctx<'_>, - value: &Object<'_>, - key: Option<&str>, - index: Option, - parent: Option<&Object<'_>>, - ancestors: &mut Vec<(usize, Rc)>, - itoa_buffer: &mut itoa::Buffer, -) -> Result<()> { - let parent_ptr = unsafe { qjs::JS_VALUE_GET_PTR(parent.unwrap_unchecked().as_raw()) as usize }; - let current_ptr = unsafe { qjs::JS_VALUE_GET_PTR(value.as_raw()) as usize }; - - while !ancestors.is_empty() - && match ancestors.last() { - Some((ptr, _)) => ptr != &parent_ptr, - _ => false, - } - { - ancestors.pop(); - } - - if ancestors.iter().any(|(ptr, _)| ptr == ¤t_ptr) { - let mut iter = ancestors.iter_mut(); - - let first = &unsafe { iter.next().unwrap_unchecked() }.1; - - let mut message = iter.rev().take(4).rev().fold( - String::from("Circular reference detected at: \".."), - |mut acc, (_, key)| { - if !key.starts_with('[') { - acc.push('.'); - } - acc.push_str(key); - acc - }, - ); - - if !first.starts_with('[') { - message.push('.'); - } - - message.push_str(first); - message.push('"'); - - return Err(Exception::throw_type(ctx, &message)); - } - ancestors.push(( - current_ptr, - key.map(|k| k.into()).unwrap_or_else(|| { - ["[", itoa_buffer.format(index.unwrap_or_default()), "]"] - .concat() - .into() - }), - )); - - Ok(()) -} - -#[inline(always)] -fn append_value(context: &mut StringifyContext<'_, '_>, add_comma: bool) -> Result { - match write_primitive(context, add_comma)? { - PrimitiveStatus::Written => Ok(true), - PrimitiveStatus::Ignored => Ok(false), - PrimitiveStatus::Iterate(new_value) => { - context.depth += 1; - iterate(context, new_value)?; - Ok(true) - } - } -} - -#[inline(always)] -fn write_key(string: &mut String, key: &str, indent: bool) { - string.push('"'); - escape_json_string(string, key.as_bytes()); - string.push_str("\":"); - if indent { - string.push(' '); - } -} - -#[inline(always)] -fn write_sep(result: &mut String, add_comma: bool, has_indentation: bool) { - if add_comma { - result.push(','); - } - if has_indentation { - result.push('\n'); - } -} - -#[inline(always)] -fn write_string(string: &mut String, value: &str) { - string.push('"'); - escape_json_string(string, value.as_bytes()); - string.push('"'); -} - -#[inline(always)] -fn get_key_or_index<'a>( - itoa_buffer: &'a mut itoa::Buffer, - key: Option<&'a str>, - index: Option, -) -> &'a str { - key.unwrap_or_else(|| itoa_buffer.format(index.unwrap_or_default())) -} - -fn iterate<'js>( - context: &mut StringifyContext<'_, 'js>, - new_value: Option>, -) -> Result<()> { - let mut add_comma; - let mut value_written; - let elem = new_value.as_ref().unwrap_or(context.value); - let depth = context.depth; - let ctx = context.ctx; - let indentation = context.indentation; - match elem.type_of() { - Type::Object | Type::Exception | Type::Proxy => { - let js_object = unsafe { elem.as_object().unwrap_unchecked() }; - if js_object.contains_key(PredefinedAtom::ToJSON)? { - return run_to_json(context, js_object); - } - - //only start detect circular reference at this level - if depth > CIRCULAR_REF_DETECTION_DEPTH { - detect_circular_reference( - ctx, - js_object, - context.key, - context.index, - context.parent, - context.ancestors, - context.itoa_buffer, - )?; - } - - context.result.push('{'); - - value_written = false; - - // Collect keys: js_object.keys() uses JS_GetOwnPropertyNames which can fail for - // Proxy objects. Fall back to Object.keys() in that case. - let keys: Vec = { - let collected: Vec = js_object.keys::().flatten().collect(); - if collected.is_empty() { - // Clear any pending exception and try Object.keys() for Proxy support - ctx.catch(); - ctx.globals() - .get::<_, Object>("Object") - .ok() - .and_then(|o| o.get::<_, Function>("keys").ok()) - .and_then(|f| f.call::<_, Vec>((js_object.clone(),)).ok()) - .unwrap_or_default() - } else { - collected - } - }; - - for key in keys { - let val = js_object.get(&key)?; - - add_comma = append_value( - &mut StringifyContext { - ctx, - result: context.result, - value: &val, - depth, - key: Some(&key), - indentation, - index: None, - parent: Some(js_object), - ancestors: context.ancestors, - replacer_fn: context.replacer_fn, - include_keys_replacer: context.include_keys_replacer, - itoa_buffer: context.itoa_buffer, - ryu_buffer: context.ryu_buffer, - }, - value_written, - )?; - value_written = value_written || add_comma; - } - - if value_written { - write_indentation(context.result, indentation, depth); - } - context.result.push('}'); - } - Type::Array => { - context.result.push('['); - add_comma = false; - value_written = false; - let js_array = unsafe { elem.as_array().unwrap_unchecked() }; - //only start detect circular reference at this level - if depth > CIRCULAR_REF_DETECTION_DEPTH { - detect_circular_reference( - ctx, - js_array.as_object(), - context.key, - context.index, - context.parent, - context.ancestors, - context.itoa_buffer, - )?; - } - for (i, val) in js_array.iter::().enumerate() { - let val = val?; - add_comma = append_value( - &mut StringifyContext { - ctx, - result: context.result, - value: &val, - depth, - key: None, - indentation, - index: Some(i), - parent: Some(js_array), - ancestors: context.ancestors, - replacer_fn: context.replacer_fn, - include_keys_replacer: context.include_keys_replacer, - itoa_buffer: context.itoa_buffer, - ryu_buffer: context.ryu_buffer, - }, - add_comma, - )?; - value_written = value_written || add_comma; - } - if value_written { - write_indentation(context.result, indentation, depth); - } - context.result.push(']'); - } - _ => {} - } - Ok(()) -} diff --git a/stdlib/src/llrt/llrt_path/lib.rs b/stdlib/src/llrt/llrt_path/lib.rs deleted file mode 100644 index 7225b753..00000000 --- a/stdlib/src/llrt/llrt_path/lib.rs +++ /dev/null @@ -1,906 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::{ - borrow::Cow, - path::{Component, Path, PathBuf, MAIN_SEPARATOR, MAIN_SEPARATOR_STR}, -}; - -use crate::llrt_utils::module::{export_default, ModuleInfo}; -use rquickjs::{ - function::Opt, - module::{Declarations, Exports, ModuleDef}, - prelude::{Func, Rest}, - Ctx, Object, Result, -}; - -pub struct PathModule; - -#[cfg(windows)] -const DELIMITER: char = ';'; -#[cfg(not(windows))] -const DELIMITER: char = ':'; - -#[cfg(windows)] -pub const CURRENT_DIR_STR: &str = ".\\"; - -#[cfg(windows)] -const FORWARD_SLASH_STR: &str = "/"; - -#[cfg(not(windows))] -pub const CURRENT_DIR_STR: &str = "./"; - -#[cfg(windows)] -use memchr::{memchr, memchr2, memchr2_iter}; - -#[cfg(windows)] -pub fn replace_backslash(path: impl Into) -> String { - let mut path = path.into(); - let bytes = unsafe { path.as_bytes_mut() }; - - let mut start = 0; - while let Some(pos) = memchr(b'\\', &bytes[start..]) { - bytes[start + pos] = b'/'; - start += pos + 1; - } - path -} - -#[cfg(not(windows))] -pub fn replace_backslash(path: impl Into) -> String { - path.into().replace('\\', "/") -} - -#[cfg(windows)] -fn find_next_separator(s: &str) -> Option { - memchr2(b'\\', b'/', s.as_bytes()) -} - -#[cfg(not(windows))] -fn find_next_separator(s: &str) -> Option { - s.find(MAIN_SEPARATOR) -} - -#[cfg(windows)] -fn find_last_sep(path: &str) -> Option { - memchr2_iter(b'\\', b'/', path.as_bytes()).next_back() -} - -#[cfg(not(windows))] -fn find_last_sep(path: &str) -> Option { - path.rfind(MAIN_SEPARATOR) -} - -pub fn dirname<'a, P: Into>>(path: P) -> String { - let path = path.into(); - let len = path.len(); - - if len == 0 { - return ".".into(); - } - - let bytes = path.as_bytes(); - - #[cfg(windows)] - { - if len == 1 { - return if is_sep(bytes[0]) { - path.into_owned() - } else { - ".".to_string() - }; - } - - // Determine root end and search offset - let (root_end, offset) = if is_sep(bytes[0]) { - if is_sep(bytes[1]) { - // UNC path: \\server\share - parse_unc_root(bytes, len).unwrap_or((1, 1)) - } else { - (1, 1) - } - } else if bytes.len() > 1 && is_drive_letter(bytes[0]) && bytes[1] == b':' { - let r = if len > 2 && is_sep(bytes[2]) { 3 } else { 2 }; - (r, r) - } else { - (0, 0) - }; - - // Find last separator (skipping trailing separators) - let end = find_dirname_end(bytes, offset); - - match end { - Some(e) => &path[..e], - None if root_end > 0 => &path[..root_end], - None => ".", - } - .into() - } - - #[cfg(not(windows))] - { - if len == 1 { - return if bytes[0] == b'/' { - path.into_owned() - } else { - ".".into() - }; - } - - let has_root = bytes[0] == b'/'; - let end = find_dirname_end(bytes, 1); - - match end { - Some(e) if has_root && e == 1 => "//", - Some(e) => &path[..e], - None if has_root => "/", - None => ".", - } - .into() - } -} - -#[cfg(windows)] -fn is_sep(c: u8) -> bool { - c == b'/' || c == b'\\' -} - -#[cfg(windows)] -fn is_drive_letter(c: u8) -> bool { - c.is_ascii_alphabetic() -} - -#[cfg(windows)] -fn parse_unc_root(bytes: &[u8], len: usize) -> Option<(usize, usize)> { - let mut j = 2; - // Skip server name - while j < len && !is_sep(bytes[j]) { - j += 1; - } - if j >= len || j == 2 { - return None; - } - // Skip separators - while j < len && is_sep(bytes[j]) { - j += 1; - } - if j >= len { - return None; - } - let share_start = j; - // Skip share name - while j < len && !is_sep(bytes[j]) { - j += 1; - } - if j == share_start { - return None; - } - if j == len { - return None; - } // UNC root only - caller handles this - Some((j + 1, j + 1)) -} - -fn find_dirname_end(bytes: &[u8], offset: usize) -> Option { - let mut matched_slash = true; - for i in (offset..bytes.len()).rev() { - #[cfg(windows)] - let is_separator = is_sep(bytes[i]); - #[cfg(not(windows))] - let is_separator = bytes[i] == b'/'; - - if is_separator { - if !matched_slash { - return Some(i); - } - } else { - matched_slash = false; - } - } - None -} - -pub fn name_extname(path: &str) -> (&str, &str) { - let path = strip_last_sep(path); - let sep_pos = find_last_sep(path); - - let path = match sep_pos { - Some(idx) => &path[idx + 1..], - None => path, - }; - if path.starts_with('.') { - return (path, ""); - } - match path.rfind('.') { - Some(idx) => path.split_at(idx), - None => (path, ""), - } -} - -fn strip_last_sep(path: &str) -> &str { - if ends_with_sep(path) { - &path[..path.len() - 1] - } else { - path - } -} - -pub fn basename(path: String, suffix: Opt) -> String { - #[cfg(windows)] - { - if path.is_empty() || path == MAIN_SEPARATOR_STR || path == FORWARD_SLASH_STR { - return String::from(""); - } - } - #[cfg(not(windows))] - { - if path.is_empty() || path == MAIN_SEPARATOR_STR { - return String::from(""); - } - } - - let (base, ext) = name_extname(&path); - let mut name = [base, ext].concat(); - if let Some(suffix) = suffix.0 { - if let Some(location) = name.rfind(&suffix) { - name.truncate(location); - return name; - } - } - name -} - -fn extname(path: String) -> String { - let (_, ext) = name_extname(&path); - ext.to_string() -} - -fn format(obj: Object) -> String { - let dir: String = obj.get("dir").unwrap_or_default(); - let root: String = obj.get("root").unwrap_or_default(); - let base: String = obj.get("base").unwrap_or_default(); - let name: String = obj.get("name").unwrap_or_default(); - let ext: String = obj.get("ext").unwrap_or_default(); - - let mut path = String::new(); - if !dir.is_empty() { - path.push_str(&dir); - if !ends_with_sep(&dir) { - path.push(MAIN_SEPARATOR); - } - } else if !root.is_empty() { - path.push_str(&root); - if !ends_with_sep(&root) { - path.push(MAIN_SEPARATOR); - } - } - if !base.is_empty() { - path.push_str(&base); - } else { - path.push_str(&name); - if !ext.is_empty() { - if !ext.starts_with('.') { - path.push('.'); - } - path.push_str(&ext); - } - } - path -} - -fn parse(ctx: Ctx, path_str: String) -> Result { - let obj = Object::new(ctx)?; - let path = Path::new(&path_str); - let parent = path - .parent() - .map(|p| p.to_str().unwrap()) - .unwrap_or_default(); - let filename = path - .file_name() - .map(|n| n.to_str().unwrap()) - .unwrap_or_default(); - - let (name, extension) = name_extname(filename); - - let root = path - .components() - .next() - .and_then(|c| match c { - Component::Prefix(prefix) => prefix.as_os_str().to_str(), - Component::RootDir => c.as_os_str().to_str(), - _ => Some(""), - }) - .unwrap_or_default(); - - obj.set("root", root)?; - obj.set("dir", parent)?; - obj.set("base", [name, extension].concat())?; - obj.set("ext", extension)?; - obj.set("name", name)?; - - Ok(obj) -} - -fn join(parts: Rest) -> String { - join_path(parts.0.iter()) -} - -pub fn join_path(parts: I) -> String -where - S: AsRef, - I: IntoIterator, -{ - join_path_with_separator(parts, false) -} - -pub fn join_path_with_separator(parts: I, force_posix_sep: bool) -> String -where - S: AsRef, - I: IntoIterator, -{ - //fine because we're either moving or storing references - let parts_vec: Vec = parts.into_iter().collect(); - //add one slash plus drive letter - //max is probably parts+size - let likely_max_size = parts_vec - .iter() - .map(|p| p.as_ref().len() + 1) - .sum::() - + 10; - let result = String::with_capacity(likely_max_size); - join_resolve_path(parts_vec, false, result, PathBuf::new(), force_posix_sep) -} - -pub fn resolve_path(parts: I) -> Result -where - S: AsRef, - I: IntoIterator, -{ - resolve_path_with_separator(parts, false) -} - -pub fn resolve_path_with_separator(parts: I, force_posix_sep: bool) -> Result -where - S: AsRef, - I: IntoIterator, -{ - let cwd = std::env::current_dir()?; - - let mut result = cwd.clone().into_os_string().into_string().unwrap(); - //add MAIN_SEPARATOR if we're not on already MAIN_SEPARATOR - if !result.ends_with(MAIN_SEPARATOR) { - result.push(MAIN_SEPARATOR); - } - #[cfg(windows)] - { - if force_posix_sep { - result = result.replace(MAIN_SEPARATOR, FORWARD_SLASH_STR); - } - } - Ok(join_resolve_path(parts, true, result, cwd, force_posix_sep)) -} - -pub fn relative(from: F, to: T) -> Result -where - F: AsRef, - T: AsRef, -{ - let from_ref = from.as_ref(); - let to_ref = to.as_ref(); - if from_ref == to_ref { - return Ok("".into()); - } - - let mut abs_from = None; - - if !is_absolute(from_ref) { - abs_from = Some( - std::env::current_dir()?.to_string_lossy().to_string() + MAIN_SEPARATOR_STR + from_ref, - ); - } - - let mut abs_to = None; - - if !is_absolute(to_ref) { - abs_to = Some( - std::env::current_dir()?.to_string_lossy().to_string() + MAIN_SEPARATOR_STR + to_ref, - ); - } - - let from_ref = abs_from.as_deref().unwrap_or(from_ref); - let to_ref = abs_to.as_deref().unwrap_or(to_ref); - - let mut from_index = 0; - let mut to_index = 0; - // skip common prefix - while from_index < from_ref.len() && to_index < to_ref.len() { - let from_next = find_next_separator(&from_ref[from_index..]) - .unwrap_or(from_ref.len() - from_index) - + from_index; - let to_next = - find_next_separator(&to_ref[to_index..]).unwrap_or(to_ref.len() - to_index) + to_index; - if from_ref[from_index..from_next] != to_ref[to_index..to_next] { - break; - } - from_index = from_next + 1; //move past the separator - to_index = to_next + 1; //move past the separator - } - let mut relative = String::new(); - // add ".." for each remaining component in 'from' - while from_index < from_ref.len() { - let from_next = find_next_separator(&from_ref[from_index..]) - .unwrap_or(from_ref.len() - from_index) - + from_index; - if !relative.is_empty() { - relative.push(MAIN_SEPARATOR); - } - relative.push_str(".."); - from_index = from_next + 1; // Move past the separator - } - // add the remaining components from 'to' - while to_index < to_ref.len() { - let to_next = - find_next_separator(&to_ref[to_index..]).unwrap_or(to_ref.len() - to_index) + to_index; - if !relative.is_empty() { - relative.push(MAIN_SEPARATOR); - } - let component = &to_ref[to_index..to_next]; - if component != "." { - relative.push_str(component); - } - to_index = to_next + 1; // Move past the separator - } - Ok(if relative.is_empty() { - ".".into() - } else { - relative - }) -} - -fn join_resolve_path( - parts: I, - resolve: bool, - mut result: String, - cwd: PathBuf, - force_posix_sep: bool, -) -> String -where - S: AsRef, - I: IntoIterator, -{ - let (sep, sep_str) = if force_posix_sep { - ('/', "/") - } else { - (MAIN_SEPARATOR, MAIN_SEPARATOR_STR) - }; - - let mut resolve_cow: Cow; - let mut empty = true; - let mut prefix_len = 0; - - let mut index_stack = Vec::with_capacity(16); - - // Remove the trailing sep: /a/b// -> /a/b - if ends_with_sep(&result) && result.len() > 1 { - result.truncate(result.len() - 1); - } - - for part in parts { - let mut part_ref: &str = part.as_ref(); - let mut start = 0; - if resolve { - if cfg!(not(windows)) { - if part_ref.starts_with(MAIN_SEPARATOR) { - empty = false; - result = MAIN_SEPARATOR.into(); - start = 1; - } - } else { - let starts_with_sep = starts_with_sep(part_ref); - if starts_with_sep { - let (prefix, _) = get_path_prefix(&cwd); - prefix_len = prefix.len(); - result = prefix; - empty = false; - result.push(sep); - } else { - let path_buf: PathBuf = PathBuf::from(part_ref); - if path_buf.is_absolute() { - empty = false; - let (prefix, mut components) = get_path_prefix(&path_buf); - if !prefix.is_empty() { - components.next(); //consume prefix - } - prefix_len = prefix.len(); - result = prefix; - result.push(sep); - resolve_cow = components - .map(|comp| comp.as_os_str().to_str().unwrap_or_default()) // Convert each component to &str - .collect::>() // Collect into a vector of &str - .join(sep_str) - .into(); - part_ref = resolve_cow.as_ref(); - } - } - } - } else if starts_with_sep(part_ref) && empty { - empty = false; - result.push(sep); - start = 1; - } - - while start < part_ref.len() { - let end = find_next_separator(&part_ref[start..]).map_or(part_ref.len(), |i| i + start); - match &part_ref[start..end] { - ".." => { - if let Some(last_index) = index_stack.pop() { - result.truncate(last_index); - } else if empty { - if let Some(last_index) = find_last_sep(&result) { - result.truncate(last_index); - } - } - } - "" | "." => { - //ignore - } - sub_part => { - let len = result.len(); - if !result.ends_with(sep) && !result.is_empty() { - result.push(sep); - } - result.push_str(sub_part); - result.push(sep); - index_stack.push(len); - } - } - start = end + 1; - } - } - - if result.len() > prefix_len + 1 && ends_with_sep(&result) { - result.truncate(result.len() - 1); - } - - result -} - -pub fn resolve(path: Rest) -> Result { - resolve_path(path.iter()) -} - -fn get_path_prefix(cwd: &Path) -> (String, std::iter::Peekable>) { - let mut components = cwd.components().peekable(); - - let prefix = if let Some(Component::Prefix(prefix)) = components.peek() { - prefix.as_os_str().to_str().unwrap().to_string() - } else { - "".into() - }; - - (prefix, components) -} - -pub fn normalize>(path: P) -> String { - join_path([path].iter()) -} - -#[allow(dead_code)] //used by windows -fn starts_with_sep(path: &str) -> bool { - matches!(path.as_bytes().first().unwrap_or(&0), b'/' | b'\\') -} - -#[cfg(windows)] -pub fn ends_with_sep(path: &str) -> bool { - matches!(path.as_bytes().last().unwrap_or(&0), b'/' | b'\\') -} - -#[cfg(not(windows))] -pub fn ends_with_sep(path: &str) -> bool { - path.ends_with(MAIN_SEPARATOR) -} - -#[cfg(windows)] -pub fn is_absolute(path: &str) -> bool { - starts_with_sep(path) || PathBuf::from(path).is_absolute() -} - -#[cfg(not(windows))] -pub fn is_absolute(path: &str) -> bool { - path.starts_with(MAIN_SEPARATOR) -} - -impl ModuleDef for PathModule { - fn declare(declare: &Declarations) -> Result<()> { - declare.declare("basename")?; - declare.declare("dirname")?; - declare.declare("extname")?; - declare.declare("format")?; - declare.declare("parse")?; - declare.declare("join")?; - declare.declare("resolve")?; - declare.declare("relative")?; - declare.declare("normalize")?; - declare.declare("isAbsolute")?; - declare.declare("delimiter")?; - declare.declare("sep")?; - - declare.declare("default")?; - Ok(()) - } - - fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { - export_default(ctx, exports, |default| { - default.set("dirname", Func::from(dirname::))?; - default.set("basename", Func::from(basename))?; - default.set("extname", Func::from(extname))?; - default.set("format", Func::from(format))?; - default.set("parse", Func::from(parse))?; - default.set("join", Func::from(join))?; - default.set("relative", Func::from(relative::))?; - default.set("resolve", Func::from(resolve))?; - default.set("normalize", Func::from(normalize::))?; - default.set("isAbsolute", Func::from(|s: String| is_absolute(&s)))?; - default.prop("delimiter", DELIMITER.to_string())?; - default.prop("sep", MAIN_SEPARATOR.to_string())?; - Ok(()) - }) - } -} - -impl From for ModuleInfo { - fn from(val: PathModule) -> Self { - ModuleInfo { - name: "path", - module: val, - } - } -} - -#[cfg(test)] -mod tests { - use std::{env::set_current_dir, sync::Mutex}; - - static THREAD_LOCK: Lazy> = Lazy::new(Mutex::default); - - use once_cell::sync::Lazy; - - use super::*; - - #[test] - fn test_relative() { - let _shared = THREAD_LOCK.lock().unwrap(); - let cwd = std::env::current_dir().expect("unable to get current working directory"); - set_current_dir("/").expect("unable to set working directory to /"); - - assert_eq!( - relative("a/b/c", "b/c").unwrap(), - "../../../b/c".replace('/', MAIN_SEPARATOR_STR) - ); - assert_eq!( - relative("/data/orandea/test/aaa", "/data/orandea/impl/bbb").unwrap(), - "../../impl/bbb".replace('/', MAIN_SEPARATOR_STR) - ); - assert_eq!( - relative("/a/b/c", "/a/d").unwrap(), - "../../d".replace('/', MAIN_SEPARATOR_STR) - ); - assert_eq!(relative("/a/b/c", "/a/b/c/d").unwrap(), "d"); - assert_eq!(relative("/a/b/c", "/a/b/c").unwrap(), ""); - - assert_eq!( - relative("a/b", "a/b/c/d").unwrap(), - "c/d".replace('/', MAIN_SEPARATOR_STR) - ); - assert_eq!( - relative("a/b/c", "b/c").unwrap(), - "../../../b/c".replace('/', MAIN_SEPARATOR_STR) - ); - - set_current_dir(cwd).expect("unable to set working directory back"); - } - - #[test] - fn test_dirname() { - assert_eq!(dirname("/usr/local/bin".to_string()), "/usr/local"); - assert_eq!(dirname("/usr/local/".to_string()), "/usr"); - assert_eq!(dirname("usr/local/bin".to_string()), "usr/local"); - assert_eq!(dirname("/".to_string()), "/"); - assert_eq!(dirname("".to_string()), "."); - } - - #[test] - fn test_basename() { - assert_eq!(basename("/usr/local/bin".to_string(), Opt(None)), "bin"); - assert_eq!( - basename("/usr/local/bin.txt".to_string(), Opt(None)), - "bin.txt" - ); - assert_eq!( - basename( - "/usr/local/bin.txt".to_string(), - Opt(Some(".txt".to_string())) - ), - "bin" - ); - assert_eq!(basename("".to_string(), Opt(None)), ""); - assert_eq!(basename("/".to_string(), Opt(None)), ""); - } - - #[test] - fn test_extname() { - assert_eq!(extname("/usr/local/bin.txt".to_string()), ".txt"); - assert_eq!(extname("/usr/local/bin".to_string()), ""); - assert_eq!(extname("file.tar.gz".to_string()), ".gz"); - assert_eq!(extname(".bashrc".to_string()), ""); - assert_eq!(extname("".to_string()), ""); - } - - #[test] - fn test_join() { - // Standard cases - assert_eq!( - join_path(["/usr", "local", "bin"].iter()), - "/usr/local/bin".replace('/', MAIN_SEPARATOR_STR) - ); - assert_eq!( - join_path(["/usr", "/local", "bin"].iter()), - "/usr/local/bin".replace('/', MAIN_SEPARATOR_STR) - ); - assert_eq!( - join_path(["usr", "local", "bin"].iter()), - "usr/local/bin".replace('/', MAIN_SEPARATOR_STR) - ); - assert_eq!(join_path(["", "bin"].iter()), "bin"); - - // Complex cases - assert_eq!( - join_path(["/usr", "..", "local", "bin"].iter()), - "/local/bin".replace('/', MAIN_SEPARATOR_STR) - ); // Parent dir - assert_eq!( - join_path([".", "usr", "local"]), - "usr/local".replace('/', MAIN_SEPARATOR_STR) - ); // Current dir - assert_eq!( - join_path(["/usr", ".", "bin"].iter()), - "/usr/bin".replace('/', MAIN_SEPARATOR_STR) - ); // Current dir in middle - assert_eq!( - join_path(["usr", "local", "bin", ".."].iter()), - "usr/local".replace('/', MAIN_SEPARATOR_STR) - ); // Ending with parent dir - assert_eq!( - join_path(["/usr", "local", "", "bin"].iter()), - "/usr/local/bin".replace('/', MAIN_SEPARATOR_STR) - ); // Empty component in path - assert_eq!( - join_path(["/usr", "local", ".hidden"].iter()), - "/usr/local/.hidden".replace('/', MAIN_SEPARATOR_STR) - ); // Hidden file - } - - #[test] - fn test_resolve_path() { - let _shared = THREAD_LOCK.lock().unwrap(); - let prefix = if cfg!(windows) { - if let Some(Component::Prefix(prefix)) = - std::env::current_dir().unwrap().components().next() - { - prefix.as_os_str().to_str().unwrap().to_string() - } else { - "".into() - } - } else { - "".into() - }; - - assert_eq!( - resolve_path(["", "foo/bar"].iter()).unwrap(), - std::env::current_dir() - .unwrap() - .join("foo/bar".replace('/', MAIN_SEPARATOR_STR)) - .to_string_lossy() - .to_string() - ); - - // Standard cases - assert_eq!( - resolve_path(["/"].iter()).unwrap(), - prefix.clone() + MAIN_SEPARATOR_STR - ); - - // Standard cases - assert_eq!( - resolve_path(["/foo/bar", "../baz"].iter()).unwrap(), - prefix.clone() + &"/foo/baz".replace('/', MAIN_SEPARATOR_STR) - ); - assert_eq!( - resolve_path(["/foo/bar", "./baz"].iter()).unwrap(), - prefix.clone() + &"/foo/bar/baz".replace('/', MAIN_SEPARATOR_STR) - ); - assert_eq!( - resolve_path(["foo/bar", "/baz"].iter()).unwrap(), - prefix.clone() + &"/baz".replace('/', MAIN_SEPARATOR_STR) - ); - - // Complex cases - assert_eq!( - resolve_path(["/foo", "bar", ".", "baz"].iter()).unwrap(), - prefix.clone() + &"/foo/bar/baz".replace('/', MAIN_SEPARATOR_STR) - ); // Current dir in middle - assert_eq!( - resolve_path(["/foo", "bar", "..", "baz"].iter()).unwrap(), - prefix.clone() + &"/foo/baz".replace('/', MAIN_SEPARATOR_STR) - ); // Parent dir in middle - assert_eq!( - resolve_path(["/foo", "bar", "../..", "baz"].iter()).unwrap(), - prefix.clone() + &"/baz".replace('/', MAIN_SEPARATOR_STR) - ); // Double parent dir - assert_eq!( - resolve_path(["/foo", "bar", ".hidden"].iter()).unwrap(), - prefix.clone() + &"/foo/bar/.hidden".replace('/', MAIN_SEPARATOR_STR) - ); // Hidden file - assert_eq!( - resolve_path(["/foo", ".", "bar", "."].iter()).unwrap(), - prefix.clone() + &"/foo/bar".replace('/', MAIN_SEPARATOR_STR) - ); // Multiple current dirs - assert_eq!( - resolve_path(["/foo", "..", "..", "bar"].iter()).unwrap(), - prefix.clone() + &"/bar".replace('/', MAIN_SEPARATOR_STR) - ); // Multiple parent dirs - assert_eq!( - resolve_path(["/foo/bar", "/..", "baz"].iter()).unwrap(), - prefix.clone() + &"/baz".replace('/', MAIN_SEPARATOR_STR) - ); // Parent dir with absolute path - - assert_eq!( - resolve_path(["../foo"].iter()).unwrap(), - std::env::current_dir() - .unwrap() - .parent() - .unwrap() - .join("foo".replace('/', MAIN_SEPARATOR_STR)) - .to_string_lossy() - .to_string() - ); // Start with .. - - assert_eq!( - resolve_path(["../".repeat(32)].iter()).unwrap(), - prefix.clone() - ); // Many .. - } - - #[test] - fn test_normalize() { - assert_eq!( - normalize("/foo//bar//baz"), - "/foo/bar/baz".replace('/', MAIN_SEPARATOR_STR) - ); - assert_eq!( - normalize("/foo/./bar/../baz"), - "/foo/baz".replace('/', MAIN_SEPARATOR_STR) - ); - assert_eq!( - normalize("foo/bar/"), - "foo/bar".replace('/', MAIN_SEPARATOR_STR) - ); - assert_eq!(normalize("./foo"), "foo"); - } - - #[test] - fn test_is_absolute() { - assert!(is_absolute("/usr/local/bin")); - assert!(!is_absolute("usr/local/bin")); - #[cfg(windows)] - assert!(is_absolute("C:\\Program Files")); // for Windows systems - assert!(!is_absolute("./local/bin")); - } - - #[test] - fn test_replace_backslash() { - assert_eq!(replace_backslash("C:\\Program Files"), "C:/Program Files"); - assert_eq!(replace_backslash("/usr/local/bin"), "/usr/local/bin"); - assert_eq!(replace_backslash("C:\\Users\\User\\"), "C:/Users/User/"); - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/lib.rs b/stdlib/src/llrt/llrt_stream_web/lib.rs deleted file mode 100644 index baa68356..00000000 --- a/stdlib/src/llrt/llrt_stream_web/lib.rs +++ /dev/null @@ -1,181 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use crate::llrt_utils::{ - module::{export_default, ModuleInfo}, - primordials::{BasePrimordials, Primordial}, -}; -use queuing_strategy::{ByteLengthQueuingStrategy, CountQueuingStrategy}; -use readable::{ - ReadableByteStreamController, ReadableStreamBYOBReader, ReadableStreamBYOBRequest, - ReadableStreamDefaultController, ReadableStreamDefaultReader, -}; -use rquickjs::{ - module::{Declarations, Exports, ModuleDef}, - Class, Ctx, Object, Result, -}; -use writable::{WritableStream, WritableStreamDefaultController, WritableStreamDefaultWriter}; - -use crate::llrt_stream_web::{ - readable::{ArrayConstructorPrimordials, IteratorPrimordials}, - transform::{TransformStream, TransformStreamDefaultController}, - utils::promise::PromisePrimordials, - writable::WritableStreamDefaultControllerPrimordials, -}; - -mod queuing_strategy; -pub mod readable; -mod readable_writable_pair; -mod transform; -pub mod utils; -mod writable; - -// Public API for creating streams from Rust -pub use readable::stream::lock_readable_stream; -pub use readable::stream::tee_readable_stream; -pub use readable::stream::try_sync_drain_closed_stream; -pub use readable::stream::ReadableStream; -pub use readable::{ - readable_byte_stream_controller_close_stream, readable_byte_stream_controller_enqueue_bytes, - readable_byte_stream_controller_enqueue_bytes_borrowed, - readable_stream_default_controller_close_stream, - readable_stream_default_controller_enqueue_value, - readable_stream_default_controller_error_stream, ReadableByteStreamControllerClass, - ReadableStreamDefaultControllerClass, -}; -pub use readable::{CancelAlgorithm, PullAlgorithm, ReadableStreamControllerClass, StartAlgorithm}; -pub use readable::{NativePull, NativePullFn, NativePullResult}; - -/// Creates a transform stream using LLRT's built-in Web Streams implementation. -/// -/// This does not consult the global `TransformStream` binding. -pub fn create_transform_stream<'js>( - ctx: &Ctx<'js>, - transformer: Object<'js>, -) -> Result> { - init_primordials(ctx)?; - Ok(TransformStream::from_transformer(ctx.clone(), transformer)?.into_inner()) -} - -fn init_primordials(ctx: &Ctx<'_>) -> Result<()> { - BasePrimordials::init(ctx)?; - PromisePrimordials::init(ctx)?; - ArrayConstructorPrimordials::init(ctx)?; - WritableStreamDefaultControllerPrimordials::init(ctx)?; - IteratorPrimordials::init(ctx)?; - Ok(()) -} - -/// Defines web streams, which are exposed through the "stream/web" Node import, but also at the global scope -/// Web streams consist of Readable, Writable, and Transform streams. Transform is currently unimplemented. -/// -/// https://developer.mozilla.org/en-US/docs/Web/API/Streams_API -/// -/// # ReadableStream -/// ReadableStream knows how to 'pull' objects or bytes from an underlying source, generally a user-defined function or an [async] iterator. -/// A source enqueues data to the stream via a controller, either ReadableStreamDefaultController or a ReadableByteStreamController optionally for byte data. -/// The controller is created at stream initialisation and cannot change. -/// -/// Data is read from the stream using a reader, which is obtained using stream.getReader(). A reader 'locks' the stream for reading, preventing -/// other readers from being created. When a reader is released with `reader.releaseLock()`, the stream goes back to having no reader and a new one can be created. -/// In the case of ReadableByteStreamController, a special reader ReadableStreamBYOBReader may be used, which allows users to provide their own -/// buffer to fill bytes into when reading. Otherwise, ReadableStreamDefaultReader is used by default, and this may also be used with byte streams. -/// -/// A ReadableStream can be 'tee'd', which splits it into two readable streams which both read the same underlying data, potentially at different -/// paces. This is an area of substantial complexity for the implementation, particularly in the case of byte streams as the alternative reader types -/// must be handled correctly. -/// -/// # WritableStream -/// WritableStream knows how to 'push' objects into an underlying sink, generally a user-defined function. It has no special casing for bytes, and so -/// only has one type of controller, WritableStreamDefaultController, and only one type of writer WritableStreamDefaultWriter. The controller is only needed for -/// error handling because writes are signalled via a function call to a user-defined 'write' method which receives the chunk directly. -/// -/// Data is written to the stream using a WritableStreamDefaultWriter, which is obtained using stream.getWriter(). A writer 'locks' the stream for writing, -/// preventing other writers from being created. When a writer is released with `writer.releaseLock()`, the stream goes back to having no writer and a new one can be created. -pub struct StreamWebModule; - -// https://nodejs.org/api/webstreams.html -impl ModuleDef for StreamWebModule { - fn declare(declare: &Declarations) -> Result<()> { - declare.declare(stringify!(ReadableStream))?; - declare.declare(stringify!(ReadableStreamDefaultReader))?; - declare.declare(stringify!(ReadableStreamBYOBReader))?; - declare.declare(stringify!(ReadableStreamDefaultController))?; - declare.declare(stringify!(ReadableByteStreamController))?; - declare.declare(stringify!(ReadableStreamBYOBRequest))?; - - declare.declare(stringify!(WritableStream))?; - declare.declare(stringify!(WritableStreamDefaultWriter))?; - declare.declare(stringify!(WritableStreamDefaultController))?; - - declare.declare(stringify!(TransformStream))?; - declare.declare(stringify!(TransformStreamDefaultController))?; - - declare.declare(stringify!(ByteLengthQueuingStrategy))?; - declare.declare(stringify!(CountQueuingStrategy))?; - - declare.declare("default")?; - Ok(()) - } - - #[inline] - fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { - export_default(ctx, exports, |default| { - Class::::define(default)?; - Class::::define(default)?; - Class::::define(default)?; - Class::::define(default)?; - Class::::define(default)?; - Class::::define(default)?; - - Class::::define(default)?; - Class::::define(default)?; - Class::::define(default)?; - - Class::::define(default)?; - Class::::define(default)?; - - Class::::define(default)?; - Class::::define(default)?; - - Ok(()) - })?; - - Ok(()) - } -} - -impl From for ModuleInfo { - fn from(val: StreamWebModule) -> Self { - ModuleInfo { - name: "stream/web", - module: val, - } - } -} - -pub fn init(ctx: &Ctx) -> Result<()> { - let globals = &ctx.globals(); - - init_primordials(ctx)?; - - // https://min-common-api.proposal.wintertc.org/#api-index - Class::::define(globals)?; - Class::::define(globals)?; - - Class::::define(globals)?; - Class::::define(globals)?; - Class::::define(globals)?; - Class::::define(globals)?; - Class::::define(globals)?; - Class::::define(globals)?; - - Class::::define(globals)?; - Class::::define(globals)?; - - // This is exposed globally by Node even though its not in the min-common-api - Class::::define(globals)?; - - Class::::define(globals)?; - Class::::define(globals)?; - - Ok(()) -} diff --git a/stdlib/src/llrt/llrt_stream_web/queuing_strategy/byte_length.rs b/stdlib/src/llrt/llrt_stream_web/queuing_strategy/byte_length.rs deleted file mode 100644 index 4de615d8..00000000 --- a/stdlib/src/llrt/llrt_stream_web/queuing_strategy/byte_length.rs +++ /dev/null @@ -1,36 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use rquickjs::{class::Trace, methods, Class, Ctx, JsLifetime, Result}; - -use super::{NativeSizeFunction, QueueingStrategyInit}; - -#[derive(JsLifetime, Trace)] -#[rquickjs::class] -pub(crate) struct ByteLengthQueuingStrategy<'js> { - high_water_mark: f64, - size: Class<'js, NativeSizeFunction>, -} - -#[methods(rename_all = "camelCase")] -impl<'js> ByteLengthQueuingStrategy<'js> { - #[qjs(constructor)] - pub(crate) fn new(ctx: Ctx<'js>, init: QueueingStrategyInit) -> Result { - // Set this.[[highWaterMark]] to init["highWaterMark"]. - Ok(Self { - high_water_mark: init.high_water_mark, - size: Class::instance(ctx, NativeSizeFunction::ByteLength)?, - }) - } - - // readonly attribute Function size; - // size is an attribute, not a method, so this function is not itself the size function, but instead returns one - #[qjs(get)] - pub(crate) fn size(&self) -> Class<'js, NativeSizeFunction> { - self.size.clone() - } - - // readonly attribute unrestricted double highWaterMark; - #[qjs(get)] - pub(crate) fn high_water_mark(&self) -> f64 { - self.high_water_mark - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/queuing_strategy/count.rs b/stdlib/src/llrt/llrt_stream_web/queuing_strategy/count.rs deleted file mode 100644 index 95460a87..00000000 --- a/stdlib/src/llrt/llrt_stream_web/queuing_strategy/count.rs +++ /dev/null @@ -1,36 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use rquickjs::{class::Trace, methods, Class, Ctx, JsLifetime, Result}; - -use super::{NativeSizeFunction, QueueingStrategyInit}; - -#[derive(JsLifetime, Trace)] -#[rquickjs::class] -pub(crate) struct CountQueuingStrategy<'js> { - high_water_mark: f64, - size: Class<'js, NativeSizeFunction>, -} - -#[methods(rename_all = "camelCase")] -impl<'js> CountQueuingStrategy<'js> { - #[qjs(constructor)] - pub(crate) fn new(ctx: Ctx<'js>, init: QueueingStrategyInit) -> Result { - // Set this.[[highWaterMark]] to init["highWaterMark"]. - Ok(Self { - high_water_mark: init.high_water_mark, - size: Class::instance(ctx, NativeSizeFunction::Count)?, - }) - } - - // readonly attribute Function size; - // size is an attribute, not a method, so this function is not itself the size function, but instead returns one - #[qjs(get)] - pub(crate) fn size(&self) -> Class<'js, NativeSizeFunction> { - self.size.clone() - } - - // readonly attribute unrestricted double highWaterMark; - #[qjs(get)] - pub(crate) fn high_water_mark(&self) -> f64 { - self.high_water_mark - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/queuing_strategy/mod.rs b/stdlib/src/llrt/llrt_stream_web/queuing_strategy/mod.rs deleted file mode 100644 index ce37c241..00000000 --- a/stdlib/src/llrt/llrt_stream_web/queuing_strategy/mod.rs +++ /dev/null @@ -1,231 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use rquickjs::{ - class::{JsCell, JsClass, Readable, Trace}, - convert::Coerced, - function::{Constructor, Params}, - prelude::This, - Class, Ctx, Error, Exception, FromJs, Function, JsLifetime, Object, Result, Value, -}; - -pub(crate) use byte_length::ByteLengthQueuingStrategy; -pub(crate) use count::CountQueuingStrategy; - -use crate::llrt_stream_web::utils::ValueOrUndefined; - -mod byte_length; -mod count; -#[cfg(test)] -mod tests; - -/// QueuingStrategy is the structure of a user-provided object describing how backpressure should be signalled. -/// https://streams.spec.whatwg.org/#qs-api -pub(super) struct QueuingStrategy<'js> { - // unrestricted double highWaterMark; - high_water_mark: Option, - // callback QueuingStrategySize = unrestricted double (any chunk); - pub(super) size: Option>, -} - -impl<'js> FromJs<'js> for QueuingStrategy<'js> { - fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let ty_name = value.type_name(); - let obj = value - .as_object() - .ok_or_else(|| Error::new_from_js(ty_name, "Object"))?; - - let high_water_mark = obj - .get_value_or_undefined::<_, Coerced>("highWaterMark")? - .map(|value| value.0); - let size = obj.get_value_or_undefined::<_, _>("size")?; - - Ok(Self { - high_water_mark, - size, - }) - } -} - -impl<'js> QueuingStrategy<'js> { - // https://streams.spec.whatwg.org/#validate-and-normalize-high-water-mark - pub(super) fn extract_high_water_mark( - ctx: &Ctx<'js>, - this: Option, - default_hwm: f64, - ) -> Result { - match this { - // If strategy["highWaterMark"] does not exist, return defaultHWM. - None => Ok(default_hwm), - // Let highWaterMark be strategy["highWaterMark"]. - Some(Self { - high_water_mark: Some(high_water_mark), - .. - }) => { - // If highWaterMark is NaN or highWaterMark < 0, throw a RangeError exception. - if high_water_mark.is_nan() || high_water_mark < 0.0 { - Err(Exception::throw_range(ctx, "Invalid highWaterMark")) - } else { - // Return highWaterMark. - Ok(high_water_mark) - } - } - - // If strategy["highWaterMark"] does not exist, return defaultHWM. - _ => Ok(default_hwm), - } - } - - // https://streams.spec.whatwg.org/#make-size-algorithm-from-size-function - pub(super) fn extract_size_algorithm(this: Option<&Self>) -> SizeAlgorithm<'js> { - // If strategy["size"] does not exist, return an algorithm that returns 1. - match this.as_ref().and_then(|t| t.size.as_ref()) { - None => SizeAlgorithm::AlwaysOne, - Some(size) => SizeAlgorithm::SizeFunction(size.clone()), - } - } -} - -/// SizeAlgorithm represents the two ways we might generate sizes - by calling a function or by simply returning 1.0 (the default) -#[derive(JsLifetime, Trace, Clone)] -pub(super) enum SizeAlgorithm<'js> { - AlwaysOne, - SizeFunction(SizeFunction<'js>), -} - -impl<'js> SizeAlgorithm<'js> { - pub(super) fn call(&self, ctx: Ctx<'js>, chunk: Value<'js>) -> Result> { - match self { - Self::AlwaysOne - | Self::SizeFunction(SizeFunction::Native(NativeSizeFunction::Count)) => { - Ok(SizeValue::Native(1.0)) - } - Self::SizeFunction(SizeFunction::Js(ref f)) => { - f.call((This(Value::new_undefined(ctx.clone())), chunk.clone())) - } - Self::SizeFunction(SizeFunction::Native(NativeSizeFunction::ByteLength)) => { - let size = byte_length_queueing_strategy_size_function(&ctx, &chunk)?; - SizeValue::from_js(&ctx, size) - } - } - } -} - -/// SizeValue abstracts over the sources of size values - they can either come from user-provided size functions, in which case they might -/// be any Value, or (more often) they come from a NativeSizeFunction or the default AlwaysOne algorithm and we can pass around a native Rust type. -pub(super) enum SizeValue<'js> { - Value(Value<'js>), - Native(f64), -} - -impl SizeValue<'_> { - pub(super) fn as_number(&self) -> Option { - match self { - Self::Value(value) => value.as_number(), - Self::Native(size) => Some(*size), - } - } -} - -impl<'js> FromJs<'js> for SizeValue<'js> { - fn from_js(_: &Ctx<'js>, value: Value<'js>) -> Result { - if let Some(size) = value.as_number() { - return Ok(Self::Native(size)); - } - - Ok(Self::Value(value)) - } -} - -/// SizeFunction abstracts over user-provided size functions (from their own queuing strategy implementations) and ones provided by us. -/// We want to be able to recognise the ones that we have provided so we can short-circuit expensive JS calls -#[derive(JsLifetime, Trace, Clone)] -pub(super) enum SizeFunction<'js> { - Js(Function<'js>), - Native(NativeSizeFunction), -} - -impl<'js> FromJs<'js> for SizeFunction<'js> { - fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { - if let Ok(nsf) = Class::::from_value(&value) { - return Ok(SizeFunction::Native(*nsf.borrow())); - } - - Ok(SizeFunction::Js(Function::from_js(ctx, value)?)) - } -} - -/// QueueingStrategyInit is the dictionary of input parameters for both native queuing strategies -/// https://streams.spec.whatwg.org/#dictdef-queuingstrategyinit -pub(crate) struct QueueingStrategyInit { - high_water_mark: f64, -} - -impl<'js> FromJs<'js> for QueueingStrategyInit { - fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let ty_name = value.type_name(); - let obj = value - .as_object() - .ok_or_else(|| Error::new_from_js(ty_name, "Object"))?; - - let high_water_mark = obj - .get_value_or_undefined::<_, Coerced>("highWaterMark")? - .ok_or_else(|| Error::new_from_js(ty_name, "QueueingStrategyInit"))?; - - Ok(Self { - high_water_mark: high_water_mark.0, - }) - } -} - -/// NativeSizeFunction is a callable class which allows us to keep track that these size functions are not user provided, but -/// in fact represent the native size functions. This allows us to avoid JS calls by noticing that a size function is this class. -#[derive(JsLifetime, Trace, Clone, Copy)] -pub(super) enum NativeSizeFunction { - ByteLength, - Count, -} - -impl<'js> JsClass<'js> for NativeSizeFunction { - const NAME: &'static str = "NativeSizeFunction"; - - const KIND: rquickjs::class::ClassKind = rquickjs::class::ClassKind::Callable; - - type Mutable = Readable; - - fn prototype(ctx: &Ctx<'js>) -> Result>> { - Ok(Some(Function::prototype(ctx.clone()))) - } - - fn constructor(_ctx: &Ctx<'js>) -> Result>> { - Ok(None) - } - - fn call<'a>(this: &JsCell<'js, Self>, params: Params<'a, 'js>) -> Result> { - match &*this.borrow() { - NativeSizeFunction::Count => Ok(Value::new_int(params.ctx().clone(), 1)), - NativeSizeFunction::ByteLength => { - let Some(chunk) = params.arg(0) else { - return Err(Exception::throw_type( - params.ctx(), - "ByteLengthQueuingStrategy expects an argument 'chunk'", - )); - }; - - byte_length_queueing_strategy_size_function(params.ctx(), &chunk) - } - } - } -} - -fn byte_length_queueing_strategy_size_function<'js>( - ctx: &Ctx<'js>, - chunk: &Value<'js>, -) -> Result> { - if let Some(chunk) = chunk.as_object() { - chunk.get("byteLength") - } else { - Err(Exception::throw_type( - ctx, - "ByteLengthQueuingStrategy argument 'chunk' must be an object", - )) - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/queuing_strategy/tests.rs b/stdlib/src/llrt/llrt_stream_web/queuing_strategy/tests.rs deleted file mode 100644 index 13f6c302..00000000 --- a/stdlib/src/llrt/llrt_stream_web/queuing_strategy/tests.rs +++ /dev/null @@ -1,159 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use crate::llrt_test::test_sync_with; - -#[tokio::test] -async fn high_water_mark_uses_javascript_number_conversion() { - test_sync_with(|ctx| { - crate::llrt_stream_web::init(&ctx)?; - ctx.eval::<(), _>( - r#" - const converted = { - valueOf() { - return "7.5"; - }, - }; - - const observed = []; - new ReadableStream({ - start(controller) { - observed.push(controller.desiredSize); - }, - }, { highWaterMark: converted }); - observed.push( - new WritableStream({}, { highWaterMark: converted }) - .getWriter().desiredSize, - ); - - const transform = new TransformStream({ - start(controller) { - observed.push(controller.desiredSize); - }, - }, { highWaterMark: converted }, { highWaterMark: converted }); - observed.push(transform.writable.getWriter().desiredSize); - - observed.push( - new CountQueuingStrategy({ highWaterMark: converted }).highWaterMark, - new ByteLengthQueuingStrategy({ highWaterMark: converted }).highWaterMark, - ); - - if (observed.length !== 6 || observed.some(value => value !== 7.5)) { - throw new Error(`Unexpected highWaterMark values: ${observed}`); - } - - const primitiveConversions = [ - [false, 0], - [true, 1], - ["2.25", 2.25], - ]; - for (const [input, expected] of primitiveConversions) { - const strategy = new CountQueuingStrategy({ highWaterMark: input }); - if (strategy.highWaterMark !== expected) { - throw new Error(`${String(input)} converted to ${strategy.highWaterMark}`); - } - } - "#, - ) - }) - .await; -} - -#[tokio::test] -async fn high_water_mark_conversion_order_and_errors_are_observable() { - test_sync_with(|ctx| { - crate::llrt_stream_web::init(&ctx)?; - ctx.eval::<(), _>( - r#" - const order = []; - new WritableStream({}, { - get highWaterMark() { - order.push("get highWaterMark"); - return { - valueOf() { - order.push("convert highWaterMark"); - return 1; - }, - }; - }, - get size() { - order.push("get size"); - return undefined; - }, - }); - - const expectedOrder = - "get highWaterMark,convert highWaterMark,get size"; - if (order.join() !== expectedOrder) { - throw new Error(`Unexpected conversion order: ${order}`); - } - - const expectedError = new Error("number conversion failed"); - const throwingValue = { - valueOf() { - throw expectedError; - }, - }; - const factories = [ - () => new ReadableStream({}, { highWaterMark: throwingValue }), - () => new WritableStream({}, { highWaterMark: throwingValue }), - () => new TransformStream({}, { highWaterMark: throwingValue }), - () => new TransformStream({}, {}, { highWaterMark: throwingValue }), - () => new CountQueuingStrategy({ highWaterMark: throwingValue }), - () => new ByteLengthQueuingStrategy({ highWaterMark: throwingValue }), - ]; - - for (const factory of factories) { - try { - factory(); - throw new Error("Expected number conversion to throw"); - } catch (error) { - if (error !== expectedError) { - throw new Error(`Unexpected conversion error: ${error}`); - } - } - } - - for (const input of [1n, Symbol("highWaterMark")]) { - try { - new CountQueuingStrategy({ highWaterMark: input }); - throw new Error("Expected ToNumber to reject the value"); - } catch (error) { - if (!(error instanceof TypeError)) { - throw new Error(`Expected TypeError, got ${error}`); - } - } - } - "#, - ) - }) - .await; -} - -#[tokio::test] -async fn invalid_converted_stream_high_water_marks_throw_range_error() { - test_sync_with(|ctx| { - crate::llrt_stream_web::init(&ctx)?; - ctx.eval::<(), _>( - r#" - for (const highWaterMark of ["-1", "not a number"]) { - const factories = [ - () => new ReadableStream({}, { highWaterMark }), - () => new WritableStream({}, { highWaterMark }), - () => new TransformStream({}, { highWaterMark }), - () => new TransformStream({}, {}, { highWaterMark }), - ]; - for (const factory of factories) { - try { - factory(); - throw new Error("Expected invalid highWaterMark to throw"); - } catch (error) { - if (!(error instanceof RangeError)) { - throw new Error(`Expected RangeError, got ${error}`); - } - } - } - } - "#, - ) - }) - .await; -} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/byob_reader.rs b/stdlib/src/llrt/llrt_stream_web/readable/byob_reader.rs deleted file mode 100644 index 6742742a..00000000 --- a/stdlib/src/llrt/llrt_stream_web/readable/byob_reader.rs +++ /dev/null @@ -1,624 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use std::collections::VecDeque; - -use crate::llrt_utils::{bytes::ObjectBytes, primordials::Primordial}; -use rquickjs::{ - atom::PredefinedAtom, - class::{JsClass, OwnedBorrowMut, Trace, Tracer}, - function::Constructor, - methods, - prelude::{Opt, This}, - ArrayBuffer, Class, Ctx, Error, Exception, FromJs, Function, IntoJs, JsLifetime, Object, - Promise, Result, Value, -}; - -use crate::llrt_stream_web::{ - readable::{ - byte_controller::ReadableByteStreamController, - controller::{ReadableStreamController, ReadableStreamControllerClass}, - default_reader::{ReadableStreamDefaultReaderOwned, ReadableStreamReadResult}, - objects::{ReadableStreamBYOBObjects, ReadableStreamObjects}, - reader::{ReadableStreamGenericReader, ReadableStreamReader, ReadableStreamReaderOwned}, - stream::{ReadableStreamOwned, ReadableStreamState}, - }, - utils::{ - promise::{promise_rejected_with_constructor, with_promise_result, ResolveablePromise}, - UnwrapOrUndefined, ValueOrUndefined, - }, -}; - -#[derive(Trace)] -#[rquickjs::class] -pub(crate) struct ReadableStreamBYOBReader<'js> { - pub(super) generic: ReadableStreamGenericReader<'js>, - pub(super) read_into_requests: VecDeque + 'js>>, -} - -pub(crate) type ReadableStreamBYOBReaderClass<'js> = Class<'js, ReadableStreamBYOBReader<'js>>; -pub(crate) type ReadableStreamBYOBReaderOwned<'js> = - OwnedBorrowMut<'js, ReadableStreamBYOBReader<'js>>; - -unsafe impl<'js> JsLifetime<'js> for ReadableStreamBYOBReader<'js> { - type Changed<'to> = ReadableStreamBYOBReader<'to>; -} - -impl<'js> ReadableStreamBYOBReader<'js> { - pub(super) fn readable_stream_byob_reader_error_read_into_requests( - mut objects: ReadableStreamBYOBObjects<'js>, - e: Value<'js>, - ) -> Result> { - // Let readIntoRequests be reader.[[readIntoRequests]]. - let read_into_requests = &mut objects.reader.read_into_requests; - - // Set reader.[[readIntoRequests]] to a new empty list. - let read_into_requests = read_into_requests.split_off(0); - // For each readIntoRequest of readIntoRequests, - for read_into_request in read_into_requests { - // Perform readIntoRequest’s error steps, given e. - objects = read_into_request.error_steps(objects, e.clone())?; - } - - Ok(objects) - } - - pub(super) fn set_up_readable_stream_byob_reader( - ctx: Ctx<'js>, - stream: ReadableStreamOwned<'js>, - ) -> Result<(ReadableStreamOwned<'js>, Class<'js, Self>)> { - // If ! IsReadableStreamLocked(stream) is true, throw a TypeError exception. - if stream.is_readable_stream_locked() { - return Err(Exception::throw_type( - &ctx, - "This stream has already been locked for exclusive reading by another reader", - )); - } - - // If stream.[[controller]] does not implement ReadableByteStreamController, throw a TypeError exception. - match stream.controller { - ReadableStreamControllerClass::ReadableStreamByteController(_) => {} - _ => { - return Err(Exception::throw_type( - &ctx, - "Cannot construct a ReadableStreamBYOBReader for a stream not constructed with a byte source", - )); - } - }; - - // Perform ! ReadableStreamReaderGenericInitialize(reader, stream). - let generic = - ReadableStreamGenericReader::readable_stream_reader_generic_initialize(&ctx, stream)?; - - let mut stream = OwnedBorrowMut::from_class(generic.stream.clone().unwrap()); - - let reader = Class::instance( - ctx.clone(), - Self { - generic, - // Set reader.[[readIntoRequests]] to a new empty list. - read_into_requests: VecDeque::new(), - }, - )?; - - stream.reader = Some(reader.clone().into()); - - Ok((stream, reader)) - } - - pub(super) fn readable_stream_byob_reader_release( - mut objects: ReadableStreamBYOBObjects<'js>, - ) -> Result> { - // Perform ! ReadableStreamReaderGenericRelease(reader). - objects - .reader - .generic - .readable_stream_reader_generic_release(&mut objects.stream, || { - objects.controller.release_steps() - })?; - - // Let e be a new TypeError exception. - let e: Value = objects - .stream - .constructor_type_error - .call(("Reader was released",))?; - // Perform ! ReadableStreamBYOBReaderErrorReadIntoRequests(reader, e). - Self::readable_stream_byob_reader_error_read_into_requests(objects, e) - } - - pub(super) fn readable_stream_byob_reader_read( - ctx: &Ctx<'js>, - // Let stream be reader.[[stream]]. - mut objects: ReadableStreamBYOBObjects<'js>, - view: ViewBytes<'js>, - min: u64, - read_into_request: impl ReadableStreamReadIntoRequest<'js> + 'js, - ) -> Result> { - // Set stream.[[disturbed]] to true. - objects.stream.disturbed = true; - - // If stream.[[state]] is "errored", perform readIntoRequest’s error steps given stream.[[storedError]]. - if let ReadableStreamState::Errored(ref stored_error) = objects.stream.state { - let stored_error = stored_error.clone(); - read_into_request.error_steps(objects, stored_error) - } else { - // Otherwise, perform ! ReadableByteStreamControllerPullInto(stream.[[controller]], view, min, readIntoRequest). - ReadableByteStreamController::readable_byte_stream_controller_pull_into( - ctx, - objects, - view, - min, - read_into_request, - ) - } - } -} - -#[methods(rename_all = "camelCase")] -impl<'js> ReadableStreamBYOBReader<'js> { - // this is required by web platform tests - #[qjs(get)] - pub fn constructor(ctx: Ctx<'js>) -> Result>> { - ::constructor(&ctx) - } - - #[qjs(constructor)] - pub fn new(ctx: Ctx<'js>, stream: ReadableStreamOwned<'js>) -> Result> { - // Perform ? SetUpReadableStreamBYOBReader(this, stream). - let (_, reader) = Self::set_up_readable_stream_byob_reader(ctx, stream)?; - Ok(reader) - } - - fn read( - ctx: Ctx<'js>, - reader: This>, - view: Opt>, - options: Opt>, - ) -> Result> { - with_promise_result(&ctx, || { - let options = match options.0 { - None => ReadableStreamBYOBReaderReadOptions { min: 1 }, - Some(value) => ReadableStreamBYOBReaderReadOptions::from_js(&ctx, value)?, - }; - - let view = ViewBytes::from_value( - &ctx, - &reader.generic.function_array_buffer_is_view, - view.0.as_ref(), - )?; - - let (buffer, byte_length, _) = view.get_array_buffer()?; - - // If view.[[ByteLength]] is 0, return a promise rejected with a TypeError exception. - if byte_length == 0 { - return promise_rejected_with_constructor( - &reader.generic.constructor_type_error, - &reader.generic.promise_primordials, - "view must have non-zero byteLength", - ); - } - - // If view.[[ViewedArrayBuffer]].[[ArrayBufferByteLength]] is 0, return a promise rejected with a TypeError exception. - if buffer.is_empty() { - return promise_rejected_with_constructor( - &reader.generic.constructor_type_error, - &reader.generic.promise_primordials, - "view's buffer must have non-zero byteLength", - ); - } - - // If ! IsDetachedBuffer(view.[[ViewedArrayBuffer]]) is true, return a promise rejected with a TypeError exception. - if buffer.as_bytes().is_none() { - return promise_rejected_with_constructor( - &reader.generic.constructor_type_error, - &reader.generic.promise_primordials, - "view's buffer has been detached", - ); - } - - // If options["min"] is 0, return a promise rejected with a TypeError exception. - if options.min == 0 { - return promise_rejected_with_constructor( - &reader.generic.constructor_type_error, - &reader.generic.promise_primordials, - "options.min must be greater than 0", - ); - } - - // If view has a [[TypedArrayName]] internal slot, - let typed_array_len = match &view.0 { - ObjectBytes::U8Array(a) => Some(a.len()), - ObjectBytes::I8Array(a) => Some(a.len()), - ObjectBytes::U16Array(a) => Some(a.len()), - ObjectBytes::I16Array(a) => Some(a.len()), - ObjectBytes::U32Array(a) => Some(a.len()), - ObjectBytes::I32Array(a) => Some(a.len()), - ObjectBytes::U64Array(a) => Some(a.len()), - ObjectBytes::I64Array(a) => Some(a.len()), - ObjectBytes::F32Array(a) => Some(a.len()), - ObjectBytes::F64Array(a) => Some(a.len()), - _ => None, - }; - if let Some(typed_array_len) = typed_array_len { - // If options["min"] > view.[[ArrayLength]], return a promise rejected with a RangeError exception. - if options.min > typed_array_len as u64 { - return promise_rejected_with_constructor( - &reader.generic.constructor_range_error, - &reader.generic.promise_primordials, - "options.min must be less than or equal to views length", - ); - } - } else { - // Otherwise (i.e., it is a DataView), - // If options["min"] > view.[[ByteLength]], return a promise rejected with a RangeError exception. - if options.min > byte_length as u64 { - return promise_rejected_with_constructor( - &reader.generic.constructor_range_error, - &reader.generic.promise_primordials, - "options.min must be less than or equal to views byteLength", - ); - } - } - - // If this.[[stream]] is undefined, return a promise rejected with a TypeError exception. - if reader.generic.stream.is_none() { - return promise_rejected_with_constructor( - &reader.generic.constructor_type_error, - &reader.generic.promise_primordials, - "Cannot read a stream using a released reader", - ); - } - - // Let promise be a new promise. - let promise = ResolveablePromise::new(&ctx)?; - // Let readIntoRequest be a new read-into request with the following items: - #[derive(Trace)] - struct ReadIntoRequest<'js> { - promise: ResolveablePromise<'js>, - } - - impl<'js> ReadableStreamReadIntoRequest<'js> for ReadIntoRequest<'js> { - // chunk steps, given chunk - // Resolve promise with «[ "value" → chunk, "done" → false ]». - fn chunk_steps( - &self, - objects: ReadableStreamBYOBObjects<'js>, - chunk: Value<'js>, - ) -> Result> { - self.promise.resolve(ReadableStreamReadResult { - value: Some(chunk), - done: false, - })?; - Ok(objects) - } - - // close steps, given chunk - // Resolve promise with «[ "value" → chunk, "done" → true ]». - fn close_steps( - &self, - objects: ReadableStreamBYOBObjects<'js>, - chunk: Value<'js>, - ) -> Result> { - self.promise.resolve(ReadableStreamReadResult { - value: Some(chunk), - done: true, - })?; - Ok(objects) - } - - // error steps, given e - // Reject promise with e. - fn error_steps( - &self, - objects: ReadableStreamBYOBObjects<'js>, - reason: Value<'js>, - ) -> Result> { - self.promise.reject(reason)?; - Ok(objects) - } - } - - let objects = ReadableStreamObjects::from_byob_reader(reader.0); - - // Perform ! ReadableStreamBYOBReaderRead(this, view, options["min"], readIntoRequest). - Self::readable_stream_byob_reader_read( - &ctx, - objects, - view, - options.min, - ReadIntoRequest { - promise: promise.clone(), - }, - )?; - - // Return promise. - Ok(promise.promise) - }) - } - - fn release_lock(reader: This>) -> Result<()> { - // If this.[[stream]] is undefined, return. - if reader.generic.stream.is_none() { - return Ok(()); - }; - - let objects = ReadableStreamObjects::from_byob_reader(reader.0); - - // Perform ! ReadableStreamBYOBReaderRelease(this). - Self::readable_stream_byob_reader_release(objects)?; - - Ok(()) - } - - #[qjs(get)] - fn closed(&self) -> Promise<'js> { - self.generic.closed_promise.promise.clone() - } - - fn cancel( - ctx: Ctx<'js>, - reader: This>, - reason: Opt>, - ) -> Result> { - if reader.generic.stream.is_none() { - // If this.[[stream]] is undefined, return a promise rejected with a TypeError exception. - return promise_rejected_with_constructor( - &reader.generic.constructor_type_error, - &reader.generic.promise_primordials, - "Cannot cancel a stream using a released reader", - ); - } - - let objects = ReadableStreamObjects::from_byob_reader(reader.0); - - // Return ! ReadableStreamReaderGenericCancel(this, reason). - let (promise, _) = ReadableStreamGenericReader::readable_stream_reader_generic_cancel( - ctx.clone(), - objects, - reason.0.unwrap_or_undefined(&ctx), - )?; - Ok(promise) - } -} - -struct ReadableStreamBYOBReaderReadOptions { - min: u64, -} - -impl<'js> FromJs<'js> for ReadableStreamBYOBReaderReadOptions { - fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let ty_name = value.type_name(); - let obj = value - .as_object() - .ok_or(Error::new_from_js(ty_name, "Object"))?; - - let min = obj.get_value_or_undefined::<_, f64>("min")?.unwrap_or(1.0); - if min < u64::MIN as f64 || min > u64::MAX as f64 { - return Err(Exception::throw_type( - ctx, - "min on ReadableStreamBYOBReaderReadOptions must fit into unsigned long long", - )); - }; - - Ok(Self { min: min as u64 }) - } -} - -pub(super) trait ReadableStreamReadIntoRequest<'js>: Trace<'js> { - fn chunk_steps( - &self, - objects: ReadableStreamBYOBObjects<'js>, - chunk: Value<'js>, - ) -> Result>; - - fn close_steps( - &self, - objects: ReadableStreamBYOBObjects<'js>, - chunk: Value<'js>, - ) -> Result>; - - fn error_steps( - &self, - objects: ReadableStreamBYOBObjects<'js>, - reason: Value<'js>, - ) -> Result>; -} - -impl<'js> Trace<'js> for Box + 'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - self.as_ref().trace(tracer); - } -} - -#[derive(JsLifetime, Clone)] -pub(super) struct ViewBytes<'js>(ObjectBytes<'js>); - -impl<'js> ViewBytes<'js> { - pub(super) fn from_object( - ctx: &Ctx<'js>, - function_array_buffer_is_view: &Function<'js>, - object: &Object<'js>, - ) -> Result { - if function_array_buffer_is_view.call::<_, bool>((object.clone(),))? { - if let Some(view) = ObjectBytes::from_array_buffer(object)? { - return Ok(Self(view)); - } - } - - Err(Exception::throw_type( - ctx, - "view must be an ArrayBufferView", - )) - } - - pub(super) fn from_value( - ctx: &Ctx<'js>, - function_array_buffer_is_view: &Function<'js>, - value: Option<&Value<'js>>, - ) -> Result { - match value.and_then(Value::as_object) { - None => { - Err(Exception::throw_type( - ctx, - "view must be typed DataView, Buffer, ArrayBuffer, or Uint8Array, but is not an object", - )) - }, - Some(object) => Self::from_object(ctx, function_array_buffer_is_view, object), - } - } - - pub(super) fn get_array_buffer(&self) -> Result<(ArrayBuffer<'js>, usize, usize)> { - Ok(self - .0 - .get_array_buffer()? - .expect("invariant broken; ViewBytes may not contain ObjectBytes::Vec")) - } - - pub(super) fn element_size(&self) -> usize { - match self.0 { - ObjectBytes::U8Array(_) => 1, - ObjectBytes::I8Array(_) => 1, - ObjectBytes::U16Array(_) => 2, - ObjectBytes::I16Array(_) => 2, - ObjectBytes::U32Array(_) => 4, - ObjectBytes::I32Array(_) => 4, - ObjectBytes::U64Array(_) => 8, - ObjectBytes::I64Array(_) => 8, - ObjectBytes::F16Array(_) => 2, - ObjectBytes::F32Array(_) => 4, - ObjectBytes::F64Array(_) => 8, - ObjectBytes::U8ClampedArray(_) => 1, - ObjectBytes::DataView(_, _, _) => 1, - ObjectBytes::Vec(_) => { - panic!("invariant broken; ViewBytes may not contain ObjectBytes::Vec") - } - } - } -} - -#[derive(Clone, JsLifetime)] -pub(crate) struct ArrayConstructorPrimordials<'js> { - pub(super) constructor_uint8array: Constructor<'js>, - constructor_int8array: Constructor<'js>, - constructor_uint16array: Constructor<'js>, - constructor_int16array: Constructor<'js>, - constructor_uint32array: Constructor<'js>, - constructor_int32array: Constructor<'js>, - constructor_uint64array: Constructor<'js>, - constructor_int64array: Constructor<'js>, - constructor_f16array: Constructor<'js>, - constructor_f32array: Constructor<'js>, - constructor_f64array: Constructor<'js>, - constructor_uint8clampedarray: Constructor<'js>, - constructor_data_view: Constructor<'js>, -} - -impl<'js> Trace<'js> for ArrayConstructorPrimordials<'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - self.constructor_uint8array.trace(tracer); - self.constructor_int8array.trace(tracer); - self.constructor_uint16array.trace(tracer); - self.constructor_int16array.trace(tracer); - self.constructor_uint32array.trace(tracer); - self.constructor_int32array.trace(tracer); - self.constructor_uint64array.trace(tracer); - self.constructor_int64array.trace(tracer); - self.constructor_f16array.trace(tracer); - self.constructor_f32array.trace(tracer); - self.constructor_f64array.trace(tracer); - self.constructor_uint8clampedarray.trace(tracer); - self.constructor_data_view.trace(tracer); - } -} - -impl<'js> Primordial<'js> for ArrayConstructorPrimordials<'js> { - fn new(ctx: &Ctx<'js>) -> Result - where - Self: Sized, - { - let globals = ctx.globals(); - Ok(Self { - constructor_uint8array: globals.get(PredefinedAtom::Uint8Array)?, - constructor_int8array: globals.get(PredefinedAtom::Int8Array)?, - constructor_uint16array: globals.get(PredefinedAtom::Uint16Array)?, - constructor_int16array: globals.get(PredefinedAtom::Int16Array)?, - constructor_uint32array: globals.get(PredefinedAtom::Uint32Array)?, - constructor_int32array: globals.get(PredefinedAtom::Int32Array)?, - constructor_uint64array: globals.get(PredefinedAtom::BigUint64Array)?, - constructor_int64array: globals.get(PredefinedAtom::BigInt64Array)?, - constructor_f16array: globals.get(PredefinedAtom::Float16Array)?, - constructor_f32array: globals.get(PredefinedAtom::Float32Array)?, - constructor_f64array: globals.get(PredefinedAtom::Float64Array)?, - constructor_uint8clampedarray: globals.get(PredefinedAtom::Uint8ClampedArray)?, - constructor_data_view: globals.get(PredefinedAtom::DataView)?, - }) - } -} - -impl<'js> ArrayConstructorPrimordials<'js> { - pub(super) fn for_view_bytes(&self, v: &ViewBytes<'js>) -> Constructor<'js> { - match v.0 { - ObjectBytes::U8Array(_) => self.constructor_uint8array.clone(), - ObjectBytes::I8Array(_) => self.constructor_int8array.clone(), - ObjectBytes::U16Array(_) => self.constructor_uint16array.clone(), - ObjectBytes::I16Array(_) => self.constructor_int16array.clone(), - ObjectBytes::U32Array(_) => self.constructor_uint32array.clone(), - ObjectBytes::I32Array(_) => self.constructor_int32array.clone(), - ObjectBytes::U64Array(_) => self.constructor_uint64array.clone(), - ObjectBytes::I64Array(_) => self.constructor_int64array.clone(), - ObjectBytes::F16Array(_) => self.constructor_f16array.clone(), - ObjectBytes::F32Array(_) => self.constructor_f32array.clone(), - ObjectBytes::F64Array(_) => self.constructor_f64array.clone(), - ObjectBytes::U8ClampedArray(_) => self.constructor_uint8clampedarray.clone(), - ObjectBytes::DataView(_, _, _) => self.constructor_data_view.clone(), - ObjectBytes::Vec(_) => { - panic!("invariant broken; ViewBytes may not contain ObjectBytes::Vec") - } - } - } -} - -impl<'js> Trace<'js> for ViewBytes<'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - self.0.trace(tracer); - } -} - -impl<'js> IntoJs<'js> for ViewBytes<'js> { - fn into_js(self, ctx: &Ctx<'js>) -> Result> { - self.0.into_js(ctx) - } -} - -impl<'js> ReadableStreamReader<'js> for ReadableStreamBYOBReaderOwned<'js> { - type Class = ReadableStreamBYOBReaderClass<'js>; - - fn with_reader( - self, - ctx: C, - _: impl FnOnce( - C, - ReadableStreamDefaultReaderOwned<'js>, - ) -> Result<(C, ReadableStreamDefaultReaderOwned<'js>)>, - byob: impl FnOnce( - C, - ReadableStreamBYOBReaderOwned<'js>, - ) -> Result<(C, ReadableStreamBYOBReaderOwned<'js>)>, - _: impl FnOnce(C) -> Result, - ) -> Result<(C, Self)> { - byob(ctx, self) - } - - fn into_inner(self) -> Self::Class { - self.into_inner() - } - - fn from_class(class: Self::Class) -> Self { - OwnedBorrowMut::from_class(class) - } - - fn try_from_erased(erased: Option>) -> Option { - match erased { - Some(ReadableStreamReaderOwned::ReadableStreamBYOBReader(r)) => Some(r), - _ => None, - } - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/byte_controller.rs b/stdlib/src/llrt/llrt_stream_web/readable/byte_controller.rs deleted file mode 100644 index 854be6f0..00000000 --- a/stdlib/src/llrt/llrt_stream_web/readable/byte_controller.rs +++ /dev/null @@ -1,2169 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use std::collections::VecDeque; - -use crate::llrt_utils::{ - error_messages::ERROR_MSG_ARRAY_BUFFER_DETACHED, - option::{Null, Undefined}, - primordials::{BasePrimordials, Primordial}, - result::ResultExt, -}; -use rquickjs::{ - class::{OwnedBorrow, OwnedBorrowMut, Trace, Tracer}, - function::Constructor, - methods, - prelude::{Opt, This}, - ArrayBuffer, Class, Ctx, Error, Exception, Function, IntoJs, JsLifetime, Object, Promise, - Result, TypedArray, Value, -}; - -use crate::llrt_stream_web::{ - readable::{ - byob_reader::{ArrayConstructorPrimordials, ReadableStreamReadIntoRequest, ViewBytes}, - controller::{ - ReadableStreamController, ReadableStreamControllerClass, ReadableStreamControllerOwned, - }, - default_controller::ReadableStreamDefaultControllerOwned, - default_reader::ReadableStreamReadRequest, - objects::{ - ReadableByteStreamObjects, ReadableStreamBYOBObjects, ReadableStreamClassObjects, - ReadableStreamDefaultReaderObjects, ReadableStreamObjects, - }, - reader::ReadableStreamReader, - stream::{ - algorithms::{CancelAlgorithm, PullAlgorithm, StartAlgorithm}, - source::UnderlyingSource, - ReadableStream, ReadableStreamClass, ReadableStreamOwned, ReadableStreamState, - }, - }, - utils::{ - class_from_owned_borrow_mut, - promise::{promise_resolved_with, upon_promise}, - UnwrapOrUndefined, - }, -}; - -#[derive(JsLifetime)] -#[rquickjs::class] -pub struct ReadableByteStreamController<'js> { - auto_allocate_chunk_size: Option, - byob_request: Option>>, - cancel_algorithm: Option>, - close_requested: bool, - pull_again: bool, - pull_algorithm: Option>, - pulling: bool, - pub(super) pending_pull_intos: VecDeque>, - queue: VecDeque>, - queue_total_size: usize, - started: bool, - strategy_hwm: f64, - pub(super) stream: ReadableStreamClass<'js>, - - pub(super) array_constructor_primordials: ArrayConstructorPrimordials<'js>, - constructor_array_buffer: Constructor<'js>, - pub(super) function_array_buffer_is_view: Function<'js>, -} - -impl<'js> Trace<'js> for ReadableByteStreamController<'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - self.auto_allocate_chunk_size.trace(tracer); - self.byob_request.trace(tracer); - self.cancel_algorithm.trace(tracer); - self.pull_algorithm.trace(tracer); - self.pending_pull_intos.trace(tracer); - self.queue.trace(tracer); - self.queue_total_size.trace(tracer); - self.started.trace(tracer); - self.strategy_hwm.trace(tracer); - self.stream.trace(tracer); - self.array_constructor_primordials.trace(tracer); - self.constructor_array_buffer.trace(tracer); - self.function_array_buffer_is_view.trace(tracer); - } -} - -pub type ReadableByteStreamControllerClass<'js> = Class<'js, ReadableByteStreamController<'js>>; -pub(crate) type ReadableByteStreamControllerOwned<'js> = - OwnedBorrowMut<'js, ReadableByteStreamController<'js>>; - -impl<'js> ReadableByteStreamController<'js> { - // SetUpReadableByteStreamControllerFromUnderlyingSource - pub(super) fn set_up_readable_byte_stream_controller_from_underlying_source( - ctx: &Ctx<'js>, - stream: ReadableStreamOwned<'js>, - underlying_source: Null>>, - underlying_source_dict: UnderlyingSource<'js>, - high_water_mark: f64, - ) -> Result<()> { - let (start_algorithm, pull_algorithm, cancel_algorithm, auto_allocate_chunk_size) = ( - // If underlyingSourceDict["start"] exists, then set startAlgorithm to an algorithm which returns the result of invoking underlyingSourceDict["start"] with argument list - // « controller » and callback this value underlyingSource. - underlying_source_dict - .start - .map(|f| StartAlgorithm::Function { - f, - underlying_source: underlying_source.clone(), - }) - .unwrap_or(StartAlgorithm::ReturnUndefined), - // If underlyingSourceDict["pull"] exists, then set pullAlgorithm to an algorithm which returns the result of invoking underlyingSourceDict["pull"] with argument list - // « controller » and callback this value underlyingSource. - underlying_source_dict - .pull - .map(|f| PullAlgorithm::Function { - f, - underlying_source: underlying_source.clone(), - }) - .unwrap_or(PullAlgorithm::ReturnPromiseUndefined), - // If underlyingSourceDict["cancel"] exists, then set cancelAlgorithm to an algorithm which takes an argument reason and returns the result of invoking underlyingSourceDict["cancel"] with argument list - // « reason » and callback this value underlyingSource. - underlying_source_dict - .cancel - .map(|f| CancelAlgorithm::Function { - f, - underlying_source, - }) - .unwrap_or(CancelAlgorithm::ReturnPromiseUndefined), - // Let autoAllocateChunkSize be underlyingSourceDict["autoAllocateChunkSize"], if it exists, or undefined otherwise. - underlying_source_dict.auto_allocate_chunk_size, - ); - - // If autoAllocateChunkSize is 0, then throw a TypeError exception. - if auto_allocate_chunk_size == Some(0) { - return Err(Exception::throw_type( - ctx, - "autoAllocateChunkSize must be greater than 0", - )); - } - - Self::set_up_readable_byte_stream_controller( - ctx.clone(), - stream, - start_algorithm, - pull_algorithm, - cancel_algorithm, - high_water_mark, - auto_allocate_chunk_size, - )?; - - Ok(()) - } - - pub(super) fn set_up_readable_byte_stream_controller( - ctx: Ctx<'js>, - stream: ReadableStreamOwned<'js>, - start_algorithm: StartAlgorithm<'js>, - pull_algorithm: PullAlgorithm<'js>, - cancel_algorithm: CancelAlgorithm<'js>, - high_water_mark: f64, - auto_allocate_chunk_size: Option, - ) -> Result> { - let (stream_class, mut stream) = class_from_owned_borrow_mut(stream); - - let array_constructor_primordials = ArrayConstructorPrimordials::get(&ctx)?.clone(); - let BasePrimordials { - constructor_array_buffer, - function_array_buffer_is_view, - .. - } = &*BasePrimordials::get(&ctx)?; - - let controller = Self { - // Set controller.[[stream]] to stream. - stream: stream_class, - - // Set controller.[[pullAgain]] and controller.[[pulling]] to false. - pull_again: false, - pulling: false, - - // Set controller.[[byobRequest]] to null. - byob_request: None, - - // Perform ! ResetQueue(controller). - queue: VecDeque::new(), - queue_total_size: 0, - - // Set controller.[[closeRequested]] and controller.[[started]] to false. - close_requested: false, - started: false, - - // Set controller.[[strategyHWM]] to highWaterMark. - strategy_hwm: high_water_mark, - - // Set controller.[[pullAlgorithm]] to pullAlgorithm. - pull_algorithm: Some(pull_algorithm), - cancel_algorithm: Some(cancel_algorithm), - - // Set controller.[[autoAllocateChunkSize]] to autoAllocateChunkSize. - auto_allocate_chunk_size, - - pending_pull_intos: VecDeque::new(), - - array_constructor_primordials, - constructor_array_buffer: constructor_array_buffer.clone(), - function_array_buffer_is_view: function_array_buffer_is_view.clone(), - }; - - let controller_class = Class::instance(ctx.clone(), controller)?; - - // Set stream.[[controller]] to controller. - stream.controller = - ReadableStreamControllerClass::ReadableStreamByteController(controller_class.clone()); - - let objects = - ReadableStreamObjects::new_byte(stream, OwnedBorrowMut::from_class(controller_class)) - .refresh_reader(); - - let promise_primordials = objects.stream.promise_primordials.clone(); - - // Let startResult be the result of performing startAlgorithm. - let (start_result, objects_class) = - Self::start_algorithm(ctx.clone(), objects, start_algorithm)?; - - // Let startPromise be a promise resolved with startResult. - let start_promise = promise_resolved_with(&ctx, &promise_primordials, Ok(start_result))?; - - let _ = upon_promise::, _>(ctx.clone(), start_promise, { - let objects_class = objects_class.clone(); - move |ctx, result| { - let mut objects = - ReadableStreamObjects::from_class_no_reader(objects_class).refresh_reader(); - match result { - // Upon fulfillment of startPromise, - Ok(_) => { - // Set controller.[[started]] to true. - objects.controller.started = true; - // Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). - Self::readable_byte_stream_controller_call_pull_if_needed(ctx, objects)?; - Ok(()) - } - // Upon rejection of startPromise with reason r, - Err(r) => { - // Perform ! ReadableByteStreamControllerError(controller, r). - Self::readable_byte_stream_controller_error(objects, r)?; - Ok(()) - } - } - } - })?; - - Ok(objects_class.controller) - } - - fn readable_byte_stream_controller_call_pull_if_needed>( - ctx: Ctx<'js>, - objects: ReadableByteStreamObjects<'js, R>, - ) -> Result> { - // Let shouldPull be ! ReadableByteStreamControllerShouldCallPull(controller). - let (should_pull, mut objects) = - Self::readable_byte_stream_controller_should_call_pull(objects); - - // If shouldPull is false, return. - if !should_pull { - return Ok(objects); - } - - // If controller.[[pulling]] is true, - if objects.controller.pulling { - // Set controller.[[pullAgain]] to true. - objects.controller.pull_again = true; - - // Return. - return Ok(objects); - } - - // Set controller.[[pulling]] to true. - objects.controller.pulling = true; - - // Let pullPromise be the result of performing controller.[[pullAlgorithm]]. - let (pull_promise, objects_class) = Self::pull_algorithm(ctx.clone(), objects)?; - - upon_promise::, ()>(ctx, pull_promise, { - let objects_class = objects_class.clone(); - move |ctx, result| { - let mut objects = - ReadableStreamObjects::from_class_no_reader(objects_class).refresh_reader(); - match result { - // Upon fulfillment of pullPromise, - Ok(_) => { - // Set controller.[[pulling]] to false. - objects.controller.pulling = false; - // If controller.[[pullAgain]] is true, - if objects.controller.pull_again { - // Set controller.[[pullAgain]] to false. - objects.controller.pull_again = false; - // Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). - Self::readable_byte_stream_controller_call_pull_if_needed( - ctx, objects, - )?; - }; - Ok(()) - } - // Upon rejection of pullPromise with reason e, - Err(e) => { - // Perform ! ReadableByteStreamControllerError(controller, e). - Self::readable_byte_stream_controller_error(objects, e)?; - Ok(()) - } - } - } - })?; - - Ok(ReadableStreamObjects::from_class(objects_class)) - } - - fn readable_byte_stream_controller_should_call_pull>( - mut objects: ReadableByteStreamObjects<'js, R>, - ) -> (bool, ReadableByteStreamObjects<'js, R>) { - // Let stream be controller.[[stream]]. - match objects.stream.state { - ReadableStreamState::Readable => {} - // If stream.[[state]] is not "readable", return false. - _ => return (false, objects), - } - - // If controller.[[closeRequested]] is true, return false. - if objects.controller.close_requested { - return (false, objects); - } - - // If controller.[[started]] is false, return false. - if !objects.controller.started { - return (false, objects); - } - - let (mut has_read_requests, mut has_read_into_requests) = (false, false); - objects = objects - .with_reader( - |objects| { - // If ! ReadableStreamHasDefaultReader(stream) is true and ! ReadableStreamGetNumReadRequests(stream) > 0, return true. - if ReadableStream::readable_stream_get_num_read_requests(&objects.reader) > 0 { - has_read_requests = true; - } - Ok(objects) - }, - |objects| { - // If ! ReadableStreamHasBYOBReader(stream) is true and ! ReadableStreamGetNumReadIntoRequests(stream) > 0, return true. - if ReadableStream::readable_stream_get_num_read_into_requests(&objects.reader) - > 0 - { - has_read_into_requests = true; - } - Ok(objects) - }, - Ok, - ) - .unwrap(); - - if has_read_requests || has_read_into_requests { - return (true, objects); - } - - // Let desiredSize be ! ReadableByteStreamControllerGetDesiredSize(controller). - let desired_size = objects - .controller - .readable_byte_stream_controller_get_desired_size(&objects.stream); - - // Assert: desiredSize is not null. - if desired_size.0.expect("desired_size must not be null") > 0.0 { - // If desiredSize > 0, return true. - return (true, objects); - } - - // Return false. - (false, objects) - } - - pub(super) fn readable_byte_stream_controller_error>( - // Let stream be controller.[[stream]]. - mut objects: ReadableByteStreamObjects<'js, R>, - e: Value<'js>, - ) -> Result> { - // If stream.[[state]] is not "readable", return. - if !matches!(objects.stream.state, ReadableStreamState::Readable) { - return Ok(objects); - }; - - // Perform ! ReadableByteStreamControllerClearPendingPullIntos(controller). - objects - .controller - .readable_byte_stream_controller_clear_pending_pull_intos(); - - // Perform ! ResetQueue(controller). - objects.controller.reset_queue(); - - // Perform ! ReadableByteStreamControllerClearAlgorithms(controller). - objects - .controller - .readable_byte_stream_controller_clear_algorithms(); - - // Perform ! ReadableStreamError(stream, e). - ReadableStream::readable_stream_error(objects, e) - } - - fn readable_byte_stream_controller_clear_pending_pull_intos(&mut self) { - // Perform ! ReadableByteStreamControllerInvalidateBYOBRequest(controller). - self.readable_byte_stream_controller_invalidate_byob_request(); - - // Set controller.[[pendingPullIntos]] to a new empty list. - self.pending_pull_intos.clear(); - } - - fn readable_byte_stream_controller_invalidate_byob_request(&mut self) { - let byob_request = match self.byob_request { - // If controller.[[byobRequest]] is null, return. - None => return, - Some(ref byob_request) => byob_request.clone(), - }; - let mut byob_request = OwnedBorrowMut::from_class(byob_request); - byob_request.controller = None; - byob_request.view = None; - - self.byob_request = None; - } - - fn readable_byte_stream_controller_clear_algorithms(&mut self) { - self.pull_algorithm = None; - self.cancel_algorithm = None; - } - - pub(super) fn readable_byte_stream_controller_get_byob_request( - ctx: Ctx<'js>, - controller: OwnedBorrowMut<'js, Self>, - ) -> Result<( - Null>>, - OwnedBorrowMut<'js, Self>, - )> { - // If controller.[[byobRequest]] is null and controller.[[pendingPullIntos]] is not empty, - if controller.byob_request.is_none() && !controller.pending_pull_intos.is_empty() { - // Let firstDescriptor be controller.[[pendingPullIntos]][0]. - let first_descriptor = &controller.pending_pull_intos[0]; - - // Let view be ! Construct(%Uint8Array%, « firstDescriptor’s buffer, firstDescriptor’s byte offset + firstDescriptor’s bytes filled, firstDescriptor’s byte length − firstDescriptor’s bytes filled »). - let view = ViewBytes::from_value( - &ctx, - &controller.function_array_buffer_is_view, - Some( - &controller - .array_constructor_primordials - .constructor_uint8array - .construct(( - first_descriptor.buffer.clone(), - first_descriptor.byte_offset + first_descriptor.bytes_filled, - first_descriptor.byte_length - first_descriptor.bytes_filled, - ))?, - ), - )?; - - let (controller_class, mut controller) = class_from_owned_borrow_mut(controller); - - // Let byobRequest be a new ReadableStreamBYOBRequest. - let byob_request = ReadableStreamBYOBRequest { - // Set byobRequest.[[controller]] to controller. - controller: Some(controller_class), - // Set byobRequest.[[view]] to view. - view: Some(view), - }; - - // Set controller.[[byobRequest]] to byobRequest. - controller.byob_request = Some(Class::instance(ctx, byob_request)?); - - Ok((Null(controller.byob_request.clone()), controller)) - } else { - // Return controller.[[byobRequest]]. - Ok((Null(controller.byob_request.clone()), controller)) - } - } - - fn readable_byte_stream_controller_get_desired_size( - &self, - stream: &ReadableStream<'js>, - ) -> Null { - // Let state be controller.[[stream]].[[state]]. - match stream.state { - // If state is "errored", return null. - ReadableStreamState::Errored(_) => Null(None), - // If state is "closed", return 0. - ReadableStreamState::Closed => Null(Some(0.0)), - // Return controller.[[strategyHWM]] − controller.[[queueTotalSize]]. - _ => Null(Some(self.strategy_hwm - self.queue_total_size as f64)), - } - } - - fn reset_queue(&mut self) { - // Set container.[[queue]] to a new empty list. - self.queue.clear(); - // Set container.[[queueTotalSize]] to 0. - self.queue_total_size = 0; - } - - pub(super) fn readable_byte_stream_controller_close>( - ctx: Ctx<'js>, - // Let stream be controller.[[stream]]. - mut objects: ReadableByteStreamObjects<'js, R>, - ) -> Result> { - // If controller.[[closeRequested]] is true or stream.[[state]] is not "readable", return. - if objects.controller.close_requested - || !matches!(objects.stream.state, ReadableStreamState::Readable) - { - return Ok(objects); - } - - // If controller.[[queueTotalSize]] > 0, - if objects.controller.queue_total_size > 0 { - // Set controller.[[closeRequested]] to true. - objects.controller.close_requested = true; - // Return. - return Ok(objects); - } - - // If controller.[[pendingPullIntos]] is not empty, - // Let firstPendingPullInto be controller.[[pendingPullIntos]][0]. - if let Some(first_pending_pull_into) = objects.controller.pending_pull_intos.front() { - // If the remainder after dividing firstPendingPullInto’s bytes filled by firstPendingPullInto’s element size is not 0, - if first_pending_pull_into.bytes_filled % first_pending_pull_into.element_size != 0 { - // Let e be a new TypeError exception. - let e: Value = objects - .stream - .constructor_type_error - .call(("Insufficient bytes to fill elements in the given buffer",))?; - Self::readable_byte_stream_controller_error(objects, e.clone())?; - return Err(ctx.throw(e)); - } - } - - // Perform ! ReadableByteStreamControllerClearAlgorithms(controller). - objects - .controller - .readable_byte_stream_controller_clear_algorithms(); - - // Perform ! ReadableStreamClose(stream). - ReadableStream::readable_stream_close(ctx, objects) - } - - pub(super) fn readable_byte_stream_controller_enqueue>( - ctx: &Ctx<'js>, - // Let stream be controller.[[stream]]. - objects: ReadableByteStreamObjects<'js, R>, - chunk: ViewBytes<'js>, - ) -> Result> { - Self::readable_byte_stream_controller_enqueue_impl( - ctx, objects, chunk, /*skip_transfer=*/ false, - ) - } - - /// Like [`readable_byte_stream_controller_enqueue`] but skips the - /// spec-mandated `TransferArrayBuffer(chunk)` step on the incoming - /// chunk. Used by producers that already own the backing allocation - /// and want to hand it to the stream without QuickJS copying or - /// detaching it (e.g. `Blob.stream()`, where the backing - /// `ArrayBuffer` must survive multiple `.stream()` calls). - /// - /// Safety vs. correctness: the chunk we enqueue is NOT detached from - /// the producer's perspective, so both producer and consumer see the - /// same underlying bytes. This matches the existing non-isolation - /// behaviour of `Blob.arrayBuffer()` / `Blob.bytes()`, which already - /// return handles that alias the blob's storage. Pending BYOB - /// transfers of reader-provided buffers are unaffected — those are - /// separate buffers and still use spec-mandated transfer. - pub(super) fn readable_byte_stream_controller_enqueue_borrowed>( - ctx: &Ctx<'js>, - objects: ReadableByteStreamObjects<'js, R>, - chunk: ViewBytes<'js>, - ) -> Result> { - Self::readable_byte_stream_controller_enqueue_impl( - ctx, objects, chunk, /*skip_transfer=*/ true, - ) - } - - fn readable_byte_stream_controller_enqueue_impl>( - ctx: &Ctx<'js>, - mut objects: ReadableByteStreamObjects<'js, R>, - chunk: ViewBytes<'js>, - skip_transfer: bool, - ) -> Result> { - // If controller.[[closeRequested]] is true or stream.[[state]] is not "readable", return. - if objects.controller.close_requested - || !matches!(objects.stream.state, ReadableStreamState::Readable) - { - return Ok(objects); - }; - - // Let buffer be chunk.[[ViewedArrayBuffer]]. - // Let byteOffset be chunk.[[ByteOffset]]. - // Let byteLength be chunk.[[ByteLength]]. - let (buffer, byte_length, byte_offset) = chunk.get_array_buffer()?; - - // If ! IsDetachedBuffer(buffer) is true, throw a TypeError exception. - buffer.as_raw().ok_or(Exception::throw_type( - ctx, - "chunk's buffer is detached and so cannot be enqueued", - ))?; - - // Let transferredBuffer be ? TransferArrayBuffer(buffer). - // (When `skip_transfer` is true, the caller guarantees that the - // buffer is already owned exclusively by the stream for the - // purposes of this enqueue — see - // `readable_byte_stream_controller_enqueue_borrowed`.) - let transferred_buffer = if skip_transfer { - buffer - } else { - transfer_array_buffer(buffer)? - }; - - // If controller.[[pendingPullIntos]] is not empty, - // Let firstPendingPullInto be controller.[[pendingPullIntos]][0]. - if !objects.controller.pending_pull_intos.is_empty() { - // If ! IsDetachedBuffer(firstPendingPullInto’s buffer) is true, throw a TypeError exception. - objects.controller.pending_pull_intos[0] - .buffer - .as_raw() - .or_throw_type( - ctx, - "The BYOB request's buffer has been detached and so cannot be filled with an enqueued chunk", - )?; - - // Perform ! ReadableByteStreamControllerInvalidateBYOBRequest(controller). - objects - .controller - .readable_byte_stream_controller_invalidate_byob_request(); - - // Set firstPendingPullInto’s buffer to ! TransferArrayBuffer(firstPendingPullInto’s buffer). - objects.controller.pending_pull_intos[0].buffer = - transfer_array_buffer(objects.controller.pending_pull_intos[0].buffer.clone())?; - - // If firstPendingPullInto’s reader type is "none", perform ? ReadableByteStreamControllerEnqueueDetachedPullIntoToQueue(controller, firstPendingPullInto). - if let PullIntoDescriptorReaderType::None = - objects.controller.pending_pull_intos[0].reader_type - { - objects = Self::readable_byte_stream_enqueue_detached_pull_into_to_queue( - ctx.clone(), - objects, - 0, - )?; - } - } - - objects = objects.with_reader( - // If ! ReadableStreamHasDefaultReader(stream) is true, - |mut objects| { - // Perform ! ReadableByteStreamControllerProcessReadRequestsUsingQueue(controller). - objects = Self::readable_byte_stream_controller_process_read_requests_using_queue( - objects, ctx, - )?; - - // If ! ReadableStreamGetNumReadRequests(stream) is 0, - if ReadableStream::readable_stream_get_num_read_requests(&objects.reader) == 0 { - // Perform ! ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength). - objects - .controller - .readable_byte_stream_controller_enqueue_chunk_to_queue( - transferred_buffer.clone(), - byte_offset, - byte_length, - ) - } else { - // Otherwise, - // If controller.[[pendingPullIntos]] is not empty, - if !objects.controller.pending_pull_intos.is_empty() { - // Perform ! ReadableByteStreamControllerShiftPendingPullInto(controller). - objects - .controller - .readable_byte_stream_controller_shift_pending_pull_into(); - } - - // Let transferredView be ! Construct(%Uint8Array%, « transferredBuffer, byteOffset, byteLength »). - let transferred_view = ViewBytes::from_value( - ctx, - &objects.controller.function_array_buffer_is_view, - Some( - &objects - .controller - .array_constructor_primordials - .constructor_uint8array - .construct(( - transferred_buffer.clone(), - byte_offset, - byte_length, - ))?, - ), - ); - - // Perform ! ReadableStreamFulfillReadRequest(stream, transferredView, false). - objects = ReadableStream::readable_stream_fulfill_read_request( - ctx, - objects, - transferred_view.into_js(ctx)?, - false, - )?; - } - - Ok(objects) - }, - |mut objects| { - // Otherwise, if ! ReadableStreamHasBYOBReader(stream) is true, - // Perform ! ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength). - objects - .controller - .readable_byte_stream_controller_enqueue_chunk_to_queue( - transferred_buffer.clone(), - byte_offset, - byte_length, - ); - // Perform ! ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller). - - Self::readable_byte_stream_controller_process_pull_into_descriptors_using_queue( - ctx, objects, - ) - }, - |mut objects| { - // Otherwise, - // Perform ! ReadableByteStreamControllerEnqueueChunkToQueue(controller, transferredBuffer, byteOffset, byteLength). - objects - .controller - .readable_byte_stream_controller_enqueue_chunk_to_queue( - transferred_buffer.clone(), - byte_offset, - byte_length, - ); - - Ok(objects) - }, - )?; - - // Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). - Self::readable_byte_stream_controller_call_pull_if_needed(ctx.clone(), objects) - } - - fn readable_byte_stream_enqueue_detached_pull_into_to_queue>( - ctx: Ctx<'js>, - mut objects: ReadableByteStreamObjects<'js, R>, - pull_into_descriptor_index: usize, - ) -> Result> { - let pull_into_descriptor = - &objects.controller.pending_pull_intos[pull_into_descriptor_index]; - // If pullIntoDescriptor’s bytes filled > 0, perform ? ReadableByteStreamControllerEnqueueClonedChunkToQueue(controller, pullIntoDescriptor’s buffer, pullIntoDescriptor’s byte offset, pullIntoDescriptor’s bytes filled). - if pull_into_descriptor.bytes_filled > 0 { - let buffer = pull_into_descriptor.buffer.clone(); - let byte_offset = pull_into_descriptor.byte_offset; - let bytes_filled = pull_into_descriptor.bytes_filled; - objects = Self::readable_byte_stream_controller_enqueue_cloned_chunk_to_queue( - ctx, - objects, - &buffer, - byte_offset, - bytes_filled, - )?; - } - - // Perform ! ReadableByteStreamControllerShiftPendingPullInto(controller). - objects - .controller - .readable_byte_stream_controller_shift_pending_pull_into(); - - Ok(objects) - } - - fn readable_byte_stream_controller_process_read_requests_using_queue( - mut objects: ReadableStreamDefaultReaderObjects<'js, OwnedBorrowMut<'js, Self>>, - ctx: &Ctx<'js>, - ) -> Result>> { - // While reader.[[readRequests]] is not empty, - while !objects.reader.read_requests.is_empty() { - // If controller.[[queueTotalSize]] is 0, return. - if objects.controller.queue_total_size == 0 { - return Ok(objects); - } - - // Let readRequest be reader.[[readRequests]][0]. - // Remove readRequest from reader.[[readRequests]]. - let read_request = objects.reader.read_requests.pop_front().unwrap(); - // Perform ! ReadableByteStreamControllerFillReadRequestFromQueue(controller, readRequest). - objects = Self::readable_byte_stream_controller_fill_read_request_from_queue( - ctx, - objects, - read_request, - )?; - } - - Ok(objects) - } - - fn readable_byte_stream_controller_shift_pending_pull_into( - &mut self, - ) -> PullIntoDescriptor<'js> { - // Invalidate byobRequest since the first pending pull-into is being removed - self.readable_byte_stream_controller_invalidate_byob_request(); - // Let descriptor be controller.[[pendingPullIntos]][0]. - // Remove descriptor from controller.[[pendingPullIntos]]. - // Return descriptor. - self.pending_pull_intos.pop_front().expect( - "ReadableByteStreamControllerShiftPendingPullInto called on empty pendingPullIntos", - ) - } - - fn readable_byte_stream_controller_enqueue_chunk_to_queue( - &mut self, - buffer: ArrayBuffer<'js>, - byte_offset: usize, - byte_length: usize, - ) { - // Append a new readable byte stream queue entry with buffer buffer, byte offset byteOffset, and byte length byteLength to controller.[[queue]]. - self.queue.push_back(ReadableByteStreamQueueEntry { - buffer, - byte_offset, - byte_length, - }); - - // Set controller.[[queueTotalSize]] to controller.[[queueTotalSize]] + byteLength. - self.queue_total_size += byte_length; - } - - fn readable_byte_stream_controller_process_pull_into_descriptors_using_queue< - R: ReadableStreamReader<'js>, - >( - ctx: &Ctx<'js>, - mut objects: ReadableByteStreamObjects<'js, R>, - ) -> Result> { - // While controller.[[pendingPullIntos]] is not empty, - while !objects.controller.pending_pull_intos.is_empty() { - // If controller.[[queueTotalSize]] is 0, return. - if objects.controller.queue_total_size == 0 { - return Ok(objects); - } - - // Let pullIntoDescriptor be controller.[[pendingPullIntos]][0]. - let mut pull_into_descriptor_ref = PullIntoDescriptorRefMut::Index(0); - - // If ! ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor) is true, - if objects - .controller - .readable_byte_stream_controller_fill_pull_into_descriptor_from_queue( - ctx, - &mut pull_into_descriptor_ref, - )? - { - // Perform ! ReadableByteStreamControllerShiftPendingPullInto(controller). - let pull_into_descriptor = objects - .controller - .readable_byte_stream_controller_shift_pending_pull_into(); - - // Perform ! ReadableByteStreamControllerCommitPullIntoDescriptor(controller.[[stream]], pullIntoDescriptor). - objects = Self::readable_byte_stream_controller_commit_pull_into_descriptor( - ctx.clone(), - objects, - pull_into_descriptor, - )?; - } - } - Ok(objects) - } - - fn readable_byte_stream_controller_enqueue_cloned_chunk_to_queue< - R: ReadableStreamReader<'js>, - >( - ctx: Ctx<'js>, - mut objects: ReadableByteStreamObjects<'js, R>, - buffer: &ArrayBuffer<'js>, - byte_offset: usize, - byte_length: usize, - ) -> Result> { - // Let cloneResult be CloneArrayBuffer(buffer, byteOffset, byteLength, %ArrayBuffer%). - let clone_result = match ArrayBuffer::new_copy( - ctx.clone(), - &buffer.as_bytes().expect( - "ReadableByteStreamControllerEnqueueClonedChunkToQueue called on detached buffer", - )[byte_offset..byte_offset + byte_length], - ) { - Ok(clone_result) => clone_result, - Err(Error::Exception) => { - let err = ctx.catch(); - Self::readable_byte_stream_controller_error(objects, err.clone())?; - return Err(ctx.throw(err)); - } - Err(err) => return Err(err), - }; - - // Perform ! ReadableByteStreamControllerEnqueueChunkToQueue(controller, cloneResult.[[Value]], 0, byteLength). - objects - .controller - .readable_byte_stream_controller_enqueue_chunk_to_queue(clone_result, 0, byte_length); - - Ok(objects) - } - - fn readable_byte_stream_controller_fill_read_request_from_queue( - ctx: &Ctx<'js>, - mut objects: ReadableStreamDefaultReaderObjects<'js, OwnedBorrowMut<'js, Self>>, - read_request: impl ReadableStreamReadRequest<'js>, - ) -> Result>> { - let entry = { - // Assert: controller.[[queueTotalSize]] > 0. - // Let entry be controller.[[queue]][0]. - // Remove entry from controller.[[queue]]. - let entry = objects.controller.queue.pop_front().expect( - "ReadableByteStreamControllerFillReadRequestFromQueue called with empty queue", - ); - - // Set controller.[[queueTotalSize]] to controller.[[queueTotalSize]] − entry’s byte length. - objects.controller.queue_total_size -= entry.byte_length; - - entry - }; - - // Perform ! ReadableByteStreamControllerHandleQueueDrain(controller). - objects = Self::readable_byte_stream_controller_handle_queue_drain(ctx.clone(), objects)?; - - // Let view be ! Construct(%Uint8Array%, « entry’s buffer, entry’s byte offset, entry’s byte length »). - let view: TypedArray = objects - .controller - .array_constructor_primordials - .constructor_uint8array - .construct((entry.buffer, entry.byte_offset, entry.byte_length))?; - - // Perform readRequest’s chunk steps, given view. - read_request.chunk_steps_typed(objects, view.into_value()) - } - - fn readable_byte_stream_controller_fill_pull_into_descriptor_from_queue<'a>( - &'a mut self, - ctx: &Ctx<'js>, - pull_into_descriptor_ref: &mut PullIntoDescriptorRefMut<'js, 'a>, - ) -> Result { - let (mut total_bytes_to_copy_remaining, ready) = { - let pull_into_descriptor = match pull_into_descriptor_ref { - PullIntoDescriptorRefMut::Index(i) => &mut self.pending_pull_intos[*i], - PullIntoDescriptorRefMut::Owned(r) => r, - }; - // Let maxBytesToCopy be min(controller.[[queueTotalSize]], pullIntoDescriptor’s byte length − pullIntoDescriptor’s bytes filled). - let max_bytes_to_copy: usize = std::cmp::min( - self.queue_total_size, - pull_into_descriptor.byte_length - pull_into_descriptor.bytes_filled, - ); - - // Let maxBytesFilled be pullIntoDescriptor’s bytes filled + maxBytesToCopy. - let max_bytes_filled = pull_into_descriptor.bytes_filled + max_bytes_to_copy; - - // Let totalBytesToCopyRemaining be maxBytesToCopy. - let mut total_bytes_to_copy_remaining = max_bytes_to_copy; - - // Let ready be false. - let mut ready = false; - - // Let remainderBytes be the remainder after dividing maxBytesFilled by pullIntoDescriptor’s element size. - let remainder_bytes = max_bytes_filled % pull_into_descriptor.element_size; - - // Let maxAlignedBytes be maxBytesFilled − remainderBytes. - let max_aligned_bytes = max_bytes_filled - remainder_bytes; - - // If maxAlignedBytes ≥ pullIntoDescriptor’s minimum fill, - if max_aligned_bytes >= pull_into_descriptor.minimum_fill { - // Set totalBytesToCopyRemaining to maxAlignedBytes − pullIntoDescriptor’s bytes filled. - total_bytes_to_copy_remaining = - max_aligned_bytes - pull_into_descriptor.bytes_filled; - // Set ready to true. - ready = true - } - - (total_bytes_to_copy_remaining, ready) - }; - - // Let queue be controller.[[queue]]. - // While totalBytesToCopyRemaining > 0, - while total_bytes_to_copy_remaining > 0 { - let bytes_to_copy = { - let pull_into_descriptor = match pull_into_descriptor_ref { - PullIntoDescriptorRefMut::Index(i) => &mut self.pending_pull_intos[*i], - PullIntoDescriptorRefMut::Owned(r) => r, - }; - - // Let headOfQueue be queue[0]. - let head_of_queue = self - .queue - .front_mut() - .expect("empty queue with bytes to copy"); - // Let bytesToCopy be min(totalBytesToCopyRemaining, headOfQueue’s byte length). - let bytes_to_copy: usize = - std::cmp::min(total_bytes_to_copy_remaining, head_of_queue.byte_length); - // Let destStart be pullIntoDescriptor’s byte offset + pullIntoDescriptor’s bytes filled. - let dest_start: usize = - pull_into_descriptor.byte_offset + pull_into_descriptor.bytes_filled; - // Perform ! CopyDataBlockBytes(pullIntoDescriptor’s buffer.[[ArrayBufferData]], destStart, headOfQueue’s buffer.[[ArrayBufferData]], headOfQueue’s byte offset, bytesToCopy). - copy_data_block_bytes( - ctx, - &pull_into_descriptor.buffer, - dest_start, - &head_of_queue.buffer, - head_of_queue.byte_offset, - bytes_to_copy, - )?; - if head_of_queue.byte_length == bytes_to_copy { - // If headOfQueue’s byte length is bytesToCopy, - // Remove queue[0]. - self.queue.pop_front(); - } else { - // Otherwise, - // Set headOfQueue’s byte offset to headOfQueue’s byte offset + bytesToCopy. - head_of_queue.byte_offset += bytes_to_copy; - // Set headOfQueue’s byte length to headOfQueue’s byte length − bytesToCopy. - head_of_queue.byte_length -= bytes_to_copy - } - - // Set controller.[[queueTotalSize]] to controller.[[queueTotalSize]] − bytesToCopy. - self.queue_total_size -= bytes_to_copy; - - bytes_to_copy - }; - - // Perform ! ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesToCopy, pullIntoDescriptor). - self.readable_byte_stream_controller_fill_head_pull_into_descriptor( - bytes_to_copy, - pull_into_descriptor_ref, - ); - - // Set totalBytesToCopyRemaining to totalBytesToCopyRemaining − bytesToCopy. - total_bytes_to_copy_remaining -= bytes_to_copy - } - - Ok(ready) - } - - fn readable_byte_stream_controller_commit_pull_into_descriptor>( - ctx: Ctx<'js>, - objects: ReadableByteStreamObjects<'js, R>, - pull_into_descriptor: PullIntoDescriptor<'js>, - ) -> Result> { - // Let done be false. - let mut done = false; - // If stream.[[state]] is "closed", - if matches!(objects.stream.state, ReadableStreamState::Closed) { - // Set done to true. - done = true - } - - let reader_type = pull_into_descriptor.reader_type; - - // Let filledView be ! ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor). - let filled_view = Self::readable_byte_stream_controller_convert_pull_into_descriptor( - ctx.clone(), - &objects.stream.function_array_buffer_is_view, - pull_into_descriptor, - )?; - - if let PullIntoDescriptorReaderType::Default = reader_type { - // If pullIntoDescriptor’s reader type is "default", - objects.with_assert_default_reader(|objects| { - // Perform ! ReadableStreamFulfillReadRequest(stream, filledView, done). - ReadableStream::readable_stream_fulfill_read_request( - &ctx, - objects, - filled_view.into_js(&ctx)?, - done, - ) - }) - } else { - // Otherwise, - objects.with_assert_byob_reader(|objects| { - // Perform ! ReadableStreamFulfillReadIntoRequest(stream, filledView, done). - ReadableStream::readable_stream_fulfill_read_into_request( - &ctx, - objects, - filled_view, - done, - ) - }) - } - } - - fn readable_byte_stream_controller_handle_queue_drain>( - ctx: Ctx<'js>, - mut objects: ReadableByteStreamObjects<'js, R>, - ) -> Result> { - // If controller.[[queueTotalSize]] is 0 and controller.[[closeRequested]] is true, - if objects.controller.queue_total_size == 0 && objects.controller.close_requested { - // Perform ! ReadableByteStreamControllerClearAlgorithms(controller). - objects - .controller - .readable_byte_stream_controller_clear_algorithms(); - // Perform ! ReadableStreamClose(controller.[[stream]]). - ReadableStream::readable_stream_close(ctx, objects) - } else { - // Otherwise, - // Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). - Self::readable_byte_stream_controller_call_pull_if_needed(ctx.clone(), objects) - } - } - - fn readable_byte_stream_controller_convert_pull_into_descriptor( - ctx: Ctx<'js>, - function_array_buffer_is_view: &Function<'js>, - pull_into_descriptor: PullIntoDescriptor<'js>, - ) -> Result> { - let PullIntoDescriptor { - // Let bytesFilled be pullIntoDescriptor’s bytes filled. - bytes_filled, - // Let elementSize be pullIntoDescriptor’s element size. - element_size, - byte_offset, - buffer, - .. - } = pull_into_descriptor; - // Let buffer be ! TransferArrayBuffer(pullIntoDescriptor’s buffer). - let buffer = transfer_array_buffer(buffer); - // Return ! Construct(pullIntoDescriptor’s view constructor, « buffer, pullIntoDescriptor’s byte offset, bytesFilled ÷ elementSize »). - let view: Object = pull_into_descriptor.view_constructor.construct(( - buffer, - byte_offset, - bytes_filled / element_size, - ))?; - ViewBytes::from_object(&ctx, function_array_buffer_is_view, &view) - } - - pub(super) fn readable_byte_stream_controller_pull_into( - ctx: &Ctx<'js>, - // Let stream be controller.[[stream]]. - mut objects: ReadableStreamBYOBObjects<'js>, - view: ViewBytes<'js>, - min: u64, - read_into_request: impl ReadableStreamReadIntoRequest<'js> + 'js, - ) -> Result> { - // Set elementSize to the element size specified in the typed array constructors table for view.[[TypedArrayName]]. - // Set ctor to the constructor specified in the typed array constructors table for view.[[TypedArrayName]]. - let (element_size, ctor) = ( - view.element_size(), - objects - .controller - .array_constructor_primordials - .for_view_bytes(&view), - ); - - // Let minimumFill be min × elementSize. - let minimum_fill: usize = (min as usize) * element_size; - - // Let byteOffset be view.[[ByteOffset]]. - // Let byteLength be view.[[ByteLength]]. - let (buffer, byte_length, byte_offset) = view.get_array_buffer()?; - - // Let bufferResult be TransferArrayBuffer(view.[[ViewedArrayBuffer]]). - let buffer_result = transfer_array_buffer(buffer); - let buffer = match buffer_result { - // If bufferResult is an abrupt completion, - Err(Error::Exception) => { - // Perform readIntoRequest’s error steps, given bufferResult.[[Value]]. - objects = read_into_request.error_steps(objects, ctx.catch())?; - // Return. - return Ok(objects); - } - Err(err) => return Err(err), - // Let buffer be bufferResult.[[Value]]. - Ok(buffer) => buffer, - }; - - let buffer_byte_length = buffer.len(); - // Let pullIntoDescriptor be a new pull-into descriptor with - let mut pull_into_descriptor = PullIntoDescriptor { - buffer, - buffer_byte_length, - byte_offset, - byte_length, - bytes_filled: 0, - minimum_fill, - element_size, - view_constructor: ctor.clone(), - reader_type: PullIntoDescriptorReaderType::Byob, - }; - - // If controller.[[pendingPullIntos]] is not empty, - if !objects.controller.pending_pull_intos.is_empty() { - // Append pullIntoDescriptor to controller.[[pendingPullIntos]]. - objects - .controller - .pending_pull_intos - .push_back(pull_into_descriptor); - - // Perform ! ReadableStreamAddReadIntoRequest(stream, readIntoRequest). - ReadableStream::readable_stream_add_read_into_request( - &mut objects.reader, - read_into_request, - ); - - // Return. - return Ok(objects); - } - - // If stream.[[state]] is "closed", - if matches!(objects.stream.state, ReadableStreamState::Closed) { - // Let emptyView be ! Construct(ctor, « pullIntoDescriptor’s buffer, pullIntoDescriptor’s byte offset, 0 »). - let empty_view: Value<'js> = ctor.construct(( - pull_into_descriptor.buffer, - pull_into_descriptor.byte_offset, - 0, - ))?; - - // Perform readIntoRequest’s close steps, given emptyView. - objects = read_into_request.close_steps(objects, empty_view)?; - - // Return. - return Ok(objects); - } - - // If controller.[[queueTotalSize]] > 0, - if objects.controller.queue_total_size > 0 { - // If ! ReadableByteStreamControllerFillPullIntoDescriptorFromQueue(controller, pullIntoDescriptor) is true, - if objects - .controller - .readable_byte_stream_controller_fill_pull_into_descriptor_from_queue( - ctx, - &mut PullIntoDescriptorRefMut::Owned(&mut pull_into_descriptor), - )? - { - // Let filledView be ! ReadableByteStreamControllerConvertPullIntoDescriptor(pullIntoDescriptor). - let filled_view = objects - .controller - .readable_byte_steam_controller_convert_pull_into_descriptor( - pull_into_descriptor, - )?; - - // Perform ! ReadableByteStreamControllerHandleQueueDrain(controller). - objects = - Self::readable_byte_stream_controller_handle_queue_drain(ctx.clone(), objects)?; - - // Perform readIntoRequest’s chunk steps, given filledView. - // Return. - return read_into_request.chunk_steps(objects, filled_view); - } - - // If controller.[[closeRequested]] is true, - if objects.controller.close_requested { - // Let e be a TypeError exception. - let e: Value = objects - .stream - .constructor_type_error - .call(("Insufficient bytes to fill elements in the given buffer",))?; - - // Perform ! ReadableByteStreamControllerError(controller, e). - objects = Self::readable_byte_stream_controller_error(objects, e.clone())?; - - // Perform readIntoRequest’s error steps, given e. - // Return. - return read_into_request.error_steps(objects, e); - } - } - - // Append pullIntoDescriptor to controller.[[pendingPullIntos]]. - objects - .controller - .pending_pull_intos - .push_back(pull_into_descriptor); - - // Perform ! ReadableStreamAddReadIntoRequest(stream, readIntoRequest). - ReadableStream::readable_stream_add_read_into_request( - &mut objects.reader, - read_into_request, - ); - - // Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). - Self::readable_byte_stream_controller_call_pull_if_needed(ctx.clone(), objects) - } - - fn readable_byte_steam_controller_convert_pull_into_descriptor( - &mut self, - pull_into_descriptor: PullIntoDescriptor<'js>, - ) -> Result> { - // Let bytesFilled be pullIntoDescriptor’s bytes filled. - let bytes_filled = pull_into_descriptor.bytes_filled; - - // Let elementSize be pullIntoDescriptor’s element size. - let element_size = pull_into_descriptor.element_size; - - // Let buffer be ! TransferArrayBuffer(pullIntoDescriptor’s buffer). - let buffer = transfer_array_buffer(pull_into_descriptor.buffer)?; - - // Return ! Construct(pullIntoDescriptor’s view constructor, « buffer, pullIntoDescriptor’s byte offset, bytesFilled ÷ elementSize »). - pull_into_descriptor.view_constructor.construct(( - buffer, - pull_into_descriptor.byte_offset, - bytes_filled / element_size, - )) - } - - pub(super) fn readable_byte_stream_controller_respond>( - ctx: Ctx<'js>, - mut objects: ReadableByteStreamObjects<'js, R>, - bytes_written: usize, - ) -> Result<()> { - // Let firstDescriptor be controller.[[pendingPullIntos]][0]. - let first_descriptor = &mut objects.controller.pending_pull_intos[0]; - - // Let state be controller.[[stream]].[[state]]. - match objects.stream.state { - // If state is "closed", - ReadableStreamState::Closed => { - // If bytesWritten is not 0, throw a TypeError exception. - if bytes_written != 0 { - return Err(Exception::throw_type( - &ctx, - "bytesWritten must be 0 when calling respond() on a closed stream", - )); - } - } - // Otherwise, - _ => { - // If bytesWritten is 0, throw a TypeError exception. - if bytes_written == 0 { - return Err(Exception::throw_type( - &ctx, - "bytesWritten must be greater than 0 when calling respond() on a readable stream", - )); - } - - // If firstDescriptor’s bytes filled + bytesWritten > firstDescriptor’s byte length, throw a RangeError exception. - if first_descriptor.bytes_filled + bytes_written > first_descriptor.byte_length { - return Err(Exception::throw_range(&ctx, "bytesWritten out of range'")); - } - } - }; - - // Set firstDescriptor’s buffer to ! TransferArrayBuffer(firstDescriptor’s buffer). - first_descriptor.buffer = transfer_array_buffer(first_descriptor.buffer.clone())?; - - // Perform ? ReadableByteStreamControllerRespondInternal(controller, bytesWritten). - Self::readable_byte_stream_controller_respond_internal(ctx, objects, bytes_written) - } - - fn readable_byte_stream_controller_respond_internal>( - ctx: Ctx<'js>, - mut objects: ReadableByteStreamObjects<'js, R>, - bytes_written: usize, - ) -> Result<()> { - // Let firstDescriptor be controller.[[pendingPullIntos]][0]. - let first_descriptor_index = 0; - - // Perform ! ReadableByteStreamControllerInvalidateBYOBRequest(controller). - objects - .controller - .readable_byte_stream_controller_invalidate_byob_request(); - - // Let state be controller.[[stream]].[[state]]. - match objects.stream.state { - // If state is "closed", - ReadableStreamState::Closed => { - // Perform ! ReadableByteStreamControllerRespondInClosedState(controller, firstDescriptor). - objects = Self::readable_byte_stream_controller_respond_in_closed_state( - ctx.clone(), - objects, - first_descriptor_index, - )?; - } - // Otherwise - _ => { - // Perform ? ReadableByteStreamControllerRespondInReadableState(controller, bytesWritten, firstDescriptor). - objects = Self::readable_byte_stream_controller_respond_in_readable_state( - ctx.clone(), - objects, - bytes_written, - first_descriptor_index, - )? - } - }; - - _ = Self::readable_byte_stream_controller_call_pull_if_needed(ctx, objects)?; - Ok(()) - } - - fn readable_byte_stream_controller_respond_in_closed_state>( - ctx: Ctx<'js>, - // Let stream be controller.[[stream]]. - mut objects: ReadableByteStreamObjects<'js, R>, - first_descriptor_index: usize, - ) -> Result> { - // If firstDescriptor’s reader type is "none", perform ! ReadableByteStreamControllerShiftPendingPullInto(controller). - if let PullIntoDescriptorReaderType::None = - objects.controller.pending_pull_intos[first_descriptor_index].reader_type - { - objects - .controller - .readable_byte_stream_controller_shift_pending_pull_into(); - } - - // If ! ReadableStreamHasBYOBReader(stream) is true, - objects.with_reader( - Ok, - |mut objects| { - // While ! ReadableStreamGetNumReadIntoRequests(stream) > 0, - while ReadableStream::readable_stream_get_num_read_into_requests(&objects.reader) - > 0 - { - // Let pullIntoDescriptor be ! ReadableByteStreamControllerShiftPendingPullInto(controller). - let pull_into_descriptor = objects - .controller - .readable_byte_stream_controller_shift_pending_pull_into(); - - // Perform ! ReadableByteStreamControllerCommitPullIntoDescriptor(stream, pullIntoDescriptor). - objects = Self::readable_byte_stream_controller_commit_pull_into_descriptor( - ctx.clone(), - objects, - pull_into_descriptor, - )?; - } - - Ok(objects) - }, - Ok, - ) - } - - fn readable_byte_stream_controller_respond_in_readable_state>( - ctx: Ctx<'js>, - // Let stream be controller.[[stream]]. - mut objects: ReadableByteStreamObjects<'js, R>, - bytes_written: usize, - pull_into_descriptor_index: usize, - ) -> Result> { - // Perform ! ReadableByteStreamControllerFillHeadPullIntoDescriptor(controller, bytesWritten, pullIntoDescriptor). - objects - .controller - .readable_byte_stream_controller_fill_head_pull_into_descriptor( - bytes_written, - &mut PullIntoDescriptorRefMut::Index(pull_into_descriptor_index), - ); - - // If pullIntoDescriptor’s reader type is "none", - if let PullIntoDescriptorReaderType::None = - objects.controller.pending_pull_intos[pull_into_descriptor_index].reader_type - { - // Perform ? ReadableByteStreamControllerEnqueueDetachedPullIntoToQueue(controller, pullIntoDescriptor). - objects = Self::readable_byte_stream_enqueue_detached_pull_into_to_queue( - ctx.clone(), - objects, - pull_into_descriptor_index, - )?; - // Perform ! ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller). - // Return. - return Self::readable_byte_stream_controller_process_pull_into_descriptors_using_queue( - &ctx, objects, - ); - } - - // If pullIntoDescriptor’s bytes filled < pullIntoDescriptor’s minimum fill, return. - if objects.controller.pending_pull_intos[pull_into_descriptor_index].bytes_filled - < objects.controller.pending_pull_intos[pull_into_descriptor_index].minimum_fill - { - return Ok(objects); - } - - // Perform ! ReadableByteStreamControllerShiftPendingPullInto(controller). - let mut pull_into_descriptor = objects - .controller - .readable_byte_stream_controller_shift_pending_pull_into(); - - // Let remainderSize be the remainder after dividing pullIntoDescriptor’s bytes filled by pullIntoDescriptor’s element size. - let remainder_size = pull_into_descriptor.bytes_filled % pull_into_descriptor.element_size; - - // If remainderSize > 0, - if remainder_size > 0 { - // Let end be pullIntoDescriptor’s byte offset + pullIntoDescriptor’s bytes filled. - let end = pull_into_descriptor.byte_offset + pull_into_descriptor.bytes_filled; - - let buffer = pull_into_descriptor.buffer.clone(); - - // Perform ? ReadableByteStreamControllerEnqueueClonedChunkToQueue(controller, pullIntoDescriptor’s buffer, end − remainderSize, remainderSize). - objects = Self::readable_byte_stream_controller_enqueue_cloned_chunk_to_queue( - ctx.clone(), - objects, - &buffer, - end - remainder_size, - remainder_size, - )?; - } - - // Set pullIntoDescriptor’s bytes filled to pullIntoDescriptor’s bytes filled − remainderSize. - pull_into_descriptor.bytes_filled -= remainder_size; - - // Perform ! ReadableByteStreamControllerCommitPullIntoDescriptor(controller.[[stream]], pullIntoDescriptor). - objects = Self::readable_byte_stream_controller_commit_pull_into_descriptor( - ctx.clone(), - objects, - pull_into_descriptor, - )?; - - // Perform ! ReadableByteStreamControllerProcessPullIntoDescriptorsUsingQueue(controller). - Self::readable_byte_stream_controller_process_pull_into_descriptors_using_queue( - &ctx, objects, - ) - } - - pub(super) fn readable_byte_stream_controller_respond_with_new_view< - R: ReadableStreamReader<'js>, - >( - ctx: Ctx<'js>, - mut objects: ReadableByteStreamObjects<'js, R>, - view: ViewBytes<'js>, - ) -> Result<()> { - // Let firstDescriptor be controller.[[pendingPullIntos]][0]. - let first_descriptor_index = 0; - - let (buffer, byte_length, byte_offset) = view.get_array_buffer()?; - - // Let state be controller.[[stream]].[[state]]. - match objects.stream.state { - // If state is "closed", - ReadableStreamState::Closed => { - // If view.[[ByteLength]] is not 0, throw a TypeError exception. - if byte_length != 0 { - return Err(Exception::throw_type(&ctx, "The view's length must be 0 when calling respondWithNewView() on a closed stream")); - } - } - // Otherwise - _ => { - // If view.[[ByteLength]] is 0, throw a TypeError exception. - if byte_length == 0 { - return Err(Exception::throw_type(&ctx, "The view's length must be greater than 0 when calling respondWithNewView() on a readable stream")); - } - } - }; - - { - let first_descriptor = - &mut objects.controller.pending_pull_intos[first_descriptor_index]; - - // If firstDescriptor’s byte offset + firstDescriptor’ bytes filled is not view.[[ByteOffset]], throw a RangeError exception. - if first_descriptor.byte_offset + first_descriptor.bytes_filled != byte_offset { - return Err(Exception::throw_range( - &ctx, - "The region specified by view does not match byobRequest", - )); - }; - - // If firstDescriptor’s buffer byte length is not view.[[ViewedArrayBuffer]].[[ByteLength]], throw a RangeError exception. - if first_descriptor.buffer_byte_length != buffer.len() { - return Err(Exception::throw_range( - &ctx, - "The buffer of view has different capacity than byobRequest", - )); - }; - - // If firstDescriptor’s bytes filled + view.[[ByteLength]] > firstDescriptor’s byte length, throw a RangeError exception. - if first_descriptor.bytes_filled + byte_length > first_descriptor.byte_length { - return Err(Exception::throw_range( - &ctx, - "The region specified by view is larger than byobRequest", - )); - } - - // Set firstDescriptor’s buffer to ? TransferArrayBuffer(view.[[ViewedArrayBuffer]]). - first_descriptor.buffer = transfer_array_buffer(buffer)?; - } - - // Perform ? ReadableByteStreamControllerRespondInternal(controller, viewByteLength). - Self::readable_byte_stream_controller_respond_internal(ctx, objects, byte_length) - } - - fn readable_byte_stream_controller_fill_head_pull_into_descriptor<'a>( - &mut self, - size: usize, - pull_into_descriptor_ref: &mut PullIntoDescriptorRefMut<'js, 'a>, - ) { - let pull_into_descriptor = match pull_into_descriptor_ref { - PullIntoDescriptorRefMut::Index(i) => &mut self.pending_pull_intos[*i], - PullIntoDescriptorRefMut::Owned(r) => *r, - }; - - // Set pullIntoDescriptor’s bytes filled to bytes filled + size. - pull_into_descriptor.bytes_filled += size; - } - - fn start_algorithm>( - ctx: Ctx<'js>, - objects: ReadableByteStreamObjects<'js, R>, - start_algorithm: StartAlgorithm<'js>, - ) -> Result<( - Value<'js>, - ReadableStreamClassObjects<'js, OwnedBorrowMut<'js, Self>, R>, - )> { - let objects_class = objects.into_inner(); - - Ok(( - start_algorithm.call( - ctx, - ReadableStreamControllerClass::ReadableStreamByteController( - objects_class.controller.clone(), - ), - )?, - objects_class, - )) - } - - fn pull_algorithm>( - ctx: Ctx<'js>, - objects: ReadableByteStreamObjects<'js, R>, - ) -> Result<( - Promise<'js>, - ReadableStreamClassObjects<'js, OwnedBorrowMut<'js, Self>, R>, - )> { - let pull_algorithm = objects - .controller - .pull_algorithm - .clone() - .expect("pull algorithm used after ReadableStreamDefaultControllerClearAlgorithms"); - let promise_primordials = objects.stream.promise_primordials.clone(); - let objects_class = objects.into_inner(); - - Ok(( - pull_algorithm.call( - ctx, - &promise_primordials, - ReadableStreamControllerClass::ReadableStreamByteController( - objects_class.controller.clone(), - ), - )?, - objects_class, - )) - } - - fn cancel_algorithm>( - ctx: Ctx<'js>, - objects: ReadableByteStreamObjects<'js, R>, - reason: Value<'js>, - ) -> Result<( - Promise<'js>, - ReadableStreamClassObjects<'js, OwnedBorrowMut<'js, Self>, R>, - )> { - let cancel_algorithm = - objects.controller.cancel_algorithm.clone().expect( - "cancel algorithm used after ReadableStreamDefaultControllerClearAlgorithms", - ); - let promise_primordials = objects.stream.promise_primordials.clone(); - let objects_class = objects.into_inner(); - - Ok(( - cancel_algorithm.call(ctx, &promise_primordials, reason)?, - objects_class, - )) - } -} - -#[methods(rename_all = "camelCase")] -impl<'js> ReadableByteStreamController<'js> { - #[qjs(constructor)] - fn new(ctx: Ctx<'js>) -> Result> { - Err(Exception::throw_type(&ctx, "Illegal constructor")) - } - - // readonly attribute ReadableStreamBYOBRequest? byobRequest; - #[qjs(get, rename = "byobRequest")] - fn byob_request_getter( - ctx: Ctx<'js>, - controller: This>, - ) -> Result>>> { - // Use `try_borrow_mut` so that reentrant access during enqueue - // (e.g. via a patched `Object.prototype.then` getter, WPT - // `readable-byte-streams/patched-global`) doesn't hard-error with - // "can't borrow" when the outer enqueue already holds the mut - // borrow. If the controller IS currently borrowed, we can still - // answer correctly by reading the state via an immutable try_borrow; - // materialization is only needed when state is consistent. - if let Ok(owned) = rquickjs::class::OwnedBorrowMut::try_from_class(controller.0.clone()) { - let (request, _) = Self::readable_byte_stream_controller_get_byob_request(ctx, owned)?; - return Ok(request); - } - // Reentrant access mid-enqueue: can't acquire immutable borrow - // either (because enqueue holds mut). Return null — the spec's - // observable state during this transient window is that the - // byob request has been invalidated (the enqueue path clears it - // as pull-into descriptors are filled). - Ok(Null(None)) - } - - // readonly attribute unrestricted double? desiredSize; - #[qjs(get)] - fn desired_size(&self) -> Null { - let stream = OwnedBorrow::from_class(self.stream.clone()); - self.readable_byte_stream_controller_get_desired_size(&stream) - } - - // undefined close(); - fn close(ctx: Ctx<'js>, controller: This>) -> Result<()> { - // If this.[[closeRequested]] is true, throw a TypeError exception. - if controller.close_requested { - return Err(Exception::throw_type(&ctx, "close() called more than once")); - } - - let objects = ReadableStreamObjects::from_byte_controller(controller.0).refresh_reader(); - - if !matches!(objects.stream.state, ReadableStreamState::Readable) { - return Err(Exception::throw_type( - &ctx, - "close() called when stream is not readable", - )); - }; - - // Perform ? ReadableByteStreamControllerClose(this). - Self::readable_byte_stream_controller_close(ctx, objects)?; - Ok(()) - } - - // undefined enqueue(ArrayBufferView chunk); - fn enqueue( - this: This>, - ctx: Ctx<'js>, - chunk: Value<'js>, - ) -> Result<()> { - let chunk = ViewBytes::from_value(&ctx, &this.function_array_buffer_is_view, Some(&chunk))?; - - let (array_buffer, byte_length, _) = chunk.get_array_buffer()?; - - // If chunk.[[ByteLength]] is 0, throw a TypeError exception. - if byte_length == 0 { - return Err(Exception::throw_type( - &ctx, - "chunk must have non-zero byteLength", - )); - } - - // If chunk.[[ViewedArrayBuffer]].[[ArrayBufferByteLength]] is 0, throw a TypeError exception. - if array_buffer.is_empty() { - return Err(Exception::throw_type( - &ctx, - "chunk must have non-zero buffer byteLength", - )); - } - - // If this.[[closeRequested]] is true, throw a TypeError exception. - if this.close_requested { - return Err(Exception::throw_type(&ctx, "stream is closed or draining")); - } - - let objects = ReadableStreamObjects::from_byte_controller(this.0).refresh_reader(); - - // If this.[[stream]].[[state]] is not "readable", throw a TypeError exception. - if !matches!(objects.stream.state, ReadableStreamState::Readable) { - return Err(Exception::throw_type( - &ctx, - "The stream is not in the readable state and cannot be enqueued to", - )); - }; - - // Return ? ReadableByteStreamControllerEnqueue(this, chunk). - Self::readable_byte_stream_controller_enqueue(&ctx, objects, chunk)?; - Ok(()) - } - - // undefined error(optional any e); - fn error( - ctx: Ctx<'js>, - controller: This>, - e: Opt>, - ) -> Result<()> { - let objects = ReadableStreamObjects::from_byte_controller(controller.0).refresh_reader(); - - // Perform ! ReadableByteStreamControllerError(this, e). - Self::readable_byte_stream_controller_error(objects, e.0.unwrap_or_undefined(&ctx))?; - Ok(()) - } -} - -impl<'js> ReadableStreamController<'js> for ReadableByteStreamControllerOwned<'js> { - type Class = ReadableByteStreamControllerClass<'js>; - - fn with_controller( - self, - ctx: C, - _: impl FnOnce( - C, - ReadableStreamDefaultControllerOwned<'js>, - ) -> Result<(O, ReadableStreamDefaultControllerOwned<'js>)>, - byte: impl FnOnce( - C, - ReadableByteStreamControllerOwned<'js>, - ) -> Result<(O, ReadableByteStreamControllerOwned<'js>)>, - ) -> Result<(O, Self)> { - let (ctx, reader) = byte(ctx, self)?; - Ok((ctx, reader)) - } - - fn into_inner(self) -> Self::Class { - OwnedBorrowMut::into_inner(self) - } - - fn from_class(class: Self::Class) -> Self { - OwnedBorrowMut::from_class(class) - } - - fn into_erased(self) -> ReadableStreamControllerOwned<'js> { - ReadableStreamControllerOwned::ReadableStreamByteController(self) - } - - fn try_from_erased(erased: ReadableStreamControllerOwned<'js>) -> Option { - match erased { - ReadableStreamControllerOwned::ReadableStreamDefaultController(_) => None, - ReadableStreamControllerOwned::ReadableStreamByteController(r) => Some(r), - } - } - - fn pull_steps( - ctx: &Ctx<'js>, - mut objects: ReadableStreamDefaultReaderObjects<'js, Self>, - read_request: impl ReadableStreamReadRequest<'js> + 'js, - ) -> Result> { - // If this.[[queueTotalSize]] > 0, - if objects.controller.queue_total_size > 0 { - // Perform ! ReadableByteStreamControllerFillReadRequestFromQueue(this, readRequest). - // Return. - return ReadableByteStreamController::readable_byte_stream_controller_fill_read_request_from_queue( - ctx, - objects, - read_request, - ); - } - - // Let autoAllocateChunkSize be this.[[autoAllocateChunkSize]]. - let auto_allocate_chunk_size = objects.controller.auto_allocate_chunk_size; - - // If autoAllocateChunkSize is not undefined, - if let Some(auto_allocate_chunk_size) = auto_allocate_chunk_size { - // Let buffer be Construct(%ArrayBuffer%, « autoAllocateChunkSize »). - let buffer: ArrayBuffer = match objects - .controller - .constructor_array_buffer - .construct((auto_allocate_chunk_size,)) - { - // If buffer is an abrupt completion, - Err(Error::Exception) => { - // Perform readRequest’s error steps, given buffer.[[Value]]. - return read_request.error_steps_typed(objects, ctx.catch()); - } - Err(err) => return Err(err), - Ok(buffer) => buffer, - }; - - // Let pullIntoDescriptor be a new pull-into descriptor with... - let pull_into_descriptor = PullIntoDescriptor { - buffer, - buffer_byte_length: auto_allocate_chunk_size, - byte_offset: 0, - byte_length: auto_allocate_chunk_size, - bytes_filled: 0, - minimum_fill: 1, - element_size: 1, - view_constructor: objects - .controller - .array_constructor_primordials - .constructor_uint8array - .clone(), - reader_type: PullIntoDescriptorReaderType::Default, - }; - - // Append pullIntoDescriptor to this.[[pendingPullIntos]]. - objects - .controller - .pending_pull_intos - .push_back(pull_into_descriptor); - } - - // Perform ! ReadableStreamAddReadRequest(stream, readRequest). - objects - .stream - .readable_stream_add_read_request(&mut objects.reader, read_request); - - // Perform ! ReadableByteStreamControllerCallPullIfNeeded(this). - ReadableByteStreamController::readable_byte_stream_controller_call_pull_if_needed( - ctx.clone(), - objects, - ) - } - - fn cancel_steps>( - ctx: &Ctx<'js>, - mut objects: ReadableStreamObjects<'js, Self, R>, - reason: Value<'js>, - ) -> Result<(Promise<'js>, ReadableStreamObjects<'js, Self, R>)> { - // Perform ! ReadableByteStreamControllerClearPendingPullIntos(this). - objects - .controller - .readable_byte_stream_controller_clear_pending_pull_intos(); - - // Perform ! ResetQueue(this). - objects.controller.reset_queue(); - - // Let result be the result of performing this.[[cancelAlgorithm]], passing in reason. - let (result, objects_class) = - ReadableByteStreamController::cancel_algorithm(ctx.clone(), objects, reason)?; - - objects = ReadableStreamObjects::from_class(objects_class); - - // Perform ! ReadableByteStreamControllerClearAlgorithms(this). - objects - .controller - .readable_byte_stream_controller_clear_algorithms(); - - // Return result. - Ok((result, objects)) - } - - fn release_steps(&mut self) { - // If this.[[pendingPullIntos]] is not empty, - if !self.pending_pull_intos.is_empty() { - // Let firstPendingPullInto be this.[[pendingPullIntos]][0]. - let first_pending_pull_into = &mut self.pending_pull_intos[0]; - - // Set firstPendingPullInto’s reader type to "none". - first_pending_pull_into.reader_type = PullIntoDescriptorReaderType::None; - - // Set this.[[pendingPullIntos]] to the list « firstPendingPullInto ». - _ = self.pending_pull_intos.split_off(1); - } - } -} - -#[derive(JsLifetime, Trace, Clone)] -#[rquickjs::class] -pub(crate) struct ReadableStreamBYOBRequest<'js> { - pub(super) view: Option>, - controller: Option>, -} - -#[methods(rename_all = "camelCase")] -impl<'js> ReadableStreamBYOBRequest<'js> { - #[qjs(constructor)] - fn new(ctx: Ctx<'js>) -> Result> { - Err(Exception::throw_type(&ctx, "Illegal constructor")) - } - - #[qjs(get)] - fn view(&self) -> Null> { - Null(self.view.clone()) - } - - fn respond( - ctx: Ctx<'js>, - byob_request: This>, - bytes_written: usize, - ) -> Result<()> { - // If this.[[controller]] is undefined, throw a TypeError exception. - let (controller, view) = match (&byob_request.controller, &byob_request.view) { - (Some(controller), Some(view)) => (controller.clone(), view), - _ => { - return Err(Exception::throw_type( - &ctx, - "This BYOB request has been invalidated", - )); - } - }; - let (buffer, _, _) = view.get_array_buffer()?; - drop(byob_request); - - // If ! IsDetachedBuffer(this.[[view]].[[ArrayBuffer]]) is true, throw a TypeError exception. - if buffer.as_bytes().is_none() { - return Err(Exception::throw_type( - &ctx, - "The BYOB request's buffer has been detached and so cannot be used as a response", - )); - } - - let objects = - ReadableStreamObjects::from_byte_controller(OwnedBorrowMut::from_class(controller)) - .refresh_reader(); - - // Perform ? ReadableByteStreamControllerRespond(this.[[controller]], bytesWritten). - ReadableByteStreamController::readable_byte_stream_controller_respond( - ctx, - objects, - bytes_written, - ) - } - - fn respond_with_new_view( - ctx: Ctx<'js>, - byob_request: This>, - view: Opt>, - ) -> Result<()> { - // If this.[[controller]] is undefined, throw a TypeError exception. - let controller = match &byob_request.controller { - Some(controller) => controller.clone(), - _ => { - return Err(Exception::throw_type( - &ctx, - "This BYOB request has been invalidated", - )); - } - }; - drop(byob_request); - - let controller = OwnedBorrowMut::from_class(controller); - - let view = ViewBytes::from_value( - &ctx, - &controller.function_array_buffer_is_view, - view.0.as_ref(), - )?; - - let (buffer, _, _) = view.get_array_buffer()?; - - // If ! IsDetachedBuffer(view.[[ViewedArrayBuffer]]) is true, throw a TypeError exception. - if buffer.as_bytes().is_none() { - return Err(Exception::throw_type( - &ctx, - "The given view's buffer has been detached and so cannot be used as a response", - )); - } - - let objects = ReadableStreamObjects::from_byte_controller(controller).refresh_reader(); - - // Return ? ReadableByteStreamControllerRespondWithNewView(this.[[controller]], view). - ReadableByteStreamController::readable_byte_stream_controller_respond_with_new_view( - ctx, objects, view, - ) - } -} - -#[derive(JsLifetime)] -pub(super) struct PullIntoDescriptor<'js> { - buffer: ArrayBuffer<'js>, - buffer_byte_length: usize, - byte_offset: usize, - byte_length: usize, - bytes_filled: usize, - minimum_fill: usize, - element_size: usize, - view_constructor: Constructor<'js>, - reader_type: PullIntoDescriptorReaderType, -} - -impl<'js> Trace<'js> for PullIntoDescriptor<'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - self.buffer.trace(tracer); - self.buffer_byte_length.trace(tracer); - self.byte_offset.trace(tracer); - self.byte_length.trace(tracer); - self.bytes_filled.trace(tracer); - self.minimum_fill.trace(tracer); - self.element_size.trace(tracer); - self.view_constructor.trace(tracer); - self.reader_type.trace(tracer); - } -} - -enum PullIntoDescriptorRefMut<'js, 'a> { - Index(usize), - Owned(&'a mut PullIntoDescriptor<'js>), -} - -#[derive(Trace, Clone, Copy)] -enum PullIntoDescriptorReaderType { - Default, - Byob, - None, -} - -#[derive(JsLifetime)] -struct ReadableByteStreamQueueEntry<'js> { - buffer: ArrayBuffer<'js>, - byte_offset: usize, - byte_length: usize, -} - -impl<'js> Trace<'js> for ReadableByteStreamQueueEntry<'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - self.buffer.trace(tracer); - self.byte_offset.trace(tracer); - self.byte_length.trace(tracer) - } -} - -fn transfer_array_buffer(buffer: ArrayBuffer<'_>) -> Result> { - buffer.get::<_, Function>("transfer")?.call((This(buffer),)) -} - -fn copy_data_block_bytes( - ctx: &Ctx<'_>, - to_block: &ArrayBuffer, - to_index: usize, - from_block: &ArrayBuffer, - from_index: usize, - count: usize, -) -> Result<()> { - let to_raw = to_block - .as_raw() - .ok_or(ERROR_MSG_ARRAY_BUFFER_DETACHED) - .or_throw(ctx)?; - let to_slice = unsafe { std::slice::from_raw_parts_mut(to_raw.ptr.as_ptr(), to_raw.len) }; - let from_raw = from_block - .as_raw() - .ok_or(ERROR_MSG_ARRAY_BUFFER_DETACHED) - .or_throw(ctx)?; - let from_slice = unsafe { std::slice::from_raw_parts(from_raw.ptr.as_ptr(), from_raw.len) }; - - to_slice[to_index..to_index + count] - .copy_from_slice(&from_slice[from_index..from_index + count]); - Ok(()) -} - -/// Public API for enqueuing a `Uint8Array` (built from the caller-supplied -/// `ArrayBuffer`) into a byte stream controller from Rust code. Used by -/// byte-source streams created via `ReadableStream::from_byte_pull_algorithm`. -pub fn readable_byte_stream_controller_enqueue_bytes<'js>( - ctx: Ctx<'js>, - controller: ReadableByteStreamControllerClass<'js>, - buffer: ArrayBuffer<'js>, -) -> Result<()> { - readable_byte_stream_controller_enqueue_bytes_inner(ctx, controller, buffer, false) -} - -/// Zero-copy variant of [`readable_byte_stream_controller_enqueue_bytes`]: -/// the incoming `ArrayBuffer` is NOT transferred/detached before being -/// queued. The producer keeps the buffer alive through the stream's -/// `'js` queue entry, so consumers get a `Uint8Array` that views directly -/// into the producer's storage. -/// -/// Only call this when the caller can guarantee the backing allocation -/// won't be mutated out from under readers (e.g. `Blob.stream()`, where -/// the blob's `ArrayBuffer` is never written after construction). For the -/// normal spec-compliant flow that detaches the source, use -/// [`readable_byte_stream_controller_enqueue_bytes`]. -pub fn readable_byte_stream_controller_enqueue_bytes_borrowed<'js>( - ctx: Ctx<'js>, - controller: ReadableByteStreamControllerClass<'js>, - buffer: ArrayBuffer<'js>, -) -> Result<()> { - readable_byte_stream_controller_enqueue_bytes_inner(ctx, controller, buffer, true) -} - -fn readable_byte_stream_controller_enqueue_bytes_inner<'js>( - ctx: Ctx<'js>, - controller: ReadableByteStreamControllerClass<'js>, - buffer: ArrayBuffer<'js>, - skip_transfer: bool, -) -> Result<()> { - let byte_length = buffer.len(); - if byte_length == 0 { - return Ok(()); - } - let view = rquickjs::TypedArray::::from_arraybuffer(buffer)?; - let borrow = OwnedBorrowMut::from_class(controller); - let chunk = ViewBytes::from_value( - &ctx, - &borrow.function_array_buffer_is_view, - Some(&view.into_value()), - )?; - let objects = ReadableStreamObjects::from_byte_controller(borrow).refresh_reader(); - if skip_transfer { - ReadableByteStreamController::readable_byte_stream_controller_enqueue_borrowed( - &ctx, objects, chunk, - )?; - } else { - ReadableByteStreamController::readable_byte_stream_controller_enqueue( - &ctx, objects, chunk, - )?; - } - Ok(()) -} - -/// Public API for closing a byte stream controller from Rust code. -pub fn readable_byte_stream_controller_close_stream<'js>( - ctx: Ctx<'js>, - controller: ReadableByteStreamControllerClass<'js>, -) -> Result<()> { - let borrow = OwnedBorrowMut::from_class(controller); - let objects = ReadableStreamObjects::from_byte_controller(borrow).refresh_reader(); - ReadableByteStreamController::readable_byte_stream_controller_close(ctx, objects)?; - Ok(()) -} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/controller.rs b/stdlib/src/llrt/llrt_stream_web/readable/controller.rs deleted file mode 100644 index 887ccedd..00000000 --- a/stdlib/src/llrt/llrt_stream_web/readable/controller.rs +++ /dev/null @@ -1,200 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use rquickjs::{ - class::{OwnedBorrowMut, Trace}, - Ctx, IntoJs, JsLifetime, Promise, Result, Value, -}; - -use crate::llrt_stream_web::readable::{ - byte_controller::{ReadableByteStreamControllerClass, ReadableByteStreamControllerOwned}, - default_controller::{ - ReadableStreamDefaultControllerClass, ReadableStreamDefaultControllerOwned, - }, - default_reader::ReadableStreamReadRequest, - objects::{ReadableStreamDefaultReaderObjects, ReadableStreamObjects}, - reader::ReadableStreamReader, -}; - -pub(crate) trait ReadableStreamController<'js>: Sized { - type Class: Clone + Trace<'js>; - - fn with_controller( - self, - ctx: C, - default: impl FnOnce( - C, - ReadableStreamDefaultControllerOwned<'js>, - ) -> Result<(O, ReadableStreamDefaultControllerOwned<'js>)>, - byte: impl FnOnce( - C, - ReadableByteStreamControllerOwned<'js>, - ) -> Result<(O, ReadableByteStreamControllerOwned<'js>)>, - ) -> Result<(O, Self)>; - - fn into_inner(self) -> Self::Class; - fn from_class(class: Self::Class) -> Self; - - fn into_erased(self) -> ReadableStreamControllerOwned<'js>; - fn try_from_erased(erased: ReadableStreamControllerOwned<'js>) -> Option; - - fn pull_steps( - ctx: &Ctx<'js>, - objects: ReadableStreamDefaultReaderObjects<'js, Self>, - read_request: impl ReadableStreamReadRequest<'js> + 'js, - ) -> Result>; - - fn cancel_steps>( - ctx: &Ctx<'js>, - objects: ReadableStreamObjects<'js, Self, R>, - reason: Value<'js>, - ) -> Result<(Promise<'js>, ReadableStreamObjects<'js, Self, R>)>; - - fn release_steps(&mut self); -} - -#[derive(JsLifetime, Trace, Clone)] -pub enum ReadableStreamControllerClass<'js> { - ReadableStreamDefaultController(ReadableStreamDefaultControllerClass<'js>), - ReadableStreamByteController(ReadableByteStreamControllerClass<'js>), - Uninitialised, // Only for use when initialising a Stream - should never be present later on -} - -impl<'js> IntoJs<'js> for ReadableStreamControllerClass<'js> { - fn into_js(self, ctx: &Ctx<'js>) -> Result> { - match self { - Self::ReadableStreamDefaultController(c) => c.into_js(ctx), - Self::ReadableStreamByteController(c) => c.into_js(ctx), - Self::Uninitialised => { - panic!("Tried to convert an uninitialised controller class to JS") - } - } - } -} - -pub(crate) enum ReadableStreamControllerOwned<'js> { - ReadableStreamDefaultController(ReadableStreamDefaultControllerOwned<'js>), - ReadableStreamByteController(ReadableByteStreamControllerOwned<'js>), -} - -impl<'js> ReadableStreamController<'js> for ReadableStreamControllerOwned<'js> { - type Class = ReadableStreamControllerClass<'js>; - - fn with_controller( - self, - ctx: C, - default: impl FnOnce( - C, - ReadableStreamDefaultControllerOwned<'js>, - ) -> Result<(O, ReadableStreamDefaultControllerOwned<'js>)>, - byob: impl FnOnce( - C, - ReadableByteStreamControllerOwned<'js>, - ) -> Result<(O, ReadableByteStreamControllerOwned<'js>)>, - ) -> Result<(O, Self)> { - match self { - ReadableStreamControllerOwned::ReadableStreamDefaultController(r) => { - let (ctx, r) = default(ctx, r)?; - Ok((ctx, Self::ReadableStreamDefaultController(r))) - } - ReadableStreamControllerOwned::ReadableStreamByteController(r) => { - let (ctx, r) = byob(ctx, r)?; - Ok((ctx, Self::ReadableStreamByteController(r))) - } - } - } - - fn into_inner(self) -> Self::Class { - match self { - ReadableStreamControllerOwned::ReadableStreamDefaultController(c) => { - ReadableStreamControllerClass::ReadableStreamDefaultController(c.into_inner()) - } - ReadableStreamControllerOwned::ReadableStreamByteController(c) => { - ReadableStreamControllerClass::ReadableStreamByteController(c.into_inner()) - } - } - } - - fn from_class(class: Self::Class) -> Self { - match class { - ReadableStreamControllerClass::ReadableStreamDefaultController(class) => { - ReadableStreamControllerOwned::ReadableStreamDefaultController( - OwnedBorrowMut::from_class(class), - ) - } - ReadableStreamControllerClass::ReadableStreamByteController(class) => { - ReadableStreamControllerOwned::ReadableStreamByteController( - OwnedBorrowMut::from_class(class), - ) - } - ReadableStreamControllerClass::Uninitialised => { - panic!("Tried to borrow an uninitialised controller class") - } - } - } - - fn into_erased(self) -> ReadableStreamControllerOwned<'js> { - self - } - - fn try_from_erased(erased: ReadableStreamControllerOwned<'js>) -> Option { - Some(erased) - } - - fn pull_steps( - ctx: &Ctx<'js>, - objects: ReadableStreamDefaultReaderObjects<'js, Self>, - read_request: impl ReadableStreamReadRequest<'js> + 'js, - ) -> Result> { - objects - .with_controller( - read_request, - |read_request, objects| { - ReadableStreamDefaultControllerOwned::<'js>::pull_steps( - ctx, - objects, - read_request, - ) - .map(|objects| ((), objects)) - }, - |read_request, objects| { - ReadableByteStreamControllerOwned::<'js>::pull_steps(ctx, objects, read_request) - .map(|objects| ((), objects)) - }, - ) - .map(|((), objects)| objects) - } - - fn cancel_steps>( - ctx: &Ctx<'js>, - objects: ReadableStreamObjects<'js, Self, R>, - reason: Value<'js>, - ) -> Result<(Promise<'js>, ReadableStreamObjects<'js, Self, R>)> { - objects.with_controller( - reason, - |reason, objects| { - ReadableStreamDefaultControllerOwned::<'js>::cancel_steps(ctx, objects, reason) - }, - |reason, objects| { - ReadableByteStreamControllerOwned::<'js>::cancel_steps(ctx, objects, reason) - }, - ) - } - - fn release_steps(&mut self) { - match self { - ReadableStreamControllerOwned::ReadableStreamDefaultController(c) => c.release_steps(), - ReadableStreamControllerOwned::ReadableStreamByteController(c) => c.release_steps(), - } - } -} - -impl<'js> From> for ReadableStreamControllerOwned<'js> { - fn from(value: ReadableStreamDefaultControllerOwned<'js>) -> Self { - Self::ReadableStreamDefaultController(value) - } -} - -impl<'js> From> for ReadableStreamControllerOwned<'js> { - fn from(value: ReadableByteStreamControllerOwned<'js>) -> Self { - Self::ReadableStreamByteController(value) - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/default_controller.rs b/stdlib/src/llrt/llrt_stream_web/readable/default_controller.rs deleted file mode 100644 index 892564e0..00000000 --- a/stdlib/src/llrt/llrt_stream_web/readable/default_controller.rs +++ /dev/null @@ -1,960 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use crate::llrt_utils::option::{Null, Undefined}; -use rquickjs::{ - class::{OwnedBorrow, OwnedBorrowMut, Trace}, - methods, - prelude::{Opt, This}, - Class, Ctx, Error, Exception, JsLifetime, Object, Promise, Result, Value, -}; -use std::{future, pin::Pin, rc::Rc}; - -/// Native async pull: returns Ok(Some(chunk)) or Ok(None) for EOF. -/// Result of a native pull: data ready, EOF, or need async. -pub enum NativePullResult<'js> { - /// Data chunk ready synchronously - Ready(Value<'js>), - /// EOF — no more data - Eof, - /// Need async — returns a future for the pending case - Pending(Pin>>> + 'js>>), -} - -pub type NativePullFn<'js> = dyn Fn(&Ctx<'js>) -> Result> + 'js; - -/// Wrapper satisfying JsLifetime/Trace. -pub struct NativePull<'js>(pub Rc>); -impl<'js> Clone for NativePull<'js> { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} -unsafe impl<'js> JsLifetime<'js> for NativePull<'js> { - type Changed<'to> = NativePull<'to>; -} -impl<'js> Trace<'js> for NativePull<'js> { - fn trace<'a>(&self, _: rquickjs::class::Tracer<'a, 'js>) {} -} - -use crate::llrt_stream_web::{ - queuing_strategy::{SizeAlgorithm, SizeValue}, - readable::{ - byte_controller::ReadableByteStreamControllerOwned, - controller::{ - ReadableStreamController, ReadableStreamControllerClass, ReadableStreamControllerOwned, - }, - default_reader::{ReadableStreamDefaultReaderOrUndefined, ReadableStreamReadRequest}, - objects::{ - ReadableStreamClassObjects, ReadableStreamDefaultControllerObjects, - ReadableStreamDefaultReaderObjects, ReadableStreamObjects, - }, - reader::ReadableStreamReader, - stream::{ - algorithms::{CancelAlgorithm, PullAlgorithm, StartAlgorithm}, - source::UnderlyingSource, - ReadableStream, ReadableStreamClass, ReadableStreamOwned, ReadableStreamState, - }, - }, - utils::{ - class_from_owned_borrow_mut, - promise::{promise_resolved_with, upon_promise}, - queue::QueueWithSizes, - UnwrapOrUndefined, - }, -}; - -#[derive(JsLifetime, Trace)] -#[rquickjs::class] -pub struct ReadableStreamDefaultController<'js> { - cancel_algorithm: Option>, - pub(super) close_requested: bool, - pull_again: bool, - pull_algorithm: Option>, - pub(crate) pulling: bool, - pub(crate) container: QueueWithSizes<'js>, - started: bool, - strategy_hwm: f64, - strategy_size_algorithm: Option>, - pub(super) stream: ReadableStreamClass<'js>, - pub native_pull: Option>, - /// Whether this stream was created with `{type: 'owning'}`. Owning streams - /// accept a non-empty `transfer` array in `controller.enqueue` and - /// structurally transfer each buffer before queueing; non-owning streams - /// throw when a non-empty `transfer` list is provided. - #[qjs(skip_trace)] - pub(super) is_owning_type: bool, -} - -impl<'js> Drop for ReadableStreamDefaultController<'js> { - fn drop(&mut self) { - self.native_pull = None; - } -} - -pub type ReadableStreamDefaultControllerClass<'js> = - Class<'js, ReadableStreamDefaultController<'js>>; -pub(super) type ReadableStreamDefaultControllerOwned<'js> = - OwnedBorrowMut<'js, ReadableStreamDefaultController<'js>>; - -impl<'js> ReadableStreamDefaultController<'js> { - pub(super) fn set_up_readable_stream_default_controller_from_underlying_source( - ctx: Ctx<'js>, - stream: ReadableStreamOwned<'js>, - underlying_source: Null>>, - underlying_source_dict: UnderlyingSource<'js>, - high_water_mark: f64, - size_algorithm: SizeAlgorithm<'js>, - is_owning_type: bool, - ) -> Result<()> { - let (start_algorithm, pull_algorithm, cancel_algorithm) = ( - // If underlyingSourceDict["start"] exists, then set startAlgorithm to an algorithm which returns the result of invoking underlyingSourceDict["start"] with argument list - // « controller » and callback this value underlyingSource. - underlying_source_dict - .start - .map(|f| StartAlgorithm::Function { - f, - underlying_source: underlying_source.clone(), - }) - .unwrap_or(StartAlgorithm::ReturnUndefined), - // If underlyingSourceDict["pull"] exists, then set pullAlgorithm to an algorithm which returns the result of invoking underlyingSourceDict["pull"] with argument list - // « controller » and callback this value underlyingSource. - underlying_source_dict - .pull - .map(|f| PullAlgorithm::Function { - f, - underlying_source: underlying_source.clone(), - }) - .unwrap_or(PullAlgorithm::ReturnPromiseUndefined), - // If underlyingSourceDict["cancel"] exists, then set cancelAlgorithm to an algorithm which takes an argument reason and returns the result of invoking underlyingSourceDict["cancel"] with argument list - // « reason » and callback this value underlyingSource. - underlying_source_dict - .cancel - .map(|f| CancelAlgorithm::Function { - f, - underlying_source, - }) - .unwrap_or(CancelAlgorithm::ReturnPromiseUndefined), - ); - - // Perform ? SetUpReadableStreamDefaultController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm). - Self::set_up_readable_stream_default_controller( - ctx.clone(), - stream, - start_algorithm, - pull_algorithm, - cancel_algorithm, - high_water_mark, - size_algorithm, - is_owning_type, - )?; - - Ok(()) - } - - #[allow(clippy::too_many_arguments)] - pub(super) fn set_up_readable_stream_default_controller( - ctx: Ctx<'js>, - stream: ReadableStreamOwned<'js>, - start_algorithm: StartAlgorithm<'js>, - pull_algorithm: PullAlgorithm<'js>, - cancel_algorithm: CancelAlgorithm<'js>, - high_water_mark: f64, - size_algorithm: SizeAlgorithm<'js>, - is_owning_type: bool, - ) -> Result> { - let (stream_class, mut stream) = class_from_owned_borrow_mut(stream); - - let controller = ReadableStreamDefaultController { - // Set controller.[[stream]] to stream. - stream: stream_class.clone(), - - // Perform ! ResetQueue(controller). - container: QueueWithSizes::new(), - - // Set controller.[[started]], controller.[[closeRequested]], controller.[[pullAgain]], and controller.[[pulling]] to false. - started: false, - close_requested: false, - pull_again: false, - pulling: false, - - // Set controller.[[strategySizeAlgorithm]] to sizeAlgorithm and controller.[[strategyHWM]] to highWaterMark. - strategy_size_algorithm: Some(size_algorithm), - strategy_hwm: high_water_mark, - - // Set controller.[[pullAlgorithm]] to pullAlgorithm. - pull_algorithm: Some(pull_algorithm), - // Set controller.[[cancelAlgorithm]] to cancelAlgorithm. - cancel_algorithm: Some(cancel_algorithm), - native_pull: None, - is_owning_type, - }; - - let controller_class = Class::instance(ctx.clone(), controller)?; - - // Set stream.[[controller]] to controller. - stream.controller = ReadableStreamControllerClass::ReadableStreamDefaultController( - controller_class.clone(), - ); - - let objects = ReadableStreamObjects::new_default( - stream, - OwnedBorrowMut::from_class(controller_class), - ); - - let promise_primordials = objects.stream.promise_primordials.clone(); - - // Let startResult be the result of performing startAlgorithm. (This might throw an exception.) - let (start_result, objects_class) = - Self::start_algorithm(ctx.clone(), objects, start_algorithm)?; - - // Let startPromise be a promise resolved with startResult. - let start_promise = promise_resolved_with(&ctx, &promise_primordials, Ok(start_result))?; - - let _ = upon_promise::, _>(ctx.clone(), start_promise, { - let objects_class = objects_class.clone(); - move |ctx, result| { - let mut objects = - ReadableStreamObjects::from_class_no_reader(objects_class).refresh_reader(); - - match result { - // Upon fulfillment of startPromise, - Ok(_) => { - // Set controller.[[started]] to true. - objects.controller.started = true; - // Perform ! ReadableByteStreamControllerCallPullIfNeeded(controller). - Self::readable_stream_default_controller_call_pull_if_needed(ctx, objects)?; - } - // Upon rejection of startPromise with reason r, - Err(r) => { - // Perform ! ReadableByteStreamControllerError(controller, r). - Self::readable_stream_default_controller_error(objects, r)?; - } - } - Ok(()) - } - })?; - - Ok(objects_class.controller) - } - - fn readable_stream_default_controller_call_pull_if_needed< - R: ReadableStreamDefaultReaderOrUndefined<'js>, - >( - ctx: Ctx<'js>, - objects: ReadableStreamDefaultControllerObjects<'js, R>, - ) -> Result> { - // Let shouldPull be ! ReadableStreamDefaultControllerShouldCallPull(controller). - - let (should_pull, mut objects) = - ReadableStreamDefaultController::readable_stream_default_controller_should_call_pull( - objects, - ); - - // If shouldPull is false, return. - if !should_pull { - return Ok(objects); - } - - // If controller.[[pulling]] is true, - if objects.controller.pulling { - // Set controller.[[pullAgain]] to true. - objects.controller.pull_again = true; - - // Return. - return Ok(objects); - } - - // Set controller.[[pulling]] to true. - objects.controller.pulling = true; - - // Let pullPromise be the result of performing controller.[[pullAlgorithm]]. - let (pull_promise, objects_class) = Self::pull_algorithm(ctx.clone(), objects)?; - - upon_promise::, _>(ctx.clone(), pull_promise, { - let objects_class = objects_class.clone(); - move |ctx, result| { - let mut objects = - ReadableStreamObjects::from_class_no_reader(objects_class).refresh_reader(); - match result { - // Upon fulfillment of pullPromise, - Ok(_) => { - // Set controller.[[pulling]] to false. - objects.controller.pulling = false; - // If controller.[[pullAgain]] is true, - if objects.controller.pull_again { - // Set controller.[[pullAgain]] to false. - objects.controller.pull_again = false; - // Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(controller). - Self::readable_stream_default_controller_call_pull_if_needed( - ctx, objects, - )?; - }; - Ok(()) - } - // Upon rejection of pullPromise with reason e, - Err(e) => { - // Perform ! ReadableStreamDefaultControllerError(controller, e). - Self::readable_stream_default_controller_error(objects, e)?; - Ok(()) - } - } - } - })?; - - Ok(ReadableStreamObjects::from_class(objects_class)) - } - - pub(super) fn readable_stream_default_controller_error>( - // Let stream be controller.[[stream]]. - mut objects: ReadableStreamDefaultControllerObjects<'js, R>, - e: Value<'js>, - ) -> Result> { - // If stream.[[state]] is not "readable", return. - if !matches!(objects.stream.state, ReadableStreamState::Readable) { - return Ok(objects); - }; - - // Perform ! ResetQueue(controller). - objects.controller.container.reset_queue(); - - // Perform ! ReadableStreamDefaultControllerClearAlgorithms(controller). - objects - .controller - .readable_stream_default_controller_clear_algorithms(); - - // Perform ! ReadableStreamError(stream, e). - ReadableStream::readable_stream_error(objects, e) - } - - fn readable_stream_default_controller_should_call_pull< - R: ReadableStreamDefaultReaderOrUndefined<'js>, - >( - mut objects: ReadableStreamDefaultControllerObjects<'js, R>, - ) -> (bool, ReadableStreamDefaultControllerObjects<'js, R>) { - // Let stream be controller.[[stream]]. - // If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(controller) is false, return false. - if !objects - .controller - .readable_stream_default_controller_can_close_or_enqueue(&objects.stream) - { - return (false, objects); - } - - // If controller.[[started]] is false, return false. - if !objects.controller.started { - return (false, objects); - } - - { - let mut ret = false; - // If ! IsReadableStreamLocked(stream) is true and ! ReadableStreamGetNumReadRequests(stream) > 0, return true. - objects = objects - .with_some_reader( - |objects| { - if ReadableStream::readable_stream_get_num_read_requests(&objects.reader) - > 0 - { - ret = true - } - Ok(objects) - }, - Ok, - ) - .unwrap(); - if ret { - return (true, objects); - } - } - - // Let desiredSize be ! ReadableStreamDefaultControllerGetDesiredSize(controller). - let desired_size = objects.controller - .readable_stream_default_controller_get_desired_size(&objects.stream) - .0 - .expect( - "desiredSize should not be null during ReadableStreamDefaultControllerShouldCallPull", - ); - // If desiredSize > 0, return true. - if desired_size > 0.0 { - return (true, objects); - } - - // Return false. - (false, objects) - } - - fn readable_stream_default_controller_clear_algorithms(&mut self) { - self.pull_algorithm = None; - self.cancel_algorithm = None; - self.strategy_size_algorithm = None; - self.native_pull = None; - } - - fn readable_stream_default_controller_can_close_or_enqueue( - &self, - stream: &ReadableStream<'js>, - ) -> bool { - // Let state be controller.[[stream]].[[state]]. - match stream.state { - // If controller.[[closeRequested]] is false and state is "readable", return true. - ReadableStreamState::Readable if !self.close_requested => true, - // Otherwise, return false. - _ => false, - } - } - - pub(crate) fn readable_stream_default_controller_get_desired_size( - &self, - stream: &ReadableStream<'js>, - ) -> Null { - // Let state be controller.[[stream]].[[state]]. - match stream.state { - // If state is "errored", return null. - ReadableStreamState::Errored(_) => Null(None), - // If state is "closed", return 0. - ReadableStreamState::Closed => Null(Some(0.0)), - // Return controller.[[strategyHWM]] − controller.[[queueTotalSize]]. - ReadableStreamState::Readable => { - Null(Some(self.strategy_hwm - self.container.queue_total_size)) - } - } - } - - pub(super) fn readable_stream_default_controller_close>( - ctx: Ctx<'js>, - // Let stream be controller.[[stream]]. - mut objects: ReadableStreamDefaultControllerObjects<'js, R>, - ) -> Result> { - // If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(controller) is false, return. - if !objects - .controller - .readable_stream_default_controller_can_close_or_enqueue(&objects.stream) - { - return Ok(objects); - } - - // Set controller.[[closeRequested]] to true. - objects.controller.close_requested = true; - - // If controller.[[queue]] is empty, - if objects.controller.container.queue.is_empty() { - // Perform ! ReadableStreamDefaultControllerClearAlgorithms(controller). - objects - .controller - .readable_stream_default_controller_clear_algorithms(); - // Perform ! ReadableStreamClose(stream). - objects = ReadableStream::readable_stream_close(ctx, objects)?; - } - - Ok(objects) - } - - pub(super) fn readable_stream_default_controller_enqueue< - R: ReadableStreamDefaultReaderOrUndefined<'js>, - >( - ctx: Ctx<'js>, - // Let stream be controller.[[stream]]. - mut objects: ReadableStreamDefaultControllerObjects<'js, R>, - chunk: Value<'js>, - ) -> Result> { - // If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(controller) is false, return. - if !objects - .controller - .readable_stream_default_controller_can_close_or_enqueue(&objects.stream) - { - return Ok(objects); - } - - let mut els = true; - // If ! IsReadableStreamLocked(stream) is true and ! ReadableStreamGetNumReadRequests(stream) > 0, perform ! ReadableStreamFulfillReadRequest(stream, chunk, false). - objects = objects.with_some_reader( - |objects| { - if ReadableStream::readable_stream_get_num_read_requests(&objects.reader) > 0 { - els = false; - ReadableStream::readable_stream_fulfill_read_request( - &ctx, - objects, - chunk.clone(), - false, - ) - } else { - Ok(objects) - } - }, - Ok, - )?; - - if els { - // Let result be the result of performing controller.[[strategySizeAlgorithm]], passing in chunk, and interpreting the result as a completion record. - let (result, objects_class) = - Self::strategy_size_algorithm(ctx.clone(), objects, chunk.clone()); - - objects = ReadableStreamObjects::from_class(objects_class); - - match result { - // If result is an abrupt completion, - Err(Error::Exception) => { - let err = ctx.catch(); - // Perform ! ReadableStreamDefaultControllerError(controller, result.[[Value]]). - Self::readable_stream_default_controller_error(objects, err.clone())?; - - return Err(ctx.throw(err)); - } - // Let chunkSize be result.[[Value]]. - Ok(chunk_size) => { - // Let enqueueResult be EnqueueValueWithSize(controller, chunk, chunkSize). - let enqueue_result = objects - .controller - .container - .enqueue_value_with_size(&ctx, chunk, chunk_size); - - match enqueue_result { - // If enqueueResult is an abrupt completion, - Err(Error::Exception) => { - let err = ctx.catch(); - // Perform ! ReadableStreamDefaultControllerError(controller, enqueueResult.[[Value]]). - Self::readable_stream_default_controller_error(objects, err.clone())?; - return Err(ctx.throw(err)); - } - Err(err) => return Err(err), - Ok(()) => {} - } - } - Err(err) => return Err(err), - } - } - - // Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(controller). - Self::readable_stream_default_controller_call_pull_if_needed(ctx, objects) - } - - fn start_algorithm>( - ctx: Ctx<'js>, - objects: ReadableStreamDefaultControllerObjects<'js, R>, - start_algorithm: StartAlgorithm<'js>, - ) -> Result<( - Value<'js>, - ReadableStreamClassObjects<'js, OwnedBorrowMut<'js, Self>, R>, - )> { - let objects_class = objects.into_inner(); - - Ok(( - start_algorithm.call( - ctx, - ReadableStreamControllerClass::ReadableStreamDefaultController( - objects_class.controller.clone(), - ), - )?, - objects_class, - )) - } - - fn pull_algorithm>( - ctx: Ctx<'js>, - objects: ReadableStreamDefaultControllerObjects<'js, R>, - ) -> Result<( - Promise<'js>, - ReadableStreamClassObjects<'js, OwnedBorrowMut<'js, Self>, R>, - )> { - let pull_algorithm = objects - .controller - .pull_algorithm - .clone() - .expect("pull algorithm used after ReadableStreamDefaultControllerClearAlgorithms"); - let promise_primordials = objects.stream.promise_primordials.clone(); - let objects_class = objects.into_inner(); - - Ok(( - pull_algorithm.call( - ctx, - &promise_primordials, - ReadableStreamControllerClass::ReadableStreamDefaultController( - objects_class.controller.clone(), - ), - )?, - objects_class, - )) - } - - fn strategy_size_algorithm>( - ctx: Ctx<'js>, - objects: ReadableStreamDefaultControllerObjects<'js, R>, - chunk: Value<'js>, - ) -> ( - Result>, - ReadableStreamClassObjects<'js, OwnedBorrowMut<'js, Self>, R>, - ) { - let strategy_size_algorithm = objects - .controller - .strategy_size_algorithm - .clone() - .expect("size algorithm used after ReadableStreamDefaultControllerClearAlgorithms"); - let objects_class = objects.into_inner(); - - (strategy_size_algorithm.call(ctx, chunk), objects_class) - } - - pub(super) fn cancel_algorithm>( - ctx: Ctx<'js>, - objects: ReadableStreamDefaultControllerObjects<'js, R>, - reason: Value<'js>, - ) -> Result<( - Promise<'js>, - ReadableStreamClassObjects<'js, OwnedBorrowMut<'js, Self>, R>, - )> { - let cancel_algorithm = - objects.controller.cancel_algorithm.clone().expect( - "cancel algorithm used after ReadableStreamDefaultControllerClearAlgorithms", - ); - let promise_primordials = objects.stream.promise_primordials.clone(); - let objects_class = objects.into_inner(); - - Ok(( - cancel_algorithm.call(ctx, &promise_primordials, reason)?, - objects_class, - )) - } -} - -#[methods(rename_all = "camelCase")] -impl<'js> ReadableStreamDefaultController<'js> { - // this is required by web platform tests for unclear reasons - fn constructor() -> Self { - unimplemented!() - } - - #[qjs(constructor)] - fn new(ctx: Ctx<'js>) -> Result> { - Err(Exception::throw_type(&ctx, "Illegal constructor")) - } - - // readonly attribute unrestricted double? desiredSize; - #[qjs(get)] - fn desired_size(&self) -> Null { - let stream = OwnedBorrow::from_class(self.stream.clone()); - self.readable_stream_default_controller_get_desired_size(&stream) - } - - // undefined close(); - fn close(ctx: Ctx<'js>, controller: This>) -> Result<()> { - let objects = ReadableStreamObjects::from_default_controller(controller.0); - - // If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(this) is false, throw a TypeError exception. - if !objects - .controller - .readable_stream_default_controller_can_close_or_enqueue(&objects.stream) - { - return Err(Exception::throw_type( - &ctx, - "The stream is not in a state that permits close", - )); - } - - // Perform ! ReadableStreamDefaultControllerClose(this). - Self::readable_stream_default_controller_close(ctx, objects)?; - Ok(()) - } - - // undefined enqueue(optional any chunk, optional ReadableStreamEnqueueOptions options = {}); - fn enqueue( - ctx: Ctx<'js>, - controller: This>, - chunk: Opt>, - options: Opt>, - ) -> Result<()> { - // Handle the `transfer` option per the `type: 'owning'` ReadableStream - // proposal (WPT `streams/readable-streams/owning-type`). The option - // is only meaningful on owning-type streams; any other stream throws - // `TypeError` if the caller passes a non-empty transfer list. - // - // WebIDL getter semantics apply: property access must propagate. - let mut transfer_list: Option> = None; - if let Some(opts) = options.0.as_ref().and_then(|v| v.as_object()) { - transfer_list = opts.get::<_, Option>>("transfer")?; - } - let has_transfer_items = transfer_list.as_ref().is_some_and(|arr| !arr.is_empty()); - if has_transfer_items && !controller.is_owning_type { - return Err(Exception::throw_type(&ctx, "transfer list is not empty")); - } - // Detach each buffer in the transfer list (owning-type streams). Uses - // JS `ArrayBuffer.prototype.transfer()` which returns a new buffer - // with the same bytes and detaches the original. We re-bind the - // chunk to the new buffer if it was the same reference. - let chunk_value = chunk.0.clone().unwrap_or_undefined(&ctx); - let transferred_chunk = if has_transfer_items && controller.is_owning_type { - transfer_owning_chunk(&ctx, chunk_value.clone(), &transfer_list.unwrap())? - } else { - chunk_value - }; - - let objects = ReadableStreamObjects::from_default_controller(controller.0); - - // If ! ReadableStreamDefaultControllerCanCloseOrEnqueue(this) is false, throw a TypeError exception. - if !objects - .controller - .readable_stream_default_controller_can_close_or_enqueue(&objects.stream) - { - return Err(Exception::throw_type( - &ctx, - "The stream is not in a state that permits enqueue", - )); - } - - objects.with_reader( - |objects| { - // Perform ? ReadableStreamDefaultControllerEnqueue(this, chunk). - Self::readable_stream_default_controller_enqueue( - ctx.clone(), - objects, - transferred_chunk.clone(), - ) - }, - |_| panic!("Default controller must not have byob reader"), - |objects| { - // Perform ? ReadableStreamDefaultControllerEnqueue(this, chunk). - Self::readable_stream_default_controller_enqueue( - ctx.clone(), - objects, - transferred_chunk.clone(), - ) - }, - )?; - - Ok(()) - } - - // undefined error(optional any e); - fn error( - ctx: Ctx<'js>, - controller: This>, - e: Opt>, - ) -> Result<()> { - let objects = ReadableStreamObjects::from_default_controller(controller.0); - - // Perform ! ReadableStreamDefaultControllerError(this, e). - Self::readable_stream_default_controller_error(objects, e.0.unwrap_or_undefined(&ctx))?; - Ok(()) - } -} - -impl<'js> ReadableStreamController<'js> for ReadableStreamDefaultControllerOwned<'js> { - type Class = ReadableStreamDefaultControllerClass<'js>; - - fn with_controller( - self, - ctx: C, - default: impl FnOnce( - C, - ReadableStreamDefaultControllerOwned<'js>, - ) -> Result<(O, ReadableStreamDefaultControllerOwned<'js>)>, - _: impl FnOnce( - C, - ReadableByteStreamControllerOwned<'js>, - ) -> Result<(O, ReadableByteStreamControllerOwned<'js>)>, - ) -> Result<(O, Self)> { - let (ctx, reader) = default(ctx, self)?; - Ok((ctx, reader)) - } - - fn into_inner(self) -> Self::Class { - OwnedBorrowMut::into_inner(self) - } - - fn from_class(class: Self::Class) -> Self { - OwnedBorrowMut::from_class(class) - } - - fn into_erased(self) -> ReadableStreamControllerOwned<'js> { - ReadableStreamControllerOwned::ReadableStreamDefaultController(self) - } - - fn try_from_erased(erased: ReadableStreamControllerOwned<'js>) -> Option { - match erased { - ReadableStreamControllerOwned::ReadableStreamDefaultController(r) => Some(r), - ReadableStreamControllerOwned::ReadableStreamByteController(_) => None, - } - } - - fn pull_steps( - ctx: &Ctx<'js>, - mut objects: ReadableStreamDefaultReaderObjects<'js, Self>, - read_request: impl ReadableStreamReadRequest<'js> + 'js, - ) -> Result> { - // If this.[[queue]] is not empty, - if !objects.controller.container.queue.is_empty() { - // Let chunk be ! DequeueValue(this). - let chunk = objects.controller.container.dequeue_value(); - // If this.[[closeRequested]] is true and this.[[queue]] is empty, - if objects.controller.close_requested && objects.controller.container.queue.is_empty() { - // Perform ! ReadableStreamDefaultControllerClearAlgorithms(this). - objects - .controller - .readable_stream_default_controller_clear_algorithms(); - // Perform ! ReadableStreamClose(stream). - objects = ReadableStream::readable_stream_close(ctx.clone(), objects)?; - } else { - // Otherwise, perform ! ReadableStreamDefaultControllerCallPullIfNeeded(this). - objects = - ReadableStreamDefaultController::readable_stream_default_controller_call_pull_if_needed( - ctx.clone(), - objects, - )?; - } - - // Perform readRequest’s chunk steps, given chunk. - read_request.chunk_steps_typed(objects, chunk) - } else { - // Otherwise, - // Perform ! ReadableStreamAddReadRequest(stream, readRequest). - objects - .stream - .readable_stream_add_read_request(&mut objects.reader, read_request); - // Perform ! ReadableStreamDefaultControllerCallPullIfNeeded(this). - - ReadableStreamDefaultController::readable_stream_default_controller_call_pull_if_needed( - ctx.clone(), - objects, - ) - } - } - - fn cancel_steps>( - ctx: &Ctx<'js>, - mut objects: ReadableStreamObjects<'js, Self, R>, - reason: Value<'js>, - ) -> Result<(Promise<'js>, ReadableStreamObjects<'js, Self, R>)> { - // Perform ! ResetQueue(this). - objects.controller.container.reset_queue(); - - // Let result be the result of performing this.[[cancelAlgorithm]], passing reason. - let (result, objects_class) = - ReadableStreamDefaultController::cancel_algorithm(ctx.clone(), objects, reason)?; - - objects = ReadableStreamObjects::from_class(objects_class); - // Perform ! ReadableStreamDefaultControllerClearAlgorithms(this). - objects - .controller - .readable_stream_default_controller_clear_algorithms(); - - // Return result. - Ok((result, objects)) - } - - fn release_steps(&mut self) {} -} - -/// Public API for enqueuing data into a default controller from Rust code -pub fn readable_stream_default_controller_enqueue_value<'js>( - ctx: Ctx<'js>, - controller: ReadableStreamDefaultControllerClass<'js>, - chunk: Value<'js>, -) -> Result<()> { - let objects = - ReadableStreamObjects::from_default_controller(OwnedBorrowMut::from_class(controller)); - - if !objects - .controller - .readable_stream_default_controller_can_close_or_enqueue(&objects.stream) - { - return Ok(()); // Silently ignore if can't enqueue - } - - objects.with_reader( - |objects| { - ReadableStreamDefaultController::readable_stream_default_controller_enqueue( - ctx.clone(), - objects, - chunk.clone(), - ) - }, - |_| panic!("Default controller must not have byob reader"), - |objects| { - ReadableStreamDefaultController::readable_stream_default_controller_enqueue( - ctx.clone(), - objects, - chunk.clone(), - ) - }, - )?; - - Ok(()) -} - -/// Public API for closing a default controller from Rust code -pub fn readable_stream_default_controller_close_stream<'js>( - ctx: Ctx<'js>, - controller: ReadableStreamDefaultControllerClass<'js>, -) -> Result<()> { - let objects = - ReadableStreamObjects::from_default_controller(OwnedBorrowMut::from_class(controller)); - - if !objects - .controller - .readable_stream_default_controller_can_close_or_enqueue(&objects.stream) - { - return Ok(()); - } - - ReadableStreamDefaultController::readable_stream_default_controller_close(ctx, objects)?; - Ok(()) -} - -/// Public API for erroring a default controller from Rust code -pub fn readable_stream_default_controller_error_stream<'js>( - controller: ReadableStreamDefaultControllerClass<'js>, - error: Value<'js>, -) -> Result<()> { - let objects = - ReadableStreamObjects::from_default_controller(OwnedBorrowMut::from_class(controller)); - - objects.with_reader( - |objects| { - ReadableStreamDefaultController::readable_stream_default_controller_error( - objects, - error.clone(), - ) - }, - |_| panic!("Default controller must not have byob reader"), - |objects| { - ReadableStreamDefaultController::readable_stream_default_controller_error( - objects, - error.clone(), - ) - }, - )?; - - Ok(()) -} - -/// Structurally transfer each `ArrayBuffer` in `transfer_list` (detaches the -/// original) and, if `chunk` references the same buffer, rebind it to the -/// transferred copy. Called for `controller.enqueue(chunk, { transfer })` on -/// `type: 'owning'` ReadableStreams. -fn transfer_owning_chunk<'js>( - ctx: &Ctx<'js>, - chunk: Value<'js>, - transfer_list: &rquickjs::Array<'js>, -) -> Result> { - use rquickjs::ArrayBuffer; - let mut chunk_replacement: Option> = None; - for v in transfer_list.iter::>() { - let v = v?; - let Some(ab) = ArrayBuffer::from_value(v.clone()) else { - return Err(rquickjs::Exception::throw_type( - ctx, - "transfer list item is not an ArrayBuffer", - )); - }; - // JS object identity: if this transfer-list entry IS the chunk - // itself, record that we need to replace the chunk with the - // transferred copy. Compare before calling transfer() (which - // detaches the buffer). - let is_chunk = chunk == v; - // Use JS `ArrayBuffer.prototype.transfer()` which returns a new - // buffer of the same byteLength and detaches the original. - let transfer_fn: rquickjs::Function<'js> = ab.as_object().get("transfer")?; - let new_buf: Value<'js> = transfer_fn.call((rquickjs::function::This(ab.clone()),))?; - if is_chunk && chunk_replacement.is_none() { - chunk_replacement = Some(new_buf); - } - } - Ok(chunk_replacement.unwrap_or(chunk)) -} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/default_reader.rs b/stdlib/src/llrt/llrt_stream_web/readable/default_reader.rs deleted file mode 100644 index bd8ea5ba..00000000 --- a/stdlib/src/llrt/llrt_stream_web/readable/default_reader.rs +++ /dev/null @@ -1,540 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use crate::llrt_stream_web::readable::default_controller::{NativePull, NativePullResult}; -use crate::llrt_stream_web::{ - readable::{ - byob_reader::ReadableStreamBYOBReaderOwned, - controller::{ReadableStreamController, ReadableStreamControllerClass}, - objects::{ReadableStreamDefaultReaderObjects, ReadableStreamObjects}, - reader::{ - ReadableStreamGenericReader, ReadableStreamReader, ReadableStreamReaderOwned, - UndefinedReader, - }, - stream::{ReadableStream, ReadableStreamOwned, ReadableStreamState}, - }, - utils::{ - promise::{ - promise_rejected_with_constructor, promise_resolved_with, PromisePrimordials, - ResolveablePromise, - }, - UnwrapOrUndefined, - }, -}; -use rquickjs::{ - atom::PredefinedAtom, - class::{OwnedBorrowMut, Trace, Tracer}, - methods, - prelude::{Opt, This}, - Class, Ctx, Exception, IntoJs, JsLifetime, Object, Promise, Result, Value, -}; -use std::collections::VecDeque; - -#[derive(Trace)] -#[rquickjs::class] -pub(crate) struct ReadableStreamDefaultReader<'js> { - pub(super) generic: ReadableStreamGenericReader<'js>, - pub(super) read_requests: VecDeque + 'js>>, -} - -pub(crate) type ReadableStreamDefaultReaderClass<'js> = - Class<'js, ReadableStreamDefaultReader<'js>>; -pub(crate) type ReadableStreamDefaultReaderOwned<'js> = - OwnedBorrowMut<'js, ReadableStreamDefaultReader<'js>>; - -unsafe impl<'js> JsLifetime<'js> for ReadableStreamDefaultReader<'js> { - type Changed<'to> = ReadableStreamDefaultReader<'to>; -} - -impl<'js> ReadableStreamDefaultReader<'js> { - pub(super) fn readable_stream_default_reader_error_read_requests< - C: ReadableStreamController<'js>, - >( - mut objects: ReadableStreamDefaultReaderObjects<'js, C>, - e: Value<'js>, - ) -> Result> { - // Let readRequests be reader.[[readRequests]]. - let read_requests = &mut objects.reader.read_requests; - - // Set reader.[[readRequests]] to a new empty list. - let read_requests = read_requests.split_off(0); - - // For each readRequest of readRequests, - for read_request in read_requests { - // Perform readRequest’s error steps, given e. - objects = read_request.error_steps_typed(objects, e.clone())?; - } - - Ok(objects) - } - - pub(super) fn readable_stream_default_reader_read< - 'closure, - C: ReadableStreamController<'js>, - >( - ctx: &Ctx<'js>, - // Let stream be reader.[[stream]]. - mut objects: ReadableStreamDefaultReaderObjects<'js, C>, - read_request: impl ReadableStreamReadRequest<'js> + 'js, - ) -> Result> { - // Set stream.[[disturbed]] to true. - objects.stream.disturbed = true; - match objects.stream.state { - // If stream.[[state]] is "closed", perform readRequest’s close steps. - ReadableStreamState::Closed => read_request.close_steps_typed(ctx, objects), - // Otherwise, if stream.[[state]] is "errored", perform readRequest’s error steps given stream.[[storedError]]. - ReadableStreamState::Errored(ref stored_error) => { - let stored_error = stored_error.clone(); - read_request.error_steps_typed(objects, stored_error) - } - // Otherwise, - _ => { - // Perform ! stream.[[controller]].[[PullSteps]](readRequest). - C::pull_steps(ctx, objects, read_request) - } - } - } - - pub(super) fn set_up_readable_stream_default_reader( - ctx: &Ctx<'js>, - stream: ReadableStreamOwned<'js>, - ) -> Result<(ReadableStreamOwned<'js>, Class<'js, Self>)> { - // If ! IsReadableStreamLocked(stream) is true, throw a TypeError exception. - if stream.is_readable_stream_locked() { - return Err(Exception::throw_type( - ctx, - "This stream has already been locked for exclusive reading by another reader", - )); - } - - // Perform ! ReadableStreamReaderGenericInitialize(reader, stream). - let generic = - ReadableStreamGenericReader::readable_stream_reader_generic_initialize(ctx, stream)?; - let mut stream = OwnedBorrowMut::from_class(generic.stream.clone().unwrap()); - - let reader = Class::instance( - ctx.clone(), - Self { - generic, - // Set reader.[[readRequests]] to a new empty list. - read_requests: VecDeque::new(), - }, - )?; - - stream.reader = Some(reader.clone().into()); - - Ok((stream, reader)) - } - - pub(super) fn readable_stream_default_reader_release>( - mut objects: ReadableStreamDefaultReaderObjects<'js, C>, - ) -> Result> { - // Clear cached native_pull to release captured resources - objects.reader.read_requests.clear(); - // Perform ! ReadableStreamReaderGenericRelease(reader). - objects - .reader - .generic - .readable_stream_reader_generic_release(&mut objects.stream, || { - objects.controller.release_steps() - })?; - - // Let e be a new TypeError exception. - let e: Value = objects - .stream - .constructor_type_error - .call(("Reader was released",))?; - - // Perform ! ReadableStreamDefaultReaderErrorReadRequests(reader, e). - Self::readable_stream_default_reader_error_read_requests(objects, e) - } -} - -#[methods(rename_all = "camelCase")] -impl<'js> ReadableStreamDefaultReader<'js> { - #[qjs(constructor)] - pub fn new(ctx: Ctx<'js>, stream: ReadableStreamOwned<'js>) -> Result> { - // Perform ? SetUpReadableStreamDefaultReader(this, stream). - let (_, reader) = Self::set_up_readable_stream_default_reader(&ctx, stream)?; - Ok(reader) - } - - fn read(ctx: Ctx<'js>, reader: This>) -> Result> { - // If this.[[stream]] is undefined, return a promise rejected with a TypeError exception. - let Some(stream_class) = &reader.generic.stream else { - return promise_rejected_with_constructor( - &reader.generic.constructor_type_error, - &reader.generic.promise_primordials, - "Cannot read from a stream using a released reader", - ) - .map(|p| p.into_value()); - }; - - // Fast-path: if the controller has a native_pull and the queue is empty, - // bypass the full spec algorithm and read directly without promise wrapping. - if let Some(np) = try_get_native_pull(stream_class) { - stream_class.borrow_mut().disturbed = true; - return read_native(&ctx, &np, &reader.generic.promise_primordials); - } - - read_default(&ctx, reader.0) - } - - fn release_lock(reader: This>) -> Result<()> { - if reader.generic.stream.is_none() { - // If this.[[stream]] is undefined, return. - return Ok(()); - } - - let objects = ReadableStreamObjects::from_default_reader(reader.0); - - // Perform ! ReadableStreamDefaultReaderRelease(this). - Self::readable_stream_default_reader_release(objects)?; - Ok(()) - } - - #[qjs(get)] - fn closed(&self) -> Promise<'js> { - self.generic.closed_promise.promise.clone() - } - - fn cancel( - ctx: Ctx<'js>, - reader: This>, - reason: Opt>, - ) -> Result> { - if reader.generic.stream.is_none() { - // If this.[[stream]] is undefined, return a promise rejected with a TypeError exception. - return promise_rejected_with_constructor( - &reader.generic.constructor_type_error, - &reader.generic.promise_primordials, - "Cannot cancel a stream using a released reader", - ); - }; - - let objects = ReadableStreamObjects::from_default_reader(reader.0); - - // Return ! ReadableStreamReaderGenericCancel(this, reason). - let (promise, _) = ReadableStreamGenericReader::readable_stream_reader_generic_cancel( - ctx.clone(), - objects, - reason.0.unwrap_or_undefined(&ctx), - )?; - Ok(promise) - } -} - -impl<'js> ReadableStreamReader<'js> for ReadableStreamDefaultReaderOwned<'js> { - type Class = ReadableStreamDefaultReaderClass<'js>; - - fn with_reader( - self, - ctx: C, - default: impl FnOnce( - C, - ReadableStreamDefaultReaderOwned<'js>, - ) -> Result<(C, ReadableStreamDefaultReaderOwned<'js>)>, - _: impl FnOnce( - C, - ReadableStreamBYOBReaderOwned<'js>, - ) -> Result<(C, ReadableStreamBYOBReaderOwned<'js>)>, - _: impl FnOnce(C) -> Result, - ) -> Result<(C, Self)> { - default(ctx, self) - } - - fn into_inner(self) -> Self::Class { - self.into_inner() - } - - fn from_class(class: Self::Class) -> Self { - OwnedBorrowMut::from_class(class) - } - - fn try_from_erased(erased: Option>) -> Option { - match erased { - Some(ReadableStreamReaderOwned::ReadableStreamDefaultReader(r)) => Some(r), - _ => None, - } - } -} - -/// Check if the stream's controller has a native_pull fast-path available. -fn try_get_native_pull<'js>( - stream_class: &Class<'js, ReadableStream<'js>>, -) -> Option> { - let stream = stream_class.borrow(); - - // Early exit if state is not Readable - if !matches!(stream.state, ReadableStreamState::Readable) { - return None; - } - - let ReadableStreamControllerClass::ReadableStreamDefaultController(ctrl) = &stream.controller - else { - return None; - }; - - let ctrl = ctrl.borrow(); - - if ctrl.container.queue.is_empty() && !ctrl.pulling { - ctrl.native_pull.clone() - } else { - None - } -} - -/// Read using the native_pull fast-path, bypassing the full spec algorithm. -fn read_native<'js>( - ctx: &Ctx<'js>, - np: &NativePull<'js>, - primordials: &PromisePrimordials<'js>, -) -> Result> { - match (np.0)(ctx)? { - // Synchronous data — wrap in a resolved promise to satisfy the spec - // (reader.read() must always return a Promise). - NativePullResult::Ready(chunk) => { - let result = ReadableStreamReadResult { - value: Some(chunk), - done: false, - } - .into_js(ctx)?; - promise_resolved_with(ctx, primordials, Ok(result)).map(|p| p.into_value()) - } - NativePullResult::Eof => { - let result = ReadableStreamReadResult { - value: None, - done: true, - } - .into_js(ctx)?; - promise_resolved_with(ctx, primordials, Ok(result)).map(|p| p.into_value()) - } - // Async data — must return a promise - NativePullResult::Pending(fut) => { - let promise = Promise::wrap_future(ctx, async move { - fut.await.map(|chunk| ReadableStreamReadResult { - done: chunk.is_none(), - value: chunk, - }) - })?; - Ok(promise.into_value()) - } - } -} - -/// Read using the standard spec algorithm (ReadableStreamDefaultReaderRead). -fn read_default<'js>( - ctx: &Ctx<'js>, - reader: OwnedBorrowMut<'js, ReadableStreamDefaultReader<'js>>, -) -> Result> { - let objects = ReadableStreamObjects::from_default_reader(reader); - // Let promise be a new promise. - let promise = ResolveablePromise::new(ctx)?; - - // Let readRequest be a new read request with the following items: - #[derive(Trace)] - struct ReadRequest<'js> { - promise: ResolveablePromise<'js>, - } - - impl<'js> ReadableStreamReadRequest<'js> for ReadRequest<'js> { - // chunk steps, given chunk - // Resolve promise with «[ "value" → chunk, "done" → false ]». - fn chunk_steps( - &self, - objects: ReadableStreamDefaultReaderObjects<'js>, - chunk: Value<'js>, - ) -> Result> { - self.promise.resolve(ReadableStreamReadResult { - value: Some(chunk), - done: false, - })?; - Ok(objects) - } - - // close steps - // Resolve promise with «[ "value" → undefined, "done" → true ]». - fn close_steps( - &self, - _: &Ctx<'js>, - objects: ReadableStreamDefaultReaderObjects<'js>, - ) -> Result> { - self.promise.resolve(ReadableStreamReadResult { - value: None, - done: true, - })?; - Ok(objects) - } - - fn error_steps( - &self, - objects: ReadableStreamDefaultReaderObjects<'js>, - e: Value<'js>, - ) -> Result> { - self.promise.reject(e)?; - Ok(objects) - } - } - - // Perform ! ReadableStreamDefaultReaderRead(this, readRequest). - ReadableStreamDefaultReader::readable_stream_default_reader_read( - ctx, - objects, - ReadRequest { - promise: promise.clone(), - }, - )?; - - // Return promise. - Ok(promise.promise.into_value()) -} - -pub(crate) trait ReadableStreamDefaultReaderOrUndefined<'js>: - ReadableStreamReader<'js> -{ -} - -impl<'js> ReadableStreamDefaultReaderOrUndefined<'js> for ReadableStreamDefaultReaderOwned<'js> {} - -impl<'js> ReadableStreamDefaultReaderOrUndefined<'js> - for Option> -{ -} - -impl ReadableStreamDefaultReaderOrUndefined<'_> for UndefinedReader {} - -pub(crate) trait ReadableStreamReadRequest<'js>: Trace<'js> { - fn chunk_steps_typed>( - &self, - objects: ReadableStreamDefaultReaderObjects<'js, C>, - chunk: Value<'js>, - ) -> Result> - where - Self: Sized, - { - let mut erased = ReadableStreamObjects { - stream: objects.stream, - controller: objects.controller.into_erased(), - reader: objects.reader, - }; - - erased = self.chunk_steps(erased, chunk)?; - - Ok(ReadableStreamObjects { - stream: erased.stream, - controller: C::try_from_erased(erased.controller) - .expect("chunk steps must not change type of controller"), - reader: erased.reader, - }) - } - - fn chunk_steps( - &self, - objects: ReadableStreamDefaultReaderObjects<'js>, - chunk: Value<'js>, - ) -> Result>; - - fn close_steps_typed>( - &self, - ctx: &Ctx<'js>, - objects: ReadableStreamDefaultReaderObjects<'js, C>, - ) -> Result> - where - Self: Sized, - { - let mut erased = ReadableStreamObjects { - stream: objects.stream, - controller: objects.controller.into_erased(), - reader: objects.reader, - }; - - erased = self.close_steps(ctx, erased)?; - - Ok(ReadableStreamObjects { - stream: erased.stream, - controller: C::try_from_erased(erased.controller) - .expect("close steps must not change type of controller"), - reader: erased.reader, - }) - } - - fn close_steps( - &self, - ctx: &Ctx<'js>, - objects: ReadableStreamDefaultReaderObjects<'js>, - ) -> Result>; - - fn error_steps_typed>( - &self, - objects: ReadableStreamDefaultReaderObjects<'js, C>, - reason: Value<'js>, - ) -> Result> - where - Self: Sized, - { - let mut erased = ReadableStreamObjects { - stream: objects.stream, - controller: objects.controller.into_erased(), - reader: objects.reader, - }; - - erased = self.error_steps(erased, reason)?; - - Ok(ReadableStreamObjects { - stream: erased.stream, - controller: C::try_from_erased(erased.controller) - .expect("error steps must not change type of controller"), - reader: erased.reader, - }) - } - - fn error_steps( - &self, - objects: ReadableStreamDefaultReaderObjects<'js>, - reason: Value<'js>, - ) -> Result>; -} - -impl<'js> Trace<'js> for Box + 'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - self.as_ref().trace(tracer); - } -} - -impl<'js> ReadableStreamReadRequest<'js> for Box + 'js> { - fn chunk_steps( - &self, - objects: ReadableStreamDefaultReaderObjects<'js>, - chunk: Value<'js>, - ) -> Result> { - self.as_ref().chunk_steps(objects, chunk) - } - - fn close_steps( - &self, - ctx: &Ctx<'js>, - objects: ReadableStreamDefaultReaderObjects<'js>, - ) -> Result> { - self.as_ref().close_steps(ctx, objects) - } - - fn error_steps( - &self, - objects: ReadableStreamDefaultReaderObjects<'js>, - reason: Value<'js>, - ) -> Result> { - self.as_ref().error_steps(objects, reason) - } -} - -pub(super) struct ReadableStreamReadResult<'js> { - pub(super) value: Option>, - pub(super) done: bool, -} - -impl<'js> IntoJs<'js> for ReadableStreamReadResult<'js> { - fn into_js(self, ctx: &Ctx<'js>) -> Result> { - let obj = Object::new(ctx.clone())?; - obj.set(PredefinedAtom::Value, self.value)?; - obj.set("done", self.done)?; - Ok(obj.into_value()) - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/iterator.rs b/stdlib/src/llrt/llrt_stream_web/readable/iterator.rs deleted file mode 100644 index ca8a9f2f..00000000 --- a/stdlib/src/llrt/llrt_stream_web/readable/iterator.rs +++ /dev/null @@ -1,698 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use std::{ - rc::Rc, - sync::atomic::{AtomicBool, Ordering}, -}; - -use crate::llrt_utils::primordials::{BasePrimordials, Primordial}; -use rquickjs::{ - atom::PredefinedAtom, - class::{ - impl_::{CloneTrait, CloneWrapper}, - JsClass, OwnedBorrow, OwnedBorrowMut, Trace, Tracer, - }, - function::Constructor, - methods, - prelude::{Opt, This}, - Class, Coerced, Ctx, Error, Exception, FromJs, Function, IntoAtom, IntoJs, JsLifetime, Object, - Promise, Result, Symbol, Type, Value, -}; - -use crate::llrt_stream_web::{ - readable::{ - controller::ReadableStreamControllerOwned, - default_reader::{ - ReadableStreamDefaultReader, ReadableStreamDefaultReaderOwned, - ReadableStreamReadRequest, ReadableStreamReadResult, - }, - objects::{ - ReadableStreamClassObjects, ReadableStreamDefaultReaderObjects, ReadableStreamObjects, - }, - reader::ReadableStreamGenericReader, - }, - utils::{ - class_from_owned_borrow_mut, - promise::{promise_resolved_with, PromisePrimordials}, - promise::{upon_promise, upon_promise_fulfilment, ResolveablePromise}, - UnwrapOrUndefined, - }, -}; - -pub(super) enum IteratorKind { - Async, -} - -#[derive(Trace)] -pub(super) struct IteratorRecord<'js> { - pub(super) iterator: Object<'js>, - next_method: Function<'js>, - #[qjs(skip_trace)] - done: AtomicBool, - sync_to_async_iterator: Function<'js>, -} - -impl<'js> IteratorRecord<'js> { - pub(super) fn get_iterator( - ctx: &Ctx<'js>, - obj: Value<'js>, - kind: IteratorKind, - ) -> Result { - let method: Option> = match kind { - // If kind is async, then - IteratorKind::Async => { - // Let method be ? GetMethod(obj, %Symbol.asyncIterator%). - let method = get_method(ctx, obj.clone(), Symbol::async_iterator(ctx.clone()))?; - // If method is undefined, then - if method.is_none() { - // Let syncMethod be ? GetMethod(obj, %Symbol.iterator%). - let sync_method = get_method(ctx, obj.clone(), Symbol::iterator(ctx.clone()))?; - - // If syncMethod is undefined, throw a TypeError exception. - let sync_method = match sync_method { - None => { - return Err(Exception::throw_type(ctx, "Object is not an iterator")); - } - Some(sync_method) => sync_method, - }; - - // Let syncIteratorRecord be ? GetIteratorFromMethod(obj, syncMethod). - let sync_iterator_record = - Self::get_iterator_from_method(ctx, &obj, sync_method)?; - - // Return CreateAsyncFromSyncIterator(syncIteratorRecord). - return sync_iterator_record.create_async_from_sync_iterator(ctx); - } - - method - } - }; - - // If method is undefined, throw a TypeError exception. - match method { - None => Err(Exception::throw_type(ctx, "Object is not an iterator")), - Some(method) => { - // Return ? GetIteratorFromMethod(obj, method). - Self::get_iterator_from_method(ctx, &obj, method) - } - } - } - - fn get_iterator_from_method( - ctx: &Ctx<'js>, - obj: &Value<'js>, - method: Function<'js>, - ) -> Result { - // Let iterator be ? Call(method, obj). - let iterator: Value<'js> = method.call((This(obj),))?; - let iterator = match iterator.into_object() { - Some(iterator) => iterator, - None => { - return Err(Exception::throw_type( - ctx, - "The iterator method must return an object", - )); - } - }; - // Let nextMethod be ? Get(iterator, "next"). - let next_method = iterator.get(PredefinedAtom::Next)?; - // Let iteratorRecord be the Iterator Record { [[Iterator]]: iterator, [[NextMethod]]: nextMethod, [[Done]]: false }. - // Return iteratorRecord. - Ok(Self { - iterator, - next_method, - done: AtomicBool::new(false), - sync_to_async_iterator: IteratorPrimordials::get(ctx)? - .sync_to_async_iterator - .clone(), - }) - } - - fn create_async_from_sync_iterator(self, ctx: &Ctx<'js>) -> Result { - let sync_iterable = Object::new(ctx.clone())?; - sync_iterable.set( - Symbol::iterator(ctx.clone()), - Function::new(ctx.clone(), { - let iterator = self.iterator.clone(); - move || iterator.clone() - }), - )?; - - let async_iterator: Object<'js> = self.sync_to_async_iterator.call((sync_iterable,))?; - - let next_method = async_iterator.get(PredefinedAtom::Next)?; - - Ok(Self { - iterator: async_iterator, - next_method, - done: AtomicBool::new(false), - sync_to_async_iterator: self.sync_to_async_iterator, - }) - } - - pub(super) fn iterator_next( - &self, - ctx: &Ctx<'js>, - value: Option>, - ) -> Result> { - let result: Result> = match value { - // If value is not present, then - None => { - // Let result be Completion(Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]])). - - self.next_method.call((This(self.iterator.clone()),)) - } - // Else, - Some(value) => { - // Let result be Completion(Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]], « value »)). - self.next_method.call((This(self.iterator.clone()), value)) - } - }; - - let result = match result { - // If result is a throw completion, then - Err(Error::Exception) => { - // Set iteratorRecord.[[Done]] to true. - self.done.store(true, Ordering::Release); - // Return ? result. - return Err(Error::Exception); - } - Err(err) => return Err(err), - // Set result to ! result. - Ok(result) => result, - }; - - let result = match result.into_object() { - // If result is not an Object, then - None => { - // Set iteratorRecord.[[Done]] to true. - self.done.store(true, Ordering::Release); - return Err(Exception::throw_type( - ctx, - "The iterator.next() method must return an object", - )); - } - Some(result) => result, - }; - // Return result. - Ok(result) - } - - pub(super) fn iterator_complete(iterator_result: &Object<'js>) -> Result { - let done: Coerced = iterator_result.get(PredefinedAtom::Done)?; - Ok(done.0) - } - - pub(super) fn iterator_value(iterator_result: &Object<'js>) -> Result> { - iterator_result.get(PredefinedAtom::Value) - } -} - -pub(super) struct ReadableStreamAsyncIterator<'js> { - objects: ReadableStreamClassObjects< - 'js, - ReadableStreamControllerOwned<'js>, - ReadableStreamDefaultReaderOwned<'js>, - >, - prevent_cancel: bool, - is_finished: Rc, - ongoing_promise: Option>, - - promise_primordials: PromisePrimordials<'js>, - end_of_iteration: Symbol<'js>, -} - -impl<'js> Trace<'js> for ReadableStreamAsyncIterator<'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - Trace::<'js>::trace(&self.objects, tracer); - if let Some(ongoing_promise) = &self.ongoing_promise { - ongoing_promise.trace(tracer); - } - Trace::<'js>::trace(&self.end_of_iteration, tracer); - } -} - -unsafe impl<'js> JsLifetime<'js> for ReadableStreamAsyncIterator<'js> { - type Changed<'to> = ReadableStreamAsyncIterator<'to>; -} - -impl<'js> ReadableStreamAsyncIterator<'js> { - pub(super) fn new( - ctx: Ctx<'js>, - objects: ReadableStreamClassObjects< - 'js, - ReadableStreamControllerOwned<'js>, - ReadableStreamDefaultReaderOwned<'js>, - >, - promise_primordials: PromisePrimordials<'js>, - prevent_cancel: bool, - ) -> Result> { - let end_of_iteration = IteratorPrimordials::get(&ctx)?.end_of_iteration.clone(); - - Class::instance( - ctx, - Self { - objects, - prevent_cancel, - is_finished: Rc::new(AtomicBool::new(false)), - ongoing_promise: None, - promise_primordials, - end_of_iteration, - }, - ) - } -} - -// Custom JsClass implementation needed until prototype, function names and function lengths can be influenced in the class derivation macro -impl<'js> JsClass<'js> for ReadableStreamAsyncIterator<'js> { - const NAME: &'static str = "ReadableStreamAsyncIterator"; - type Mutable = rquickjs::class::Writable; - fn prototype(ctx: &Ctx<'js>) -> Result>> { - use rquickjs::class::impl_::MethodImplementor; - let proto = Object::new(ctx.clone())?; - let primordial = IteratorPrimordials::get(ctx)?; - proto.set_prototype(Some(&primordial.async_iterator_prototype))?; - let implementor = rquickjs::class::impl_::MethodImpl::::new(); - implementor.implement(&proto)?; - let next_fn: Function<'js> = proto.get("next")?; - // yup, the wpt tests really do check these. - next_fn.set_name("next")?; - let return_fn: Function<'js> = proto.get("return")?; - return_fn.set_name("return")?; - return_fn.set_length(1)?; - // Make `next` and `return` enumerable per WebIDL (rquickjs defaults to - // non-enumerable, but the async-iterator.any.js WPT tests check this). - let define_property: Function<'js> = ctx - .globals() - .get::<_, Object<'js>>("Object")? - .get("defineProperty")?; - for name in ["next", "return"] { - let value: Value<'js> = proto.get(name)?; - let desc = Object::new(ctx.clone())?; - desc.set("value", value)?; - desc.set("writable", true)?; - desc.set("enumerable", true)?; - desc.set("configurable", true)?; - define_property.call::<_, ()>((proto.clone(), name, desc))?; - } - Ok(Some(proto)) - } - fn constructor(ctx: &Ctx<'js>) -> Result>> { - use rquickjs::class::impl_::ConstructorCreator; - let implementor = rquickjs::class::impl_::ConstructorCreate::::new(); - (&implementor).create_constructor(ctx) - } -} -impl<'js> IntoJs<'js> for ReadableStreamAsyncIterator<'js> { - fn into_js(self, ctx: &Ctx<'js>) -> Result> { - let cls = Class::::instance(ctx.clone(), self)?; - IntoJs::into_js(cls, ctx) - } -} - -impl<'js> FromJs<'js> for ReadableStreamAsyncIterator<'js> -where - for<'a> CloneWrapper<'a, Self>: CloneTrait, -{ - fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { - use rquickjs::class::impl_::CloneTrait; - let value = Class::::from_js(ctx, value)?; - let borrow = value.try_borrow()?; - Ok(CloneWrapper(&*borrow).wrap_clone()) - } -} - -#[methods] -impl<'js> ReadableStreamAsyncIterator<'js> { - fn next(ctx: Ctx<'js>, iterator: This>) -> Result> { - let is_finished = iterator.is_finished.clone(); - - let next_steps = move |ctx: Ctx<'js>, iterator: &Self, iterator_class: Class<'js, Self>| { - if is_finished.load(Ordering::Acquire) { - return promise_resolved_with( - &ctx, - &iterator.promise_primordials, - Ok(ReadableStreamReadResult { - value: None, - done: true, - } - .into_js(&ctx)?), - ); - } - - let next_promise = Self::next_steps(&ctx, iterator)?; - - upon_promise( - ctx, - next_promise, - move |ctx, result: std::result::Result, _>| { - let mut iterator = OwnedBorrowMut::from_class(iterator_class); - match result { - Ok(next) => { - iterator.ongoing_promise = None; - if next.as_symbol() == Some(&iterator.end_of_iteration) { - iterator.is_finished.store(true, Ordering::Release); - Ok(ReadableStreamReadResult { - value: None, - done: true, - }) - } else { - Ok(ReadableStreamReadResult { - value: Some(next), - done: false, - }) - } - } - Err(reason) => { - iterator.ongoing_promise = None; - iterator.is_finished.store(true, Ordering::Release); - Err(ctx.throw(reason)) - } - } - }, - ) - }; - - let (iterator_class, mut iterator) = class_from_owned_borrow_mut(iterator.0); - let ongoing_promise = iterator.ongoing_promise.take(); - - let ongoing_promise = match ongoing_promise { - Some(ongoing_promise) => upon_promise( - ctx, - ongoing_promise, - move |ctx, _: std::result::Result, _>| { - let iterator = OwnedBorrow::from_class(iterator_class.clone()); - next_steps(ctx, &iterator, iterator_class) - }, - )?, - None => next_steps(ctx, &iterator, iterator_class)?, - }; - - Ok(iterator.ongoing_promise.insert(ongoing_promise).clone()) - } - - #[qjs(rename = "return")] - fn r#return( - ctx: Ctx<'js>, - iterator: This>, - value: Opt>, - ) -> Result> { - let is_finished = iterator.is_finished.clone(); - let value = value.0.unwrap_or_undefined(&ctx); - - let return_steps = { - let value = value.clone(); - move |ctx: Ctx<'js>, iterator: &Self| { - if is_finished.swap(true, Ordering::AcqRel) { - return promise_resolved_with( - &ctx, - &iterator.promise_primordials, - Ok(ReadableStreamReadResult { - value: Some(value), - done: true, - } - .into_js(&ctx)?), - ); - } - - Self::return_steps(ctx.clone(), iterator, value) - } - }; - - let (iterator_class, mut iterator) = class_from_owned_borrow_mut(iterator.0); - let ongoing_promise = iterator.ongoing_promise.take(); - - let ongoing_promise = match ongoing_promise { - Some(ongoing_promise) => upon_promise( - ctx.clone(), - ongoing_promise, - move |ctx, _: std::result::Result, _>| { - let iterator = OwnedBorrow::from_class(iterator_class.clone()); - return_steps(ctx, &iterator) - }, - )?, - None => return_steps(ctx.clone(), &iterator)?, - }; - - iterator.ongoing_promise = Some(ongoing_promise.clone()); - - upon_promise_fulfilment(ctx, ongoing_promise, move |_, ()| { - Ok(ReadableStreamReadResult { - value: Some(value), - done: true, - }) - }) - } -} - -impl<'js> ReadableStreamAsyncIterator<'js> { - // The get the next iteration result steps for a ReadableStream, given stream and iterator, are: - fn next_steps(ctx: &Ctx<'js>, iterator: &Self) -> Result> { - // Let reader be iterator’s reader. - let objects = iterator.objects.clone(); - - // Let promise be a new promise. - let promise = ResolveablePromise::new(ctx)?; - - // Let readRequest be a new read request with the following items: - #[derive(Trace)] - struct ReadRequest<'js> { - promise: ResolveablePromise<'js>, - end_of_iteration: Symbol<'js>, - } - - impl<'js> ReadableStreamReadRequest<'js> for ReadRequest<'js> { - fn chunk_steps( - &self, - objects: ReadableStreamDefaultReaderObjects<'js>, - chunk: Value<'js>, - ) -> Result> { - // Resolve promise with chunk. - self.promise.resolve(chunk)?; - Ok(objects) - } - - fn close_steps( - &self, - _ctx: &Ctx<'js>, - mut objects: ReadableStreamDefaultReaderObjects<'js>, - ) -> Result> { - // Perform ! ReadableStreamDefaultReaderRelease(reader). - objects = - ReadableStreamDefaultReader::readable_stream_default_reader_release(objects)?; - - // Resolve promise with end of iteration. - self.promise.resolve(self.end_of_iteration.clone())?; - Ok(objects) - } - - fn error_steps( - &self, - mut objects: ReadableStreamDefaultReaderObjects<'js>, - reason: Value<'js>, - ) -> Result> { - // Perform ! ReadableStreamDefaultReaderRelease(reader). - objects = - ReadableStreamDefaultReader::readable_stream_default_reader_release(objects)?; - - // Reject promise with e. - self.promise.reject(reason)?; - Ok(objects) - } - } - - let objects = ReadableStreamObjects::from_class(objects); - - // Perform ! ReadableStreamDefaultReaderRead(this, readRequest). - ReadableStreamDefaultReader::readable_stream_default_reader_read( - ctx, - objects, - ReadRequest { - promise: promise.clone(), - end_of_iteration: iterator.end_of_iteration.clone(), - }, - )?; - - // Return promise. - Ok(promise.promise) - } - - // The asynchronous iterator return steps for a ReadableStream, given stream, iterator, and arg, are: - fn return_steps(ctx: Ctx<'js>, iterator: &Self, arg: Value<'js>) -> Result> { - // Let reader be iterator’s reader. - let objects = ReadableStreamObjects::from_class(iterator.objects.clone()); - - // If iterator’s prevent cancel is false: - if !iterator.prevent_cancel { - // Let result be ! ReadableStreamReaderGenericCancel(reader, arg). - let (result, objects) = - ReadableStreamGenericReader::readable_stream_reader_generic_cancel( - ctx.clone(), - objects, - arg, - )?; - - // Perform ! ReadableStreamDefaultReaderRelease(reader). - ReadableStreamDefaultReader::readable_stream_default_reader_release(objects)?; - - // Return result. - return Ok(result); - } - - // Perform ! ReadableStreamDefaultReaderRelease(reader). - ReadableStreamDefaultReader::readable_stream_default_reader_release(objects)?; - - // Return a promise resolved with undefined. - Ok(iterator - .promise_primordials - .promise_resolved_with_undefined - .clone()) - } -} - -#[derive(Clone, JsLifetime, Trace)] -pub(crate) struct IteratorPrimordials<'js> { - end_of_iteration: Symbol<'js>, - sync_to_async_iterator: Function<'js>, - async_iterator_prototype: Object<'js>, -} - -impl<'js> Primordial<'js> for IteratorPrimordials<'js> { - fn new(ctx: &Ctx<'js>) -> Result - where - Self: Sized, - { - let sync_to_async_iterator = ctx.eval::, _>( - r#" - (syncIterable) => (async function* () { - return yield* syncIterable; - })() - "#, - )?; - - // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncIterator - // ```js - // const AsyncIteratorPrototype = Object.getPrototypeOf( - // Object.getPrototypeOf(Object.getPrototypeOf((async function* () {})())), - // ); - // ``` - let async_iterator_prototype = ctx - .eval::, _>("(async function* () {})()")? - .get_prototype() - .as_ref() - .and_then(Object::get_prototype) - .as_ref() - .and_then(Object::get_prototype) - .expect("async iterator prototype not found"); - - Ok(Self { - end_of_iteration: Symbol::new_global(ctx.clone(), "async iterator end of iteration")?, - sync_to_async_iterator, - async_iterator_prototype, - }) - } -} - -// https://tc39.es/ecma262/multipage/abstract-operations.html#sec-getmethod -fn get_method<'js>( - ctx: &Ctx<'js>, - value: Value<'js>, - property: impl IntoAtom<'js>, -) -> Result>> { - // 1. Let func be ? GetV(V, P). - let func = get_v(ctx, value, property)?; - - // 2. If func is either undefined or null, return undefined. - if func.is_undefined() || func.is_null() { - return Ok(None); - } - - match func.into_function() { - // 3. If IsCallable(func) is false, throw a TypeError exception. - None => Err(Exception::throw_type(ctx, "not a function")), - // 4. Return func. - Some(func) => Ok(Some(func)), - } -} - -// https://tc39.es/ecma262/multipage/abstract-operations.html#sec-getv -fn get_v<'js>( - ctx: &Ctx<'js>, - value: Value<'js>, - property: impl IntoAtom<'js>, -) -> Result> { - // 1. Let O be ? ToObject(V). - let o: Object<'js> = to_object(ctx, value)?; - - // 2. Return ? O.[[Get]](P, V). - o.get(property) -} - -// https://tc39.es/ecma262/multipage/abstract-operations.html#sec-toobject -fn to_object<'js>(ctx: &Ctx<'js>, value: Value<'js>) -> Result> { - let base_primordials = BasePrimordials::get(ctx)?; - - match value.type_of() { - // Return a new Boolean object whose [[BooleanData]] internal slot is set to argument - Type::Bool => base_primordials.constructor_bool.construct((value,))?, - // Return a new Number object whose [[NumberData]] internal slot is set to argument - Type::Int | Type::Float => base_primordials.constructor_number.construct((value,))?, - // Return a new String object whose [[StringData]] internal slot is set to argument - Type::String => base_primordials.constructor_string.construct((value,))?, - // Return a new Symbol object whose [[SymbolData]] internal slot is set to argument - // `new Symbol` is invalid but we can use `Object(symbol) - Type::Symbol => base_primordials.constructor_object.call((value,))?, - // Return a new BigInt object whose [[BigIntData]] internal slot is set to argument - // `new BigInt` is invalid but we can use `Object(bigInt) - Type::BigInt => base_primordials.constructor_object.call((value,))?, - // Return argument - typ if typ.interpretable_as(Type::Object) => Ok(value.into_object().unwrap()), - // Throw a TypeError exception. - typ => Err(Exception::throw_type( - ctx, - &format!("{typ} cannot be converted to an object"), - )), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::llrt_test::test_sync_with; - use rquickjs::BigInt; - - #[tokio::test] - async fn test_to_object() { - test_sync_with(|ctx| { - BasePrimordials::init(&ctx)?; - let good_values: [Value; 7] = [ - Value::new_bool(ctx.clone(), false), - Value::new_int(ctx.clone(), 123), - Value::new_float(ctx.clone(), 1.5), - rquickjs::String::from_str(ctx.clone(), "abc")?.into_value(), - Symbol::new_global(ctx.clone(), "def")?.into_value(), - BigInt::from_i64(ctx.clone(), 123456)?.into_value(), - Object::new(ctx.clone())?.into_value(), - ]; - - for value in good_values { - to_object(&ctx, value)?; - } - - let bad_values: [Value; 3] = [ - Value::new_uninitialized(ctx.clone()), - Value::new_undefined(ctx.clone()), - Value::new_null(ctx.clone()), - ]; - - for value in bad_values { - let ty = value.type_of(); - if to_object(&ctx, value).is_ok() { - panic!("Values of type {ty} should not be convertible to object") - } - } - - Ok(()) - }) - .await; - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/mod.rs b/stdlib/src/llrt/llrt_stream_web/readable/mod.rs deleted file mode 100644 index aefc0078..00000000 --- a/stdlib/src/llrt/llrt_stream_web/readable/mod.rs +++ /dev/null @@ -1,31 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -mod byob_reader; -mod byte_controller; -mod controller; -mod default_controller; -mod default_reader; -mod iterator; -mod objects; -mod reader; -pub mod stream; - -pub(crate) use byob_reader::{ArrayConstructorPrimordials, ReadableStreamBYOBReader}; -pub use byte_controller::ReadableByteStreamController; -pub(crate) use byte_controller::ReadableStreamBYOBRequest; -pub use byte_controller::{ - readable_byte_stream_controller_close_stream, readable_byte_stream_controller_enqueue_bytes, - readable_byte_stream_controller_enqueue_bytes_borrowed, ReadableByteStreamControllerClass, -}; -pub(crate) use default_controller::ReadableStreamDefaultController; -pub use default_controller::{ - readable_stream_default_controller_close_stream, - readable_stream_default_controller_enqueue_value, - readable_stream_default_controller_error_stream, NativePull, NativePullFn, NativePullResult, - ReadableStreamDefaultControllerClass, -}; -pub(crate) use default_reader::ReadableStreamDefaultReader; -pub(crate) use iterator::IteratorPrimordials; -pub(crate) use stream::ReadableStreamClass; - -pub use controller::ReadableStreamControllerClass; -pub use stream::{CancelAlgorithm, PullAlgorithm, StartAlgorithm}; diff --git a/stdlib/src/llrt/llrt_stream_web/readable/objects.rs b/stdlib/src/llrt/llrt_stream_web/readable/objects.rs deleted file mode 100644 index 9b626f7c..00000000 --- a/stdlib/src/llrt/llrt_stream_web/readable/objects.rs +++ /dev/null @@ -1,459 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use rquickjs::{ - class::{OwnedBorrowMut, Trace, Tracer}, - Result, -}; - -use crate::llrt_stream_web::readable::{ - byob_reader::ReadableStreamBYOBReaderOwned, - byte_controller::ReadableByteStreamControllerOwned, - controller::{ - ReadableStreamController, ReadableStreamControllerClass, ReadableStreamControllerOwned, - }, - default_controller::ReadableStreamDefaultControllerOwned, - default_reader::{ReadableStreamDefaultReaderOrUndefined, ReadableStreamDefaultReaderOwned}, - reader::{ReadableStreamReader, ReadableStreamReaderOwned, UndefinedReader}, - stream::{ReadableStream, ReadableStreamClass, ReadableStreamOwned}, -}; - -pub(crate) struct ReadableStreamObjects<'js, C, R> { - pub(super) stream: ReadableStreamOwned<'js>, - pub(super) controller: C, - pub(super) reader: R, -} - -pub(super) type ReadableStreamDefaultControllerObjects<'js, R> = - ReadableStreamObjects<'js, ReadableStreamDefaultControllerOwned<'js>, R>; -pub(super) type ReadableStreamDefaultReaderObjects<'js, C = ReadableStreamControllerOwned<'js>> = - ReadableStreamObjects<'js, C, ReadableStreamDefaultReaderOwned<'js>>; -pub(super) type ReadableByteStreamObjects<'js, R> = - ReadableStreamObjects<'js, ReadableByteStreamControllerOwned<'js>, R>; -pub(super) type ReadableStreamBYOBObjects<'js> = ReadableStreamObjects< - 'js, - ReadableByteStreamControllerOwned<'js>, - ReadableStreamBYOBReaderOwned<'js>, ->; - -pub(crate) struct ReadableStreamClassObjects< - 'js, - C: ReadableStreamController<'js>, - R: ReadableStreamReader<'js>, -> { - pub(crate) stream: ReadableStreamClass<'js>, - pub(super) controller: C::Class, - pub(super) reader: R::Class, -} - -// derive(Clone) isn't clever enough to figure out that C and R don't need to implement Clone, but only C::Class and R::Class. -impl<'js, C: ReadableStreamController<'js>, R: ReadableStreamReader<'js>> Clone - for ReadableStreamClassObjects<'js, C, R> -{ - fn clone(&self) -> Self { - Self { - stream: self.stream.clone(), - controller: self.controller.clone(), - reader: self.reader.clone(), - } - } -} - -// derive(Trace) isn't clever enough to figure out that C and R don't need to implement Trace, but only C::Class and R::Class. -impl<'js, C: ReadableStreamController<'js>, R: ReadableStreamReader<'js>> Trace<'js> - for ReadableStreamClassObjects<'js, C, R> -{ - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - self.stream.trace(tracer); - self.controller.trace(tracer); - self.reader.trace(tracer); - } -} - -impl<'js, C: ReadableStreamController<'js>, R: ReadableStreamReader<'js>> - ReadableStreamClassObjects<'js, C, R> -{ - pub(super) fn set_reader>( - self, - reader: RNext::Class, - ) -> ReadableStreamClassObjects<'js, C, RNext> { - drop(self.reader); - ReadableStreamClassObjects { - stream: self.stream, - controller: self.controller, - reader, - } - } -} - -impl<'js, C: ReadableStreamController<'js>, R: ReadableStreamReader<'js>> - ReadableStreamObjects<'js, C, R> -{ - pub(super) fn with_assert_default_controller( - mut self, - f: impl FnOnce( - ReadableStreamDefaultControllerObjects<'js, R>, - ) -> Result>, - ) -> Result { - ((), self) = self.with_controller( - (), - |(), controller| Ok(((), f(controller)?)), - |_, _| panic!("expected default controller, found byte controller"), - )?; - Ok(self) - } - - pub(super) fn with_assert_byte_controller( - mut self, - f: impl FnOnce(ReadableByteStreamObjects<'js, R>) -> Result>, - ) -> Result { - ((), self) = self.with_controller( - (), - |_, _| panic!("expected byte controller, found default controller"), - |(), controller| Ok(((), f(controller)?)), - )?; - Ok(self) - } - - pub(super) fn with_controller( - self, - ctx: Ctx, - default: impl FnOnce( - Ctx, - ReadableStreamDefaultControllerObjects<'js, R>, - ) -> Result<(O, ReadableStreamDefaultControllerObjects<'js, R>)>, - byte: impl FnOnce( - Ctx, - ReadableByteStreamObjects<'js, R>, - ) -> Result<(O, ReadableByteStreamObjects<'js, R>)>, - ) -> Result<(O, Self)> { - let ((out, stream, reader), controller) = self.controller.with_controller( - (ctx, self.stream, self.reader), - |(ctx, stream, reader), controller| { - let (out, objects) = default( - ctx, - ReadableStreamObjects { - stream, - controller, - reader, - }, - )?; - - Ok(((out, objects.stream, objects.reader), objects.controller)) - }, - |(ctx, stream, reader), controller| { - let (out, objects) = byte( - ctx, - ReadableStreamObjects { - stream, - controller, - reader, - }, - )?; - - Ok(((out, objects.stream, objects.reader), objects.controller)) - }, - )?; - - Ok(( - out, - Self { - stream, - controller, - reader, - }, - )) - } - - pub(super) fn with_assert_byob_reader( - self, - f: impl FnOnce(ReadableStreamBYOBObjects<'js>) -> Result>, - ) -> Result { - self.with_reader( - |_| panic!("expected byob reader, found default reader"), - f, - |_| panic!("expected byob reader, found no reader"), - ) - } - - pub(super) fn with_assert_default_reader( - self, - f: impl FnOnce( - ReadableStreamDefaultReaderObjects<'js, C>, - ) -> Result>, - ) -> Result { - self.with_reader( - f, - |_| panic!("expected default reader, found byob reader"), - |_| panic!("expected default reader, found no reader"), - ) - } - - pub(super) fn with_reader( - mut self, - default: impl FnOnce( - ReadableStreamDefaultReaderObjects<'js, C>, - ) -> Result>, - byob: impl FnOnce(ReadableStreamBYOBObjects<'js>) -> Result>, - none: impl FnOnce( - ReadableStreamObjects<'js, C, UndefinedReader>, - ) -> Result>, - ) -> Result { - ((self.stream, self.controller), self.reader) = self.reader.with_reader( - (self.stream, self.controller), - |(stream, controller), reader| { - let objects = default(ReadableStreamObjects { - stream, - controller, - reader, - })?; - - Ok(((objects.stream, objects.controller), objects.reader)) - }, - |(mut stream, mut controller), mut reader| { - ((stream, reader), controller) = controller.with_controller( - (stream, reader), - |_, _| panic!("byob reader must have a byte controller"), - |(stream, reader), controller| { - let objects = byob(ReadableStreamObjects { - stream, - controller, - reader, - })?; - - Ok(((objects.stream, objects.reader), objects.controller)) - }, - )?; - - Ok(((stream, controller), reader)) - }, - |(stream, controller)| { - let objects = none(ReadableStreamObjects { - stream, - controller, - reader: UndefinedReader, - })?; - - Ok((objects.stream, objects.controller)) - }, - )?; - - Ok(self) - } - - pub(super) fn into_inner(self) -> ReadableStreamClassObjects<'js, C, R> { - ReadableStreamClassObjects { - stream: self.stream.into_inner(), - controller: self.controller.into_inner(), - reader: self.reader.into_inner(), - } - } - - pub(super) fn from_class(objects_class: ReadableStreamClassObjects<'js, C, R>) -> Self { - Self { - stream: OwnedBorrowMut::from_class(objects_class.stream), - controller: C::from_class(objects_class.controller), - reader: R::from_class(objects_class.reader), - } - } - - pub(super) fn from_class_no_reader( - objects_class: ReadableStreamClassObjects<'js, C, R>, - ) -> ReadableStreamObjects<'js, C, UndefinedReader> { - ReadableStreamObjects { - stream: OwnedBorrowMut::from_class(objects_class.stream), - controller: C::from_class(objects_class.controller), - reader: UndefinedReader, - } - } - - pub(super) fn clear_reader(self) -> ReadableStreamObjects<'js, C, UndefinedReader> { - drop(self.reader); - ReadableStreamObjects { - stream: self.stream, - controller: self.controller, - reader: UndefinedReader, - } - } -} - -impl<'js> - ReadableStreamDefaultControllerObjects<'js, Option>> -{ - pub(super) fn from_default_controller( - controller: ReadableStreamDefaultControllerOwned<'js>, - ) -> Self { - Self::new_default( - OwnedBorrowMut::from_class(controller.stream.clone()), - controller, - ) - } - - pub(super) fn new_default( - stream: ReadableStreamOwned<'js>, - controller: ReadableStreamDefaultControllerOwned<'js>, - ) -> Self { - ReadableStreamObjects { - stream, - controller, - reader: UndefinedReader, - } - .refresh_reader() - } -} - -impl<'js, R: ReadableStreamReader<'js>> ReadableStreamDefaultControllerObjects<'js, R> { - pub(super) fn refresh_reader( - mut self, - ) -> ReadableStreamDefaultControllerObjects<'js, Option>> - { - drop(self.reader); - let reader = self.stream.reader_mut(); - ReadableStreamObjects { - stream: self.stream, - controller: self.controller, - reader: ReadableStreamReader::try_from_erased(reader) - .expect("default controller must have default reader or no reader"), - } - } -} - -impl<'js> ReadableByteStreamObjects<'js, UndefinedReader> { - pub(super) fn from_byte_controller(controller: ReadableByteStreamControllerOwned<'js>) -> Self { - Self::new_byte( - OwnedBorrowMut::from_class(controller.stream.clone()), - controller, - ) - } - - pub(super) fn new_byte( - stream: ReadableStreamOwned<'js>, - controller: ReadableByteStreamControllerOwned<'js>, - ) -> Self { - ReadableStreamObjects { - stream, - controller, - reader: UndefinedReader, - } - } - - pub(super) fn set_reader>( - self, - reader: RNext, - ) -> ReadableByteStreamObjects<'js, RNext> { - ReadableStreamObjects { - stream: self.stream, - controller: self.controller, - reader, - } - } -} - -impl<'js> ReadableStreamBYOBObjects<'js> { - pub(super) fn from_byob_reader(reader: ReadableStreamBYOBReaderOwned<'js>) -> Self { - let stream = OwnedBorrowMut::from_class( - reader - .generic - .stream - .clone() - .expect("ReadableStreamBYOBReader must have a stream"), - ); - let controller = match &stream.controller { - ReadableStreamControllerClass::ReadableStreamByteController(c) => c.clone(), - _ => panic!("ReadableStreamBYOBReader stream must have byte controller"), - }; - Self { - stream, - controller: OwnedBorrowMut::from_class(controller), - reader, - } - } -} - -impl<'js, R: ReadableStreamReader<'js>> ReadableByteStreamObjects<'js, R> { - pub(super) fn refresh_reader( - mut self, - ) -> ReadableByteStreamObjects<'js, Option>> { - drop(self.reader); - let reader = self.stream.reader_mut(); - ReadableStreamObjects { - stream: self.stream, - controller: self.controller, - reader, - } - } -} - -impl<'js, C: ReadableStreamController<'js>, R: ReadableStreamDefaultReaderOrUndefined<'js>> - ReadableStreamObjects<'js, C, R> -{ - pub(super) fn with_some_reader( - self, - default: impl FnOnce( - ReadableStreamDefaultReaderObjects<'js, C>, - ) -> Result>, - none: impl FnOnce( - ReadableStreamObjects<'js, C, UndefinedReader>, - ) -> Result>, - ) -> Result { - self.with_reader( - default, - |_| panic!("byob reader cannot implement DefaultReaderOrUndefined"), - none, - ) - } -} - -impl<'js> ReadableStreamObjects<'js, ReadableStreamControllerOwned<'js>, UndefinedReader> { - pub(super) fn from_stream(stream: ReadableStreamOwned<'js>) -> Self { - let controller = ReadableStreamControllerOwned::from_class(stream.controller.clone()); - Self::new(stream, controller) - } - - fn new( - stream: OwnedBorrowMut<'js, ReadableStream<'js>>, - controller: ReadableStreamControllerOwned<'js>, - ) -> Self { - ReadableStreamObjects { - stream, - controller, - reader: UndefinedReader, - } - } -} - -impl<'js, R: ReadableStreamReader<'js>> - ReadableStreamObjects<'js, ReadableStreamControllerOwned<'js>, R> -{ - pub(super) fn refresh_reader( - mut self, - ) -> ReadableStreamObjects< - 'js, - ReadableStreamControllerOwned<'js>, - Option>, - > { - drop(self.reader); - let reader = self.stream.reader_mut(); - ReadableStreamObjects { - stream: self.stream, - controller: self.controller, - reader, - } - } -} - -impl<'js> ReadableStreamDefaultReaderObjects<'js> { - pub(super) fn from_default_reader(reader: ReadableStreamDefaultReaderOwned<'js>) -> Self { - let stream = OwnedBorrowMut::from_class( - reader - .generic - .stream - .clone() - .expect("ReadableStreamDefaultReader must have a stream"), - ); - let controller = ReadableStreamControllerOwned::from_class(stream.controller.clone()); - Self { - stream, - controller, - reader, - } - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/reader.rs b/stdlib/src/llrt/llrt_stream_web/readable/reader.rs deleted file mode 100644 index 87d34c1f..00000000 --- a/stdlib/src/llrt/llrt_stream_web/readable/reader.rs +++ /dev/null @@ -1,404 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use rquickjs::{ - class::{OwnedBorrowMut, Trace, Tracer}, - function::Constructor, - Ctx, Error, FromJs, Function, IntoJs, JsLifetime, Promise, Result, Value, -}; - -use crate::llrt_stream_web::{ - readable::{ - byob_reader::ReadableStreamBYOBReader, - byob_reader::{ReadableStreamBYOBReaderClass, ReadableStreamBYOBReaderOwned}, - controller::ReadableStreamController, - default_reader::{ - ReadableStreamDefaultReader, ReadableStreamDefaultReaderClass, - ReadableStreamDefaultReaderOwned, - }, - objects::ReadableStreamObjects, - stream::{ReadableStream, ReadableStreamClass, ReadableStreamOwned, ReadableStreamState}, - }, - utils::promise::{PromisePrimordials, ResolveablePromise}, -}; - -pub(crate) trait ReadableStreamReader<'js>: Sized + 'js { - type Class: Clone + Trace<'js>; - - fn with_reader( - self, - ctx: C, - default: impl FnOnce( - C, - ReadableStreamDefaultReaderOwned<'js>, - ) -> Result<(C, ReadableStreamDefaultReaderOwned<'js>)>, - byob: impl FnOnce( - C, - ReadableStreamBYOBReaderOwned<'js>, - ) -> Result<(C, ReadableStreamBYOBReaderOwned<'js>)>, - none: impl FnOnce(C) -> Result, - ) -> Result<(C, Self)>; - - fn into_inner(self) -> Self::Class; - - fn from_class(class: Self::Class) -> Self; - - fn try_from_erased(erased: Option>) -> Option; -} - -// typedef (ReadableStreamDefaultController or ReadableByteStreamController) ReadableStreamController; -#[derive(JsLifetime, Clone, PartialEq, Eq)] -pub(crate) enum ReadableStreamReaderClass<'js> { - ReadableStreamDefaultReader(ReadableStreamDefaultReaderClass<'js>), - ReadableStreamBYOBReader(ReadableStreamBYOBReaderClass<'js>), -} - -impl<'js> ReadableStreamReaderClass<'js> { - pub(super) fn closed_promise(&self) -> Promise<'js> { - match self { - Self::ReadableStreamDefaultReader(r) => { - r.borrow().generic.closed_promise.promise.clone() - } - Self::ReadableStreamBYOBReader(r) => r.borrow().generic.closed_promise.promise.clone(), - } - } -} - -impl<'js> From> for ReadableStreamReaderClass<'js> { - fn from(value: ReadableStreamDefaultReaderClass<'js>) -> Self { - Self::ReadableStreamDefaultReader(value) - } -} - -impl<'js> From> for ReadableStreamReaderClass<'js> { - fn from(value: ReadableStreamBYOBReaderClass<'js>) -> Self { - Self::ReadableStreamBYOBReader(value) - } -} - -pub(crate) enum ReadableStreamReaderOwned<'js> { - ReadableStreamDefaultReader(ReadableStreamDefaultReaderOwned<'js>), - ReadableStreamBYOBReader(ReadableStreamBYOBReaderOwned<'js>), -} - -impl<'js> ReadableStreamReader<'js> for ReadableStreamReaderOwned<'js> { - type Class = ReadableStreamReaderClass<'js>; - - fn with_reader( - self, - ctx: C, - default: impl FnOnce( - C, - ReadableStreamDefaultReaderOwned<'js>, - ) -> Result<(C, ReadableStreamDefaultReaderOwned<'js>)>, - byob: impl FnOnce( - C, - ReadableStreamBYOBReaderOwned<'js>, - ) -> Result<(C, ReadableStreamBYOBReaderOwned<'js>)>, - _: impl FnOnce(C) -> Result, - ) -> Result<(C, Self)> { - match self { - Self::ReadableStreamDefaultReader(r) => { - let (ctx, r) = default(ctx, r)?; - Ok((ctx, Self::ReadableStreamDefaultReader(r))) - } - Self::ReadableStreamBYOBReader(r) => { - let (ctx, r) = byob(ctx, r)?; - Ok((ctx, Self::ReadableStreamBYOBReader(r))) - } - } - } - - fn into_inner(self) -> Self::Class { - match self { - ReadableStreamReaderOwned::ReadableStreamDefaultReader(r) => { - ReadableStreamReaderClass::ReadableStreamDefaultReader(r.into_inner()) - } - ReadableStreamReaderOwned::ReadableStreamBYOBReader(r) => { - ReadableStreamReaderClass::ReadableStreamBYOBReader(r.into_inner()) - } - } - } - - fn from_class(class: Self::Class) -> Self { - match class { - ReadableStreamReaderClass::ReadableStreamDefaultReader(r) => { - Self::ReadableStreamDefaultReader(OwnedBorrowMut::from_class(r)) - } - ReadableStreamReaderClass::ReadableStreamBYOBReader(r) => { - Self::ReadableStreamBYOBReader(OwnedBorrowMut::from_class(r)) - } - } - } - - fn try_from_erased(erased: Option>) -> Option { - erased - } -} - -impl<'js, T: ReadableStreamReader<'js>> ReadableStreamReader<'js> for Option { - type Class = Option<>::Class>; - - fn with_reader( - self, - mut ctx: C, - default: impl FnOnce( - C, - ReadableStreamDefaultReaderOwned<'js>, - ) -> Result<(C, ReadableStreamDefaultReaderOwned<'js>)>, - byob: impl FnOnce( - C, - ReadableStreamBYOBReaderOwned<'js>, - ) -> Result<(C, ReadableStreamBYOBReaderOwned<'js>)>, - none: impl FnOnce(C) -> Result, - ) -> Result<(C, Self)> { - match self { - Some(mut reader) => { - (ctx, reader) = reader.with_reader(ctx, default, byob, none)?; - Ok((ctx, Some(reader))) - } - None => Ok((none(ctx)?, None)), - } - } - - fn into_inner(self) -> Self::Class { - self.map(ReadableStreamReader::into_inner) - } - - fn from_class(class: Self::Class) -> Self { - class.map(ReadableStreamReader::from_class) - } - - fn try_from_erased(erased: Option>) -> Option { - match erased { - Some(r) => Some(Some(T::try_from_erased(Some(r))?)), - None => Some(None), - } - } -} - -#[derive(Clone, Trace)] -pub(crate) struct UndefinedReader; - -impl<'js> ReadableStreamReader<'js> for UndefinedReader { - type Class = UndefinedReader; - - fn with_reader( - self, - ctx: C, - _: impl FnOnce( - C, - ReadableStreamDefaultReaderOwned<'js>, - ) -> Result<(C, ReadableStreamDefaultReaderOwned<'js>)>, - _: impl FnOnce( - C, - ReadableStreamBYOBReaderOwned<'js>, - ) -> Result<(C, ReadableStreamBYOBReaderOwned<'js>)>, - none: impl FnOnce(C) -> Result, - ) -> Result<(C, Self)> { - Ok((none(ctx)?, self)) - } - - fn into_inner(self) -> Self::Class { - UndefinedReader - } - - fn from_class(_: Self::Class) -> Self { - UndefinedReader - } - - fn try_from_erased(erased: Option>) -> Option { - match erased { - None => Some(UndefinedReader), - _ => None, - } - } -} - -impl<'js> From> for ReadableStreamReaderOwned<'js> { - fn from(value: ReadableStreamDefaultReaderOwned<'js>) -> Self { - Self::ReadableStreamDefaultReader(value) - } -} - -impl<'js> From> for ReadableStreamReaderOwned<'js> { - fn from(value: ReadableStreamBYOBReaderOwned<'js>) -> Self { - Self::ReadableStreamBYOBReader(value) - } -} - -#[derive(JsLifetime)] -pub struct ReadableStreamGenericReader<'js> { - pub(super) closed_promise: ResolveablePromise<'js>, - pub(super) stream: Option>, - pub(super) promise_primordials: PromisePrimordials<'js>, - pub(super) constructor_type_error: Constructor<'js>, - pub(super) constructor_range_error: Constructor<'js>, - pub(super) function_array_buffer_is_view: Function<'js>, -} - -impl<'js> Trace<'js> for ReadableStreamGenericReader<'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - self.closed_promise.trace(tracer); - self.stream.trace(tracer); - self.promise_primordials.trace(tracer); - // Constructor and Function are persistent - trace their underlying values - self.constructor_type_error.as_value().trace(tracer); - self.constructor_range_error.as_value().trace(tracer); - self.function_array_buffer_is_view.as_value().trace(tracer); - } -} - -impl<'js> ReadableStreamGenericReader<'js> { - pub(super) fn readable_stream_reader_generic_initialize( - ctx: &Ctx<'js>, - stream: OwnedBorrowMut<'js, ReadableStream<'js>>, - ) -> Result { - let closed_promise = match stream.state { - // If stream.[[state]] is "readable", - ReadableStreamState::Readable => { - // Set reader.[[closedPromise]] to a new promise. - ResolveablePromise::new(ctx)? - } - // Otherwise, if stream.[[state]] is "closed", - ReadableStreamState::Closed => { - // Set reader.[[closedPromise]] to a promise resolved with undefined. - ResolveablePromise::resolved_with_undefined(&stream.promise_primordials) - } - // Otherwise, - ReadableStreamState::Errored(ref stored_error) => { - // Set reader.[[closedPromise]] to a promise rejected with stream.[[storedError]]. - let promise = ResolveablePromise::rejected_with( - &stream.promise_primordials, - stored_error.clone(), - )?; - - // Set reader.[[closedPromise]].[[PromiseIsHandled]] to true. - promise.set_is_handled()?; - - promise - } - }; - - let promise_primordials = stream.promise_primordials.clone(); - let constructor_type_error = stream.constructor_type_error.clone(); - let constructor_range_error = stream.constructor_range_error.clone(); - let function_array_buffer_is_view = stream.function_array_buffer_is_view.clone(); - - Ok(Self { - // Set reader.[[stream]] to stream. - stream: Some(stream.into_inner()), - closed_promise, - promise_primordials, - constructor_type_error, - constructor_range_error, - function_array_buffer_is_view, - }) - } - - pub(super) fn readable_stream_reader_generic_release( - &mut self, - - stream: &mut ReadableStream<'js>, - controller_release_steps: impl FnOnce(), - ) -> Result<()> { - // Let stream be reader.[[stream]]. - // Assert: stream is not undefined. - - // If stream.[[state]] is "readable", reject reader.[[closedPromise]] with a TypeError exception. - if let ReadableStreamState::Readable = stream.state { - self.closed_promise.reject_with_constructor( - &stream.constructor_type_error, - "Reader was released and can no longer be used to monitor the stream's closedness", - )?; - } else { - // Otherwise, set reader.[[closedPromise]] to a promise rejected with a TypeError exception. - self.closed_promise = ResolveablePromise::rejected_with_constructor( - &stream.promise_primordials, - &stream.constructor_type_error, - "Reader was released and can no longer be used to monitor the stream's closedness", - )?; - } - - // Set reader.[[closedPromise]].[[PromiseIsHandled]] to true. - self.closed_promise.set_is_handled()?; - - // Perform ! stream.[[controller]].[[ReleaseSteps]](). - controller_release_steps(); - - // Set stream.[[reader]] to undefined. - stream.reader = None; - - // Set reader.[[stream]] to undefined. - self.stream = None; - - Ok(()) - } - - pub(super) fn readable_stream_reader_generic_cancel< - C: ReadableStreamController<'js>, - R: ReadableStreamReader<'js>, - >( - ctx: Ctx<'js>, - // Let stream be reader.[[stream]]. - objects: ReadableStreamObjects<'js, C, R>, - reason: Value<'js>, - ) -> Result<(Promise<'js>, ReadableStreamObjects<'js, C, R>)> { - // Return ! ReadableStreamCancel(stream, reason). - ReadableStream::readable_stream_cancel(ctx, objects, reason) - } -} - -impl<'js> ReadableStreamReaderClass<'js> { - pub fn acquire_readable_stream_default_reader( - ctx: Ctx<'js>, - stream: ReadableStreamOwned<'js>, - ) -> Result<( - ReadableStreamOwned<'js>, - ReadableStreamDefaultReaderClass<'js>, - )> { - ReadableStreamDefaultReader::set_up_readable_stream_default_reader(&ctx, stream) - } - - pub(super) fn acquire_readable_stream_byob_reader( - ctx: Ctx<'js>, - stream: ReadableStreamOwned<'js>, - ) -> Result<(ReadableStreamOwned<'js>, ReadableStreamBYOBReaderClass<'js>)> { - ReadableStreamBYOBReader::set_up_readable_stream_byob_reader(ctx, stream) - } -} - -impl<'js> IntoJs<'js> for ReadableStreamReaderClass<'js> { - fn into_js(self, ctx: &Ctx<'js>) -> Result> { - match self { - Self::ReadableStreamDefaultReader(r) => r.into_js(ctx), - Self::ReadableStreamBYOBReader(r) => r.into_js(ctx), - } - } -} - -impl<'js> Trace<'js> for ReadableStreamReaderClass<'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - match self { - Self::ReadableStreamDefaultReader(r) => r.trace(tracer), - Self::ReadableStreamBYOBReader(r) => r.trace(tracer), - } - } -} - -impl<'js> FromJs<'js> for ReadableStreamReaderClass<'js> { - fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let ty_name = value.type_name(); - let obj = value - .as_object() - .ok_or(Error::new_from_js(ty_name, "Object"))?; - - if let Ok(default) = obj.into_class() { - return Ok(Self::ReadableStreamDefaultReader(default)); - } - - if let Ok(default) = obj.into_class() { - return Ok(Self::ReadableStreamBYOBReader(default)); - } - - Err(Error::new_from_js(ty_name, "ReadableStreamReader")) - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/stream/algorithms.rs b/stdlib/src/llrt/llrt_stream_web/readable/stream/algorithms.rs deleted file mode 100644 index f677e4f4..00000000 --- a/stdlib/src/llrt/llrt_stream_web/readable/stream/algorithms.rs +++ /dev/null @@ -1,281 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use std::{cell::RefCell, rc::Rc}; - -use crate::llrt_utils::option::{Null, Undefined}; -use crate::llrt_utils::primordials::Primordial; -use rquickjs::{ - class::Trace, prelude::This, Class, Ctx, Function, JsLifetime, Object, Promise, Result, Value, -}; - -use crate::llrt_stream_web::{ - readable::controller::ReadableStreamControllerClass, - transform::{ - controller::TransformStreamDefaultControllerClass, - stream::{self as transform_stream, TransformStreamClass}, - }, - utils::promise::{promise_resolved_with, PromisePrimordials}, -}; - -use super::tee::TeeState; - -#[derive(Clone)] -pub enum StartAlgorithm<'js> { - ReturnUndefined, - Function { - f: Function<'js>, - underlying_source: Null>>, - }, -} - -impl<'js> StartAlgorithm<'js> { - pub(crate) fn call( - &self, - ctx: Ctx<'js>, - controller: ReadableStreamControllerClass<'js>, - ) -> Result> { - match self { - StartAlgorithm::ReturnUndefined => Ok(Value::new_undefined(ctx.clone())), - StartAlgorithm::Function { - f, - underlying_source, - } => f.call::<_, Value>((This(underlying_source.clone()), controller)), - } - } -} - -type PullRustFn<'js> = - Box, ReadableStreamControllerClass<'js>) -> Result> + 'js>; - -#[allow(private_interfaces)] -#[derive(Clone)] -pub enum PullAlgorithm<'js> { - ReturnPromiseUndefined, - Function { - f: Function<'js>, - underlying_source: Null>>, - }, - RustFunction(Rc>), - Tee(Class<'js, TeeState<'js>>), - Transform(TransformStreamClass<'js>), -} - -impl<'js> Trace<'js> for PullAlgorithm<'js> { - fn trace<'a>(&self, tracer: rquickjs::class::Tracer<'a, 'js>) { - match self { - Self::ReturnPromiseUndefined => {} - Self::Function { - f, - underlying_source, - } => { - f.trace(tracer); - underlying_source.trace(tracer); - } - Self::RustFunction(_) => {} - Self::Tee(state) => state.trace(tracer), - Self::Transform(stream) => stream.trace(tracer), - } - } -} - -unsafe impl<'js> JsLifetime<'js> for PullAlgorithm<'js> { - type Changed<'to> = PullAlgorithm<'to>; -} - -impl<'js> PullAlgorithm<'js> { - pub fn from_fn( - f: impl Fn(Ctx<'js>, ReadableStreamControllerClass<'js>) -> Result> + 'js, - ) -> Self { - Self::RustFunction(Rc::new(Box::new(f))) - } - - /// Wrap a one-shot pull closure. Subsequent invocations after the first - /// resolve with `undefined` without calling `f` again — useful for - /// streams that enqueue their whole payload in one go and then close. - pub fn from_fn_once( - f: impl FnOnce(Ctx<'js>, ReadableStreamControllerClass<'js>) -> Result> + 'js, - ) -> Self { - type OnceSlot<'js> = Rc< - RefCell< - Option< - Box< - dyn FnOnce( - Ctx<'js>, - ReadableStreamControllerClass<'js>, - ) -> Result> - + 'js, - >, - >, - >, - >; - let slot: OnceSlot<'js> = Rc::new(RefCell::new(Some(Box::new(f)))); - Self::from_fn(move |ctx, ctrl| { - if let Some(f) = slot.borrow_mut().take() { - f(ctx, ctrl) - } else { - Ok(PromisePrimordials::get(&ctx)? - .promise_resolved_with_undefined - .clone()) - } - }) - } - - pub(super) fn from_tee_state(state: Class<'js, TeeState<'js>>) -> Self { - Self::Tee(state) - } - - pub(crate) fn call( - &self, - ctx: Ctx<'js>, - promise_primordials: &PromisePrimordials<'js>, - controller: ReadableStreamControllerClass<'js>, - ) -> Result> { - match self { - PullAlgorithm::ReturnPromiseUndefined => { - Ok(promise_primordials.promise_resolved_with_undefined.clone()) - } - PullAlgorithm::Function { - f, - underlying_source, - } => promise_resolved_with( - &ctx, - promise_primordials, - f.call::<_, Value>((This(underlying_source.clone()), controller)), - ), - PullAlgorithm::RustFunction(f) => f(ctx, controller), - PullAlgorithm::Tee(state) => { - crate::llrt_stream_web::readable::stream::tee::tee_pull_algorithm( - ctx, - state.clone(), - ) - } - PullAlgorithm::Transform(stream) => { - transform_stream::source_pull_algorithm(ctx, stream) - } - } - } -} - -type CancelRustFn<'js> = Box) -> Result> + 'js>; - -#[allow(private_interfaces)] -pub enum CancelAlgorithm<'js> { - ReturnPromiseUndefined, - Function { - f: Function<'js>, - underlying_source: Null>>, - }, - RustFunction(Rc>>>), - Tee1(Class<'js, TeeState<'js>>), - Tee2(Class<'js, TeeState<'js>>), - Transform { - stream: TransformStreamClass<'js>, - controller: TransformStreamDefaultControllerClass<'js>, - }, -} - -impl<'js> Clone for CancelAlgorithm<'js> { - fn clone(&self) -> Self { - match self { - Self::ReturnPromiseUndefined => Self::ReturnPromiseUndefined, - Self::Function { - f, - underlying_source, - } => Self::Function { - f: f.clone(), - underlying_source: underlying_source.clone(), - }, - Self::RustFunction(rc) => Self::RustFunction(rc.clone()), - Self::Tee1(state) => Self::Tee1(state.clone()), - Self::Tee2(state) => Self::Tee2(state.clone()), - Self::Transform { stream, controller } => Self::Transform { - stream: stream.clone(), - controller: controller.clone(), - }, - } - } -} - -impl<'js> Trace<'js> for CancelAlgorithm<'js> { - fn trace<'a>(&self, tracer: rquickjs::class::Tracer<'a, 'js>) { - match self { - Self::ReturnPromiseUndefined => {} - Self::Function { - f, - underlying_source, - } => { - f.trace(tracer); - underlying_source.trace(tracer); - } - Self::RustFunction(_) => {} - Self::Tee1(state) | Self::Tee2(state) => state.trace(tracer), - Self::Transform { stream, controller } => { - stream.trace(tracer); - controller.trace(tracer); - } - } - } -} - -unsafe impl<'js> JsLifetime<'js> for CancelAlgorithm<'js> { - type Changed<'to> = CancelAlgorithm<'to>; -} - -impl<'js> CancelAlgorithm<'js> { - pub fn from_fn(f: impl FnOnce(Value<'js>) -> Result> + 'js) -> Self { - Self::RustFunction(Rc::new(RefCell::new(Some(Box::new(f))))) - } - - pub(super) fn from_tee_state_1(state: Class<'js, TeeState<'js>>) -> Self { - Self::Tee1(state) - } - - pub(super) fn from_tee_state_2(state: Class<'js, TeeState<'js>>) -> Self { - Self::Tee2(state) - } - - pub(crate) fn call( - &self, - ctx: Ctx<'js>, - promise_primordials: &PromisePrimordials<'js>, - reason: Value<'js>, - ) -> Result> { - match self { - CancelAlgorithm::ReturnPromiseUndefined => { - Ok(promise_primordials.promise_resolved_with_undefined.clone()) - } - CancelAlgorithm::Function { - f, - underlying_source, - } => { - let result: Result = f.call((This(underlying_source.clone()), reason)); - promise_resolved_with(&ctx, promise_primordials, result) - } - CancelAlgorithm::RustFunction(f) => { - let f = f - .borrow_mut() - .take() - .expect("cancel algorithm must only be called once"); - f(reason) - } - CancelAlgorithm::Tee1(state) => { - crate::llrt_stream_web::readable::stream::tee::tee_cancel_algorithm( - ctx, - state.clone(), - reason, - 0, - ) - } - CancelAlgorithm::Tee2(state) => { - crate::llrt_stream_web::readable::stream::tee::tee_cancel_algorithm( - ctx, - state.clone(), - reason, - 1, - ) - } - CancelAlgorithm::Transform { stream, controller } => { - transform_stream::source_cancel_algorithm(ctx, stream, controller, reason) - } - } - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/stream/mod.rs b/stdlib/src/llrt/llrt_stream_web/readable/stream/mod.rs deleted file mode 100644 index f447ac32..00000000 --- a/stdlib/src/llrt/llrt_stream_web/readable/stream/mod.rs +++ /dev/null @@ -1,1117 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use std::{cell::OnceCell, panic, rc::Rc}; - -use crate::llrt_stream_web::{ - queuing_strategy::{QueuingStrategy, SizeAlgorithm}, - readable::{ - byob_reader::{ReadableStreamBYOBReader, ReadableStreamReadIntoRequest, ViewBytes}, - byte_controller::{ReadableByteStreamController, ReadableByteStreamControllerClass}, - controller::{ReadableStreamController, ReadableStreamControllerClass}, - default_controller::{ - ReadableStreamDefaultController, ReadableStreamDefaultControllerOwned, - }, - default_reader::{ReadableStreamDefaultReader, ReadableStreamReadRequest}, - iterator::{IteratorKind, IteratorRecord, ReadableStreamAsyncIterator}, - objects::{ - ReadableStreamBYOBObjects, ReadableStreamClassObjects, - ReadableStreamDefaultReaderObjects, ReadableStreamObjects, - }, - reader::{ - ReadableStreamReader, ReadableStreamReaderClass, ReadableStreamReaderOwned, - UndefinedReader, - }, - }, - readable_writable_pair::ReadableWritablePair, - utils::{ - promise::{ - promise_rejected_catch, promise_rejected_with, promise_rejected_with_constructor, - promise_resolved_with, upon_promise_fulfilment, with_promise_result, - PromisePrimordials, - }, - UnwrapOrUndefined, ValueOrUndefined, - }, - writable::WritableStreamOwned, -}; - -use pipe::StreamPipeOptions; -use source::UnderlyingSource; - -use crate::llrt_utils::{ - option::{Null, NullableOpt, Undefined}, - primordials::{BasePrimordials, Primordial}, - result::ResultExt, -}; -pub use algorithms::{CancelAlgorithm, PullAlgorithm, StartAlgorithm}; -use rquickjs::{ - atom::PredefinedAtom, - class::{OwnedBorrowMut, Trace}, - function::Constructor, - prelude::{List, Opt, This}, - Class, Coerced, Ctx, Error, Exception, FromJs, Function, IntoJs, JsLifetime, Object, Promise, - Result, Value, -}; - -pub mod algorithms; -mod pipe; -pub(super) mod source; -mod tee; - -/// Acquire a default reader for the stream, locking it. Subsequent -/// `getReader()` calls from JS will throw per spec. -pub fn lock_readable_stream<'js>( - ctx: Ctx<'js>, - stream: Class<'js, ReadableStream<'js>>, -) -> Result<()> { - let owned = rquickjs::class::OwnedBorrowMut::from_class(stream); - super::reader::ReadableStreamReaderClass::acquire_readable_stream_default_reader(ctx, owned)?; - Ok(()) -} - -/// Fast-path drain for a default-controller ReadableStream whose queue holds -/// all the data synchronously (e.g. the stream was enqueued in `start()` and -/// then closed). Bypasses the JS reader + Promise machinery, so user code -/// that poisons `Object.prototype.then` cannot swap the streamed chunks -/// (WPT `response-stream-with-broken-then`). -/// -/// Returns `Some(chunks)` if the fast path applied, `None` otherwise (stream -/// locked, disturbed, has pending pull, byte controller, not yet closed, -/// etc). Sets `disturbed = true` on success. -pub fn try_sync_drain_closed_stream<'js>( - stream: &Class<'js, ReadableStream<'js>>, -) -> Option>> { - use super::controller::ReadableStreamControllerClass; - use super::default_controller::ReadableStreamDefaultController; - use super::stream::ReadableStreamState; - use rquickjs::class::OwnedBorrowMut; - - let mut stream_ref = stream.try_borrow_mut().ok()?; - if stream_ref.disturbed || stream_ref.is_readable_stream_locked() { - return None; - } - // Stream state must be Readable (not Errored). Closed would also be OK - // but then the queue should already be empty. - if !matches!(stream_ref.state, ReadableStreamState::Readable) { - return None; - } - let controller_class = match &stream_ref.controller { - ReadableStreamControllerClass::ReadableStreamDefaultController(c) => c.clone(), - _ => return None, - }; - let mut controller: OwnedBorrowMut<'js, ReadableStreamDefaultController<'js>> = - OwnedBorrowMut::try_from_class(controller_class).ok()?; - // Only fast-path when close has been requested — otherwise there could - // be more data coming via `pull()` that we'd miss. - if !controller.close_requested { - return None; - } - let mut chunks = Vec::with_capacity(controller.container.queue.len()); - while !controller.container.queue.is_empty() { - chunks.push(controller.container.dequeue_value()); - } - stream_ref.disturbed = true; - // Transition the stream to Closed now that its queue is drained, so that - // later consumers see a consistent state. - stream_ref.state = ReadableStreamState::Closed; - Some(chunks) -} - -/// Tee a ReadableStream into two branches. The stream must not be locked or disturbed. -pub fn tee_readable_stream<'js>( - ctx: Ctx<'js>, - stream: Class<'js, ReadableStream<'js>>, -) -> Result<( - Class<'js, ReadableStream<'js>>, - Class<'js, ReadableStream<'js>>, -)> { - { - let stream_ref = stream.borrow(); - if stream_ref.disturbed { - return Err(Exception::throw_type( - &ctx, - "Cannot tee a disturbed ReadableStream", - )); - } - if stream_ref.is_readable_stream_locked() { - return Err(Exception::throw_type( - &ctx, - "Cannot tee a locked ReadableStream", - )); - } - } - let owned = OwnedBorrowMut::from_class(stream); - let objects = ReadableStreamObjects::from_stream(owned); - ReadableStream::readable_stream_tee(ctx, objects) -} - -#[rquickjs::class] -#[derive(JsLifetime)] -pub struct ReadableStream<'js> { - pub controller: ReadableStreamControllerClass<'js>, - pub disturbed: bool, - pub state: ReadableStreamState<'js>, - pub(crate) reader: Option>, - pub(crate) promise_primordials: PromisePrimordials<'js>, - pub(crate) constructor_type_error: Constructor<'js>, - pub(crate) constructor_range_error: Constructor<'js>, - pub(crate) function_array_buffer_is_view: Function<'js>, -} - -impl<'js> Trace<'js> for ReadableStream<'js> { - fn trace<'a>(&self, tracer: rquickjs::class::Tracer<'a, 'js>) { - self.controller.trace(tracer); - self.state.trace(tracer); - self.reader.trace(tracer); - - self.promise_primordials.trace(tracer); - self.constructor_type_error.trace(tracer); - self.constructor_range_error.trace(tracer); - self.function_array_buffer_is_view.trace(tracer); - } -} - -pub(crate) type ReadableStreamClass<'js> = Class<'js, ReadableStream<'js>>; -pub(crate) type ReadableStreamOwned<'js> = OwnedBorrowMut<'js, ReadableStream<'js>>; - -#[derive(Debug, Trace, Clone, JsLifetime)] -pub enum ReadableStreamState<'js> { - Readable, - Closed, - Errored(Value<'js>), -} - -#[rquickjs::methods(rename_all = "camelCase")] -impl<'js> ReadableStream<'js> { - // Streams Spec: 4.2.4: https://streams.spec.whatwg.org/#rs-prototype - // constructor(optional object underlyingSource, optional QueuingStrategy strategy = {}); - #[qjs(constructor)] - fn new( - ctx: Ctx<'js>, - underlying_source: Opt>>, - queuing_strategy: Opt>>, - ) -> Result> { - // If underlyingSource is missing, set it to null. - let underlying_source = Null(underlying_source.0); - - // Let underlyingSourceDict be underlyingSource, converted to an IDL value of type UnderlyingSource. - let underlying_source_dict = match underlying_source { - Null(None) | Null(Some(Undefined(None))) => UnderlyingSource::default(), - Null(Some(Undefined(Some(ref obj)))) => UnderlyingSource::from_object(obj.clone())?, - }; - - let promise_primordials = PromisePrimordials::get(&ctx)?.clone(); - let base_primordials = BasePrimordials::get(&ctx)?; - - let stream_class = Class::instance( - ctx.clone(), - Self { - // Set stream.[[state]] to "readable". - state: ReadableStreamState::Readable, - // Set stream.[[reader]] and stream.[[storedError]] to undefined. - reader: None, - // Set stream.[[disturbed]] to false. - disturbed: false, - controller: ReadableStreamControllerClass::Uninitialised, - constructor_type_error: base_primordials.constructor_type_error.clone(), - constructor_range_error: base_primordials.constructor_range_error.clone(), - function_array_buffer_is_view: base_primordials - .function_array_buffer_is_view - .clone(), - promise_primordials, - }, - )?; - drop(base_primordials); - let stream = OwnedBorrowMut::from_class(stream_class.clone()); - let queuing_strategy = queuing_strategy.0.and_then(|qs| qs.0); - - match underlying_source_dict.r#type { - // If underlyingSourceDict["type"] is "bytes": - Some(ReadableStreamType::Bytes) => { - // If strategy["size"] exists, throw a RangeError exception. - if queuing_strategy - .as_ref() - .and_then(|qs| qs.size.as_ref()) - .is_some() - { - return Err(Exception::throw_range( - &ctx, - "The strategy for a byte stream cannot have a size function", - )); - } - // Let highWaterMark be ? ExtractHighWaterMark(strategy, 0). - let high_water_mark = - QueuingStrategy::extract_high_water_mark(&ctx, queuing_strategy, 0.0)?; - - // Perform ? SetUpReadableByteStreamControllerFromUnderlyingSource(this, underlyingSource, underlyingSourceDict, highWaterMark). - ReadableByteStreamController::set_up_readable_byte_stream_controller_from_underlying_source( - &ctx, - stream, - underlying_source, - underlying_source_dict, - high_water_mark, - )?; - } - // Otherwise (no type, or "owning" which we treat as a default - // controller that also accepts the `transfer` enqueue option): - None | Some(ReadableStreamType::Owning) => { - let is_owning_type = matches!( - underlying_source_dict.r#type, - Some(ReadableStreamType::Owning) - ); - // Let sizeAlgorithm be ! ExtractSizeAlgorithm(strategy). - let size_algorithm = - QueuingStrategy::extract_size_algorithm(queuing_strategy.as_ref()); - - // Let highWaterMark be ? ExtractHighWaterMark(strategy, 1). - let high_water_mark = - QueuingStrategy::extract_high_water_mark(&ctx, queuing_strategy, 1.0)?; - - // Perform ? SetUpReadableStreamDefaultControllerFromUnderlyingSource(this, underlyingSource, underlyingSourceDict, highWaterMark, sizeAlgorithm). - ReadableStreamDefaultController::set_up_readable_stream_default_controller_from_underlying_source( - ctx, - stream, - underlying_source, - underlying_source_dict, - high_water_mark, - size_algorithm, - is_owning_type, - )?; - } - } - - Ok(stream_class) - } - - // static ReadableStream from(any asyncIterable); - #[qjs(static)] - fn from(ctx: Ctx<'js>, async_iterable: Value<'js>) -> Result> { - // Return ? ReadableStreamFromIterable(asyncIterable). - Self::readable_stream_from_iterable(&ctx, async_iterable) - } - - // readonly attribute boolean locked; - #[qjs(get)] - fn locked(&self) -> bool { - // Return ! IsReadableStreamLocked(this). - self.is_readable_stream_locked() - } - - // Internal property for checking if stream has been read from - #[qjs(get)] - fn disturbed(&self) -> bool { - self.disturbed - } - - // Promise cancel(optional any reason); - fn cancel( - ctx: Ctx<'js>, - stream: This>, - reason: Opt>, - ) -> Result> { - // If ! IsReadableStreamLocked(this) is true, return a promise rejected with a TypeError exception. - if stream.is_readable_stream_locked() { - return promise_rejected_with_constructor( - &stream.constructor_type_error, - &stream.promise_primordials, - "Cannot cancel a stream that already has a reader", - ); - } - - let objects = ReadableStreamObjects::from_stream(stream.0).refresh_reader(); - - let (promise, _) = - Self::readable_stream_cancel(ctx.clone(), objects, reason.0.unwrap_or_undefined(&ctx))?; - Ok(promise) - } - - // ReadableStreamReader getReader(optional ReadableStreamGetReaderOptions options = {}); - fn get_reader( - ctx: Ctx<'js>, - stream: This>, - options: Opt>, - ) -> Result> { - // If options["mode"] does not exist, return ? AcquireReadableStreamDefaultReader(this). - let reader = match options.0 { - None | Some(None | Some(ReadableStreamGetReaderOptions { mode: None })) => { - let (_, reader) = - ReadableStreamReaderClass::acquire_readable_stream_default_reader( - ctx.clone(), - stream.0, - )?; - reader.into() - } - // Return ? AcquireReadableStreamBYOBReader(this). - Some(Some(ReadableStreamGetReaderOptions { - mode: Some(ReadableStreamReaderMode::Byob), - })) => { - let (_, reader) = ReadableStreamReaderClass::acquire_readable_stream_byob_reader( - ctx.clone(), - stream.0, - )?; - reader.into() - } - }; - - Ok(reader) - } - - // ReadableStream pipeThrough(ReadableWritablePair transform, optional StreamPipeOptions options = {}); - fn pipe_through( - ctx: Ctx<'js>, - stream: This>, - transform: ReadableWritablePair<'js>, - options: NullableOpt>, - ) -> Result> { - // If ! IsReadableStreamLocked(this) is true, throw a TypeError exception. - if stream.is_readable_stream_locked() { - return Err(Exception::throw_type( - &ctx, - "ReadableStream.prototype.pipeThrough cannot be used on a locked ReadableStream", - )); - } - - let readable_class = transform.readable.clone(); - let writable = OwnedBorrowMut::from_class(transform.writable); - - // If ! IsWritableStreamLocked(transform["writable"]) is true, throw a TypeError exception. - if writable.is_writable_stream_locked() { - return Err(Exception::throw_type( - &ctx, - "ReadableStream.prototype.pipeThrough cannot be used on a locked WritableStream", - )); - } - - // Let signal be options["signal"] if it exists, or undefined otherwise. - let options = options.0.unwrap_or_default(); - - // Let promise be ! ReadableStreamPipeTo(this, transform["writable"], options["preventClose"], options["preventAbort"], options["preventCancel"], signal). - let promise = ReadableStream::readable_stream_pipe_to( - ctx.clone(), - stream.0, - writable, - options.prevent_close, - options.prevent_abort, - options.prevent_cancel, - options.signal, - )?; - - // Set promise.[[PromiseIsHandled]] to true. - let () = promise - .catch()? - .call((This(promise.clone()), Function::new(ctx, || {})))?; - - // Return transform["readable"]. - Ok(readable_class) - } - - // Promise pipeTo(WritableStream destination, optional StreamPipeOptions options = {}); - fn pipe_to( - ctx: Ctx<'js>, - stream: This>, - destination: Value<'js>, - options: NullableOpt>, - ) -> Result> { - with_promise_result(&ctx, || { - let stream = - ReadableStreamOwned::from_class(Class::from_value(&stream.0).or_throw_type( - &ctx, - "'pipeTo' called on an object that is not a valid instance of ReadableStream.", - )?); - - let options = match options.0 { - Some(options) => Some(StreamPipeOptions::from_js(&ctx, options)?), - None => None, - }; - - // If ! IsReadableStreamLocked(this) is true, return a promise rejected with a TypeError exception. - if stream.is_readable_stream_locked() { - return promise_rejected_with_constructor( - &stream.constructor_type_error, - &stream.promise_primordials, - "ReadableStream.prototype.pipeTo cannot be used on a locked ReadableStream", - ); - } - - let destination = WritableStreamOwned::from_class( - Class::from_value(&destination).or_throw_type(&ctx,"'pipeTo' instructed to pipe to an object that is not a valid instance of WritableStream.")?, - ); - - // If ! IsWritableStreamLocked(destination) is true, return a promise rejected with a TypeError exception. - if destination.is_writable_stream_locked() { - return promise_rejected_with_constructor( - &stream.constructor_type_error, - &stream.promise_primordials, - "ReadableStream.prototype.pipeTo cannot be used on a locked WritableStream", - ); - } - - // Let signal be options["signal"] if it exists, or undefined otherwise. - let options = options.unwrap_or_default(); - - // Return ! ReadableStreamPipeTo(this, destination, options["preventClose"], options["preventAbort"], options["preventCancel"], signal). - Self::readable_stream_pipe_to( - ctx.clone(), - stream, - destination, - options.prevent_close, - options.prevent_abort, - options.prevent_cancel, - options.signal, - ) - }) - } - - // sequence tee(); - fn tee( - ctx: Ctx<'js>, - stream: This>, - ) -> Result, Class<'js, Self>)>> { - Ok(List(Self::readable_stream_tee( - ctx, - ReadableStreamObjects::from_stream(stream.0), - )?)) - } - - #[qjs(rename = PredefinedAtom::SymbolAsyncIterator)] - fn async_iterate( - ctx: Ctx<'js>, - stream: This>, - ) -> Result>> { - Self::values(ctx, stream, Opt(None)) - } - - fn values( - ctx: Ctx<'js>, - stream: This>, - arg: Opt>, - ) -> Result>> { - // Let reader be ? AcquireReadableStreamDefaultReader(stream). - let (stream, reader) = ReadableStreamReaderClass::acquire_readable_stream_default_reader( - ctx.clone(), - stream.0, - )?; - - // Let preventCancel be args[0]["preventCancel"]. - let prevent_cancel = match arg.0 { - None => false, - Some(arg) => matches!(arg.get_value_or_undefined("preventCancel")?, Some(true)), - }; - - let promise_primordials = stream.promise_primordials.clone(); - let controller = stream.controller.clone(); - - ReadableStreamAsyncIterator::new( - ctx, - ReadableStreamClassObjects { - stream: stream.into_inner(), - controller, - reader, - }, - promise_primordials, - prevent_cancel, - ) - } -} - -impl<'js> ReadableStream<'js> { - pub(super) fn readable_stream_error< - C: ReadableStreamController<'js>, - R: ReadableStreamReader<'js>, - >( - // Let reader be stream.[[reader]]. - mut objects: ReadableStreamObjects<'js, C, R>, - e: Value<'js>, - ) -> Result> { - // Set stream.[[state]] to "errored". - // Set stream.[[storedError]] to e. - objects.stream.state = ReadableStreamState::Errored(e.clone()); - - objects = objects.with_reader( - // If reader implements ReadableStreamDefaultReader, - |mut objects| { - // Reject reader.[[closedPromise]] with e. - objects.reader - .generic - .closed_promise - .reject(e.clone())?; - - // Set reader.[[closedPromise]].[[PromiseIsHandled]] to true. - objects.reader.generic.closed_promise.set_is_handled()?; - - // Perform ! ReadableStreamDefaultReaderErrorReadRequests(reader, e). - objects = ReadableStreamDefaultReader::readable_stream_default_reader_error_read_requests( - objects, e.clone(), - )?; - Ok(objects) - }, - // Otherwise, - |mut objects| { - // Reject reader.[[closedPromise]] with e. - objects.reader - .generic - .closed_promise - .reject(e.clone())?; - - // Set reader.[[closedPromise]].[[PromiseIsHandled]] to true. - objects.reader.generic.closed_promise.set_is_handled()?; - - // Perform ! ReadableStreamBYOBReaderErrorReadIntoRequests(reader, e). - objects = ReadableStreamBYOBReader::readable_stream_byob_reader_error_read_into_requests( - objects, e.clone(), - )?; - - Ok(objects) - }, - // If reader is undefined, return. - Ok)?; - - Ok(objects) - } - - pub(super) fn readable_stream_get_num_read_requests( - reader: &ReadableStreamDefaultReader, - ) -> usize { - reader.read_requests.len() - } - - pub(super) fn readable_stream_get_num_read_into_requests( - reader: &ReadableStreamBYOBReader, - ) -> usize { - reader.read_into_requests.len() - } - - pub(super) fn readable_stream_fulfill_read_request>( - ctx: &Ctx<'js>, - // Let reader be stream.[[reader]]. - mut objects: ReadableStreamDefaultReaderObjects<'js, C>, - chunk: Value<'js>, - done: bool, - ) -> Result> { - // Let readRequest be reader.[[readRequests]][0]. - // Remove readRequest from reader.[[readRequests]]. - let read_request = objects - .reader - .read_requests - .pop_front() - .expect("ReadableStreamFulfillReadRequest called with empty readRequests"); - - if done { - // If done is true, perform readRequest’s close steps. - read_request.close_steps_typed(ctx, objects) - } else { - // Otherwise, perform readRequest’s chunk steps, given chunk. - read_request.chunk_steps_typed(objects, chunk) - } - } - - pub(super) fn readable_stream_fulfill_read_into_request( - ctx: &Ctx<'js>, - mut objects: ReadableStreamBYOBObjects<'js>, - chunk: ViewBytes<'js>, - done: bool, - ) -> Result> { - // Let readIntoRequest be reader.[[readIntoRequests]][0]. - // Remove readIntoRequest from reader.[[readIntoRequests]]. - let read_into_request = objects - .reader - .read_into_requests - .pop_front() - .expect("ReadableStreamFulfillReadIntoRequest called with empty readIntoRequests"); - - if done { - // If done is true, perform readIntoRequest’s close steps, given chunk. - read_into_request.close_steps(objects, chunk.into_js(ctx)?) - } else { - // Otherwise, perform readIntoRequest’s chunk steps, given chunk. - read_into_request.chunk_steps(objects, chunk.into_js(ctx)?) - } - } - - pub(super) fn readable_stream_close< - C: ReadableStreamController<'js>, - R: ReadableStreamReader<'js>, - >( - ctx: Ctx<'js>, - // Let reader be stream.[[reader]]. - mut objects: ReadableStreamObjects<'js, C, R>, - ) -> Result> { - // Set stream.[[state]] to "closed". - objects.stream.state = ReadableStreamState::Closed; - - objects.with_reader( - |mut objects| { - // Resolve reader.[[closedPromise]] with undefined. - objects.reader.generic.closed_promise.resolve_undefined()?; - - // If reader implements ReadableStreamDefaultReader, - // Let readRequests be reader.[[readRequests]]. - // Set reader.[[readRequests]] to an empty list. - let read_requests = objects.reader.read_requests.split_off(0); - - // For each readRequest of readRequests, - for read_request in read_requests { - // Perform readRequest’s close steps. - objects = read_request.close_steps_typed(&ctx, objects)?; - } - - Ok(objects) - }, - |objects| { - objects.reader.generic.closed_promise.resolve_undefined()?; - - Ok(objects) - }, - // If reader is undefined, return. - Ok, - ) - } - - pub fn is_readable_stream_locked(&self) -> bool { - // If stream.[[reader]] is undefined, return false. - if self.reader.is_none() { - return false; - } - // Return true. - true - } - - pub(super) fn readable_stream_add_read_request( - &mut self, - reader: &mut ReadableStreamDefaultReader<'js>, - read_request: impl ReadableStreamReadRequest<'js> + 'js, - ) { - reader.read_requests.push_back(Box::new(read_request)); - } - - pub(super) fn readable_stream_cancel< - C: ReadableStreamController<'js>, - R: ReadableStreamReader<'js>, - >( - ctx: Ctx<'js>, - mut objects: ReadableStreamObjects<'js, C, R>, - reason: Value<'js>, - ) -> Result<(Promise<'js>, ReadableStreamObjects<'js, C, R>)> { - // Set stream.[[disturbed]] to true. - objects.stream.disturbed = true; - - match objects.stream.state { - // If stream.[[state]] is "closed", return a promise resolved with undefined. - ReadableStreamState::Closed => Ok(( - // wpt tests expect that this is a new promise every time so we can't duplicate the primordial promise_resolved_with_undefined - promise_resolved_with( - &ctx, - &objects.stream.promise_primordials, - Ok(Value::new_undefined(ctx.clone())), - )?, - objects, - )), - // If stream.[[state]] is "errored", return a promise rejected with stream.[[storedError]]. - ReadableStreamState::Errored(ref stored_error) => Ok(( - promise_rejected_with(&objects.stream.promise_primordials, stored_error.clone())?, - objects, - )), - ReadableStreamState::Readable => { - // Perform ! ReadableStreamClose(stream). - objects = ReadableStream::readable_stream_close(ctx.clone(), objects)?; - // Let reader be stream.[[reader]]. - // If reader is not undefined and reader implements ReadableStreamBYOBReader, - - objects = objects.with_reader( - Ok, - |mut objects| { - // Let readIntoRequests be reader.[[readIntoRequests]]. - // Set reader.[[readIntoRequests]] to an empty list. - let read_into_requests = objects.reader.read_into_requests.split_off(0); - // For each readIntoRequest of readIntoRequests, - for read_into_request in read_into_requests { - // Perform readIntoRequest’s close steps, given undefined. - objects = read_into_request - .close_steps(objects, Value::new_undefined(ctx.clone()))?; - } - - Ok(objects) - }, - Ok, - )?; - - // Let sourceCancelPromise be ! stream.[[controller]].[[CancelSteps]](reason). - let (source_cancel_promise, objects) = C::cancel_steps(&ctx, objects, reason)?; - - // Return the result of reacting to sourceCancelPromise with a fulfillment step that returns undefined. - let promise = upon_promise_fulfilment(ctx, source_cancel_promise, |_, ()| { - Ok(rquickjs::Undefined) - })?; - - Ok((promise, objects)) - } - } - } - - pub(super) fn readable_stream_add_read_into_request( - reader: &mut ReadableStreamBYOBReader<'js>, - read_request: impl ReadableStreamReadIntoRequest<'js> + 'js, - ) { - // Append readRequest to stream.[[reader]].[[readIntoRequests]]. - reader.read_into_requests.push_back(Box::new(read_request)) - } - - // CreateReadableStream(startAlgorithm, pullAlgorithm, cancelAlgorithm[, highWaterMark, [, sizeAlgorithm]]) performs the following steps: - pub(crate) fn create_readable_stream( - ctx: Ctx<'js>, - start_algorithm: StartAlgorithm<'js>, - pull_algorithm: PullAlgorithm<'js>, - cancel_algorithm: CancelAlgorithm<'js>, - high_water_mark: Option, - size_algorithm: Option>, - ) -> Result< - ReadableStreamClassObjects<'js, ReadableStreamDefaultControllerOwned<'js>, UndefinedReader>, - > { - // If highWaterMark was not passed, set it to 1. - let high_water_mark = high_water_mark.unwrap_or(1.0); - - // If sizeAlgorithm was not passed, set it to an algorithm that returns 1. - let size_algorithm = size_algorithm.unwrap_or(SizeAlgorithm::AlwaysOne); - - let base_primordials = BasePrimordials::get(&ctx)?; - - // Let stream be a new ReadableStream. - let stream_class = Class::instance( - ctx.clone(), - Self { - // Set stream.[[state]] to "readable". - state: ReadableStreamState::Readable, - // Set stream.[[reader]] and stream.[[storedError]] to undefined. - reader: None, - // Set stream.[[disturbed]] to false. - disturbed: false, - controller: ReadableStreamControllerClass::Uninitialised, - promise_primordials: PromisePrimordials::get(&ctx)?.clone(), - constructor_range_error: base_primordials.constructor_range_error.clone(), - constructor_type_error: base_primordials.constructor_type_error.clone(), - function_array_buffer_is_view: base_primordials - .function_array_buffer_is_view - .clone(), - }, - )?; - drop(base_primordials); - - // Perform ? SetUpReadableStreamDefaultController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm). - let controller_class = - ReadableStreamDefaultController::set_up_readable_stream_default_controller( - ctx, - OwnedBorrowMut::from_class(stream_class.clone()), - start_algorithm, - pull_algorithm, - cancel_algorithm, - high_water_mark, - size_algorithm, - false, // not owning-type; Rust-side streams never set it - )?; - - // Return stream. - Ok(ReadableStreamClassObjects { - stream: stream_class, - controller: controller_class, - reader: UndefinedReader, - }) - } - - /// Create a ReadableStream from Rust pull/cancel algorithms - pub fn from_pull_algorithm( - ctx: Ctx<'js>, - pull_algorithm: PullAlgorithm<'js>, - cancel_algorithm: CancelAlgorithm<'js>, - ) -> Result> { - Self::from_pull_algorithm_with_options(ctx, pull_algorithm, cancel_algorithm, None) - } - - /// Create a ReadableStream from Rust pull/cancel algorithms with custom highWaterMark - pub fn from_pull_algorithm_with_options( - ctx: Ctx<'js>, - pull_algorithm: PullAlgorithm<'js>, - cancel_algorithm: CancelAlgorithm<'js>, - high_water_mark: Option, - ) -> Result> { - Ok(Self::create_readable_stream( - ctx, - StartAlgorithm::ReturnUndefined, - pull_algorithm, - cancel_algorithm, - high_water_mark, - None, - )? - .stream) - } - - /// Create a byte-source ReadableStream (i.e. `type: "bytes"`) from Rust - /// pull/cancel algorithms. BYOB readers can attach to the returned - /// stream, and the pull algorithm receives a byte controller so it can - /// enqueue `Uint8Array` chunks that stream directly into BYOB reads - /// without copying. - pub fn from_byte_pull_algorithm( - ctx: Ctx<'js>, - pull_algorithm: PullAlgorithm<'js>, - cancel_algorithm: CancelAlgorithm<'js>, - ) -> Result> { - let (stream, _controller) = Self::create_readable_byte_stream( - ctx, - StartAlgorithm::ReturnUndefined, - pull_algorithm, - cancel_algorithm, - )?; - Ok(stream) - } - - // CreateReadableByteStream(startAlgorithm, pullAlgorithm, cancelAlgorithm) performs the following steps: - pub fn create_readable_byte_stream( - ctx: Ctx<'js>, - start_algorithm: StartAlgorithm<'js>, - pull_algorithm: PullAlgorithm<'js>, - cancel_algorithm: CancelAlgorithm<'js>, - ) -> Result<(Class<'js, Self>, ReadableByteStreamControllerClass<'js>)> { - let base_primordials = BasePrimordials::get(&ctx)?; - - // Let stream be a new ReadableStream. - let stream_class = Class::instance( - ctx.clone(), - Self { - // Set stream.[[state]] to "readable". - state: ReadableStreamState::Readable, - // Set stream.[[reader]] and stream.[[storedError]] to undefined. - reader: None, - // Set stream.[[disturbed]] to false. - disturbed: false, - controller: ReadableStreamControllerClass::Uninitialised, - promise_primordials: PromisePrimordials::get(&ctx)?.clone(), - constructor_type_error: base_primordials.constructor_type_error.clone(), - constructor_range_error: base_primordials.constructor_range_error.clone(), - function_array_buffer_is_view: base_primordials - .function_array_buffer_is_view - .clone(), - }, - )?; - drop(base_primordials); - - // Perform ? SetUpReadableStreamDefaultController(stream, controller, startAlgorithm, pullAlgorithm, cancelAlgorithm, highWaterMark, sizeAlgorithm). - let controller_class = - ReadableByteStreamController::set_up_readable_byte_stream_controller( - ctx, - OwnedBorrowMut::from_class(stream_class.clone()), - start_algorithm, - pull_algorithm, - cancel_algorithm, - 0.0, - None, - )?; - - // Return stream. - Ok((stream_class, controller_class)) - } - - fn readable_stream_from_iterable( - ctx: &Ctx<'js>, - async_iterable: Value<'js>, - ) -> Result> { - let stream: Rc>> = Rc::new(OnceCell::new()); - - // Let iteratorRecord be ? GetIterator(asyncIterable, async). - let iterator_record = - IteratorRecord::get_iterator(ctx, async_iterable, IteratorKind::Async)?; - let iterator = iterator_record.iterator.clone(); - - // Let startAlgorithm be an algorithm that returns undefined. - let start_algorithm = StartAlgorithm::ReturnUndefined; - - let promise_primordials = PromisePrimordials::get(ctx)?.clone(); - - // Let pullAlgorithm be the following steps: - let pull_algorithm = { - let stream = stream.clone(); - let promise_primordials = promise_primordials.clone(); - move |ctx: Ctx<'js>, controller: ReadableStreamControllerClass<'js>| { - // Let nextResult be IteratorNext(iteratorRecord). - let next_result: Result> = iterator_record.iterator_next(&ctx, None); - let next_promise = match next_result { - // If nextResult is an abrupt completion, return a promise rejected with nextResult.[[Value]]. - Err(Error::Exception) => { - return promise_rejected_catch(&ctx, &promise_primordials); - } - Err(err) => return Err(err), - // Let nextPromise be a promise resolved with nextResult.[[Value]]. - Ok(next_result) => promise_resolved_with( - &ctx, - &promise_primordials, - Ok(next_result.into_inner()), - )?, - }; - - // Return the result of reacting to nextPromise with the following fulfillment steps, given iterResult: - upon_promise_fulfilment(ctx, next_promise, { - let stream = stream.clone(); - move |ctx, iter_result: Value<'js>| { - let iter_result = match iter_result.into_object() { - // If Type(iterResult) is not Object, throw a TypeError. - None => { - return Err(Exception::throw_type(&ctx, "The promise returned by the iterator.next() method must fulfill with an object")); - } - Some(iter_result) => iter_result, - }; - - // Let done be ? IteratorComplete(iterResult). - let done = IteratorRecord::iterator_complete(&iter_result)?; - - let stream = OwnedBorrowMut::from_class(stream.get().cloned().expect("ReadableStreamFromIterable pull steps called with uninitialised stream")); - let controller = match controller { - ReadableStreamControllerClass::ReadableStreamDefaultController(c) => OwnedBorrowMut::from_class(c), - _ => panic!("ReadableStreamFromIterable pull steps called without default controller") - }; - - let objects = ReadableStreamObjects::new_default(stream, controller); - - // If done is true: - if done { - // Perform ! ReadableStreamDefaultControllerClose(stream.[[controller]]). - ReadableStreamDefaultController::readable_stream_default_controller_close(ctx.clone(), objects)?; - } else { - // Let value be ? IteratorValue(iterResult). - let value = IteratorRecord::iterator_value(&iter_result)?; - - // Perform ! ReadableStreamDefaultControllerEnqueue(stream.[[controller]], value). - ReadableStreamDefaultController::readable_stream_default_controller_enqueue(ctx.clone(), objects, value)?; - } - - Ok(()) - } - }) - } - }; - - // Let cancelAlgorithm be the following steps, given reason: - let cancel_algorithm = { - let ctx = ctx.clone(); - let promise_primordials = promise_primordials.clone(); - move |reason: Value<'js>| { - // Let iterator be iteratorRecord.[[Iterator]]. - - // Let returnMethod be GetMethod(iterator, "return"). - let return_method_val: Value<'js> = match iterator.get(PredefinedAtom::Return) { - Err(Error::Exception) => { - return promise_rejected_catch(&ctx, &promise_primordials); - } - Err(err) => return Err(err), - Ok(val) => val, - }; - - let return_method: Function<'js> = - if return_method_val.is_undefined() || return_method_val.is_null() { - // If returnMethod.[[Value]] is undefined, return a promise resolved with undefined. - return Ok(promise_primordials.promise_resolved_with_undefined.clone()); - } else if let Some(func) = return_method_val.as_function() { - func.clone() - } else { - // returnMethod is not callable — reject with TypeError - let _ = Exception::throw_type(&ctx, "return is not a function"); - return promise_rejected_catch(&ctx, &promise_primordials); - }; - - // Let returnResult be Call(returnMethod.[[Value]], iterator, « reason »). - let return_result: Result> = - return_method.call((This(iterator), reason)); - - let return_result = match return_result { - // If returnResult is an abrupt completion, return a promise rejected with returnResult.[[Value]]. - Err(Error::Exception) => { - return promise_rejected_catch(&ctx, &promise_primordials); - } - Err(err) => return Err(err), - Ok(return_result) => return_result, - }; - - // Let returnPromise be a promise resolved with returnResult.[[Value]]. - let return_promise = - promise_resolved_with(&ctx, &promise_primordials, Ok(return_result))?; - - // Return the result of reacting to returnPromise with the following fulfillment steps, given iterResult: - upon_promise_fulfilment( - ctx, - return_promise, - move |ctx: Ctx<'js>, iter_result: Value<'js>| { - // If Type(iterResult) is not Object, throw a TypeError. - if !iter_result.is_object() { - return Err(Exception::throw_type(&ctx, "The promise returned by the iterator.next() method must fulfill with an object")); - } - // Return undefined. - Ok(rquickjs::Undefined) - }, - ) - } - }; - - let objects_class = ReadableStream::create_readable_stream( - ctx.clone(), - start_algorithm, - PullAlgorithm::from_fn(pull_algorithm), - CancelAlgorithm::from_fn(cancel_algorithm), - Some(0.0), - None, - )?; - _ = stream.set(objects_class.stream.clone()); - Ok(objects_class.stream) - } - - pub(super) fn reader_mut(&mut self) -> Option> { - self.reader - .clone() - .map(ReadableStreamReaderOwned::from_class) - } -} - -// enum ReadableStreamType { "bytes", "owning" }; -enum ReadableStreamType { - Bytes, - Owning, -} - -impl<'js> FromJs<'js> for ReadableStreamType { - fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let typ = value.type_of(); - - match Coerced::::from_js(ctx, value)?.as_str() { - "bytes" => Ok(Self::Bytes), - "owning" => Ok(Self::Owning), - _ => Err(Error::new_from_js(typ.as_str(), "ReadableStreamType")), - } - } -} - -struct ReadableStreamGetReaderOptions { - mode: Option, -} - -impl<'js> FromJs<'js> for ReadableStreamGetReaderOptions { - fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let ty_name = value.type_name(); - let obj = value - .as_object() - .ok_or(Error::new_from_js(ty_name, "Object"))?; - - let mode = obj.get_value_or_undefined::<_, ReadableStreamReaderMode>("mode")?; - - Ok(Self { mode }) - } -} - -// enum ReadableStreamReaderMode { "byob" }; -enum ReadableStreamReaderMode { - Byob, -} - -impl<'js> FromJs<'js> for ReadableStreamReaderMode { - fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let typ = value.type_of(); - - match Coerced::::from_js(ctx, value)?.as_str() { - "byob" => Ok(Self::Byob), - _ => Err(Error::new_from_js(typ.as_str(), "ReadableStreamReaderMode")), - } - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/stream/pipe.rs b/stdlib/src/llrt/llrt_stream_web/readable/stream/pipe.rs deleted file mode 100644 index 6ea6e9e6..00000000 --- a/stdlib/src/llrt/llrt_stream_web/readable/stream/pipe.rs +++ /dev/null @@ -1,700 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use std::{ - cell::RefCell, - rc::Rc, - sync::atomic::{AtomicBool, Ordering}, -}; - -use crate::llrt_abort::AbortSignal; -use crate::llrt_utils::{option::Undefined, result::ResultExt}; -use rquickjs::{ - class::{OwnedBorrow, Trace}, - prelude::{OnceFn, This}, - Class, Coerced, Ctx, Error, FromJs, Function, Promise, Result, Value, -}; - -use crate::llrt_stream_web::{ - readable::{ - controller::ReadableStreamControllerOwned, - default_reader::{ - ReadableStreamDefaultReader, ReadableStreamDefaultReaderOwned, - ReadableStreamReadRequest, - }, - objects::{ - ReadableStreamClassObjects, ReadableStreamDefaultReaderObjects, ReadableStreamObjects, - }, - reader::ReadableStreamReaderClass, - stream::{ReadableStream, ReadableStreamOwned, ReadableStreamState}, - }, - utils::{ - promise::{ - promise_resolved_with, upon_promise, upon_promise_fulfilment, PromisePrimordials, - ResolveablePromise, - }, - UnwrapOrUndefined, ValueOrUndefined, - }, - writable::{ - WritableStream, WritableStreamClassObjects, WritableStreamDefaultWriter, - WritableStreamDefaultWriterOwned, WritableStreamObjects, WritableStreamOwned, - WritableStreamState, - }, -}; - -impl<'js> ReadableStream<'js> { - pub(super) fn readable_stream_pipe_to( - ctx: Ctx<'js>, - source: ReadableStreamOwned<'js>, - dest: WritableStreamOwned<'js>, - prevent_close: bool, - prevent_abort: bool, - prevent_cancel: bool, - signal: Option>>, - ) -> Result> { - let (source_stored_error, source_closed) = match source.state { - ReadableStreamState::Errored(ref stored_error) => (Some(stored_error.clone()), false), - ReadableStreamState::Closed => (None, true), - _ => (None, false), - }; - let dest_stored_error = dest.stored_error(); - let dest_closing = dest.writable_stream_close_queued_or_in_flight() - || matches!(dest.state, WritableStreamState::Closed); - - let source_controller = source.controller.clone(); - - let dest_controller = dest - .controller - .clone() - .expect("pipeTo called on writable stream without controller"); - - // If source.[[controller]] implements ReadableByteStreamController, let reader be either ! AcquireReadableStreamBYOBReader(source) or ! AcquireReadableStreamDefaultReader(source), at the user agent’s discretion. - // Otherwise, let reader be ! AcquireReadableStreamDefaultReader(source). - let (mut source, reader) = - ReadableStreamReaderClass::acquire_readable_stream_default_reader(ctx.clone(), source)?; - - let source_closed_promise = reader.borrow().generic.closed_promise.promise.clone(); - - // Let writer be ! AcquireWritableStreamDefaultWriter(dest). - let (dest, writer) = - WritableStreamDefaultWriter::acquire_writable_stream_default_writer(&ctx, dest)?; - - let dest_closed_promise = writer.borrow().closed_promise.promise.clone(); - - // Set source.[[disturbed]] to true. - source.disturbed = true; - - let current_write = Rc::new(RefCell::new( - source - .promise_primordials - .promise_resolved_with_undefined - .clone(), - )); - - let promise_primordials = source.promise_primordials.clone(); - let constructor_type_error = source.constructor_type_error.clone(); - - let mut pipe_to = PipeTo { - source_objects: ReadableStreamClassObjects { - stream: source.into_inner(), - controller: source_controller, - reader, - }, - dest_objects: WritableStreamClassObjects { - stream: dest.into_inner(), - controller: dest_controller, - writer, - }, - current_write, - // Let shuttingDown be false. - shutting_down: Rc::new(AtomicBool::new(false)), - signal, - abort_callback: None, - // Let promise be a new promise. - promise: ResolveablePromise::new(&ctx)?, - promise_primordials: promise_primordials.clone(), - }; - - // If signal is not undefined, - if let Some(signal) = &pipe_to.signal { - // Let abortAlgorithm be the following steps: - let abort_algorithm = { - let signal = signal.clone(); - let pipe_to = pipe_to.clone(); - move |ctx: Ctx<'js>| -> Result<()> { - // Let error be signal’s abort reason. - let error = signal.borrow().reason().unwrap_or_undefined(&ctx); - - // Let actions be an empty ordered set. - let mut actions = - Vec::) -> Result>>>::new(); - - // If preventAbort is false, append the following action to actions: - if !prevent_abort { - let dest_objects = pipe_to.dest_objects.clone(); - let error = error.clone(); - actions.push(Box::new(move |ctx| { - let dest_objects = WritableStreamObjects::from_class(dest_objects); - - if matches!(dest_objects.stream.state, WritableStreamState::Writable) { - // If dest.[[state]] is "writable", return ! WritableStreamAbort(dest, error). - let (promise, _) = WritableStream::writable_stream_abort( - ctx, - dest_objects, - Some(error.clone()), - )?; - Ok(promise) - } else { - // Otherwise, return a promise resolved with undefined. - Ok(dest_objects - .stream - .promise_primordials - .promise_resolved_with_undefined - .clone()) - } - })); - } - - // If preventCancel is false, append the following action action to actions: - if !prevent_cancel { - let source_objects = pipe_to.source_objects.clone(); - let error = error.clone(); - actions.push(Box::new(move |ctx| { - let source_objects = ReadableStreamObjects::from_class(source_objects); - - if let ReadableStreamState::Readable = source_objects.stream.state { - // If source.[[state]] is "readable", return ! ReadableStreamCancel(source, error). - let (promise, _) = ReadableStream::readable_stream_cancel( - ctx, - source_objects, - error.clone(), - )?; - - Ok(promise) - } else { - // Otherwise, return a promise resolved with undefined. - Ok(source_objects - .stream - .promise_primordials - .promise_resolved_with_undefined - .clone()) - } - })); - } - - // Shutdown with an action consisting of getting a promise to wait for all of the actions in actions, and with error. - pipe_to.shutdown_with_action( - ctx, - move |ctx| { - let promises: Vec> = actions - .into_iter() - .map(|action| action(ctx.clone())) - .collect::>>()?; - - let all_promises: Promise<'js> = - promise_primordials.promise_all.call(( - This(promise_primordials.promise_constructor.clone()), - promises, - ))?; - - Ok(all_promises) - }, - Some(error), - ) - } - }; - - // If signal is aborted, perform abortAlgorithm and return promise. - { - let signal = signal.borrow(); - - if signal.aborted { - abort_algorithm(ctx.clone())?; - - return Ok(pipe_to.promise.promise); - } - } - - let abort_callback = pipe_to - .abort_callback - .insert(Function::new(ctx.clone(), OnceFn::new(abort_algorithm))?); - - // Add abortAlgorithm to signal. - AbortSignal::set_on_abort(This(signal.clone()), ctx.clone(), abort_callback.clone())?; - } - - // In parallel but not really; see #905, using reader and writer, read all chunks from source and write them to dest. - // Due to the locking provided by the reader and writer, the exact manner in which this happens is not observable to author code, and so there is flexibility in how this is done. - // The following constraints apply regardless of the exact algorithm used: - - // Errors must be propagated forward - PipeTo::is_or_becomes_errored( - ctx.clone(), - source_stored_error, - source_closed_promise.clone(), - { - let pipe_to = pipe_to.clone(); - move |ctx, stored_error| { - if !prevent_abort { - pipe_to.shutdown_with_action( - ctx, - { - let pipe_to = pipe_to.clone(); - let stored_error = stored_error.clone(); - move |ctx| { - let dest_objects = WritableStreamObjects::from_class( - pipe_to.dest_objects.clone(), - ); - - let (promise, _) = WritableStream::writable_stream_abort( - ctx, - dest_objects, - Some(stored_error), - )?; - - Ok(promise) - } - }, - Some(stored_error), - ) - } else { - pipe_to.shutdown(ctx, Some(stored_error)) - } - } - }, - )?; - - // Errors must be propagated backward - PipeTo::is_or_becomes_errored(ctx.clone(), dest_stored_error, dest_closed_promise, { - let pipe_to = pipe_to.clone(); - move |ctx, stored_error| { - if !prevent_cancel { - pipe_to.shutdown_with_action( - ctx, - { - let pipe_to = pipe_to.clone(); - let stored_error = stored_error.clone(); - move |ctx| { - let source_objects = ReadableStreamObjects::from_class( - pipe_to.source_objects.clone(), - ); - - let (promise, _) = ReadableStream::readable_stream_cancel( - ctx, - source_objects, - stored_error, - )?; - - Ok(promise) - } - }, - Some(stored_error), - ) - } else { - pipe_to.shutdown(ctx, Some(stored_error)) - } - } - })?; - - // Closing must be propagated forward - PipeTo::is_or_becomes_closed(ctx.clone(), source_closed, source_closed_promise, { - let pipe_to = pipe_to.clone(); - move |ctx| { - if !prevent_close { - pipe_to.shutdown_with_action( - ctx, - { - let pipe_to = pipe_to.clone(); - move |ctx| { - let dest_objects = WritableStreamObjects::from_class(pipe_to.dest_objects); - - WritableStreamDefaultWriter::writable_stream_default_writer_close_with_error_propagation(ctx, dest_objects) - } - }, - None, - ) - } else { - pipe_to.shutdown(ctx, None) - } - } - })?; - - // Closing must be propagated backward - if dest_closing { - let dest_closed: Value<'js> = constructor_type_error.call(( - "the destination writable stream closed before all data could be piped to it", - ))?; - - if !prevent_cancel { - pipe_to.shutdown_with_action( - ctx.clone(), - { - let pipe_to = pipe_to.clone(); - let dest_closed = dest_closed.clone(); - move |ctx| { - let source_objects = - ReadableStreamObjects::from_class(pipe_to.source_objects.clone()); - - let (promise, _) = ReadableStream::readable_stream_cancel( - ctx, - source_objects, - dest_closed, - )?; - - Ok(promise) - } - }, - Some(dest_closed), - )?; - } else { - pipe_to.shutdown(ctx.clone(), Some(dest_closed))?; - } - } - - let result_promise = pipe_to.promise.promise.clone(); - let pipe_loop_promise = pipe_to.pipe_loop(ctx)?; - pipe_loop_promise.set_is_handled()?; - - Ok(result_promise) - } -} - -#[derive(Clone)] -struct PipeTo<'js> { - source_objects: ReadableStreamClassObjects< - 'js, - ReadableStreamControllerOwned<'js>, - ReadableStreamDefaultReaderOwned<'js>, - >, - dest_objects: WritableStreamClassObjects<'js, WritableStreamDefaultWriterOwned<'js>>, - current_write: Rc>>, - shutting_down: Rc, - signal: Option>>, - abort_callback: Option>, - promise: ResolveablePromise<'js>, - - promise_primordials: PromisePrimordials<'js>, -} - -impl<'js> PipeTo<'js> { - // Using reader and writer, read all chunks from this and write them to dest - // - Backpressure must be enforced - // - Shutdown must stop all activity - fn pipe_loop(self, ctx: Ctx<'js>) -> Result> { - let loop_promise = ResolveablePromise::new(&ctx)?; - - self.next(ctx, false, loop_promise.clone())?; - - Ok(loop_promise) - } - - fn next(&self, ctx: Ctx<'js>, done: bool, loop_promise: ResolveablePromise<'js>) -> Result<()> { - if done { - loop_promise.resolve_undefined()? - } else { - let pipe_step_promise = self.pipe_step(ctx.clone())?; - upon_promise(ctx, pipe_step_promise, { - { - let pipe_to = self.clone(); - move |ctx, result| match result { - Ok(done) => pipe_to.next(ctx, done, loop_promise), - Err(err) => loop_promise.reject(err), - } - } - })?; - } - - Ok(()) - } - - fn pipe_step(&self, ctx: Ctx<'js>) -> Result> { - if self.shutting_down.load(Ordering::Acquire) { - return promise_resolved_with( - &ctx, - &self.promise_primordials, - Ok(Value::new_bool(ctx.clone(), true)), - ); - } - - let writer_ready = self - .dest_objects - .writer - .borrow() - .ready_promise - .promise - .clone(); - - upon_promise_fulfilment(ctx, writer_ready, { - let current_write = self.current_write.clone(); - let source_objects = self.source_objects.clone(); - let dest_objects = self.dest_objects.clone(); - move |ctx: Ctx<'js>, ()| -> Result> { - let read_promise = ResolveablePromise::new(&ctx)?; - - struct ReadRequest<'js> { - dest_objects: - WritableStreamClassObjects<'js, WritableStreamDefaultWriterOwned<'js>>, - current_write: Rc>>, - read_promise: ResolveablePromise<'js>, - } - - impl<'js> Trace<'js> for ReadRequest<'js> { - fn trace<'a>(&self, tracer: rquickjs::class::Tracer<'a, 'js>) { - self.current_write.as_ref().borrow().trace(tracer); - self.read_promise.trace(tracer); - } - } - - impl<'js> ReadableStreamReadRequest<'js> for ReadRequest<'js> { - fn chunk_steps( - &self, - objects: ReadableStreamDefaultReaderObjects<'js>, - chunk: Value<'js>, - ) -> Result> { - let ctx = chunk.ctx().clone(); - - // calling write can trigger user code; ensure we don't hold locks - let objects = objects.into_inner(); - - let dest_objects = - WritableStreamObjects::from_class(self.dest_objects.clone()); - let write_promise = - WritableStreamDefaultWriter::writable_stream_default_writer_write( - ctx.clone(), - dest_objects, - chunk, - )?; - - let write_promise: Promise<'js> = write_promise.catch()?.call(( - This(write_promise.clone()), - Function::new(ctx.clone(), || {}), - ))?; - - self.current_write.replace(write_promise); - self.read_promise - .resolve(Value::new_bool(ctx.clone(), false))?; - - Ok(ReadableStreamObjects::from_class(objects)) - } - - fn close_steps( - &self, - ctx: &Ctx<'js>, - objects: ReadableStreamDefaultReaderObjects<'js>, - ) -> Result> { - self.read_promise - .resolve(Value::new_bool(ctx.clone(), true))?; - - Ok(objects) - } - - fn error_steps( - &self, - objects: ReadableStreamDefaultReaderObjects<'js>, - reason: Value<'js>, - ) -> Result> { - self.read_promise.reject(reason)?; - - Ok(objects) - } - } - - let objects = ReadableStreamObjects::from_class(source_objects); - - let promise = read_promise.promise.clone(); - - ReadableStreamDefaultReader::readable_stream_default_reader_read( - &ctx, - objects, - ReadRequest { - current_write, - read_promise, - dest_objects, - }, - )?; - - Ok(promise) - } - }) - } - - fn is_or_becomes_errored( - ctx: Ctx<'js>, - stored_error: Option>, - promise: Promise<'js>, - action: impl FnOnce(Ctx<'js>, Value<'js>) -> Result<()> + 'js, - ) -> Result<()> { - if let Some(stored_error) = stored_error { - action(ctx, stored_error) - } else { - promise.catch()?.call(( - This(promise.clone()), - Function::new(ctx.clone(), OnceFn::new(action)), - )) - } - } - - fn is_or_becomes_closed( - ctx: Ctx<'js>, - already_closed: bool, - promise: Promise<'js>, - action: impl FnOnce(Ctx<'js>) -> Result<()> + 'js, - ) -> Result<()> { - if already_closed { - action(ctx)?; - } else { - upon_promise_fulfilment(ctx, promise, |ctx, ()| action(ctx))?; - } - Ok(()) - } - - fn shutdown_with_action( - &self, - ctx: Ctx<'js>, - action: impl FnOnce(Ctx<'js>) -> Result> + 'js, - original_error: Option>, - ) -> Result<()> { - if self.shutting_down.swap(true, Ordering::AcqRel) { - // already shutting down - return Ok(()); - } - - let do_the_rest = { - let pipe_to = self.clone(); - move |ctx: Ctx<'js>| -> Result<()> { - let action_promise = action(ctx.clone())?; - upon_promise(ctx, action_promise, move |ctx, result| match result { - Ok(()) => pipe_to.finalize(ctx, original_error), - Err(new_error) => pipe_to.finalize(ctx, Some(new_error)), - })?; - Ok(()) - } - }; - - let writable = { - let dest_stream = OwnedBorrow::from_class(self.dest_objects.stream.clone()); - matches!(dest_stream.state, WritableStreamState::Writable) - && !dest_stream.writable_stream_close_queued_or_in_flight() - }; - - if writable { - let wait_promise = - Self::wait_for_writes_to_finish(ctx.clone(), self.current_write.clone())?; - upon_promise_fulfilment(ctx, wait_promise, |ctx: Ctx<'js>, ()| do_the_rest(ctx))?; - } else { - do_the_rest(ctx)? - } - Ok(()) - } - - fn shutdown(&self, ctx: Ctx<'js>, error: Option>) -> Result<()> { - if self.shutting_down.swap(true, Ordering::AcqRel) { - // already shutting down - return Ok(()); - } - - let writable = { - let dest_stream = OwnedBorrow::from_class(self.dest_objects.stream.clone()); - matches!(dest_stream.state, WritableStreamState::Writable) - && !dest_stream.writable_stream_close_queued_or_in_flight() - }; - - if writable { - let wait_promise = - Self::wait_for_writes_to_finish(ctx.clone(), self.current_write.clone())?; - let pipe_to = self.clone(); - upon_promise_fulfilment(ctx, wait_promise, move |ctx, ()| { - pipe_to.finalize(ctx, error) - })?; - } else { - self.finalize(ctx, error)?; - } - Ok(()) - } - - fn wait_for_writes_to_finish( - ctx: Ctx<'js>, - current_write: Rc>>, - ) -> Result> { - let old_current_write: Promise<'js> = current_write.as_ref().borrow().clone(); - - upon_promise_fulfilment( - ctx, - old_current_write.clone(), - move |ctx: Ctx<'js>, ()| -> Result>> { - if !old_current_write.eq(¤t_write.as_ref().borrow()) { - Ok(Undefined(Some(Self::wait_for_writes_to_finish( - ctx, - current_write, - )?))) - } else { - Ok(Undefined(None)) - } - }, - ) - } - - fn finalize(&self, ctx: Ctx<'js>, error: Option>) -> Result<()> { - let source_objects = ReadableStreamObjects::from_class(self.source_objects.clone()); - let dest_objects = WritableStreamObjects::from_class(self.dest_objects.clone()); - - WritableStreamDefaultWriter::writable_stream_default_writer_release(dest_objects)?; - ReadableStreamDefaultReader::readable_stream_default_reader_release(source_objects)?; - - if let (Some(signal), Some(abort_callback)) = (&self.signal, &self.abort_callback) { - AbortSignal::remove_on_abort( - This(signal.clone()), - ctx.clone(), - abort_callback.clone(), - )?; - } - - if let Some(error) = error { - self.promise.reject(error) - } else { - self.promise.resolve_undefined() - } - } -} - -#[derive(Default)] -pub struct StreamPipeOptions<'js> { - pub prevent_close: bool, - pub prevent_abort: bool, - pub prevent_cancel: bool, - pub signal: Option>>, -} - -impl<'js> FromJs<'js> for StreamPipeOptions<'js> { - fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let ty_name = value.type_name(); - let obj = value - .as_object() - .ok_or(Error::new_from_js(ty_name, "Object"))?; - - let get_bool = |key| { - Result::Ok( - obj.get_value_or_undefined::<_, Coerced>(key)? - .map(|b| b.0) - .unwrap_or(false), - ) // missing is treated as false - }; - - let prevent_abort = get_bool("preventAbort")?; - let prevent_cancel = get_bool("preventCancel")?; - let prevent_close = get_bool("preventClose")?; - - let signal = match obj.get_value_or_undefined::<_, Value<'js>>("signal")? { - Some(signal) => Some( - Class::::from_js(ctx, signal) - .or_throw_type(ctx, "Invalid signal argument")?, - ), - None => None, - }; - - Ok(Self { - prevent_close, - prevent_abort, - prevent_cancel, - signal, - }) - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/stream/source.rs b/stdlib/src/llrt/llrt_stream_web/readable/stream/source.rs deleted file mode 100644 index b6afad48..00000000 --- a/stdlib/src/llrt/llrt_stream_web/readable/stream/source.rs +++ /dev/null @@ -1,36 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use rquickjs::{Function, Object, Result}; - -use crate::llrt_stream_web::{readable::stream::ReadableStreamType, utils::ValueOrUndefined}; - -#[derive(Default)] -pub(crate) struct UnderlyingSource<'js> { - // callback UnderlyingSourceStartCallback = any (ReadableStreamController controller); - pub(crate) start: Option>, - // callback UnderlyingSourcePullCallback = Promise (ReadableStreamController controller); - pub(crate) pull: Option>, - // callback UnderlyingSourceCancelCallback = Promise (optional any reason); - pub(crate) cancel: Option>, - pub(super) r#type: Option, - // [EnforceRange] unsigned long long autoAllocateChunkSize; - pub(crate) auto_allocate_chunk_size: Option, -} - -impl<'js> UnderlyingSource<'js> { - pub(super) fn from_object(obj: Object<'js>) -> Result { - let start = obj.get_value_or_undefined::<_, _>("start")?; - let pull = obj.get_value_or_undefined::<_, _>("pull")?; - let cancel = obj.get_value_or_undefined::<_, _>("cancel")?; - let r#type = obj.get_value_or_undefined::<_, _>("type")?; - let auto_allocate_chunk_size = - obj.get_value_or_undefined::<_, _>("autoAllocateChunkSize")?; - - Ok(Self { - start, - pull, - cancel, - r#type, - auto_allocate_chunk_size, - }) - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/readable/stream/tee.rs b/stdlib/src/llrt/llrt_stream_web/readable/stream/tee.rs deleted file mode 100644 index 4303fadb..00000000 --- a/stdlib/src/llrt/llrt_stream_web/readable/stream/tee.rs +++ /dev/null @@ -1,1713 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use std::{ - cell::{OnceCell, RefCell}, - rc::Rc, - sync::atomic::{AtomicBool, Ordering}, -}; - -use rquickjs::{ - class::{OwnedBorrowMut, Trace, Tracer}, - function::Constructor, - prelude::{List, OnceFn}, - ArrayBuffer, Class, Ctx, Error, Function, IntoJs, JsLifetime, Promise, Result, Value, -}; - -use crate::llrt_stream_web::{ - readable::{ - byob_reader::{ReadableStreamBYOBReader, ReadableStreamReadIntoRequest, ViewBytes}, - byte_controller::{ReadableByteStreamController, ReadableByteStreamControllerOwned}, - controller::{ReadableStreamController, ReadableStreamControllerClass}, - default_controller::{ - ReadableStreamDefaultController, ReadableStreamDefaultControllerOwned, - }, - default_reader::{ - ReadableStreamDefaultReader, ReadableStreamDefaultReaderOwned, - ReadableStreamReadRequest, - }, - objects::{ReadableByteStreamObjects, ReadableStreamDefaultControllerObjects}, - objects::{ - ReadableStreamBYOBObjects, ReadableStreamClassObjects, - ReadableStreamDefaultReaderObjects, ReadableStreamObjects, - }, - reader::{ - ReadableStreamReader, ReadableStreamReaderClass, ReadableStreamReaderOwned, - UndefinedReader, - }, - stream::{ - algorithms::{CancelAlgorithm, PullAlgorithm, StartAlgorithm}, - ReadableStream, ReadableStreamClass, - }, - }, - utils::promise::{upon_promise, ResolveablePromise}, -}; - -/// State for tee() operation, stored in a Class for GC tracing -#[rquickjs::class] -pub(crate) struct TeeState<'js> { - pub(super) stream: ReadableStreamClass<'js>, - pub(super) controller: Class<'js, ReadableStreamDefaultController<'js>>, - pub(super) reader: Class<'js, ReadableStreamDefaultReader<'js>>, - pub(super) cancel_promise: ResolveablePromise<'js>, - pub(super) reading: Rc, - pub(super) read_again: Rc, - pub(super) reason_1: Rc>>, - pub(super) reason_2: Rc>>, - pub(super) branch_1: Rc< - OnceCell< - ReadableStreamClassObjects< - 'js, - ReadableStreamDefaultControllerOwned<'js>, - UndefinedReader, - >, - >, - >, - pub(super) branch_2: Rc< - OnceCell< - ReadableStreamClassObjects< - 'js, - ReadableStreamDefaultControllerOwned<'js>, - UndefinedReader, - >, - >, - >, -} - -unsafe impl<'js> JsLifetime<'js> for TeeState<'js> { - type Changed<'to> = TeeState<'to>; -} - -impl<'js> Trace<'js> for TeeState<'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - self.stream.trace(tracer); - self.controller.trace(tracer); - self.reader.trace(tracer); - self.cancel_promise.trace(tracer); - if let Some(r) = self.reason_1.get() { - r.trace(tracer); - } - if let Some(r) = self.reason_2.get() { - r.trace(tracer); - } - if let Some(b) = self.branch_1.get() { - b.trace(tracer); - } - if let Some(b) = self.branch_2.get() { - b.trace(tracer); - } - } -} - -type ReadableStreamPair<'js> = (ReadableStreamClass<'js>, ReadableStreamClass<'js>); - -impl<'js> ReadableStream<'js> { - pub(super) fn readable_stream_tee>( - ctx: Ctx<'js>, - objects: ReadableStreamObjects<'js, C, UndefinedReader>, - ) -> Result> { - let (streams, _) = objects.with_controller( - ctx, - |ctx, objects| { - let (streams, objects) = Self::readable_stream_default_tee(ctx, objects)?; - Ok((streams, objects.clear_reader())) - }, - |ctx, objects| { - // If stream.[[controller]] implements ReadableByteStreamController, return ? ReadableByteStreamTee(stream). - Self::readable_byte_stream_tee(ctx, objects) - }, - )?; - - Ok(streams) - } - - fn readable_stream_default_tee( - ctx: Ctx<'js>, - mut objects: ReadableStreamDefaultControllerObjects<'js, UndefinedReader>, - ) -> Result<( - ReadableStreamPair<'js>, - ReadableStreamDefaultControllerObjects<'js, ReadableStreamDefaultReaderOwned<'js>>, - )> { - // Let reader be ? AcquireReadableStreamDefaultReader(stream). - let (stream, reader) = ReadableStreamReaderClass::acquire_readable_stream_default_reader( - ctx.clone(), - objects.stream, - )?; - objects.stream = stream; - // Let reading be false. - let reading = Rc::new(AtomicBool::new(false)); - // Let readAgain be false. - let read_again = Rc::new(AtomicBool::new(false)); - // Let canceled1 be false. - // Let canceled2 be false. - // Let reason1 be undefined. - let reason_1 = Rc::new(OnceCell::new()); - // Let reason2 be undefined. - let reason_2 = Rc::new(OnceCell::new()); - // Let branch1 be undefined. - let branch_1: Rc< - OnceCell< - ReadableStreamClassObjects< - 'js, - ReadableStreamDefaultControllerOwned<'js>, - UndefinedReader, - >, - >, - > = Rc::new(OnceCell::new()); - // Let branch2 be undefined. - let branch_2: Rc< - OnceCell< - ReadableStreamClassObjects< - 'js, - ReadableStreamDefaultControllerOwned<'js>, - UndefinedReader, - >, - >, - > = Rc::new(OnceCell::new()); - // Let cancelPromise be a new promise. - let cancel_promise = ResolveablePromise::new(&ctx)?; - - // Let startAlgorithm be an algorithm that returns undefined. - let start_algorithm = StartAlgorithm::ReturnUndefined; - - let objects_class: ReadableStreamClassObjects< - 'js, - ReadableStreamDefaultControllerOwned<'js>, - ReadableStreamDefaultReaderOwned<'js>, - > = objects.into_inner().set_reader(reader); - - // Create TeeState to hold all JS values for GC tracing - let tee_state = Class::instance( - ctx.clone(), - TeeState { - stream: objects_class.stream.clone(), - controller: objects_class.controller.clone(), - reader: objects_class.reader.clone(), - cancel_promise: cancel_promise.clone(), - reading: reading.clone(), - read_again: read_again.clone(), - reason_1: reason_1.clone(), - reason_2: reason_2.clone(), - branch_1: branch_1.clone(), - branch_2: branch_2.clone(), - }, - )?; - - let pull_algorithm = PullAlgorithm::from_tee_state(tee_state.clone()); - let cancel_algorithm_1 = CancelAlgorithm::from_tee_state_1(tee_state.clone()); - let cancel_algorithm_2 = CancelAlgorithm::from_tee_state_2(tee_state.clone()); - - // Set branch1 to ! CreateReadableStream(startAlgorithm, pullAlgorithm, cancel1Algorithm). - let branch_1_objects = { - let objects = Self::create_readable_stream( - ctx.clone(), - start_algorithm.clone(), - pull_algorithm.clone(), - cancel_algorithm_1, - None, - None, - )?; - _ = branch_1.set(objects.clone()); - objects - }; - - // Set branch2 to ! CreateReadableStream(startAlgorithm, pullAlgorithm, cancel2Algorithm). - let branch_2_objects = { - let objects = Self::create_readable_stream( - ctx.clone(), - start_algorithm, - pull_algorithm, - cancel_algorithm_2, - None, - None, - )?; - _ = branch_2.set(objects.clone()); - objects - }; - - upon_promise( - ctx.clone(), - objects_class - .reader - .borrow() - .generic - .closed_promise - .promise - .clone(), - { - let tee_state = tee_state.clone(); - let branch_1_objects = branch_1_objects.clone(); - let branch_2_objects = branch_2_objects.clone(); - move |_, result| match result { - Ok(()) => Ok(()), - // Upon rejection of reader.[[closedPromise]] with reason r, - Err(reason) => { - // Perform ! ReadableStreamDefaultControllerError(branch1.[[controller]], r). - let objects_1 = - ReadableStreamObjects::from_class_no_reader(branch_1_objects) - .refresh_reader(); - ReadableStreamDefaultController::readable_stream_default_controller_error( - objects_1, - reason.clone(), - )?; - - // Perform ! ReadableStreamDefaultControllerError(branch2.[[controller]], r). - let objects_2 = - ReadableStreamObjects::from_class_no_reader(branch_2_objects) - .refresh_reader(); - ReadableStreamDefaultController::readable_stream_default_controller_error( - objects_2, reason, - )?; - // If canceled1 is false or canceled2 is false, resolve cancelPromise with undefined. - let state = tee_state.borrow(); - if state.reason_1.get().is_none() || state.reason_2.get().is_none() { - state.cancel_promise.resolve_undefined()?; - } - - Ok(()) - } - } - }, - )?; - - Ok(( - (branch_1_objects.stream, branch_2_objects.stream), - ReadableStreamObjects::from_class(objects_class), - )) - } -} - -/// Pull algorithm for tee - called from PullAlgorithm::Tee -pub fn tee_pull_algorithm<'js>( - ctx: Ctx<'js>, - state: Class<'js, TeeState<'js>>, -) -> Result> { - let state_ref = state.borrow(); - - // If reading is true, set readAgain to true and return resolved promise - if state_ref.reading.load(Ordering::Acquire) { - state_ref.read_again.store(true, Ordering::Release); - return Ok(state_ref - .stream - .borrow() - .promise_primordials - .promise_resolved_with_undefined - .clone()); - } - - // Set reading to true - state_ref.reading.store(true, Ordering::Release); - - let objects_class: ReadableStreamClassObjects< - 'js, - ReadableStreamDefaultControllerOwned<'js>, - ReadableStreamDefaultReaderOwned<'js>, - > = ReadableStreamClassObjects { - stream: state_ref.stream.clone(), - controller: state_ref.controller.clone(), - reader: state_ref.reader.clone(), - }; - drop(state_ref); - - let mut objects = ReadableStreamObjects::from_class(objects_class.clone()); - - // ReadRequest that just holds TeeState - #[derive(Clone)] - struct TeeReadRequest<'js>(Class<'js, TeeState<'js>>); - - impl<'js> Trace<'js> for TeeReadRequest<'js> { - fn trace<'a>(&self, tracer: rquickjs::class::Tracer<'a, 'js>) { - self.0.trace(tracer); - } - } - - impl<'js> ReadableStreamReadRequest<'js> for TeeReadRequest<'js> { - fn chunk_steps( - &self, - objects: ReadableStreamDefaultReaderObjects<'js>, - chunk: Value<'js>, - ) -> Result> { - let ctx = chunk.ctx().clone(); - let state = self.0.clone(); - - objects.with_assert_default_controller(|objects| { - let objects_class = objects.into_inner(); - let f = { - let ctx = ctx.clone(); - let _objects_class = objects_class.clone(); - move || -> Result<()> { - let s = state.borrow(); - s.read_again.store(false, Ordering::Release); - - let chunk_1 = chunk.clone(); - let chunk_2 = chunk; - - if s.reason_1.get().is_none() { - let objects_1 = ReadableStreamObjects::from_class( - s.branch_1.get().cloned().expect("branch1 not set"), - ) - .refresh_reader(); - ReadableStreamDefaultController::readable_stream_default_controller_enqueue( - ctx.clone(), - objects_1, - chunk_1, - )?; - } - - if s.reason_2.get().is_none() { - let objects_2 = ReadableStreamObjects::from_class( - s.branch_2.get().cloned().expect("branch2 not set"), - ) - .refresh_reader(); - ReadableStreamDefaultController::readable_stream_default_controller_enqueue( - ctx.clone(), - objects_2, - chunk_2, - )?; - } - - s.reading.store(false, Ordering::Release); - - if s.read_again.load(Ordering::Acquire) { - drop(s); - tee_pull_algorithm(ctx, state)?; - } - - Ok(()) - } - }; - - let () = Function::new(ctx, OnceFn::new(f))?.defer(())?; - Ok(ReadableStreamObjects::from_class(objects_class)) - }) - } - - fn close_steps( - &self, - ctx: &Ctx<'js>, - objects: ReadableStreamDefaultReaderObjects<'js>, - ) -> Result> { - let s = self.0.borrow(); - s.reading.store(false, Ordering::Release); - - if s.reason_1.get().is_none() { - let objects_1 = ReadableStreamObjects::from_class( - s.branch_1.get().cloned().expect("branch1 not set"), - ) - .refresh_reader(); - ReadableStreamDefaultController::readable_stream_default_controller_close( - ctx.clone(), - objects_1, - )?; - } - - if s.reason_2.get().is_none() { - let objects_2 = ReadableStreamObjects::from_class( - s.branch_2.get().cloned().expect("branch2 not set"), - ) - .refresh_reader(); - ReadableStreamDefaultController::readable_stream_default_controller_close( - ctx.clone(), - objects_2, - )?; - } - - if s.reason_1.get().is_none() || s.reason_2.get().is_none() { - s.cancel_promise.resolve_undefined()?; - } - - Ok(objects) - } - - fn error_steps( - &self, - objects: ReadableStreamDefaultReaderObjects<'js>, - _e: Value<'js>, - ) -> Result> { - self.0.borrow().reading.store(false, Ordering::Release); - Ok(objects) - } - } - - objects = ReadableStreamDefaultReader::readable_stream_default_reader_read( - &ctx, - objects, - TeeReadRequest(state), - )?; - - Ok(objects - .stream - .promise_primordials - .promise_resolved_with_undefined - .clone()) -} - -/// Cancel algorithm for tee - called from CancelAlgorithm::Tee1/Tee2 -pub fn tee_cancel_algorithm<'js>( - ctx: Ctx<'js>, - state: Class<'js, TeeState<'js>>, - reason: Value<'js>, - branch: usize, -) -> Result> { - let state_ref = state.borrow(); - let objects_class: ReadableStreamClassObjects< - 'js, - ReadableStreamDefaultControllerOwned<'js>, - ReadableStreamDefaultReaderOwned<'js>, - > = ReadableStreamClassObjects { - stream: state_ref.stream.clone(), - controller: state_ref.controller.clone(), - reader: state_ref.reader.clone(), - }; - let objects = ReadableStreamObjects::from_class(objects_class); - ReadableStream::tee_cancel_algorithm_impl( - ctx, - objects, - [&state_ref.reason_1, &state_ref.reason_2], - state_ref.cancel_promise.clone(), - reason, - branch, - ) -} - -impl<'js> ReadableStream<'js> { - // Cancel algorithm for tee - handles both branches - fn tee_cancel_algorithm_impl( - ctx: Ctx<'js>, - objects: ReadableStreamObjects< - 'js, - impl ReadableStreamController<'js>, - impl ReadableStreamReader<'js>, - >, - reasons: [&Rc>>; 2], - cancel_promise: ResolveablePromise<'js>, - reason: Value<'js>, - branch: usize, - ) -> Result> { - let other = 1 - branch; - - // Set canceled[branch] to true, set reason[branch] to reason - reasons[branch] - .set(reason.clone()) - .expect("tee stream already has a cancel reason"); - - // If other branch is also canceled - if let Some(other_reason) = reasons[other].get().cloned() { - // CreateArrayFromList with reasons in correct order - let composite_reason = if branch == 0 { - List((reason, other_reason)) - } else { - List((other_reason, reason)) - }; - let (cancel_result, _) = ReadableStream::readable_stream_cancel( - ctx.clone(), - objects, - composite_reason.into_js(&ctx)?, - )?; - cancel_promise.resolve(cancel_result)?; - } - - Ok(cancel_promise.promise) - } - - fn readable_byte_stream_tee( - ctx: Ctx<'js>, - mut objects: ReadableByteStreamObjects<'js, UndefinedReader>, - ) -> Result<( - ReadableStreamPair<'js>, - ReadableByteStreamObjects<'js, UndefinedReader>, - )> { - // Let reader be ? AcquireReadableStreamDefaultReader(stream). - let (stream, reader) = ReadableStreamReaderClass::acquire_readable_stream_default_reader( - ctx.clone(), - objects.stream, - )?; - objects.stream = stream; - let reader: Rc>> = - Rc::new(RefCell::new(reader.into())); - // Let reading be false. - let reading = Rc::new(AtomicBool::new(false)); - // Let readAgainForBranch1 be false. - let read_again_for_branch_1 = Rc::new(AtomicBool::new(false)); - // Let readAgainForBranch2 be false. - let read_again_for_branch_2 = Rc::new(AtomicBool::new(false)); - // Let canceled1 be false. - // Let canceled2 be false. - // Let reason1 be undefined. - let reason_1 = Rc::new(OnceCell::new()); - // Let reason2 be undefined. - let reason_2 = Rc::new(OnceCell::new()); - // Let branch1 be undefined. - let branch_1: Rc>> = Rc::new(OnceCell::new()); - // Let branch2 be undefined. - let branch_2: Rc>> = Rc::new(OnceCell::new()); - // Let cancelPromise be a new promise. - let cancel_promise = ResolveablePromise::new(&ctx)?; - - let objects_class = objects.into_inner(); - - // Let pull1Algorithm be the following steps: - let pull_1_algorithm = PullAlgorithm::from_fn({ - let objects_class = objects_class.clone(); - let reader = reader.clone(); - let reading = reading.clone(); - let read_again_for_branch_1 = read_again_for_branch_1.clone(); - let read_again_for_branch_2 = read_again_for_branch_2.clone(); - let reason_1 = reason_1.clone(); - let reason_2 = reason_2.clone(); - let branch_1 = branch_1.clone(); - let branch_2 = branch_2.clone(); - let cancel_promise = cancel_promise.clone(); - move |ctx, branch_1_controller| { - let objects = ReadableStreamObjects::from_class(objects_class.clone()); - - let branch_1_controller = OwnedBorrowMut::from_class(match branch_1_controller { - ReadableStreamControllerClass::ReadableStreamByteController(c) => c, - _ => panic!( - "ReadableByteStream tee pull1 algorithm called without branch1 having a byte controller" - ), - }); - - let branch_2 = OwnedBorrowMut::from_class(branch_2.get().cloned().expect("ReadableByteStream tee pull1 algorithm called without branch2 being initialised")); - let branch_2_controller = match branch_2.controller { - ReadableStreamControllerClass::ReadableStreamByteController(ref c) => { - OwnedBorrowMut::from_class(c.clone()) - } - _ => { - panic!("ReadableByteStream tee pull1 algorithm called without branch2 having a byte controller") - } - }; - - Self::readable_byte_stream_pull_1_algorithm( - ctx, - objects, - reader.clone(), - reading.clone(), - read_again_for_branch_1.clone(), - read_again_for_branch_2.clone(), - reason_1.clone(), - reason_2.clone(), - ReadableStreamObjects::new_byte(OwnedBorrowMut::from_class(branch_1.get().cloned().expect("ReadableByteStream tee pull1 algorithm called without branch1 being initialised")), branch_1_controller) , - ReadableStreamObjects::new_byte(branch_2, branch_2_controller), - cancel_promise.clone(), - ) - } - }); - - // Let pull2Algorithm be the following steps: - let pull_2_algorithm = PullAlgorithm::from_fn({ - let objects_class = objects_class.clone(); - let reader = reader.clone(); - let reading = reading.clone(); - let read_again_for_branch_1 = read_again_for_branch_1.clone(); - let read_again_for_branch_2 = read_again_for_branch_2.clone(); - let reason_1 = reason_1.clone(); - let reason_2 = reason_2.clone(); - let branch_1 = branch_1.clone(); - let branch_2 = branch_2.clone(); - let cancel_promise = cancel_promise.clone(); - move |ctx, branch_2_controller| { - let objects = ReadableStreamObjects::from_class(objects_class.clone()); - - let branch_2 = OwnedBorrowMut::from_class(branch_2.get().cloned().expect("ReadableByteStream tee pull2 algorithm called without branch2 being initialised")); - let branch_2_controller = match branch_2_controller { - ReadableStreamControllerClass::ReadableStreamByteController(ref c) => { - OwnedBorrowMut::from_class(c.clone()) - } - _ => { - panic!("ReadableByteStream tee pull2 algorithm called without branch2 having a byte controller") - } - }; - - let branch_1 = OwnedBorrowMut::from_class(branch_1.get().cloned().expect("ReadableByteStream tee pull2 algorithm called without branch1 being initialised")); - let branch_1_controller = match branch_1.controller { - ReadableStreamControllerClass::ReadableStreamByteController(ref c) => { - OwnedBorrowMut::from_class(c.clone()) - } - _ => { - panic!("ReadableByteStream tee pull2 algorithm called without branch1 having a byte controller") - } - }; - Self::readable_byte_stream_pull_2_algorithm( - ctx, - objects, - reader.clone(), - reading.clone(), - read_again_for_branch_1.clone(), - read_again_for_branch_2.clone(), - reason_1.clone(), - reason_2.clone(), - ReadableStreamObjects::new_byte(branch_1, branch_1_controller), - ReadableStreamObjects::new_byte(branch_2, branch_2_controller), - cancel_promise.clone(), - ) - } - }); - - let cancel_algorithm_1 = CancelAlgorithm::from_fn({ - let objects_class = objects_class.clone(); - let reader = reader.clone(); - let reason_1 = reason_1.clone(); - let reason_2 = reason_2.clone(); - let cancel_promise = cancel_promise.clone(); - move |reason: Value<'js>| { - let reader = ReadableStreamReaderOwned::from_class(reader.borrow().clone()); - let objects = ReadableStreamObjects::from_class(objects_class).set_reader(reader); - Self::tee_cancel_algorithm_impl( - reason.ctx().clone(), - objects, - [&reason_1, &reason_2], - cancel_promise, - reason, - 0, - ) - } - }); - - let cancel_algorithm_2 = CancelAlgorithm::from_fn({ - let objects_class = objects_class.clone(); - let reader = reader.clone(); - let reason_1 = reason_1.clone(); - let reason_2 = reason_2.clone(); - let cancel_promise = cancel_promise.clone(); - move |reason: Value<'js>| { - let reader = ReadableStreamReaderOwned::from_class(reader.borrow().clone()); - let objects = ReadableStreamObjects::from_class(objects_class).set_reader(reader); - Self::tee_cancel_algorithm_impl( - reason.ctx().clone(), - objects, - [&reason_1, &reason_2], - cancel_promise, - reason, - 1, - ) - } - }); - - // Let startAlgorithm be an algorithm that returns undefined. - let start_algorithm = StartAlgorithm::ReturnUndefined; - - // Set branch1 to ! CreateReadableByteStream(startAlgorithm, pull1Algorithm, cancel1Algorithm). - let objects_1 = { - let (s, c) = Self::create_readable_byte_stream( - ctx.clone(), - start_algorithm.clone(), - pull_1_algorithm.clone(), - cancel_algorithm_1, - )?; - _ = branch_1.set(s.clone()); - ReadableStreamClassObjects { - stream: s, - controller: c, - reader: UndefinedReader, - } - }; - - // Set branch2 to ! CreateReadableByteStream(startAlgorithm, pull2Algorithm, cancel2Algorithm). - let objects_2 = { - let (s, c) = Self::create_readable_byte_stream( - ctx.clone(), - start_algorithm, - pull_2_algorithm, - cancel_algorithm_2, - )?; - _ = branch_2.set(s.clone()); - ReadableStreamClassObjects { - stream: s, - controller: c, - reader: UndefinedReader, - } - }; - - // Perform forwardReaderError, given reader. - let this_reader = reader.borrow().clone(); - Self::readable_byte_stream_forward_reader_error( - ctx, - reader, - objects_1.clone(), - objects_2.clone(), - reason_1, - reason_2, - this_reader, - cancel_promise, - )?; - - // Return « branch1, branch2 ». - Ok(( - (objects_1.stream, objects_2.stream), - ReadableStreamObjects::from_class(objects_class), - )) - } - - // Let forwardReaderError be the following steps, taking a thisReader argument: - #[allow(clippy::too_many_arguments)] - fn readable_byte_stream_forward_reader_error( - ctx: Ctx<'js>, - reader: Rc>>, - objects_1: ReadableStreamClassObjects< - 'js, - ReadableByteStreamControllerOwned<'js>, - UndefinedReader, - >, - objects_2: ReadableStreamClassObjects< - 'js, - ReadableByteStreamControllerOwned<'js>, - UndefinedReader, - >, - reason_1: Rc>>, - reason_2: Rc>>, - this_reader: ReadableStreamReaderClass<'js>, - cancel_promise: ResolveablePromise<'js>, - ) -> Result<()> { - // Upon rejection of thisReader.[[closedPromise]] with reason r, - upon_promise( - ctx, - this_reader.closed_promise(), - move |_, result| match result { - Err(r) => { - // If thisReader is not reader, return. - if !reader.borrow().eq(&this_reader) { - return Ok(()); - } - - let objects_1 = - ReadableStreamObjects::from_class_no_reader(objects_1).refresh_reader(); - - // Perform ! ReadableByteStreamControllerError(branch1.[[controller]], r). - ReadableByteStreamController::readable_byte_stream_controller_error( - objects_1, - r.clone(), - )?; - - let objects_2 = - ReadableStreamObjects::from_class_no_reader(objects_2).refresh_reader(); - - // Perform ! ReadableByteStreamControllerError(branch2.[[controller]], r). - ReadableByteStreamController::readable_byte_stream_controller_error( - objects_2, - r.clone(), - )?; - - // If canceled1 is false or canceled2 is false, resolve cancelPromise with undefined. - if reason_1.get().is_none() || reason_2.get().is_none() { - cancel_promise.resolve_undefined()?; - } - Ok(()) - } - Ok(()) => Ok(()), - }, - )?; - Ok(()) - } - - // Let pullWithDefaultReader be the following steps: - #[allow(clippy::too_many_arguments)] - fn readable_byte_stream_pull_with_default_reader( - ctx: Ctx<'js>, - mut objects: ReadableByteStreamObjects<'js, UndefinedReader>, - reader: Rc>>, - reading: Rc, - read_again_for_branch_1: Rc, - read_again_for_branch_2: Rc, - reason_1: Rc>>, - reason_2: Rc>>, - objects_1: ReadableByteStreamObjects<'js, UndefinedReader>, - objects_2: ReadableByteStreamObjects<'js, UndefinedReader>, - cancel_promise: ResolveablePromise<'js>, - ) -> Result> { - let objects_class_1 = objects_1.into_inner(); - let objects_class_2 = objects_2.into_inner(); - - // If reader implements ReadableStreamBYOBReader, - let current_reader = reader.borrow().clone(); - let current_reader = match current_reader { - ReadableStreamReaderClass::ReadableStreamBYOBReader(r) => { - let byob_reader = OwnedBorrowMut::from_class(r.clone()); - - // Perform ! ReadableStreamBYOBReaderRelease(reader). - objects = ReadableStreamBYOBReader::readable_stream_byob_reader_release( - objects.set_reader(byob_reader), - )? - .clear_reader(); - // Set reader to ! AcquireReadableStreamDefaultReader(stream). - let (s, new_reader) = - ReadableStreamReaderClass::acquire_readable_stream_default_reader( - ctx.clone(), - objects.stream, - )?; - objects.stream = s; - reader.replace(new_reader.clone().into()); - - // Perform forwardReaderError, given reader. - Self::readable_byte_stream_forward_reader_error( - ctx.clone(), - reader.clone(), - objects_class_1.clone(), - objects_class_2.clone(), - reason_1.clone(), - reason_2.clone(), - new_reader.clone().into(), - cancel_promise.clone(), - )?; - new_reader - } - ReadableStreamReaderClass::ReadableStreamDefaultReader(r) => r, - }; - - // Let readRequest be a read request with the following items: - #[derive(Clone)] - struct ReadRequest<'js> { - reader: Rc>>, - reading: Rc, - read_again_for_branch_1: Rc, - read_again_for_branch_2: Rc, - reason_1: Rc>>, - reason_2: Rc>>, - objects_class_1: ReadableStreamClassObjects< - 'js, - ReadableByteStreamControllerOwned<'js>, - UndefinedReader, - >, - objects_class_2: ReadableStreamClassObjects< - 'js, - ReadableByteStreamControllerOwned<'js>, - UndefinedReader, - >, - cancel_promise: ResolveablePromise<'js>, - } - - impl<'js> Trace<'js> for ReadRequest<'js> { - fn trace<'a>(&self, tracer: rquickjs::class::Tracer<'a, 'js>) { - if let Ok(r) = self.reader.try_borrow() { - r.trace(tracer) - } - if let Some(r) = self.reason_1.get() { - r.trace(tracer) - } - if let Some(r) = self.reason_2.get() { - r.trace(tracer) - } - self.objects_class_1.trace(tracer); - self.objects_class_2.trace(tracer); - self.cancel_promise.trace(tracer); - } - } - - impl<'js> ReadableStreamReadRequest<'js> for ReadRequest<'js> { - fn chunk_steps( - &self, - objects: ReadableStreamDefaultReaderObjects<'js>, - chunk: Value<'js>, - ) -> Result> { - let ctx = chunk.ctx().clone(); - let this = self.clone(); - - objects.with_assert_byte_controller(|objects| { - let constructor_uint8array = objects.controller.array_constructor_primordials.constructor_uint8array.clone(); - let function_array_buffer_is_view = objects.controller.function_array_buffer_is_view.clone(); - let chunk = ViewBytes::from_value(&ctx, &function_array_buffer_is_view, Some(&chunk))?; - let objects_class = objects.into_inner(); - // Queue a microtask to perform the following steps: - let f = { - let ctx = ctx.clone(); - let objects_class = objects_class.clone(); - move || -> Result<()> { - // Set readAgainForBranch1 to false. - this.read_again_for_branch_1.store(false, Ordering::Release); - // Set readAgainForBranch2 to false. - this.read_again_for_branch_2.store(false, Ordering::Release); - - // Let chunk1 and chunk2 be chunk. - let chunk_1 = chunk.clone(); - let mut chunk_2 = chunk.clone(); - - // If canceled1 is false and canceled2 is false, - if this.reason_1.get().is_none() && this.reason_2.get().is_none() { - // Let cloneResult be CloneAsUint8Array(chunk). - match clone_as_uint8_array(ctx.clone(), &constructor_uint8array, &function_array_buffer_is_view, chunk) { - // If cloneResult is an abrupt completion, - Err(Error::Exception) => { - let err = ctx.catch(); - - let objects_1 = - ReadableStreamObjects::from_class(this.objects_class_1); - - // Perform ! ReadableByteStreamControllerError(branch1.[[controller]], cloneResult.[[Value]]). - ReadableByteStreamController::readable_byte_stream_controller_error( - objects_1, - err.clone(), - )?; - - let objects_2 = - ReadableStreamObjects::from_class(this.objects_class_2); - - // Perform ! ReadableByteStreamControllerError(branch2.[[controller]], cloneResult.[[Value]]). - ReadableByteStreamController::readable_byte_stream_controller_error( - objects_2, - err.clone(), - )?; - - // Resolve cancelPromise with ! ReadableStreamCancel(stream, cloneResult.[[Value]]). - let (promise, _) = ReadableStream::readable_stream_cancel( - ctx, - ReadableStreamObjects::from_class(objects_class), - err.clone(), - )?; - this.cancel_promise.resolve(promise)?; - - // Return. - return Ok(()); - }, - // Otherwise, set chunk2 to cloneResult.[[Value]]. - Ok(clone_result) => chunk_2 = clone_result, - Err(err) => return Err(err), - }; - } - - // If canceled1 is false, perform ! ReadableByteStreamControllerEnqueue(branch1.[[controller]], chunk1). - if this.reason_1.get().is_none() { - let objects_1 = ReadableStreamObjects::from_class_no_reader( - this.objects_class_1.clone(), - ).refresh_reader(); - ReadableByteStreamController::readable_byte_stream_controller_enqueue( - &ctx, objects_1, chunk_1, - )?; - } - - // If canceled2 is false, perform ! ReadableByteStreamControllerEnqueue(branch2.[[controller]], chunk2). - if this.reason_2.get().is_none() { - let objects_2 = ReadableStreamObjects::from_class_no_reader( - this.objects_class_2.clone(), - ).refresh_reader(); - ReadableByteStreamController::readable_byte_stream_controller_enqueue( - &ctx, objects_2, chunk_2, - )?; - } - - // Set reading to false. - this.reading.store(false, Ordering::Release); - - let objects_1 = ReadableStreamObjects::from_class(this.objects_class_1); - let objects_2 = ReadableStreamObjects::from_class(this.objects_class_2); - - let objects = ReadableStreamObjects::from_class_no_reader(objects_class); - - // If readAgainForBranch1 is true, perform pull1Algorithm. - if this.read_again_for_branch_1.load(Ordering::Acquire) { - ReadableStream::readable_byte_stream_pull_1_algorithm( - ctx.clone(), - objects, - this.reader, - this.reading, - this.read_again_for_branch_1, - this.read_again_for_branch_2, - this.reason_1, - this.reason_2, - objects_1, - objects_2, - this.cancel_promise, - )?; - } else if this.read_again_for_branch_2.load(Ordering::Acquire) { - // Otherwise, if readAgainForBranch2 is true, perform pull2Algorithm. - ReadableStream::readable_byte_stream_pull_2_algorithm( - ctx.clone(), - objects, - this.reader, - this.reading, - this.read_again_for_branch_1, - this.read_again_for_branch_2, - this.reason_1, - this.reason_2, - objects_1, - objects_2, - this.cancel_promise, - )?; - } - - Ok(()) - } - }; - - let () = Function::new(ctx, OnceFn::new(f))?.defer(())?; - - let objects = ReadableStreamObjects::from_class(objects_class); - - Ok(objects) - }) - } - - fn close_steps( - &self, - ctx: &Ctx<'js>, - objects: ReadableStreamDefaultReaderObjects<'js>, - ) -> Result> { - // Set reading to false. - self.reading.store(false, Ordering::Release); - - let mut objects_1 = - ReadableStreamObjects::from_class_no_reader(self.objects_class_1.clone()) - .refresh_reader(); - - let mut objects_2 = - ReadableStreamObjects::from_class_no_reader(self.objects_class_2.clone()) - .refresh_reader(); - - // If canceled1 is false, perform ! ReadableByteStreamControllerClose(branch1.[[controller]]). - if self.reason_1.get().is_none() { - objects_1 = - ReadableByteStreamController::readable_byte_stream_controller_close( - ctx.clone(), - objects_1, - )?; - } - // If canceled2 is false, perform ! ReadableByteStreamControllerClose(branch2.[[controller]]). - if self.reason_2.get().is_none() { - objects_2 = - ReadableByteStreamController::readable_byte_stream_controller_close( - ctx.clone(), - objects_2, - )?; - } - // If branch1.[[controller]].[[pendingPullIntos]] is not empty, perform ! ReadableByteStreamControllerRespond(branch1.[[controller]], 0). - if !objects_1.controller.pending_pull_intos.is_empty() { - ReadableByteStreamController::readable_byte_stream_controller_respond( - ctx.clone(), - objects_1, - 0, - )? - } - - // If branch2.[[controller]].[[pendingPullIntos]] is not empty, perform ! ReadableByteStreamControllerRespond(branch2.[[controller]], 0). - if !objects_2.controller.pending_pull_intos.is_empty() { - ReadableByteStreamController::readable_byte_stream_controller_respond( - ctx.clone(), - objects_2, - 0, - )? - } - - // If canceled1 is false or canceled2 is false, resolve cancelPromise with undefined. - if self.reason_1.get().is_none() || self.reason_2.get().is_none() { - self.cancel_promise.resolve_undefined()? - } - Ok(objects) - } - - fn error_steps( - &self, - objects: ReadableStreamDefaultReaderObjects<'js>, - _: Value<'js>, - ) -> Result> { - // Set reading to false. - self.reading.store(false, Ordering::Release); - Ok(objects) - } - } - - // Perform ! ReadableStreamDefaultReaderRead(reader, readRequest). - Ok( - ReadableStreamDefaultReader::readable_stream_default_reader_read( - &ctx, - objects.set_reader(OwnedBorrowMut::from_class(current_reader)), - ReadRequest { - reader, - reading, - read_again_for_branch_1, - read_again_for_branch_2, - reason_1, - reason_2, - objects_class_1, - objects_class_2, - cancel_promise, - }, - )? - .clear_reader(), - ) - } - - #[allow(clippy::too_many_arguments)] - fn readable_byte_stream_pull_with_byob_reader( - ctx: Ctx<'js>, - mut objects: ReadableByteStreamObjects<'js, UndefinedReader>, - reader: Rc>>, - reading: Rc, - read_again_for_branch_1: Rc, - read_again_for_branch_2: Rc, - reason_1: Rc>>, - reason_2: Rc>>, - objects_1: ReadableByteStreamObjects<'js, UndefinedReader>, - objects_2: ReadableByteStreamObjects<'js, UndefinedReader>, - cancel_promise: ResolveablePromise<'js>, - view: ViewBytes<'js>, - for_branch_2: bool, - ) -> Result> { - let objects_1 = objects_1.into_inner(); - let objects_2 = objects_2.into_inner(); - - // If reader implements ReadableStreamDefaultReader, - let current_reader = reader.borrow().clone(); - let current_reader = match current_reader { - ReadableStreamReaderClass::ReadableStreamDefaultReader(r) => { - let default_reader = OwnedBorrowMut::from_class(r.clone()); - - // Perform ! ReadableStreamDefaultReaderRelease(reader). - objects = ReadableStreamDefaultReader::readable_stream_default_reader_release( - objects.set_reader(default_reader), - )? - .clear_reader(); - - // Set reader to ! AcquireReadableStreamBYOBReader(stream). - let (s, new_reader) = - ReadableStreamReaderClass::acquire_readable_stream_byob_reader( - ctx.clone(), - objects.stream, - )?; - objects.stream = s; - reader.replace(new_reader.clone().into()); - - // Perform forwardReaderError, given reader. - Self::readable_byte_stream_forward_reader_error( - ctx.clone(), - reader.clone(), - objects_1.clone(), - objects_2.clone(), - reason_1.clone(), - reason_2.clone(), - new_reader.clone().into(), - cancel_promise.clone(), - )?; - - new_reader - } - ReadableStreamReaderClass::ReadableStreamBYOBReader(r) => r.clone(), - }; - - // Let byobBranch be branch2 if forBranch2 is true, and branch1 otherwise. - // Let otherBranch be branch2 if forBranch2 is false, and branch1 otherwise. - let (byob_objects, other_objects) = if for_branch_2 { - (objects_2.clone(), objects_1.clone()) - } else { - (objects_1.clone(), objects_2.clone()) - }; - - // Let readIntoRequest be a read-into request with the following items: - #[derive(Clone)] - struct ReadIntoRequest<'js> { - reader: Rc>>, - reading: Rc, - read_again_for_branch_1: Rc, - read_again_for_branch_2: Rc, - reason_1: Rc>>, - reason_2: Rc>>, - objects_1: ReadableStreamClassObjects< - 'js, - ReadableByteStreamControllerOwned<'js>, - UndefinedReader, - >, - objects_2: ReadableStreamClassObjects< - 'js, - ReadableByteStreamControllerOwned<'js>, - UndefinedReader, - >, - byob_objects: ReadableStreamClassObjects< - 'js, - ReadableByteStreamControllerOwned<'js>, - UndefinedReader, - >, - other_objects: ReadableStreamClassObjects< - 'js, - ReadableByteStreamControllerOwned<'js>, - UndefinedReader, - >, - cancel_promise: ResolveablePromise<'js>, - for_branch_2: bool, - } - - impl<'js> Trace<'js> for ReadIntoRequest<'js> { - fn trace<'a>(&self, tracer: rquickjs::class::Tracer<'a, 'js>) { - if let Ok(r) = self.reader.try_borrow() { - r.trace(tracer) - } - if let Some(r) = self.reason_1.get() { - r.trace(tracer) - } - if let Some(r) = self.reason_2.get() { - r.trace(tracer) - } - self.objects_1.trace(tracer); - self.objects_2.trace(tracer); - self.byob_objects.trace(tracer); - self.other_objects.trace(tracer); - self.cancel_promise.trace(tracer); - } - } - - impl<'js> ReadableStreamReadIntoRequest<'js> for ReadIntoRequest<'js> { - fn chunk_steps( - &self, - objects: ReadableStreamBYOBObjects<'js>, - chunk: Value<'js>, - ) -> Result> { - let ctx = chunk.ctx().clone(); - - let constructor_uint8array = objects - .controller - .array_constructor_primordials - .constructor_uint8array - .clone(); - let function_array_buffer_is_view = - objects.controller.function_array_buffer_is_view.clone(); - let chunk = - ViewBytes::from_value(&ctx, &function_array_buffer_is_view, Some(&chunk))?; - - let objects_class = objects.into_inner(); - - // Queue a microtask to perform the following steps: - let f = { - let ctx = ctx.clone(); - let objects_class = objects_class.clone(); - let this = self.clone(); - move || -> Result<()> { - // Set readAgainForBranch1 to false. - this.read_again_for_branch_1.store(false, Ordering::Release); - // Set readAgainForBranch2 to false. - this.read_again_for_branch_2.store(false, Ordering::Release); - - // Let byobCanceled be canceled2 if forBranch2 is true, and canceled1 otherwise. - // Let otherCanceled be canceled2 if forBranch2 is false, and canceled1 otherwise. - let (byob_canceled, other_canceled) = if this.for_branch_2 { - (this.reason_2.get().is_some(), this.reason_1.get().is_some()) - } else { - (this.reason_1.get().is_some(), this.reason_2.get().is_some()) - }; - - // If otherCanceled is false, - if !other_canceled { - // Let cloneResult be CloneAsUint8Array(chunk). - match clone_as_uint8_array( - ctx.clone(), - &constructor_uint8array, - &function_array_buffer_is_view, - chunk.clone(), - ) { - // If cloneResult is an abrupt completion, - Err(Error::Exception) => { - let err = ctx.catch(); - - let byob_objects = ReadableStreamObjects::from_class_no_reader( - this.byob_objects.clone(), - ) - .refresh_reader(); - - // Perform ! ReadableByteStreamControllerError(byobBranch.[[controller]], cloneResult.[[Value]]). - ReadableByteStreamController::readable_byte_stream_controller_error( - byob_objects, - err.clone(), - )?; - - let other_objects = - ReadableStreamObjects::from_class_no_reader( - this.other_objects.clone(), - ) - .refresh_reader(); - - // Perform ! ReadableByteStreamControllerError(otherBranch.[[controller]], cloneResult.[[Value]]). - ReadableByteStreamController::readable_byte_stream_controller_error( - other_objects, - err.clone(), - )?; - - // Resolve cancelPromise with ! ReadableStreamCancel(stream, cloneResult.[[Value]]). - let (promise, _) = ReadableStream::readable_stream_cancel( - ctx, - ReadableStreamObjects::from_class(objects_class), - err.clone(), - )?; - this.cancel_promise.resolve(promise)?; - - // Return. - return Ok(()); - } - // Otherwise, let clonedChunk be cloneResult.[[Value]]. - Ok(cloned_chunk) => { - // If byobCanceled is false, perform ! ReadableByteStreamControllerRespondWithNewView(byobBranch.[[controller]], chunk). - if !byob_canceled { - let byob_objects = - ReadableStreamObjects::from_class_no_reader( - this.byob_objects.clone(), - ) - .refresh_reader(); - - ReadableByteStreamController::readable_byte_stream_controller_respond_with_new_view(ctx.clone(), byob_objects, chunk)?; - } - - let other_objects = - ReadableStreamObjects::from_class_no_reader( - this.other_objects.clone(), - ) - .refresh_reader(); - - // Perform ! ReadableByteStreamControllerEnqueue(otherBranch.[[controller]], clonedChunk). - ReadableByteStreamController::readable_byte_stream_controller_enqueue(&ctx, other_objects, cloned_chunk)?; - } - Err(err) => return Err(err), - }; - } else if !byob_canceled { - let byob_objects = ReadableStreamObjects::from_class_no_reader( - this.byob_objects.clone(), - ) - .refresh_reader(); - - // Otherwise, if byobCanceled is false, perform ! ReadableByteStreamControllerRespondWithNewView(byobBranch.[[controller]], chunk). - ReadableByteStreamController::readable_byte_stream_controller_respond_with_new_view(ctx.clone(), byob_objects, chunk)?; - } - - let objects_1 = ReadableStreamObjects::from_class(this.objects_1.clone()); - let objects_2 = ReadableStreamObjects::from_class(this.objects_2.clone()); - - // Set reading to false. - this.reading.store(false, Ordering::Release); - - // If readAgainForBranch1 is true, perform pull1Algorithm. - if this.read_again_for_branch_1.load(Ordering::Acquire) { - ReadableStream::readable_byte_stream_pull_1_algorithm( - ctx.clone(), - ReadableStreamObjects::from_class(objects_class).clear_reader(), - this.reader.clone(), - this.reading.clone(), - this.read_again_for_branch_1.clone(), - this.read_again_for_branch_2.clone(), - this.reason_1.clone(), - this.reason_2.clone(), - objects_1, - objects_2, - this.cancel_promise.clone(), - )?; - } else if this.read_again_for_branch_2.load(Ordering::Acquire) { - // Otherwise, if readAgainForBranch2 is true, perform pull2Algorithm. - ReadableStream::readable_byte_stream_pull_2_algorithm( - ctx.clone(), - ReadableStreamObjects::from_class(objects_class).clear_reader(), - this.reader.clone(), - this.reading.clone(), - this.read_again_for_branch_1.clone(), - this.read_again_for_branch_2.clone(), - this.reason_1.clone(), - this.reason_2.clone(), - objects_1, - objects_2, - this.cancel_promise.clone(), - )?; - } - - Ok(()) - } - }; - - let () = Function::new(ctx, OnceFn::new(f))?.defer(())?; - - let objects = ReadableStreamObjects::from_class(objects_class); - - Ok(objects) - } - - fn close_steps( - &self, - objects: ReadableStreamBYOBObjects<'js>, - chunk: Value<'js>, - ) -> Result> { - let ctx = chunk.ctx().clone(); - - // Set reading to false. - self.reading.store(false, Ordering::Release); - - // Let byobCanceled be canceled2 if forBranch2 is true, and canceled1 otherwise. - // Let otherCanceled be canceled2 if forBranch2 is false, and canceled1 otherwise. - let (byob_canceled, other_canceled) = if self.for_branch_2 { - (self.reason_2.get().is_some(), self.reason_1.get().is_some()) - } else { - (self.reason_1.get().is_some(), self.reason_2.get().is_some()) - }; - - // If byobCanceled is false, perform ! ReadableByteStreamControllerClose(byobBranch.[[controller]]). - if !byob_canceled { - let byob_objects = - ReadableStreamObjects::from_class_no_reader(self.byob_objects.clone()) - .refresh_reader(); - - ReadableByteStreamController::readable_byte_stream_controller_close( - ctx.clone(), - byob_objects, - )?; - } - // If otherCanceled is false, perform ! ReadableByteStreamControllerClose(otherBranch.[[controller]]). - if !other_canceled { - let other_objects = - ReadableStreamObjects::from_class_no_reader(self.other_objects.clone()) - .refresh_reader(); - - ReadableByteStreamController::readable_byte_stream_controller_close( - ctx.clone(), - other_objects, - )?; - } - - // If chunk is not undefined, - if !chunk.is_undefined() { - let chunk = ViewBytes::from_value( - &ctx, - &objects.controller.function_array_buffer_is_view, - Some(&chunk), - )?; - - // If byobCanceled is false, perform ! ReadableByteStreamControllerRespondWithNewView(byobBranch.[[controller]], chunk). - if !byob_canceled { - let byob_objects = - ReadableStreamObjects::from_class_no_reader(self.byob_objects.clone()) - .refresh_reader(); - - ReadableByteStreamController::readable_byte_stream_controller_respond_with_new_view(ctx.clone(), byob_objects, chunk)?; - } - - let other_objects = - ReadableStreamObjects::from_class_no_reader(self.other_objects.clone()) - .refresh_reader(); - - // If otherCanceled is false and otherBranch.[[controller]].[[pendingPullIntos]] is not empty, perform ! ReadableByteStreamControllerRespond(otherBranch.[[controller]], 0). - if !other_canceled && !other_objects.controller.pending_pull_intos.is_empty() { - ReadableByteStreamController::readable_byte_stream_controller_respond( - ctx.clone(), - other_objects, - 0, - )?; - } - } - - // If byobCanceled is false or otherCanceled is false, resolve cancelPromise with undefined. - if !byob_canceled || !other_canceled { - self.cancel_promise.resolve_undefined()? - } - - Ok(objects) - } - - fn error_steps( - &self, - objects: ReadableStreamBYOBObjects<'js>, - _: Value<'js>, - ) -> Result> { - // Set reading to false. - self.reading.store(false, Ordering::Release); - Ok(objects) - } - } - - // Perform ! ReadableStreamBYOBReaderRead(reader, view, 1, readIntoRequest). - Ok(ReadableStreamBYOBReader::readable_stream_byob_reader_read( - &ctx, - objects.set_reader(OwnedBorrowMut::from_class(current_reader)), - view, - 1, - ReadIntoRequest { - reader, - reading, - read_again_for_branch_1, - read_again_for_branch_2, - reason_1, - reason_2, - objects_1, - objects_2, - byob_objects, - other_objects, - cancel_promise, - for_branch_2, - }, - )? - .clear_reader()) - } - - // Let pull1Algorithm be the following steps: - #[allow(clippy::too_many_arguments)] - fn readable_byte_stream_pull_1_algorithm( - ctx: Ctx<'js>, - mut objects: ReadableByteStreamObjects<'js, UndefinedReader>, - reader: Rc>>, - reading: Rc, - read_again_for_branch_1: Rc, - read_again_for_branch_2: Rc, - reason_1: Rc>>, - reason_2: Rc>>, - mut objects_1: ReadableByteStreamObjects<'js, UndefinedReader>, - objects_2: ReadableByteStreamObjects<'js, UndefinedReader>, - cancel_promise: ResolveablePromise<'js>, - ) -> Result> { - // If reading is true, - if reading.swap(true, Ordering::AcqRel) { - // Set readAgainForBranch1 to true. - read_again_for_branch_1.store(true, Ordering::Release); - // Return a promise resolved with undefined. - return Ok(objects - .stream - .promise_primordials - .promise_resolved_with_undefined - .clone()); - } - // Set reading to true. - - // Let byobRequest be ! ReadableByteStreamControllerGetBYOBRequest(branch1.[[controller]]). - let (byob_request, branch_1_controller) = - ReadableByteStreamController::readable_byte_stream_controller_get_byob_request( - ctx.clone(), - objects_1.controller, - )?; - objects_1.controller = branch_1_controller; - - // If byobRequest is null, perform pullWithDefaultReader. - objects = match byob_request.0 { - None => Self::readable_byte_stream_pull_with_default_reader( - ctx.clone(), - objects, - reader.clone(), - reading.clone(), - read_again_for_branch_1, - read_again_for_branch_2, - reason_1, - reason_2, - objects_1, - objects_2, - cancel_promise.clone(), - )?, - // Otherwise, perform pullWithBYOBReader, given byobRequest.[[view]] and false. - Some(byob_request) => { - let view = byob_request.borrow().view.clone().expect( - "ReadableByteStream tee pull1Algorithm called with invalidated byobRequest", - ); - Self::readable_byte_stream_pull_with_byob_reader( - ctx.clone(), - objects, - reader.clone(), - reading.clone(), - read_again_for_branch_1, - read_again_for_branch_2, - reason_1, - reason_2, - objects_1, - objects_2, - cancel_promise.clone(), - view, - false, - )? - } - }; - - // Return a promise resolved with undefined. - Ok(objects - .stream - .promise_primordials - .promise_resolved_with_undefined - .clone()) - } - - // Let pull2Algorithm be the following steps: - #[allow(clippy::too_many_arguments)] - fn readable_byte_stream_pull_2_algorithm( - ctx: Ctx<'js>, - mut objects: ReadableByteStreamObjects<'js, UndefinedReader>, - reader: Rc>>, - reading: Rc, - read_again_for_branch_1: Rc, - read_again_for_branch_2: Rc, - reason_1: Rc>>, - reason_2: Rc>>, - objects_1: ReadableByteStreamObjects<'js, UndefinedReader>, - mut objects_2: ReadableByteStreamObjects<'js, UndefinedReader>, - cancel_promise: ResolveablePromise<'js>, - ) -> Result> { - // If reading is true, - if reading.swap(true, Ordering::AcqRel) { - // Set readAgainForBranch2 to true. - read_again_for_branch_2.store(true, Ordering::Release); - // Return a promise resolved with undefined. - return Ok(objects - .stream - .promise_primordials - .promise_resolved_with_undefined - .clone()); - } - // Set reading to true. - - // Let byobRequest be ! ReadableByteStreamControllerGetBYOBRequest(branch2.[[controller]]). - let (byob_request, branch_2_controller) = - ReadableByteStreamController::readable_byte_stream_controller_get_byob_request( - ctx.clone(), - objects_2.controller, - )?; - objects_2.controller = branch_2_controller; - - // If byobRequest is null, perform pullWithDefaultReader. - objects = match byob_request.0 { - None => Self::readable_byte_stream_pull_with_default_reader( - ctx.clone(), - objects, - reader.clone(), - reading.clone(), - read_again_for_branch_1, - read_again_for_branch_2, - reason_1, - reason_2, - objects_1, - objects_2, - cancel_promise, - )?, - // Otherwise, perform pullWithBYOBReader, given byobRequest.[[view]] and true. - Some(byob_request) => Self::readable_byte_stream_pull_with_byob_reader( - ctx.clone(), - objects, - reader.clone(), - reading.clone(), - read_again_for_branch_1, - read_again_for_branch_2, - reason_1, - reason_2, - objects_1, - objects_2, - cancel_promise, - byob_request.borrow().view.clone().expect( - "ReadableByteStream tee pull2Algorithm called with invalidated byobRequest", - ), - true, - )?, - }; - - // Return a promise resolved with undefined. - Ok(objects - .stream - .promise_primordials - .promise_resolved_with_undefined - .clone()) - } -} - -fn clone_as_uint8_array<'js>( - ctx: Ctx<'js>, - constructor_uint8array: &Constructor<'js>, - function_array_buffer_is_view: &Function<'js>, - chunk: ViewBytes<'js>, -) -> Result> { - let (buffer, byte_length, byte_offset) = chunk.get_array_buffer()?; - - // Let buffer be ? CloneArrayBuffer(O.[[ViewedArrayBuffer]], O.[[ByteOffset]], O.[[ByteLength]], %ArrayBuffer%). - let buffer = ArrayBuffer::new_copy( - ctx.clone(), - &buffer - .as_bytes() - .expect("CloneAsUInt8Array called on detached buffer") - [byte_offset..byte_offset + byte_length], - )?; - - // Let array be ! Construct(%Uint8Array%, « buffer »). - // Return array. - ViewBytes::from_value( - &ctx, - function_array_buffer_is_view, - Some(&constructor_uint8array.construct((buffer,))?), - ) -} diff --git a/stdlib/src/llrt/llrt_stream_web/readable_writable_pair.rs b/stdlib/src/llrt/llrt_stream_web/readable_writable_pair.rs deleted file mode 100644 index 83c16bbd..00000000 --- a/stdlib/src/llrt/llrt_stream_web/readable_writable_pair.rs +++ /dev/null @@ -1,25 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use rquickjs::{Ctx, Error, FromJs, Result, Value}; - -use crate::llrt_stream_web::{readable::ReadableStreamClass, writable::WritableStreamClass}; - -/// An object containing a pair of linked streams, one readable and one writable -/// https://streams.spec.whatwg.org/#dictdef-readablewritablepair -pub struct ReadableWritablePair<'js> { - pub readable: ReadableStreamClass<'js>, - pub writable: WritableStreamClass<'js>, -} - -impl<'js> FromJs<'js> for ReadableWritablePair<'js> { - fn from_js(_ctx: &Ctx<'js>, value: Value<'js>) -> Result { - let ty_name = value.type_name(); - let obj = value - .as_object() - .ok_or(Error::new_from_js(ty_name, "Object"))?; - - let readable = obj.get::<_, ReadableStreamClass<'js>>("readable")?; - let writable = obj.get::<_, WritableStreamClass<'js>>("writable")?; - - Ok(Self { readable, writable }) - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/transform/controller.rs b/stdlib/src/llrt/llrt_stream_web/transform/controller.rs deleted file mode 100644 index ce2f3cda..00000000 --- a/stdlib/src/llrt/llrt_stream_web/transform/controller.rs +++ /dev/null @@ -1,308 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use rquickjs::{ - class::{OwnedBorrowMut, Trace}, - prelude::{Opt, This}, - Class, Ctx, Exception, Function, JsLifetime, Object, Promise, Result, Value, -}; - -use crate::llrt_stream_web::{ - readable::{ - readable_stream_default_controller_close_stream, - readable_stream_default_controller_enqueue_value, - readable_stream_default_controller_error_stream, ReadableStreamDefaultControllerClass, - }, - utils::promise::{promise_resolved_with, ResolveablePromise}, -}; - -use crate::llrt_utils::primordials::Primordial; - -use super::stream::TransformStreamClass; - -#[rquickjs::class] -#[derive(JsLifetime, Trace)] -pub(crate) struct TransformStreamDefaultController<'js> { - pub(super) stream: TransformStreamClass<'js>, - pub(super) transform_algorithm: Option>, - pub(super) flush_algorithm: Option>, - pub(super) cancel_algorithm: Option>, - pub(super) finish_promise: Option>, -} - -pub(crate) type TransformStreamDefaultControllerClass<'js> = - Class<'js, TransformStreamDefaultController<'js>>; - -fn get_readable_default_controller<'js>( - stream_class: &TransformStreamClass<'js>, -) -> Option> { - let stream = stream_class.borrow(); - let readable = stream.readable.as_ref()?; - let readable = readable.borrow(); - match &readable.controller { - crate::llrt_stream_web::readable::ReadableStreamControllerClass::ReadableStreamDefaultController(c) => { - Some(c.clone()) - }, - _ => None, - } -} - -#[rquickjs::methods(rename_all = "camelCase")] -impl<'js> TransformStreamDefaultController<'js> { - #[qjs(constructor)] - fn new(ctx: Ctx<'js>) -> Result> { - Err(Exception::throw_type(&ctx, "Illegal constructor")) - } - - #[qjs(get)] - fn desired_size(&self) -> Option { - let stream = self.stream.borrow(); - let readable_class = stream.readable.as_ref()?; - let readable = readable_class.borrow(); - match &readable.controller { - crate::llrt_stream_web::readable::ReadableStreamControllerClass::ReadableStreamDefaultController(c) => { - let c = c.borrow(); - c.readable_stream_default_controller_get_desired_size(&readable) - .0 - }, - _ => None, - } - } - - fn enqueue( - ctx: Ctx<'js>, - this: This>, - chunk: Opt>, - ) -> Result<()> { - let chunk = chunk.0.unwrap_or_else(|| Value::new_undefined(ctx.clone())); - let stream_class = this.stream.clone(); - drop(this); - transform_stream_default_controller_enqueue(ctx, &stream_class, chunk) - } - - fn error( - ctx: Ctx<'js>, - this: This>, - reason: Opt>, - ) -> Result<()> { - let reason = reason - .0 - .unwrap_or_else(|| Value::new_undefined(ctx.clone())); - let stream_class = this.stream.clone(); - drop(this); - transform_stream_error(ctx, &stream_class, reason) - } - - fn terminate(ctx: Ctx<'js>, this: This>) -> Result<()> { - let stream_class = this.stream.clone(); - drop(this); - transform_stream_default_controller_terminate(ctx, &stream_class) - } -} - -impl<'js> TransformStreamDefaultController<'js> { - pub(super) fn clear_algorithms(&mut self) { - self.transform_algorithm = None; - self.flush_algorithm = None; - self.cancel_algorithm = None; - } -} - -#[derive(Trace, JsLifetime, Clone)] -pub(super) enum TransformAlgorithm<'js> { - Identity, - Function { - f: Function<'js>, - transformer: Option>, - }, -} - -#[derive(Trace, JsLifetime, Clone)] -pub(super) enum FlushAlgorithm<'js> { - Noop, - Function { - f: Function<'js>, - transformer: Option>, - }, -} - -#[derive(Trace, JsLifetime, Clone)] -pub(super) enum CancelAlgorithm<'js> { - Noop, - Function { - f: Function<'js>, - transformer: Option>, - }, -} - -// --- Abstract operations --- - -pub(super) fn transform_stream_default_controller_enqueue<'js>( - ctx: Ctx<'js>, - stream_class: &TransformStreamClass<'js>, - chunk: Value<'js>, -) -> Result<()> { - let controller_class = get_readable_default_controller(stream_class) - .ok_or_else(|| Exception::throw_type(&ctx, "readable controller not available"))?; - - readable_stream_default_controller_enqueue_value(ctx.clone(), controller_class.clone(), chunk)?; - - // Update backpressure - let has_backpressure = { - let stream = stream_class.borrow(); - let readable_class = stream.readable.as_ref().unwrap(); - let readable = readable_class.borrow(); - let c = controller_class.borrow(); - let desired = c.readable_stream_default_controller_get_desired_size(&readable); - desired.0.is_none_or(|size| size <= 0.0) - }; - - let current_bp = stream_class.borrow().backpressure; - if has_backpressure != current_bp { - transform_stream_set_backpressure(&ctx, stream_class, true)?; - } - - Ok(()) -} - -pub(super) fn transform_stream_default_controller_terminate<'js>( - ctx: Ctx<'js>, - stream_class: &TransformStreamClass<'js>, -) -> Result<()> { - let controller_class = get_readable_default_controller(stream_class) - .ok_or_else(|| Exception::throw_type(&ctx, "readable controller not available"))?; - - readable_stream_default_controller_close_stream(ctx.clone(), controller_class)?; - - let error = ctx.eval::("new TypeError('TransformStream terminated')")?; - transform_stream_error_writable_and_unblock_write(stream_class, error)?; - Ok(()) -} - -pub(super) fn transform_stream_error<'js>( - ctx: Ctx<'js>, - stream_class: &TransformStreamClass<'js>, - e: Value<'js>, -) -> Result<()> { - let controller_class = get_readable_default_controller(stream_class) - .ok_or_else(|| Exception::throw_type(&ctx, "readable controller not available"))?; - - readable_stream_default_controller_error_stream(controller_class, e.clone())?; - transform_stream_error_writable_and_unblock_write(stream_class, e)?; - Ok(()) -} - -pub(super) fn transform_stream_error_writable_and_unblock_write<'js>( - stream_class: &TransformStreamClass<'js>, - _e: Value<'js>, -) -> Result<()> { - let mut stream = stream_class.borrow_mut(); - if let Some(ref controller_class) = stream.controller { - controller_class.borrow_mut().clear_algorithms(); - } - // Always resolve and clear backpressure promise to break reference cycles - if let Some(ref bp) = stream.backpressure_change_promise { - bp.resolve_undefined()?; - } - stream.backpressure_change_promise = None; - stream.backpressure = false; - Ok(()) -} - -pub(super) fn transform_stream_set_backpressure<'js>( - ctx: &Ctx<'js>, - stream_class: &TransformStreamClass<'js>, - backpressure: bool, -) -> Result> { - let new_bp_promise = ResolveablePromise::new(ctx)?; - let promise = new_bp_promise.promise.clone(); - let mut stream = stream_class.borrow_mut(); - if let Some(ref bp_promise) = stream.backpressure_change_promise { - bp_promise.resolve_undefined()?; - } - stream.backpressure_change_promise = Some(new_bp_promise); - stream.backpressure = backpressure; - Ok(promise) -} - -pub(super) fn transform_stream_default_controller_perform_transform<'js>( - ctx: Ctx<'js>, - stream_class: &TransformStreamClass<'js>, - controller_class: &TransformStreamDefaultControllerClass<'js>, - chunk: Value<'js>, -) -> Result> { - let controller = controller_class.borrow(); - let algorithm = controller - .transform_algorithm - .clone() - .expect("transform algorithm must exist"); - drop(controller); - - let promise_primordials = - crate::llrt_stream_web::utils::promise::PromisePrimordials::get(&ctx)?.clone(); - - let transform_promise = match algorithm { - TransformAlgorithm::Identity => { - let result = - transform_stream_default_controller_enqueue(ctx.clone(), stream_class, chunk); - promise_resolved_with( - &ctx, - &promise_primordials, - result.map(|_| Value::new_undefined(ctx.clone())), - )? - } - TransformAlgorithm::Function { f, transformer } => { - let result: Result = - f.call((This(transformer), chunk, controller_class.clone())); - promise_resolved_with(&ctx, &promise_primordials, result)? - } - }; - - Ok(transform_promise) -} - -pub(super) fn perform_flush<'js>( - ctx: Ctx<'js>, - _stream_class: &TransformStreamClass<'js>, - controller_class: &TransformStreamDefaultControllerClass<'js>, -) -> Result> { - let controller = controller_class.borrow(); - let algorithm = controller - .flush_algorithm - .clone() - .unwrap_or(FlushAlgorithm::Noop); - drop(controller); - - let promise_primordials = - crate::llrt_stream_web::utils::promise::PromisePrimordials::get(&ctx)?.clone(); - - match algorithm { - FlushAlgorithm::Noop => Ok(promise_primordials.promise_resolved_with_undefined.clone()), - FlushAlgorithm::Function { f, transformer } => { - let result: Result = f.call((This(transformer), controller_class.clone())); - promise_resolved_with(&ctx, &promise_primordials, result) - } - } -} - -pub(super) fn perform_cancel<'js>( - ctx: Ctx<'js>, - controller_class: &TransformStreamDefaultControllerClass<'js>, - reason: Value<'js>, -) -> Result> { - let controller = controller_class.borrow(); - let algorithm = controller - .cancel_algorithm - .clone() - .unwrap_or(CancelAlgorithm::Noop); - drop(controller); - - let promise_primordials = - crate::llrt_stream_web::utils::promise::PromisePrimordials::get(&ctx)?.clone(); - - match algorithm { - CancelAlgorithm::Noop => Ok(promise_primordials.promise_resolved_with_undefined.clone()), - CancelAlgorithm::Function { f, transformer } => { - let result: Result = f.call((This(transformer), reason)); - promise_resolved_with(&ctx, &promise_primordials, result) - } - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/transform/mod.rs b/stdlib/src/llrt/llrt_stream_web/transform/mod.rs deleted file mode 100644 index 7ce3b230..00000000 --- a/stdlib/src/llrt/llrt_stream_web/transform/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -pub(crate) mod controller; -pub(crate) mod stream; -#[cfg(test)] -mod tests; -mod transformer; - -pub(crate) use controller::TransformStreamDefaultController; -pub(crate) use stream::TransformStream; diff --git a/stdlib/src/llrt/llrt_stream_web/transform/stream.rs b/stdlib/src/llrt/llrt_stream_web/transform/stream.rs deleted file mode 100644 index 23e2c8e6..00000000 --- a/stdlib/src/llrt/llrt_stream_web/transform/stream.rs +++ /dev/null @@ -1,352 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use crate::llrt_utils::option::Undefined; -use rquickjs::{ - class::Trace, - prelude::{Opt, This}, - Class, Ctx, Exception, JsLifetime, Object, Promise, Result, Value, -}; - -use crate::llrt_stream_web::{ - queuing_strategy::QueuingStrategy, - readable::stream::{ - algorithms::{CancelAlgorithm, PullAlgorithm, StartAlgorithm}, - ReadableStream, - }, - utils::promise::ResolveablePromise, - writable::WritableStream, -}; - -use super::{ - controller::{ - self, CancelAlgorithm as TsCancelAlgorithm, FlushAlgorithm, TransformAlgorithm, - TransformStreamDefaultController, TransformStreamDefaultControllerClass, - }, - transformer::Transformer, -}; - -#[rquickjs::class] -#[derive(JsLifetime, Trace)] -pub(crate) struct TransformStream<'js> { - pub(super) readable: Option>>, - pub(super) writable: Option>>, - pub(super) controller: Option>, - pub(super) backpressure: bool, - pub(super) backpressure_change_promise: Option>, -} - -pub(crate) type TransformStreamClass<'js> = Class<'js, TransformStream<'js>>; - -#[rquickjs::methods(rename_all = "camelCase")] -impl<'js> TransformStream<'js> { - pub(crate) fn from_transformer( - ctx: Ctx<'js>, - transformer: Object<'js>, - ) -> Result> { - Self::new( - ctx, - Opt(Some(Undefined(Some(transformer)))), - Opt(None), - Opt(None), - ) - } - - #[qjs(constructor)] - fn new( - ctx: Ctx<'js>, - transformer: Opt>>, - writable_strategy: Opt>>, - readable_strategy: Opt>>, - ) -> Result> { - let transformer_obj = transformer.0.and_then(|u| u.0); - let transformer_dict = transformer_obj - .as_ref() - .map(|obj| Transformer::from_object(obj.clone())) - .transpose()? - .unwrap_or_default(); - - if transformer_dict.readable_type { - return Err(Exception::throw_range( - &ctx, - "readableType is not supported", - )); - } - if transformer_dict.writable_type { - return Err(Exception::throw_range( - &ctx, - "writableType is not supported", - )); - } - - let readable_strategy = readable_strategy.0.and_then(|qs| qs.0); - let writable_strategy = writable_strategy.0.and_then(|qs| qs.0); - - let readable_size = QueuingStrategy::extract_size_algorithm(readable_strategy.as_ref()); - let writable_size = QueuingStrategy::extract_size_algorithm(writable_strategy.as_ref()); - let readable_hwm = QueuingStrategy::extract_high_water_mark(&ctx, readable_strategy, 0.0)?; - let writable_hwm = QueuingStrategy::extract_high_water_mark(&ctx, writable_strategy, 1.0)?; - - // Create the TransformStream instance - let stream_class = Class::instance( - ctx.clone(), - Self { - readable: None, - writable: None, - controller: None, - backpressure: true, - backpressure_change_promise: None, - }, - )?; - - // Initial backpressure change promise - let bp_promise = ResolveablePromise::new(&ctx)?; - stream_class.borrow_mut().backpressure_change_promise = Some(bp_promise); - - // Build controller algorithms - let transform_algorithm = transformer_dict - .transform - .map(|f| TransformAlgorithm::Function { - f, - transformer: transformer_obj.clone(), - }) - .unwrap_or(TransformAlgorithm::Identity); - - let flush_algorithm = transformer_dict - .flush - .map(|f| FlushAlgorithm::Function { - f, - transformer: transformer_obj.clone(), - }) - .unwrap_or(FlushAlgorithm::Noop); - - let cancel_algorithm = transformer_dict - .cancel - .map(|f| TsCancelAlgorithm::Function { - f, - transformer: transformer_obj.clone(), - }) - .unwrap_or(TsCancelAlgorithm::Noop); - - // Create controller - let controller_class = Class::instance( - ctx.clone(), - TransformStreamDefaultController { - stream: stream_class.clone(), - transform_algorithm: Some(transform_algorithm), - flush_algorithm: Some(flush_algorithm), - cancel_algorithm: Some(cancel_algorithm), - finish_promise: None, - }, - )?; - stream_class.borrow_mut().controller = Some(controller_class.clone()); - - // Start promise - let start_promise = ResolveablePromise::new(&ctx)?; - - // --- Create writable side with properly traced algorithm variants --- - let writable_class = WritableStream::create_for_transform( - ctx.clone(), - start_promise.promise.clone(), - stream_class.clone(), - controller_class.clone(), - writable_hwm, - writable_size, - )?; - - // --- Create readable side --- - let pull_algorithm = PullAlgorithm::Transform(stream_class.clone()); - - let cancel_algo = CancelAlgorithm::Transform { - stream: stream_class.clone(), - controller: controller_class.clone(), - }; - - let readable_objects = ReadableStream::create_readable_stream( - ctx.clone(), - StartAlgorithm::ReturnUndefined, - pull_algorithm, - cancel_algo, - Some(readable_hwm), - Some(readable_size), - )?; - - { - let mut stream = stream_class.borrow_mut(); - stream.readable = Some(readable_objects.stream.clone()); - stream.writable = Some(writable_class); - } - - // Invoke start() if present - if let Some(start_fn) = transformer_dict.start { - match start_fn.call::<_, Value>((This(transformer_obj), controller_class)) { - Ok(val) => { - start_promise.resolve(val)?; - } - Err(_) => { - let err = ctx.catch(); - start_promise.reject(err)?; - } - } - } else { - start_promise.resolve_undefined()?; - } - - Ok(stream_class) - } - - #[qjs(get)] - fn readable(&self) -> Option>> { - self.readable.clone() - } - - #[qjs(get)] - fn writable(&self) -> Option>> { - self.writable.clone() - } -} - -// --- Sink algorithms --- - -pub(crate) fn sink_write_algorithm<'js>( - ctx: Ctx<'js>, - stream_class: &TransformStreamClass<'js>, - controller_class: &TransformStreamDefaultControllerClass<'js>, - chunk: Value<'js>, -) -> Result> { - let stream = stream_class.borrow(); - if stream.backpressure { - let bp_promise = stream - .backpressure_change_promise - .as_ref() - .map(|p| p.promise.clone()); - drop(stream); - - if let Some(bp_promise) = bp_promise { - let sc = stream_class.clone(); - let cc = controller_class.clone(); - return crate::llrt_stream_web::utils::promise::upon_promise::, _>( - ctx.clone(), - bp_promise, - move |ctx, _| { - let p = controller::transform_stream_default_controller_perform_transform( - ctx.clone(), - &sc, - &cc, - chunk, - )?; - Ok(p.into_value()) - }, - ); - } - } else { - drop(stream); - } - - controller::transform_stream_default_controller_perform_transform( - ctx, - stream_class, - controller_class, - chunk, - ) -} - -pub(crate) fn sink_close_algorithm<'js>( - ctx: Ctx<'js>, - stream_class: &TransformStreamClass<'js>, - controller_class: &TransformStreamDefaultControllerClass<'js>, -) -> Result> { - let flush_promise = controller::perform_flush(ctx.clone(), stream_class, controller_class)?; - - let sc = stream_class.clone(); - let cc = controller_class.clone(); - crate::llrt_stream_web::utils::promise::upon_promise::, _>( - ctx.clone(), - flush_promise, - move |ctx, result| { - cc.borrow_mut().clear_algorithms(); - match result { - Ok(_) => { - let mut stream = sc.borrow_mut(); - // Resolve any pending backpressure promise to break the cycle - if let Some(ref bp) = stream.backpressure_change_promise { - bp.resolve_undefined()?; - } - stream.backpressure_change_promise = None; - let readable_controller = stream.readable.as_ref().and_then(|readable| { - let r = readable.borrow(); - if let crate::llrt_stream_web::readable::ReadableStreamControllerClass::ReadableStreamDefaultController(c) = &r.controller { - Some(c.clone()) - } else { - None - } - }); - drop(stream); - if let Some(c) = readable_controller { - crate::llrt_stream_web::readable::readable_stream_default_controller_close_stream( - ctx.clone(), - c, - )?; - } - Ok(Value::new_undefined(ctx)) - } - Err(r) => { - controller::transform_stream_error(ctx.clone(), &sc, r.clone())?; - Err(ctx.throw(r)) - } - } - }, - ) -} - -pub(crate) fn sink_abort_algorithm<'js>( - ctx: Ctx<'js>, - controller_class: &TransformStreamDefaultControllerClass<'js>, - reason: Value<'js>, -) -> Result> { - let cancel_promise = controller::perform_cancel(ctx.clone(), controller_class, reason)?; - - let cc = controller_class.clone(); - crate::llrt_stream_web::utils::promise::upon_promise::, _>( - ctx.clone(), - cancel_promise, - move |ctx, result| { - cc.borrow_mut().clear_algorithms(); - match result { - Ok(_) => Ok(Value::new_undefined(ctx)), - Err(r) => Err(ctx.throw(r)), - } - }, - ) -} - -// --- Source algorithms --- - -pub(crate) fn source_pull_algorithm<'js>( - ctx: Ctx<'js>, - stream_class: &TransformStreamClass<'js>, -) -> Result> { - controller::transform_stream_set_backpressure(&ctx, stream_class, false) -} - -pub(crate) fn source_cancel_algorithm<'js>( - ctx: Ctx<'js>, - stream_class: &TransformStreamClass<'js>, - controller_class: &TransformStreamDefaultControllerClass<'js>, - reason: Value<'js>, -) -> Result> { - let cancel_promise = controller::perform_cancel(ctx.clone(), controller_class, reason.clone())?; - - let sc = stream_class.clone(); - let cc = controller_class.clone(); - crate::llrt_stream_web::utils::promise::upon_promise::, _>( - ctx.clone(), - cancel_promise, - move |ctx, result| { - cc.borrow_mut().clear_algorithms(); - controller::transform_stream_error_writable_and_unblock_write(&sc, reason)?; - match result { - Ok(_) => Ok(Value::new_undefined(ctx)), - Err(r) => Err(ctx.throw(r)), - } - }, - ) -} diff --git a/stdlib/src/llrt/llrt_stream_web/transform/tests.rs b/stdlib/src/llrt/llrt_stream_web/transform/tests.rs deleted file mode 100644 index 25aff6c9..00000000 --- a/stdlib/src/llrt/llrt_stream_web/transform/tests.rs +++ /dev/null @@ -1,440 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use crate::llrt_test::test_async_with; -use rquickjs::Promise; - -fn eval_async<'js>(ctx: &rquickjs::Ctx<'js>, js: &str) -> rquickjs::Result> { - ctx.eval(format!("(async () => {{ {js} }})()")) -} - -#[tokio::test] -async fn identity_passthrough() { - test_async_with(|ctx| { - crate::llrt_stream_web::init(&ctx).unwrap(); - Box::pin(async move { - eval_async( - &ctx, - r#" - const ts = new TransformStream(); - const writer = ts.writable.getWriter(); - const reader = ts.readable.getReader(); - - writer.write("one"); - writer.write("two"); - writer.close(); - - const chunks = []; - while (true) { - const { value, done } = await reader.read(); - if (done) break; - chunks.push(value); - } - if (chunks.join(",") !== "one,two") throw new Error("got: " + chunks); - "#, - ) - .unwrap() - .into_future::<()>() - .await - .unwrap(); - }) - }) - .await; -} - -#[tokio::test] -async fn transform_chunks() { - test_async_with(|ctx| { - crate::llrt_stream_web::init(&ctx).unwrap(); - Box::pin(async move { - eval_async( - &ctx, - r#" - const ts = new TransformStream({ - transform(chunk, controller) { - controller.enqueue(chunk.toUpperCase()); - } - }); - const writer = ts.writable.getWriter(); - const reader = ts.readable.getReader(); - - writer.write("hello"); - writer.write("world"); - writer.close(); - - const chunks = []; - while (true) { - const { value, done } = await reader.read(); - if (done) break; - chunks.push(value); - } - if (chunks.join(" ") !== "HELLO WORLD") throw new Error("got: " + chunks); - "#, - ) - .unwrap() - .into_future::<()>() - .await - .unwrap(); - }) - }) - .await; -} - -#[tokio::test] -async fn one_to_many_expansion() { - test_async_with(|ctx| { - crate::llrt_stream_web::init(&ctx).unwrap(); - Box::pin(async move { - eval_async( - &ctx, - r#" - const ts = new TransformStream({ - transform(chunk, controller) { - for (const byte of chunk) { - controller.enqueue(byte); - } - } - }); - const writer = ts.writable.getWriter(); - const reader = ts.readable.getReader(); - - writer.write([1, 2, 3]); - writer.close(); - - const chunks = []; - while (true) { - const { value, done } = await reader.read(); - if (done) break; - chunks.push(value); - } - if (chunks.join(",") !== "1,2,3") throw new Error("got: " + chunks); - "#, - ) - .unwrap() - .into_future::<()>() - .await - .unwrap(); - }) - }) - .await; -} - -#[tokio::test] -async fn readable_high_water_mark_applies_backpressure() { - test_async_with(|ctx| { - crate::llrt_stream_web::init(&ctx).unwrap(); - Box::pin(async move { - ctx.eval::<(), _>( - r#" - globalThis.transformed = []; - globalThis.ts = new TransformStream({ - transform(chunk, controller) { - transformed.push(chunk); - controller.enqueue(chunk); - } - }, undefined, { highWaterMark: 3 }); - globalThis.writer = ts.writable.getWriter(); - [0, 1, 2, 3].forEach(chunk => writer.write(chunk)); - "#, - ) - .unwrap(); - - while ctx.execute_pending_job() {} - assert_eq!( - ctx.eval::("transformed.join(',')").unwrap(), - "0,1,2" - ); - - ctx.eval::<(), _>("globalThis.reader = ts.readable.getReader(); reader.read();") - .unwrap(); - while ctx.execute_pending_job() {} - assert_eq!( - ctx.eval::("transformed.join(',')").unwrap(), - "0,1,2,3" - ); - }) - }) - .await; -} - -#[tokio::test] -async fn flush_on_close() { - test_async_with(|ctx| { - crate::llrt_stream_web::init(&ctx).unwrap(); - Box::pin(async move { - eval_async( - &ctx, - r#" - const ts = new TransformStream({ - transform(chunk, controller) { - controller.enqueue(chunk); - }, - flush(controller) { - controller.enqueue("DONE"); - } - }); - const writer = ts.writable.getWriter(); - const reader = ts.readable.getReader(); - - writer.write("a"); - writer.close(); - - const chunks = []; - while (true) { - const { value, done } = await reader.read(); - if (done) break; - chunks.push(value); - } - if (chunks.join(",") !== "a,DONE") throw new Error("got: " + chunks); - "#, - ) - .unwrap() - .into_future::<()>() - .await - .unwrap(); - }) - }) - .await; -} - -#[tokio::test] -async fn pipe_through_chain() { - test_async_with(|ctx| { - crate::llrt_stream_web::init(&ctx).unwrap(); - Box::pin(async move { - eval_async( - &ctx, - r#" - const source = new ReadableStream({ - start(controller) { - controller.enqueue("hello"); - controller.enqueue("world"); - controller.close(); - } - }); - - const upper = new TransformStream({ - transform(chunk, c) { c.enqueue(chunk.toUpperCase()); } - }); - const exclaim = new TransformStream({ - transform(chunk, c) { c.enqueue(chunk + "!"); } - }); - - const reader = source - .pipeThrough(upper) - .pipeThrough(exclaim) - .getReader(); - - const chunks = []; - while (true) { - const { value, done } = await reader.read(); - if (done) break; - chunks.push(value); - } - if (chunks.join(" ") !== "HELLO! WORLD!") throw new Error("got: " + chunks); - "#, - ) - .unwrap() - .into_future::<()>() - .await - .unwrap(); - }) - }) - .await; -} - -#[tokio::test] -async fn async_transform() { - test_async_with(|ctx| { - crate::llrt_stream_web::init(&ctx).unwrap(); - Box::pin(async move { - eval_async( - &ctx, - r#" - const ts = new TransformStream({ - async transform(chunk, controller) { - await new Promise(r => r()); - controller.enqueue(chunk * 2); - } - }); - const writer = ts.writable.getWriter(); - const reader = ts.readable.getReader(); - - writer.write(5); - writer.close(); - - const { value } = await reader.read(); - if (value !== 10) throw new Error("expected 10, got " + value); - "#, - ) - .unwrap() - .into_future::<()>() - .await - .unwrap(); - }) - }) - .await; -} - -#[tokio::test] -async fn error_propagates_to_reader() { - test_async_with(|ctx| { - crate::llrt_stream_web::init(&ctx).unwrap(); - Box::pin(async move { - eval_async( - &ctx, - r#" - const ts = new TransformStream({ - transform(chunk, controller) { - controller.error(new Error("broken")); - } - }); - const writer = ts.writable.getWriter(); - const reader = ts.readable.getReader(); - - writer.write("x").catch(() => {}); - - try { - await reader.read(); - throw new Error("should have thrown"); - } catch (e) { - if (e.message !== "broken") throw new Error("wrong error: " + e.message); - } - "#, - ) - .unwrap() - .into_future::<()>() - .await - .unwrap(); - }) - }) - .await; -} - -#[tokio::test] -async fn start_receives_controller() { - test_async_with(|ctx| { - crate::llrt_stream_web::init(&ctx).unwrap(); - Box::pin(async move { - eval_async( - &ctx, - r#" - let controllerRef; - const ts = new TransformStream({ - start(controller) { - controllerRef = controller; - controller.enqueue("from-start"); - } - }); - - if (typeof controllerRef.desiredSize !== "number") - throw new Error("controller.desiredSize should be a number"); - - const reader = ts.readable.getReader(); - const { value } = await reader.read(); - if (value !== "from-start") throw new Error("got: " + value); - "#, - ) - .unwrap() - .into_future::<()>() - .await - .unwrap(); - }) - }) - .await; -} - -#[tokio::test] -async fn terminate_closes_readable() { - test_async_with(|ctx| { - crate::llrt_stream_web::init(&ctx).unwrap(); - Box::pin(async move { - eval_async( - &ctx, - r#" - const ts = new TransformStream({ - transform(chunk, controller) { - if (chunk === "stop") { - controller.terminate(); - return; - } - controller.enqueue(chunk); - } - }); - const writer = ts.writable.getWriter(); - const reader = ts.readable.getReader(); - - writer.write("keep").catch(() => {}); - writer.write("stop").catch(() => {}); - - const { value } = await reader.read(); - if (value !== "keep") throw new Error("got: " + value); - - const { done } = await reader.read(); - if (!done) throw new Error("expected stream to be closed after terminate"); - "#, - ) - .unwrap() - .into_future::<()>() - .await - .unwrap(); - }) - }) - .await; -} - -#[tokio::test] -async fn illegal_constructor() { - test_async_with(|ctx| { - crate::llrt_stream_web::init(&ctx).unwrap(); - Box::pin(async move { - eval_async( - &ctx, - r#" - try { - new TransformStreamDefaultController(); - throw new Error("should have thrown"); - } catch (e) { - if (!(e instanceof TypeError)) throw new Error("expected TypeError, got " + e); - } - "#, - ) - .unwrap() - .into_future::<()>() - .await - .unwrap(); - }) - }) - .await; -} - -#[tokio::test] -async fn pipe_to_writable_stream() { - test_async_with(|ctx| { - crate::llrt_stream_web::init(&ctx).unwrap(); - Box::pin(async move { - eval_async( - &ctx, - r#" - const collected = []; - const source = new ReadableStream({ - start(c) { c.enqueue(1); c.enqueue(2); c.enqueue(3); c.close(); } - }); - const transform = new TransformStream({ - transform(chunk, c) { c.enqueue(chunk * 10); } - }); - const sink = new WritableStream({ - write(chunk) { collected.push(chunk); } - }); - - await source.pipeThrough(transform).pipeTo(sink); - - if (collected.join(",") !== "10,20,30") throw new Error("got: " + collected); - "#, - ) - .unwrap() - .into_future::<()>() - .await - .unwrap(); - }) - }) - .await; -} diff --git a/stdlib/src/llrt/llrt_stream_web/transform/transformer.rs b/stdlib/src/llrt/llrt_stream_web/transform/transformer.rs deleted file mode 100644 index ad27cf02..00000000 --- a/stdlib/src/llrt/llrt_stream_web/transform/transformer.rs +++ /dev/null @@ -1,44 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use rquickjs::{Function, Object, Result}; - -use crate::llrt_stream_web::utils::ValueOrUndefined; - -/// dictionary Transformer { -/// TransformerStartCallback start; -/// TransformerTransformCallback transform; -/// TransformerFlushCallback flush; -/// TransformerCancelCallback cancel; -/// any readableType; -/// any writableType; -/// }; -#[derive(Default)] -pub(super) struct Transformer<'js> { - pub start: Option>, - pub transform: Option>, - pub flush: Option>, - pub cancel: Option>, - pub readable_type: bool, - pub writable_type: bool, -} - -impl<'js> Transformer<'js> { - pub fn from_object(obj: Object<'js>) -> Result { - let start = obj.get_value_or_undefined::<_, _>("start")?; - let transform = obj.get_value_or_undefined::<_, _>("transform")?; - let flush = obj.get_value_or_undefined::<_, _>("flush")?; - let cancel = obj.get_value_or_undefined::<_, _>("cancel")?; - let readable_type: Option> = - obj.get_value_or_undefined("readableType")?; - let writable_type: Option> = - obj.get_value_or_undefined("writableType")?; - - Ok(Self { - start, - transform, - flush, - cancel, - readable_type: readable_type.is_some(), - writable_type: writable_type.is_some(), - }) - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/utils/mod.rs b/stdlib/src/llrt/llrt_stream_web/utils/mod.rs deleted file mode 100644 index 3e38532c..00000000 --- a/stdlib/src/llrt/llrt_stream_web/utils/mod.rs +++ /dev/null @@ -1,58 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use crate::llrt_utils::option::Undefined; -use rquickjs::{ - class::{JsClass, OwnedBorrowMut}, - Class, Ctx, FromJs, IntoAtom, Object, Result, Value, -}; - -pub mod promise; -pub mod queue; - -// the trait used elsewhere in this repo accepts null values as 'None', which causes many web platform tests to fail as they -// like to check that undefined is accepted and null isn't. -pub trait ValueOrUndefined<'js> { - fn get_value_or_undefined + Clone, V: FromJs<'js>>( - &self, - k: K, - ) -> Result>; -} - -impl<'js> ValueOrUndefined<'js> for Object<'js> { - fn get_value_or_undefined + Clone, V: FromJs<'js> + Sized>( - &self, - k: K, - ) -> Result> { - let value = self.get::>(k)?; - Ok(Undefined::from_js(self.ctx(), value)?.0) - } -} - -impl<'js> ValueOrUndefined<'js> for Value<'js> { - fn get_value_or_undefined + Clone, V: FromJs<'js>>( - &self, - k: K, - ) -> Result> { - if let Some(obj) = self.as_object() { - return obj.get_value_or_undefined(k); - } - Ok(None) - } -} - -pub trait UnwrapOrUndefined<'js> { - fn unwrap_or_undefined(self, ctx: &Ctx<'js>) -> Value<'js>; -} - -impl<'js> UnwrapOrUndefined<'js> for Option> { - fn unwrap_or_undefined(self, ctx: &Ctx<'js>) -> Value<'js> { - self.unwrap_or_else(|| Value::new_undefined(ctx.clone())) - } -} - -pub fn class_from_owned_borrow_mut<'js, T: JsClass<'js>>( - borrow: OwnedBorrowMut<'js, T>, -) -> (Class<'js, T>, OwnedBorrowMut<'js, T>) { - let class = borrow.into_inner(); - let borrow = OwnedBorrowMut::from_class(class.clone()); - (class, borrow) -} diff --git a/stdlib/src/llrt/llrt_stream_web/utils/promise.rs b/stdlib/src/llrt/llrt_stream_web/utils/promise.rs deleted file mode 100644 index 584f4481..00000000 --- a/stdlib/src/llrt/llrt_stream_web/utils/promise.rs +++ /dev/null @@ -1,260 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use std::{cell::Cell, rc::Rc}; - -use crate::llrt_utils::primordials::Primordial; -use rquickjs::{ - atom::PredefinedAtom, - class::{Trace, Tracer}, - function::Constructor, - prelude::{IntoArg, OnceFn, This}, - promise::PromiseState, - Ctx, Error, FromJs, Function, IntoJs, JsLifetime, Object, Promise, Result, Value, -}; - -pub fn promise_rejected_with<'js>( - primordials: &PromisePrimordials<'js>, - value: Value<'js>, -) -> Result> { - primordials - .promise_reject - .call((This(primordials.promise_constructor.clone()), value)) -} - -pub fn promise_rejected_catch<'js>( - ctx: &Ctx<'js>, - promise_primordials: &PromisePrimordials<'js>, -) -> Result> { - promise_rejected_with(promise_primordials, ctx.catch()) -} - -pub fn promise_rejected_with_constructor<'js, T: From>( - constructor: &Constructor<'js>, - promise_primordials: &PromisePrimordials<'js>, - msg: &str, -) -> std::result::Result, T> { - let e: Value = constructor.call((msg,))?; - Ok(promise_rejected_with(promise_primordials, e)?) -} - -pub fn promise_resolved_with<'js>( - ctx: &Ctx<'js>, - primordials: &PromisePrimordials<'js>, - value: Result>, -) -> Result> { - match value { - Ok(value) => primordials - .promise_resolve - .call((This(primordials.promise_constructor.clone()), value)), - Err(Error::Exception) => primordials - .promise_reject - .call((This(primordials.promise_constructor.clone()), ctx.catch())), - Err(err) => Err(err), - } -} - -#[derive(JsLifetime, Clone)] -pub struct PromisePrimordials<'js> { - pub promise_constructor: Constructor<'js>, - pub promise_resolve: Function<'js>, - pub promise_reject: Function<'js>, - pub promise_all: Function<'js>, - pub promise_resolved_with_undefined: Promise<'js>, - pub promise_prototype_then: Function<'js>, -} - -impl<'js> Trace<'js> for PromisePrimordials<'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - self.promise_constructor.trace(tracer); - self.promise_resolve.trace(tracer); - self.promise_reject.trace(tracer); - self.promise_all.trace(tracer); - self.promise_resolved_with_undefined.trace(tracer); - self.promise_prototype_then.trace(tracer); - } -} - -impl<'js> Primordial<'js> for PromisePrimordials<'js> { - fn new(ctx: &Ctx<'js>) -> Result - where - Self: Sized, - { - let promise_constructor: Constructor<'js> = ctx.globals().get(PredefinedAtom::Promise)?; - let promise_resolve: Function<'js> = promise_constructor.get("resolve")?; - let promise_reject: Function<'js> = promise_constructor.get("reject")?; - let promise_all: Function<'js> = promise_constructor.get("all")?; - let promise_prototype_then: Function<'js> = promise_constructor - .get::<_, Object>("prototype")? - .get("then")?; - - let promise_resolved_with_undefined = promise_resolve.call(( - This(promise_constructor.clone()), - Value::new_undefined(ctx.clone()), - ))?; - - Ok(Self { - promise_constructor, - promise_resolve, - promise_reject, - promise_all, - promise_resolved_with_undefined, - promise_prototype_then, - }) - } -} - -// https://webidl.spec.whatwg.org/#dfn-perform-steps-once-promise-is-settled -pub fn upon_promise<'js, Input: FromJs<'js> + 'js, Output: IntoJs<'js> + 'js>( - ctx: Ctx<'js>, - promise: Promise<'js>, - then: impl FnOnce(Ctx<'js>, std::result::Result>) -> Result + 'js, -) -> Result> { - let promise_then = PromisePrimordials::get(&ctx)? - .promise_prototype_then - .clone(); - let then_cb = Rc::new(Cell::new(Some(then))); - let then_cb2 = then_cb.clone(); - promise_then.call(( - This(promise), - Function::new( - ctx.clone(), - OnceFn::new(move |ctx, input| { - then_cb - .take() - .expect("Promise.then should only call either resolve or reject")( - ctx, - Ok(input), - ) - }), - ), - Function::new( - ctx, - OnceFn::new(move |ctx, e: Value<'js>| { - then_cb2 - .take() - .expect("Promise.then should only call either resolve or reject")( - ctx, Err(e) - ) - }), - ), - )) -} - -pub fn upon_promise_fulfilment<'js, Input: FromJs<'js> + 'js, Output: IntoJs<'js> + 'js>( - ctx: Ctx<'js>, - promise: Promise<'js>, - then: impl FnOnce(Ctx<'js>, Input) -> Result + 'js, -) -> Result> { - let promise_then = PromisePrimordials::get(&ctx)? - .promise_prototype_then - .clone(); - promise_then.call((This(promise), Function::new(ctx.clone(), OnceFn::new(then)))) -} - -#[derive(Debug, JsLifetime, Clone)] -pub struct ResolveablePromise<'js> { - pub promise: Promise<'js>, - resolve: Option>, - reject: Option>, -} - -impl<'js> ResolveablePromise<'js> { - pub fn new(ctx: &Ctx<'js>) -> Result { - let (promise, resolve, reject) = Promise::new(ctx)?; - Ok(Self { - promise, - resolve: Some(resolve), - reject: Some(reject), - }) - } - - pub fn resolved_with_undefined(primordials: &PromisePrimordials<'js>) -> Self { - Self { - promise: primordials.promise_resolved_with_undefined.clone(), - resolve: None, - reject: None, - } - } - - pub fn rejected_with(primordials: &PromisePrimordials<'js>, error: Value<'js>) -> Result { - Ok(Self { - promise: promise_rejected_with(primordials, error)?, - resolve: None, - reject: None, - }) - } - - pub fn rejected_with_constructor( - primordials: &PromisePrimordials<'js>, - constructor: &Constructor<'js>, - msg: &str, - ) -> Result { - Ok(Self { - promise: promise_rejected_with_constructor::( - constructor, - primordials, - msg, - )?, - resolve: None, - reject: None, - }) - } - - pub fn resolve(&self, value: impl IntoArg<'js>) -> Result<()> { - if let Some(resolve) = &self.resolve { - let () = resolve.call((value,))?; - } - Ok(()) - } - - pub fn resolve_undefined(&self) -> Result<()> { - if let Some(resolve) = &self.resolve { - let () = resolve.call((rquickjs::Undefined,))?; - } - Ok(()) - } - - pub fn reject(&self, value: impl IntoArg<'js>) -> Result<()> { - if let Some(reject) = &self.reject { - let () = reject.call((value,))?; - } - Ok(()) - } - - pub fn reject_with_constructor(&self, constructor: &Constructor<'js>, msg: &str) -> Result<()> { - if let Some(reject) = &self.reject { - let e: Value = constructor.call((msg,))?; - let () = reject.call((e,))?; - } - Ok(()) - } - - pub fn is_pending(&self) -> bool { - self.promise.state() == PromiseState::Pending - } - - pub fn set_is_handled(&self) -> Result<()> { - self.promise.catch()?.call(( - This(self.promise.clone()), - Function::new(self.promise.ctx().clone(), || {}), - )) - } -} - -impl<'js> Trace<'js> for ResolveablePromise<'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - self.promise.trace(tracer); - self.resolve.trace(tracer); - self.reject.trace(tracer); - } -} - -pub fn with_promise_result<'js>( - ctx: &Ctx<'js>, - f: impl FnOnce() -> Result>, -) -> Result> { - match f() { - Ok(value) => Ok(value), - Err(Error::Exception) => promise_rejected_catch(ctx, &*PromisePrimordials::get(ctx)?), - Err(err) => Err(err), - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/utils/queue.rs b/stdlib/src/llrt/llrt_stream_web/utils/queue.rs deleted file mode 100644 index 4588b4eb..00000000 --- a/stdlib/src/llrt/llrt_stream_web/utils/queue.rs +++ /dev/null @@ -1,103 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use std::collections::VecDeque; - -use rquickjs::{class::Trace, Ctx, Exception, JsLifetime, Result, Value}; - -use crate::llrt_stream_web::queuing_strategy::SizeValue; - -/// QueueWithSize is present in readable and writable streams and abstracts away certain queue operations -/// https://streams.spec.whatwg.org/#queue-with-sizes -#[derive(JsLifetime, Trace, Default)] -pub struct QueueWithSizes<'js> { - pub queue: VecDeque>, - pub queue_total_size: f64, -} - -impl<'js> QueueWithSizes<'js> { - pub fn new() -> Self { - Self { - queue: VecDeque::new(), - queue_total_size: 0.0, - } - } - - pub(crate) fn enqueue_value_with_size( - &mut self, - ctx: &Ctx<'js>, - value: Value<'js>, - size: SizeValue<'js>, - ) -> Result<()> { - let size = match is_non_negative_number(size) { - None => { - // If ! IsNonNegativeNumber(size) is false, throw a RangeError exception. - return Err(Exception::throw_range( - ctx, - "Size must be a finite, non-NaN, non-negative number.", - )); - } - Some(size) => size, - }; - - // If size is +∞, throw a RangeError exception. - if size.is_infinite() { - return Err(Exception::throw_range( - ctx, - "Size must be a finite, non-NaN, non-negative number.", - )); - }; - - // Append a new value-with-size with value value and size size to container.[[queue]]. - self.queue.push_back(ValueWithSize { value, size }); - - // Set container.[[queueTotalSize]] to container.[[queueTotalSize]] + size. - self.queue_total_size += size; - - Ok(()) - } - - pub fn dequeue_value(&mut self) -> Value<'js> { - // Let valueWithSize be container.[[queue]][0]. - // Remove valueWithSize from container.[[queue]]. - let value_with_size = self - .queue - .pop_front() - .expect("DequeueValue called with empty queue"); - // Set container.[[queueTotalSize]] to container.[[queueTotalSize]] − valueWithSize’s size. - self.queue_total_size -= value_with_size.size; - // If container.[[queueTotalSize]] < 0, set container.[[queueTotalSize]] to 0. (This can occur due to rounding errors.) - if self.queue_total_size < 0.0 { - self.queue_total_size = 0.0 - } - value_with_size.value - } - - pub fn reset_queue(&mut self) { - // Set container.[[queue]] to a new empty list. - self.queue.clear(); - // Set container.[[queueTotalSize]] to 0. - self.queue_total_size = 0.0; - } -} - -#[derive(JsLifetime, Trace, Clone)] -pub struct ValueWithSize<'js> { - pub value: Value<'js>, - size: f64, -} - -fn is_non_negative_number(value: SizeValue<'_>) -> Option { - // If Type(v) is not Number, return false. - let number = value.as_number()?; - // If v is NaN, return false. - if number.is_nan() { - return None; - } - - // If v < 0, return false. - if number < 0.0 { - return None; - } - - // Return true. - Some(number) -} diff --git a/stdlib/src/llrt/llrt_stream_web/writable/default_controller.rs b/stdlib/src/llrt/llrt_stream_web/writable/default_controller.rs deleted file mode 100644 index b24b6ea8..00000000 --- a/stdlib/src/llrt/llrt_stream_web/writable/default_controller.rs +++ /dev/null @@ -1,871 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use crate::llrt_abort::{AbortController, AbortSignal}; -use crate::llrt_utils::{ - option::{Null, Undefined}, - primordials::Primordial, -}; -use rquickjs::{ - class::{JsClass, OwnedBorrowMut, Trace}, - function::Constructor, - methods, - prelude::{Opt, This}, - Class, Ctx, Error, Exception, Function, JsLifetime, Object, Promise, Result, Symbol, Value, -}; - -use crate::llrt_stream_web::{ - queuing_strategy::{SizeAlgorithm, SizeValue}, - transform::controller::TransformStreamDefaultControllerClass, - transform::stream::TransformStreamClass, - utils::{ - class_from_owned_borrow_mut, - promise::{promise_resolved_with, upon_promise, PromisePrimordials}, - queue::QueueWithSizes, - UnwrapOrUndefined, - }, - writable::{ - default_writer::WritableStreamDefaultWriterOwned, - objects::{WritableStreamClassObjects, WritableStreamObjects}, - stream::{ - sink::UnderlyingSink, WritableStream, WritableStreamClass, WritableStreamOwned, - WritableStreamState, - }, - writer::{UndefinedWriter, WritableStreamWriter}, - }, -}; - -#[rquickjs::class] -#[derive(JsLifetime, Trace)] -pub(crate) struct WritableStreamDefaultController<'js> { - abort_algorithm: Option>, - close_algorithm: Option>, - container: QueueWithSizes<'js>, - pub(super) started: bool, - strategy_hwm: f64, - strategy_size_algorithm: Option>, - pub(super) abort_controller: Class<'js, AbortController<'js>>, - pub(super) stream: WritableStreamClass<'js>, - write_algorithm: Option>, - - primordials: WritableStreamDefaultControllerPrimordials<'js>, -} - -pub(crate) type WritableStreamDefaultControllerClass<'js> = - Class<'js, WritableStreamDefaultController<'js>>; -pub(crate) type WritableStreamDefaultControllerOwned<'js> = - OwnedBorrowMut<'js, WritableStreamDefaultController<'js>>; - -impl<'js> WritableStreamDefaultController<'js> { - pub(super) fn set_up_writable_stream_default_controller_from_underlying_sink( - ctx: Ctx<'js>, - stream: WritableStreamOwned<'js>, - underlying_sink: Null>>, - underlying_sink_dict: UnderlyingSink<'js>, - high_water_mark: f64, - size_algorithm: SizeAlgorithm<'js>, - ) -> Result<()> { - let (start_algorithm, write_algorithm, close_algorithm, abort_algorithm) = ( - // If underlyingSinkDict["start"] exists, then set startAlgorithm to an algorithm which returns the result of invoking underlyingSinkDict["start"] with argument list - // « controller », exception behavior "rethrow", and callback this value underlyingSink. - underlying_sink_dict - .start - .map(|f| WritableStartAlgorithm::Function { - f, - underlying_sink: underlying_sink.clone(), - }) - .unwrap_or(WritableStartAlgorithm::ReturnUndefined), - // If underlyingSinkDict["write"] exists, then set writeAlgorithm to an algorithm which takes an argument chunk and returns the result of invoking underlyingSinkDict["write"] with argument list - // « chunk, controller » and callback this value underlyingSink. - underlying_sink_dict - .write - .map(|f| WritableWriteAlgorithm::Function { - f, - underlying_sink: underlying_sink.clone(), - }) - .unwrap_or(WritableWriteAlgorithm::ReturnPromiseUndefined), - // If underlyingSinkDict["close"] exists, then set closeAlgorithm to an algorithm which returns the result of invoking underlyingSinkDict["close"] with argument list - // «» and callback this value underlyingSink. - underlying_sink_dict - .close - .map(|f| WritableCloseAlgorithm::Function { - f, - underlying_sink: underlying_sink.clone(), - }) - .unwrap_or(WritableCloseAlgorithm::ReturnPromiseUndefined), - // If underlyingSinkDict["abort"] exists, then set abortAlgorithm to an algorithm which takes an argument reason and returns the result of invoking underlyingSinkDict["abort"] with argument list - // « reason » and callback this value underlyingSink. - underlying_sink_dict - .abort - .map(|f| WritableAbortAlgorithm::Function { - f, - underlying_sink: underlying_sink.clone(), - }) - .unwrap_or(WritableAbortAlgorithm::ReturnPromiseUndefined), - ); - - // Perform ? SetUpWritableStreamDefaultController(stream, controller, startAlgorithm, writeAlgorithm, closeAlgorithm, abortAlgorithm, highWaterMark, sizeAlgorithm). - Self::set_up_writable_stream_default_controller( - ctx, - stream, - start_algorithm, - write_algorithm, - close_algorithm, - abort_algorithm, - high_water_mark, - size_algorithm, - ) - } - - #[allow(clippy::too_many_arguments)] - pub(crate) fn set_up_writable_stream_default_controller( - ctx: Ctx<'js>, - stream: WritableStreamOwned<'js>, - start_algorithm: WritableStartAlgorithm<'js>, - write_algorithm: WritableWriteAlgorithm<'js>, - close_algorithm: WritableCloseAlgorithm<'js>, - abort_algorithm: WritableAbortAlgorithm<'js>, - high_water_mark: f64, - size_algorithm: SizeAlgorithm<'js>, - ) -> Result<()> { - // TODO: needed? - let (stream_class, mut stream) = class_from_owned_borrow_mut(stream); - - let controller = Self { - // Set controller.[[stream]] to stream. - stream: stream_class, - - // Perform ! ResetQueue(controller). - container: QueueWithSizes::new(), - - // Set controller.[[abortController]] to a new AbortController. - abort_controller: Class::instance(ctx.clone(), AbortController::new(ctx.clone())?)?, - - // Set controller.[[started]] to false. - started: false, - - // Set controller.[[strategySizeAlgorithm]] to sizeAlgorithm. - strategy_size_algorithm: Some(size_algorithm), - // Set controller.[[strategyHWM]] to highWaterMark. - strategy_hwm: high_water_mark, - - // Set controller.[[writeAlgorithm]] to writeAlgorithm. - write_algorithm: Some(write_algorithm), - // Set controller.[[closeAlgorithm]] to closeAlgorithm. - close_algorithm: Some(close_algorithm), - // Set controller.[[abortAlgorithm]] to abortAlgorithm. - abort_algorithm: Some(abort_algorithm), - - primordials: WritableStreamDefaultControllerPrimordials::get(&ctx)?.clone(), - }; - - let controller_class = Class::instance(ctx.clone(), controller)?; - - // Set stream.[[controller]] to controller. - stream.controller = Some(controller_class.clone()); - - let objects = WritableStreamObjects::from_stream(stream); - - // Let backpressure be ! WritableStreamDefaultControllerGetBackpressure(controller). - let backpressure = objects - .controller - .writable_stream_default_controller_get_backpressure(); - // Perform ! WritableStreamUpdateBackpressure(stream, backpressure). - let objects = WritableStream::writable_stream_update_backpressure( - ctx.clone(), - objects, - backpressure, - )?; - let promise_primordials = objects.stream.promise_primordials.clone(); - - // Let startResult be the result of performing startAlgorithm. (This may throw an exception.) - let (start_result, objects_class) = - Self::start_algorithm(ctx.clone(), objects, start_algorithm)?; - - // Let startPromise be a promise resolved with startResult. - let start_promise = promise_resolved_with(&ctx, &promise_primordials, Ok(start_result))?; - - let _ = upon_promise::, _>(ctx.clone(), start_promise, { - move |ctx, result| { - let mut objects = - WritableStreamObjects::from_class_no_writer(objects_class).refresh_writer(); - match result { - // Upon fulfillment of startPromise, - Ok(_) => { - // Set controller.[[started]] to true. - objects.controller.started = true; - // Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). - Self::writable_stream_default_controller_advance_queue_if_needed( - ctx, objects, - )?; - } - // Upon rejection of startPromise with reason r, - Err(r) => { - // Set controller.[[started]] to true. - objects.controller.started = true; - - // Perform ! WritableStreamDealWithRejection(stream, r). - WritableStream::writable_stream_deal_with_rejection(ctx, objects, r)?; - } - } - Ok(()) - } - })?; - - Ok(()) - } - - pub(super) fn writable_stream_default_controller_close>( - ctx: Ctx<'js>, - mut objects: WritableStreamObjects<'js, W>, - ) -> Result> { - let close_sentinel = objects - .controller - .primordials - .close_sentinel - .as_value() - .clone(); - - // Perform ! EnqueueValueWithSize(controller, close sentinel, 0). - objects.controller.container.enqueue_value_with_size( - &ctx, - close_sentinel, - SizeValue::Native(0.0), - )?; - - // Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). - objects = Self::writable_stream_default_controller_advance_queue_if_needed(ctx, objects)?; - - Ok(objects) - } - - pub(super) fn writable_stream_default_controller_get_desired_size(&self) -> f64 { - self.strategy_hwm - self.container.queue_total_size - } - - pub fn writable_stream_default_controller_get_backpressure(&self) -> bool { - // Let desiredSize be ! WritableStreamDefaultControllerGetDesiredSize(controller). - let desired_size = self.writable_stream_default_controller_get_desired_size(); - // Return true if desiredSize ≤ 0, or false otherwise. - desired_size <= 0.0 - } - - pub(super) fn writable_stream_default_controller_get_chunk_size( - ctx: Ctx<'js>, - mut objects: WritableStreamObjects<'js, WritableStreamDefaultWriterOwned<'js>>, - chunk: Value<'js>, - ) -> Result<( - SizeValue<'js>, - WritableStreamObjects<'js, WritableStreamDefaultWriterOwned<'js>>, - )> { - let (return_value, objects_class) = - Self::strategy_size_algorithm(ctx.clone(), objects, chunk); - - // Let returnValue be the result of performing controller.[[strategySizeAlgorithm]], passing in chunk, and interpreting the result as a completion record. - match return_value { - Ok(chunk_size) => { - objects = WritableStreamObjects::from_class(objects_class); - Ok((chunk_size, objects)) - } - // If returnValue is an abrupt completion, - Err(Error::Exception) => { - let reason = ctx.catch(); - - objects = WritableStreamObjects::from_class(objects_class); - - // Perform ! WritableStreamDefaultControllerErrorIfNeeded(controller, returnValue.[[Value]]). - objects = Self::writable_stream_default_controller_error_if_needed( - ctx.clone(), - objects, - reason, - )?; - - // Return 1. - Ok((SizeValue::Native(1.0), objects)) - } - Err(err) => Err(err), - } - } - - fn writable_stream_default_controller_error_if_needed( - ctx: Ctx<'js>, - objects: WritableStreamObjects<'js, WritableStreamDefaultWriterOwned<'js>>, - error: Value<'js>, - ) -> Result>> { - // If controller.[[stream]].[[state]] is "writable", perform ! WritableStreamDefaultControllerError(controller, error). - if let WritableStreamState::Writable = objects.stream.state { - Self::writable_stream_default_controller_error(ctx, objects, error) - } else { - Ok(objects) - } - } - - fn writable_stream_default_controller_error>( - ctx: Ctx<'js>, - // Let stream be controller.[[stream]]. - mut objects: WritableStreamObjects<'js, W>, - reason: Value<'js>, - ) -> Result> { - // Perform ! WritableStreamDefaultControllerClearAlgorithms(controller). - objects - .controller - .writable_stream_default_controller_clear_algorithms(); - - // Perform ! WritableStreamStartErroring(stream, error). - objects = WritableStream::writable_stream_start_erroring(ctx, objects, reason)?; - - Ok(objects) - } - - fn writable_stream_default_controller_clear_algorithms(&mut self) { - // Set controller.[[writeAlgorithm]] to undefined. - self.write_algorithm = None; - - // Set controller.[[closeAlgorithm]] to undefined. - self.close_algorithm = None; - - // Set controller.[[abortAlgorithm]] to undefined. - self.abort_algorithm = None; - - // Set controller.[[strategySizeAlgorithm]] to undefined. - self.strategy_size_algorithm = None; - } - - pub(super) fn writable_stream_default_controller_write( - ctx: Ctx<'js>, - // Let stream be controller.[[stream]]. - mut objects: WritableStreamObjects<'js, WritableStreamDefaultWriterOwned<'js>>, - chunk: Value<'js>, - chunk_size: SizeValue<'js>, - ) -> Result>> { - // Let enqueueResult be EnqueueValueWithSize(controller, chunk, chunkSize). - let enqueue_result = objects - .controller - .container - .enqueue_value_with_size(&ctx, chunk, chunk_size); - - match enqueue_result { - // If enqueueResult is an abrupt completion, - Err(Error::Exception) => { - let reason = ctx.catch(); - // Perform ! WritableStreamDefaultControllerErrorIfNeeded(controller, enqueueResult.[[Value]]). - objects = - Self::writable_stream_default_controller_error_if_needed(ctx, objects, reason)?; - - return Ok(objects); - } - Err(err) => return Err(err), - Ok(()) => {} - } - - // If ! WritableStreamCloseQueuedOrInFlight(stream) is false and stream.[[state]] is "writable", - if !objects.stream.writable_stream_close_queued_or_in_flight() - && matches!(objects.stream.state, WritableStreamState::Writable) - { - // Let backpressure be ! WritableStreamDefaultControllerGetBackpressure(controller). - let backpressure = objects - .controller - .writable_stream_default_controller_get_backpressure(); - - // Perform ! WritableStreamUpdateBackpressure(stream, backpressure). - objects = WritableStream::writable_stream_update_backpressure( - ctx.clone(), - objects, - backpressure, - )?; - } - - // Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). - let objects = - Self::writable_stream_default_controller_advance_queue_if_needed(ctx, objects)?; - - Ok(objects) - } - - fn writable_stream_default_controller_advance_queue_if_needed>( - ctx: Ctx<'js>, - // Let stream be controller.[[stream]]. - objects: WritableStreamObjects<'js, W>, - ) -> Result> { - // If controller.[[started]] is false, return. - // If stream.[[inFlightWriteRequest]] is not undefined, return. - if !objects.controller.started || objects.stream.in_flight_write_request.is_some() { - return Ok(objects); - } - - // Let state be stream.[[state]]. - - // If state is "erroring", - if let WritableStreamState::Erroring(ref stored_error) = objects.stream.state { - let stored_error = stored_error.clone(); - // Perform ! WritableStreamFinishErroring(stream). - // Return. - return WritableStream::writable_stream_finish_erroring(ctx, objects, stored_error); - } - - let value = match objects.controller.container.queue.front() { - // If controller.[[queue]] is empty, return. - None => { - return Ok(objects); - } - // Let value be ! PeekQueueValue(controller). - Some(value) => value.clone(), - }; - - if value.value.as_symbol() == Some(&objects.controller.primordials.close_sentinel) { - // If value is the close sentinel, perform ! WritableStreamDefaultControllerProcessClose(controller). - Self::writable_stream_default_controller_process_close(ctx, objects) - } else { - // Otherwise, perform ! WritableStreamDefaultControllerProcessWrite(controller, value). - Self::writable_stream_default_controller_process_write(ctx, objects, value.value) - } - } - - fn writable_stream_default_controller_process_close>( - ctx: Ctx<'js>, - // Let stream be controller.[[stream]]. - mut objects: WritableStreamObjects<'js, W>, - ) -> Result> { - // Perform ! WritableStreamMarkCloseRequestInFlight(stream). - objects - .stream - .writable_stream_mark_close_request_in_flight(); - - // Perform ! DequeueValue(controller). - objects.controller.container.dequeue_value(); - - // Assert: controller.[[queue]] is empty. - - // Let sinkClosePromise be the result of performing controller.[[closeAlgorithm]]. - let (sink_close_promise, objects_class) = Self::close_algorithm(&ctx, objects)?; - - objects = WritableStreamObjects::from_class(objects_class.clone()); - - // Perform ! WritableStreamDefaultControllerClearAlgorithms(controller). - objects - .controller - .writable_stream_default_controller_clear_algorithms(); - - upon_promise::, ()>(ctx, sink_close_promise, |ctx, result| { - let objects = WritableStreamObjects::from_class(objects_class); - match result { - // Upon fulfillment of sinkClosePromise, - Ok(_) => { - // Perform ! WritableStreamFinishInFlightClose(stream). - WritableStream::writable_stream_finish_in_flight_close(objects)?; - } - // Upon rejection of sinkClosePromise with reason reason, - Err(reason) => { - // Perform ! WritableStreamFinishInFlightCloseWithError(stream, reason). - WritableStream::writable_stream_finish_in_flight_close_with_error( - ctx, objects, reason, - )?; - } - } - - Ok(()) - })?; - - Ok(objects) - } - - fn writable_stream_default_controller_process_write>( - ctx: Ctx<'js>, - // Let stream be controller.[[stream]]. - mut objects: WritableStreamObjects<'js, W>, - chunk: Value<'js>, - ) -> Result> { - // Perform ! WritableStreamMarkFirstWriteRequestInFlight(stream). - objects - .stream - .writable_stream_mark_first_write_request_in_flight(); - - // Let sinkWritePromise be the result of performing controller.[[writeAlgorithm]], passing in chunk. - let (sink_write_promise, objects_class) = Self::write_algorithm(&ctx, objects, chunk)?; - - // Upon fulfillment of sinkWritePromise, - upon_promise::, ()>(ctx, sink_write_promise, { - let objects_class = objects_class.clone(); - |ctx, result| { - let mut objects = WritableStreamObjects::from_class(objects_class).refresh_writer(); - match result { - Ok(_) => { - // Upon fulfillment of sinkWritePromise, - // Perform ! WritableStreamFinishInFlightWrite(stream). - objects.stream.writable_stream_finish_in_flight_write()?; - - // Let state be stream.[[state]]. - let state = &objects.stream.state; - - // Perform ! DequeueValue(controller). - objects.controller.container.dequeue_value(); - - // If ! WritableStreamCloseQueuedOrInFlight(stream) is false and state is "writable", - if !objects.stream.writable_stream_close_queued_or_in_flight() - && matches!(state, WritableStreamState::Writable) - { - // Let backpressure be ! WritableStreamDefaultControllerGetBackpressure(controller). - let backpressure = objects - .controller - .writable_stream_default_controller_get_backpressure(); - - // Perform ! WritableStreamUpdateBackpressure(stream, backpressure). - objects = WritableStream::writable_stream_update_backpressure( - ctx.clone(), - objects, - backpressure, - )?; - } - - // Perform ! WritableStreamDefaultControllerAdvanceQueueIfNeeded(controller). - WritableStreamDefaultController::writable_stream_default_controller_advance_queue_if_needed(ctx, objects)?; - } - Err(reason) => { - // Upon rejection of sinkWritePromise with reason, - if let WritableStreamState::Writable = objects.stream.state { - // If stream.[[state]] is "writable", perform ! WritableStreamDefaultControllerClearAlgorithms(controller). - objects - .controller - .writable_stream_default_controller_clear_algorithms(); - } - // Perform ! WritableStreamFinishInFlightWriteWithError(stream, reason). - WritableStream::writable_stream_finish_in_flight_write_with_error( - ctx, objects, reason, - )?; - } - } - - Ok(()) - } - })?; - - Ok(WritableStreamObjects::from_class(objects_class)) - } - - pub(super) fn error_steps(&mut self) { - // Perform ! ResetQueue(this). - self.reset_queue() - } - - fn reset_queue(&mut self) { - // Set container.[[queue]] to a new empty list. - self.container.queue.clear(); - // Set container.[[queueTotalSize]] to 0. - self.container.queue_total_size = 0.0; - } - - pub(super) fn abort_steps>( - ctx: &Ctx<'js>, - mut objects: WritableStreamObjects<'js, W>, - reason: Value<'js>, - ) -> Result<(Promise<'js>, WritableStreamObjects<'js, W>)> { - // Let result be the result of performing this.[[abortAlgorithm]], passing reason. - let (result, objects_class) = Self::abort_algorithm(ctx, objects, reason)?; - - objects = WritableStreamObjects::from_class(objects_class); - - // Perform ! WritableStreamDefaultControllerClearAlgorithms(this). - objects - .controller - .writable_stream_default_controller_clear_algorithms(); - - // Return result. - Ok((result, objects)) - } - - fn strategy_size_algorithm( - ctx: Ctx<'js>, - objects: WritableStreamObjects<'js, WritableStreamDefaultWriterOwned<'js>>, - chunk: Value<'js>, - ) -> ( - Result>, - WritableStreamClassObjects<'js, WritableStreamDefaultWriterOwned<'js>>, - ) { - let strategy_size_algorithm = objects - .controller - .strategy_size_algorithm - .clone() - .unwrap_or(SizeAlgorithm::AlwaysOne); - - let objects_class = objects.into_inner(); - - (strategy_size_algorithm.call(ctx, chunk), objects_class) - } - - fn start_algorithm( - ctx: Ctx<'js>, - objects: WritableStreamObjects<'js, UndefinedWriter>, - start_algorithm: WritableStartAlgorithm<'js>, - ) -> Result<(Value<'js>, WritableStreamClassObjects<'js, UndefinedWriter>)> { - let objects_class = objects.into_inner(); - - Ok(( - start_algorithm.call(ctx, objects_class.controller.clone())?, - objects_class, - )) - } - - fn write_algorithm>( - ctx: &Ctx<'js>, - objects: WritableStreamObjects<'js, W>, - chunk: Value<'js>, - ) -> Result<(Promise<'js>, WritableStreamClassObjects<'js, W>)> { - let write_algorithm = - objects.controller.write_algorithm.clone().expect( - "write algorithm used after WritableStreamDefaultControllerClearAlgorithms", - ); - let promise_primordials = objects.stream.promise_primordials.clone(); - let objects_class = objects.into_inner(); - - Ok(( - write_algorithm.call( - ctx, - &promise_primordials, - objects_class.controller.clone().clone(), - chunk, - )?, - objects_class, - )) - } - - fn close_algorithm>( - ctx: &Ctx<'js>, - objects: WritableStreamObjects<'js, W>, - ) -> Result<(Promise<'js>, WritableStreamClassObjects<'js, W>)> { - let close_algorithm = - objects.controller.close_algorithm.clone().expect( - "close algorithm used after WritableStreamDefaultControllerClearAlgorithms", - ); - let promise_primordials = objects.stream.promise_primordials.clone(); - let objects_class = objects.into_inner(); - - Ok(( - close_algorithm.call(ctx, &promise_primordials)?, - objects_class, - )) - } - - fn abort_algorithm>( - ctx: &Ctx<'js>, - objects: WritableStreamObjects<'js, W>, - reason: Value<'js>, - ) -> Result<(Promise<'js>, WritableStreamClassObjects<'js, W>)> { - let abort_algorithm = - objects.controller.abort_algorithm.clone().expect( - "abort algorithm used after WritableStreamDefaultControllerClearAlgorithms", - ); - let promise_primordials = objects.stream.promise_primordials.clone(); - let objects_class = objects.into_inner(); - - Ok(( - abort_algorithm.call(ctx, &promise_primordials, reason)?, - objects_class, - )) - } -} - -#[methods(rename_all = "camelCase")] -impl<'js> WritableStreamDefaultController<'js> { - // this is required by web platform tests - #[qjs(get)] - pub fn constructor(ctx: Ctx<'js>) -> Result>> { - ::constructor(&ctx) - } - - #[qjs(constructor)] - fn new(ctx: Ctx<'js>) -> Result> { - Err(Exception::throw_type(&ctx, "Illegal constructor")) - } - - // readonly attribute AbortSignal signal; - #[qjs(get)] - fn signal(&self) -> Class<'js, AbortSignal<'js>> { - // Return this.[[abortController]]'s signal. - self.abort_controller.borrow().signal() - } - - // undefined error(optional any e); - fn error( - ctx: Ctx<'js>, - controller: This>, - e: Opt>, - ) -> Result<()> { - let objects = WritableStreamObjects::from_controller(controller.0); - - // Let state be this.[[stream]].[[state]]. - // If state is not "writable", return. - if !matches!(objects.stream.state, WritableStreamState::Writable) { - return Ok(()); - } - - // Perform ! WritableStreamDefaultControllerError(this, e). - Self::writable_stream_default_controller_error( - ctx.clone(), - objects.refresh_writer(), - e.0.unwrap_or_undefined(&ctx), - )?; - - Ok(()) - } -} - -#[derive(Clone)] -pub(crate) enum WritableStartAlgorithm<'js> { - ReturnUndefined, - Function { - f: Function<'js>, - underlying_sink: Null>>, - }, - Transform(Promise<'js>), -} - -impl<'js> WritableStartAlgorithm<'js> { - fn call( - &self, - ctx: Ctx<'js>, - controller: WritableStreamDefaultControllerClass<'js>, - ) -> Result> { - match self { - WritableStartAlgorithm::ReturnUndefined => Ok(Value::new_undefined(ctx.clone())), - WritableStartAlgorithm::Function { f, underlying_sink } => { - f.call::<_, Value>((This(underlying_sink.clone()), controller)) - } - WritableStartAlgorithm::Transform(promise) => Ok(promise.clone().into_value()), - } - } -} - -#[derive(JsLifetime, Trace, Clone)] -pub(crate) enum WritableWriteAlgorithm<'js> { - ReturnPromiseUndefined, - Function { - f: Function<'js>, - underlying_sink: Null>>, - }, - Transform { - stream: TransformStreamClass<'js>, - controller: TransformStreamDefaultControllerClass<'js>, - }, -} - -impl<'js> WritableWriteAlgorithm<'js> { - fn call( - &self, - ctx: &Ctx<'js>, - promise_primordials: &PromisePrimordials<'js>, - controller: WritableStreamDefaultControllerClass<'js>, - chunk: Value<'js>, - ) -> Result> { - match self { - WritableWriteAlgorithm::ReturnPromiseUndefined => { - Ok(promise_primordials.promise_resolved_with_undefined.clone()) - } - WritableWriteAlgorithm::Function { f, underlying_sink } => promise_resolved_with( - ctx, - promise_primordials, - f.call::<_, Value>((This(underlying_sink.clone()), chunk, controller)), - ), - WritableWriteAlgorithm::Transform { - stream, - controller: ts_controller, - } => crate::llrt_stream_web::transform::stream::sink_write_algorithm( - ctx.clone(), - stream, - ts_controller, - chunk, - ), - } - } -} - -#[derive(JsLifetime, Trace, Clone)] -pub(crate) enum WritableCloseAlgorithm<'js> { - ReturnPromiseUndefined, - Function { - f: Function<'js>, - underlying_sink: Null>>, - }, - Transform { - stream: TransformStreamClass<'js>, - controller: TransformStreamDefaultControllerClass<'js>, - }, -} - -impl<'js> WritableCloseAlgorithm<'js> { - fn call( - &self, - ctx: &Ctx<'js>, - promise_primordials: &PromisePrimordials<'js>, - ) -> Result> { - match self { - WritableCloseAlgorithm::ReturnPromiseUndefined => { - Ok(promise_primordials.promise_resolved_with_undefined.clone()) - } - WritableCloseAlgorithm::Function { f, underlying_sink } => promise_resolved_with( - ctx, - promise_primordials, - f.call::<_, Value>((This(underlying_sink.clone()),)), - ), - WritableCloseAlgorithm::Transform { stream, controller } => { - crate::llrt_stream_web::transform::stream::sink_close_algorithm( - ctx.clone(), - stream, - controller, - ) - } - } - } -} - -#[derive(JsLifetime, Trace, Clone)] -pub(crate) enum WritableAbortAlgorithm<'js> { - ReturnPromiseUndefined, - Function { - f: Function<'js>, - underlying_sink: Null>>, - }, - Transform { - controller: TransformStreamDefaultControllerClass<'js>, - }, -} - -impl<'js> WritableAbortAlgorithm<'js> { - fn call( - &self, - ctx: &Ctx<'js>, - promise_primordials: &PromisePrimordials<'js>, - reason: Value<'js>, - ) -> Result> { - match self { - WritableAbortAlgorithm::ReturnPromiseUndefined => { - Ok(promise_primordials.promise_resolved_with_undefined.clone()) - } - WritableAbortAlgorithm::Function { f, underlying_sink } => promise_resolved_with( - ctx, - promise_primordials, - f.call::<_, Value>((This(underlying_sink.clone()), reason)), - ), - WritableAbortAlgorithm::Transform { controller } => { - crate::llrt_stream_web::transform::stream::sink_abort_algorithm( - ctx.clone(), - controller, - reason, - ) - } - } - } -} - -#[derive(Trace, Clone, JsLifetime)] -pub(crate) struct WritableStreamDefaultControllerPrimordials<'js> { - close_sentinel: Symbol<'js>, -} - -impl<'js> Primordial<'js> for WritableStreamDefaultControllerPrimordials<'js> { - fn new(ctx: &Ctx<'js>) -> Result - where - Self: Sized, - { - Ok(Self { - close_sentinel: Symbol::new_global(ctx.clone(), "close sentinel")?, - }) - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/writable/default_writer.rs b/stdlib/src/llrt/llrt_stream_web/writable/default_writer.rs deleted file mode 100644 index 0a4be1aa..00000000 --- a/stdlib/src/llrt/llrt_stream_web/writable/default_writer.rs +++ /dev/null @@ -1,497 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use crate::llrt_utils::option::Null; -use rquickjs::{ - class::{JsClass, OwnedBorrow, OwnedBorrowMut, Trace}, - function::Constructor, - prelude::{Opt, This}, - Class, Ctx, Exception, JsLifetime, Promise, Result, Value, -}; - -use crate::llrt_stream_web::{ - utils::{ - promise::{ - promise_rejected_with, promise_rejected_with_constructor, PromisePrimordials, - ResolveablePromise, - }, - UnwrapOrUndefined, - }, - writable::{ - default_controller::WritableStreamDefaultController, - objects::WritableStreamObjects, - stream::{WritableStream, WritableStreamOwned, WritableStreamState}, - writer::WritableStreamWriter, - }, -}; - -#[rquickjs::class] -#[derive(JsLifetime)] -pub(crate) struct WritableStreamDefaultWriter<'js> { - pub(crate) ready_promise: ResolveablePromise<'js>, - pub(crate) closed_promise: ResolveablePromise<'js>, - pub(super) stream: Option>>, - - constructor_type_error: Constructor<'js>, - promise_primordials: PromisePrimordials<'js>, -} - -impl<'js> Trace<'js> for WritableStreamDefaultWriter<'js> { - fn trace<'a>(&self, tracer: rquickjs::class::Tracer<'a, 'js>) { - self.ready_promise.trace(tracer); - self.closed_promise.trace(tracer); - self.stream.trace(tracer); - self.constructor_type_error.trace(tracer); - self.promise_primordials.trace(tracer); - } -} - -pub(crate) type WritableStreamDefaultWriterClass<'js> = - Class<'js, WritableStreamDefaultWriter<'js>>; -pub(crate) type WritableStreamDefaultWriterOwned<'js> = - OwnedBorrowMut<'js, WritableStreamDefaultWriter<'js>>; - -#[rquickjs::methods(rename_all = "camelCase")] -impl<'js> WritableStreamDefaultWriter<'js> { - // this is required by web platform tests - #[qjs(get)] - pub fn constructor(ctx: Ctx<'js>) -> Result>> { - ::constructor(&ctx) - } - - #[qjs(constructor)] - fn new(ctx: Ctx<'js>, stream: WritableStreamOwned<'js>) -> Result> { - // Perform ? SetUpWritableStreamDefaultWriter(this, stream). - let (_, writer) = Self::set_up_writable_stream_default_writer(&ctx, stream)?; - Ok(writer) - } - - #[qjs(get)] - fn closed(writer: This>) -> Promise<'js> { - // Return this.[[closedPromise]]. - writer.0.closed_promise.promise.clone() - } - - #[qjs(get)] - fn desired_size(ctx: Ctx<'js>, writer: This>) -> Result> { - match writer.0.stream { - // If this.[[stream]] is undefined, throw a TypeError exception. - None => Err(Exception::throw_type( - &ctx, - "Cannot desiredSize a stream using a released writer", - )), - Some(ref stream) => { - // Return ! WritableStreamDefaultWriterGetDesiredSize(this). - Self::writable_stream_default_writer_get_desired_size(&OwnedBorrowMut::from_class( - stream.clone(), - )) - } - } - } - - #[qjs(get)] - fn ready(writer: This>) -> Promise<'js> { - // Return this.[[readyPromise]]. - writer.0.ready_promise.promise.clone() - } - - fn abort( - ctx: Ctx<'js>, - writer: This>, - reason: Opt>, - ) -> Result> { - // If this.[[stream]] is undefined, throw a TypeError exception. - if writer.0.stream.is_none() { - promise_rejected_with_constructor( - &writer.constructor_type_error, - &writer.promise_primordials, - "Cannot abort a stream using a released writer", - ) - } else { - let objects = WritableStreamObjects::from_writer(writer.0); - - // Return ! WritableStreamDefaultWriterAbort(this, reason). - Self::writable_stream_default_writer_abort(ctx.clone(), objects, reason.0) - } - } - - fn close(ctx: Ctx<'js>, writer: This>) -> Result> { - // If this.[[stream]] is undefined, throw a TypeError exception. - if writer.0.stream.is_none() { - promise_rejected_with_constructor( - &writer.constructor_type_error, - &writer.promise_primordials, - "Cannot close a stream using a released writer", - ) - } else { - let objects = WritableStreamObjects::from_writer(writer.0); - - // If ! WritableStreamCloseQueuedOrInFlight(stream) is true, return a promise rejected with a TypeError exception. - if objects.stream.writable_stream_close_queued_or_in_flight() { - return promise_rejected_with_constructor( - &objects.writer.constructor_type_error, - &objects.writer.promise_primordials, - "Cannot close an already-closing", - ); - } - - // Return ! WritableStreamDefaultWriterClose(this). - Self::writable_stream_default_writer_close(ctx, objects) - } - } - - fn release_lock(writer: This>) -> Result<()> { - // If stream is undefined, return. - if writer.0.stream.is_none() { - Ok(()) - } else { - let objects = WritableStreamObjects::from_writer(writer.0); - - // Perform ! WritableStreamDefaultWriterRelease(this). - Self::writable_stream_default_writer_release(objects) - } - } - - fn write( - ctx: Ctx<'js>, - writer: This>, - chunk: Opt>, - ) -> Result> { - // If this.[[stream]] is undefined, throw a TypeError exception. - if writer.0.stream.is_none() { - promise_rejected_with_constructor( - &writer.constructor_type_error, - &writer.promise_primordials, - "Cannot write a stream using a released writer", - ) - } else { - let objects = WritableStreamObjects::from_writer(writer.0); - - // Return ! WritableStreamDefaultWriterWrite(this, chunk). - Self::writable_stream_default_writer_write( - ctx.clone(), - objects, - chunk.0.unwrap_or_undefined(&ctx), - ) - } - } -} - -impl<'js> WritableStreamDefaultWriter<'js> { - pub(crate) fn acquire_writable_stream_default_writer( - ctx: &Ctx<'js>, - stream: WritableStreamOwned<'js>, - ) -> Result<(WritableStreamOwned<'js>, Class<'js, Self>)> { - Self::set_up_writable_stream_default_writer(ctx, stream) - } - - pub(super) fn set_up_writable_stream_default_writer( - ctx: &Ctx<'js>, - mut stream: WritableStreamOwned<'js>, - ) -> Result<(WritableStreamOwned<'js>, Class<'js, Self>)> { - // If ! IsWritableStreamLocked(stream) is true, throw a TypeError exception. - if stream.is_writable_stream_locked() { - return Err(Exception::throw_type( - ctx, - "This stream has already been locked for exclusive writing by another writer", - )); - } - - let promise_primordials = stream.promise_primordials.clone(); - let constructor_type_error = stream.constructor_type_error.clone(); - let stream_class = stream.into_inner(); - stream = OwnedBorrowMut::from_class(stream_class.clone()); - - let (ready_promise, closed_promise) = match stream.state { - WritableStreamState::Writable => { - let ready_promise = - if !stream.writable_stream_close_queued_or_in_flight() && stream.backpressure { - // If ! WritableStreamCloseQueuedOrInFlight(stream) is false and stream.[[backpressure]] is true, set writer.[[readyPromise]] to a new promise. - ResolveablePromise::new(ctx)? - } else { - // Otherwise, set writer.[[readyPromise]] to a promise resolved with undefined. - ResolveablePromise::resolved_with_undefined(&stream.promise_primordials) - }; - - // Set writer.[[closedPromise]] to a new promise. - (ready_promise, ResolveablePromise::new(ctx)?) - } - WritableStreamState::Erroring(ref stored_error) => { - let ready_promise = ResolveablePromise::rejected_with( - &stream.promise_primordials, - stored_error.clone(), - )?; - ready_promise.set_is_handled()?; - // Set writer.[[closedPromise]] to a new promise. - (ready_promise, ResolveablePromise::new(ctx)?) - } - WritableStreamState::Closed => { - let promise = - ResolveablePromise::resolved_with_undefined(&stream.promise_primordials); - // Set writer.[[readyPromise]] to a promise resolved with undefined. - // Set writer.[[closedPromise]] to a promise resolved with undefined. - (promise.clone(), promise) - } - // Let storedError be stream.[[storedError]]. - WritableStreamState::Errored(ref stored_error) => { - let promise = ResolveablePromise::rejected_with( - &stream.promise_primordials, - stored_error.clone(), - )?; - promise.set_is_handled()?; - // Set writer.[[readyPromise]] to a promise rejected with storedError. - // Set writer.[[readyPromise]].[[PromiseIsHandled]] to true. - // Set writer.[[closedPromise]] to a promise rejected with storedError. - // Set writer.[[closedPromise]].[[PromiseIsHandled]] to true. - (promise.clone(), promise) - } - }; - - let writer = Self { - ready_promise, - closed_promise, - // Set writer.[[stream]] to stream. - stream: Some(stream_class), - promise_primordials, - constructor_type_error, - }; - - let writer = Class::instance(ctx.clone(), writer)?; - - stream.writer = Some(writer.clone()); - - Ok((stream, writer)) - } - - pub(super) fn writable_stream_default_writer_ensure_ready_promise_rejected( - &mut self, - promise_primordials: &PromisePrimordials<'js>, - error: Value<'js>, - ) -> Result<()> { - if self.ready_promise.is_pending() { - // If writer.[[readyPromise]].[[PromiseState]] is "pending", reject writer.[[readyPromise]] with error. - self.ready_promise.reject(error)?; - } else { - // Otherwise, set writer.[[readyPromise]] to a promise rejected with error. - self.ready_promise = ResolveablePromise::rejected_with(promise_primordials, error)?; - } - - // Set writer.[[readyPromise]].[[PromiseIsHandled]] to true. - self.ready_promise.set_is_handled()?; - Ok(()) - } - - pub(super) fn writable_stream_default_writer_ensure_closed_promise_rejected( - &mut self, - promise_primordials: &PromisePrimordials<'js>, - error: Value<'js>, - ) -> Result<()> { - if self.closed_promise.is_pending() { - // If writer.[[closedPromise]].[[PromiseState]] is "pending", reject writer.[[closedPromise]] with error. - self.closed_promise.reject(error)?; - } else { - // Otherwise, set writer.[[closedPromise]] to a promise rejected with error. - self.closed_promise = ResolveablePromise::rejected_with(promise_primordials, error)?; - } - - // Set writer.[[closedPromise]].[[PromiseIsHandled]] to true. - self.closed_promise.set_is_handled()?; - Ok(()) - } - - pub(super) fn writable_stream_default_writer_get_desired_size( - // Let stream be writer.[[stream]]. - stream: &WritableStream<'js>, - ) -> Result> { - // Let state be stream.[[state]]. - // If state is "errored" or "erroring", return null. - if matches!( - stream.state, - WritableStreamState::Errored(_) | WritableStreamState::Erroring(_) - ) { - return Ok(Null(None)); - } - - // If state is "closed", return 0. - if matches!(stream.state, WritableStreamState::Closed) { - return Ok(Null(Some(0.0))); - } - - // Return ! WritableStreamDefaultControllerGetDesiredSize(stream.[[controller]]). - let controller = OwnedBorrow::from_class( - stream - .controller - .clone() - .expect("Stream in state writable must have a controller"), - ); - - Ok(Null(Some( - controller.writable_stream_default_controller_get_desired_size(), - ))) - } - - fn writable_stream_default_writer_abort( - ctx: Ctx<'js>, - objects: WritableStreamObjects<'js, OwnedBorrowMut<'js, Self>>, - reason: Option>, - ) -> Result> { - // Return ! WritableStreamAbort(stream, reason). - let (promise, _) = WritableStream::writable_stream_abort(ctx, objects, reason)?; - Ok(promise) - } - - fn writable_stream_default_writer_close( - ctx: Ctx<'js>, - objects: WritableStreamObjects<'js, OwnedBorrowMut<'js, Self>>, - ) -> Result> { - // Return ! WritableStreamClose(stream). - let (promise, _) = WritableStream::writable_stream_close(ctx, objects)?; - Ok(promise) - } - - pub(crate) fn writable_stream_default_writer_close_with_error_propagation( - ctx: Ctx<'js>, - // Let stream be writer.[[stream]]. - objects: WritableStreamObjects<'js, OwnedBorrowMut<'js, Self>>, - ) -> Result> { - // Let state be stream.[[state]]. - // If ! WritableStreamCloseQueuedOrInFlight(stream) is true or state is "closed", return a promise resolved with undefined. - if objects.stream.writable_stream_close_queued_or_in_flight() - || matches!(objects.stream.state, WritableStreamState::Closed) - { - return Ok(objects - .stream - .promise_primordials - .promise_resolved_with_undefined - .clone()); - } - - // If state is "errored", return a promise rejected with stream.[[storedError]]. - if let WritableStreamState::Errored(ref stored_error) = objects.stream.state { - return promise_rejected_with( - &objects.stream.promise_primordials, - stored_error.clone(), - ); - } - - // Return ! WritableStreamDefaultWriterClose(writer). - Self::writable_stream_default_writer_close(ctx, objects) - } - - pub(crate) fn writable_stream_default_writer_release( - mut objects: WritableStreamObjects<'js, OwnedBorrowMut<'js, Self>>, - ) -> Result<()> { - // Let releasedError be a new TypeError. - let released_error: Value = objects.stream.constructor_type_error.call(( - "Writer was released and can no longer be used to monitor the stream's closedness", - ))?; - - // Perform ! WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, releasedError). - objects - .writer - .writable_stream_default_writer_ensure_ready_promise_rejected( - &objects.stream.promise_primordials, - released_error.clone(), - )?; - // Perform ! WritableStreamDefaultWriterEnsureClosedPromiseRejected(writer, releasedError). - objects - .writer - .writable_stream_default_writer_ensure_closed_promise_rejected( - &objects.stream.promise_primordials, - released_error, - )?; - - // Set stream.[[writer]] to undefined. - objects.stream.writer = None; - // Set writer.[[stream]] to undefined. - objects.writer.stream = None; - - Ok(()) - } - - pub(crate) fn writable_stream_default_writer_write( - ctx: Ctx<'js>, - objects: WritableStreamObjects<'js, OwnedBorrowMut<'js, Self>>, - chunk: Value<'js>, - ) -> Result> { - // Let chunkSize be ! WritableStreamDefaultControllerGetChunkSize(controller, chunk). - let (chunk_size, mut objects) = - WritableStreamDefaultController::writable_stream_default_controller_get_chunk_size( - ctx.clone(), - objects, - chunk.clone(), - )?; - - let stream_class = objects.stream.into_inner(); - objects.stream = OwnedBorrowMut::from_class(stream_class.clone()); - - // If stream is not equal to writer.[[stream]], return a promise rejected with a TypeError exception. - if objects.writer.stream != Some(stream_class) { - return promise_rejected_with_constructor( - &objects.stream.constructor_type_error, - &objects.stream.promise_primordials, - "Cannot write to a stream using a released writer", - ); - } - - // Let state be stream.[[state]]. - // If state is "errored", return a promise rejected with stream.[[storedError]]. - if let WritableStreamState::Errored(ref stored_error) = objects.stream.state { - return promise_rejected_with( - &objects.stream.promise_primordials, - stored_error.clone(), - ); - } - - // If ! WritableStreamCloseQueuedOrInFlight(stream) is true or state is "closed", return a promise rejected with a TypeError exception indicating that the stream is closing or closed. - if objects.stream.writable_stream_close_queued_or_in_flight() - || matches!(objects.stream.state, WritableStreamState::Closed) - { - return promise_rejected_with_constructor( - &objects.stream.constructor_type_error, - &objects.stream.promise_primordials, - "The stream is closing or closed and cannot be written to", - ); - } - - // If state is "erroring", return a promise rejected with stream.[[storedError]]. - if let WritableStreamState::Erroring(ref stored_error) = objects.stream.state { - return promise_rejected_with( - &objects.stream.promise_primordials, - stored_error.clone(), - ); - } - - // Let promise be ! WritableStreamAddWriteRequest(stream). - let promise = objects.stream.writable_stream_add_write_request(&ctx); - // Perform ! WritableStreamDefaultControllerWrite(controller, chunk, chunkSize). - WritableStreamDefaultController::writable_stream_default_controller_write( - ctx, objects, chunk, chunk_size, - )?; - - // Return promise. - promise - } -} - -impl<'js> WritableStreamWriter<'js> for WritableStreamDefaultWriterOwned<'js> { - type Class = WritableStreamDefaultWriterClass<'js>; - - fn with_writer( - self, - ctx: C, - default: impl FnOnce( - C, - WritableStreamDefaultWriterOwned<'js>, - ) -> Result<(C, WritableStreamDefaultWriterOwned<'js>)>, - _: impl FnOnce(C) -> Result, - ) -> Result<(C, Self)> { - default(ctx, self) - } - - fn into_inner(self) -> Self::Class { - self.into_inner() - } - - fn from_class(class: Self::Class) -> Self { - OwnedBorrowMut::from_class(class) - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/writable/mod.rs b/stdlib/src/llrt/llrt_stream_web/writable/mod.rs deleted file mode 100644 index 4b7e3284..00000000 --- a/stdlib/src/llrt/llrt_stream_web/writable/mod.rs +++ /dev/null @@ -1,17 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -mod default_controller; -mod default_writer; -mod objects; -mod stream; -mod writer; - -pub(crate) use default_controller::{ - WritableAbortAlgorithm, WritableCloseAlgorithm, WritableStartAlgorithm, - WritableStreamDefaultController, WritableStreamDefaultControllerPrimordials, - WritableWriteAlgorithm, -}; -pub(crate) use default_writer::{WritableStreamDefaultWriter, WritableStreamDefaultWriterOwned}; -pub(crate) use objects::{WritableStreamClassObjects, WritableStreamObjects}; -pub(crate) use stream::{ - WritableStream, WritableStreamClass, WritableStreamOwned, WritableStreamState, -}; diff --git a/stdlib/src/llrt/llrt_stream_web/writable/objects.rs b/stdlib/src/llrt/llrt_stream_web/writable/objects.rs deleted file mode 100644 index 01bf593c..00000000 --- a/stdlib/src/llrt/llrt_stream_web/writable/objects.rs +++ /dev/null @@ -1,162 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use rquickjs::{class::OwnedBorrowMut, Class, Result}; - -use crate::llrt_stream_web::writable::{ - default_controller::{ - WritableStreamDefaultControllerClass, WritableStreamDefaultControllerOwned, - }, - default_writer::WritableStreamDefaultWriterOwned, - stream::{WritableStream, WritableStreamOwned}, - writer::{UndefinedWriter, WritableStreamWriter}, -}; - -pub(crate) struct WritableStreamObjects<'js, W> { - pub(crate) stream: WritableStreamOwned<'js>, - pub(crate) controller: WritableStreamDefaultControllerOwned<'js>, - pub(crate) writer: W, -} - -pub(crate) struct WritableStreamClassObjects<'js, W: WritableStreamWriter<'js>> { - pub(crate) stream: Class<'js, WritableStream<'js>>, - pub(crate) controller: WritableStreamDefaultControllerClass<'js>, - pub(crate) writer: W::Class, -} - -impl<'js, W: WritableStreamWriter<'js>> Clone for WritableStreamClassObjects<'js, W> { - fn clone(&self) -> Self { - Self { - stream: self.stream.clone(), - controller: self.controller.clone(), - writer: self.writer.clone(), - } - } -} - -impl<'js, W: WritableStreamWriter<'js>> WritableStreamObjects<'js, W> { - pub(super) fn into_inner(self) -> WritableStreamClassObjects<'js, W> { - WritableStreamClassObjects { - stream: self.stream.into_inner(), - controller: self.controller.into_inner(), - writer: self.writer.into_inner(), - } - } - - pub(crate) fn from_class(objects_class: WritableStreamClassObjects<'js, W>) -> Self { - Self { - stream: OwnedBorrowMut::from_class(objects_class.stream), - controller: OwnedBorrowMut::from_class(objects_class.controller), - writer: W::from_class(objects_class.writer), - } - } - - pub(super) fn from_class_no_writer( - objects_class: WritableStreamClassObjects<'js, W>, - ) -> WritableStreamObjects<'js, UndefinedWriter> { - WritableStreamObjects { - stream: OwnedBorrowMut::from_class(objects_class.stream), - controller: OwnedBorrowMut::from_class(objects_class.controller), - writer: UndefinedWriter, - } - } - - pub(super) fn with_writer( - mut self, - default: impl FnOnce( - WritableStreamObjects<'js, WritableStreamDefaultWriterOwned<'js>>, - ) -> Result< - WritableStreamObjects<'js, WritableStreamDefaultWriterOwned<'js>>, - >, - none: impl FnOnce( - WritableStreamObjects<'js, UndefinedWriter>, - ) -> Result>, - ) -> Result { - ((self.stream, self.controller), self.writer) = self.writer.with_writer( - (self.stream, self.controller), - |(stream, controller), writer| { - let objects = default(WritableStreamObjects { - stream, - controller, - writer, - })?; - - Ok(((objects.stream, objects.controller), objects.writer)) - }, - |(stream, controller)| { - let objects = none(WritableStreamObjects { - stream, - controller, - writer: UndefinedWriter, - })?; - - Ok((objects.stream, objects.controller)) - }, - )?; - - Ok(self) - } -} - -impl<'js, W: WritableStreamWriter<'js>> WritableStreamObjects<'js, W> { - pub(super) fn refresh_writer( - mut self, - ) -> WritableStreamObjects<'js, Option>> { - drop(self.writer); - let writer = self.stream.writer_mut(); - WritableStreamObjects { - stream: self.stream, - controller: self.controller, - writer, - } - } -} - -impl<'js> WritableStreamObjects<'js, UndefinedWriter> { - pub(super) fn from_stream(stream: WritableStreamOwned<'js>) -> Self { - let controller = OwnedBorrowMut::from_class( - stream - .controller - .clone() - .expect("WritableStream must have controller"), - ); - - WritableStreamObjects { - stream, - controller, - writer: UndefinedWriter, - } - } - - pub(super) fn from_controller(controller: WritableStreamDefaultControllerOwned<'js>) -> Self { - let stream = OwnedBorrowMut::from_class(controller.stream.clone()); - - WritableStreamObjects { - stream, - controller, - writer: UndefinedWriter, - } - } -} - -impl<'js> WritableStreamObjects<'js, WritableStreamDefaultWriterOwned<'js>> { - pub(super) fn from_writer(writer: WritableStreamDefaultWriterOwned<'js>) -> Self { - let stream = OwnedBorrowMut::from_class( - writer - .stream - .clone() - .expect("WritableStreamDefaultWriter must have a stream"), - ); - - let controller = OwnedBorrowMut::from_class( - stream - .controller - .clone() - .expect("WritableStreamDefaultWriter stream must have a controller"), - ); - - WritableStreamObjects { - stream, - controller, - writer, - } - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/writable/stream/mod.rs b/stdlib/src/llrt/llrt_stream_web/writable/stream/mod.rs deleted file mode 100644 index 264c9cba..00000000 --- a/stdlib/src/llrt/llrt_stream_web/writable/stream/mod.rs +++ /dev/null @@ -1,772 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use std::collections::VecDeque; - -use crate::llrt_abort::AbortController; -use crate::llrt_utils::{ - option::{Null, Undefined}, - primordials::{BasePrimordials, Primordial}, -}; -use rquickjs::{ - class::{OwnedBorrowMut, Trace, Tracer}, - function::Constructor, - prelude::{Opt, This}, - Class, Ctx, Exception, JsLifetime, Object, Promise, Result, Value, -}; - -use crate::llrt_stream_web::{ - queuing_strategy::QueuingStrategy, - utils::{ - promise::{ - promise_rejected_with_constructor, upon_promise, PromisePrimordials, ResolveablePromise, - }, - UnwrapOrUndefined, - }, - writable::{ - default_controller::{ - WritableStreamDefaultController, WritableStreamDefaultControllerClass, - }, - default_writer::{ - WritableStreamDefaultWriter, WritableStreamDefaultWriterClass, - WritableStreamDefaultWriterOwned, - }, - objects::WritableStreamObjects, - writer::WritableStreamWriter, - }, -}; -use sink::UnderlyingSink; - -pub(super) mod sink; - -#[rquickjs::class] -#[derive(JsLifetime)] -pub struct WritableStream<'js> { - pub(super) backpressure: bool, - close_request: Option>, - pub(crate) controller: Option>, - pub in_flight_write_request: Option>, - in_flight_close_request: Option>, - pending_abort_request: Option>, - pub(crate) state: WritableStreamState<'js>, - pub(crate) writer: Option>, - write_requests: VecDeque>, - pub(super) constructor_type_error: Constructor<'js>, - pub(crate) promise_primordials: PromisePrimordials<'js>, -} - -impl<'js> Trace<'js> for WritableStream<'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - self.close_request.trace(tracer); - self.controller.trace(tracer); - self.in_flight_write_request.trace(tracer); - self.in_flight_close_request.trace(tracer); - self.pending_abort_request.trace(tracer); - self.state.trace(tracer); - self.writer.trace(tracer); - self.write_requests.trace(tracer); - self.constructor_type_error.trace(tracer); - self.promise_primordials.trace(tracer); - } -} - -pub(crate) type WritableStreamClass<'js> = Class<'js, WritableStream<'js>>; -pub(crate) type WritableStreamOwned<'js> = OwnedBorrowMut<'js, WritableStream<'js>>; - -#[rquickjs::methods(rename_all = "camelCase")] -impl<'js> WritableStream<'js> { - // constructor(optional object underlyingSink, optional QueuingStrategy strategy = {}); - #[qjs(constructor)] - fn new( - ctx: Ctx<'js>, - underlying_sink: Opt>>, - queuing_strategy: Opt>>, - ) -> Result> { - // If underlyingSink is missing, set it to null. - let underlying_sink = Null(underlying_sink.0); - - // Let underlyingSinkDict be underlyingSink, converted to an IDL value of type UnderlyingSink. - let underlying_sink_dict = match underlying_sink { - Null(None) | Null(Some(Undefined(None))) => UnderlyingSink::default(), - Null(Some(Undefined(Some(ref obj)))) => UnderlyingSink::from_object(obj.clone())?, - }; - - // If underlyingSinkDict["type"] exists, throw a RangeError exception. - if underlying_sink_dict.r#type.is_some() { - return Err(Exception::throw_range(&ctx, "Invalid type is specified")); - } - - // Perform ! InitializeWritableStream(this). - let stream_class = Class::instance( - ctx.clone(), - Self { - // Set stream.[[state]] to "writable". - state: WritableStreamState::Writable, - // Set stream.[[storedError]], stream.[[writer]], stream.[[controller]], stream.[[inFlightWriteRequest]], stream.[[closeRequest]], stream.[[inFlightCloseRequest]], and stream.[[pendingAbortRequest]] to undefined. - writer: None, - controller: None, - in_flight_write_request: None, - close_request: None, - in_flight_close_request: None, - pending_abort_request: None, - // Set stream.[[writeRequests]] to a new empty list. - write_requests: VecDeque::new(), - // Set stream.[[backpressure]] to false. - backpressure: false, - constructor_type_error: BasePrimordials::get(&ctx)?.constructor_type_error.clone(), - promise_primordials: PromisePrimordials::get(&ctx)?.clone(), - }, - )?; - let stream = OwnedBorrowMut::from_class(stream_class.clone()); - let queuing_strategy = queuing_strategy.0.and_then(|qs| qs.0); - - // Let sizeAlgorithm be ! ExtractSizeAlgorithm(strategy). - let size_algorithm = QueuingStrategy::extract_size_algorithm(queuing_strategy.as_ref()); - - // Let highWaterMark be ? ExtractHighWaterMark(strategy, 1). - let high_water_mark = - QueuingStrategy::extract_high_water_mark(&ctx, queuing_strategy, 1.0)?; - - // Perform ? SetUpWritableStreamDefaultControllerFromUnderlyingSink(this, underlyingSink, underlyingSinkDict, highWaterMark, sizeAlgorithm). - WritableStreamDefaultController::set_up_writable_stream_default_controller_from_underlying_sink(ctx, stream, underlying_sink, underlying_sink_dict, high_water_mark, size_algorithm)?; - - Ok(stream_class) - } - - // readonly attribute boolean locked; - #[qjs(get)] - fn locked(&self) -> bool { - // Return ! IsWritableStreamLocked(this). - self.is_writable_stream_locked() - } - - fn abort( - ctx: Ctx<'js>, - stream: This>, - reason: Opt>, - ) -> Result> { - if stream.is_writable_stream_locked() { - // If ! IsWritableStreamLocked(this) is true, return a promise rejected with a TypeError exception. - return promise_rejected_with_constructor( - &stream.constructor_type_error, - &stream.promise_primordials, - "Cannot abort a stream that already has a writer", - ); - } - - let objects = WritableStreamObjects::from_stream(stream.0); - - // Return ! WritableStreamAbort(this, reason). - let (promise, _) = Self::writable_stream_abort(ctx.clone(), objects, reason.0)?; - - Ok(promise) - } - - fn close(ctx: Ctx<'js>, stream: This>) -> Result> { - if stream.is_writable_stream_locked() { - // If ! IsWritableStreamLocked(this) is true, return a promise rejected with a TypeError exception. - return promise_rejected_with_constructor( - &stream.constructor_type_error, - &stream.promise_primordials, - "Cannot close a stream that already has a writer", - ); - } - - if Self::writable_stream_close_queued_or_in_flight(&stream.0) { - // If ! WritableStreamCloseQueuedOrInFlight(this) is true, return a promise rejected with a TypeError exception. - return promise_rejected_with_constructor( - &stream.constructor_type_error, - &stream.promise_primordials, - "Cannot close an already-closing stream", - ); - } - - let objects = WritableStreamObjects::from_stream(stream.0); - - // Return ! WritableStreamClose(this). - let (promise, _) = Self::writable_stream_close(ctx.clone(), objects)?; - - Ok(promise) - } - - fn get_writer( - ctx: Ctx<'js>, - stream: This>, - ) -> Result> { - // Return ? AcquireWritableStreamDefaultWriter(this). - let (_, writer) = - WritableStreamDefaultWriter::acquire_writable_stream_default_writer(&ctx, stream.0)?; - - Ok(writer) - } -} - -impl<'js> WritableStream<'js> { - /// Create a WritableStream for use by TransformStream with properly traced algorithm variants - pub(crate) fn create_for_transform( - ctx: Ctx<'js>, - start_promise: Promise<'js>, - ts_stream: crate::llrt_stream_web::transform::stream::TransformStreamClass<'js>, - ts_controller: crate::llrt_stream_web::transform::controller::TransformStreamDefaultControllerClass<'js>, - high_water_mark: f64, - size_algorithm: crate::llrt_stream_web::queuing_strategy::SizeAlgorithm<'js>, - ) -> Result> { - let stream_class = Class::instance( - ctx.clone(), - Self { - state: WritableStreamState::Writable, - writer: None, - controller: None, - in_flight_write_request: None, - close_request: None, - in_flight_close_request: None, - pending_abort_request: None, - write_requests: VecDeque::new(), - backpressure: false, - constructor_type_error: BasePrimordials::get(&ctx)?.constructor_type_error.clone(), - promise_primordials: PromisePrimordials::get(&ctx)?.clone(), - }, - )?; - - let stream = OwnedBorrowMut::from_class(stream_class.clone()); - - WritableStreamDefaultController::set_up_writable_stream_default_controller( - ctx, - stream, - super::WritableStartAlgorithm::Transform(start_promise), - super::WritableWriteAlgorithm::Transform { - stream: ts_stream.clone(), - controller: ts_controller.clone(), - }, - super::WritableCloseAlgorithm::Transform { - stream: ts_stream, - controller: ts_controller.clone(), - }, - super::WritableAbortAlgorithm::Transform { - controller: ts_controller, - }, - high_water_mark, - size_algorithm, - )?; - - Ok(stream_class) - } - - pub(crate) fn is_writable_stream_locked(&self) -> bool { - if self.writer.is_none() { - // If stream.[[writer]] is undefined, return false. - false - } else { - // Return true. - true - } - } - - pub(crate) fn writable_stream_abort>( - ctx: Ctx<'js>, - mut objects: WritableStreamObjects<'js, W>, - mut reason: Option>, - ) -> Result<(Promise<'js>, WritableStreamObjects<'js, W>)> { - // If stream.[[state]] is "closed" or "errored", return a promise resolved with undefined. - if matches!( - objects.stream.state, - WritableStreamState::Closed | WritableStreamState::Errored(_) - ) { - return Ok(( - objects - .stream - .promise_primordials - .promise_resolved_with_undefined - .clone(), - objects, - )); - } - - // Signal abort on stream.[[controller]].[[abortController]] with reason. - { - // this executes user code, so we should ensure we hold no locks - let abort_controller = objects.controller.abort_controller.clone(); - let objects_class = objects.into_inner(); - AbortController::abort(ctx.clone(), This(abort_controller), Opt(reason.clone()))?; - objects = WritableStreamObjects::from_class(objects_class); - } - - // Let state be stream.[[state]]. - // If state is "closed" or "errored", return a promise resolved with undefined. - if matches!( - objects.stream.state, - WritableStreamState::Closed | WritableStreamState::Errored(_) - ) { - return Ok(( - objects - .stream - .promise_primordials - .promise_resolved_with_undefined - .clone(), - objects, - )); - } - - // If stream.[[pendingAbortRequest]] is not undefined, return stream.[[pendingAbortRequest]]'s promise. - match objects.stream.pending_abort_request { - None => {} - Some(ref pending_abort_request) => { - return Ok((pending_abort_request.promise.promise.clone(), objects)) - } - } - - let was_already_erroring = match objects.stream.state { - // If state is "erroring", - // Set wasAlreadyErroring to true. - // Set reason to undefined. - WritableStreamState::Erroring(_) => { - reason = None; - true - } - // Let wasAlreadyErroring be false. - _ => false, - }; - - // Let promise be a new promise. - let promise = ResolveablePromise::new(&ctx)?; - - let reason = reason.unwrap_or_undefined(&ctx); - - // Set stream.[[pendingAbortRequest]] to a new pending abort request whose promise is promise, reason is reason, and was already erroring is wasAlreadyErroring. - objects.stream.pending_abort_request = Some(PendingAbortRequest { - promise: promise.clone(), - reason: reason.clone(), - was_already_erroring, - }); - - // If wasAlreadyErroring is false, perform ! WritableStreamStartErroring(stream, reason). - if !was_already_erroring { - objects = Self::writable_stream_start_erroring(ctx, objects, reason)?; - } - - Ok((promise.promise.clone(), objects)) - } - - pub(super) fn writable_stream_close>( - ctx: Ctx<'js>, - mut objects: WritableStreamObjects<'js, W>, - ) -> Result<(Promise<'js>, WritableStreamObjects<'js, W>)> { - // Let state be stream.[[state]]. - // If state is "closed" or "errored", return a promise rejected with a TypeError exception. - if matches!( - objects.stream.state, - WritableStreamState::Closed | WritableStreamState::Errored(_) - ) { - return Ok(( - promise_rejected_with_constructor::( - &objects.stream.constructor_type_error, - &objects.stream.promise_primordials, - "The stream is not in the writable state and cannot be closed", - )?, - objects, - )); - } - - // Let promise be a new promise. - let promise = ResolveablePromise::new(&ctx)?; - // Set stream.[[closeRequest]] to promise. - objects.stream.close_request = Some(promise.clone()); - - // Let writer be stream.[[writer]]. - // If writer is not undefined, and stream.[[backpressure]] is true, and state is "writable", resolve writer.[[readyPromise]] with undefined. - objects = objects.with_writer( - |objects| { - if objects.stream.backpressure - && matches!(objects.stream.state, WritableStreamState::Writable) - { - let () = objects.writer.ready_promise.resolve_undefined()?; - } - Ok(objects) - }, - Ok, - )?; - - // Perform ! WritableStreamDefaultControllerClose(stream.[[controller]]). - objects = WritableStreamDefaultController::writable_stream_default_controller_close( - ctx, objects, - )?; - - // Return promise. - Ok((promise.promise.clone(), objects)) - } - - pub(super) fn writable_stream_start_erroring>( - ctx: Ctx<'js>, - // Let controller be stream.[[controller]]. - // Let writer be stream.[[writer]]. - mut objects: WritableStreamObjects<'js, W>, - reason: Value<'js>, - ) -> Result> { - // Set stream.[[state]] to "erroring". - // Set stream.[[storedError]] to reason. - objects.stream.state = WritableStreamState::Erroring(reason.clone()); - - // If writer is not undefined, perform ! WritableStreamDefaultWriterEnsureReadyPromiseRejected(writer, reason). - objects = objects.with_writer( - |mut objects| { - objects - .writer - .writable_stream_default_writer_ensure_ready_promise_rejected( - &objects.stream.promise_primordials, - reason.clone(), - )?; - Ok(objects) - }, - Ok, - )?; - - // If ! WritableStreamHasOperationMarkedInFlight(stream) is false and controller.[[started]] is true, perform ! WritableStreamFinishErroring(stream). - if !objects - .stream - .writable_stream_has_operation_marked_in_flight() - && objects.controller.started - { - objects = Self::writable_stream_finish_erroring(ctx, objects, reason)?; - } - - Ok(objects) - } - - pub(super) fn writable_stream_finish_erroring>( - ctx: Ctx<'js>, - mut objects: WritableStreamObjects<'js, W>, - // Let storedError be stream.[[storedError]]. - stored_error: Value<'js>, - ) -> Result> { - // Set stream.[[state]] to "errored". - objects.stream.state = WritableStreamState::Errored(stored_error.clone()); - - // Perform ! stream.[[controller]].[[ErrorSteps]](). - objects.controller.error_steps(); - - // For each writeRequest of stream.[[writeRequests]]: - for write_request in &mut objects.stream.write_requests { - let () = write_request.reject(stored_error.clone())?; - } - - // Set stream.[[writeRequests]] to an empty list. - objects.stream.write_requests.clear(); - - // Let abortRequest be stream.[[pendingAbortRequest]]. - // Set stream.[[pendingAbortRequest]] to undefined. - let abort_request = if let Some(pending_abort_request) = - objects.stream.pending_abort_request.take() - { - pending_abort_request - } else { - // If stream.[[pendingAbortRequest]] is undefined, - // Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). - objects = - WritableStream::writable_stream_reject_close_and_closed_promise_if_needed(objects)?; - // Return. - return Ok(objects); - }; - - // If abortRequest’s was already erroring is true, - if abort_request.was_already_erroring { - // Reject abortRequest’s promise with storedError. - let () = abort_request.promise.reject(stored_error.clone())?; - - // Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). - objects = - WritableStream::writable_stream_reject_close_and_closed_promise_if_needed(objects)?; - - // Return. - return Ok(objects); - } - - // Let promise be ! stream.[[controller]].[[AbortSteps]](abortRequest’s reason). - let (promise, objects) = - WritableStreamDefaultController::abort_steps(&ctx, objects, abort_request.reason)?; - - let objects_class = objects.into_inner(); - - // Upon fulfillment of promise, - let _ = upon_promise::, _>(ctx.clone(), promise, { - let objects_class = objects_class.clone(); - move |_, result| { - let objects = - WritableStreamObjects::from_class_no_writer(objects_class).refresh_writer(); - match result { - // Upon fulfillment of promise, - Ok(_) => { - // Resolve abortRequest’s promise with undefined. - let () = abort_request.promise.resolve_undefined()?; - // Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). - WritableStream::writable_stream_reject_close_and_closed_promise_if_needed( - objects, - )?; - Ok(()) - } - // Upon rejection of promise with reason reason, - Err(reason) => { - // Reject abortRequest’s promise with reason. - let () = abort_request.promise.reject(reason)?; - // Perform ! WritableStreamRejectCloseAndClosedPromiseIfNeeded(stream). - WritableStream::writable_stream_reject_close_and_closed_promise_if_needed( - objects, - )?; - Ok(()) - } - } - } - })?; - - Ok(WritableStreamObjects::from_class(objects_class)) - } - - fn writable_stream_reject_close_and_closed_promise_if_needed>( - // Let writer be stream.[[writer]]. - mut objects: WritableStreamObjects<'js, W>, - ) -> Result> { - // If stream.[[closeRequest]] is not undefined, - if let Some(ref close_request) = objects.stream.close_request { - // Reject stream.[[closeRequest]] with stream.[[storedError]]. - let () = close_request.reject(objects.stream.stored_error())?; - // Set stream.[[closeRequest]] to undefined. - objects.stream.close_request = None; - } - - // If writer is not undefined, - objects.with_writer( - |objects| { - // Reject writer.[[closedPromise]] with stream.[[storedError]]. - let () = objects - .writer - .closed_promise - .reject(objects.stream.stored_error())?; - - // Set writer.[[closedPromise]].[[PromiseIsHandled]] to true. - objects.writer.closed_promise.set_is_handled()?; - - Ok(objects) - }, - Ok, - ) - } - - pub(super) fn writable_stream_mark_first_write_request_in_flight(&mut self) { - // Let writeRequest be stream.[[writeRequests]][0]. - // Remove writeRequest from stream.[[writeRequests]]. - let write_request = self.write_requests.pop_front().expect("writable_stream_mark_first_write_request_in_flight must be called with non-empty write requests"); - // Set stream.[[inFlightWriteRequest]] to writeRequest. - self.in_flight_write_request = Some(write_request); - } - - pub(super) fn writable_stream_mark_close_request_in_flight(&mut self) { - // Set stream.[[inFlightCloseRequest]] to stream.[[closeRequest]]. - // Set stream.[[closeRequest]] to undefined. - self.in_flight_close_request = - Some(self.close_request.take().expect( - "writable_stream_mark_close_request_in_flight called without close request", - )) - } - - fn writable_stream_has_operation_marked_in_flight(&self) -> bool { - if self.in_flight_write_request.is_none() && self.in_flight_close_request.is_none() { - // If stream.[[inFlightWriteRequest]] is undefined and stream.[[inFlightCloseRequest]] is undefined, return false. - false - } else { - // Return true. - true - } - } - - pub(crate) fn writable_stream_close_queued_or_in_flight(&self) -> bool { - if self.close_request.is_none() && self.in_flight_close_request.is_none() { - // If stream.[[closeRequest]] is undefined and stream.[[inFlightCloseRequest]] is undefined, return false. - false - } else { - // Return true. - true - } - } - - pub(super) fn writable_stream_add_write_request( - &mut self, - ctx: &Ctx<'js>, - ) -> Result> { - // Let promise be a new promise. - let promise = ResolveablePromise::new(ctx)?; - // Append promise to stream.[[writeRequests]]. - self.write_requests.push_back(promise.clone()); - Ok(promise.promise.clone()) - } - - pub(super) fn writable_stream_finish_in_flight_write_with_error< - W: WritableStreamWriter<'js>, - >( - ctx: Ctx<'js>, - mut objects: WritableStreamObjects<'js, W>, - error: Value<'js>, - ) -> Result<()> { - // Reject stream.[[inFlightWriteRequest]] with error. - // Set stream.[[inFlightWriteRequest]] to undefined. - objects.stream.in_flight_write_request.take().expect("writable_stream_finish_in_flight_write_with_error called without in flight write request").reject(error.clone())?; - - // Perform ! WritableStreamDealWithRejection(stream, error). - Self::writable_stream_deal_with_rejection(ctx, objects, error)?; - - Ok(()) - } - - pub(super) fn writable_stream_finish_in_flight_close_with_error< - W: WritableStreamWriter<'js>, - >( - ctx: Ctx<'js>, - mut objects: WritableStreamObjects<'js, W>, - error: Value<'js>, - ) -> Result<()> { - // Reject stream.[[inFlightCloseRequest]] with error. - // Set stream.[[inFlightCloseRequest]] to undefined. - objects.stream.in_flight_close_request.take().expect("writable_stream_finish_in_flight_close_with_error called without in flight close request").reject(error.clone())?; - - // Assert: stream.[[state]] is "writable" or "erroring". - - // If stream.[[pendingAbortRequest]] is not undefined, - if let Some(pending_abort_request) = objects.stream.pending_abort_request.take() { - // Reject stream.[[pendingAbortRequest]]'s promise with error. - // Set stream.[[pendingAbortRequest]] to undefined. - pending_abort_request.promise.reject(error.clone())? - } - - // Perform ! WritableStreamDealWithRejection(stream, error). - Self::writable_stream_deal_with_rejection(ctx, objects, error)?; - - Ok(()) - } - - pub(super) fn writable_stream_deal_with_rejection>( - ctx: Ctx<'js>, - objects: WritableStreamObjects<'js, W>, - error: Value<'js>, - ) -> Result> { - // Let state be stream.[[state]]. - match &objects.stream.state { - // If state is "writable", - WritableStreamState::Writable => { - // Perform ! WritableStreamStartErroring(stream, error). - Self::writable_stream_start_erroring(ctx, objects, error) - }, - WritableStreamState::Erroring(ref stored_error) => { - let stored_error = stored_error.clone(); - // Perform ! WritableStreamFinishErroring(stream). - Self::writable_stream_finish_erroring(ctx, objects, stored_error) - }, - other => panic!("WritableStreamDealWithRejection must be called in state 'writable' or 'erroring', found {other:?}"), - } - } - - pub(super) fn writable_stream_finish_in_flight_write(&mut self) -> Result<()> { - // Resolve stream.[[inFlightWriteRequest]] with undefined. - // Set stream.[[inFlightWriteRequest]] to undefined. - self.in_flight_write_request - .take() - .expect("writable_stream_finish_in_flight_write called without in flight write request") - .resolve_undefined() - } - - pub(super) fn writable_stream_finish_in_flight_close>( - // Let writer be stream.[[writer]]. - mut objects: WritableStreamObjects<'js, W>, - ) -> Result> { - // Assert: stream.[[inFlightCloseRequest]] is not undefined. - - // Resolve stream.[[inFlightCloseRequest]] with undefined. - // Set stream.[[inFlightCloseRequest]] to undefined. - objects - .stream - .in_flight_close_request - .take() - .expect("writable_stream_finish_in_flight_close called without in flight close request") - .resolve_undefined()?; - - // Let state be stream.[[state]]. - // If state is "erroring", - if let WritableStreamState::Erroring(_) = objects.stream.state { - // Set stream.[[storedError]] to undefined. - // (implicitly covered by change to Closed below) - - // If stream.[[pendingAbortRequest]] is not undefined, - if let Some(pending_abort_request) = objects.stream.pending_abort_request.take() { - // Resolve stream.[[pendingAbortRequest]]'s promise with undefined. - // Set stream.[[pendingAbortRequest]] to undefined. - pending_abort_request.promise.resolve_undefined()?; - } - } - - // Set stream.[[state]] to "closed". - objects.stream.state = WritableStreamState::Closed; - - // If writer is not undefined, resolve writer.[[closedPromise]] with undefined. - objects.with_writer( - |objects| { - objects.writer.closed_promise.resolve_undefined()?; - - Ok(objects) - }, - Ok, - ) - } - - pub(super) fn writable_stream_update_backpressure>( - ctx: Ctx<'js>, - // Let writer be stream.[[writer]]. - mut objects: WritableStreamObjects<'js, W>, - backpressure: bool, - ) -> Result> { - // If writer is not undefined and backpressure is not stream.[[backpressure]], - objects = objects.with_writer( - |mut objects| { - if backpressure != objects.stream.backpressure { - if backpressure { - // If backpressure is true, set writer.[[readyPromise]] to a new promise. - objects.writer.ready_promise = ResolveablePromise::new(&ctx)?; - } else { - // Otherwise, - // Resolve writer.[[readyPromise]] with undefined. - objects.writer.ready_promise.resolve_undefined()? - } - } - - Ok(objects) - }, - Ok, - )?; - - // Set stream.[[backpressure]] to backpressure. - objects.stream.backpressure = backpressure; - - Ok(objects) - } - - pub(super) fn writer_mut(&mut self) -> Option> { - self.writer.clone().map(OwnedBorrowMut::from_class) - } - - pub(crate) fn stored_error(&self) -> Option> { - match self.state { - WritableStreamState::Erroring(ref stored_error) - | WritableStreamState::Errored(ref stored_error) => Some(stored_error.clone()), - _ => None, - } - } -} - -#[derive(Debug, Trace, Clone, JsLifetime)] -pub(crate) enum WritableStreamState<'js> { - Writable, - Closed, - Erroring(Value<'js>), - Errored(Value<'js>), -} - -#[derive(JsLifetime, Trace)] -struct PendingAbortRequest<'js> { - promise: ResolveablePromise<'js>, - reason: Value<'js>, - was_already_erroring: bool, -} diff --git a/stdlib/src/llrt/llrt_stream_web/writable/stream/sink.rs b/stdlib/src/llrt/llrt_stream_web/writable/stream/sink.rs deleted file mode 100644 index a690cecc..00000000 --- a/stdlib/src/llrt/llrt_stream_web/writable/stream/sink.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use rquickjs::{Function, Object, Result, Value}; - -use crate::llrt_stream_web::utils::ValueOrUndefined; - -#[derive(Default)] -pub struct UnderlyingSink<'js> { - // callback UnderlyingSinkStartCallback = any (WritableStreamDefaultController controller); - pub start: Option>, - // callback UnderlyingSinkWriteCallback = Promise (any chunk, WritableStreamDefaultController controller); - pub write: Option>, - // callback UnderlyingSinkCloseCallback = Promise (); - pub close: Option>, - // callback UnderlyingSinkAbortCallback = Promise (optional any reason); - pub abort: Option>, - pub r#type: Option>, -} - -impl<'js> UnderlyingSink<'js> { - pub fn from_object(obj: Object<'js>) -> Result { - let start = obj.get_value_or_undefined::<_, _>("start")?; - let write = obj.get_value_or_undefined::<_, _>("write")?; - let close = obj.get_value_or_undefined::<_, _>("close")?; - let abort = obj.get_value_or_undefined::<_, _>("abort")?; - let r#type = obj.get_value_or_undefined::<_, _>("type")?; - - Ok(Self { - start, - write, - close, - abort, - r#type, - }) - } -} diff --git a/stdlib/src/llrt/llrt_stream_web/writable/writer.rs b/stdlib/src/llrt/llrt_stream_web/writable/writer.rs deleted file mode 100644 index b0d6ac87..00000000 --- a/stdlib/src/llrt/llrt_stream_web/writable/writer.rs +++ /dev/null @@ -1,79 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use rquickjs::{class::Trace, Result}; - -use crate::llrt_stream_web::writable::default_writer::WritableStreamDefaultWriterOwned; - -pub(crate) trait WritableStreamWriter<'js>: Sized + 'js { - type Class: Clone + Trace<'js>; - - fn with_writer( - self, - ctx: C, - default: impl FnOnce( - C, - WritableStreamDefaultWriterOwned<'js>, - ) -> Result<(C, WritableStreamDefaultWriterOwned<'js>)>, - none: impl FnOnce(C) -> Result, - ) -> Result<(C, Self)>; - - fn into_inner(self) -> Self::Class; - - fn from_class(class: Self::Class) -> Self; -} - -#[derive(Clone, Trace)] -pub(super) struct UndefinedWriter; - -impl<'js> WritableStreamWriter<'js> for UndefinedWriter { - type Class = UndefinedWriter; - - fn with_writer( - self, - ctx: C, - _: impl FnOnce( - C, - WritableStreamDefaultWriterOwned<'js>, - ) -> Result<(C, WritableStreamDefaultWriterOwned<'js>)>, - none: impl FnOnce(C) -> Result, - ) -> Result<(C, Self)> { - Ok((none(ctx)?, self)) - } - - fn into_inner(self) -> Self::Class { - self - } - - fn from_class(class: Self::Class) -> Self { - class - } -} - -impl<'js, T: WritableStreamWriter<'js>> WritableStreamWriter<'js> for Option { - type Class = Option<>::Class>; - - fn with_writer( - self, - mut ctx: C, - default: impl FnOnce( - C, - WritableStreamDefaultWriterOwned<'js>, - ) -> Result<(C, WritableStreamDefaultWriterOwned<'js>)>, - none: impl FnOnce(C) -> Result, - ) -> Result<(C, Self)> { - match self { - Some(mut writer) => { - (ctx, writer) = writer.with_writer(ctx, default, none)?; - Ok((ctx, Some(writer))) - } - None => Ok((none(ctx)?, None)), - } - } - - fn into_inner(self) -> Self::Class { - self.map(WritableStreamWriter::into_inner) - } - - fn from_class(class: Self::Class) -> Self { - class.map(WritableStreamWriter::from_class) - } -} diff --git a/stdlib/src/llrt/llrt_test/lib.rs b/stdlib/src/llrt/llrt_test/lib.rs deleted file mode 100644 index 5c201c79..00000000 --- a/stdlib/src/llrt/llrt_test/lib.rs +++ /dev/null @@ -1,149 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use std::{ - fs, - path::{Path, PathBuf}, -}; - -use rquickjs::{ - function::IntoArgs, - loader::{BuiltinLoader, ImportAttributes, Resolver}, - markers::ParallelSend, - module::{Evaluated, ModuleDef}, - promise::MaybePromise, - AsyncContext, AsyncRuntime, CatchResultExt, CaughtError, Ctx, FromJs, Function, Module, Result, -}; - -pub async fn given_file(content: &str) -> PathBuf { - let tmp_dir = std::env::temp_dir(); - let path = tmp_dir.join(uuid::Uuid::new_v4().to_string()); - tokio::fs::write(&path, content).await.unwrap(); - path -} - -struct TestResolver; - -impl Resolver for TestResolver { - fn resolve( - &mut self, - _ctx: &Ctx<'_>, - base: &str, - name: &str, - _attributes: Option>, - ) -> Result { - if !name.starts_with(".") { - return Ok(name.into()); - } - let base = Path::new(base); - let combined_path = base.join(name); - Ok(fs::canonicalize(combined_path) - .unwrap() - .to_string_lossy() - .to_string()) - } -} - -pub async fn given_runtime() -> (AsyncRuntime, AsyncContext) { - let rt = AsyncRuntime::new().unwrap(); - rt.set_loader((TestResolver,), (BuiltinLoader::default(),)) - .await; - let ctx = AsyncContext::full(&rt).await.unwrap(); - - (rt, ctx) -} - -pub async fn test_async_with(func: F) -where - F: for<'js> FnOnce(Ctx<'js>) -> std::pin::Pin + 'js>> - + Send, -{ - test_async_with_opts(func, TestOptions::default()).await; -} - -#[derive(Default)] -pub struct TestOptions { - no_pending_jobs: bool, -} - -impl TestOptions { - pub fn new() -> Self { - Self::default() - } - - pub fn no_pending_jobs(mut self) -> Self { - self.no_pending_jobs = true; - self - } -} - -pub async fn test_async_with_opts(func: F, options: TestOptions) -where - F: for<'js> FnOnce(Ctx<'js>) -> std::pin::Pin + 'js>> - + Send, -{ - let (rt, ctx) = given_runtime().await; - - ctx.async_with(async |ctx| func(ctx).await).await; - - if options.no_pending_jobs { - assert!(!rt.is_job_pending().await); - } -} - -pub async fn test_sync_with(func: F) -where - F: for<'js> FnOnce(Ctx<'js>) -> Result<()> + ParallelSend, -{ - let (_rt, ctx) = given_runtime().await; - - ctx.with(|ctx| func(ctx.clone()).catch(&ctx).unwrap()).await; -} - -pub async fn call_test<'js, T, A>(ctx: &Ctx<'js>, module: &Module<'js, Evaluated>, args: A) -> T -where - T: FromJs<'js>, - A: IntoArgs<'js>, -{ - call_test_err(ctx, module, args).await.unwrap() -} - -pub async fn call_test_err<'js, T, A>( - ctx: &Ctx<'js>, - module: &Module<'js, Evaluated>, - args: A, -) -> std::result::Result> -where - T: FromJs<'js>, - A: IntoArgs<'js>, -{ - module - .get::<_, Function>("test") - .catch(ctx)? - .call::<_, MaybePromise>(args) - .catch(ctx)? - .into_future::() - .await - .catch(ctx) -} - -pub struct ModuleEvaluator; - -impl ModuleEvaluator { - pub async fn eval_js<'js>( - ctx: Ctx<'js>, - name: &str, - source: &str, - ) -> Result> { - let (module, module_eval) = Module::declare(ctx, name, source)?.eval()?; - module_eval.into_future::<()>().await?; - Ok(module) - } - - pub async fn eval_rust<'js, M>(ctx: Ctx<'js>, name: &str) -> Result> - where - M: ModuleDef, - { - let (module, module_eval) = Module::evaluate_def::(ctx, name)?; - module_eval.into_future::<()>().await?; - Ok(module) - } -} diff --git a/stdlib/src/llrt/llrt_timers/lib.rs b/stdlib/src/llrt/llrt_timers/lib.rs deleted file mode 100644 index b2d23560..00000000 --- a/stdlib/src/llrt/llrt_timers/lib.rs +++ /dev/null @@ -1,557 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::{ - pin::{pin, Pin}, - ptr::NonNull, - rc::Rc, - sync::{ - atomic::{AtomicUsize, Ordering}, - Mutex, MutexGuard, - }, - time::Duration, -}; - -use crate::llrt_context::CtxExtension; -pub use crate::llrt_hooking::{invoke_async_hook, register_finalization_registry, HookType}; -use crate::llrt_utils::{ - module::{export_default, ModuleInfo}, - provider::ProviderType, -}; -use once_cell::sync::Lazy; -use rquickjs::{ - module::{Declarations, Exports, ModuleDef}, - prelude::{Func, Opt}, - qjs, Ctx, Exception, Function, Persistent, Result, Value, -}; -use tokio::{ - select, - sync::Notify, - time::{Instant, Sleep}, -}; - -static TIMER_ID: AtomicUsize = AtomicUsize::new(0); -static RT_TIMER_STATE: Lazy>> = Lazy::new(|| Mutex::new(Vec::new())); - -pub struct RuntimeTimerState { - timers: Vec, - rt: *mut qjs::JSRuntime, - running: bool, - deadline: Instant, - notify: Rc, -} -impl RuntimeTimerState { - fn new(rt: *mut qjs::JSRuntime) -> Self { - let deadline = Instant::now() + Duration::from_secs(86400 * 365 * 30); - Self { - timers: Default::default(), - rt, - deadline, - running: false, - notify: Default::default(), - } - } -} - -unsafe impl Send for RuntimeTimerState {} - -#[derive(Clone)] -pub struct Timeout { - callback: Option>>, - deadline: Instant, - raw_ctx: NonNull, - id: usize, - repeating: bool, - interval: u64, -} - -impl Default for Timeout { - fn default() -> Self { - Self { - callback: None, - deadline: Instant::now(), - raw_ctx: NonNull::dangling(), - id: 0, - repeating: false, - interval: 0, - } - } -} - -fn queue_microtask<'js>(_ctx: Ctx<'js>, cb: Function<'js>) -> Result<()> { - // SAFETY: Since it checks in advance whether it is an Function type, we can always get a pointer to the Function. - let uid = unsafe { qjs::JS_VALUE_GET_PTR(cb.as_raw()) } as usize; - register_finalization_registry(&_ctx, cb.clone().into_value(), uid)?; - invoke_async_hook(&_ctx, HookType::Init, ProviderType::Microtask, uid)?; - // NOTE: Defer simply registers a task in a microtask queue - // and is separate from the timing of when the actual callback runs. - // Therefore, asynchronous before/after hooks are not meaningful and will not be implemented. - - cb.defer::<()>(())?; - Ok(()) -} - -pub fn set_timeout_interval<'js>( - ctx: &Ctx<'js>, - cb: Function<'js>, - delay: u64, - provider_type: ProviderType, -) -> Result { - // SAFETY: Since it checks in advance whether it is an Function type, we can always get a pointer to the Function. - let uid = unsafe { qjs::JS_VALUE_GET_PTR(cb.as_raw()) } as usize; - - // NOTE: https://noncodersuccess.medium.com/understanding-setimmediate-vs-settimeout-in-node-js-6a3ef8fc02d4 - // If `setImmediate(fn)` and `setTimeout(fn, 0) are queued at the exact same time, - // `setImmediate(fn) takes precedence in Node.js, regardless of their execution order. - // This is due to the specifications of the Node.js event loop. - // The event loop specifications of LLRT are completely different from those of Node.js, - // but to make them the same, `setImmedaite()` is executed before any delay setting of `setTimeout()`. - let (repeating, deadline) = match provider_type { - ProviderType::Immediate => (false, Instant::now() - Duration::from_secs(600)), // before any setTimeout(fn, delay) - ProviderType::Timeout => (false, Instant::now() + Duration::from_millis(delay)), - ProviderType::Interval => (true, Instant::now() + Duration::from_millis(delay)), - _ => { - return Err(Exception::throw_type( - ctx, - "The specified provider type is not supported.", - )) - } - }; - - register_finalization_registry(ctx, cb.clone().into_value(), uid)?; - invoke_async_hook(ctx, HookType::Init, provider_type, uid)?; - - let id = TIMER_ID.fetch_add(1, Ordering::Relaxed); - - let callback = Persistent::::save(ctx, cb); - - let timeout = Timeout { - deadline, - callback: Some(callback), - raw_ctx: ctx.as_raw(), - id, - repeating, - interval: delay, - }; - - let rt_ptr = unsafe { qjs::JS_GetRuntime(ctx.as_raw().as_ptr()) }; - - let mut rt_timer = RT_TIMER_STATE.lock().unwrap(); - let state = get_timer_state(&mut rt_timer, rt_ptr); - state.timers.push(timeout); - let task_running = state.running; - if task_running { - if deadline < state.deadline { - state.deadline = deadline; - state.notify.notify_one(); - } - } else { - state.running = true; - let timer_abort = state.notify.clone(); - drop(rt_timer); - create_spawn_loop(rt_ptr, ctx, timer_abort, deadline)?; - } - - Ok(id) -} - -fn get_timer_state<'a>( - state_ref: &'a mut MutexGuard>, - rt: *mut qjs::JSRuntime, -) -> &'a mut RuntimeTimerState { - let rt_timers = state_ref.iter_mut().find(|state| state.rt == rt); - - //save a branch - unsafe { rt_timers.unwrap_unchecked() } -} - -fn clear_timeout_interval(ctx: Ctx<'_>, id: Opt) -> Result<()> { - if let Some(id) = id.0.and_then(|v| v.as_number()) { - let id = id as usize; - let rt = unsafe { qjs::JS_GetRuntime(ctx.as_raw().as_ptr()) }; - let mut rt_timers = RT_TIMER_STATE.lock().unwrap(); - - let state = get_timer_state(&mut rt_timers, rt); - if let Some(timeout) = state.timers.iter_mut().find(|t| t.id == id) { - let _ = timeout.callback.take(); - timeout.repeating = false; - timeout.deadline = Instant::now() - Duration::from_secs(1); - state.notify.notify_one() - } - } - - Ok(()) -} - -pub struct TimersModule; - -impl ModuleDef for TimersModule { - fn declare(declare: &Declarations) -> Result<()> { - declare.declare("setTimeout")?; - declare.declare("clearTimeout")?; - declare.declare("setInterval")?; - declare.declare("setImmediate")?; - declare.declare("clearInterval")?; - declare.declare("queueMicrotask")?; - declare.declare("default")?; - Ok(()) - } - - fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { - let globals = ctx.globals(); - - export_default(ctx, exports, |default| { - let functions = [ - "setTimeout", - "clearTimeout", - "setInterval", - "clearInterval", - "setImmediate", - "queueMicrotask", - ]; - for func_name in functions { - let function: Function = globals.get(func_name)?; - default.set(func_name, function)?; - } - Ok(()) - })?; - - Ok(()) - } -} - -impl From for ModuleInfo { - fn from(val: TimersModule) -> Self { - ModuleInfo { - name: "timers", - module: val, - } - } -} - -pub fn init(ctx: &Ctx<'_>) -> Result<()> { - let rt_ptr = unsafe { qjs::JS_GetRuntime(ctx.as_raw().as_ptr()) }; - - let mut rt_timers = RT_TIMER_STATE.lock().unwrap(); - rt_timers.push(RuntimeTimerState::new(rt_ptr)); - - let globals = ctx.globals(); - - globals.set( - "setTimeout", - Func::from(move |ctx, cb, delay: Opt| { - let delay = delay.unwrap_or(0.).max(0.) as u64; - set_timeout_interval(&ctx, cb, delay, ProviderType::Timeout) - }), - )?; - - globals.set( - "setInterval", - Func::from(move |ctx, cb, delay: Opt| { - let delay = delay.unwrap_or(0.).max(0.) as u64; - set_timeout_interval(&ctx, cb, delay, ProviderType::Interval) - }), - )?; - - globals.set("clearTimeout", Func::from(clear_timeout_interval))?; - - globals.set("clearInterval", Func::from(clear_timeout_interval))?; - - globals.set( - "setImmediate", - Func::from(move |ctx, cb| set_timeout_interval(&ctx, cb, 0, ProviderType::Immediate)), - )?; - - globals.set("queueMicrotask", Func::from(queue_microtask))?; - - Ok(()) -} - -#[inline(always)] -fn create_spawn_loop( - rt: *mut qjs::JSRuntime, - ctx: &Ctx<'_>, - timer_abort: Rc, - deadline: Instant, -) -> Result<()> { - ctx.spawn_exit_simple(async move { - let mut sleep = pin!(tokio::time::sleep_until(deadline)); - - let mut executing_timers: Vec> = Default::default(); - - loop { - select! { - _ = timer_abort.notified() => {} - _ = sleep.as_mut() => {} - } - - if !poll_timers(rt, &mut executing_timers, Some(&mut sleep), None)? { - break; - } - } - Ok(()) - }); - - Ok(()) -} - -pub struct ExecutingTimer( - Instant, - NonNull, - Persistent>, -); - -unsafe impl Send for ExecutingTimer {} - -pub fn poll_timers( - rt: *mut qjs::JSRuntime, - call_vec: &mut Vec>, - sleep: Option<&mut Pin<&mut Sleep>>, - deadline: Option<&mut Instant>, -) -> Result { - static MIN_SLEEP: Duration = Duration::from_millis(4); - static FAR_FUTURE: Duration = Duration::from_secs(84200 * 365 * 30); - - let mut rt_timers = RT_TIMER_STATE.lock().unwrap(); - let state = get_timer_state(&mut rt_timers, rt); - let now = Instant::now(); - - let mut had_items = false; - let mut lowest = now + FAR_FUTURE; - state.timers.retain_mut(|timeout| { - had_items = true; - if timeout.deadline < now { - let ctx = timeout.raw_ctx; - if let Some(cb) = timeout.callback.take() { - if !timeout.repeating { - call_vec.push(Some(ExecutingTimer(timeout.deadline, ctx, cb))); - return false; - } - timeout.deadline = now + Duration::from_millis(timeout.interval); - if timeout.deadline < lowest { - lowest = timeout.deadline; - } - call_vec.push(Some(ExecutingTimer(timeout.deadline, ctx, cb.clone()))); - timeout.callback.replace(cb); - } else { - return false; - } - } else if timeout.deadline < lowest { - lowest = timeout.deadline; - } - true - }); - - let has_items = !state.timers.is_empty(); - - if had_items { - if lowest - now < MIN_SLEEP { - lowest = now + MIN_SLEEP; - } - if let Some(sleep) = sleep { - sleep.as_mut().reset(lowest); - } - if let Some(deadline) = deadline { - *deadline = lowest; - } - state.deadline = lowest; - } - - drop(rt_timers); - - call_vec.sort_unstable_by_key(|v| v.as_ref().map(|v| v.0)); - - let mut is_first_time = true; - for item in call_vec.iter_mut() { - if let Some(ExecutingTimer(_, ctx, timeout)) = item.take() { - let ctx2 = unsafe { Ctx::from_raw(ctx) }; - - if is_first_time { - while ctx2.execute_pending_job() {} - is_first_time = false; - } - - if let Ok(timeout) = timeout.restore(&ctx2) { - // SAFETY: Since it checks in advance whether it is an Function type, we can always get a pointer to the Function. - let uid: usize = unsafe { qjs::JS_VALUE_GET_PTR(timeout.as_raw()) } as usize; - - invoke_async_hook(&ctx2, HookType::Before, ProviderType::None, uid)?; - - timeout.call::<_, ()>(())?; - - invoke_async_hook(&ctx2, HookType::After, ProviderType::None, uid)?; - } - - while ctx2.execute_pending_job() {} - } - } - call_vec.clear(); - - if !has_items { - let mut rt_timers = RT_TIMER_STATE.lock().unwrap(); - let state = get_timer_state(&mut rt_timers, rt); - let is_empty = state.timers.is_empty(); - state.running = !is_empty; - - return Ok(!is_empty); - } - Ok(true) -} - -#[cfg(test)] -mod tests { - use crate::llrt_test::{call_test, test_async_with, ModuleEvaluator}; - - use super::*; - - #[tokio::test] - async fn test_timers() { - test_async_with(|ctx| { - Box::pin(async move { - init(&ctx).unwrap(); - - // Assume we have a TimersModule that provides setTimeout, setImmediate, and setInterval - ModuleEvaluator::eval_rust::(ctx.clone(), "timers") - .await - .unwrap(); - - // Test setTimeout - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test_setTimeout", - r#" - import { setTimeout } from 'timers'; - export async function test() { - return new Promise((resolve) => { - setTimeout(() => resolve('timeout'), 100); - }); - } - "#, - ) - .await - .unwrap(); - let result = call_test::(&ctx, &module, ()).await; - assert_eq!(result, "timeout"); - - // Test setImmediate - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test_setImmediate", - r#" - import { setImmediate } from 'timers'; - export async function test() { - return new Promise((resolve) => { - setImmediate(() => resolve('immediate')); - }); - } - "#, - ) - .await - .unwrap(); - let result = call_test::(&ctx, &module, ()).await; - assert_eq!(result, "immediate"); - - // Test setInterval - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test_setInterval", - r#" - import { setInterval, clearInterval } from 'timers'; - export async function test() { - return new Promise((resolve) => { - let count = 0; - const intervalId = setInterval(() => { - count++; - if (count === 3) { - clearInterval(intervalId); - resolve(count); - } - }, 10); - }); - } - "#, - ) - .await - .unwrap(); - let result = call_test::(&ctx, &module, ()).await; - assert_eq!(result, 3); - - // Test nested timers - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test_nestedTimers", - r#" - import { setTimeout, setImmediate } from 'timers'; - export async function test() { - return new Promise((resolve) => { - setTimeout(() => { - setImmediate(() => { - setTimeout(() => { - resolve('nested'); - }, 10); - }); - }, 10); - }); - } - "#, - ) - .await - .unwrap(); - let result = call_test::(&ctx, &module, ()).await; - assert_eq!(result, "nested"); - - // Test canceling timeout - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test_cancelTimeout", - r#" - import { setTimeout, clearTimeout } from 'timers'; - export async function test() { - return new Promise((resolve) => { - const timeoutId = setTimeout(() => { - resolve('should not happen'); - }, 10); - clearTimeout(timeoutId); - setTimeout(() => resolve('canceled'), 20); - }); - } - "#, - ) - .await - .unwrap(); - let result = call_test::(&ctx, &module, ()).await; - assert_eq!(result, "canceled"); - - // Test multiple intervals - let module = ModuleEvaluator::eval_js( - ctx.clone(), - "test_multipleIntervals", - r#" - import { setInterval, clearInterval } from 'timers'; - export async function test() { - return new Promise((resolve) => { - let count1 = 0, count2 = 0; - const id1 = setInterval(() => { - count1++; - if (count1 === 2) clearInterval(id1); - }, 10); - const id2 = setInterval(() => { - count2++; - if (count2 === 3) { - clearInterval(id2); - resolve([count1, count2]); - } - }, 20); - }); - } - "#, - ) - .await - .unwrap(); - let result = call_test::, _>(&ctx, &module, ()).await; - assert_eq!(result, vec![2, 3]); - }) - }) - .await; - } -} diff --git a/stdlib/src/llrt/llrt_url/lib.rs b/stdlib/src/llrt/llrt_url/lib.rs deleted file mode 100644 index 36f4a094..00000000 --- a/stdlib/src/llrt/llrt_url/lib.rs +++ /dev/null @@ -1,356 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -#![allow(clippy::inherent_to_string)] -pub mod url_class; -pub mod url_search_params; - -use std::{path::PathBuf, str::FromStr}; - -use crate::llrt_utils::{ - module::{export_default, ModuleInfo}, - primordials::{BasePrimordials, Primordial}, - result::ResultExt, -}; -use rquickjs::{ - function::{Constructor, Func}, - module::{Declarations, Exports, ModuleDef}, - prelude::Opt, - Class, Coerced, Ctx, Exception, Result, Value, -}; -use url::{quirks, Url}; - -use self::url_class::{url_to_http_options, URL}; -use self::url_search_params::URLSearchParams; - -/// Returns whether the given scheme is a [special scheme](https://url.spec.whatwg.org/#special-scheme). -pub fn is_special_scheme(scheme: &str) -> bool { - matches!(scheme, "http" | "https" | "ftp" | "ws" | "wss" | "file") -} - -pub fn domain_to_unicode(domain: &str) -> String { - quirks::domain_to_unicode(domain) -} - -pub fn domain_to_ascii(domain: &str) -> String { - quirks::domain_to_ascii(domain) -} - -//options are ignored, no windows support yet -pub fn path_to_file_url<'js>(ctx: Ctx<'js>, path: String, _: Opt) -> Result> { - let url = Url::from_file_path(&path) - .map_err(|_| Exception::throw_type(&ctx, &["Path is not absolute: ", &path].concat()))?; - - URL::from_url(ctx, url) -} - -//options are ignored, no windows support yet -pub fn file_url_to_path<'js>(ctx: Ctx<'js>, url: Value<'js>) -> Result { - let url_string = if let Ok(url) = Class::::from_value(&url) { - url.borrow().to_string() - } else { - url.get::>()?.to_string() - }; - - let path = url_string.trim_start_matches("file://"); - - Ok(PathBuf::from_str(path) - .or_throw(&ctx)? - .to_string_lossy() - .to_string()) -} - -pub fn url_format<'js>(url: Class<'js, URL<'js>>, options: Opt>) -> Result { - let url = url.borrow(); - let mut string = url.protocol(); - string.push_str("//"); - - let mut include_fragment = true; - let mut unicode_encode = false; - let mut include_auth = true; - let mut include_search = true; - - // Parse options if provided - if let Some(options) = options.into_inner() { - if let Some(options) = options.as_object() { - if let Ok(value) = options.get("unicode") { - unicode_encode = value; - } - if let Ok(value) = options.get("auth") { - include_auth = value; - } - if let Ok(value) = options.get("fragment") { - include_fragment = value; - } - if let Ok(value) = options.get("search") { - include_search = value - } - } - } - - if include_auth { - let username = url.username(); - let password = url.password(); - if !username.is_empty() { - string.push_str(&username); - if !password.is_empty() { - string.push(':'); - string.push_str(&password); - } - string.push('@'); - } - } - - if unicode_encode { - string.push_str(&domain_to_unicode(&url.host())); - } else { - string.push_str(&url.host()); - } - - string.push_str(&url.pathname()); - - if include_search { - string.push_str(&url.search()); - } - - if include_fragment { - string.push_str(&url.hash()); - } - - Ok(string) -} - -/// Encode trailing space as `%20` in opaque paths before a setter runs. -/// -/// Used by [`URLSearchParams`] which mutates the shared [`Url`] directly. -pub fn convert_trailing_space(url: &mut Url) { - if is_special_scheme(url.scheme()) { - return; - } - - let path = url.path(); - let has_remaining = url.fragment().is_some() || url.query().is_some(); - - #[allow(clippy::manual_strip)] - if path.ends_with(' ') && has_remaining { - let new_path = [&path[..path.len() - 1], "%20"].concat(); - url.set_path(&new_path); - } -} - -/// Per WHATWG URL spec §4.5.3 ("URL serializer"), the `/.` path sentinel is -/// only inserted when a URL has no host AND its path starts with `//`. The -/// `url` crate inserts the sentinel during parsing and can leave it in the -/// serialization even after a host is set, breaking WPT `url-setters` -/// subtests like `.hostname = 'h'`. -/// -/// This strips the sentinel whenever the URL has a non-empty host and the -/// path begins with `/./`. -/// Per WHATWG URL spec §4.2, a file URL path segment matching `[A-Za-z]|` -/// followed by `/`, `\`, `?`, `#`, or end-of-path is a Windows drive letter. -/// Parsers normalize the `|` to `:`. The `url` crate doesn't perform this -/// rewrite itself, so we do it after parsing (WPT `url-constructor.any.js` -/// "Parsing: "). -/// When a `file://HOST/C:/...` string is parsed, the `url` crate drops -/// HOST (normalizing to `file:///C:/...`). Per WHATWG URL spec the host -/// must be preserved when non-empty (drive-letter state only applies when -/// host is null). Extract the host from the original source string and -/// re-set it on the parsed URL so downstream `join()` sees the host. -pub fn preserve_file_url_host(source: &str, mut url: Url) -> Url { - if url.scheme() != "file" { - return url; - } - if url.host_str().is_some_and(|h| !h.is_empty()) { - return url; - } - // Look for `file://HOST/...` in the original string. - let Some(rest) = source.strip_prefix("file://") else { - return url; - }; - let Some((host, _)) = rest.split_once('/') else { - return url; - }; - if host.is_empty() { - return url; - } - let _ = url.set_host(Some(host)); - url -} - -/// When resolving a relative URL against a file:// base whose first path -/// segment is a Windows drive letter (e.g. `file://h/C:/a/b`), the url crate -/// loses the host during `join`. Per WHATWG URL spec the host must be -/// preserved (WPT `url-constructor.any.js` "" base). -/// Patch the joined URL by restoring the base's host. -pub fn restore_file_url_host(base: &Url, joined: &mut Url) { - if base.scheme() != "file" || joined.scheme() != "file" { - return; - } - // Only when base had a host and joined has none / empty. - let Some(base_host) = base.host_str() else { - return; - }; - if base_host.is_empty() { - return; - } - if joined.host_str().is_some_and(|h| !h.is_empty()) { - return; - } - // Only when base's first path segment is a Windows drive letter — that's - // the code path that the url crate mishandles. - let base_path = base.path(); - let is_drive_letter_first_seg = base_path - .as_bytes() - .get(1) - .is_some_and(|b| b.is_ascii_alphabetic()) - && base_path.as_bytes().get(2) == Some(&b':') - && matches!(base_path.as_bytes().get(3), Some(&b'/') | None); - if !is_drive_letter_first_seg { - return; - } - let _ = joined.set_host(Some(base_host)); -} - -pub fn normalize_windows_drive_letter(url: &mut Url) { - if url.scheme() != "file" { - return; - } - let path = url.path(); - let bytes = path.as_bytes(); - // Expect path like "/|/..." — 4+ bytes, leading slash, letter, - // pipe, trailing slash. - if bytes.len() < 4 - || bytes[0] != b'/' - || !bytes[1].is_ascii_alphabetic() - || bytes[2] != b'|' - || bytes[3] != b'/' - { - return; - } - let new_path = ["/", &path[1..2], ":", &path[3..]].concat(); - url.set_path(&new_path); -} - -/// Per WHATWG URL spec, a non-special URL with an empty host can have its -/// path erased (WPT `url-setters.any.js`). The `url` crate keeps a trailing -/// `/` after the authority; reparse the serialization with it stripped when -/// the caller has explicitly set an empty pathname on such a URL. -pub fn erase_empty_host_path(url: &mut Url) { - if is_special_scheme(url.scheme()) { - return; - } - if url.path() != "/" { - return; - } - let serialized = url.as_str(); - // Serialized form must be `://` + `/` to qualify. (`scheme:/`, - // without authority, isn't eligible — the extra `/` is not a sentinel - // but a real path character.) - let Some(scheme_end) = serialized.find("://") else { - return; - }; - let authority_and_path = &serialized[scheme_end + 3..]; - // After "://": optional userinfo + host + port, then the path. If the - // path is just "/" and everything before is empty, the full - // authority_and_path is "/". - if authority_and_path != "/" { - return; - } - // Strip the trailing `/`. - let stripped = &serialized[..serialized.len() - 1]; - if let Ok(reparsed) = Url::parse(stripped) { - *url = reparsed; - } -} - -pub fn strip_path_sentinel(url: &mut Url) { - if is_special_scheme(url.scheme()) { - return; - } - // Path starting with `//` is what triggers the `/.` sentinel in the url - // crate's serialization — but the sentinel is only spec-correct when - // there's no authority. If the URL has `://` and its serialization still - // contains `/./` at the path boundary, reparse with it stripped. - if !url.path().starts_with("//") { - return; - } - let serialized = url.as_str(); - // Authority is present iff serialization contains "://". - let Some(auth_start) = serialized.find("://") else { - return; - }; - let after_auth = auth_start + 3; - // Look for next `/` that starts the path region. - let Some(path_start_rel) = serialized[after_auth..].find('/') else { - return; - }; - let path_idx = after_auth + path_start_rel; - if serialized[path_idx..].starts_with("/./") { - let stripped = [&serialized[..path_idx], &serialized[path_idx + 2..]].concat(); - if let Ok(reparsed) = Url::parse(&stripped) { - *url = reparsed; - } - } -} - -pub fn init(ctx: &Ctx<'_>) -> Result<()> { - let globals = ctx.globals(); - - Class::::define(&globals)?; - Class::::define(&globals)?; - - Ok(()) -} - -pub struct UrlModule; - -impl ModuleDef for UrlModule { - fn declare(declare: &Declarations) -> Result<()> { - declare.declare(stringify!(URL))?; - declare.declare(stringify!(URLSearchParams))?; - declare.declare("urlToHttpOptions")?; - declare.declare("domainToUnicode")?; - declare.declare("domainToASCII")?; - declare.declare("fileURLToPath")?; - declare.declare("pathToFileURL")?; - declare.declare("format")?; - declare.declare("default")?; - Ok(()) - } - - fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { - let globals = ctx.globals(); - BasePrimordials::init(ctx)?; - let url: Constructor = globals.get(stringify!(URL))?; - let url_search_params: Constructor = globals.get(stringify!(URLSearchParams))?; - - export_default(ctx, exports, |default| { - default.set(stringify!(URL), url)?; - default.set(stringify!(URLSearchParams), url_search_params)?; - default.set("urlToHttpOptions", Func::from(url_to_http_options))?; - default.set( - "domainToUnicode", - Func::from(|domain: String| domain_to_unicode(&domain)), - )?; - default.set( - "domainToASCII", - Func::from(|domain: String| domain_to_ascii(&domain)), - )?; - default.set("fileURLToPath", Func::from(file_url_to_path))?; - default.set("pathToFileURL", Func::from(path_to_file_url))?; - default.set("format", Func::from(url_format))?; - Ok(()) - })?; - - Ok(()) - } -} - -impl From for ModuleInfo { - fn from(val: UrlModule) -> Self { - ModuleInfo { - name: "url", - module: val, - } - } -} diff --git a/stdlib/src/llrt/llrt_url/url_class.rs b/stdlib/src/llrt/llrt_url/url_class.rs deleted file mode 100644 index d7bb6229..00000000 --- a/stdlib/src/llrt/llrt_url/url_class.rs +++ /dev/null @@ -1,347 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -#![allow(clippy::uninlined_format_args)] - -use std::{cell::RefCell, rc::Rc}; - -use rquickjs::{ - atom::PredefinedAtom, class::Trace, function::Opt, Class, Coerced, Ctx, Exception, FromJs, - IntoJs, Null, Object, Result, Value, -}; -use url::{quirks, Url}; - -use super::url_search_params::URLSearchParams; - -/// Represents a JavaScript -/// [`URL`](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) as defined -/// by the [WHATWG URL standard](https://url.spec.whatwg.org/). -#[derive(Clone, Trace, rquickjs::JsLifetime)] -#[rquickjs::class] -pub struct URL<'js> { - #[qjs(skip_trace)] - url: Rc>, - search_params: Class<'js, URLSearchParams>, -} - -#[rquickjs::methods(rename_all = "camelCase")] -impl<'js> URL<'js> { - #[qjs(constructor)] - pub fn new(ctx: Ctx<'js>, input: Value<'js>, base: Opt>) -> Result { - // USVString conversion per WHATWG URL spec: lone UTF-16 surrogates - // must be replaced with U+FFFD (not rejected) before the basic URL - // parser runs (WPT `url-origin.any.js` passes URLs containing lone - // surrogates and expects them to parse). - let input: Result = if input.is_string() { - crate::llrt_utils::bytes::get_lossy_string(input.clone()) - } else { - Coerced::::from_js(&ctx, input.clone()).map(|c| c.0) - }; - if let Some(base) = base.into_inner() { - if let Some(base) = base.as_string() { - if let Ok(base) = base.to_string() { - let base_url: Url = base - .parse() - .map_err(|_| Exception::throw_type(&ctx, "Invalid base URL"))?; - // Work around a url-crate normalization that loses the - // host when a file:// URL's path starts with a Windows - // drive letter (WPT url-constructor.any.js file-URL- - // with-host base cases). Extract the host manually - // from the original source string and preserve it. - let base_url = super::preserve_file_url_host(&base, base_url); - if let Ok(input) = input { - let mut joined = base_url - .join(input.as_str()) - .map_err(|_| Exception::throw_type(&ctx, "Invalid URL"))?; - super::restore_file_url_host(&base_url, &mut joined); - return Self::from_url(ctx, joined); - } - return Self::from_str(ctx, &base); - } - } - } - if let Ok(input) = input { - Self::from_str(ctx, input.as_str()) - } else { - Err(Exception::throw_message(&ctx, "Invalid URL")) - } - } - - #[qjs(get)] - pub fn hash(&self) -> String { - quirks::hash(&self.url.borrow()).to_string() - } - - #[qjs(set, rename = "hash")] - pub fn set_hash(&mut self, hash: String) -> String { - self.before_mutation(); - quirks::set_hash(&mut self.url.borrow_mut(), &hash); - hash - } - - #[qjs(get)] - pub fn host(&self) -> String { - quirks::host(&self.url.borrow()).to_string() - } - - #[qjs(set, rename = "host")] - pub fn set_host(&mut self, host: Coerced) -> String { - self.before_mutation(); - let _ = quirks::set_host(&mut self.url.borrow_mut(), &host); - host.0 - } - - #[qjs(get)] - pub fn hostname(&self) -> String { - quirks::hostname(&self.url.borrow()).to_string() - } - - #[qjs(set, rename = "hostname")] - pub fn set_hostname(&mut self, hostname: Coerced) -> String { - self.before_mutation(); - let _ = quirks::set_hostname(&mut self.url.borrow_mut(), hostname.as_str()); - super::strip_path_sentinel(&mut self.url.borrow_mut()); - hostname.0 - } - - #[qjs(get)] - pub fn href(&self) -> String { - quirks::href(&self.url.borrow()).to_string() - } - - #[qjs(set, rename = "href")] - pub fn set_href(&mut self, href: String) -> String { - self.before_mutation(); - let _ = quirks::set_href(&mut self.url.borrow_mut(), &href); - href - } - - #[qjs(get)] - pub fn origin(&self) -> String { - let url = self.url.borrow(); - // Per WHATWG URL spec §6.2, origin of a blob URL is computed by parsing - // the path as a URL. If the result's scheme is HTTP(S), return that - // URL's origin; otherwise, return an opaque (null) origin. The `url` - // crate returns the nested URL's origin even for non-HTTP schemes, - // breaking WPT `url-origin.any.js` on cases like `blob:ftp://...` and - // `blob:blob:https://...`. - if url.scheme() == "blob" { - return match url::Url::parse(url.path()) { - Ok(inner) if matches!(inner.scheme(), "http" | "https") => quirks::origin(&inner), - _ => "null".into(), - }; - } - quirks::origin(&url) - } - - #[qjs(get)] - pub fn password(&self) -> String { - quirks::password(&self.url.borrow()).to_string() - } - - #[qjs(set, rename = "password")] - pub fn set_password(&mut self, password: Coerced) -> String { - self.before_mutation(); - let _ = quirks::set_password(&mut self.url.borrow_mut(), &password); - password.0 - } - - #[qjs(get)] - pub fn pathname(&self) -> String { - quirks::pathname(&self.url.borrow()).to_string() - } - - #[qjs(set, rename = "pathname")] - pub fn set_pathname(&mut self, pathname: Coerced) -> String { - self.before_mutation(); - quirks::set_pathname(&mut self.url.borrow_mut(), pathname.as_str()); - // Per WHATWG URL spec, a non-special URL with an empty host can have - // its path erased (WPT `url-setters.any.js` "Non-special URLs with - // an empty host can have their paths erased"). The `url` crate - // forces a single `/` after the authority; strip it when the caller - // set an empty pathname on such a URL. - if pathname.0.is_empty() { - super::erase_empty_host_path(&mut self.url.borrow_mut()); - } - pathname.0 - } - - #[qjs(get)] - pub fn port(&self) -> String { - quirks::port(&self.url.borrow()).to_string() - } - - #[qjs(set, rename = "port")] - pub fn set_port(&mut self, ctx: Ctx<'js>, port: Value<'js>) -> Value<'js> { - if port.is_null() - || port.is_undefined() - || (port.is_int() && unsafe { port.as_int().unwrap_unchecked() } < 0) - { - return port; - } - if let Ok(port_string) = Coerced::::from_js(&ctx, port.clone()) { - self.before_mutation(); - // Per WHATWG URL spec, the port-state parser strips tab/LF/CR - // before reading. An empty STRIPPED value (but non-empty original) - // makes port parsing fail, which per spec means no-op (keep - // existing port). An empty ORIGINAL value, however, clears the - // port. - if port_string.is_empty() { - let _ = quirks::set_port(&mut self.url.borrow_mut(), ""); - } else { - let stripped: String = port_string - .chars() - .filter(|c| !matches!(c, '\t' | '\n' | '\r')) - .collect(); - if !stripped.is_empty() { - let _ = quirks::set_port(&mut self.url.borrow_mut(), &stripped); - } - // stripped is empty → parse failure per spec → no-op - } - } - port - } - - #[qjs(get)] - pub fn protocol(&self) -> String { - quirks::protocol(&self.url.borrow()).to_string() - } - - #[qjs(set, rename = "protocol")] - pub fn set_protocol(&mut self, protocol: Coerced) -> String { - self.before_mutation(); - let _ = quirks::set_protocol(&mut self.url.borrow_mut(), &protocol); - protocol.0 - } - - #[qjs(get)] - pub fn search(&self) -> String { - quirks::search(&self.url.borrow()).to_string() - } - - #[qjs(set, rename = "search")] - pub fn set_search(&mut self, search: Coerced) -> String { - self.before_mutation(); - quirks::set_search(&mut self.url.borrow_mut(), &search); - search.0 - } - - #[qjs(get)] - pub fn search_params(&self) -> &Value<'js> { - self.search_params.as_value() - } - - #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] - pub fn to_string_tag() -> &'static str { - stringify!(URL) - } - - #[qjs(get)] - pub fn username(&self) -> String { - quirks::username(&self.url.borrow()).to_string() - } - - #[qjs(set, rename = "username")] - pub fn set_username(&mut self, username: Coerced) -> String { - self.before_mutation(); - let _ = quirks::set_username(&mut self.url.borrow_mut(), &username); - username.0 - } - - #[qjs(static)] - pub fn can_parse(ctx: Ctx<'js>, input: Value<'js>, base: Opt>) -> bool { - Self::new(ctx, input, base).is_ok() - } - - #[qjs(static)] - pub fn parse(ctx: Ctx<'js>, input: Value<'js>, base: Opt>) -> Result> { - Self::new(ctx.clone(), input, base) - .map_or_else(|_| Null.into_js(&ctx), |instance| instance.into_js(&ctx)) - } - - #[qjs(rename = PredefinedAtom::ToJSON)] - pub fn to_json(&self) -> String { - self.to_string() - } - - pub fn to_string(&self) -> String { - self.href() - } -} - -impl<'js> URL<'js> { - pub fn from_str(ctx: Ctx<'js>, input: &str) -> Result { - let mut url: Url = input - .parse() - .map_err(|_| Exception::throw_type(&ctx, "Invalid URL"))?; - super::normalize_windows_drive_letter(&mut url); - super::convert_trailing_space(&mut url); - Self::build(ctx, url) - } - - pub fn from_url(ctx: Ctx<'js>, mut url: Url) -> Result { - super::normalize_windows_drive_letter(&mut url); - super::convert_trailing_space(&mut url); - Self::build(ctx, url) - } - - /// Validate that a string parses as a URL without constructing a JS - /// instance. Used by callers (e.g. `llrt_fetch`) that just need to know - /// whether a user-supplied string is a valid URL. - pub fn is_valid(input: &str) -> bool { - input.parse::().is_ok() - } - - fn build(ctx: Ctx<'js>, url: Url) -> Result { - let shared = Rc::new(RefCell::new(url)); - let search_params = Class::instance(ctx, URLSearchParams::from_url(&shared))?; - Ok(Self { - url: shared, - search_params, - }) - } - - fn before_mutation(&mut self) { - super::convert_trailing_space(&mut self.url.borrow_mut()); - } - - pub(crate) fn inner_url(&self) -> std::cell::Ref<'_, Url> { - self.url.borrow() - } -} - -pub fn url_to_http_options<'js>(ctx: Ctx<'js>, url: Class<'js, URL<'js>>) -> Result> { - let obj = Object::new(ctx)?; - let url = url.borrow(); - - let port = url.port(); - let username = url.username(); - let search = url.search(); - let hash = url.inner_url().fragment().unwrap_or("").to_string(); - - obj.set("protocol", url.protocol())?; - obj.set("hostname", url.hostname())?; - - if !hash.is_empty() { - obj.set("hash", hash)?; - } - - let pathname = url.pathname(); - let path = [pathname.as_str(), search.as_str()].concat(); - if !search.is_empty() { - obj.set("search", search)?; - } - obj.set("pathname", pathname)?; - obj.set("path", path)?; - obj.set("href", url.href())?; - - if !username.is_empty() { - obj.set("auth", [username, url.password()].join(":"))?; - } - - if !port.is_empty() { - obj.set("port", port)?; - } - - Ok(obj) -} diff --git a/stdlib/src/llrt/llrt_url/url_search_params.rs b/stdlib/src/llrt/llrt_url/url_search_params.rs deleted file mode 100644 index 198b4a48..00000000 --- a/stdlib/src/llrt/llrt_url/url_search_params.rs +++ /dev/null @@ -1,1058 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::{ - cell::RefCell, - collections::{HashMap, HashSet}, - rc::Rc, -}; - -use crate::llrt_utils::{ - bytes::get_lossy_string, - class::{iterator_result, live_iterator, IterKind}, - primordials::{BasePrimordials, Primordial}, - string::get_coerced_defined_string, -}; -use rquickjs::{ - atom::PredefinedAtom, class::Trace, function::Opt, prelude::This, Array, Class, Coerced, Ctx, - Exception, FromJs, Function, IntoJs, Null, Object, Result, Symbol, Value, -}; -use url::Url; - -use super::convert_trailing_space; - -/// Represents `URLSearchParams` in the JavaScript context -/// -/// -/// -/// # Examples -/// -/// ```rust,ignore -/// // This is JavaScript -/// const params = new URLSearchParams(); -/// params.set("foo", "bar"); -/// ``` -#[derive(Clone, Trace, rquickjs::JsLifetime)] -#[rquickjs::class] -pub struct URLSearchParams { - // URL and URLSearchParams work together to manipulate URLs, so using a - // reference counter (Rc) allows them to have shared ownership of the - // undering Url, and a RefCell allows interior mutability. - #[qjs(skip_trace)] - pub url: Rc>, -} - -// URLSearchParams is designed to operate directly on the underlying Url to -// avoid maintaining derived state that can get out of sync. When it's used -// independently, it still needs a valid URL (http://example.com), but this -// doesn't have any effect on using URLSearchParams with URL as the params are -// stringified when added to a URL. -// -// ```js -// const params = new URLSearchParams("foo=bar"); -// const url = new URL("http://github.com"); -// url.search = params; // This works as expected -// ``` -#[rquickjs::methods(rename_all = "camelCase")] -impl<'js> URLSearchParams { - #[qjs(constructor)] - pub fn new(ctx: Ctx<'js>, init: Opt>) -> Result { - if let Some(init) = init.into_inner() { - if init.is_string() { - let string = get_lossy_string(init)?; - return Ok(Self::from_str(string)); - } else if init.is_array() { - return Self::from_array(&ctx, unsafe { init.into_array().unwrap_unchecked() }); - } else if init.is_object() { - return Self::from_object(&ctx, unsafe { init.into_object().unwrap_unchecked() }); - } - } - let url: Url = unsafe { "http://example.com".parse().unwrap_unchecked() }; - - Ok(URLSearchParams { - url: Rc::new(RefCell::new(url)), - }) - } - - // - // Properties - // - - #[qjs(get)] - pub fn size(&self) -> usize { - self.url.borrow().query_pairs().count() - } - - #[qjs(prop, rename = PredefinedAtom::SymbolToStringTag, configurable)] - pub fn to_string_tag() -> &'static str { - stringify!(URLSearchParams) - } - - // - // Instance methods - // - - pub fn append(&mut self, key: Coerced, value: Coerced) { - convert_trailing_space(&mut self.url.borrow_mut()); - - self.url - .borrow_mut() - .query_pairs_mut() - .append_pair(key.as_str(), value.as_str()); - self.sync_query(); - } - - pub fn delete(&mut self, key: Coerced, value: Opt>) { - convert_trailing_space(&mut self.url.borrow_mut()); - - let key = key.0; - - let value = get_coerced_defined_string(&value.0); - - let new_pairs: Vec<_> = self - .url - .borrow() - .query_pairs() - .filter(|(k, v)| { - if let Some(value) = value.as_ref() { - return !(*k == key && *v == *value); - } - *k != key - }) - .map(|(k, v)| (k.to_string(), v.to_string())) - .collect(); - - if !new_pairs.is_empty() { - self.url - .borrow_mut() - .query_pairs_mut() - .clear() - .extend_pairs(new_pairs); - } else { - self.url.borrow_mut().set_query(None); - } - self.sync_query(); - } - - pub fn entries( - this: This>, - ctx: Ctx<'js>, - ) -> Result>> { - URLSearchParamsIter::new(&ctx, this.0, IterKind::Entries) - } - - pub fn for_each( - this: This>, - callback: Function<'js>, - ) -> Result<()> { - // Re-read each index so the callback's mutations are observed. - let mut index = 0; - loop { - let pair = this - .0 - .borrow() - .url - .borrow() - .query_pairs() - .nth(index) - .map(|(k, v)| (k.to_string(), v.to_string())); - let Some((k, v)) = pair else { - break; - }; - () = callback.call((v, k, this.0.clone()))?; - index += 1; - } - Ok(()) - } - - pub fn get(&mut self, ctx: Ctx<'js>, key: String) -> Result> { - match self - .url - .borrow() - .query_pairs() - .find(|(k, _)| *k == key) - .map(|(_, v)| v) - { - Some(value) => value.into_js(&ctx), - None => Null.into_js(&ctx), - } - } - - pub fn get_all(&mut self, key: String) -> Vec { - self.url - .borrow() - .query_pairs() - .filter_map(|(k, v)| if k == key { Some(v.to_string()) } else { None }) - .collect() - } - - pub fn has(&self, key: Coerced, value: Opt>) -> bool { - let value = get_coerced_defined_string(&value.0); - let key = key.0; - self.url.borrow().query_pairs().any(|(k, v)| { - if let Some(value) = value.as_ref() { - return *k == key && *v == *value; - } - *k == key - }) - } - - pub fn keys( - this: This>, - ctx: Ctx<'js>, - ) -> Result>> { - URLSearchParamsIter::new(&ctx, this.0, IterKind::Keys) - } - - pub fn set(&mut self, key: Coerced, value: Coerced) { - convert_trailing_space(&mut self.url.borrow_mut()); - - let key = key.0; - let value = value.0; - - // Use a HashSet just to filter duplicates - let mut uniques = HashSet::new(); - let mut new_query_pairs: Vec<(String, String)> = Vec::new(); - - for (k, v) in self.url.borrow().query_pairs() { - // Update the value for an existing key - let value = if k == key { - value.clone() - } else { - v.to_string() - }; - - let query_pair = (k.to_string(), value); - if uniques.insert(query_pair.clone()) { - new_query_pairs.push(query_pair); - } - } - - // Append a new key/value pair - let query_pair = (key, value); - if uniques.insert(query_pair.clone()) { - new_query_pairs.push(query_pair); - } - - self.url - .borrow_mut() - .query_pairs_mut() - .clear() - .extend_pairs(new_query_pairs); - self.sync_query(); - } - - pub fn sort(&mut self) { - let mut new_pairs: Vec<(String, String)> = - self.url.borrow().query_pairs().into_owned().collect(); - new_pairs.sort_by(|(a, _), (b, _)| { - // Spec requires sorting by UTF-16 code units - let a_utf16 = a.encode_utf16(); - let b_utf16 = b.encode_utf16(); - a_utf16.cmp(b_utf16) - }); - - if new_pairs.is_empty() { - self.url.borrow_mut().set_query(None); - } else { - self.url - .borrow_mut() - .query_pairs_mut() - .clear() - .extend_pairs(new_pairs); - } - self.sync_query(); - } - - pub fn to_string(&self) -> String { - // The Url create doesn't properly encode query params for all edge - // cases, so we need to construct the query string by percent-encoding - // each key/value - // TODO: This should probably be fixed in the Url crate - let url = self.url.borrow(); - url.query_pairs().fold( - String::with_capacity(url.query().map_or(0, |q| q.len())), - |mut acc, (key, value)| { - if !acc.is_empty() { - acc.push('&'); - } - url::form_urlencoded::byte_serialize(key.as_bytes()).for_each(|b| acc.push_str(b)); - acc.push('='); - url::form_urlencoded::byte_serialize(value.as_bytes()) - .for_each(|b| acc.push_str(b)); - acc - }, - ) - } - - pub fn values( - this: This>, - ctx: Ctx<'js>, - ) -> Result>> { - URLSearchParamsIter::new(&ctx, this.0, IterKind::Values) - } - - #[qjs(rename = PredefinedAtom::SymbolIterator)] - pub fn iterator( - this: This>, - ctx: Ctx<'js>, - ) -> Result>> { - URLSearchParamsIter::new(&ctx, this.0, IterKind::Entries) - } -} - -impl<'js> URLSearchParams { - fn read_entry(&self, index: usize, ctx: &Ctx<'js>) -> Result, Value<'js>)>> { - let pair = self - .url - .borrow() - .query_pairs() - .nth(index) - .map(|(k, v)| (k.to_string(), v.to_string())); - match pair { - Some((k, v)) => Ok(Some((k.into_js(ctx)?, v.into_js(ctx)?))), - None => Ok(None), - } - } - - /// Re-serialize the query string with proper percent-encoding. - /// The url crate doesn't encode commas, so we rebuild the query - /// using form_urlencoded::byte_serialize after each mutation. - fn sync_query(&self) { - let query = self.to_string(); - let mut url = self.url.borrow_mut(); - if query.is_empty() { - url.set_query(None); - } else { - url.set_query(Some(&query)); - } - } - - #[allow(clippy::should_implement_trait)] - pub fn from_str(query: String) -> Self { - let query = if !query.starts_with('?') { - ["?", &query].concat() - } else { - query - }; - let url = unsafe { - "http://example.com" - .parse::() - .unwrap_unchecked() - .join(&query) - .unwrap_unchecked() - }; - Self { - url: Rc::new(RefCell::new(url)), - } - } - - pub fn from_url(url: &Rc>) -> Self { - Self { - url: Rc::clone(url), - } - } - - pub fn from_array(ctx: &Ctx<'js>, array: Array<'js>) -> Result { - let mut url: Url = "http://example.com".parse().unwrap(); - let query_pairs: Vec<(String, String)> = array - .into_iter() - .map(|value| { - if let Ok(value) = value { - if let Some(pair) = value.as_array() { - if pair.len() == 2 { - let key_val: Value = pair.get(0)?; - let val_val: Value = pair.get(1)?; - let key = if key_val.is_string() { - get_lossy_string(key_val)? - } else { - Coerced::::from_js(ctx, key_val)?.0 - }; - let value = if val_val.is_string() { - get_lossy_string(val_val)? - } else { - Coerced::::from_js(ctx, val_val)?.0 - }; - return Ok((key, value)); - } - } - }; - Err(Exception::throw_type( - ctx, - "Invalid tuple: Each query pair must be an iterable [name, value] tuple", - )) - }) - .collect::>>()? - .into_iter() - .collect(); - - url.query_pairs_mut().extend_pairs(query_pairs); - - Ok(Self { - url: Rc::new(RefCell::new(url)), - }) - } - - pub fn from_object(ctx: &Ctx<'js>, object: Object<'js>) -> Result { - let iterator = Symbol::iterator(ctx.clone()); - if object.contains_key(iterator)? { - let query_pairs: Array = BasePrimordials::get(ctx)? - .function_array_from - .call((object,))?; - return Self::from_array(ctx, query_pairs); - } - - let mut url: Url = "http://example.com".parse().unwrap(); - let raw_pairs: Vec<(String, String)> = object - .keys::>() - .map(|key| { - let key = key?; - let key_string = if key.is_string() { - get_lossy_string(key.clone())? - } else { - Coerced::::from_js(ctx, key.clone())?.0 - }; - let value_val: Value = object.get(key)?; - let value = if value_val.is_string() { - get_lossy_string(value_val)? - } else { - Coerced::::from_js(ctx, value_val)?.0 - }; - Ok((key_string, value)) - }) - .collect::>>()?; - - // WebIDL record conversion: when multiple input keys normalise to the - // same string (e.g. two different lone surrogates both map to U+FFFD), - // the *last* value wins. Preserve original iteration order for keys - // that were only seen once. - let mut order: Vec = Vec::with_capacity(raw_pairs.len()); - let mut map: HashMap = HashMap::with_capacity(raw_pairs.len()); - for (k, v) in raw_pairs { - if !map.contains_key(&k) { - order.push(k.clone()); - } - map.insert(k, v); - } - let query_pairs: Vec<(String, String)> = order - .into_iter() - .map(|k| { - let v = map.remove(&k).unwrap_or_default(); - (k, v) - }) - .collect(); - - url.query_pairs_mut().extend_pairs(query_pairs); - - Ok(Self { - url: Rc::new(RefCell::new(url)), - }) - } -} - -/// Live iterator over a [`URLSearchParams`]. Re-reads on each `next()` so -/// mutations during iteration are observed. -#[derive(Trace, rquickjs::JsLifetime)] -#[rquickjs::class] -pub struct URLSearchParamsIter<'js> { - params: Class<'js, URLSearchParams>, - #[qjs(skip_trace)] - index: usize, - #[qjs(skip_trace)] - kind: IterKind, -} - -impl<'js> URLSearchParamsIter<'js> { - fn new( - ctx: &Ctx<'js>, - params: Class<'js, URLSearchParams>, - kind: IterKind, - ) -> Result> { - live_iterator( - ctx, - Self { - params, - index: 0, - kind, - }, - ) - } -} - -#[rquickjs::methods] -impl<'js> URLSearchParamsIter<'js> { - fn next(&mut self, ctx: Ctx<'js>) -> Result> { - let entry = self.params.borrow().read_entry(self.index, &ctx)?; - if entry.is_some() { - self.index += 1; - } - iterator_result(&ctx, self.kind, entry) - } - - #[qjs(rename = PredefinedAtom::SymbolIterator)] - fn iter(this: This>) -> Class<'js, Self> { - this.0 - } -} - -#[cfg(test)] -mod tests { - use crate::llrt_test::test_sync_with; - use rquickjs::{CatchResultExt, Class}; - - use super::*; - - fn setup(ctx: &rquickjs::Ctx) { - BasePrimordials::init(ctx).unwrap(); - Class::::define(&ctx.globals()).unwrap(); - } - - #[tokio::test] - async fn test_basic() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams(); - params.append('a', '1'); - params.append('b', '2'); - params.append('a', '3'); - params.append('b', '4'); - params.append('c', 8); - params.delete('a'); - params.delete('b', '2'); - params.toString() - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, "b=4&c=8"); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_iterate() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams(); - params.append('a', '1'); - params.append('b', '2'); - params.append('a', '3'); - let res = []; - for (const [name, value] of params) { - res.push(`${name}=${value}`); - } - res.join('&') - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, "a=1&b=2&a=3"); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_iterate_live_delete() { - // Deleting the current/later entry during iteration skips it. - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams("foo=0&baz=1&BAR=2"); - const keys = []; - for (const [name] of params) { - keys.push(name); - params.delete("baz"); - } - keys.join(",") - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, "foo,BAR"); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_iterate_live_append() { - // Appending during iteration causes the new pair to be reached. - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams("foo=0&baz=1"); - const keys = []; - for (const [name] of params) { - keys.push(name); - if (name === "baz") params.append("end", "9"); - } - keys.join(",") - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, "foo,baz,end"); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_iterate_keys_values_live() { - // keys()/values() must also be live index-based iterators. - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams("a=1&b=2&c=3"); - const k = [...params.keys()].join(","); - const v = [...params.values()].join(","); - `${k}|${v}` - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, "a,b,c|1,2,3"); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_iterate_entries() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams(); - params.append('a', '1'); - params.append('b', '2'); - params.append('a', '3'); - let res = []; - for (const [name, value] of params.entries()) { - res.push(`${name}=${value}`); - } - res.join('&') - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, "a=1&b=2&a=3"); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_iterate_keys() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams(); - params.append('a', '1'); - params.append('b', '2'); - params.append('a', '3'); - let res = []; - for (const name of params.keys()) { - res.push(name); - } - res.join('&') - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, "a&b&a"); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_iterate_values() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams(); - params.append('a', '1'); - params.append('b', '2'); - params.append('a', '3'); - let res = []; - for (const name of params.values()) { - res.push(name); - } - res.join('&') - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, "1&2&3"); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_new_string() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams('a=1&b=2&a=3'); - params.toString() - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, "a=1&b=2&a=3"); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_new_string_url() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams('https://google.com?a=1&b=2&a=3'); - params.toString() - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, "https%3A%2F%2Fgoogle.com%3Fa=1&b=2&a=3"); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_new_object() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams({'a': 1, 'b': 2}); - params.toString() - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, "a=1&b=2"); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_new_array() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams([['a', 1], ['b', 2], ['a', 3]]); - params.toString() - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, "a=1&b=2&a=3"); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_new_iterator() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams(); - params.append('a', '1'); - params.append('b', '2'); - params.append('a', '3'); - const params2 = new URLSearchParams(params.entries()); - params2.toString() - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, "a=1&b=2&a=3"); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_size() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams(); - params.append('a', '1'); - params.append('b', '2'); - params.append('a', '3'); - params.size - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, 3); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_set() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams(); - params.append('a', '1'); - params.append('b', '2'); - params.append('a', '3'); - params.set('a', '4'); - params.toString() - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, "a=4&b=2"); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_get() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams(); - params.append('a', '1'); - params.append('b', '2'); - params.append('a', '3'); - params.get('a') - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, "1"); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_get_missing() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams(); - params.append('a', '1'); - params.append('b', '2'); - params.append('a', '3'); - params.get('c') === null - "#, - ) - .catch(&ctx) - .unwrap(); - assert!(result); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_get_all() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams(); - params.append('a', '1'); - params.append('b', '2'); - params.append('a', '3'); - params.getAll('a').join('&') - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, "1&3"); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_get_all_missing() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams(); - params.append('a', '1'); - params.append('b', '2'); - params.append('a', '3'); - params.getAll('c').join('&') - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, ""); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_has() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams(); - params.append('a', '1'); - params.append('b', '2'); - params.append('a', '3'); - params.has('b') - "#, - ) - .catch(&ctx) - .unwrap(); - assert!(result); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_has_value() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams(); - params.append('a', '1'); - params.append('b', '2'); - params.append('a', '3'); - params.has('b', 5) - "#, - ) - .catch(&ctx) - .unwrap(); - assert!(!result); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_has_not() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams(); - params.append('a', '1'); - params.append('b', '2'); - params.append('a', '3'); - params.has('c') - "#, - ) - .catch(&ctx) - .unwrap(); - assert!(!result); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_sort() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams(); - params.append('a', '3'); - params.append('b', '2'); - params.append('a', '1'); - params.sort(); - params.toString() - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, "a=3&a=1&b=2"); - Ok(()) - }) - .await - } - - #[tokio::test] - async fn test_for_each() { - test_sync_with(|ctx| { - setup(&ctx); - let result = ctx - .eval::( - r#" - const params = new URLSearchParams(); - params.append('a', '3'); - params.append('b', '2'); - params.append('a', '1'); - let res = []; - params.forEach((value, name) => { - res.push(`${name}=${value}`); - }); - res.join('&') - "#, - ) - .catch(&ctx) - .unwrap(); - assert_eq!(result, "a=3&b=2&a=1"); - Ok(()) - }) - .await - } -} diff --git a/stdlib/src/llrt/llrt_utils/any_of.rs b/stdlib/src/llrt/llrt_utils/any_of.rs deleted file mode 100644 index 4dff62ce..00000000 --- a/stdlib/src/llrt/llrt_utils/any_of.rs +++ /dev/null @@ -1,299 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use rquickjs::{ - class::{Trace, Tracer}, - Ctx, FromJs, IntoJs, JsLifetime, Result, Value, -}; - -macro_rules! define_any_of { - ($name:ident, $($variant:ident),+) => { - #[derive(Debug, Clone)] - pub enum $name<$($variant),+> { - $( - $variant($variant), - )+ - } - - define_any_of_from_js!($name, $($variant),+); - - impl<'js, $($variant: IntoJs<'js>),+> IntoJs<'js> for $name<$($variant),+> { - fn into_js(self, ctx: &Ctx<'js>) -> Result> { - match self { - $( - Self::$variant(val) => val.into_js(ctx), - )+ - } - } - } - - unsafe impl<'js, $($variant: JsLifetime<'js>),+> JsLifetime<'js> for $name<$($variant),+> { - type Changed<'to> = $name<$($variant::Changed<'to>),+>; - } - - impl<'js, $($variant: Trace<'js>),+> Trace<'js> for $name<$($variant),+> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - match self { - $( - Self::$variant(val) => val.trace(tracer), - )+ - } - } - } - - define_any_of_methods!($name, $($variant),+); - }; -} - -macro_rules! define_any_of_from_js { - ($name:ident, $first:ident, $($rest:ident),+) => { - impl<'js, $first: FromJs<'js>, $($rest: FromJs<'js>),+> FromJs<'js> for $name<$first, $($rest),+> { - fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { - define_any_of_from_js_impl!($name, ctx, value, $first, $($rest),+) - } - } - }; -} - -macro_rules! define_any_of_from_js_impl { - ($name:ident, $ctx:ident, $value:ident, $first:ident) => { - $first::from_js($ctx, $value).map($name::$first) - }; - - ($name:ident, $ctx:ident, $value:ident, $first:ident, $($rest:ident),+) => { - $first::from_js($ctx, $value.clone()).map($name::$first).or_else(|error| { - if error.is_from_js() { - define_any_of_from_js_impl!($name, $ctx, $value, $($rest),+) - } else { - Err(error) - } - }) - }; -} - -macro_rules! define_any_of_variant_methods { - ($variant:ident, $is_fn:ident, $as_fn:ident, $as_mut_fn:ident, $into_fn:ident) => { - #[allow(dead_code)] - pub fn $is_fn(&self) -> bool { - matches!(self, Self::$variant(_)) - } - - #[allow(dead_code)] - pub fn $as_fn(&self) -> Option<&$variant> { - match self { - Self::$variant(val) => Some(val), - _ => None, - } - } - - #[allow(dead_code)] - pub fn $as_mut_fn(&mut self) -> Option<&mut $variant> { - match self { - Self::$variant(val) => Some(val), - _ => None, - } - } - - #[allow(dead_code)] - pub fn $into_fn(self) -> std::result::Result<$variant, Self> { - match self { - Self::$variant(val) => Ok(val), - other => Err(other), - } - } - }; - - (A) => { - define_any_of_variant_methods!(A, is_a, as_a, as_a_mut, into_a); - }; - (B) => { - define_any_of_variant_methods!(B, is_b, as_b, as_b_mut, into_b); - }; - (C) => { - define_any_of_variant_methods!(C, is_c, as_c, as_c_mut, into_c); - }; - (D) => { - define_any_of_variant_methods!(D, is_d, as_d, as_d_mut, into_d); - }; - (E) => { - define_any_of_variant_methods!(E, is_e, as_e, as_e_mut, into_e); - }; - (F) => { - define_any_of_variant_methods!(F, is_f, as_f, as_f_mut, into_f); - }; - (G) => { - define_any_of_variant_methods!(G, is_g, as_g, as_g_mut, into_g); - }; - (H) => { - define_any_of_variant_methods!(H, is_h, as_h, as_h_mut, into_h); - }; -} - -macro_rules! define_any_of_methods { - ($name:ident, $($variant:ident),+) => { - impl<$($variant),+> $name<$($variant),+> { - $( - define_any_of_variant_methods!($variant); - )+ - } - }; -} - -define_any_of!(AnyOf2, A, B); -define_any_of!(AnyOf3, A, B, C); -define_any_of!(AnyOf4, A, B, C, D); -define_any_of!(AnyOf5, A, B, C, D, E); -define_any_of!(AnyOf6, A, B, C, D, E, F); -define_any_of!(AnyOf7, A, B, C, D, E, F, G); -define_any_of!(AnyOf8, A, B, C, D, E, F, G, H); - -#[cfg(test)] -mod tests { - use super::*; - use rquickjs::{Context, Runtime}; - - #[test] - fn test_any_of_string_number() { - let rt = Runtime::new().unwrap(); - let ctx = Context::full(&rt).unwrap(); - - ctx.with(|ctx| { - // Test string conversion - let val: Value = ctx.eval("'hello'").unwrap(); - let any: AnyOf2 = AnyOf2::from_js(&ctx, val).unwrap(); - assert!(any.is_a()); - assert_eq!(any.as_a().unwrap(), "hello"); - assert!(!any.is_b()); - assert!(any.as_b().is_none()); - - // Test number conversion - let val: Value = ctx.eval("42").unwrap(); - let any: AnyOf2 = AnyOf2::from_js(&ctx, val).unwrap(); - assert!(!any.is_a()); - assert!(any.is_b()); - assert_eq!(*any.as_b().unwrap(), 42); - }); - } - - #[test] - fn test_any_of_fallback() { - let rt = Runtime::new().unwrap(); - let ctx = Context::full(&rt).unwrap(); - - ctx.with(|ctx| { - // Test that it tries in order - let val: Value = ctx.eval("true").unwrap(); - let any: AnyOf3 = AnyOf3::from_js(&ctx, val).unwrap(); - assert!(any.is_c()); - assert!(*any.as_c().unwrap()); - assert!(!any.is_a()); - assert!(!any.is_b()); - }); - } - - #[test] - fn test_any_of_into_js() { - let rt = Runtime::new().unwrap(); - let ctx = Context::full(&rt).unwrap(); - - ctx.with(|ctx| { - let any: AnyOf2 = AnyOf2::A("test".to_string()); - let val: Value = any.into_js(&ctx).unwrap(); - let result: String = val.get().unwrap(); - assert_eq!(result, "test"); - - let any: AnyOf2 = AnyOf2::B(99); - let val: Value = any.into_js(&ctx).unwrap(); - let result: i32 = val.get().unwrap(); - assert_eq!(result, 99); - }); - } - - #[test] - fn test_any_of_methods() { - let rt = Runtime::new().unwrap(); - let ctx = Context::full(&rt).unwrap(); - - ctx.with(|ctx| { - // Test all methods for variant A - let val: Value = ctx.eval("'test'").unwrap(); - let any: AnyOf3 = AnyOf3::from_js(&ctx, val).unwrap(); - assert!(any.is_a()); - assert_eq!(any.as_a().unwrap(), "test"); - assert_eq!(any.into_a().unwrap(), "test"); - - // Test all methods for variant B - let val: Value = ctx.eval("42").unwrap(); - let any: AnyOf3 = AnyOf3::from_js(&ctx, val).unwrap(); - assert!(any.is_b()); - assert_eq!(*any.as_b().unwrap(), 42); - assert_eq!(any.into_b().unwrap(), 42); - - // Test all methods for variant C - let val: Value = ctx.eval("true").unwrap(); - let any: AnyOf3 = AnyOf3::from_js(&ctx, val).unwrap(); - assert!(any.is_c()); - assert!(*any.as_c().unwrap()); - assert!(any.into_c().unwrap()); - }); - } - - #[test] - fn test_any_of_mutable_methods() { - let rt = Runtime::new().unwrap(); - let ctx = Context::full(&rt).unwrap(); - - ctx.with(|ctx| { - let val: Value = ctx.eval("42").unwrap(); - let mut any: AnyOf4 = AnyOf4::from_js(&ctx, val).unwrap(); - - if let Some(n) = any.as_b_mut() { - *n = 100; - } - - assert_eq!(any.into_b().unwrap(), 100); - }); - } - - #[test] - fn test_any_of_error_propagation() { - use rquickjs::{Array, Object}; - - let rt = Runtime::new().unwrap(); - let ctx = Context::full(&rt).unwrap(); - - ctx.with(|ctx| { - // Test that conversion errors cause fallback to next type - let val: Value = ctx.eval("42").unwrap(); - let any: AnyOf2 = AnyOf2::from_js(&ctx, val).unwrap(); - assert!(any.is_b()); - - // Test that all types fail results in an error - let val: Value = ctx.eval("null").unwrap(); - let result: Result> = AnyOf2::from_js(&ctx, val); - assert!(result.is_err()); - }); - } - - #[test] - fn test_any_of_conversion_order() { - let rt = Runtime::new().unwrap(); - let ctx = Context::full(&rt).unwrap(); - - ctx.with(|ctx| { - // Test that conversion happens in order A, B, C, D, E - // Since 42 can be converted to f64, i32, etc., but String comes first and fails, - // it should try the next successful conversion - let val: Value = ctx.eval("42").unwrap(); - - // String should fail, so it tries i32 which succeeds - let any: AnyOf3 = AnyOf3::from_js(&ctx, val).unwrap(); - assert!(any.is_b()); - - // If we flip the order, f64 would be tried first (but both work) - let val: Value = ctx.eval("3.14").unwrap(); - let any: AnyOf3 = AnyOf3::from_js(&ctx, val).unwrap(); - assert!(any.is_b()); // f64 should succeed first - }); - } -} diff --git a/stdlib/src/llrt/llrt_utils/array_buffer.rs b/stdlib/src/llrt/llrt_utils/array_buffer.rs deleted file mode 100644 index 0c0ff8d2..00000000 --- a/stdlib/src/llrt/llrt_utils/array_buffer.rs +++ /dev/null @@ -1,122 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -//! Zero-copy `ArrayBuffer` helpers built on QuickJS-NG primitives. -//! -//! `rquickjs` doesn't yet ship safe wrappers for QuickJS-NG's -//! [immutable ArrayBuffer](https://tc39.es/proposal-immutable-arraybuffer/) -//! support, so we go through `rquickjs::qjs::*` directly. The two -//! capabilities exposed here are: -//! -//! * [`shared_array_buffer_view`] — create a fresh `ArrayBuffer` that -//! borrows the bytes of an existing one (no memcpy), kept alive via a -//! dup'd `JSValue` reference. The view is marked immutable, which is -//! both a correctness guarantee (consumer mutations can't leak into the -//! source) and a hard safety rail (the only QuickJS code path that -//! would lose our refcount handle is `.transfer()`, which immutability -//! blocks at the JS layer). -//! * [`set_immutable`] — flip the immutable flag on an existing -//! `ArrayBuffer` (used when the source is a freshly-allocated buffer -//! we own and want to seal before handing out). -//! -//! These are used by `Blob.stream()` / `Blob.slice()` and by fetch's -//! `Response.body` / `Request.body` getters to hand out aliased, -//! transfer-safe views into producer-owned storage. - -use std::ffi::c_void; - -use rquickjs::{qjs, ArrayBuffer, Ctx, Exception, Result, Value}; - -/// Mark an `ArrayBuffer` as immutable: subsequent writes through any -/// `Uint8Array` / `DataView` view silently fail (or `TypeError` in strict -/// mode), and `.transfer()` throws `TypeError: ArrayBuffer is immutable`. -/// -/// Calling this on an already-immutable buffer is a no-op. Calling it on -/// a detached buffer is a no-op (QuickJS returns -1 internally). The flag -/// is checked at write/transfer time, not at create time, so the buffer -/// can be initialised with bytes before being sealed. -pub fn set_immutable(ab: &ArrayBuffer<'_>) { - // Safety: the JSValue is owned by `ab`; we only flip a boolean flag - // on the underlying `JSArrayBuffer` struct. - unsafe { - qjs::JS_SetImmutableArrayBuffer(ab.as_value().as_raw(), true); - } -} - -/// Create a fresh, **immutable** `ArrayBuffer` that shares storage with -/// `source` at `[offset..offset+len]` without copying any bytes. The -/// returned buffer holds a dup'd reference to the source's `JSValue`, so -/// the backing allocation stays alive exactly as long as any view (or -/// transferred descendant of it) is reachable. -/// -/// Immutability is what makes this sound: -/// -/// * Writes through `Uint8Array` / `DataView` views silently no-op -/// (strict mode: `TypeError`) — aliased consumers can't corrupt the -/// source. -/// * `buffer.transfer()` throws `TypeError: ArrayBuffer is immutable` -/// — so a consumer can't detach the view and drop the `opaque` -/// pointer that keeps the source alive. Without this guard the -/// `free_func` would later fire with `opaque=NULL` (QuickJS strips -/// `opaque` on transfer; see `js_array_buffer_constructor3`) and -/// panic in `Box::from_raw(null)`. Because immutability blocks -/// transfer at the JS layer, that path is unreachable. -/// -/// If a future caller wants a *mutable* shared view, they need a -/// different cleanup strategy (ptr-keyed side table, upstream QuickJS -/// patch, or accepting a per-transfer leak). -pub fn shared_array_buffer_view<'js>( - ctx: &Ctx<'js>, - source: &ArrayBuffer<'js>, - offset: usize, - len: usize, -) -> Result> { - let raw = source - .as_raw() - .ok_or_else(|| Exception::throw_type(ctx, "cannot view a detached ArrayBuffer"))?; - debug_assert!( - offset.checked_add(len).is_some_and(|e| e <= raw.len), - "shared_array_buffer_view: slice out of range" - ); - let ptr = unsafe { raw.ptr.as_ptr().add(offset) }; - - // Dup the source's JSValue. The returned ArrayBuffer's free-callback - // (below) will drop this reference. - let ctx_ptr = ctx.as_raw().as_ptr(); - let rt = unsafe { qjs::JS_GetRuntime(ctx_ptr) }; - let source_val = unsafe { qjs::JS_DupValueRT(rt, source.as_value().as_raw()) }; - let opaque = Box::into_raw(Box::new(source_val)) as *mut c_void; - - extern "C" fn free_shared(rt: *mut qjs::JSRuntime, opaque: *mut c_void, _ptr: *mut c_void) { - // `opaque` is guaranteed non-null: the only QuickJS code path - // that loses it is `.transfer()`, which is blocked by the - // immutability flag we set below. - unsafe { - let boxed = Box::from_raw(opaque as *mut qjs::JSValue); - qjs::JS_FreeValueRT(rt, *boxed); - } - } - - let view = unsafe { - let val = qjs::JS_NewArrayBuffer( - ctx_ptr, - ptr, - len as _, - Some(free_shared), - opaque, - /*is_shared=*/ false, - ); - if qjs::JS_IsException(val) { - // QuickJS didn't take ownership of `opaque`; drop it ourselves. - let boxed = Box::from_raw(opaque as *mut qjs::JSValue); - qjs::JS_FreeValueRT(rt, *boxed); - return Err(ctx.throw(ctx.catch())); - } - let value = Value::from_raw(ctx.clone(), val); - ArrayBuffer::from_value(value) - .ok_or_else(|| Exception::throw_type(ctx, "expected ArrayBuffer"))? - }; - - set_immutable(&view); - Ok(view) -} diff --git a/stdlib/src/llrt/llrt_utils/bytearray_buffer.rs b/stdlib/src/llrt/llrt_utils/bytearray_buffer.rs deleted file mode 100644 index b1f379b2..00000000 --- a/stdlib/src/llrt/llrt_utils/bytearray_buffer.rs +++ /dev/null @@ -1,227 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::{ - cmp::min, - collections::VecDeque, - sync::{ - atomic::{AtomicBool, AtomicUsize, Ordering}, - Arc, Mutex, - }, -}; - -use tokio::sync::{Notify, Semaphore}; - -#[derive(Clone)] -pub struct BytearrayBuffer { - inner: Arc>>, - max_capacity: Arc, - len: Arc, - notify: Arc, - closed: Arc, - write_semaphore: Arc, -} - -impl BytearrayBuffer { - pub fn new(capacity: usize) -> Self { - let queue = VecDeque::with_capacity(capacity); - let capacity = queue.capacity(); - Self { - inner: Arc::new(Mutex::new(queue)), - len: Arc::new(AtomicUsize::new(0)), - max_capacity: Arc::new(AtomicUsize::new(capacity)), - notify: Arc::new(Notify::new()), - closed: Arc::new(AtomicBool::new(false)), - write_semaphore: Arc::new(Semaphore::new(1)), - } - } - - pub fn len(&self) -> usize { - self.len.load(Ordering::Relaxed) - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } - - #[allow(dead_code)] - pub fn write_forced(&self, item: &[u8]) { - let mut inner = self.inner.lock().unwrap(); - inner.extend(item); - let capacity = inner.capacity(); - self.len.fetch_add(item.len(), Ordering::Relaxed); - self.max_capacity.store(capacity, Ordering::Relaxed); - } - - pub async fn write(&self, item: &mut [u8]) -> usize { - let _ = self.write_semaphore.acquire().await.unwrap(); - let mut slice_index = 0; - loop { - let max_capacity = self.max_capacity.load(Ordering::Relaxed); - if self.closed.load(Ordering::Relaxed) { - return max_capacity; - } - - let len = self.len.load(Ordering::Relaxed); - - let available = max_capacity - len; - - if available > 0 { - let end_index = min(item.len() - 1, slice_index + available - 1); - let sub_slice = &item[slice_index..=end_index]; - let slice_length = sub_slice.len(); - slice_index += slice_length; - - self.inner.lock().unwrap().extend(sub_slice); - self.len.fetch_add(slice_length, Ordering::Relaxed); - - if slice_index == item.len() { - return max_capacity; - } - } - self.notify.notified().await; - } - } - - #[allow(dead_code)] - pub fn is_closed(&self) -> bool { - self.closed.load(Ordering::Relaxed) - } - - pub async fn close(&self) { - self.closed.store(true, Ordering::Relaxed); - self.notify.notify_one(); - //wait for write to finish - let _ = self.write_semaphore.acquire().await.unwrap(); - } - - pub async fn clear(&self) { - self.closed.store(false, Ordering::Relaxed); - self.notify.notify_one(); - //wait for write to finish - let _ = self.write_semaphore.acquire().await.unwrap(); - self.len.store(0, Ordering::Relaxed); - self.inner.lock().unwrap().clear(); - self.closed.store(false, Ordering::Relaxed); - } - - pub fn read(&self, desired_size: Option) -> Option> { - let mut inner = self.inner.lock().unwrap(); - let done = self.closed.load(Ordering::Relaxed); - - let items = if done { - Some(inner.drain(0..).collect()) - } else if let Some(desired_len) = desired_size { - let max_capacity = self.max_capacity.load(Ordering::Relaxed); - if desired_len > max_capacity { - let diff = desired_len - max_capacity; - inner.reserve(diff - 1); - let mut max_capacity = inner.capacity(); - if desired_len > max_capacity { - inner.reserve(desired_len - max_capacity); - max_capacity = inner.capacity(); - } - drop(inner); - self.max_capacity.store(max_capacity, Ordering::Relaxed); - self.notify.notify_one(); - return None; - } - - let len = self.len.load(Ordering::Relaxed); - if desired_len > len { - self.notify.notify_one(); - return None; - } - - Some(inner.drain(0..desired_len).collect()) - } else { - Some(inner.drain(0..).collect()) - }; - self.len.store(inner.len(), Ordering::Relaxed); - drop(inner); - self.notify.notify_one(); - items - } -} - -#[cfg(test)] -mod tests { - use super::BytearrayBuffer; - - #[tokio::test] - async fn clear_while_writing() { - let queue = BytearrayBuffer::new(8); - let queue2 = queue.clone(); - - tokio::task::spawn(async move { - let mut vec: Vec = (0..=255).collect(); - queue.write(&mut vec).await; - }); - - queue2.clear().await - } - - #[tokio::test] - async fn write_one_at_a_time() { - let queue = BytearrayBuffer::new(8); - let queue2 = queue.clone(); - let queue3 = queue.clone(); - - tokio::task::spawn(async move { - let mut vec: Vec = (0..=127).collect(); - queue.write(&mut vec).await; - }); - - tokio::task::spawn(async move { - let mut vec: Vec = (128..=255).collect(); - queue2.write(&mut vec).await; - }); - - let mut data = Vec::::new(); - - loop { - tokio::task::yield_now().await; - if let Some(bytes) = queue3.read(Some(256)) { - data.extend(bytes); - break; - } - } - - //assert that data in vec is increment from 0 to 255 - for i in 0..=255 { - assert_eq!(data[i as usize], i); - } - } - - #[tokio::test] - async fn queue() { - let queue = BytearrayBuffer::new(8); - let queue2 = queue.clone(); - - let write_task = tokio::task::spawn(async move { - for _ in 0..=255 { - let mut vec: Vec = (0..=255).collect(); - queue.write(&mut vec).await; - } - queue.close().await; - }); - - let mut data = Vec::::new(); - - loop { - let done = queue2.is_closed(); - - tokio::task::yield_now().await; - if let Some(bytes) = queue2.read(Some(9)) { - data.extend(bytes); - } - if done { - break; - } - } - - let _ = write_task.await; - - assert_eq!(data.len(), 256 * 256) - } -} diff --git a/stdlib/src/llrt/llrt_utils/bytes.rs b/stdlib/src/llrt/llrt_utils/bytes.rs deleted file mode 100644 index 8057f74a..00000000 --- a/stdlib/src/llrt/llrt_utils/bytes.rs +++ /dev/null @@ -1,679 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::{rc::Rc, slice}; - -use half::f16; -use rquickjs::{ - atom::PredefinedAtom, - class::{Trace, Tracer}, - function::Constructor, - ArrayBuffer, Coerced, Ctx, Error, Exception, FromJs, IntoJs, JsLifetime, Object, Result, - TypedArray, U8Clamped, Value, -}; - -/// Convert a JS string to a `String`, replacing lone UTF-16 surrogates -/// with U+FFFD per WHATWG USVString. Use when ill-formed strings must -/// not fail. -// -// SAFETY (module-wide): QuickJS only emits valid WTF-8, so any run -// without 0xED is valid strict UTF-8. -pub fn get_lossy_string(string_value: Value) -> Result { - let js_str = string_value.into_string().ok_or_else(|| Error::FromJs { - from: "Value", - to: "JSString", - message: Some("Value is not a string".into()), - })?; - let cstr = js_str.to_cstring()?; - let bytes = unsafe { slice::from_raw_parts(cstr.as_ptr() as *const u8, cstr.len()) }; - - let first = match memchr::memchr(0xED, bytes) { - None => return Ok(unsafe { String::from_utf8_unchecked(bytes.to_vec()) }), - Some(idx) => idx, - }; - let mut result = String::with_capacity(bytes.len()); - result.push_str(unsafe { std::str::from_utf8_unchecked(&bytes[..first]) }); - qjs_substitute_into(&bytes[first..], &mut result); - Ok(result) -} - -fn qjs_substitute_into(bytes: &[u8], result: &mut String) { - let mut start = 0; - while start < bytes.len() { - let next_ed = match memchr::memchr(0xED, &bytes[start..]) { - None => { - result.push_str(unsafe { std::str::from_utf8_unchecked(&bytes[start..]) }); - return; - } - Some(rel) => start + rel, - }; - if next_ed > start { - result.push_str(unsafe { std::str::from_utf8_unchecked(&bytes[start..next_ed]) }); - } - if next_ed + 3 > bytes.len() { - replace_invalid_utf8_and_utf16_into(&bytes[next_ed..], result); - return; - } - let b1 = bytes[next_ed + 1]; - let b2 = bytes[next_ed + 2]; - if (b1 & 0xC0) != 0x80 || (b2 & 0xC0) != 0x80 { - replace_invalid_utf8_and_utf16_into(&bytes[next_ed..], result); - return; - } - if (b1 & 0xE0) == 0xA0 { - result.push('\u{FFFD}'); - } else { - result.push_str(unsafe { std::str::from_utf8_unchecked(&bytes[next_ed..next_ed + 3]) }); - } - start = next_ed + 3; - } -} - -#[doc(hidden)] -pub fn replace_invalid_utf8_and_utf16(bytes: &[u8]) -> String { - let err = match simdutf8::compat::from_utf8(bytes) { - Ok(s) => return s.to_owned(), - Err(e) => e, - }; - let valid_up_to = err.valid_up_to(); - let mut result = String::with_capacity(bytes.len()); - result.push_str(unsafe { std::str::from_utf8_unchecked(&bytes[..valid_up_to]) }); - replace_invalid_utf8_and_utf16_into(&bytes[valid_up_to..], &mut result); - result -} - -fn replace_invalid_utf8_and_utf16_into(bytes: &[u8], result: &mut String) { - let mut i = 0; - - while i < bytes.len() { - let current = bytes[i]; - match current { - 0x00..=0x7F => { - result.push(current as char); - i += 1; - } - 0xC0..=0xDF if i + 1 < bytes.len() => { - let next = bytes[i + 1]; - if (next & 0xC0) == 0x80 { - let code_point = ((current as u32 & 0x1F) << 6) | (next as u32 & 0x3F); - result.push(char::from_u32(code_point).unwrap_or('\u{FFFD}')); - i += 2; - } else { - result.push('\u{FFFD}'); - i += 1; - } - } - 0xE0..=0xEF if i + 2 < bytes.len() => { - let next1 = bytes[i + 1]; - let next2 = bytes[i + 2]; - if (next1 & 0xC0) == 0x80 && (next2 & 0xC0) == 0x80 { - let code_point = ((current as u32 & 0x0F) << 12) - | ((next1 as u32 & 0x3F) << 6) - | (next2 as u32 & 0x3F); - result.push(char::from_u32(code_point).unwrap_or('\u{FFFD}')); - i += 3; - } else { - result.push('\u{FFFD}'); - i += 1; - } - } - 0xF0..=0xF7 if i + 3 < bytes.len() => { - let next1 = bytes[i + 1]; - let next2 = bytes[i + 2]; - let next3 = bytes[i + 3]; - if (next1 & 0xC0) == 0x80 && (next2 & 0xC0) == 0x80 && (next3 & 0xC0) == 0x80 { - let code_point = ((current as u32 & 0x07) << 18) - | ((next1 as u32 & 0x3F) << 12) - | ((next2 as u32 & 0x3F) << 6) - | (next3 as u32 & 0x3F); - result.push(char::from_u32(code_point).unwrap_or('\u{FFFD}')); - i += 4; - } else { - result.push('\u{FFFD}'); - i += 1; - } - } - _ => { - result.push('\u{FFFD}'); - i += 1; - } - } - } -} - -#[cfg(test)] -mod replace_invalid_utf8_tests { - use super::replace_invalid_utf8_and_utf16; - - fn cases() -> Vec<(&'static str, Vec, &'static str)> { - vec![ - ("empty", vec![], ""), - ("ascii", b"hello world".to_vec(), "hello world"), - ( - "ascii_with_control", - vec![b'a', 0x00, b'b', 0x7f, b'c'], - "a\u{0}b\u{7f}c", - ), - ("two_byte_latin1", vec![0xC3, 0xA9], "\u{00E9}"), - ("three_byte_cjk", vec![0xE4, 0xB8, 0x96], "\u{4e16}"), - ("four_byte_emoji", vec![0xF0, 0x9F, 0xA6, 0x80], "\u{1f980}"), - ("lone_high_surrogate", vec![0xED, 0xA0, 0xBD], "\u{FFFD}"), - ("lone_low_surrogate", vec![0xED, 0xB0, 0x80], "\u{FFFD}"), - ( - "surrogate_pair_in_wtf8", - vec![0xED, 0xA0, 0xBD, 0xED, 0xB2, 0xA9], - "\u{FFFD}\u{FFFD}", - ), - ("stray_continuation", vec![0x80], "\u{FFFD}"), - ("truncated_two_byte", vec![0xC3], "\u{FFFD}"), - ("truncated_three_byte", vec![0xE0, 0xA0], "\u{FFFD}\u{FFFD}"), - ( - "truncated_four_byte", - vec![0xF0, 0x9F, 0xA6], - "\u{FFFD}\u{FFFD}\u{FFFD}", - ), - ( - "two_byte_bad_continuation", - vec![0xC3, 0x20, b'a'], - "\u{FFFD} a", - ), - ( - "three_byte_bad_continuation", - vec![0xE4, 0xB8, 0x20, b'a'], - "\u{FFFD}\u{FFFD} a", - ), - ("high_byte_above_f7", vec![0xF8, b'a'], "\u{FFFD}a"), - ( - "mixed_valid_and_invalid", - { - let mut v = b"hello ".to_vec(); - v.extend_from_slice(&[0xED, 0xA0, 0xBD]); - v.extend_from_slice(" world".as_bytes()); - v - }, - "hello \u{FFFD} world", - ), - ( - "long_ascii", - b"the quick brown fox jumps over the lazy dog".repeat(20), - &*Box::leak( - "the quick brown fox jumps over the lazy dog" - .repeat(20) - .into_boxed_str(), - ), - ), - ] - } - - #[test] - fn matches_contract() { - for (name, input, expected) in cases() { - let got = replace_invalid_utf8_and_utf16(&input); - assert_eq!( - got, expected, - "case `{}`: got {:?}, expected {:?}", - name, got, expected - ); - } - } -} - -use crate::llrt_utils::{error_messages::ERROR_MSG_ARRAY_BUFFER_DETACHED, result::ResultExt}; - -#[derive(Clone, PartialEq)] -pub enum ObjectBytes<'js> { - U8Array(TypedArray<'js, u8>), - I8Array(TypedArray<'js, i8>), - U16Array(TypedArray<'js, u16>), - I16Array(TypedArray<'js, i16>), - U32Array(TypedArray<'js, u32>), - I32Array(TypedArray<'js, i32>), - U64Array(TypedArray<'js, u64>), - I64Array(TypedArray<'js, i64>), - F16Array(TypedArray<'js, f16>), - F32Array(TypedArray<'js, f32>), - F64Array(TypedArray<'js, f64>), - U8ClampedArray(TypedArray<'js, U8Clamped>), - DataView(ArrayBuffer<'js>, usize, usize), // buffer, offset, length - Vec(Vec), -} - -// Requires manual implementation because rquickjs hasn't implemented JsLifetime for f32 or f64 -unsafe impl<'js> JsLifetime<'js> for ObjectBytes<'js> { - type Changed<'to> = ObjectBytes<'to>; -} - -impl<'js> Trace<'js> for ObjectBytes<'js> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - match self { - ObjectBytes::U8Array(a) => a.trace(tracer), - ObjectBytes::I8Array(a) => a.trace(tracer), - ObjectBytes::U16Array(a) => a.trace(tracer), - ObjectBytes::I16Array(a) => a.trace(tracer), - ObjectBytes::U32Array(a) => a.trace(tracer), - ObjectBytes::I32Array(a) => a.trace(tracer), - ObjectBytes::U64Array(a) => a.trace(tracer), - ObjectBytes::I64Array(a) => a.trace(tracer), - ObjectBytes::F16Array(a) => a.trace(tracer), - ObjectBytes::F32Array(a) => a.trace(tracer), - ObjectBytes::F64Array(a) => a.trace(tracer), - ObjectBytes::U8ClampedArray(a) => a.trace(tracer), - ObjectBytes::DataView(ab, _, _) => ab.trace(tracer), - ObjectBytes::Vec(v) => v.trace(tracer), - } - } -} - -impl<'js> IntoJs<'js> for ObjectBytes<'js> { - fn into_js(self, ctx: &Ctx<'js>) -> Result> { - match self { - ObjectBytes::U8Array(a) => a.into_js(ctx), - ObjectBytes::I8Array(a) => a.into_js(ctx), - ObjectBytes::U16Array(a) => a.into_js(ctx), - ObjectBytes::I16Array(a) => a.into_js(ctx), - ObjectBytes::U32Array(a) => a.into_js(ctx), - ObjectBytes::I32Array(a) => a.into_js(ctx), - ObjectBytes::U64Array(a) => a.into_js(ctx), - ObjectBytes::I64Array(a) => a.into_js(ctx), - ObjectBytes::F16Array(a) => a.into_js(ctx), - ObjectBytes::F32Array(a) => a.into_js(ctx), - ObjectBytes::F64Array(a) => a.into_js(ctx), - ObjectBytes::U8ClampedArray(a) => a.into_js(ctx), - ObjectBytes::DataView(ab, _, _) => { - let ctor: Constructor = ctx.globals().get(PredefinedAtom::DataView)?; - ctor.construct((ab,)) - } - ObjectBytes::Vec(v) => v.into_js(ctx), - } - } -} - -impl<'js> TryFrom> for Vec { - type Error = Rc; - fn try_from(value: ObjectBytes<'js>) -> std::result::Result { - value.into_bytes_inner() - } -} - -impl<'a, 'js> TryFrom<&'a ObjectBytes<'js>> for &'a [u8] { - type Error = Rc; - fn try_from(value: &'a ObjectBytes<'js>) -> std::result::Result { - value.as_bytes_inner() - } -} - -impl<'js> FromJs<'js> for ObjectBytes<'js> { - fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { - Self::from_offset(ctx, &value, 0, None) - } -} - -impl<'js> ObjectBytes<'js> { - pub fn from(ctx: &Ctx<'js>, value: &Value<'js>) -> Result { - Self::from_offset(ctx, value, 0, None) - } - - pub fn from_offset( - ctx: &Ctx<'js>, - value: &Value<'js>, - offset: usize, - length: Option, - ) -> Result { - if value.is_undefined() { - return Ok(ObjectBytes::Vec(vec![])); - } - if let Some(bytes) = get_string_bytes(value, offset, length)? { - return Ok(ObjectBytes::Vec(bytes)); - } - if let Some(bytes) = get_array_bytes(value, offset, length)? { - return Ok(ObjectBytes::Vec(bytes)); - } - - if let Some(obj) = value.as_object() { - if let Some(bytes) = Self::from_array_buffer(obj)? { - return Ok(bytes); - } - } - - if let Some(bytes) = get_coerced_string_bytes(value, offset, length) { - return Ok(ObjectBytes::Vec(bytes)); - } - - Err(Exception::throw_message( - ctx, - "value must be typed DataView, Buffer, ArrayBuffer, Uint8Array or interpretable as string", - )) - } - - pub fn as_bytes(&self, ctx: &Ctx<'js>) -> Result<&[u8]> { - self.as_bytes_inner().or_throw(ctx) - } - - /// Returns the underlying bytes, or `None` if the buffer is detached or - /// the DataView range is invalid (including arithmetic overflow). Unlike - /// [`as_bytes`], does not raise a JS exception. - pub fn as_bytes_opt(&self) -> Option<&[u8]> { - self.as_bytes_inner().ok() - } - - fn as_bytes_inner(&self) -> std::result::Result<&[u8], Rc> { - match self { - ObjectBytes::U8Array(array) => array.as_bytes(), - ObjectBytes::I8Array(array) => array.as_bytes(), - ObjectBytes::U16Array(array) => array.as_bytes(), - ObjectBytes::I16Array(array) => array.as_bytes(), - ObjectBytes::U32Array(array) => array.as_bytes(), - ObjectBytes::I32Array(array) => array.as_bytes(), - ObjectBytes::U64Array(array) => array.as_bytes(), - ObjectBytes::I64Array(array) => array.as_bytes(), - ObjectBytes::F16Array(array) => array.as_bytes(), - ObjectBytes::F32Array(array) => array.as_bytes(), - ObjectBytes::F64Array(array) => array.as_bytes(), - ObjectBytes::U8ClampedArray(array) => array.as_bytes(), - ObjectBytes::DataView(ab, offset, length) => ab.as_bytes().and_then(|bytes| { - let end = offset.checked_add(*length)?; - bytes.get(*offset..end) - }), - ObjectBytes::Vec(bytes) => Some(bytes.as_ref()), - } - .ok_or(ERROR_MSG_ARRAY_BUFFER_DETACHED.into()) - } - - pub fn into_bytes(self, ctx: &Ctx<'_>) -> Result> { - self.into_bytes_inner().or_throw(ctx) - } - - fn into_bytes_inner(self) -> std::result::Result, Rc> { - if let ObjectBytes::Vec(bytes) = self { - return Ok(bytes); - } - Ok(self.as_bytes_inner()?.to_vec()) - } - - pub fn from_array_buffer(obj: &Object<'js>) -> Result>> { - //most common - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - return Ok(Some(ObjectBytes::U8Array(typed_array))); - } - //second most common - if let Some(array_buffer) = ArrayBuffer::from_object(obj.clone()) { - let len = array_buffer.len(); - return Ok(Some(ObjectBytes::DataView(array_buffer, 0, len))); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - return Ok(Some(ObjectBytes::I8Array(typed_array))); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - return Ok(Some(ObjectBytes::U16Array(typed_array))); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - return Ok(Some(ObjectBytes::I16Array(typed_array))); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - return Ok(Some(ObjectBytes::U32Array(typed_array))); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - return Ok(Some(ObjectBytes::I32Array(typed_array))); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - return Ok(Some(ObjectBytes::U64Array(typed_array))); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - return Ok(Some(ObjectBytes::I64Array(typed_array))); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - return Ok(Some(ObjectBytes::F16Array(typed_array))); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - return Ok(Some(ObjectBytes::F32Array(typed_array))); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - return Ok(Some(ObjectBytes::F64Array(typed_array))); - } - - if let Ok(typed_array) = TypedArray::::from_object(obj.clone()) { - return Ok(Some(ObjectBytes::U8ClampedArray(typed_array))); - } - - if let Ok(ab) = obj.get::<_, ArrayBuffer>("buffer") { - let offset: usize = obj.get("byteOffset").unwrap_or(0); - let length: usize = obj.get("byteLength").unwrap_or_else(|_| ab.len()); - return Ok(Some(ObjectBytes::DataView(ab, offset, length))); - } - - Ok(None) - } - - pub fn get_array_buffer(&self) -> Result, usize, usize)>> { - let buffer = match self { - ObjectBytes::U8Array(typed_array) => { - let byte_length = typed_array.len(); - ( - typed_array.arraybuffer()?, - byte_length, - typed_array.get("byteOffset")?, - ) - } - ObjectBytes::I8Array(typed_array) => { - let byte_length = typed_array.len(); - ( - typed_array.arraybuffer()?, - byte_length, - typed_array.get("byteOffset")?, - ) - } - ObjectBytes::U16Array(typed_array) => { - let byte_length = typed_array.len() * 2; - ( - typed_array.arraybuffer()?, - byte_length, - typed_array.get("byteOffset")?, - ) - } - ObjectBytes::I16Array(typed_array) => { - let byte_length = typed_array.len() * 2; - ( - typed_array.arraybuffer()?, - byte_length, - typed_array.get("byteOffset")?, - ) - } - ObjectBytes::U32Array(typed_array) => { - let byte_length = typed_array.len() * 4; - ( - typed_array.arraybuffer()?, - byte_length, - typed_array.get("byteOffset")?, - ) - } - ObjectBytes::I32Array(typed_array) => { - let byte_length = typed_array.len() * 4; - ( - typed_array.arraybuffer()?, - byte_length, - typed_array.get("byteOffset")?, - ) - } - ObjectBytes::U64Array(typed_array) => { - let byte_length = typed_array.len() * 8; - ( - typed_array.arraybuffer()?, - byte_length, - typed_array.get("byteOffset")?, - ) - } - ObjectBytes::I64Array(typed_array) => { - let byte_length = typed_array.len() * 8; - ( - typed_array.arraybuffer()?, - byte_length, - typed_array.get("byteOffset")?, - ) - } - ObjectBytes::F16Array(typed_array) => { - let byte_length = typed_array.len() * 2; - ( - typed_array.arraybuffer()?, - byte_length, - typed_array.get("byteOffset")?, - ) - } - ObjectBytes::F32Array(typed_array) => { - let byte_length = typed_array.len() * 4; - ( - typed_array.arraybuffer()?, - byte_length, - typed_array.get("byteOffset")?, - ) - } - ObjectBytes::F64Array(typed_array) => { - let byte_length = typed_array.len() * 8; - ( - typed_array.arraybuffer()?, - byte_length, - typed_array.get("byteOffset")?, - ) - } - ObjectBytes::U8ClampedArray(typed_array) => { - let byte_length = typed_array.len(); - ( - typed_array.arraybuffer()?, - byte_length, - typed_array.get("byteOffset")?, - ) - } - ObjectBytes::DataView(array_buffer, offset, length) => { - (array_buffer.clone(), *length, *offset) - } - _ => return Ok(None), - }; - - Ok(Some(buffer)) - } -} - -#[cfg(test)] -mod object_bytes_tests { - use super::{ObjectBytes, ERROR_MSG_ARRAY_BUFFER_DETACHED}; - use rquickjs::{ArrayBuffer, Context, Runtime}; - - #[test] - fn data_view_ranges_are_checked() { - let rt = Runtime::new().unwrap(); - let ctx = Context::full(&rt).unwrap(); - - ctx.with(|ctx| { - let buffer = ArrayBuffer::new_copy(ctx, [1_u8, 2, 3, 4]).unwrap(); - for (offset, length) in [(3, 2), (usize::MAX, 1)] { - let bytes = ObjectBytes::DataView(buffer.clone(), offset, length); - - assert_eq!( - bytes.as_bytes_inner().unwrap_err().as_ref(), - ERROR_MSG_ARRAY_BUFFER_DETACHED - ); - } - - let valid_bytes = ObjectBytes::DataView(buffer, 1, 2); - assert_eq!(valid_bytes.as_bytes_inner().unwrap(), &[2, 3]); - }); - } - - #[test] - fn data_view_detached_buffer_returns_error() { - let rt = Runtime::new().unwrap(); - let ctx = Context::full(&rt).unwrap(); - - ctx.with(|ctx| { - let mut buffer = ArrayBuffer::new_copy(ctx, [1_u8, 2, 3, 4]).unwrap(); - buffer.detach(); - let bytes = ObjectBytes::DataView(buffer, 0, 4); - - assert_eq!( - bytes.as_bytes_inner().unwrap_err().as_ref(), - ERROR_MSG_ARRAY_BUFFER_DETACHED - ); - }); - } -} - -pub fn get_start_end_indexes( - source_len: usize, - target_len: Option, - offset: usize, -) -> (usize, usize) { - if offset > source_len { - return (0, 0); - } - - let target_len = target_len.unwrap_or(source_len - offset); - - if offset + target_len > source_len { - return (offset, source_len); - } - - (offset, target_len + offset) -} - -pub fn get_array_bytes( - value: &Value<'_>, - offset: usize, - length: Option, -) -> Result>> { - if value.is_array() { - let array = value.as_array().unwrap(); - let (start, end) = get_start_end_indexes(array.len(), length, offset); - let size = end - start; - let mut bytes: Vec = Vec::with_capacity(size); - - for val in array.iter::().skip(start).take(size) { - let val: u8 = val?; - bytes.push(val); - } - - return Ok(Some(bytes)); - } - Ok(None) -} - -pub fn get_coerced_string_bytes( - value: &Value<'_>, - offset: usize, - length: Option, -) -> Option> { - if let Ok(val) = value.get::>() { - return Some(bytes_from_js_string(val.0, offset, length)); - }; - None -} - -fn bytes_from_js_string(string: String, offset: usize, length: Option) -> Vec { - let (start, end) = get_start_end_indexes(string.len(), length, offset); - string.as_bytes()[start..end].to_vec() -} - -#[inline] -pub fn get_string_bytes( - value: &Value<'_>, - offset: usize, - length: Option, -) -> Result>> { - if value.is_string() { - let string = get_lossy_string(value.clone())?; - return Ok(Some(bytes_from_js_string(string, offset, length))); - } - Ok(None) -} - -pub fn bytes_to_typed_array<'js>(ctx: Ctx<'js>, bytes: &[u8]) -> Result> { - TypedArray::::new(ctx.clone(), bytes).into_js(&ctx) -} diff --git a/stdlib/src/llrt/llrt_utils/class.rs b/stdlib/src/llrt/llrt_utils/class.rs deleted file mode 100644 index 72f13ad7..00000000 --- a/stdlib/src/llrt/llrt_utils/class.rs +++ /dev/null @@ -1,126 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use rquickjs::{ - atom::PredefinedAtom, class::JsClass, object::Accessor, object::Property, prelude::This, Array, - Class, Ctx, Function, Object, Result, Symbol, Value, -}; - -use super::{ - object::ObjectExt, - primordials::{BasePrimordials, Primordial}, - result::OptionExt, -}; - -pub static CUSTOM_INSPECT_SYMBOL_DESCRIPTION: &str = "llrt.inspect.custom"; - -/// Which view an iterator yields: `keys()`, `values()`, or `entries()`. -#[derive(Clone, Copy)] -pub enum IterKind { - Keys, - Values, - Entries, -} - -/// Wrap an entry into a `{ value, done }` iterator result. `None` means done. -pub fn iterator_result<'js>( - ctx: &Ctx<'js>, - kind: IterKind, - entry: Option<(Value<'js>, Value<'js>)>, -) -> Result> { - let obj = Object::new(ctx.clone())?; - match entry { - Some((key, value)) => { - obj.set(PredefinedAtom::Done, false)?; - match kind { - IterKind::Keys => obj.set(PredefinedAtom::Value, key)?, - IterKind::Values => obj.set(PredefinedAtom::Value, value)?, - IterKind::Entries => { - let entry = Array::new(ctx.clone())?; - entry.set(0, key)?; - entry.set(1, value)?; - obj.set(PredefinedAtom::Value, entry)?; - } - } - } - None => obj.set(PredefinedAtom::Done, true)?, - } - Ok(obj) -} - -/// Create a WebIDL iterator instance, wiring its prototype the first time: -/// the prototype inherits `%IteratorPrototype%` (so it's tagged -/// `[object Iterator]`) and `next` becomes enumerable. Idempotent — later -/// calls skip the setup — so callers just build iterators and never register -/// anything separately. -pub fn live_iterator<'js, C>(ctx: &Ctx<'js>, iter: C) -> Result> -where - C: JsClass<'js> + 'js, -{ - let instance = Class::::instance(ctx.clone(), iter)?; - if let Some(proto) = Class::::prototype(ctx)? { - let iterator_proto = &BasePrimordials::get(ctx)?.prototype_iterator; - if proto.get_prototype().as_ref() != Some(iterator_proto) { - proto.set_prototype(Some(iterator_proto))?; - let next_fn: Function = proto.get(PredefinedAtom::Next)?; - proto.prop( - PredefinedAtom::Next, - Property::from(next_fn) - .writable() - .enumerable() - .configurable(), - )?; - } - } - Ok(instance) -} - -pub fn get_class_name(value: &Value) -> Result> { - value - .get_optional::<_, Object>(PredefinedAtom::Constructor)? - .and_then_ok(|ctor| ctor.get_optional::<_, String>(PredefinedAtom::Name)) -} - -#[inline(always)] -pub fn get_class<'js, C>(provided: &Value<'js>) -> Result>> -where - C: JsClass<'js>, -{ - if provided - .as_object() - .map(|p| p.instance_of::()) - .unwrap_or_default() - { - return Ok(Some(Class::::from_value(provided)?)); - } - Ok(None) -} - -pub trait CustomInspectExtension<'js> { - fn define_with_custom_inspect(globals: &Object<'js>) -> Result<()>; -} - -pub trait CustomInspect<'js> -where - Self: JsClass<'js>, -{ - fn custom_inspect(&self, ctx: Ctx<'js>) -> Result>; -} - -impl<'js, C> CustomInspectExtension<'js> for Class<'js, C> -where - C: JsClass<'js> + CustomInspect<'js> + 'js, -{ - fn define_with_custom_inspect(globals: &Object<'js>) -> Result<()> { - Self::define(globals)?; - let custom_inspect_symbol = - Symbol::new_global(globals.ctx().clone(), CUSTOM_INSPECT_SYMBOL_DESCRIPTION)?; - if let Some(proto) = Class::::prototype(globals.ctx())? { - proto.prop( - custom_inspect_symbol, - Accessor::from(|this: This>, ctx| this.borrow().custom_inspect(ctx)), - )?; - } - Ok(()) - } -} diff --git a/stdlib/src/llrt/llrt_utils/clone.rs b/stdlib/src/llrt/llrt_utils/clone.rs deleted file mode 100644 index 50c0b00e..00000000 --- a/stdlib/src/llrt/llrt_utils/clone.rs +++ /dev/null @@ -1,21 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use rquickjs::{class::JsClass, Class, Ctx, Object, Result, Value}; - -pub trait StructuredClone<'js>: JsClass<'js> { - fn structured_clone(&self, ctx: &Ctx<'js>) -> Result>; -} - -pub fn clone_platform_object<'js, T>( - ctx: &Ctx<'js>, - object: &Object<'js>, -) -> Result>> -where - T: StructuredClone<'js>, -{ - if let Some(class) = Class::::from_object(object) { - return Ok(Some(class.borrow().structured_clone(ctx)?)); - } - Ok(None) -} diff --git a/stdlib/src/llrt/llrt_utils/ctx.rs b/stdlib/src/llrt/llrt_utils/ctx.rs deleted file mode 100644 index efbf1f31..00000000 --- a/stdlib/src/llrt/llrt_utils/ctx.rs +++ /dev/null @@ -1,18 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use rquickjs::{Ctx, Result}; - -pub trait CtxExt { - fn get_script_or_module_name(&self) -> Result; -} - -impl CtxExt for Ctx<'_> { - fn get_script_or_module_name(&self) -> Result { - if let Some(name) = self.script_or_module_name(0) { - name.to_string() - } else { - Ok(String::from(".")) - } - } -} diff --git a/stdlib/src/llrt/llrt_utils/error.rs b/stdlib/src/llrt/llrt_utils/error.rs deleted file mode 100644 index 915e3072..00000000 --- a/stdlib/src/llrt/llrt_utils/error.rs +++ /dev/null @@ -1,24 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use rquickjs::{CatchResultExt, CaughtError, Ctx, Error, IntoJs, Result, Value}; - -pub trait ErrorExtensions<'js> { - fn into_value(self, ctx: &Ctx<'js>) -> Result>; -} - -impl<'js> ErrorExtensions<'js> for Error { - fn into_value(self, ctx: &Ctx<'js>) -> Result> { - Err::<(), _>(self).catch(ctx).unwrap_err().into_value(ctx) - } -} - -impl<'js> ErrorExtensions<'js> for CaughtError<'js> { - fn into_value(self, ctx: &Ctx<'js>) -> Result> { - Ok(match self { - CaughtError::Error(err) => err.to_string().into_js(ctx)?, - CaughtError::Exception(ex) => ex.into_value(), - CaughtError::Value(val) => val, - }) - } -} diff --git a/stdlib/src/llrt/llrt_utils/error_messages.rs b/stdlib/src/llrt/llrt_utils/error_messages.rs deleted file mode 100644 index 60e4b56e..00000000 --- a/stdlib/src/llrt/llrt_utils/error_messages.rs +++ /dev/null @@ -1,4 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -pub const ERROR_MSG_NOT_ARRAY_BUFFER: &str = "Not an ArrayBuffer"; -pub const ERROR_MSG_ARRAY_BUFFER_DETACHED: &str = "ArrayBuffer is detached"; -pub const ERROR_MSG_BROADCAST_LAGGED: &str = "Lagged too much behind"; diff --git a/stdlib/src/llrt/llrt_utils/fs.rs b/stdlib/src/llrt/llrt_utils/fs.rs deleted file mode 100644 index 3e349e0e..00000000 --- a/stdlib/src/llrt/llrt_utils/fs.rs +++ /dev/null @@ -1,104 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::{fs::Metadata, io, path::PathBuf}; - -use tokio::fs::{self}; - -pub struct DirectoryWalker -where - T: Fn(&str) -> bool, -{ - stack: Vec<(PathBuf, Option)>, - filter: T, - recursive: bool, - eat_root: bool, -} - -impl DirectoryWalker -where - T: Fn(&str) -> bool, -{ - pub fn new(root: PathBuf, filter: T) -> Self { - Self { - stack: vec![(root, None)], - filter, - recursive: false, - eat_root: true, - } - } - - pub fn set_recursive(&mut self, recursive: bool) { - self.recursive = recursive; - } - - pub async fn walk(&mut self) -> io::Result> { - if self.eat_root { - self.eat_root = false; - let (dir, _) = self.stack.pop().unwrap(); - self.append_stack(&dir).await?; - } - if let Some((entry, metadata)) = self.stack.pop() { - let metadata = metadata.unwrap(); - if self.recursive && metadata.is_dir() { - self.append_stack(&entry).await?; - } - - Ok(Some((entry, metadata))) - } else { - Ok(None) - } - } - - pub fn walk_sync(&mut self) -> io::Result> { - if self.eat_root { - self.eat_root = false; - let (dir, _) = self.stack.pop().unwrap(); - self.append_stack_sync(&dir)?; - } - if let Some((entry, metadata)) = self.stack.pop() { - let metadata = metadata.unwrap(); - if self.recursive && metadata.is_dir() { - self.append_stack_sync(&entry)?; - } - - Ok(Some((entry, metadata))) - } else { - Ok(None) - } - } - - async fn append_stack(&mut self, dir: &PathBuf) -> io::Result<()> { - let mut stream = fs::read_dir(dir).await?; - - while let Some(entry) = stream.next_entry().await? { - let name = entry.file_name(); - let name = name.to_string_lossy(); - if !(self.filter)(name.as_ref()) { - continue; - } - let entry_path = entry.path(); - let metadata = fs::symlink_metadata(&entry_path).await?; - - self.stack.push((entry_path, Some(metadata))); - } - Ok(()) - } - - fn append_stack_sync(&mut self, dir: &PathBuf) -> io::Result<()> { - let dir = std::fs::read_dir(dir)?; - - for entry in dir.flatten() { - let name = entry.file_name(); - let name = name.to_string_lossy(); - if !(self.filter)(name.as_ref()) { - continue; - } - let entry_path = entry.path(); - let metadata = entry_path.symlink_metadata()?; - self.stack.push((entry_path, Some(metadata))) - } - - Ok(()) - } -} diff --git a/stdlib/src/llrt/llrt_utils/hash.rs b/stdlib/src/llrt/llrt_utils/hash.rs deleted file mode 100644 index 5b3d17d3..00000000 --- a/stdlib/src/llrt/llrt_utils/hash.rs +++ /dev/null @@ -1,9 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use std::hash::{DefaultHasher, Hash, Hasher}; - -#[inline] -pub fn default_hash(v: &T) -> usize { - let mut state = DefaultHasher::default(); - v.hash(&mut state); - state.finish() as usize -} diff --git a/stdlib/src/llrt/llrt_utils/io.rs b/stdlib/src/llrt/llrt_utils/io.rs deleted file mode 100644 index baefaea8..00000000 --- a/stdlib/src/llrt/llrt_utils/io.rs +++ /dev/null @@ -1,31 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -macro_rules! define_extension { - ($base:ident, $file:ident, $ext:expr) => { - #[allow(dead_code)] - pub const $base: &str = $ext; - #[allow(dead_code)] - pub const $file: &str = concat!(".", $ext); - }; -} - -define_extension!(BYTECODE_EXT, BYTECODE_FILE_EXT, "lrt"); - -macro_rules! define_supported_extensions { - // Accepts a list of supported extensions and a single additional constant extension - ($constant_ext:ident, $($ext:literal),*) => { - // Define the array of extensions as a constant - pub const SUPPORTED_EXTENSIONS: &[&str] = &[$($ext),*, $constant_ext]; - - pub const JS_EXTENSIONS: &[&str] = &[$($ext),*]; - - // Define the function `is_supported_ext` using a match statement - pub fn is_supported_ext(ext: &str) -> bool { - matches!(ext, $($ext)|* | $constant_ext) - } - }; -} - -define_supported_extensions!(BYTECODE_FILE_EXT, ".js", ".mjs", ".cjs"); diff --git a/stdlib/src/llrt/llrt_utils/latch.rs b/stdlib/src/llrt/llrt_utils/latch.rs deleted file mode 100644 index 1fdda570..00000000 --- a/stdlib/src/llrt/llrt_utils/latch.rs +++ /dev/null @@ -1,31 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::sync::atomic::{AtomicUsize, Ordering}; - -use tokio::sync::Notify; - -#[derive(Default)] -pub struct Latch { - count: AtomicUsize, - notify: Notify, -} - -impl Latch { - pub fn increment(&self) { - self.count.fetch_add(1, Ordering::Relaxed); - } - - pub fn decrement(&self) { - let previous = self.count.fetch_sub(1, Ordering::Relaxed); - if previous == 1 { - self.notify.notify_waiters(); - } - } - - pub async fn wait(&self) { - if self.count.load(Ordering::Relaxed) > 0 { - self.notify.notified().await; - } - } -} diff --git a/stdlib/src/llrt/llrt_utils/lib.rs b/stdlib/src/llrt/llrt_utils/lib.rs deleted file mode 100644 index 32b92a2b..00000000 --- a/stdlib/src/llrt/llrt_utils/lib.rs +++ /dev/null @@ -1,37 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -pub mod any_of; -pub mod array_buffer; -#[cfg(any())] -pub mod bytearray_buffer; -pub mod bytes; -pub mod class; -pub mod clone; -pub mod ctx; -pub mod error; -pub mod error_messages; -#[cfg(any())] -pub mod fs; -pub mod hash; -pub mod io; -pub mod latch; -pub mod macros; -pub mod mc_oneshot; -pub mod module; -pub mod object; -pub mod option; -pub mod primordials; -pub mod provider; -pub mod result; -pub mod reuse_list; -pub mod string; -pub mod sysinfo; -pub mod time; - -pub mod signals; - -pub const VERSION: &str = "0.9.0-beta"; - -// Macro exports move to the combined crate root. -pub(crate) use crate::{count_members, iterable_enum, str_enum}; diff --git a/stdlib/src/llrt/llrt_utils/macros.rs b/stdlib/src/llrt/llrt_utils/macros.rs deleted file mode 100644 index 85a8c0d3..00000000 --- a/stdlib/src/llrt/llrt_utils/macros.rs +++ /dev/null @@ -1,56 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -#[macro_export] -macro_rules! count_members { - () => (0); - ($head:tt $(,$tail:tt)*) => (1 + count_members!($($tail),*)); -} - -#[macro_export] -macro_rules! iterable_enum { - ($name:ident, $($variant:ident),*) => { - impl $name { - const VARIANTS: &'static [$name] = &[$($name::$variant,)*]; - pub fn iter() -> std::slice::Iter<'static, $name> { - Self::VARIANTS.iter() - } - - #[allow(dead_code)] - fn _ensure_all_variants(s: Self) { - match s { - $($name::$variant => {},)* - } - } - } - }; -} - -#[macro_export] -macro_rules! str_enum { - ($name:ident, $($variant:ident => $str:expr),*) => { - impl $name { - pub fn as_str(&self) -> &'static str { - match self { - $($name::$variant => $str,)* - } - } - } - - impl AsRef for $name { - fn as_ref(&self) -> &str { - self.as_str() - } - } - - impl TryFrom<&str> for $name { - type Error = String; - fn try_from(s: &str) -> std::result::Result { - match s { - $($str => Ok($name::$variant),)* - _ => Err(["'", s, "' not available"].concat()) - } - } - } - }; -} diff --git a/stdlib/src/llrt/llrt_utils/mc_oneshot.rs b/stdlib/src/llrt/llrt_utils/mc_oneshot.rs deleted file mode 100644 index dac3be5c..00000000 --- a/stdlib/src/llrt/llrt_utils/mc_oneshot.rs +++ /dev/null @@ -1,119 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 - -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, RwLock, -}; - -use rquickjs::{ - class::{Trace, Tracer}, - Value, -}; -use std::ops::Deref; -use tokio::sync::Notify; - -#[derive(Debug)] -pub struct Shared { - is_sent: AtomicBool, - value: RwLock>, - notify: Notify, -} - -#[derive(Clone, Debug)] -pub struct Sender(Arc>); - -impl Deref for Sender { - type Target = Arc>; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl<'js> Trace<'js> for Sender> { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - if let Ok(v) = self.value.read() { - if let Some(v) = v.as_ref() { - tracer.mark(v) - } - } - } -} - -impl Sender { - pub fn send(&self, value: T) { - if !self.is_sent.load(Ordering::Relaxed) { - self.value.write().unwrap().replace(value); - self.is_sent.store(true, Ordering::Release); - self.notify.notify_waiters(); - } - } - - pub fn subscribe(&self) -> Receiver { - Receiver(Arc::clone(&self.0)) - } -} - -#[derive(Clone, Debug)] -pub struct Receiver(Arc>); - -impl Deref for Receiver { - type Target = Arc>; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl Receiver { - pub async fn recv(&self) -> T { - if !self.is_sent.load(Ordering::Acquire) { - self.notify.notified().await; - } - self.value.read().unwrap().clone().unwrap() - } -} - -pub fn channel() -> (Sender, Receiver) { - let shared = Arc::new(Shared { - is_sent: AtomicBool::new(false), - value: RwLock::new(None), - notify: Notify::new(), - }); - - (Sender(Arc::clone(&shared)), Receiver(shared)) -} - -#[cfg(test)] -mod tests { - use tokio::join; - - #[tokio::test] - async fn test() { - let (tx, rx1) = super::channel::(); - - let rx2 = tx.subscribe(); - let rx3 = tx.subscribe(); - - let a = tokio::spawn(async move { - let val = rx1.recv().await; //wait for value to become false - assert!(val) - }); - - let b = tokio::spawn(async move { - let val = rx2.recv().await; //wait for value to become false - assert!(val) - }); - - tokio::time::sleep(std::time::Duration::from_millis(10)).await; - - tx.send(true); - - let val = rx3.recv().await; - assert!(val); - - let (a, b) = join!(a, b); - a.unwrap(); - b.unwrap(); - } -} diff --git a/stdlib/src/llrt/llrt_utils/module.rs b/stdlib/src/llrt/llrt_utils/module.rs deleted file mode 100644 index 950922d4..00000000 --- a/stdlib/src/llrt/llrt_utils/module.rs +++ /dev/null @@ -1,30 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use rquickjs::{ - module::{Exports, ModuleDef}, - Ctx, Object, Result, Value, -}; - -pub struct ModuleInfo { - pub name: &'static str, - pub module: T, -} - -pub fn export_default<'js, F>(ctx: &Ctx<'js>, exports: &Exports<'js>, f: F) -> Result<()> -where - F: FnOnce(&Object<'js>) -> Result<()>, -{ - let default = Object::new(ctx.clone())?; - f(&default)?; - - for name in default.keys::() { - let name = name?; - let value: Value = default.get(&name)?; - exports.export(name, value)?; - } - - exports.export("default", default)?; - - Ok(()) -} diff --git a/stdlib/src/llrt/llrt_utils/object.rs b/stdlib/src/llrt/llrt_utils/object.rs deleted file mode 100644 index 25495967..00000000 --- a/stdlib/src/llrt/llrt_utils/object.rs +++ /dev/null @@ -1,171 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use std::collections::BTreeMap; - -use rquickjs::{ - atom::PredefinedAtom, - function::{Constructor, IntoJsFunc}, - object::Property, - prelude::Func, - Array, Coerced, Ctx, Error, Exception, FromJs, IntoAtom, IntoJs, Object, Result, Undefined, - Value, -}; - -use crate::llrt_utils::primordials::{BasePrimordials, Primordial}; - -pub trait ObjectExt<'js> { - fn get_optional + Clone, V: FromJs<'js>>(&self, k: K) -> Result>; - fn get_required, V: FromJs<'js>>( - &self, - k: K, - object_name: &'static str, - ) -> Result; - fn into_object_or_throw(self, ctx: &Ctx<'js>, object_name: &'static str) - -> Result>; -} - -impl<'js> ObjectExt<'js> for Object<'js> { - fn get_optional + Clone, V: FromJs<'js> + Sized>( - &self, - k: K, - ) -> Result> { - self.get::>(k) - } - - fn get_required, V: FromJs<'js>>( - &self, - k: K, - object_name: &'static str, - ) -> Result { - let k = k.as_ref(); - self.get::<&str, Option>(k)?.ok_or_else(|| { - Exception::throw_type( - self.ctx(), - &[object_name, " '", k, "' property required"].concat(), - ) - }) - } - - fn into_object_or_throw(self, _: &Ctx<'js>, _: &'static str) -> Result> { - Ok(self) - } -} - -impl<'js> ObjectExt<'js> for Value<'js> { - fn get_optional + Clone, V: FromJs<'js>>(&self, k: K) -> Result> { - if let Some(obj) = self.as_object() { - return obj.get_optional(k); - } - Ok(None) - } - - fn get_required, V: FromJs<'js>>( - &self, - k: K, - object_name: &'static str, - ) -> Result { - self.as_object() - .ok_or_else(|| not_a_object_error(self.ctx(), object_name))? - .get_required(k, object_name) - } - - fn into_object_or_throw( - self, - ctx: &Ctx<'js>, - object_name: &'static str, - ) -> Result> { - self.into_object() - .ok_or_else(|| not_a_object_error(ctx, object_name)) - } -} - -pub fn not_a_object_error(ctx: &Ctx<'_>, object_name: &str) -> Error { - Exception::throw_type(ctx, &[object_name, " is not an object"].concat()) -} - -pub struct Proxy<'js> { - target: Value<'js>, - options: Object<'js>, -} - -impl<'js> IntoJs<'js> for Proxy<'js> { - fn into_js(self, ctx: &Ctx<'js>) -> Result> { - BasePrimordials::get(ctx)? - .constructor_proxy - .construct::<_, Value>((self.target, self.options)) - } -} - -impl<'js> Proxy<'js> { - pub fn new(ctx: Ctx<'js>) -> Result { - let options = Object::new(ctx.clone())?; - Ok(Self { - target: Undefined.into_value(ctx), - options, - }) - } - - pub fn with_target(ctx: Ctx<'js>, target: Value<'js>) -> Result { - let options = Object::new(ctx)?; - Ok(Self { target, options }) - } - - pub fn setter(&self, setter: Func) -> Result<()> - where - T: IntoJsFunc<'js, P> + 'js, - { - self.options.set(PredefinedAtom::Setter, setter)?; - Ok(()) - } - - pub fn getter(&self, getter: Func) -> Result<()> - where - T: IntoJsFunc<'js, P> + 'js, - { - self.options.set(PredefinedAtom::Getter, getter)?; - Ok(()) - } -} - -pub fn array_to_btree_map<'js>( - ctx: &Ctx<'js>, - array: Array<'js>, -) -> Result>> { - let value = object_from_entries(ctx, array)?; - let value = value.into_value(); - BTreeMap::from_js(ctx, value) -} - -pub fn object_from_entries<'js>(ctx: &Ctx<'js>, array: Array<'js>) -> Result> { - let obj = Object::new(ctx.clone())?; - for value in array.into_iter().flatten() { - if let Some(entry) = value.as_array() { - if let Ok(key) = entry.get::(0) { - if let Ok(value) = entry.get::(1) { - let _ = obj.set(key, value); //ignore result of failed - } - } - } - } - Ok(obj) -} - -/// Build a constructor that behaves like `class Name extends Parent` -pub fn define_subclass<'js, F, P>( - ctx: &Ctx<'js>, - name: &str, - parent: &Constructor<'js>, - construct: F, -) -> Result> -where - F: IntoJsFunc<'js, P> + 'js, -{ - let parent_proto: Object = parent.get(PredefinedAtom::Prototype)?; - let proto = Object::new(ctx.clone())?; - proto.set_prototype(Some(&parent_proto))?; - let constructor = Constructor::new_prototype(ctx, proto, construct)?; - constructor.set_prototype(parent.as_object())?; - constructor.prop(PredefinedAtom::Name, Property::from(name).configurable())?; - Ok(constructor) -} diff --git a/stdlib/src/llrt/llrt_utils/option.rs b/stdlib/src/llrt/llrt_utils/option.rs deleted file mode 100644 index 95994fbb..00000000 --- a/stdlib/src/llrt/llrt_utils/option.rs +++ /dev/null @@ -1,102 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use rquickjs::{ - class::{Trace, Tracer}, - function::{FromParam, ParamRequirement, ParamsAccessor}, - Ctx, FromJs, IntoJs, JsLifetime, Result, Type, Value, -}; - -/// Helper type for treating an undefined value as None, without treating null as None -#[derive(Clone)] -pub struct Undefined(pub Option); - -impl<'js, T: FromJs<'js>> FromJs<'js> for Undefined { - fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { - if value.type_of() == Type::Undefined { - Ok(Self(None)) - } else { - Ok(Self(Some(FromJs::from_js(ctx, value)?))) - } - } -} - -impl<'js, T: IntoJs<'js>> IntoJs<'js> for Undefined { - fn into_js(self, ctx: &Ctx<'js>) -> Result> { - match self.0 { - None => Ok(Value::new_undefined(ctx.clone())), - Some(val) => val.into_js(ctx), - } - } -} - -impl Default for Undefined { - fn default() -> Self { - Self(None) - } -} - -unsafe impl<'js, T: JsLifetime<'js>> JsLifetime<'js> for Undefined { - type Changed<'to> = Undefined>; -} - -impl<'js, T: Trace<'js>> Trace<'js> for Undefined { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - self.0.trace(tracer) - } -} - -/// Helper type for converting an None into null instead of undefined. -/// Needed while rquickjs::function::Null has no IntoJs implementation -#[derive(Clone)] -pub struct Null(pub Option); - -impl<'js, T: FromJs<'js>> FromJs<'js> for Null { - fn from_js(ctx: &Ctx<'js>, value: Value<'js>) -> Result { - if value.type_of() == Type::Null { - Ok(Self(None)) - } else { - Ok(Self(Some(FromJs::from_js(ctx, value)?))) - } - } -} - -impl<'js, T: IntoJs<'js>> IntoJs<'js> for Null { - fn into_js(self, ctx: &Ctx<'js>) -> Result> { - match self.0 { - None => Ok(Value::new_null(ctx.clone())), - Some(val) => val.into_js(ctx), - } - } -} - -unsafe impl<'js, T: JsLifetime<'js>> JsLifetime<'js> for Null { - type Changed<'to> = Null>; -} - -impl<'js, T: Trace<'js>> Trace<'js> for Null { - fn trace<'a>(&self, tracer: Tracer<'a, 'js>) { - self.0.trace(tracer) - } -} - -/// Helper type for accepting no value, or null, but considering undefined as a value -pub struct NullableOpt(pub Option); - -impl<'js, T: FromJs<'js>> FromParam<'js> for NullableOpt { - fn param_requirement() -> ParamRequirement { - ParamRequirement::optional() - } - - fn from_param<'a>(params: &mut ParamsAccessor<'a, 'js>) -> Result { - if !params.is_empty() { - let arg = params.arg(); - if arg.is_null() { - Ok(NullableOpt(None)) - } else { - let ctx = params.ctx().clone(); - Ok(NullableOpt(Some(T::from_js(&ctx, arg)?))) - } - } else { - Ok(NullableOpt(None)) - } - } -} diff --git a/stdlib/src/llrt/llrt_utils/primordials.rs b/stdlib/src/llrt/llrt_utils/primordials.rs deleted file mode 100644 index dd50dc59..00000000 --- a/stdlib/src/llrt/llrt_utils/primordials.rs +++ /dev/null @@ -1,166 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use std::any::type_name; - -use rquickjs::{ - atom::PredefinedAtom, function::Constructor, runtime::UserDataGuard, Ctx, Exception, Function, - JsLifetime, Object, Result, -}; - -use crate::llrt_utils::result::ResultExt; - -#[derive(JsLifetime)] -pub struct BasePrimordials<'js> { - // Constructors - pub constructor_map: Constructor<'js>, - pub constructor_set: Constructor<'js>, - pub constructor_date: Constructor<'js>, - pub constructor_error: Constructor<'js>, - pub constructor_type_error: Constructor<'js>, - pub constructor_range_error: Constructor<'js>, - pub constructor_regexp: Constructor<'js>, - pub constructor_uint8array: Constructor<'js>, - pub constructor_array_buffer: Constructor<'js>, - pub constructor_proxy: Constructor<'js>, - pub constructor_object: Constructor<'js>, - pub constructor_bool: Constructor<'js>, - pub constructor_number: Constructor<'js>, - pub constructor_string: Constructor<'js>, - - // Prototypes - pub prototype_object: Object<'js>, - pub prototype_date: Object<'js>, - pub prototype_regexp: Object<'js>, - pub prototype_set: Object<'js>, - pub prototype_map: Object<'js>, - pub prototype_error: Object<'js>, - - // Functions - pub function_array_from: Function<'js>, - pub function_array_buffer_is_view: Function<'js>, - pub function_get_own_property_descriptor: Function<'js>, - pub function_reflect_own_keys: Function<'js>, - pub function_parse_int: Function<'js>, - pub function_parse_float: Function<'js>, - pub prototype_iterator: Object<'js>, -} - -pub trait Primordial<'js> -where - Self: Sized + JsLifetime<'js>, -{ - fn get<'a>(ctx: &'a Ctx<'js>) -> Result> { - let userdata = ctx.userdata::().or_throw_msg( - ctx, - &[ - "Userdata of ", - type_name::(), - " not initialized. Call init(&ctx) on this type.", - ] - .concat(), - )?; - - Ok(userdata) - } - - fn init<'a>(ctx: &'a Ctx<'js>) -> Result<()> { - if ctx.userdata::().is_none() { - let primoridals = Self::new(ctx)?; - let _ = ctx.store_userdata(primoridals); - } - - Ok(()) - } - fn new(ctx: &Ctx<'js>) -> Result; -} - -impl<'js> Primordial<'js> for BasePrimordials<'js> { - fn new(ctx: &Ctx<'js>) -> Result { - let globals = ctx.globals(); - - let constructor_object: Constructor = globals.get(PredefinedAtom::Object)?; - let prototype_object: Object = constructor_object.get(PredefinedAtom::Prototype)?; - - let constructor_proxy: Constructor = globals.get(PredefinedAtom::Proxy)?; - - let function_get_own_property_descriptor: Function = - constructor_object.get(PredefinedAtom::GetOwnPropertyDescriptor)?; - - let constructor_date: Constructor = globals.get(PredefinedAtom::Date)?; - let prototype_date: Object = constructor_date.get(PredefinedAtom::Prototype)?; - - let constructor_map: Constructor = globals.get(PredefinedAtom::Map)?; - let prototype_map: Object = constructor_map.get(PredefinedAtom::Prototype)?; - - let constructor_set: Constructor = globals.get(PredefinedAtom::Set)?; - let prototype_set: Object = constructor_set.get(PredefinedAtom::Prototype)?; - - let constructor_regexp: Constructor = globals.get(PredefinedAtom::RegExp)?; - let prototype_regexp: Object = constructor_regexp.get(PredefinedAtom::Prototype)?; - - let constructor_uint8array: Constructor = globals.get(PredefinedAtom::Uint8Array)?; - let constructor_arraybuffer: Constructor = globals.get(PredefinedAtom::ArrayBuffer)?; - - let constructor_error: Constructor = globals.get(PredefinedAtom::Error)?; - let constructor_type_error: Constructor = ctx.globals().get(PredefinedAtom::TypeError)?; - let constructor_range_error: Constructor = ctx.globals().get(PredefinedAtom::RangeError)?; - let prototype_error: Object = constructor_error.get(PredefinedAtom::Prototype)?; - - let constructor_array: Object = globals.get(PredefinedAtom::Array)?; - let function_array_from: Function = constructor_array.get(PredefinedAtom::From)?; - - let constructor_array_buffer: Object = globals.get(PredefinedAtom::ArrayBuffer)?; - let function_array_buffer_is_view: Function = constructor_array_buffer.get("isView")?; - - let constructor_bool: Constructor = globals.get(PredefinedAtom::Boolean)?; - - let constructor_number: Constructor = globals.get(PredefinedAtom::Number)?; - let function_parse_float: Function = constructor_number.get("parseFloat")?; - let function_parse_int: Function = constructor_number.get("parseInt")?; - - let constructor_string: Constructor = globals.get(PredefinedAtom::String)?; - - let reflect: Object = globals.get("Reflect")?; - let function_reflect_own_keys: Function = reflect.get("ownKeys")?; - - // Walk to %IteratorPrototype% via an array iterator. - let array = rquickjs::Array::new(ctx.clone())?; - let iter_fn: Function = array - .as_object() - .get(rquickjs::atom::PredefinedAtom::SymbolIterator)?; - let array_iter: Object = iter_fn.call((rquickjs::function::This(array),))?; - let prototype_iterator = array_iter - .get_prototype() - .and_then(|p| p.get_prototype()) - .ok_or_else(|| Exception::throw_internal(ctx, "missing %IteratorPrototype%"))?; - - Ok(Self { - constructor_map, - constructor_set, - constructor_date, - constructor_proxy, - constructor_error, - constructor_type_error, - constructor_range_error, - constructor_regexp, - constructor_uint8array, - constructor_array_buffer: constructor_arraybuffer, - constructor_object, - constructor_bool, - constructor_number, - constructor_string, - prototype_object, - prototype_date, - prototype_regexp, - prototype_set, - prototype_map, - prototype_error, - function_array_from, - function_array_buffer_is_view, - function_get_own_property_descriptor, - function_reflect_own_keys, - function_parse_float, - function_parse_int, - prototype_iterator, - }) - } -} diff --git a/stdlib/src/llrt/llrt_utils/provider.rs b/stdlib/src/llrt/llrt_utils/provider.rs deleted file mode 100644 index db99b88d..00000000 --- a/stdlib/src/llrt/llrt_utils/provider.rs +++ /dev/null @@ -1,25 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -#[derive(PartialEq)] -pub enum ProviderType { - None, - Resource(String), // Custom asynchronous resource - // Userland provider types - Immediate, // [Immediate] Processing by setImmediate() - Interval, // [Interval] Timer by setInterval() - MessagePort, // [MessagePort] Port for worker_threads - Microtask, // [Microtask] Processing by queueMicrotask() - TickObject, // [TickObject] Processing by process.nextTick() - Timeout, // [Timeout] Timer by setTimeout() - // Internal provider types - FsReqCallback, // [FSREQCALLBACK] Callback for file system operations - GetAddrInfoReqWrap, // [GETADDRINFOREQWRAP] When resolving DNS (dns.lookup(), etc.) - GetNameInfoReqWrap, // [GETNAMEINFOREQWRAP] DNS reverse lookup - PipeWrap, // [PIPEWRAP] Pipe connection - StatWatcher, // [STATWACHER] File monitoring such as fs.watch() - TcpWrap, // [TCPWRAP] TCP socket wrap (net.Socket, etc.) - TimerWrap, // [TIMERWRAP] Internal timer wrap (low level) - TlsWrap, // [TLSWRAP] TLS socket (HTTPS, etc.) - UdpWrap, // [UDPWRAP] UDP socket wrap (dgram module) -} diff --git a/stdlib/src/llrt/llrt_utils/result.rs b/stdlib/src/llrt/llrt_utils/result.rs deleted file mode 100644 index 91364ce9..00000000 --- a/stdlib/src/llrt/llrt_utils/result.rs +++ /dev/null @@ -1,105 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -#![allow(clippy::uninlined_format_args)] - -use std::{fmt::Write, result::Result as StdResult}; - -use rquickjs::{Ctx, Exception, Result}; - -pub trait ResultExt { - fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> Result; - fn or_throw_range(self, ctx: &Ctx, msg: &str) -> Result; - fn or_throw_type(self, ctx: &Ctx, msg: &str) -> Result; - fn or_throw(self, ctx: &Ctx) -> Result; -} - -pub trait OptionExt { - fn and_then_ok(self, f: F) -> StdResult, E> - where - F: FnOnce(T) -> StdResult, E>; - - fn unwrap_or_else_ok(self, f: F) -> StdResult - where - F: FnOnce() -> StdResult; -} - -impl ResultExt for StdResult { - fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> Result { - self.map_err(|e| { - let mut message = String::with_capacity(100); - message.push_str(msg); - message.push_str(". "); - write!(message, "{}", e).unwrap(); - Exception::throw_message(ctx, &message) - }) - } - - fn or_throw_range(self, ctx: &Ctx, msg: &str) -> Result { - self.map_err(|e| { - let mut message = String::with_capacity(100); - if !message.is_empty() { - message.push_str(msg); - message.push_str(". "); - } - write!(message, "{}", e).unwrap(); - Exception::throw_range(ctx, &message) - }) - } - - fn or_throw_type(self, ctx: &Ctx, msg: &str) -> Result { - self.map_err(|e| { - let mut message = String::with_capacity(100); - if !msg.is_empty() { - message.push_str(msg); - message.push_str(". "); - } - write!(message, "{}", e).unwrap(); - Exception::throw_type(ctx, &message) - }) - } - - fn or_throw(self, ctx: &Ctx) -> Result { - self.map_err(|err| Exception::throw_message(ctx, &err.to_string())) - } -} - -impl ResultExt for Option { - fn or_throw_msg(self, ctx: &Ctx, msg: &str) -> Result { - self.ok_or_else(|| Exception::throw_message(ctx, msg)) - } - - fn or_throw_range(self, ctx: &Ctx, msg: &str) -> Result { - self.ok_or_else(|| Exception::throw_range(ctx, msg)) - } - - fn or_throw_type(self, ctx: &Ctx, msg: &str) -> Result { - self.ok_or_else(|| Exception::throw_type(ctx, msg)) - } - - fn or_throw(self, ctx: &Ctx) -> Result { - self.ok_or_else(|| Exception::throw_message(ctx, "Value is not present")) - } -} - -impl OptionExt for Option { - fn and_then_ok(self, f: F) -> StdResult, E> - where - F: FnOnce(T) -> StdResult, E>, - { - match self { - Some(v) => f(v), - None => Ok(None), - } - } - - fn unwrap_or_else_ok(self, f: F) -> StdResult - where - F: FnOnce() -> StdResult, - { - match self { - Some(v) => Ok(v), - None => f(), - } - } -} diff --git a/stdlib/src/llrt/llrt_utils/reuse_list.rs b/stdlib/src/llrt/llrt_utils/reuse_list.rs deleted file mode 100644 index 624126db..00000000 --- a/stdlib/src/llrt/llrt_utils/reuse_list.rs +++ /dev/null @@ -1,338 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use std::fmt::Debug; - -#[derive(Default, Clone)] -pub struct ReuseList { - items: Vec>, - slots: Vec, - last_slot_idx: usize, - len: usize, - slot_size: usize, -} - -impl Debug for ReuseList { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ReuseList") - .field("items", &self.items) - .field("slots", &self.slots) - .finish() - } -} - -impl ReuseList { - pub fn new() -> Self { - Self::with_capacity(0) - } - - //is empty - pub fn is_empty(&self) -> bool { - self.len == 0 - } - - //create a with capacity - pub fn with_capacity(capacity: usize) -> Self { - Self { - items: Vec::with_capacity(capacity), - slots: Vec::with_capacity(capacity >> 2), - last_slot_idx: 0, - len: 0, - slot_size: 0, - } - } - - pub fn append(&mut self, item: T) -> usize { - if self.slot_size > 0 { - //reuse empty slot if valid - let slot = self.slots[self.last_slot_idx - 1]; - if slot > 0 { - self.items[slot - 1] = Some(item); - self.slots[self.last_slot_idx - 1] = 0; - if self.last_slot_idx > 1 { - self.last_slot_idx -= 1; - } - - self.len += 1; - return slot - 1; - } - } - //no valid empty slots, append to end - self.items.push(Some(item)); - self.len += 1; - self.items.len() - 1 - } - - pub fn remove(&mut self, index: usize) -> Option { - if index >= self.items.len() { - return None; - } - - let item = self.items[index].take(); - if item.is_some() { - if self.slot_size > 0 && self.slots[self.last_slot_idx - 1] == 0 { - self.slots[self.last_slot_idx - 1] = index + 1; - } else { - self.slots.push(index + 1); - self.last_slot_idx += 1; - self.slot_size += 1; - } - self.len -= 1; - } - item - } - - pub fn get(&self, index: usize) -> Option<&T> { - if index >= self.items.len() { - None - } else { - self.items[index].as_ref() - } - } - - pub fn get_mut(&mut self, index: usize) -> Option<&mut T> { - if index >= self.items.len() { - None - } else { - self.items[index].as_mut() - } - } - - pub fn capacity(&self) -> usize { - self.items.capacity() - } - - pub fn len(&self) -> usize { - self.len - } - - pub fn iter(&self) -> impl Iterator { - self.items.iter().filter_map(|x| x.as_ref()) - } - - pub fn iter_mut(&mut self) -> impl Iterator { - self.items.iter_mut().filter_map(|x| x.as_mut()) - } - - //implement clear - pub fn clear(&mut self) { - self.items.clear(); - self.slots.clear(); - self.last_slot_idx = 0; - self.len = 0; - self.slot_size = 0; - } - - pub fn optimize(&mut self) { - let mut new_items = Vec::with_capacity(self.len); - - for item in self.items.iter_mut() { - let a = item.take(); - if a.is_some() { - new_items.push(a); - } - } - self.items = new_items; - self.slots.clear(); - self.last_slot_idx = 0; - self.slot_size = 0; - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_new() { - let list: ReuseList = ReuseList::new(); - assert_eq!(list.len(), 0); - assert_eq!(list.capacity(), 0); - assert_eq!(list.items.len(), 0); - assert_eq!(list.slots.len(), 0); - } - - #[test] - fn test_with_capacity() { - let list: ReuseList = ReuseList::with_capacity(10); - assert_eq!(list.len(), 0); - assert_eq!(list.capacity(), 10); - assert_eq!(list.items.len(), 0); - assert_eq!(list.slots.len(), 0); - } - - #[test] - fn test_append() { - let mut list = ReuseList::new(); - assert_eq!(list.append(1), 0); - assert_eq!(list.append(2), 1); - assert_eq!(list.append(3), 2); - assert_eq!(list.len(), 3); - - let items: Vec = list.iter().cloned().collect(); - assert_eq!(items, vec![1, 2, 3]); - assert_eq!(list.items, vec![Some(1), Some(2), Some(3)]); - assert_eq!(list.slots, vec![]); - } - - #[test] - fn test_remove() { - let mut list = ReuseList::new(); - list.append(1); - list.append(2); - list.append(3); - - assert_eq!(list.remove(1), Some(2)); - assert_eq!(list.len(), 2); - - let items: Vec = list.iter().cloned().collect(); - assert_eq!(items, vec![1, 3]); - assert_eq!(list.items, vec![Some(1), None, Some(3)]); - assert_eq!(list.slots, vec![2]); - - assert_eq!(list.remove(5), None); - } - - #[test] - fn test_reuse_slots() { - let mut list = ReuseList::new(); - list.append(1); - list.append(2); - list.append(3); - - list.remove(1); // Remove 2 - assert_eq!(list.append(4), 1); // Should reuse index 1 - - let items: Vec = list.iter().cloned().collect(); - assert_eq!(items, vec![1, 4, 3]); - assert_eq!(list.items, vec![Some(1), Some(4), Some(3)]); - assert_eq!(list.slots, vec![0]); - } - - #[test] - fn test_get() { - let mut list = ReuseList::new(); - list.append(1); - list.append(2); - - assert_eq!(list.get(0), Some(&1)); - assert_eq!(list.get(1), Some(&2)); - assert_eq!(list.get(2), None); - assert_eq!(list.items, vec![Some(1), Some(2)]); - assert_eq!(list.slots, vec![]); - } - - #[test] - fn test_get_mut() { - let mut list = ReuseList::new(); - list.append(1); - list.append(2); - - if let Some(value) = list.get_mut(0) { - *value = 10; - } - - assert_eq!(list.get(0), Some(&10)); - assert_eq!(list.items, vec![Some(10), Some(2)]); - assert_eq!(list.slots, vec![]); - } - - #[test] - fn test_iter() { - let mut list = ReuseList::new(); - list.append(1); - list.append(2); - list.append(3); - list.remove(1); - - let items: Vec = list.iter().cloned().collect(); - assert_eq!(items, vec![1, 3]); - assert_eq!(list.items, vec![Some(1), None, Some(3)]); - assert_eq!(list.slots, vec![2]); - } - - #[test] - fn test_iter_mut() { - let mut list = ReuseList::new(); - list.append(1); - list.append(2); - list.append(3); - - for item in list.iter_mut() { - *item *= 2; - } - - let items: Vec = list.iter().cloned().collect(); - assert_eq!(items, vec![2, 4, 6]); - assert_eq!(list.items, vec![Some(2), Some(4), Some(6)]); - assert_eq!(list.slots, vec![]); - } - - #[test] - fn test_multiple_removes() { - let mut list = ReuseList::new(); - for i in 0..5 { - list.append(i); - } - - list.remove(1); - list.remove(3); - - let items: Vec = list.iter().cloned().collect(); - assert_eq!(items, vec![0, 2, 4]); - assert_eq!(list.items, vec![Some(0), None, Some(2), None, Some(4)]); - assert_eq!(list.slots, vec![2, 4]); - - // Test reuse of both slots - list.append(10); - list.append(11); - - let items: Vec = list.iter().cloned().collect(); - assert_eq!(items, vec![0, 11, 2, 10, 4]); - assert_eq!( - list.items, - vec![Some(0), Some(11), Some(2), Some(10), Some(4)] - ); - assert_eq!(list.slots, vec![0, 0]); - - list.remove(0); - - let items: Vec = list.iter().cloned().collect(); - assert_eq!(items, vec![11, 2, 10, 4]); - assert_eq!(list.items, vec![None, Some(11), Some(2), Some(10), Some(4)]); - assert_eq!(list.slots, vec![1, 0]); - - list.append(20); - - let items: Vec = list.iter().cloned().collect(); - assert_eq!(items, vec![20, 11, 2, 10, 4]); - assert_eq!( - list.items, - vec![Some(20), Some(11), Some(2), Some(10), Some(4)] - ); - assert_eq!(list.slots, vec![0, 0]); - - //remove all items - list.clear(); - } - - #[test] - fn test_optimize() { - let mut list = ReuseList::new(); - list.append(1); - list.append(2); - list.append(3); - list.remove(1); - - assert_eq!(list.items, vec![Some(1), None, Some(3)]); - assert_eq!(list.slots, vec![2]); - - list.optimize(); - - assert_eq!(list.items, vec![Some(1), Some(3)]); - assert_eq!(list.slots, vec![]); - assert_eq!(list.last_slot_idx, 0); - assert_eq!(list.slot_size, 0); - - let items: Vec = list.iter().cloned().collect(); - assert_eq!(items, vec![1, 3]); - } -} diff --git a/stdlib/src/llrt/llrt_utils/signals.rs b/stdlib/src/llrt/llrt_utils/signals.rs deleted file mode 100644 index 47a1bb8d..00000000 --- a/stdlib/src/llrt/llrt_utils/signals.rs +++ /dev/null @@ -1,150 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use rquickjs::{prelude::Opt, Ctx, Exception, Result, Value}; - -use crate::llrt_utils::result::ResultExt; -use std::io; - -#[cfg(unix)] -macro_rules! generate_signal_from_str_fn { - ($($signal:path),*) => { - pub fn signal_from_str(signal: &str) -> Option { - let signal = ["libc::", signal].concat(); - match signal.as_str() { - $(stringify!($signal) => Some($signal),)* - _ => None, - } - } - - pub fn signal_str_from_i32(signal: i32) -> Option<&'static str> { - $(if signal == $signal { - return Some(&stringify!($signal)[6..]); - })* - None - } - }; -} - -#[cfg(unix)] -generate_signal_from_str_fn!( - libc::SIGHUP, - libc::SIGINT, - libc::SIGQUIT, - libc::SIGILL, - libc::SIGABRT, - libc::SIGFPE, - libc::SIGKILL, - libc::SIGSEGV, - libc::SIGPIPE, - libc::SIGALRM, - libc::SIGTERM -); - -#[cfg(not(unix))] -static WINDOWS_SIGTERM: i32 = -1; - -pub fn parse_signal(signal: Option>) -> Result { - let Some(val) = signal else { - #[cfg(unix)] - return Ok(libc::SIGTERM); - #[cfg(not(unix))] - return Ok(WINDOWS_SIGTERM); - }; - - if let Some(num) = val.as_number() { - let sig = num as i32; - #[cfg(unix)] - return Ok(sig); - // On Windows: 0 checks existence, anything else kills - #[cfg(not(unix))] - return Ok(if sig == 0 { 0 } else { WINDOWS_SIGTERM }); - } - - if let Some(str_val) = val.as_string() { - let s = str_val.to_string()?; - - #[cfg(unix)] - let mapped_sig = signal_from_str(&s); - - #[cfg(not(unix))] - let mapped_sig = match s.as_str() { - "SIGINT" | "SIGTERM" | "SIGKILL" | "SIGQUIT" | "SIGHUP" | "SIGUSR1" => { - Some(WINDOWS_SIGTERM) - } - _ => None, - }; - - return match mapped_sig { - Some(sig) => Ok(sig), - None => Err(Exception::throw_type( - val.ctx(), - &format!("Unknown signal: {}", s), - )), - }; - } - - Err(Exception::throw_type(val.ctx(), "Invalid signal type")) -} - -#[cfg(unix)] -pub fn kill_process_raw(pid: u32, signal: i32) -> io::Result<()> { - // libc::kill returns 0 on success, -1 on error - // SAFETY: kill is a safe system call as long as the signal value is valid, which is ensured by parse_signal - if unsafe { libc::kill(pid as i32, signal) } == 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } -} - -#[cfg(windows)] -pub fn kill_process_raw(pid: u32, signal: i32) -> io::Result<()> { - use windows_sys::Win32::Foundation::CloseHandle; - use windows_sys::Win32::System::Threading::{OpenProcess, TerminateProcess, PROCESS_TERMINATE}; - - // SAFETY: OpenProcess is safe to call with valid parameters, and PROCESS_TERMINATE is a valid access right - let handle = unsafe { OpenProcess(PROCESS_TERMINATE, 0, pid) }; - if handle == std::ptr::null_mut() { - return Err(io::Error::last_os_error()); - } - - let result = if signal == 0 { - Ok(()) - } else { - // SAFETY: TerminateProcess is safe to call with a valid process handle obtained from OpenProcess - if unsafe { TerminateProcess(handle, 1) } != 0 { - Ok(()) - } else { - Err(io::Error::last_os_error()) - } - }; - - // SAFETY: CloseHandle is safe to call with a valid handle obtained from OpenProcess - unsafe { CloseHandle(handle) }; - result -} - -pub fn kill(ctx: &Ctx<'_>, pid: u32, signal: Opt>) -> Result { - let signal = parse_signal(signal.0)?; - - kill_process_raw(pid, signal) - .map(|_| true) - .or_else(|e| { - // Handle "Process Not Found" / "Existence Check" logic - // If signal is 0 (check existence) and we hit a specific error, return Ok(false). - - #[cfg(unix)] - let is_not_found = e.raw_os_error() == Some(libc::ESRCH); // Error 3 - - #[cfg(windows)] - let is_not_found = true; // On Windows, any OpenProcess failure during check implies "not found" (or not accessible) - - if signal == 0 && is_not_found { - Ok(false) - } else { - Err(e) - } - }) - .or_throw(ctx) -} diff --git a/stdlib/src/llrt/llrt_utils/string.rs b/stdlib/src/llrt/llrt_utils/string.rs deleted file mode 100644 index d066cbe9..00000000 --- a/stdlib/src/llrt/llrt_utils/string.rs +++ /dev/null @@ -1,27 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use rquickjs::{Coerced, Result, Value}; - -#[inline] -pub fn get_string(value: &Value<'_>) -> Result> { - if let Some(val) = value.as_string() { - let string = val.to_string()?; - return Ok(Some(string)); - } - Ok(None) -} - -pub fn get_coerced_string(value: &Value<'_>) -> Option { - if let Ok(val) = value.get::>() { - return Some(val.0); - }; - None -} - -pub fn get_coerced_defined_string<'js>(value: &Option>) -> Option { - if let Some(value) = value { - if !value.is_undefined() { - return get_coerced_string(value); - } - }; - None -} diff --git a/stdlib/src/llrt/llrt_utils/sysinfo.rs b/stdlib/src/llrt/llrt_utils/sysinfo.rs deleted file mode 100644 index f10c3e65..00000000 --- a/stdlib/src/llrt/llrt_utils/sysinfo.rs +++ /dev/null @@ -1,14 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -#[cfg(target_os = "macos")] -pub const PLATFORM: &str = "darwin"; -#[cfg(target_os = "windows")] -pub const PLATFORM: &str = "win32"; -#[cfg(not(any(target_os = "macos", target_os = "windows")))] -pub const PLATFORM: &str = std::env::consts::OS; - -#[cfg(any(target_arch = "x86_64", target_arch = "x86"))] -pub const ARCH: &str = "x64"; -#[cfg(target_arch = "aarch64")] -pub const ARCH: &str = "arm64"; -#[cfg(not(any(target_arch = "x86_64", target_arch = "x86", target_arch = "aarch64")))] -pub const ARCH: &str = std::env::consts::ARCH; diff --git a/stdlib/src/llrt/llrt_utils/time.rs b/stdlib/src/llrt/llrt_utils/time.rs deleted file mode 100644 index 2998cd07..00000000 --- a/stdlib/src/llrt/llrt_utils/time.rs +++ /dev/null @@ -1,48 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -use std::{ - sync::atomic::{AtomicU64, Ordering}, - time::SystemTime, -}; - -static TIME_ORIGIN: AtomicU64 = AtomicU64::new(0); - -/// Get the current time in nanoseconds. -/// -/// # Safety -/// - Good until the year 2554 -/// - Always use a checked substraction since this can return 0 -pub fn now_nanos() -> u64 { - SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_nanos() as u64 -} - -/// Get the current time in millis. -/// -/// # Safety -/// - Good until the year 2554 -/// - Always use a checked substraction since this can return 0 -pub fn now_millis() -> i64 { - SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as i64 -} - -/// Get the origin time in nanoseconds. -/// -/// # Safety -/// - Good until the year 2554 -/// - Always use a checked substraction since this can return 0 -pub fn origin_nanos() -> u64 { - TIME_ORIGIN.load(Ordering::Relaxed) -} - -// For accuracy reasons, this function should be executed when the vm is initialized -pub fn init() { - if TIME_ORIGIN.load(Ordering::Relaxed) == 0 { - let time_origin = now_nanos(); - TIME_ORIGIN.store(time_origin, Ordering::Relaxed) - } -} diff --git a/stdlib/src/llrt/llrt_zlib/brotli.rs b/stdlib/src/llrt/llrt_zlib/brotli.rs deleted file mode 100644 index 2d74a48e..00000000 --- a/stdlib/src/llrt/llrt_zlib/brotli.rs +++ /dev/null @@ -1,50 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use crate::llrt_buffer::Buffer; -use crate::llrt_context::CtxExtension; -use crate::llrt_utils::{bytes::ObjectBytes, result::ResultExt}; -use rquickjs::{ - prelude::{Opt, Rest}, - Ctx, Error, Exception, Function, IntoJs, Null, Result, Value, -}; - -use super::{define_cb_function, define_sync_function, max_output_length, read_to_end_limited}; - -enum BrotliCommand { - Compress, - Decompress, -} - -fn brotli_converter<'js>( - ctx: Ctx<'js>, - bytes: ObjectBytes<'js>, - options: Opt>, - command: BrotliCommand, -) -> Result> { - let src = bytes.as_bytes(&ctx)?; - let limit = max_output_length(&options)?; - - let dst = match command { - BrotliCommand::Compress => read_to_end_limited( - &ctx, - crate::llrt_compression::brotli::encoder(src), - limit, - src.len(), - )?, - BrotliCommand::Decompress => read_to_end_limited( - &ctx, - crate::llrt_compression::brotli::decoder(src), - limit, - src.len(), - )?, - }; - - Buffer(dst).into_js(&ctx) -} - -define_cb_function!(br_comp, brotli_converter, BrotliCommand::Compress); -define_sync_function!(br_comp_sync, brotli_converter, BrotliCommand::Compress); - -define_cb_function!(br_decomp, brotli_converter, BrotliCommand::Decompress); -define_sync_function!(br_decomp_sync, brotli_converter, BrotliCommand::Decompress); diff --git a/stdlib/src/llrt/llrt_zlib/lib.rs b/stdlib/src/llrt/llrt_zlib/lib.rs deleted file mode 100644 index 91352f33..00000000 --- a/stdlib/src/llrt/llrt_zlib/lib.rs +++ /dev/null @@ -1,212 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use crate::llrt_utils::module::{export_default, ModuleInfo}; -use rquickjs::{ - function::Func, - module::{Declarations, Exports, ModuleDef}, - Ctx, Result, -}; - -mod brotli; -mod zlib; -mod zstd; - -use std::io::Read; - -use crate::llrt_utils::object::ObjectExt; -use rquickjs::{prelude::Opt, Exception, Value}; - -/// Reads the `maxOutputLength` option, which `node:zlib` uses to cap the output -/// of the convenience methods. -pub(crate) fn max_output_length<'js>(options: &Opt>) -> Result> { - match options.0.as_ref() { - Some(options) => options.get_optional::<_, usize>("maxOutputLength"), - None => Ok(None), - } -} - -/// Drains `reader` into a buffer, rejecting output longer than `limit` bytes -/// with the same `RangeError` Node.js raises for `maxOutputLength`. -/// -/// Reading stops one byte past the limit, so an over-long result is detected -/// without decompressing (or allocating) the rest of the payload. -pub(crate) fn read_to_end_limited( - ctx: &Ctx<'_>, - reader: R, - limit: Option, - capacity: usize, -) -> Result> { - let Some(limit) = limit else { - let mut dst = Vec::with_capacity(capacity); - let mut reader = reader; - reader.read_to_end(&mut dst)?; - return Ok(dst); - }; - - let cutoff = limit.saturating_add(1); - let mut dst = Vec::with_capacity(capacity.min(cutoff)); - reader.take(cutoff as u64).read_to_end(&mut dst)?; - - if dst.len() > limit { - return Err(Exception::throw_range( - ctx, - &[ - "Cannot create a Buffer larger than ", - &limit.to_string(), - " bytes", - ] - .concat(), - )); - } - - Ok(dst) -} - -use self::brotli::{br_comp, br_comp_sync, br_decomp, br_decomp_sync}; -use self::zlib::{ - deflate, deflate_raw, deflate_raw_sync, deflate_sync, gunzip, gunzip_sync, gzip, gzip_sync, - inflate, inflate_raw, inflate_raw_sync, inflate_sync, -}; -use self::zstd::{zstd_comp, zstd_comp_sync, zstd_decomp, zstd_decomp_sync}; - -#[macro_export] -macro_rules! define_sync_function { - ($fn_name:ident, $converter:expr, $command:expr) => { - pub(crate) fn $fn_name<'js>( - ctx: Ctx<'js>, - value: ObjectBytes<'js>, - options: Opt>, - ) -> Result> { - $converter(ctx.clone(), value, options, $command) - } - }; -} - -#[macro_export] -macro_rules! define_cb_function { - ($fn_name:ident, $converter:expr, $command:expr) => { - pub(crate) fn $fn_name<'js>( - ctx: Ctx<'js>, - value: ObjectBytes<'js>, - args: Rest>, - ) -> Result<()> { - let mut args_iter = args.0.into_iter().rev(); - let cb: Function = args_iter - .next() - .and_then(|v| v.into_function()) - .or_throw_msg(&ctx, "Callback parameter is not a function")?; - let options = match args_iter.next() { - Some(v) => Opt(Some(v)), - None => Opt(None), - }; - - ctx.clone().spawn_exit(async move { - match $converter(ctx.clone(), value, options, $command) { - Ok(obj) => { - () = cb.call((Null.into_js(&ctx), obj))?; - Ok::<_, Error>(()) - } - Err(err) => { - // `Error::Exception` is only a marker; the thrown value - // (and therefore the real message) lives in ctx.catch(). - let err = if matches!(err, Error::Exception) { - ctx.catch() - } else { - Exception::from_message(ctx.clone(), &err.to_string())?.into_value() - }; - () = cb.call((err,))?; - Ok(()) - } - } - })?; - Ok(()) - } - }; -} -pub struct ZlibModule; - -impl ModuleDef for ZlibModule { - fn declare(declare: &Declarations) -> Result<()> { - declare.declare("deflate")?; - declare.declare("deflateSync")?; - - declare.declare("deflateRaw")?; - declare.declare("deflateRawSync")?; - - declare.declare("gzip")?; - declare.declare("gzipSync")?; - - declare.declare("inflate")?; - declare.declare("inflateSync")?; - - declare.declare("inflateRaw")?; - declare.declare("inflateRawSync")?; - - declare.declare("gunzip")?; - declare.declare("gunzipSync")?; - - declare.declare("brotliCompress")?; - declare.declare("brotliCompressSync")?; - - declare.declare("brotliDecompress")?; - declare.declare("brotliDecompressSync")?; - - declare.declare("zstdCompress")?; - declare.declare("zstdCompressSync")?; - - declare.declare("zstdDecompress")?; - declare.declare("zstdDecompressSync")?; - - declare.declare("default")?; - Ok(()) - } - - fn evaluate<'js>(ctx: &Ctx<'js>, exports: &Exports<'js>) -> Result<()> { - export_default(ctx, exports, |default| { - default.set("deflate", Func::from(deflate))?; - default.set("deflateSync", Func::from(deflate_sync))?; - - default.set("deflateRaw", Func::from(deflate_raw))?; - default.set("deflateRawSync", Func::from(deflate_raw_sync))?; - - default.set("gzip", Func::from(gzip))?; - default.set("gzipSync", Func::from(gzip_sync))?; - - default.set("inflate", Func::from(inflate))?; - default.set("inflateSync", Func::from(inflate_sync))?; - - default.set("inflateRaw", Func::from(inflate_raw))?; - default.set("inflateRawSync", Func::from(inflate_raw_sync))?; - - default.set("gunzip", Func::from(gunzip))?; - default.set("gunzipSync", Func::from(gunzip_sync))?; - - default.set("brotliCompress", Func::from(br_comp))?; - default.set("brotliCompressSync", Func::from(br_comp_sync))?; - - default.set("brotliDecompress", Func::from(br_decomp))?; - default.set("brotliDecompressSync", Func::from(br_decomp_sync))?; - - default.set("zstdCompress", Func::from(zstd_comp))?; - default.set("zstdCompressSync", Func::from(zstd_comp_sync))?; - - default.set("zstdDecompress", Func::from(zstd_decomp))?; - default.set("zstdDecompressSync", Func::from(zstd_decomp_sync))?; - - Ok(()) - }) - } -} - -impl From for ModuleInfo { - fn from(val: ZlibModule) -> Self { - ModuleInfo { - name: "zlib", - module: val, - } - } -} - -// Macro exports move to the combined crate root. -pub(crate) use crate::{define_cb_function, define_sync_function}; diff --git a/stdlib/src/llrt/llrt_zlib/zlib.rs b/stdlib/src/llrt/llrt_zlib/zlib.rs deleted file mode 100644 index a4a08a04..00000000 --- a/stdlib/src/llrt/llrt_zlib/zlib.rs +++ /dev/null @@ -1,97 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use crate::llrt_buffer::Buffer; -use crate::llrt_context::CtxExtension; -use crate::llrt_utils::{bytes::ObjectBytes, object::ObjectExt, result::ResultExt}; -use rquickjs::{ - prelude::{Opt, Rest}, - Ctx, Error, Exception, Function, IntoJs, Null, Result, Value, -}; - -use super::{define_cb_function, define_sync_function, max_output_length, read_to_end_limited}; - -enum ZlibCommand { - Deflate, - DeflateRaw, - Gzip, - Inflate, - InflateRaw, - Gunzip, -} - -fn zlib_converter<'js>( - ctx: Ctx<'js>, - bytes: ObjectBytes<'js>, - options: Opt>, - command: ZlibCommand, -) -> Result> { - let src = bytes.as_bytes(&ctx)?; - - let mut level = crate::llrt_compression::zlib::Compression::default(); - if let Some(options) = options.0.as_ref() { - if let Some(opt) = options.get_optional("level")? { - level = crate::llrt_compression::zlib::Compression::new(opt); - } - } - let limit = max_output_length(&options)?; - - let dst = match command { - ZlibCommand::Deflate => read_to_end_limited( - &ctx, - crate::llrt_compression::zlib::encoder(src, level), - limit, - src.len(), - )?, - ZlibCommand::DeflateRaw => read_to_end_limited( - &ctx, - crate::llrt_compression::deflate::encoder(src, level), - limit, - src.len(), - )?, - ZlibCommand::Gzip => read_to_end_limited( - &ctx, - crate::llrt_compression::gz::encoder(src, level), - limit, - src.len(), - )?, - ZlibCommand::Inflate => read_to_end_limited( - &ctx, - crate::llrt_compression::zlib::decoder(src), - limit, - src.len(), - )?, - ZlibCommand::InflateRaw => read_to_end_limited( - &ctx, - crate::llrt_compression::deflate::decoder(src), - limit, - src.len(), - )?, - ZlibCommand::Gunzip => read_to_end_limited( - &ctx, - crate::llrt_compression::gz::decoder(src), - limit, - src.len(), - )?, - }; - - Buffer(dst).into_js(&ctx) -} - -define_cb_function!(deflate, zlib_converter, ZlibCommand::Deflate); -define_sync_function!(deflate_sync, zlib_converter, ZlibCommand::Deflate); - -define_cb_function!(deflate_raw, zlib_converter, ZlibCommand::DeflateRaw); -define_sync_function!(deflate_raw_sync, zlib_converter, ZlibCommand::DeflateRaw); - -define_cb_function!(gzip, zlib_converter, ZlibCommand::Gzip); -define_sync_function!(gzip_sync, zlib_converter, ZlibCommand::Gzip); - -define_cb_function!(inflate, zlib_converter, ZlibCommand::Inflate); -define_sync_function!(inflate_sync, zlib_converter, ZlibCommand::Inflate); - -define_cb_function!(inflate_raw, zlib_converter, ZlibCommand::InflateRaw); -define_sync_function!(inflate_raw_sync, zlib_converter, ZlibCommand::InflateRaw); - -define_cb_function!(gunzip, zlib_converter, ZlibCommand::Gunzip); -define_sync_function!(gunzip_sync, zlib_converter, ZlibCommand::Gunzip); diff --git a/stdlib/src/llrt/llrt_zlib/zstd.rs b/stdlib/src/llrt/llrt_zlib/zstd.rs deleted file mode 100644 index 66677f46..00000000 --- a/stdlib/src/llrt/llrt_zlib/zstd.rs +++ /dev/null @@ -1,57 +0,0 @@ -// Redistributed from LLRT; module paths and backend cfgs adapted by scripts/import-stdlib.py. -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: Apache-2.0 -use crate::llrt_buffer::Buffer; -use crate::llrt_context::CtxExtension; -use crate::llrt_utils::{bytes::ObjectBytes, object::ObjectExt, result::ResultExt}; -use rquickjs::{ - prelude::{Opt, Rest}, - Ctx, Error, Exception, Function, IntoJs, Null, Result, Value, -}; - -use super::{define_cb_function, define_sync_function, max_output_length, read_to_end_limited}; - -enum ZstdCommand { - Compress, - Decompress, -} - -fn zstd_converter<'js>( - ctx: Ctx<'js>, - bytes: ObjectBytes<'js>, - options: Opt>, - command: ZstdCommand, -) -> Result> { - let src = bytes.as_bytes(&ctx)?; - - let mut level = crate::llrt_compression::zstd::DEFAULT_COMPRESSION_LEVEL; - if let Some(options) = options.0.as_ref() { - if let Some(opt) = options.get_optional("level")? { - level = opt; - } - } - let limit = max_output_length(&options)?; - - let dst = match command { - ZstdCommand::Compress => read_to_end_limited( - &ctx, - crate::llrt_compression::zstd::encoder(src, level)?, - limit, - src.len(), - )?, - ZstdCommand::Decompress => read_to_end_limited( - &ctx, - crate::llrt_compression::zstd::decoder(src)?, - limit, - src.len(), - )?, - }; - - Buffer(dst).into_js(&ctx) -} - -define_cb_function!(zstd_comp, zstd_converter, ZstdCommand::Compress); -define_sync_function!(zstd_comp_sync, zstd_converter, ZstdCommand::Compress); - -define_cb_function!(zstd_decomp, zstd_converter, ZstdCommand::Decompress); -define_sync_function!(zstd_decomp_sync, zstd_converter, ZstdCommand::Decompress); diff --git a/stdlib/tests/modules.rs b/stdlib/tests/modules.rs index 7a380a59..ef8dccde 100644 --- a/stdlib/tests/modules.rs +++ b/stdlib/tests/modules.rs @@ -38,7 +38,7 @@ fn crypto_hash_and_compression_roundtrip() { import { gzipSync, gunzipSync } from 'zlib'; const digest = createHash('sha256').update('abc').digest('hex'); if (digest !== 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad') throw Error(digest); - const input = Buffer.from('redistributed standard library'); + const input = Buffer.from('external standard library'); if (gunzipSync(gzipSync(input)).toString() !== input.toString()) throw Error('compression'); "#, );