From 0d1acf1eb351f3d85eda100e375e3026772ff75e Mon Sep 17 00:00:00 2001 From: KhooLy <73142442+KhooLy@users.noreply.github.com> Date: Mon, 20 Jul 2026 01:30:05 +0300 Subject: [PATCH] Port Nuvio-compatible scraper JS engine into fluxa_core Adds a plugin_runtime module (rquickjs VM, cheerio-style DOM bridge via scraper/ego-tree, CryptoJS + WebCrypto polyfill backed by native digest/HMAC/PBKDF2/AES/RSA/ECDSA) ported from fluxa-desktop's plugin_runtime, gated behind a new plugin-js-engine feature. Unlike the desktop version, the in-script fetch() bridge never touches reqwest directly: it calls a new PluginHttpClient UniFFI callback interface (Box -> Arc internally), so Kotlin/Tauri own the actual HTTP execution and any SSRF protections, consistent with fluxa_core never performing direct HTTP. execute_plugin_scraper is exposed as a synchronous UniFFI function alongside the existing core_invoke surface. --- Cargo.lock | 1166 +++++++++++++++++++++++- Cargo.toml | 41 +- src/bindings/uniffi.rs | 25 + src/lib.rs | 2 + src/plugin_runtime/crypto_bridge.rs | 415 +++++++++ src/plugin_runtime/dom_bridge.rs | 210 +++++ src/plugin_runtime/mod.rs | 1314 +++++++++++++++++++++++++++ 7 files changed, 3164 insertions(+), 9 deletions(-) create mode 100644 src/plugin_runtime/crypto_bridge.rs create mode 100644 src/plugin_runtime/dom_bridge.rs create mode 100644 src/plugin_runtime/mod.rs diff --git a/Cargo.lock b/Cargo.lock index a058531..4930e76 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,52 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common 0.1.7", + "generic-array 0.14.7", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + +[[package]] +name = "aes" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" +dependencies = [ + "cipher 0.5.2", + "cpubits", + "cpufeatures 0.3.0", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes 0.8.4", + "cipher 0.4.4", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "aho-corasick" version = "1.1.4" @@ -122,6 +168,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -254,12 +311,24 @@ dependencies = [ "rand 0.8.6", ] +[[package]] +name = "base16ct" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd307490d624467aa6f74b0eabb77633d1f758a7b25f12bceb0b22e08d9726f6" + [[package]] name = "base64" version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + [[package]] name = "basic-toml" version = "0.1.10" @@ -340,6 +409,24 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "block-buffer" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa" +dependencies = [ + "hybrid-array", +] + +[[package]] +name = "block-padding" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b" +dependencies = [ + "hybrid-array", +] + [[package]] name = "bs58" version = "0.5.1" @@ -416,6 +503,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "cbc" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" +dependencies = [ + "cipher 0.5.2", +] + [[package]] name = "cc" version = "1.2.63" @@ -446,6 +542,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core 0.10.1", +] + [[package]] name = "chrono" version = "0.4.45" @@ -487,6 +594,26 @@ dependencies = [ "half", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common 0.1.7", + "inout 0.1.4", +] + +[[package]] +name = "cipher" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" +dependencies = [ + "crypto-common 0.2.2", + "inout 0.2.2", +] + [[package]] name = "clap" version = "4.5.60" @@ -535,6 +662,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "combine" version = "4.6.7" @@ -562,6 +695,27 @@ version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc14f565cf027a105f7a44ccf9e5b424348421a1d8952a8fc9d499d313107789" +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" + [[package]] name = "const_panic" version = "0.2.15" @@ -571,6 +725,15 @@ dependencies = [ "typewit", ] +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.10.1" @@ -587,6 +750,12 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[package]] +name = "cpubits" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -596,6 +765,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -666,6 +844,22 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a52aa3fcda4e6302a9f48734f234d35d4721b96f8fe07d073f07ce9df4f0271" +dependencies = [ + "cpubits", + "ctutils", + "getrandom 0.4.2", + "hybrid-array", + "num-traits", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -673,9 +867,63 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" dependencies = [ "generic-array 0.14.7", + "rand_core 0.6.4", "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "getrandom 0.4.2", + "hybrid-array", + "rand_core 0.10.1", +] + +[[package]] +name = "cssparser" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9cdaae01d5ed7882b04d795e7f752f46ff52d2fa3b50a20d28c464510bba98" +dependencies = [ + "cssparser-macros", + "dtoa-short", + "itoa", + "phf 0.13.1", + "smallvec", +] + +[[package]] +name = "cssparser-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a2a99df6e410a8ff4245aa2006499ea662245f967cc7c0a38c83ef8eb44dbf" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher 0.4.4", +] + +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", + "subtle", +] + [[package]] name = "darling" version = "0.23.0" @@ -731,6 +979,28 @@ version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid 0.9.6", + "pem-rfc7468 0.7.0", + "zeroize", +] + +[[package]] +name = "der" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69dedd701da44b0536442edf09c81a64b0ab97a7a4a5e3d1971f00027cbc63d" +dependencies = [ + "const-oid 0.10.2", + "pem-rfc7468 1.0.0", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.8" @@ -740,14 +1010,48 @@ dependencies = [ "serde_core", ] +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "syn", +] + [[package]] name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ - "block-buffer", - "crypto-common", + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", +] + +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", ] [[package]] @@ -782,6 +1086,29 @@ dependencies = [ "syn", ] +[[package]] +name = "dlopen2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" +dependencies = [ + "dlopen2_derive", + "libc", + "once_cell", + "winapi", +] + +[[package]] +name = "dlopen2_derive" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "dolby_vision" version = "3.3.2" @@ -795,6 +1122,21 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "dtoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c3cf4824e2d5f025c7b531afcb2325364084a16806f6d47fbc1f5fbd9960590" + +[[package]] +name = "dtoa-short" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd1511a7b6a56299bd043a9c167a6d2bfb37bf84a6dfceaba651168adfb43c87" +dependencies = [ + "dtoa", +] + [[package]] name = "dunce" version = "1.0.5" @@ -807,12 +1149,63 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "ecb" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbfbf3db731928d6912bc3beda911d55b834cee3df6131ba79b337a1298a3fa9" +dependencies = [ + "cipher 0.5.2", +] + +[[package]] +name = "ecdsa" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0681a4fc24c767085329728d8dfba959af91228aa4610cca4f8ce317ba46ae0" +dependencies = [ + "der 0.8.1", + "digest 0.11.3", + "elliptic-curve", + "rfc6979", + "signature 3.0.0", + "spki 0.8.0", + "zeroize", +] + +[[package]] +name = "ego-tree" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b04dc5a38e4f151a79d9f2451ae6037fb6eaf5cba34771f44781f80e508498e3" + [[package]] name = "either" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" +[[package]] +name = "elliptic-curve" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d65aa39b3a5c1c9c1b745c9a019234bb7a21b77abcb4f4d266d706e2d577d65" +dependencies = [ + "base16ct", + "crypto-bigint", + "crypto-common 0.2.2", + "digest 0.11.3", + "ff", + "group", + "hybrid-array", + "pem-rfc7468 1.0.0", + "pkcs8 0.11.0", + "rand_core 0.10.1", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "equivalent" version = "1.0.2" @@ -829,12 +1222,42 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + [[package]] name = "fastrand" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "ff" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1f686ab92a9fb0eaf188f6c6c87b89490baa6fdb0db4544ba4dc47f7942489f" +dependencies = [ + "rand_core 0.10.1", + "subtle", +] + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -855,15 +1278,31 @@ dependencies = [ name = "fluxa_core" version = "0.1.0" dependencies = [ + "aes 0.9.1", + "aes-gcm", "base64", + "cbc", "chrono", "criterion", "dolby_vision", + "ecb", + "ego-tree", + "hmac", "jni 0.21.1", + "log", + "md-5", + "p256", + "pbkdf2", + "rand 0.10.2", "regex", + "rquickjs", + "rsa", + "scraper", "serde", "serde_json", - "sha2", + "sha1", + "sha2 0.11.0", + "tokio", "uniffi", "wasm-bindgen", "web-time", @@ -887,6 +1326,12 @@ dependencies = [ "url", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foldhash" version = "0.1.5" @@ -1042,6 +1487,15 @@ dependencies = [ "version_check", ] +[[package]] +name = "getopts" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe4fbac503b8d1f88e6676011885f34b7174f46e59956bba534ba83abded4df" +dependencies = [ + "unicode-width", +] + [[package]] name = "getrandom" version = "0.2.17" @@ -1078,10 +1532,21 @@ dependencies = [ "cfg-if", "libc", "r-efi 6.0.0", + "rand_core 0.10.1", "wasip2", "wasip3", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "glob" version = "0.3.3" @@ -1122,6 +1587,17 @@ dependencies = [ "web-time", ] +[[package]] +name = "group" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fd1a1c7a5206c5b7a3f5a0d7ccd3ff85d0c8f5133d62a02680255b0004af5f4" +dependencies = [ + "ff", + "rand_core 0.10.1", + "subtle", +] + [[package]] name = "half" version = "2.7.1" @@ -1170,6 +1646,11 @@ name = "hashbrown" version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] [[package]] name = "heck" @@ -1189,6 +1670,25 @@ version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + +[[package]] +name = "html5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a1761807faccc9a19e86944bbf40610014066306f96edcdedc2fb714bcb7b8" +dependencies = [ + "log", + "markup5ever", +] + [[package]] name = "http" version = "1.4.2" @@ -1234,6 +1734,17 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "818356c5132c1fede50f837ca96afbe78ff42413047f4abb886217845e1b6c8c" +dependencies = [ + "subtle", + "typenum", + "zeroize", +] + [[package]] name = "hyper" version = "1.10.1" @@ -1456,6 +1967,25 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array 0.14.7", +] + +[[package]] +name = "inout" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7" +dependencies = [ + "block-padding", + "hybrid-array", +] + [[package]] name = "instant" version = "0.1.13" @@ -1611,6 +2141,15 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + [[package]] name = "leaky-bucket" version = "1.1.2" @@ -1634,6 +2173,12 @@ version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.17" @@ -1892,12 +2437,33 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" +[[package]] +name = "markup5ever" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7122d987ec5f704ee56f6e5b41a7d93722e9aae27ae07cafa4036c4d3f9757de" +dependencies = [ + "log", + "tendril", + "web_atoms", +] + [[package]] name = "matchit" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + [[package]] name = "memchr" version = "2.8.0" @@ -1968,6 +2534,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "new_debug_unreachable" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" + [[package]] name = "nom" version = "7.1.3" @@ -1997,6 +2569,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.2.4" @@ -2051,6 +2639,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2065,6 +2654,12 @@ version = "11.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl-probe" version = "0.2.1" @@ -2077,6 +2672,19 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "p256" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c9239b2dbc807adbbe147e8cf72ea7450c3a0aabe62cb8e75ff4ec22e1f72a" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primefield", + "primeorder", + "sha2 0.11.0", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -2100,18 +2708,159 @@ dependencies = [ "windows-link", ] +[[package]] +name = "pbkdf2" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" +dependencies = [ + "digest 0.11.3", + "hmac", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "pem-rfc7468" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6305423e0e7738146434843d1694d621cce767262b2a86910beab705e4493d9" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" +dependencies = [ + "phf_shared 0.14.0", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49aa7f9d80421bca176ca8dbfebe668cc7a2684708594ec9f3c0db0805d5d6e1" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" +dependencies = [ + "fastrand", + "phf_shared 0.14.0", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der 0.7.10", + "pkcs8 0.10.2", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", +] + +[[package]] +name = "pkcs8" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" +dependencies = [ + "der 0.8.1", + "spki 0.8.0", +] + [[package]] name = "pkg-config" version = "0.3.33" @@ -2124,6 +2873,18 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.13.1" @@ -2154,6 +2915,12 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "prettyplease" version = "0.2.37" @@ -2164,6 +2931,42 @@ dependencies = [ "syn", ] +[[package]] +name = "primefield" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c555a6e4eb7d4e158fcb028c835c3b8642206ddc279b5c6b202ef9a8bdb592f4" +dependencies = [ + "crypto-bigint", + "crypto-common 0.2.2", + "ff", + "rand_core 0.10.1", + "subtle", + "zeroize", +] + +[[package]] +name = "primeorder" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c9f42978c78a00e3d68f69fc03e57a234debae69da4020a4fb588fcdcd07b06" +dependencies = [ + "elliptic-curve", + "once_cell", + "primefield", + "serdect", + "wnaf", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2302,6 +3105,17 @@ dependencies = [ "rand_core 0.9.5", ] +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.2", + "rand_core 0.10.1", +] + [[package]] name = "rand_chacha" version = "0.3.1" @@ -2340,6 +3154,12 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + [[package]] name = "raw-cpuid" version = "11.6.0" @@ -2418,6 +3238,15 @@ version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" +[[package]] +name = "relative-path" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bca40a312222d8ba74837cb474edef44b37f561da5f773981007a10bbaa992b0" +dependencies = [ + "serde", +] + [[package]] name = "reqwest" version = "0.12.28" @@ -2499,6 +3328,16 @@ dependencies = [ "web-sys", ] +[[package]] +name = "rfc6979" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4a459cddafb3fe76b31fd8f1108007566c40301feb64dc7b54656eb7388172b" +dependencies = [ + "crypto-bigint", + "hmac", +] + [[package]] name = "ring" version = "0.17.14" @@ -2522,6 +3361,84 @@ dependencies = [ "libc", ] +[[package]] +name = "rquickjs" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ce6b626d30ecbedaaf8097a04982bc3b081f958f5bdf9b8796be4ac00ee48bb" +dependencies = [ + "either", + "indexmap 2.14.0", + "rquickjs-core", + "rquickjs-macro", +] + +[[package]] +name = "rquickjs-core" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9334dd11023d5ae6c751f64496510a576208802ed1d022ef94afa7639c2c55e" +dependencies = [ + "async-lock", + "chrono", + "dlopen2", + "either", + "hashbrown 0.17.1", + "indexmap 2.14.0", + "phf 0.14.0", + "relative-path", + "rquickjs-sys", +] + +[[package]] +name = "rquickjs-macro" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7204086521740ca03f33036579801871aa5b714a0e7f68b4c2e98d68b0380bfb" +dependencies = [ + "convert_case", + "fnv", + "ident_case", + "indexmap 2.14.0", + "phf_generator 0.14.0", + "phf_shared 0.14.0", + "proc-macro-crate", + "proc-macro2", + "quote", + "rquickjs-core", + "syn", +] + +[[package]] +name = "rquickjs-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eef73520804cf5aa4876097ac5733058d628c769eba9678e0f4dba5a4ef79703" +dependencies = [ + "cc", +] + +[[package]] +name = "rsa" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" +dependencies = [ + "const-oid 0.9.6", + "digest 0.10.7", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sha2 0.10.9", + "signature 2.2.0", + "spki 0.7.3", + "subtle", + "zeroize", +] + [[package]] name = "rustc-hash" version = "2.1.2" @@ -2686,6 +3603,21 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "scraper" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd0be4d296f048bfb06dd01bbc80ef789ddd2e55583e8d2e6b804942abfabc2" +dependencies = [ + "cssparser", + "ego-tree", + "getopts", + "html5ever", + "precomputed-hash", + "selectors", + "tendril", +] + [[package]] name = "scroll" version = "0.12.0" @@ -2706,6 +3638,20 @@ dependencies = [ "syn", ] +[[package]] +name = "sec1" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d56d437c2f19203ce5f7122e507831de96f3d2d4d3be5af44a0b0a09d8a80e4d" +dependencies = [ + "base16ct", + "ctutils", + "der 0.8.1", + "hybrid-array", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -2729,6 +3675,25 @@ dependencies = [ "libc", ] +[[package]] +name = "selectors" +version = "0.38.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8adfa1c298912827b8a28b223b3b874357397ae706e6190acd9bf28cee99114d" +dependencies = [ + "bitflags", + "cssparser", + "derive_more", + "log", + "new_debug_unreachable", + "phf 0.13.1", + "phf_codegen", + "precomputed-hash", + "rustc-hash", + "servo_arc", + "smallvec", +] + [[package]] name = "semver" version = "1.0.28" @@ -2846,6 +3811,36 @@ dependencies = [ "syn", ] +[[package]] +name = "serdect" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66cf8fedced2fcf12406bcb34223dffb92eaf34908ede12fed414c82b7f00b3e" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "servo_arc" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "170fb83ab34de17dc69aa7c67482b22218ddb85da56546f9bd6b929e32a05930" +dependencies = [ + "stable_deref_trait", +] + +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + [[package]] name = "sha2" version = "0.10.9" @@ -2853,8 +3848,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.2.17", + "digest 0.10.7", +] + +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -2873,6 +3879,26 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d567dcbaf0049cb8ac2608a76cd95ff9e4412e1899d389ee400918ca7537f5" +dependencies = [ + "digest 0.11.3", + "rand_core 0.10.1", +] + [[package]] name = "simd-adler32" version = "0.3.9" @@ -2939,6 +3965,12 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e" + [[package]] name = "spinning_top" version = "0.3.0" @@ -2948,6 +3980,26 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.10", +] + +[[package]] +name = "spki" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" +dependencies = [ + "base64ct", + "der 0.8.1", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -2960,6 +4012,30 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +[[package]] +name = "string_cache" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a18596f8c785a729f2819c0f6a7eae6ebeebdfffbfe4214ae6b087f690e31901" +dependencies = [ + "new_debug_unreachable", + "parking_lot", + "phf_shared 0.13.1", + "precomputed-hash", +] + +[[package]] +name = "string_cache_codegen" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "585635e46db231059f76c5849798146164652513eb9e8ab2685939dd90f29b69" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", +] + [[package]] name = "strsim" version = "0.11.1" @@ -3022,6 +4098,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "tendril" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fed54709c5b3a53d09bb1c113ea4f5ceafd1e772ddcb0030a82e1d56c087b08" +dependencies = [ + "new_debug_unreachable", +] + [[package]] name = "textwrap" version = "0.16.2" @@ -3219,7 +4304,7 @@ dependencies = [ "indexmap 2.14.0", "serde_core", "serde_spanned", - "toml_datetime", + "toml_datetime 0.7.5+spec-1.1.0", "toml_parser", "toml_writer", "winnow 0.7.15", @@ -3234,6 +4319,27 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.13+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6975367e4d2ef766d86af01ffad14b622fecc8d4357a998fbc4deb6e9bacaf9b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + [[package]] name = "toml_parser" version = "1.1.2+spec-1.1.0" @@ -3357,6 +4463,18 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "unicode-xid" version = "0.2.6" @@ -3484,6 +4602,16 @@ dependencies = [ "weedle2", ] +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common 0.1.7", + "subtle", +] + [[package]] name = "untrusted" version = "0.7.1" @@ -3715,6 +4843,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web_atoms" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "075474b12bcb3d2e3d4546580e9de478eeeead668a1761e2a8860c836b7ef297" +dependencies = [ + "phf 0.13.1", + "phf_codegen", + "string_cache", + "string_cache_codegen", +] + [[package]] name = "webpki-root-certs" version = "1.0.8" @@ -4068,6 +5208,9 @@ name = "winnow" version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] [[package]] name = "wit-bindgen" @@ -4163,6 +5306,17 @@ dependencies = [ "wasmparser", ] +[[package]] +name = "wnaf" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab12e7090f27e2ffd9322651492942d50c2926094af30601e1964337db39daf1" +dependencies = [ + "ff", + "group", + "hybrid-array", +] + [[package]] name = "writeable" version = "0.6.3" diff --git a/Cargo.toml b/Cargo.toml index 1b6c5c2..9b73d13 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,9 +49,10 @@ native = [ "dep:jni", "dep:dolby_vision", "uniffi-bindings", + "plugin-js-engine", ] -desktop = ["full-api"] -ios = ["full-api", "uniffi-bindings"] +desktop = ["full-api", "plugin-js-engine"] +ios = ["full-api", "uniffi-bindings", "plugin-js-engine"] full-api = [] streaming-shared = [] uniffi-bindings = ["dep:uniffi", "full-api"] @@ -59,16 +60,50 @@ uniffi-cli = ["uniffi-bindings", "uniffi/cli"] wasm = ["full-api", "dep:wasm-bindgen", "chrono/wasmbind"] fuzzing = [] bench = [] +plugin-js-engine = [ + "dep:rquickjs", + "dep:scraper", + "dep:ego-tree", + "dep:aes", + "dep:aes-gcm", + "dep:cbc", + "dep:ecb", + "dep:hmac", + "dep:md-5", + "dep:pbkdf2", + "dep:rsa", + "dep:p256", + "dep:rand", + "dep:sha1", + "dep:log", + "dep:tokio", +] [dependencies] +aes = { version = "0.9.1", optional = true } +aes-gcm = { version = "0.10", optional = true } base64 = "0.22" +cbc = { version = "0.2.1", optional = true } chrono = { version = "0.4.45", features = ["serde"] } dolby_vision = { workspace = true, optional = true } +ecb = { version = "0.2.0", optional = true } +ego-tree = { version = "0.11", optional = true } +hmac = { version = "0.13.0", optional = true } jni = { workspace = true, optional = true } +log = { version = "0.4", optional = true } +"md-5" = { version = "0.11.0", optional = true } +p256 = { version = "0.14.0", optional = true } +pbkdf2 = { version = "0.13.0", optional = true } +rand = { version = "0.10.2", optional = true } regex = "1" -sha2 = "0.10" +rquickjs = { version = "0.12.1", features = ["full-async"], optional = true } +rsa = { version = "0.9.10", features = ["sha2"], optional = true } +scraper = { version = "0.27.0", optional = true } +sha1 = { version = "0.11.0", optional = true } +sha2 = "0.11.0" serde = { workspace = true } serde_json = { workspace = true } +tokio = { version = "1", features = ["rt", "macros", "time"], optional = true } uniffi = { version = "0.31.1", optional = true } wasm-bindgen = { version = "0.2", optional = true } web-time = "1" diff --git a/src/bindings/uniffi.rs b/src/bindings/uniffi.rs index 7409b5b..413f9bd 100644 --- a/src/bindings/uniffi.rs +++ b/src/bindings/uniffi.rs @@ -1,4 +1,6 @@ use crate::{app_state, core_contract, headless_engine}; +#[cfg(feature = "plugin-js-engine")] +use std::sync::Arc; // A panic anywhere below must not unwind across the UniFFI boundary into // Swift/Kotlin — that's undefined behavior, not a catchable exception there. @@ -105,3 +107,26 @@ pub fn app_core_dispatch_json(handle: i64, action_json: String) -> String { app_state::app_core_dispatch_json(handle as u64, &action_json).unwrap_or_default() }) } + +#[cfg(feature = "plugin-js-engine")] +#[uniffi::export] +pub fn execute_plugin_scraper( + client: Box, + code: String, + tmdb_id: String, + media_type: String, + season: Option, + episode: Option, +) -> String { + guard("[]".to_string(), || { + crate::plugin_runtime::execute_scraper( + Arc::from(client), + code, + tmdb_id, + media_type, + season, + episode, + ) + .unwrap_or_else(|_| "[]".to_string()) + }) +} diff --git a/src/lib.rs b/src/lib.rs index b9ae922..985e428 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -82,6 +82,8 @@ mod player_policy; mod player_scrobble; #[cfg(any(feature = "full-api", not(feature = "streaming-shared")))] mod plugins; +#[cfg(feature = "plugin-js-engine")] +pub mod plugin_runtime; #[cfg(any(feature = "full-api", not(feature = "streaming-shared")))] mod profile_contract; #[cfg(any(feature = "full-api", not(feature = "streaming-shared")))] diff --git a/src/plugin_runtime/crypto_bridge.rs b/src/plugin_runtime/crypto_bridge.rs new file mode 100644 index 0000000..5d1df9d --- /dev/null +++ b/src/plugin_runtime/crypto_bridge.rs @@ -0,0 +1,415 @@ +use aes::cipher::block_padding::{NoPadding, Pkcs7}; +use aes::cipher::{BlockModeDecrypt, BlockModeEncrypt, KeyInit, KeyIvInit}; +use aes::{Aes128, Aes192, Aes256}; +use hmac::{Hmac, Mac}; +use md5::Md5; +use pbkdf2::pbkdf2_hmac; +use rand::Rng; +use rsa::pkcs1v15::{SigningKey as RsaSigningKey, VerifyingKey as RsaVerifyingKey}; +use rsa::pkcs8::{DecodePrivateKey, DecodePublicKey}; +use rsa::signature::{SignatureEncoding, Signer, Verifier}; +use rsa::{RsaPrivateKey, RsaPublicKey}; +use sha1::Sha1; +use sha2::{Digest, Sha256, Sha384, Sha512}; + +pub fn digest(algorithm: &str, data: &[u8]) -> Result, String> { + Ok(match algorithm { + "MD5" => Md5::digest(data).to_vec(), + "SHA1" => Sha1::digest(data).to_vec(), + "SHA256" => Sha256::digest(data).to_vec(), + "SHA384" => Sha384::digest(data).to_vec(), + "SHA512" => Sha512::digest(data).to_vec(), + other => return Err(format!("unsupported digest algorithm: {other}")), + }) +} + +pub fn hmac(algorithm: &str, key: &[u8], data: &[u8]) -> Result, String> { + macro_rules! hmac_with { + ($hash:ty) => {{ + let mut mac = Hmac::<$hash>::new_from_slice(key).map_err(|e| e.to_string())?; + mac.update(data); + mac.finalize().into_bytes().to_vec() + }}; + } + Ok(match algorithm { + "MD5" => hmac_with!(Md5), + "SHA1" => hmac_with!(Sha1), + "SHA256" => hmac_with!(Sha256), + "SHA384" => hmac_with!(Sha384), + "SHA512" => hmac_with!(Sha512), + other => return Err(format!("unsupported HMAC algorithm: {other}")), + }) +} + +pub fn pbkdf2( + password: &[u8], + salt: &[u8], + iterations: u32, + key_size_bits: u32, + algorithm: &str, +) -> Result, String> { + let out_len = (key_size_bits as usize).div_ceil(8); + let mut out = vec![0u8; out_len]; + macro_rules! pbkdf2_with { + ($hash:ty) => { + pbkdf2_hmac::<$hash>(password, salt, iterations, &mut out) + }; + } + match algorithm { + "MD5" => pbkdf2_with!(Md5), + "SHA1" => pbkdf2_with!(Sha1), + "SHA256" => pbkdf2_with!(Sha256), + "SHA384" => pbkdf2_with!(Sha384), + "SHA512" => pbkdf2_with!(Sha512), + other => return Err(format!("unsupported PBKDF2 algorithm: {other}")), + } + Ok(out) +} + +pub fn random_bytes(len: usize) -> Vec { + let mut bytes = vec![0u8; len]; + rand::rng().fill_bytes(&mut bytes); + bytes +} + +pub fn aes_encrypt(mode: &str, key: &[u8], iv: &[u8], data: &[u8]) -> Result, String> { + let (base_mode, no_padding) = split_padding(mode); + match base_mode { + "AES-CBC" => cbc_encrypt(key, iv, data, no_padding), + "AES-ECB" => ecb_encrypt(key, data, no_padding), + "AES-GCM" => gcm_encrypt(key, iv, data), + other => Err(format!("unsupported AES mode: {other}")), + } +} + +pub fn aes_decrypt(mode: &str, key: &[u8], iv: &[u8], data: &[u8]) -> Result, String> { + let (base_mode, no_padding) = split_padding(mode); + match base_mode { + "AES-CBC" => cbc_decrypt(key, iv, data, no_padding), + "AES-ECB" => ecb_decrypt(key, data, no_padding), + "AES-GCM" => gcm_decrypt(key, iv, data), + other => Err(format!("unsupported AES mode: {other}")), + } +} + +fn split_padding(mode: &str) -> (&str, bool) { + match mode.strip_suffix("-NoPadding") { + Some(base) => (base, true), + None => (mode, false), + } +} + +fn cbc_encrypt(key: &[u8], iv: &[u8], data: &[u8], no_padding: bool) -> Result, String> { + if iv.len() != 16 { + return Err("AES-CBC requires a 16-byte IV".to_string()); + } + match key.len() { + 16 => cbc_encrypt_with::(key, iv, data, no_padding), + 24 => cbc_encrypt_with::(key, iv, data, no_padding), + 32 => cbc_encrypt_with::(key, iv, data, no_padding), + other => Err(format!("unsupported AES key length: {other} bytes")), + } +} + +fn cbc_decrypt(key: &[u8], iv: &[u8], data: &[u8], no_padding: bool) -> Result, String> { + if iv.len() != 16 { + return Err("AES-CBC requires a 16-byte IV".to_string()); + } + match key.len() { + 16 => cbc_decrypt_with::(key, iv, data, no_padding), + 24 => cbc_decrypt_with::(key, iv, data, no_padding), + 32 => cbc_decrypt_with::(key, iv, data, no_padding), + other => Err(format!("unsupported AES key length: {other} bytes")), + } +} + +fn cbc_encrypt_with(key: &[u8], iv: &[u8], data: &[u8], no_padding: bool) -> Result, String> +where + cbc::Encryptor: KeyIvInit + BlockModeEncrypt, + C: aes::cipher::BlockCipherEncrypt, +{ + let encryptor = cbc::Encryptor::::new_from_slices(key, iv).map_err(|e| e.to_string())?; + let mut buf = data.to_vec(); + buf.resize(data.len() + 16, 0); + let result = if no_padding { + encryptor + .encrypt_padded::(&mut buf, data.len()) + .map_err(|e| e.to_string())? + } else { + encryptor + .encrypt_padded::(&mut buf, data.len()) + .map_err(|e| e.to_string())? + }; + Ok(result.to_vec()) +} + +fn cbc_decrypt_with(key: &[u8], iv: &[u8], data: &[u8], no_padding: bool) -> Result, String> +where + cbc::Decryptor: KeyIvInit + BlockModeDecrypt, + C: aes::cipher::BlockCipherDecrypt, +{ + let decryptor = cbc::Decryptor::::new_from_slices(key, iv).map_err(|e| e.to_string())?; + let mut buf = data.to_vec(); + let result = if no_padding { + decryptor + .decrypt_padded::(&mut buf) + .map_err(|e| e.to_string())? + } else { + decryptor + .decrypt_padded::(&mut buf) + .map_err(|e| e.to_string())? + }; + Ok(result.to_vec()) +} + +fn ecb_encrypt(key: &[u8], data: &[u8], no_padding: bool) -> Result, String> { + match key.len() { + 16 => ecb_encrypt_with::(key, data, no_padding), + 24 => ecb_encrypt_with::(key, data, no_padding), + 32 => ecb_encrypt_with::(key, data, no_padding), + other => Err(format!("unsupported AES key length: {other} bytes")), + } +} + +fn ecb_decrypt(key: &[u8], data: &[u8], no_padding: bool) -> Result, String> { + match key.len() { + 16 => ecb_decrypt_with::(key, data, no_padding), + 24 => ecb_decrypt_with::(key, data, no_padding), + 32 => ecb_decrypt_with::(key, data, no_padding), + other => Err(format!("unsupported AES key length: {other} bytes")), + } +} + +fn ecb_encrypt_with(key: &[u8], data: &[u8], no_padding: bool) -> Result, String> +where + ecb::Encryptor: aes::cipher::KeyInit + BlockModeEncrypt, + C: aes::cipher::BlockCipherEncrypt, +{ + let encryptor = ecb::Encryptor::::new_from_slice(key).map_err(|e| e.to_string())?; + let mut buf = data.to_vec(); + buf.resize(data.len() + 16, 0); + let result = if no_padding { + encryptor + .encrypt_padded::(&mut buf, data.len()) + .map_err(|e| e.to_string())? + } else { + encryptor + .encrypt_padded::(&mut buf, data.len()) + .map_err(|e| e.to_string())? + }; + Ok(result.to_vec()) +} + +fn ecb_decrypt_with(key: &[u8], data: &[u8], no_padding: bool) -> Result, String> +where + ecb::Decryptor: aes::cipher::KeyInit + BlockModeDecrypt, + C: aes::cipher::BlockCipherDecrypt, +{ + let decryptor = ecb::Decryptor::::new_from_slice(key).map_err(|e| e.to_string())?; + let mut buf = data.to_vec(); + let result = if no_padding { + decryptor + .decrypt_padded::(&mut buf) + .map_err(|e| e.to_string())? + } else { + decryptor + .decrypt_padded::(&mut buf) + .map_err(|e| e.to_string())? + }; + Ok(result.to_vec()) +} + +fn gcm_encrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result, String> { + use aes_gcm::aead::{Aead, KeyInit}; + use aes_gcm::{Aes128Gcm, Aes256Gcm, Nonce}; + + if iv.len() != 12 { + return Err("AES-GCM requires a 12-byte IV".to_string()); + } + let nonce = Nonce::from_slice(iv); + match key.len() { + 16 => Aes128Gcm::new_from_slice(key) + .map_err(|e| e.to_string())? + .encrypt(nonce, data) + .map_err(|e| e.to_string()), + 32 => Aes256Gcm::new_from_slice(key) + .map_err(|e| e.to_string())? + .encrypt(nonce, data) + .map_err(|e| e.to_string()), + other => Err(format!("unsupported AES-GCM key length: {other} bytes")), + } +} + +fn gcm_decrypt(key: &[u8], iv: &[u8], data: &[u8]) -> Result, String> { + use aes_gcm::aead::{Aead, KeyInit}; + use aes_gcm::{Aes128Gcm, Aes256Gcm, Nonce}; + + if iv.len() != 12 { + return Err("AES-GCM requires a 12-byte IV".to_string()); + } + let nonce = Nonce::from_slice(iv); + match key.len() { + 16 => Aes128Gcm::new_from_slice(key) + .map_err(|e| e.to_string())? + .decrypt(nonce, data) + .map_err(|e| e.to_string()), + 32 => Aes256Gcm::new_from_slice(key) + .map_err(|e| e.to_string())? + .decrypt(nonce, data) + .map_err(|e| e.to_string()), + other => Err(format!("unsupported AES-GCM key length: {other} bytes")), + } +} + +pub fn sign(algorithm: &str, private_key_der: &[u8], data: &[u8]) -> Result, String> { + if let Some(hash) = algorithm.strip_prefix("RSASSA-PKCS1-V1_5-") { + return rsa_sign(hash, private_key_der, data); + } + if algorithm.starts_with("ECDSA") { + return ecdsa_sign(private_key_der, data); + } + Err(format!("unsupported signature algorithm: {algorithm}")) +} + +pub fn verify(algorithm: &str, public_key_der: &[u8], signature: &[u8], data: &[u8]) -> Result { + if let Some(hash) = algorithm.strip_prefix("RSASSA-PKCS1-V1_5-") { + return rsa_verify(hash, public_key_der, signature, data); + } + if algorithm.starts_with("ECDSA") { + return ecdsa_verify(public_key_der, signature, data); + } + Err(format!("unsupported signature algorithm: {algorithm}")) +} + +fn rsa_sign(hash: &str, private_key_der: &[u8], data: &[u8]) -> Result, String> { + use rsa::sha2::{Sha256, Sha384, Sha512}; + + let key = RsaPrivateKey::from_pkcs8_der(private_key_der).map_err(|e| e.to_string())?; + macro_rules! sign_with { + ($d:ty) => {{ + let signing_key = RsaSigningKey::<$d>::new(key); + signing_key.sign(data).to_vec() + }}; + } + Ok(match hash { + "SHA256" => sign_with!(Sha256), + "SHA384" => sign_with!(Sha384), + "SHA512" => sign_with!(Sha512), + other => return Err(format!("unsupported RSA signature hash: {other}")), + }) +} + +fn rsa_verify(hash: &str, public_key_der: &[u8], signature: &[u8], data: &[u8]) -> Result { + use rsa::sha2::{Sha256, Sha384, Sha512}; + + let key = RsaPublicKey::from_public_key_der(public_key_der).map_err(|e| e.to_string())?; + macro_rules! verify_with { + ($d:ty) => {{ + let verifying_key = RsaVerifyingKey::<$d>::new(key); + let sig = rsa::pkcs1v15::Signature::try_from(signature).map_err(|e| e.to_string())?; + verifying_key.verify(data, &sig).is_ok() + }}; + } + Ok(match hash { + "SHA256" => verify_with!(Sha256), + "SHA384" => verify_with!(Sha384), + "SHA512" => verify_with!(Sha512), + other => return Err(format!("unsupported RSA signature hash: {other}")), + }) +} + +fn ecdsa_sign(private_key_der: &[u8], data: &[u8]) -> Result, String> { + use p256::ecdsa::signature::Signer as EcdsaSigner; + use p256::ecdsa::{Signature, SigningKey}; + use p256::pkcs8::DecodePrivateKey as EcdsaDecodePrivateKey; + + let signing_key = + SigningKey::from_pkcs8_der(private_key_der).map_err(|e| e.to_string())?; + let signature: Signature = signing_key.sign(data); + Ok(signature.to_bytes().to_vec()) +} + +fn ecdsa_verify(public_key_der: &[u8], signature: &[u8], data: &[u8]) -> Result { + use p256::ecdsa::signature::Verifier as EcdsaVerifier; + use p256::ecdsa::{Signature, VerifyingKey}; + use p256::pkcs8::DecodePublicKey as EcdsaDecodePublicKey; + + let verifying_key = + VerifyingKey::from_public_key_der(public_key_der).map_err(|e| e.to_string())?; + let sig = Signature::from_slice(signature).map_err(|e| e.to_string())?; + Ok(verifying_key.verify(data, &sig).is_ok()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hmac_sha256_output_is_32_bytes() { + let mac = hmac("SHA256", b"key", b"The quick brown fox jumps over the lazy dog").unwrap(); + assert_eq!(mac.len(), 32, "HMAC-SHA256 must be 32 bytes, got {}", mac.len()); + let hex: String = mac.iter().map(|b| format!("{b:02x}")).collect(); + assert_eq!( + hex, + "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8" + ); + } + + fn hex_decode(s: &str) -> Vec { + (0..s.len() / 2) + .map(|i| u8::from_str_radix(&s[i * 2..i * 2 + 2], 16).unwrap()) + .collect() + } + + const RSA_PRIV_DER_HEX: &str = "308204bd020100300d06092a864886f70d0101010500048204a7308204a30201000282010100bddba8f4e0cb99a11283571767832821a23220b11ec92160e3a99185a7c11342abc002d1b981f2fa73beb88b682e95a06d51cf0ecc82bd6a33ab0d778d1063d3229711313c5350d984b3bce669ddbebabfe34e261588cb6c560edb6d7f83a403e492ef387d8119cbc2f2f695dca535fed5df1fc54001ddb8712b895d90fbd9bd43d3e4b2cf2512f6c6e88d99e711883039d108bfb80e1882b52d0c869740f0c2106c706bd2fb003a50a18414c135042b97ba70874bc5d4100437be9a375ac85c48c768cdfbdc958c0f7a7c3b18cd2de9ef2d5241e1406e0f672f17199fd4c94d6179991f1b1860fa1d13a9a4adcd1ef1a261a4241e5f0e78feaf0ebb54408b9702030100010282010018f74bae77e90e18ba407f315ab14a93b1aa79d610c04e1d5992b0ca2c971d1b454e88905f9e90ff123d603324ce0b6aff3524cf436165db135448ff1193b82cc97da80b1b6db152093b4179e32dffa8bb16feedcf231feced1c230ab11bf473769943cfdfe11c2d49437ddd93183e937638530b33318ba077714c2cfba5c2aa00b2b92c6c6c0d845d689aa3d0745d38b799a6ee65c9c2b601466d350faa03dc089ecd282eca2300bcaebd8ca90fb56136d526dc4e88381e137d07db846b099a3960f418440a2ba90bdbb57bdba68cec424d25524f6d4b3e6fb2b0afe60bc04b6b94e5eb5c94b4b0f17a2a068cabf852af771968958e69bb57d6b12b92d11f4102818100e898d69b8b669ce41491374ae51a4d7ed8d411424b58b0dffe7e10603070782c7600fa69cd86ddaf4bffa4aa84e53f8776c298f3ae246c2072e089688b550757f6129842be9a1fde12d880c8185bf8321ed0175bd3a148e4f7bf29b6e58f2d28003a5e8d9084c9258128591ac99f5a4e5aa8bf3911606b036f344ab8790cce3102818100d0f5f6e2fed04943dc6f37ff2fd34ba6f525a5ae1eb2a838c8f81ae1666d082d9ed9cafec15c146fd141fa2ae2c4da6ad9c88f77e399b905bd54bb62db0f23e5e02fedcc0d5c9bf699cf829efdc1b1af0b512e51dc7105ce15565e8b948e7f1e6eb945d94b5c99bdf7f8fe697deaf60c43cebb38abf2996d13d85de066061c4702818100bf07afd266062bbac30d952ce4c6da78e09a2366a03d425f6543a22d6dfb2127daeee8bb76ba5a5dbb7c45806acf6182df4de992d12646cbcbad900d15ca0ad2fa5dc141a80b2b3b179d4aff8bca15290cd77927c8c340fc1461a35ffc5fc0058752690fb96ee548f12ff326a60b437ef0c480f0dccda47658a1c8f08332ba3102818038589ae24ae3cf5dc52b18666f7702875ac37411a7a575471aaa0c558b510b4b81f22892d98102e1c4038e79d1fc53094d008fef3c284f8404b1bc75c3ac2979eb2201756c84f46962c059f0956c8d852e000e0574f3e524d50c1d22764496729658cf44f0d55f7fa2463a1428281b80e327f3c42c5996959b2c54e4159ec0310281800bbc5ab68bdd62ebe027a1e86e46b9cfdd888c0e6e733e9c8e41a5dd4bb743583671cab340e0372b3d21a59d6dd903ec8a2a21473317e4bc99c54ac1647f1169f624da78dfc23eff30971c0f0ca3e108ea7a93027e9721ed9d8631c76a73aed28592e593b47bfd39c6f3cf5278dcbf7e127ed2024427f4836dbd0a64839dd223"; + const RSA_PUB_DER_HEX: &str = "30820122300d06092a864886f70d01010105000382010f003082010a0282010100bddba8f4e0cb99a11283571767832821a23220b11ec92160e3a99185a7c11342abc002d1b981f2fa73beb88b682e95a06d51cf0ecc82bd6a33ab0d778d1063d3229711313c5350d984b3bce669ddbebabfe34e261588cb6c560edb6d7f83a403e492ef387d8119cbc2f2f695dca535fed5df1fc54001ddb8712b895d90fbd9bd43d3e4b2cf2512f6c6e88d99e711883039d108bfb80e1882b52d0c869740f0c2106c706bd2fb003a50a18414c135042b97ba70874bc5d4100437be9a375ac85c48c768cdfbdc958c0f7a7c3b18cd2de9ef2d5241e1406e0f672f17199fd4c94d6179991f1b1860fa1d13a9a4adcd1ef1a261a4241e5f0e78feaf0ebb54408b970203010001"; + const RSA_SIG_HEX: &str = "1f6e08bb7841498b47f553cb6a3c4a09a177169bdeb6152634720f63ec7348e04c1e07b2915c1999bcb4bdbfbc85f970b641b4a6295e2befda427b51ab2cbd6c2fceaf57e0854c0e1c943802539c050026a89e1fcefe35fd4a6d8ab5cf8955d4f9da1e8713686d038ebda8a9623b1c18cfc2f5d30a3846c6c133fe50930ca03c3b3645999d0eb89056406c5746dd6635dd45ebbcdecb6e48e6853d1594bd41bda6017c6cc2b053df7a791d8c8822528b42a6e554e21ad86bb800c9b298dff248ad807233bc0c95e6db63d98d92926e328f78a5f9a2495d319ba6d43c594107e2829d3fe2b09da6fbbd5e398f57fdc5f0975b78e8aceeb9f63a474bad6fc89198"; + const RSA_SIGNED_MESSAGE: &[u8] = b"hello plugin signature test"; + + const EC_PRIV_DER_HEX: &str = "308187020100301306072a8648ce3d020106082a8648ce3d030107046d306b020101042042f8c39da6ffb2b063b1fc24f521c8588064773e0f46c0c1c515759ca3a80f3aa144034200049e6a723242258b9d87c8362fd321140b80e16d1671b5cb9b2dba3da7ccc42b82380102e3fd415ed40dfc8c4b4b218218995327daedc7eb35493f4f5b419aeaf8"; + const EC_PUB_DER_HEX: &str = "3059301306072a8648ce3d020106082a8648ce3d030107034200049e6a723242258b9d87c8362fd321140b80e16d1671b5cb9b2dba3da7ccc42b82380102e3fd415ed40dfc8c4b4b218218995327daedc7eb35493f4f5b419aeaf8"; + const EC_SIG_HEX: &str = "f982b6fd52964b591329f5b503627dc1dc5b7f74ff0cf9acc840ab160636a99a526a7f11ee179e77d176827ab0035ee92653e3e7408c6c8fea3f566ec79e8c8f"; + const EC_SIGNED_MESSAGE: &[u8] = b"hello plugin signature test"; + + #[test] + fn rsa_verify_accepts_an_openssl_produced_signature() { + let key = hex_decode(RSA_PUB_DER_HEX); + let sig = hex_decode(RSA_SIG_HEX); + let ok = verify("RSASSA-PKCS1-V1_5-SHA256", &key, &sig, RSA_SIGNED_MESSAGE).unwrap(); + assert!(ok, "should verify a real openssl-produced RSA signature"); + } + + #[test] + fn rsa_verify_rejects_a_tampered_message() { + let key = hex_decode(RSA_PUB_DER_HEX); + let sig = hex_decode(RSA_SIG_HEX); + let ok = verify("RSASSA-PKCS1-V1_5-SHA256", &key, &sig, b"tampered message").unwrap(); + assert!(!ok); + } + + #[test] + fn rsa_sign_then_openssl_style_verify_roundtrips() { + let priv_key = hex_decode(RSA_PRIV_DER_HEX); + let pub_key = hex_decode(RSA_PUB_DER_HEX); + let sig = sign("RSASSA-PKCS1-V1_5-SHA256", &priv_key, RSA_SIGNED_MESSAGE).unwrap(); + let ok = verify("RSASSA-PKCS1-V1_5-SHA256", &pub_key, &sig, RSA_SIGNED_MESSAGE).unwrap(); + assert!(ok); + } + + #[test] + fn ecdsa_verify_accepts_a_python_cryptography_produced_signature() { + let key = hex_decode(EC_PUB_DER_HEX); + let sig = hex_decode(EC_SIG_HEX); + let ok = verify("ECDSA-SHA256", &key, &sig, EC_SIGNED_MESSAGE).unwrap(); + assert!(ok, "should verify a real python `cryptography`-produced P-256 signature"); + } + + #[test] + fn ecdsa_sign_then_verify_roundtrips() { + let priv_key = hex_decode(EC_PRIV_DER_HEX); + let pub_key = hex_decode(EC_PUB_DER_HEX); + let sig = sign("ECDSA-SHA256", &priv_key, EC_SIGNED_MESSAGE).unwrap(); + let ok = verify("ECDSA-SHA256", &pub_key, &sig, EC_SIGNED_MESSAGE).unwrap(); + assert!(ok); + } +} diff --git a/src/plugin_runtime/dom_bridge.rs b/src/plugin_runtime/dom_bridge.rs new file mode 100644 index 0000000..2333d25 --- /dev/null +++ b/src/plugin_runtime/dom_bridge.rs @@ -0,0 +1,210 @@ +use ego_tree::NodeId; +use scraper::{ElementRef, Html, Selector}; +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; + +#[derive(Default)] +struct DomState { + documents: HashMap, + elements: HashMap, + counter: u64, +} + +impl DomState { + fn next_id(&mut self) -> u64 { + let id = self.counter; + self.counter += 1; + id + } +} + +pub struct DomBridge { + state: RefCell, +} + +impl DomBridge { + pub fn new() -> Rc { + Rc::new(Self { + state: RefCell::new(DomState::default()), + }) + } + + pub fn load(&self, html: String) -> String { + let mut state = self.state.borrow_mut(); + let doc_id = format!("doc_{}", state.next_id()); + state.documents.insert(doc_id.clone(), Html::parse_document(&html)); + doc_id + } + + pub fn select(&self, doc_id: String, selector: String) -> String { + let mut state = self.state.borrow_mut(); + if selector.is_empty() { + return "[]".into(); + } + let ids: Vec = match state.documents.get(&doc_id) { + Some(doc) => run_selector(doc, &selector).iter().map(|el| el.id()).collect(), + None => return "[]".into(), + }; + cache_and_encode(&mut state, &doc_id, ids) + } + + pub fn find(&self, doc_id: String, element_id: String, selector: String) -> String { + let mut state = self.state.borrow_mut(); + let scope = match state.elements.get(&element_id) { + Some((_, node_id)) => *node_id, + None => return "[]".into(), + }; + let ids: Vec = match state.documents.get(&doc_id) { + Some(doc) => run_selector_within(doc, scope, &selector) + .iter() + .map(|el| el.id()) + .collect(), + None => return "[]".into(), + }; + cache_and_encode(&mut state, &doc_id, ids) + } + + pub fn text(&self, element_ids_csv: String) -> String { + let state = self.state.borrow(); + element_ids_csv + .split(',') + .filter(|id| !id.is_empty()) + .filter_map(|id| resolve(&state, id)) + .map(|el| el.text().collect::()) + .collect::>() + .join(" ") + } + + pub fn html(&self, doc_id: String, element_id: String) -> String { + let state = self.state.borrow(); + if element_id.is_empty() { + state.documents.get(&doc_id).map(|d| d.html()).unwrap_or_default() + } else { + resolve(&state, &element_id).map(|el| el.inner_html()).unwrap_or_default() + } + } + + pub fn inner_html(&self, element_id: String) -> String { + let state = self.state.borrow(); + resolve(&state, &element_id).map(|el| el.inner_html()).unwrap_or_default() + } + + pub fn attr(&self, element_id: String, attr_name: String) -> String { + let state = self.state.borrow(); + match resolve(&state, &element_id).and_then(|el| el.attr(&attr_name)) { + Some(value) if !value.is_empty() => value.to_string(), + _ => "__UNDEFINED__".to_string(), + } + } + + pub fn next(&self, doc_id: String, element_id: String) -> String { + self.sibling(&doc_id, &element_id, true) + } + + pub fn prev(&self, doc_id: String, element_id: String) -> String { + self.sibling(&doc_id, &element_id, false) + } + + fn sibling(&self, doc_id: &str, element_id: &str, forward: bool) -> String { + let mut state = self.state.borrow_mut(); + let start = match state.elements.get(element_id) { + Some((_, node_id)) => *node_id, + None => return "__NONE__".into(), + }; + + let found = match state.documents.get(doc_id) { + Some(doc) => next_element_sibling_id(doc, start, forward), + None => return "__NONE__".into(), + }; + + match found { + Some(node_id) => { + let element_id = format!("{doc_id}:{}", state.next_id()); + state.elements.insert(element_id.clone(), (doc_id.to_string(), node_id)); + element_id + } + None => "__NONE__".to_string(), + } + } +} + +fn resolve<'a>(state: &'a DomState, element_id: &str) -> Option> { + let (doc_id, node_id) = state.elements.get(element_id)?; + let doc = state.documents.get(doc_id)?; + ElementRef::wrap(doc.tree.get(*node_id)?) +} + +fn next_element_sibling_id(doc: &Html, from: NodeId, forward: bool) -> Option { + let mut cursor = doc.tree.get(from); + loop { + let node_ref = cursor?; + let sib = if forward { + node_ref.next_sibling() + } else { + node_ref.prev_sibling() + }; + let sib = sib?; + let sib_id = sib.id(); + if ElementRef::wrap(sib).is_some() { + return Some(sib_id); + } + cursor = doc.tree.get(sib_id); + } +} + +fn split_contains(selector: &str) -> (String, Option) { + let Some(start) = selector.find(":contains(") else { + return (selector.to_string(), None); + }; + let after = &selector[start + ":contains(".len()..]; + let Some(end) = after.find(')') else { + return (selector.to_string(), None); + }; + let needle = after[..end].trim_matches(|c| c == '\'' || c == '"').to_string(); + let mut base = String::new(); + base.push_str(&selector[..start]); + base.push_str(&after[end + 1..]); + let base = base.trim(); + let base = if base.is_empty() { "*".to_string() } else { base.to_string() }; + (base, Some(needle)) +} + +fn run_selector<'a>(doc: &'a Html, selector: &str) -> Vec> { + let (base, needle) = split_contains(selector); + let Ok(sel) = Selector::parse(&base) else { + return Vec::new(); + }; + let mut results: Vec> = doc.select(&sel).collect(); + if let Some(needle) = needle { + results.retain(|el| el.text().collect::().contains(&needle)); + } + results +} + +fn run_selector_within<'a>(doc: &'a Html, scope: NodeId, selector: &str) -> Vec> { + let (base, needle) = split_contains(selector); + let Ok(sel) = Selector::parse(&base) else { + return Vec::new(); + }; + let Some(scope_ref) = doc.tree.get(scope).and_then(ElementRef::wrap) else { + return Vec::new(); + }; + let mut results: Vec> = scope_ref.select(&sel).collect(); + if let Some(needle) = needle { + results.retain(|el| el.text().collect::().contains(&needle)); + } + results +} + +fn cache_and_encode(state: &mut DomState, doc_id: &str, ids: Vec) -> String { + let encoded: Vec = ids + .into_iter() + .map(|node_id| { + let element_id = format!("{doc_id}:{}", state.next_id()); + state.elements.insert(element_id.clone(), (doc_id.to_string(), node_id)); + element_id + }) + .collect(); + serde_json::to_string(&encoded).unwrap_or_else(|_| "[]".into()) +} diff --git a/src/plugin_runtime/mod.rs b/src/plugin_runtime/mod.rs new file mode 100644 index 0000000..0703cd3 --- /dev/null +++ b/src/plugin_runtime/mod.rs @@ -0,0 +1,1314 @@ +mod crypto_bridge; +mod dom_bridge; + +use dom_bridge::DomBridge; +use rquickjs::{AsyncContext, AsyncRuntime, CatchResultExt, Ctx, Function}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::rc::Rc; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +const PLUGIN_TIMEOUT_SECS: u64 = 60; +const PLUGIN_MEMORY_LIMIT: usize = 256 * 1024 * 1024; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] +#[serde(rename_all = "camelCase")] +pub struct PluginHttpRequest { + pub method: String, + pub url: String, + pub headers: HashMap, + pub body: Option, + pub follow_redirects: bool, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[cfg_attr(feature = "uniffi-bindings", derive(uniffi::Record))] +#[serde(rename_all = "camelCase")] +pub struct PluginHttpResponse { + pub status: u16, + pub headers: HashMap, + pub body: String, + pub ok: bool, + pub error: Option, +} + +#[cfg_attr(feature = "uniffi-bindings", uniffi::export(callback_interface))] +pub trait PluginHttpClient: Send + Sync { + fn fetch(&self, request: PluginHttpRequest) -> PluginHttpResponse; +} + +pub fn execute_scraper( + client: Arc, + code: String, + tmdb_id: String, + media_type: String, + season: Option, + episode: Option, +) -> Result { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|e| e.to_string())?; + let local = tokio::task::LocalSet::new(); + local.block_on(&rt, async move { + tokio::time::timeout( + Duration::from_secs(PLUGIN_TIMEOUT_SECS), + run(client, code, tmdb_id, media_type, season, episode), + ) + .await + .unwrap_or_else(|_| Err("plugin timed out".to_string())) + }) +} + +async fn run( + client: Arc, + code: String, + tmdb_id: String, + media_type: String, + season: Option, + episode: Option, +) -> Result { + let qjs_rt = AsyncRuntime::new().map_err(|e| e.to_string())?; + qjs_rt.set_memory_limit(PLUGIN_MEMORY_LIMIT).await; + let deadline = std::time::Instant::now() + Duration::from_secs(PLUGIN_TIMEOUT_SECS); + qjs_rt + .set_interrupt_handler(Some(Box::new(move || std::time::Instant::now() > deadline))) + .await; + tokio::task::spawn_local(qjs_rt.drive()); + let ctx = AsyncContext::full(&qjs_rt).await.map_err(|e| e.to_string())?; + + let captured: Arc>> = Default::default(); + let captured_clone = captured.clone(); + let dom = DomBridge::new(); + + let eval_result: rquickjs::Result<()> = ctx + .async_with(async |ctx| { + register_host_functions(&ctx, &dom, client)?; + + let tmdb_id_arg = serde_json::to_string(&tmdb_id).unwrap_or_else(|_| "\"\"".into()); + let media_type_arg = + serde_json::to_string(&media_type).unwrap_or_else(|_| "\"movie\"".into()); + let season_arg = season.map(|s| s.to_string()).unwrap_or_else(|| "undefined".into()); + let episode_arg = episode.map(|e| e.to_string()).unwrap_or_else(|| "undefined".into()); + + let script = format!( + r#" + globalThis.global = globalThis; + globalThis.window = globalThis; + + function fetch(url, options) {{ + options = options || {{}}; + var method = options.method || 'GET'; + var headersJson = JSON.stringify(options.headers || {{}}); + var body = options.body === undefined || options.body === null ? null : String(options.body); + var followRedirects = options.redirect !== 'manual'; + var raw = __native_fetch(url, method, headersJson, body, followRedirects); + var parsed = JSON.parse(raw); + return Promise.resolve({{ + ok: parsed.ok, + status: parsed.status, + text: function() {{ return Promise.resolve(parsed.body); }}, + json: function() {{ + try {{ return Promise.resolve(JSON.parse(parsed.body)); }} + catch (e) {{ return Promise.resolve(null); }} + }} + }}); + }} + + {base64_polyfill} + {text_encoder_polyfill} + {crypto_polyfill} + {cheerio_polyfill} + + var require = function(name) {{ + if (name.indexOf('cheerio') !== -1) return cheerio; + if (name === 'crypto-js') return CryptoJS; + throw new Error('module not available: ' + name); + }}; + + var module = {{ exports: {{}} }}; + var exports = module.exports; + (function() {{ + {code} + }})(); + + (async function() {{ + try {{ + var getStreams = module.exports.getStreams || globalThis.getStreams; + if (!getStreams) {{ + __capture_result(JSON.stringify([])); + return; + }} + var streams = await getStreams({tmdb_id_arg}, {media_type_arg}, {season_arg}, {episode_arg}); + __capture_result(JSON.stringify(streams || [])); + }} catch (e) {{ + __capture_result(JSON.stringify([])); + }} + }})(); + "#, + base64_polyfill = BASE64_POLYFILL, + text_encoder_polyfill = TEXT_ENCODER_POLYFILL, + crypto_polyfill = CRYPTO_POLYFILL, + cheerio_polyfill = CHEERIO_POLYFILL, + code = code, + ); + + ctx.globals().set( + "__capture_result", + Function::new(ctx.clone(), move |s: String| { + *captured_clone.lock().expect("capture lock poisoned") = Some(s); + })?, + )?; + + ctx.eval::<(), _>(script).catch(&ctx).map_err(|e| { + rquickjs::Error::new_from_js_message("plugin", "js", e.to_string()) + })?; + + Ok(()) + }) + .await; + + eval_result.map_err(|e| e.to_string())?; + qjs_rt.idle().await; + + let result = captured + .lock() + .expect("capture lock poisoned") + .take() + .unwrap_or_else(|| "[]".to_string()); + Ok(result) +} + +fn native_fetch( + client: &Arc, + url: String, + method: String, + headers_json: String, + body: Option, + follow_redirects: bool, +) -> String { + let headers: HashMap = + serde_json::from_str(&headers_json).unwrap_or_default(); + let response = client.fetch(PluginHttpRequest { + method, + url, + headers, + body, + follow_redirects, + }); + format!( + "{{\"ok\":{},\"status\":{},\"body\":{}}}", + response.ok, + response.status, + serde_json::to_string(&response.body).unwrap_or_else(|_| "\"\"".into()) + ) +} + +fn to_hex(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{b:02x}")).collect() +} + +fn from_hex(hex: &str) -> Vec { + let clean: String = hex.chars().filter(|c| c.is_ascii_hexdigit()).collect(); + (0..clean.len() / 2) + .filter_map(|i| u8::from_str_radix(clean.get(i * 2..i * 2 + 2)?, 16).ok()) + .collect() +} + +fn register_host_functions( + ctx: &Ctx<'_>, + dom: &Rc, + client: Arc, +) -> rquickjs::Result<()> { + ctx.globals().set( + "console", + rquickjs::Object::new(ctx.clone()).and_then(|obj| { + obj.set( + "log", + Function::new(ctx.clone(), |msg: String| log::debug!("[plugin] {msg}"))?, + )?; + obj.set( + "warn", + Function::new(ctx.clone(), |msg: String| log::warn!("[plugin] {msg}"))?, + )?; + obj.set( + "error", + Function::new(ctx.clone(), |msg: String| log::warn!("[plugin] {msg}"))?, + )?; + obj.set( + "info", + Function::new(ctx.clone(), |msg: String| log::debug!("[plugin] {msg}"))?, + )?; + obj.set( + "debug", + Function::new(ctx.clone(), |msg: String| log::debug!("[plugin] {msg}"))?, + )?; + Ok(obj) + })?, + )?; + + ctx.globals().set( + "__native_fetch", + Function::new( + ctx.clone(), + move |url: String, method: String, headers_json: String, body: Option, follow_redirects: bool| { + native_fetch(&client, url, method, headers_json, body, follow_redirects) + }, + )?, + )?; + + let d = dom.clone(); + ctx.globals().set( + "__cheerio_load", + Function::new(ctx.clone(), move |html: String| d.load(html))?, + )?; + + let d = dom.clone(); + ctx.globals().set( + "__cheerio_select", + Function::new(ctx.clone(), move |doc_id: String, selector: String| { + d.select(doc_id, selector) + })?, + )?; + + let d = dom.clone(); + ctx.globals().set( + "__cheerio_find", + Function::new( + ctx.clone(), + move |doc_id: String, element_id: String, selector: String| d.find(doc_id, element_id, selector), + )?, + )?; + + let d = dom.clone(); + ctx.globals().set( + "__cheerio_text", + Function::new(ctx.clone(), move |_doc_id: String, element_ids: String| { + d.text(element_ids) + })?, + )?; + + let d = dom.clone(); + ctx.globals().set( + "__cheerio_html", + Function::new(ctx.clone(), move |doc_id: String, element_id: String| { + d.html(doc_id, element_id) + })?, + )?; + + let d = dom.clone(); + ctx.globals().set( + "__cheerio_inner_html", + Function::new(ctx.clone(), move |_doc_id: String, element_id: String| { + d.inner_html(element_id) + })?, + )?; + + let d = dom.clone(); + ctx.globals().set( + "__cheerio_attr", + Function::new(ctx.clone(), move |_doc_id: String, element_id: String, attr_name: String| { + d.attr(element_id, attr_name) + })?, + )?; + + let d = dom.clone(); + ctx.globals().set( + "__cheerio_next", + Function::new(ctx.clone(), move |doc_id: String, element_id: String| { + d.next(doc_id, element_id) + })?, + )?; + + let d = dom.clone(); + ctx.globals().set( + "__cheerio_prev", + Function::new(ctx.clone(), move |doc_id: String, element_id: String| { + d.prev(doc_id, element_id) + })?, + )?; + + ctx.globals().set( + "__crypto_get_random_values_hex", + Function::new(ctx.clone(), |len: usize| to_hex(&crypto_bridge::random_bytes(len)))?, + )?; + + ctx.globals().set( + "__crypto_digest_hex_raw", + Function::new(ctx.clone(), |algorithm: String, data_hex: String| { + crypto_bridge::digest(&algorithm, &from_hex(&data_hex)) + .map(|bytes| to_hex(&bytes)) + .unwrap_or_default() + })?, + )?; + + ctx.globals().set( + "__crypto_hmac_hex_raw", + Function::new(ctx.clone(), |algorithm: String, key_hex: String, data_hex: String| { + crypto_bridge::hmac(&algorithm, &from_hex(&key_hex), &from_hex(&data_hex)) + .map(|bytes| to_hex(&bytes)) + .unwrap_or_default() + })?, + )?; + + ctx.globals().set( + "__crypto_pbkdf2_hex", + Function::new( + ctx.clone(), + |password_hex: String, salt_hex: String, iterations: u32, key_size_bits: u32, algorithm: String| { + crypto_bridge::pbkdf2( + &from_hex(&password_hex), + &from_hex(&salt_hex), + iterations, + key_size_bits, + &algorithm, + ) + .map(|bytes| to_hex(&bytes)) + .unwrap_or_default() + }, + )?, + )?; + + ctx.globals().set( + "__crypto_aes_encrypt_hex", + Function::new( + ctx.clone(), + |mode: String, key_hex: String, iv_hex: String, data_hex: String| { + crypto_bridge::aes_encrypt(&mode, &from_hex(&key_hex), &from_hex(&iv_hex), &from_hex(&data_hex)) + .map(|bytes| to_hex(&bytes)) + .unwrap_or_default() + }, + )?, + )?; + + ctx.globals().set( + "__crypto_aes_decrypt_hex", + Function::new( + ctx.clone(), + |mode: String, key_hex: String, iv_hex: String, data_hex: String| { + crypto_bridge::aes_decrypt(&mode, &from_hex(&key_hex), &from_hex(&iv_hex), &from_hex(&data_hex)) + .map(|bytes| to_hex(&bytes)) + .unwrap_or_default() + }, + )?, + )?; + + ctx.globals().set( + "__crypto_utf8_to_hex", + Function::new(ctx.clone(), |text: String| to_hex(text.as_bytes()))?, + )?; + + ctx.globals().set( + "__crypto_hex_to_utf8", + Function::new(ctx.clone(), |hex: String| { + String::from_utf8_lossy(&from_hex(&hex)).into_owned() + })?, + )?; + + ctx.globals().set( + "__crypto_sign_hex", + Function::new(ctx.clone(), |algorithm: String, key_hex: String, data_hex: String| { + crypto_bridge::sign(&algorithm, &from_hex(&key_hex), &from_hex(&data_hex)) + .map(|bytes| to_hex(&bytes)) + .unwrap_or_default() + })?, + )?; + + ctx.globals().set( + "__crypto_verify_hex", + Function::new( + ctx.clone(), + |algorithm: String, key_hex: String, signature_hex: String, data_hex: String| { + crypto_bridge::verify( + &algorithm, + &from_hex(&key_hex), + &from_hex(&signature_hex), + &from_hex(&data_hex), + ) + .unwrap_or(false) + }, + )?, + )?; + + Ok(()) +} + +const BASE64_POLYFILL: &str = r#" +if (typeof atob === 'undefined') { + globalThis.atob = function(input) { + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var str = String(input).replace(/=+$/, ''); + if (str.length % 4 === 1) throw new Error('InvalidCharacterError'); + var output = ''; + var bc = 0, bs, buffer, idx = 0; + while ((buffer = str.charAt(idx++))) { + buffer = chars.indexOf(buffer); + if (buffer === -1) continue; + bs = bc % 4 ? bs * 64 + buffer : buffer; + if (bc++ % 4) output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6))); + } + return output; + }; +} + +if (typeof btoa === 'undefined') { + globalThis.btoa = function(input) { + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; + var str = String(input); + var output = ''; + for (var block, charCode, idx = 0, map = chars; + str.charAt(idx | 0) || (map = '=', idx % 1); + output += map.charAt(63 & (block >> (8 - (idx % 1) * 8)))) { + charCode = str.charCodeAt(idx += 3 / 4); + if (charCode > 0xFF) throw new Error('InvalidCharacterError'); + block = (block << 8) | charCode; + } + return output; + }; +} +"#; + +const TEXT_ENCODER_POLYFILL: &str = r#" +if (typeof TextEncoder === 'undefined') { + globalThis.TextEncoder = function() {}; + TextEncoder.prototype.encode = function(str) { + var hex = __crypto_utf8_to_hex(str); + var bytes = new Uint8Array(hex.length / 2); + for (var i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); + } + return bytes; + }; +} +if (typeof TextDecoder === 'undefined') { + globalThis.TextDecoder = function() {}; + TextDecoder.prototype.decode = function(data) { + var bytes = data; + if (data instanceof ArrayBuffer) bytes = new Uint8Array(data); + var hex = ''; + for (var i = 0; i < bytes.length; i++) { + hex += bytes[i].toString(16).padStart(2, '0'); + } + return __crypto_hex_to_utf8(hex); + }; +} +"#; + +const CRYPTO_POLYFILL: &str = r#" +var WordArray = { + init: function(words, sigBytes) { + this.words = words || []; + this.sigBytes = sigBytes != undefined ? sigBytes : this.words.length * 4; + }, + toString: function(encoder) { + return (encoder || CryptoJS.enc.Hex).stringify(this); + }, + concat: function(wordArray) { + var thisWords = this.words; + var thatWords = wordArray.words; + var thisSigBytes = this.sigBytes; + var thatSigBytes = wordArray.sigBytes; + + this.clamp(); + + for (var i = 0; i < thatSigBytes; i++) { + var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8); + } + this.sigBytes += thatSigBytes; + return this; + }, + clamp: function() { + var words = this.words; + var sigBytes = this.sigBytes; + if (sigBytes % 4) { + words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8); + } + words.length = Math.ceil(sigBytes / 4); + return this; + }, + clone: function() { + return __wordArrayCreate(this.words.slice(0), this.sigBytes); + } +}; + +function __wordArrayCreate(words, sigBytes) { + var wa = Object.create(WordArray); + wa.init(words, sigBytes); + return wa; +} + +function __isWordArray(value) { + return value && typeof value === 'object' && Array.isArray(value.words) && typeof value.sigBytes === 'number'; +} + +function __copyUint8Array(bytes) { + bytes = __toUint8Array(bytes); + var copy = new Uint8Array(bytes.length); + copy.set(bytes); + return copy; +} + +function __toUint8Array(data) { + if (!data) return new Uint8Array(0); + if (data instanceof Uint8Array) return data; + if (data instanceof ArrayBuffer) return new Uint8Array(data); + if (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView && ArrayBuffer.isView(data)) { + return new Uint8Array(data.buffer, data.byteOffset || 0, data.byteLength); + } + if (Array.isArray(data)) return new Uint8Array(data); + if (typeof data.length === 'number') return new Uint8Array(Array.prototype.slice.call(data)); + return new Uint8Array(0); +} + +function __bytesToArrayBuffer(bytes) { + return __copyUint8Array(bytes).buffer; +} + +function __wordArrayToBytes(wordArray) { + if (!__isWordArray(wordArray)) return typeof wordArray === 'string' ? new TextEncoder().encode(wordArray) : __toUint8Array(wordArray); + var bytes = new Uint8Array(wordArray.sigBytes); + for (var i = 0; i < wordArray.sigBytes; i++) { + bytes[i] = (wordArray.words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff; + } + return bytes; +} + +function __bytesToWordArray(bytes) { + bytes = __toUint8Array(bytes); + var words = []; + for (var i = 0; i < bytes.length; i++) { + words[i >>> 2] |= (bytes[i] & 0xff) << (24 - (i % 4) * 8); + } + return __wordArrayCreate(words, bytes.length); +} + +function __normalizeWordArrayInput(value) { + if (__isWordArray(value)) return __wordArrayToBytes(value); + if (typeof value === 'string') return new TextEncoder().encode(value); + return __toUint8Array(value); +} + +function __bytesToHex(bytes) { + bytes = __toUint8Array(bytes); + var out = []; + for (var i = 0; i < bytes.length; i++) { + var hex = bytes[i].toString(16); + out.push(hex.length < 2 ? '0' + hex : hex); + } + return out.join(''); +} + +function __hexToBytes(hex) { + hex = String(hex || '').replace(/[^0-9a-fA-F]/g, ''); + if (hex.length % 2) hex = '0' + hex; + var bytes = new Uint8Array(hex.length / 2); + for (var i = 0; i < hex.length; i += 2) { + bytes[i / 2] = parseInt(hex.substr(i, 2), 16) & 0xff; + } + return bytes; +} + +function __concatBytes() { + var total = 0; + var parts = []; + for (var i = 0; i < arguments.length; i++) { + var part = __toUint8Array(arguments[i]); + parts.push(part); + total += part.length; + } + var out = new Uint8Array(total); + var offset = 0; + for (var j = 0; j < parts.length; j++) { + out.set(parts[j], offset); + offset += parts[j].length; + } + return out; +} + +function __normalizeHashName(hash) { + var name = hash && hash.name ? hash.name : hash; + name = String(name || 'SHA-256').toUpperCase().replace(/[^A-Z0-9]/g, ''); + if (name === 'SHA1' || name === 'SHA256' || name === 'SHA384' || name === 'SHA512' || name === 'MD5') return name; + throw new Error('Unsupported hash algorithm: ' + name); +} + +function __normalizeAlgorithmName(algo) { + var name = algo && algo.name ? algo.name : algo; + name = String(name || '').toUpperCase(); + if (name.indexOf('AES-GCM') >= 0) return 'AES-GCM'; + if (name.indexOf('AES-CBC') >= 0) return 'AES-CBC'; + if (name.indexOf('AES-ECB') >= 0 || name === 'ECB') return 'AES-ECB'; + if (name.indexOf('PBKDF2') >= 0) return 'PBKDF2'; + if (name.indexOf('HMAC') >= 0) return 'HMAC'; + if (name.indexOf('RSASSA-PKCS1') >= 0) return 'RSASSA-PKCS1-V1_5'; + if (name.indexOf('ECDSA') >= 0) return 'ECDSA'; + return name; +} + +function __aesModeName(mode, padding) { + var normalized = __normalizeAlgorithmName(mode || 'AES-CBC'); + if (padding === CryptoJS.pad.NoPadding || padding === 'NoPadding') normalized += '-NoPadding'; + return normalized; +} + +function __nativeDigestBytes(hash, dataBytes) { + if (typeof __crypto_digest_hex_raw === 'undefined') throw new Error('Native digest bridge is unavailable'); + return __hexToBytes(__crypto_digest_hex_raw(__normalizeHashName(hash), __bytesToHex(dataBytes))); +} + +function __nativeHmacBytes(hash, keyBytes, dataBytes) { + if (typeof __crypto_hmac_hex_raw === 'undefined') throw new Error('Native HMAC bridge is unavailable'); + return __hexToBytes(__crypto_hmac_hex_raw(__normalizeHashName(hash), __bytesToHex(keyBytes), __bytesToHex(dataBytes))); +} + +function __nativePbkdf2Bytes(passwordBytes, saltBytes, iterations, keySizeBits, hash) { + if (typeof __crypto_pbkdf2_hex === 'undefined') throw new Error('Native PBKDF2 bridge is unavailable'); + return __hexToBytes(__crypto_pbkdf2_hex(__bytesToHex(passwordBytes), __bytesToHex(saltBytes), iterations, keySizeBits, __normalizeHashName(hash))); +} + +function __nativeAesBytes(encrypt, mode, keyBytes, ivBytes, dataBytes) { + var fn = encrypt ? __crypto_aes_encrypt_hex : __crypto_aes_decrypt_hex; + if (typeof fn === 'undefined') throw new Error('Native AES bridge is unavailable'); + return __hexToBytes(fn(mode, __bytesToHex(keyBytes), __bytesToHex(ivBytes), __bytesToHex(dataBytes))); +} + +function __evpKdf(passwordBytes, saltBytes, keySizeBytes, ivSizeBytes) { + var targetSize = keySizeBytes + ivSizeBytes; + var derived = new Uint8Array(targetSize); + var block = new Uint8Array(0); + var offset = 0; + while (offset < targetSize) { + block = __nativeDigestBytes('MD5', __concatBytes(block, passwordBytes, saltBytes || new Uint8Array(0))); + var take = Math.min(block.length, targetSize - offset); + derived.set(block.subarray(0, take), offset); + offset += take; + } + return { + key: derived.subarray(0, keySizeBytes), + iv: derived.subarray(keySizeBytes, keySizeBytes + ivSizeBytes) + }; +} + +function __opensslSaltHeader() { + return new Uint8Array([83, 97, 108, 116, 101, 100, 95, 95]); +} + +function __hasOpenSslSaltHeader(bytes) { + var header = __opensslSaltHeader(); + if (!bytes || bytes.length < 16) return false; + for (var i = 0; i < header.length; i++) { + if (bytes[i] !== header[i]) return false; + } + return true; +} + +function __makeCipherParams(ciphertext, key, iv, salt, mode) { + return { + ciphertext: __bytesToWordArray(ciphertext), + key: key ? __bytesToWordArray(key) : undefined, + iv: iv ? __bytesToWordArray(iv) : undefined, + salt: salt ? __bytesToWordArray(salt) : undefined, + mode: mode, + toString: function(formatter) { + return (formatter || CryptoJS.format.OpenSSL).stringify(this); + } + }; +} + +var CryptoJS = { + enc: { + Hex: { + stringify: function(wordArray) { + return __bytesToHex(__wordArrayToBytes(wordArray)); + }, + parse: function(hexStr) { + return __bytesToWordArray(__hexToBytes(hexStr)); + } + }, + Utf8: { + stringify: function(wordArray) { + return new TextDecoder('utf-8').decode(__wordArrayToBytes(wordArray)); + }, + parse: function(utf8Str) { + return __bytesToWordArray(new TextEncoder().encode(String(utf8Str))); + } + }, + Latin1: { + stringify: function(wordArray) { + var bytes = __wordArrayToBytes(wordArray); + var out = ''; + for (var i = 0; i < bytes.length; i++) out += String.fromCharCode(bytes[i]); + return out; + }, + parse: function(str) { + str = String(str || ''); + var bytes = new Uint8Array(str.length); + for (var i = 0; i < str.length; i++) bytes[i] = str.charCodeAt(i) & 0xff; + return __bytesToWordArray(bytes); + } + }, + Base64: { + stringify: function(wordArray) { + var bytes = __wordArrayToBytes(wordArray); + var binaryStr = ''; + for (var j = 0; j < bytes.length; j++) binaryStr += String.fromCharCode(bytes[j]); + return btoa(binaryStr); + }, + parse: function(base64Str) { + var binaryStr = atob(String(base64Str || '')); + var bytes = new Uint8Array(binaryStr.length); + for (var i = 0; i < binaryStr.length; i++) bytes[i] = binaryStr.charCodeAt(i) & 0xff; + return __bytesToWordArray(bytes); + } + }, + Base64url: { + stringify: function(wordArray) { + return CryptoJS.enc.Base64.stringify(wordArray).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, ''); + }, + parse: function(str) { + str = String(str || '').replace(/-/g, '+').replace(/_/g, '/'); + while (str.length % 4) str += '='; + return CryptoJS.enc.Base64.parse(str); + } + } + }, + lib: { + WordArray: { + create: function(words, sigBytes) { + if (words == null) return __wordArrayCreate([], sigBytes || 0); + if (__isWordArray(words)) return words.clone(); + if (typeof words === 'string') return CryptoJS.enc.Utf8.parse(words); + if (words instanceof ArrayBuffer || (typeof ArrayBuffer !== 'undefined' && ArrayBuffer.isView && ArrayBuffer.isView(words))) { + var bytes = __toUint8Array(words); + return __bytesToWordArray(sigBytes != undefined ? bytes.subarray(0, sigBytes) : bytes); + } + return __wordArrayCreate(words, sigBytes); + }, + random: function(nBytes) { + var bytes = new Uint8Array(nBytes || 0); + globalThis.crypto.getRandomValues(bytes); + return __bytesToWordArray(bytes); + } + }, + CipherParams: { + create: function(params) { + params = params || {}; + params.toString = params.toString || function(formatter) { + return (formatter || CryptoJS.format.OpenSSL).stringify(this); + }; + return params; + } + } + }, + format: { + OpenSSL: { + stringify: function(cipherParams) { + var cipherBytes = __wordArrayToBytes(cipherParams.ciphertext); + var out = cipherParams.salt + ? __concatBytes(__opensslSaltHeader(), __wordArrayToBytes(cipherParams.salt), cipherBytes) + : cipherBytes; + return CryptoJS.enc.Base64.stringify(__bytesToWordArray(out)); + }, + parse: function(str) { + var bytes = __wordArrayToBytes(CryptoJS.enc.Base64.parse(str)); + if (__hasOpenSslSaltHeader(bytes)) { + return CryptoJS.lib.CipherParams.create({ + salt: __bytesToWordArray(bytes.subarray(8, 16)), + ciphertext: __bytesToWordArray(bytes.subarray(16)) + }); + } + return CryptoJS.lib.CipherParams.create({ ciphertext: __bytesToWordArray(bytes) }); + } + } + }, + mode: { CBC: 'AES-CBC', GCM: 'AES-GCM', ECB: 'AES-ECB' }, + pad: { Pkcs7: 'Pkcs7', NoPadding: 'NoPadding' }, + algo: { MD5: 'MD5', SHA1: 'SHA1', SHA256: 'SHA256', SHA384: 'SHA384', SHA512: 'SHA512', AES: 'AES' }, + MD5: function(m) { return __bytesToWordArray(__nativeDigestBytes('MD5', __normalizeWordArrayInput(m))); }, + SHA1: function(m) { return __bytesToWordArray(__nativeDigestBytes('SHA1', __normalizeWordArrayInput(m))); }, + SHA256: function(m) { return __bytesToWordArray(__nativeDigestBytes('SHA256', __normalizeWordArrayInput(m))); }, + SHA384: function(m) { return __bytesToWordArray(__nativeDigestBytes('SHA384', __normalizeWordArrayInput(m))); }, + SHA512: function(m) { return __bytesToWordArray(__nativeDigestBytes('SHA512', __normalizeWordArrayInput(m))); }, + HmacMD5: function(m, k) { return __bytesToWordArray(__nativeHmacBytes('MD5', __normalizeWordArrayInput(k), __normalizeWordArrayInput(m))); }, + HmacSHA1: function(m, k) { return __bytesToWordArray(__nativeHmacBytes('SHA1', __normalizeWordArrayInput(k), __normalizeWordArrayInput(m))); }, + HmacSHA256: function(m, k) { return __bytesToWordArray(__nativeHmacBytes('SHA256', __normalizeWordArrayInput(k), __normalizeWordArrayInput(m))); }, + HmacSHA384: function(m, k) { return __bytesToWordArray(__nativeHmacBytes('SHA384', __normalizeWordArrayInput(k), __normalizeWordArrayInput(m))); }, + HmacSHA512: function(m, k) { return __bytesToWordArray(__nativeHmacBytes('SHA512', __normalizeWordArrayInput(k), __normalizeWordArrayInput(m))); }, + PBKDF2: function(pass, salt, options) { + options = options || {}; + var pBytes = __normalizeWordArrayInput(pass); + var sBytes = __normalizeWordArrayInput(salt); + var iter = options.iterations || 1000; + var kSize = options.keySize || 8; + var algo = options.hasher || 'SHA1'; + return __bytesToWordArray(__nativePbkdf2Bytes(pBytes, sBytes, iter, kSize * 32, algo)); + }, + AES: { + encrypt: function(message, key, options) { + options = options || {}; + var data = __normalizeWordArrayInput(message); + var kBytes; + var ivBytes; + var saltBytes; + var isPassphrase = typeof key === 'string'; + if (isPassphrase) { + saltBytes = options.salt ? __wordArrayToBytes(options.salt) : __wordArrayToBytes(CryptoJS.lib.WordArray.random(8)); + var derived = __evpKdf(new TextEncoder().encode(key), saltBytes, 32, 16); + kBytes = derived.key; + ivBytes = options.iv ? __wordArrayToBytes(options.iv) : derived.iv; + } else { + kBytes = __wordArrayToBytes(key); + ivBytes = options.iv ? __wordArrayToBytes(options.iv) : new Uint8Array(0); + } + var mode = __aesModeName(options.mode || 'AES-CBC', options.padding); + var resBytes = __nativeAesBytes(true, mode, kBytes, ivBytes, data); + return __makeCipherParams(resBytes, kBytes, ivBytes, saltBytes, mode); + }, + decrypt: function(cipher, key, options) { + options = options || {}; + var cipherParams = typeof cipher === 'string' ? CryptoJS.format.OpenSSL.parse(cipher) : cipher; + var data = cipherParams.ciphertext ? __wordArrayToBytes(cipherParams.ciphertext) : __toUint8Array(cipherParams); + var kBytes; + var ivBytes; + var isPassphrase = typeof key === 'string'; + if (isPassphrase) { + var saltBytes = options.salt ? __wordArrayToBytes(options.salt) : (cipherParams.salt ? __wordArrayToBytes(cipherParams.salt) : new Uint8Array(0)); + var derived = __evpKdf(new TextEncoder().encode(key), saltBytes, 32, 16); + kBytes = derived.key; + ivBytes = options.iv ? __wordArrayToBytes(options.iv) : derived.iv; + } else { + kBytes = __wordArrayToBytes(key); + ivBytes = options.iv ? __wordArrayToBytes(options.iv) : new Uint8Array(0); + } + var mode = __aesModeName(options.mode || 'AES-CBC', options.padding); + return __bytesToWordArray(__nativeAesBytes(false, mode, kBytes, ivBytes, data)); + } + } +}; +globalThis.CryptoJS = CryptoJS; + +function __makeCryptoKey(type, algorithm, extractable, usages, rawBytes) { + return { + type: type, + extractable: !!extractable, + algorithm: algorithm, + usages: usages || [], + _raw: __copyUint8Array(rawBytes) + }; +} + +function __webCryptoAlgorithm(algo) { + var name = __normalizeAlgorithmName(algo); + var out = { name: name }; + if (algo && typeof algo === 'object' && algo.length) out.length = algo.length; + if (algo && typeof algo === 'object' && algo.hash) out.hash = { name: __normalizeHashName(algo.hash) }; + return out; +} + +function __signatureAlgorithmName(algo, key) { + var name = __normalizeAlgorithmName(algo || (key && key.algorithm)); + var hash = algo && algo.hash ? __normalizeHashName(algo.hash) : (key && key.algorithm && key.algorithm.hash ? key.algorithm.hash.name : 'SHA256'); + if (name === 'RSASSA-PKCS1-V1_5') return 'RSASSA-PKCS1-V1_5-' + hash; + if (name === 'ECDSA') return 'ECDSA-' + hash; + return name; +} + +globalThis.crypto = { + subtle: { + digest: async function(algo, data) { + return __bytesToArrayBuffer(__nativeDigestBytes(algo, __toUint8Array(data))); + }, + importKey: async function(fmt, data, algo, extractable, usages) { + fmt = String(fmt || 'raw').toLowerCase(); + if (fmt !== 'raw' && fmt !== 'pkcs8' && fmt !== 'spki') throw new Error('Unsupported key format: ' + fmt); + var algorithm = __webCryptoAlgorithm(algo || {}); + var type = fmt === 'spki' ? 'public' : (fmt === 'pkcs8' ? 'private' : 'secret'); + return __makeCryptoKey(type, algorithm, extractable, usages || [], __toUint8Array(data)); + }, + exportKey: async function(fmt, key) { + fmt = String(fmt || 'raw').toLowerCase(); + if (fmt !== 'raw' && fmt !== 'pkcs8' && fmt !== 'spki') throw new Error('Unsupported key format: ' + fmt); + return __bytesToArrayBuffer(key._raw); + }, + generateKey: async function(algo, extractable, usages) { + var algorithm = __webCryptoAlgorithm(algo || {}); + if (algorithm.name !== 'AES-CBC' && algorithm.name !== 'AES-GCM' && algorithm.name !== 'HMAC') { + throw new Error('Unsupported generateKey algorithm: ' + algorithm.name); + } + var length = algorithm.length || 256; + var bytes = new Uint8Array(length / 8); + globalThis.crypto.getRandomValues(bytes); + return __makeCryptoKey('secret', algorithm, extractable, usages || [], bytes); + }, + deriveBits: async function(params, key, len) { + if (__normalizeAlgorithmName(params) !== 'PBKDF2') throw new Error('Only PBKDF2 deriveBits is supported'); + var pBytes = __toUint8Array(key._raw); + var sBytes = __toUint8Array(params.salt); + var hash = params.hash || 'SHA-256'; + return __bytesToArrayBuffer(__nativePbkdf2Bytes(pBytes, sBytes, params.iterations || 1000, len, hash)); + }, + deriveKey: async function(params, key, derivedKeyAlgo, extractable, usages) { + var algorithm = __webCryptoAlgorithm(derivedKeyAlgo || {}); + var length = algorithm.length || 256; + var raw = await globalThis.crypto.subtle.deriveBits(params, key, length); + return __makeCryptoKey('secret', algorithm, extractable, usages || [], new Uint8Array(raw)); + }, + encrypt: async function(params, key, data) { + var mode = __normalizeAlgorithmName(params); + if (mode !== 'AES-CBC' && mode !== 'AES-GCM') throw new Error('Unsupported encrypt algorithm: ' + mode); + var ivBytes = __toUint8Array(params.iv || new Uint8Array(0)); + return __bytesToArrayBuffer(__nativeAesBytes(true, mode, __toUint8Array(key._raw), ivBytes, __toUint8Array(data))); + }, + decrypt: async function(params, key, data) { + var mode = __normalizeAlgorithmName(params); + if (mode !== 'AES-CBC' && mode !== 'AES-GCM') throw new Error('Unsupported decrypt algorithm: ' + mode); + var ivBytes = __toUint8Array(params.iv || new Uint8Array(0)); + return __bytesToArrayBuffer(__nativeAesBytes(false, mode, __toUint8Array(key._raw), ivBytes, __toUint8Array(data))); + }, + sign: async function(algo, key, data) { + if (__normalizeAlgorithmName(algo || key.algorithm) === 'HMAC' || key.algorithm.name === 'HMAC') { + var hash = (algo && algo.hash) || (key.algorithm && key.algorithm.hash) || 'SHA-256'; + return __bytesToArrayBuffer(__nativeHmacBytes(hash, __toUint8Array(key._raw), __toUint8Array(data))); + } + if (typeof __crypto_sign_hex === 'undefined') throw new Error('Native signature bridge is unavailable'); + var sigHex = __crypto_sign_hex(__signatureAlgorithmName(algo, key), __bytesToHex(key._raw), __bytesToHex(__toUint8Array(data))); + return __bytesToArrayBuffer(__hexToBytes(sigHex)); + }, + verify: async function(algo, key, sig, data) { + if (__normalizeAlgorithmName(algo || key.algorithm) === 'HMAC' || key.algorithm.name === 'HMAC') { + var expected = __nativeHmacBytes((algo && algo.hash) || (key.algorithm && key.algorithm.hash) || 'SHA-256', __toUint8Array(key._raw), __toUint8Array(data)); + var actual = __toUint8Array(sig); + if (expected.length !== actual.length) return false; + var diff = 0; + for (var i = 0; i < expected.length; i++) diff |= expected[i] ^ actual[i]; + return diff === 0; + } + if (typeof __crypto_verify_hex === 'undefined') throw new Error('Native signature bridge is unavailable'); + return __crypto_verify_hex(__signatureAlgorithmName(algo, key), __bytesToHex(key._raw), __bytesToHex(__toUint8Array(sig)), __bytesToHex(__toUint8Array(data))); + } + }, + getRandomValues: function(arr) { + if (!arr) return arr; + var byteLength = arr.byteLength != undefined ? arr.byteLength : arr.length; + if (!byteLength) return arr; + if (typeof __crypto_get_random_values_hex === 'undefined') throw new Error('Native random bridge is unavailable'); + var random = __hexToBytes(__crypto_get_random_values_hex(byteLength)); + if (arr.buffer && arr.byteLength != undefined) { + new Uint8Array(arr.buffer, arr.byteOffset || 0, arr.byteLength).set(random); + } else { + for (var i = 0; i < arr.length; i++) arr[i] = random[i] || 0; + } + return arr; + }, + randomUUID: function() { + var b = new Uint8Array(16); + globalThis.crypto.getRandomValues(b); + b[6] = (b[6] & 0x0f) | 0x40; + b[8] = (b[8] & 0x3f) | 0x80; + var h = __bytesToHex(b); + return h.substr(0, 8) + '-' + h.substr(8, 4) + '-' + h.substr(12, 4) + '-' + h.substr(16, 4) + '-' + h.substr(20); + } +}; +"#; + +const CHEERIO_POLYFILL: &str = r#" +var cheerio = { + load: function(html) { + var docId = __cheerio_load(html); + var $ = function(selector, context) { + if (selector && selector._elementIds) return selector; + if (context && context._elementIds && context._elementIds.length > 0) { + var allIds = []; + for (var i = 0; i < context._elementIds.length; i++) { + var childIdsJson = __cheerio_find(docId, context._elementIds[i], selector); + var childIds = JSON.parse(childIdsJson); + allIds = allIds.concat(childIds); + } + return createCheerioWrapperFromIds(docId, allIds); + } + return createCheerioWrapper(docId, selector); + }; + $.html = function(el) { + if (el && el._elementIds && el._elementIds.length > 0) { + return __cheerio_html(docId, el._elementIds[0]); + } + return __cheerio_html(docId, ''); + }; + return $; + } +}; + +function createCheerioWrapper(docId, selector) { + var elementIds; + if (typeof selector === 'string') { + var idsJson = __cheerio_select(docId, selector); + elementIds = JSON.parse(idsJson); + } else { + elementIds = []; + } + return createCheerioWrapperFromIds(docId, elementIds); +} + +function createCheerioWrapperFromIds(docId, ids) { + var wrapper = { + _docId: docId, + _elementIds: ids, + length: ids.length, + each: function(callback) { + for (var i = 0; i < ids.length; i++) { + var elWrapper = createCheerioWrapperFromIds(docId, [ids[i]]); + callback.call(elWrapper, i, elWrapper); + } + return wrapper; + }, + find: function(sel) { + var allIds = []; + for (var i = 0; i < ids.length; i++) { + var childIdsJson = __cheerio_find(docId, ids[i], sel); + var childIds = JSON.parse(childIdsJson); + allIds = allIds.concat(childIds); + } + return createCheerioWrapperFromIds(docId, allIds); + }, + text: function() { + if (ids.length === 0) return ''; + return __cheerio_text(docId, ids.join(',')); + }, + html: function() { + if (ids.length === 0) return ''; + return __cheerio_inner_html(docId, ids[0]); + }, + attr: function(name) { + if (ids.length === 0) return undefined; + var val = __cheerio_attr(docId, ids[0], name); + return val === '__UNDEFINED__' ? undefined : val; + }, + first: function() { return createCheerioWrapperFromIds(docId, ids.length > 0 ? [ids[0]] : []); }, + last: function() { return createCheerioWrapperFromIds(docId, ids.length > 0 ? [ids[ids.length - 1]] : []); }, + next: function() { + var nextIds = []; + for (var i = 0; i < ids.length; i++) { + var nextId = __cheerio_next(docId, ids[i]); + if (nextId && nextId !== '__NONE__') nextIds.push(nextId); + } + return createCheerioWrapperFromIds(docId, nextIds); + }, + prev: function() { + var prevIds = []; + for (var i = 0; i < ids.length; i++) { + var prevId = __cheerio_prev(docId, ids[i]); + if (prevId && prevId !== '__NONE__') prevIds.push(prevId); + } + return createCheerioWrapperFromIds(docId, prevIds); + }, + eq: function(index) { + if (index >= 0 && index < ids.length) return createCheerioWrapperFromIds(docId, [ids[index]]); + return createCheerioWrapperFromIds(docId, []); + }, + get: function(index) { + if (typeof index === 'number') { + if (index >= 0 && index < ids.length) return createCheerioWrapperFromIds(docId, [ids[index]]); + return undefined; + } + return ids.map(function(id) { return createCheerioWrapperFromIds(docId, [id]); }); + }, + map: function(callback) { + var results = []; + for (var i = 0; i < ids.length; i++) { + var elWrapper = createCheerioWrapperFromIds(docId, [ids[i]]); + var result = callback.call(elWrapper, i, elWrapper); + if (result !== undefined && result !== null) results.push(result); + } + return { + length: results.length, + get: function(index) { return typeof index === 'number' ? results[index] : results; }, + toArray: function() { return results; } + }; + }, + filter: function(selectorOrCallback) { + if (typeof selectorOrCallback === 'function') { + var filteredIds = []; + for (var i = 0; i < ids.length; i++) { + var elWrapper = createCheerioWrapperFromIds(docId, [ids[i]]); + var result = selectorOrCallback.call(elWrapper, i, elWrapper); + if (result) filteredIds.push(ids[i]); + } + return createCheerioWrapperFromIds(docId, filteredIds); + } + return wrapper; + }, + children: function(sel) { return this.find(sel || '*'); }, + parent: function() { return createCheerioWrapperFromIds(docId, []); }, + toArray: function() { return ids.map(function(id) { return createCheerioWrapperFromIds(docId, [id]); }); } + }; + return wrapper; +} +"#; + +#[cfg(test)] +mod tests { + use super::*; + + struct MockHttpClient; + + impl PluginHttpClient for MockHttpClient { + fn fetch(&self, _request: PluginHttpRequest) -> PluginHttpResponse { + PluginHttpResponse { + status: 0, + headers: HashMap::new(), + body: String::new(), + ok: false, + error: Some("network disabled in unit tests".to_string()), + } + } + } + + fn mock_client() -> Arc { + Arc::new(MockHttpClient) + } + + fn run_scraper(code: &str, tmdb_id: &str, media_type: &str) -> String { + execute_scraper(mock_client(), code.to_string(), tmdb_id.to_string(), media_type.to_string(), None, None) + .unwrap() + } + + #[test] + fn scraper_can_use_cheerio_and_module_exports_without_network() { + let code = r#" + module.exports.getStreams = async function(tmdbId, mediaType) { + var $ = cheerio.load('
Alpha
'); + var el = $('.row'); + return [{ title: el.attr('data-q'), url: 'https://example.com/' + tmdbId + '-' + mediaType }]; + }; + "#; + + let result = run_scraper(code, "123", "movie"); + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert_eq!(parsed[0]["title"], "1080p"); + assert_eq!(parsed[0]["url"], "https://example.com/123-movie"); + } + + #[test] + fn scraper_that_throws_returns_empty_array_instead_of_erroring() { + let code = r#" + module.exports.getStreams = async function() { + throw new Error("boom"); + }; + "#; + + let result = run_scraper(code, "1", "movie"); + assert_eq!(result, "[]"); + } + + #[test] + fn fetch_bridge_surfaces_host_rejection_as_not_ok() { + let code = r#" + module.exports.getStreams = async function() { + var res = await fetch('https://example.com/secret'); + return [{ title: 'x', url: 'https://example.com/blocked', ok: res.ok }]; + }; + "#; + + let result = run_scraper(code, "1", "movie"); + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert_eq!(parsed[0]["ok"], false); + } + + #[test] + fn cryptojs_sha256_matches_known_vector() { + let code = r#" + module.exports.getStreams = async function() { + var hash = CryptoJS.SHA256('abc').toString(CryptoJS.enc.Hex); + return [{ title: hash, url: 'https://example.com/x' }]; + }; + "#; + let result = run_scraper(code, "1", "movie"); + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert_eq!( + parsed[0]["title"], + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" + ); + } + + #[test] + fn cryptojs_hmac_sha256_matches_known_vector() { + let code = r#" + module.exports.getStreams = async function() { + var mac = CryptoJS.HmacSHA256('The quick brown fox jumps over the lazy dog', 'key').toString(CryptoJS.enc.Hex); + return [{ title: mac, url: 'https://example.com/x' }]; + }; + "#; + let result = run_scraper(code, "1", "movie"); + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert_eq!( + parsed[0]["title"], + "f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8" + ); + } + + #[test] + fn cryptojs_aes_cbc_roundtrips_with_explicit_key_and_iv() { + let code = r#" + module.exports.getStreams = async function() { + var key = CryptoJS.enc.Utf8.parse('0123456789abcdef'); + var iv = CryptoJS.enc.Utf8.parse('abcdef9876543210'); + var plaintext = 'secret stream url payload'; + var encrypted = CryptoJS.AES.encrypt(plaintext, key, { iv: iv }); + var decrypted = CryptoJS.AES.decrypt(encrypted, key, { iv: iv }).toString(CryptoJS.enc.Utf8); + return [{ title: decrypted, url: 'https://example.com/x' }]; + }; + "#; + let result = run_scraper(code, "1", "movie"); + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert_eq!(parsed[0]["title"], "secret stream url payload"); + } + + #[test] + fn webcrypto_subtle_digest_matches_cryptojs() { + let code = r#" + module.exports.getStreams = async function() { + var buf = await crypto.subtle.digest('SHA-256', new TextEncoder().encode('abc')); + var hex = Array.from(new Uint8Array(buf)).map(function(b) { + return b.toString(16).padStart(2, '0'); + }).join(''); + return [{ title: hex, url: 'https://example.com/x' }]; + }; + "#; + let result = run_scraper(code, "1", "movie"); + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert_eq!( + parsed[0]["title"], + "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad" + ); + } + + #[test] + fn webcrypto_ecdsa_verify_accepts_a_python_produced_signature() { + let key_hex = "3059301306072a8648ce3d020106082a8648ce3d030107034200049e6a723242258b9d87c8362fd321140b80e16d1671b5cb9b2dba3da7ccc42b82380102e3fd415ed40dfc8c4b4b218218995327daedc7eb35493f4f5b419aeaf8"; + let sig_hex = "f982b6fd52964b591329f5b503627dc1dc5b7f74ff0cf9acc840ab160636a99a526a7f11ee179e77d176827ab0035ee92653e3e7408c6c8fea3f566ec79e8c8f"; + let code = format!( + r#" + module.exports.getStreams = async function() {{ + function hexToBytes(hex) {{ + var bytes = new Uint8Array(hex.length / 2); + for (var i = 0; i < hex.length; i += 2) bytes[i / 2] = parseInt(hex.substr(i, 2), 16); + return bytes; + }} + var keyBytes = hexToBytes('{key_hex}'); + var sigBytes = hexToBytes('{sig_hex}'); + var key = await crypto.subtle.importKey('spki', keyBytes, {{ name: 'ECDSA', namedCurve: 'P-256' }}, false, ['verify']); + var ok = await crypto.subtle.verify({{ name: 'ECDSA', hash: 'SHA-256' }}, key, sigBytes, new TextEncoder().encode('hello plugin signature test')); + return [{{ title: String(ok), url: 'https://example.com/x' }}]; + }}; + "#, + key_hex = key_hex, + sig_hex = sig_hex, + ); + let result = run_scraper(&code, "1", "movie"); + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert_eq!(parsed[0]["title"], "true"); + } +}