diff --git a/Cargo.lock b/Cargo.lock index af497b0c2..7e7744f37 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,14 +8,35 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aae1277d39aeec15cb388266ecc24b11c80469deae6067e17a1a7aa9e5c1f234" +[[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", +] + [[package]] name = "aead" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1973cfbc1a2daf9cf550e74e1f088c28e7f7d8c1e1418fb6c9dc5184b7e84c99" dependencies = [ - "crypto-common", - "inout", + "crypto-common 0.2.2", + "inout 0.2.2", +] + +[[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]] @@ -24,9 +45,23 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138" dependencies = [ - "cipher", + "cipher 0.5.2", "cpubits", - "cpufeatures", + "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 0.5.2", + "aes 0.8.4", + "cipher 0.4.4", + "ctr 0.9.2", + "ghash 0.5.1", + "subtle", ] [[package]] @@ -35,11 +70,11 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdf011db2e21ce0d575593d749db5554b47fed37aff429e4dc50bc91ac93a028" dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", + "aead 0.6.1", + "aes 0.9.1", + "cipher 0.5.2", + "ctr 0.10.1", + "ghash 0.6.0", "subtle", ] @@ -279,6 +314,12 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.23.1" @@ -375,6 +416,15 @@ dependencies = [ "constant_time_eq 0.4.2", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "block-buffer" version = "0.12.1" @@ -449,7 +499,7 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896" dependencies = [ - "cipher", + "cipher 0.5.2", ] [[package]] @@ -485,6 +535,17 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chacha20" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3613f74bd2eac03dad61bd53dbe620703d4371614fe0bc3b9f04dd36fe4e818" +dependencies = [ + "cfg-if", + "cipher 0.4.4", + "cpufeatures 0.2.17", +] + [[package]] name = "chacha20" version = "0.10.2" @@ -492,10 +553,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.0", "rand_core 0.10.1", ] +[[package]] +name = "chacha20poly1305" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10cd79432192d1c0f4e1a0fef9527696cc039165d729fb41b3f4f4f354c2dc35" +dependencies = [ + "aead 0.5.2", + "chacha20 0.9.1", + "cipher 0.4.4", + "poly1305", + "zeroize", +] + [[package]] name = "char_str" version = "0.0.2" @@ -535,15 +609,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", + "zeroize", +] + [[package]] name = "cipher" version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c" dependencies = [ - "block-buffer", - "crypto-common", - "inout", + "block-buffer 0.12.1", + "crypto-common 0.2.2", + "inout 0.2.2", ] [[package]] @@ -682,7 +767,7 @@ version = "3.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "faf9468729b8cbcea668e36183cb69d317348c2e08e994829fb56ebfdfbaac34" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -719,6 +804,12 @@ dependencies = [ "wasm-bindgen", ] +[[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" @@ -762,6 +853,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + [[package]] name = "cpufeatures" version = "0.3.0" @@ -1001,6 +1101,28 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "crypto-common" version = "0.2.2" @@ -1019,13 +1141,22 @@ dependencies = [ "memchr", ] +[[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 = "ctr" version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baaca1c4b237092596f64d571e9db6ce4109c4ef9742e27590f1709594461f21" dependencies = [ - "cipher", + "cipher 0.5.2", ] [[package]] @@ -1037,6 +1168,33 @@ dependencies = [ "cmov", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "data-encoding" version = "2.11.0" @@ -1074,14 +1232,25 @@ dependencies = [ "thiserror", ] +[[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", - "pem-rfc7468", + "const-oid 0.10.2", + "pem-rfc7468 1.0.0", "zeroize", ] @@ -1116,15 +1285,27 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid 0.9.6", + "crypto-common 0.1.7", + "subtle", +] + [[package]] name = "digest" version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" dependencies = [ - "block-buffer", - "const-oid", - "crypto-common", + "block-buffer 0.12.1", + "const-oid 0.10.2", + "crypto-common 0.2.2", "ctutils", ] @@ -1184,12 +1365,71 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der 0.7.10", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki 0.7.3", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8 0.10.2", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + [[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.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468 0.7.0", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "encode_unicode" version = "1.0.0" @@ -1238,7 +1478,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1259,6 +1499,22 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1358,6 +1614,17 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + [[package]] name = "get-size-derive2" version = "0.10.3" @@ -1409,8 +1676,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -1439,13 +1708,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval 0.6.2", +] + [[package]] name = "ghash" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2eecf2d5dc9b66b732b97707a0210906b1d30523eb773193ab777c0c84b3e8d5" dependencies = [ - "polyval", + "polyval 0.7.1", ] [[package]] @@ -1476,6 +1755,17 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "half" version = "2.7.1" @@ -1529,13 +1819,31 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac 0.12.1", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "hmac" version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" dependencies = [ - "digest", + "digest 0.11.3", ] [[package]] @@ -1698,6 +2006,15 @@ dependencies = [ "hashbrown 0.17.1", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "inout" version = "0.2.2" @@ -1746,7 +2063,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -1881,7 +2198,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e24a010dd405bd7ed803e5253182815b41bf2e6a80cc3bfc066658e03a198aa" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.3.0", ] [[package]] @@ -1895,6 +2212,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "lexical-parse-float" @@ -2147,7 +2467,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" dependencies = [ "cfg-if", - "digest", + "digest 0.11.3", ] [[package]] @@ -2244,6 +2564,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 1.5.0", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.6", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -2268,6 +2604,16 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -2275,6 +2621,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2344,6 +2691,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" version = "0.10.81" @@ -2418,6 +2771,30 @@ dependencies = [ "indexmap", ] +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + [[package]] name = "parking_lot" version = "0.12.5" @@ -2452,8 +2829,17 @@ version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629" dependencies = [ - "digest", - "hmac", + "digest 0.11.3", + "hmac 0.13.0", +] + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", ] [[package]] @@ -2552,21 +2938,53 @@ 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 = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "der 0.7.10", + "spki 0.7.3", +] + [[package]] name = "pkcs5" version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63d440a804ec8d6fafbb6b84471e013286658d373248927692ab3366686220ca" dependencies = [ - "aes", - "aes-gcm", + "aes 0.9.1", + "aes-gcm 0.11.0", "cbc", - "der", + "der 0.8.1", "pbkdf2", "rand_core 0.10.1", "scrypt", - "sha2", - "spki", + "sha2 0.11.0", + "spki 0.8.0", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.10", + "pkcs5 0.7.1", + "spki 0.7.3", ] [[package]] @@ -2575,10 +2993,10 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "451913da69c775a56034ea8d9003d27ee8948e12443eae7c038ba100a4f21cb7" dependencies = [ - "der", - "pkcs5", + "der 0.8.1", + "pkcs5 0.8.1", "rand_core 0.10.1", - "spki", + "spki 0.8.0", ] [[package]] @@ -2626,6 +3044,29 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "poly1305" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8159bd90725d2df49889a078b54f4f79e87f1f8a8444194cdca81d38f5393abf" +dependencies = [ + "cpufeatures 0.2.17", + "opaque-debug", + "universal-hash 0.5.1", +] + +[[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 0.5.1", +] + [[package]] name = "polyval" version = "0.7.1" @@ -2633,8 +3074,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfc63250416fea14f5749b90725916a6c903f599d51cb635aa7a52bfd03eede" dependencies = [ "cpubits", - "cpufeatures", - "universal-hash", + "cpufeatures 0.3.0", + "universal-hash 0.6.1", ] [[package]] @@ -2688,6 +3129,15 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro-utils" version = "0.10.0" @@ -2868,7 +3318,7 @@ version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" dependencies = [ - "chacha20", + "chacha20 0.10.2", "getrandom 0.4.3", "rand_core 0.10.1", ] @@ -3049,6 +3499,16 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -3063,12 +3523,42 @@ dependencies = [ "windows-sys 0.52.0", ] +[[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", + "spki 0.7.3", + "subtle", + "zeroize", +] + [[package]] name = "rustc-hash" version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rusticata-macros" version = "4.1.0" @@ -3088,7 +3578,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -3100,7 +3590,7 @@ dependencies = [ "aws-lc-rs", "once_cell", "rustls-pki-types", - "rustls-webpki", + "rustls-webpki 0.103.13", "subtle", "zeroize", ] @@ -3142,9 +3632,51 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "764899a24af3980067ee14bc143654f297b22eaebfe3c7b6b211920a5a59b046" dependencies = [ + "web-time", "zeroize", ] +[[package]] +name = "rustls-rustcrypto" +version = "0.0.2-alpha" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f12052947763ab8515f753315357599e9b0b4dab3b8ba15f30f725fe6d025557" +dependencies = [ + "aead 0.5.2", + "aes-gcm 0.10.3", + "chacha20poly1305", + "crypto-common 0.1.7", + "der 0.7.10", + "digest 0.10.7", + "ecdsa", + "ed25519-dalek", + "hmac 0.12.1", + "p256", + "p384", + "paste", + "pkcs8 0.10.2", + "rand_core 0.6.4", + "rsa", + "rustls", + "rustls-pki-types", + "rustls-webpki 0.102.8", + "sec1", + "sha2 0.10.9", + "signature", + "x25519-dalek", +] + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustls-webpki" version = "0.103.13" @@ -3250,7 +3782,7 @@ dependencies = [ "rustpython-wtf8", "scrypt", "sha1", - "sha2", + "sha2 0.11.0", "sha3", "shake", "siphasher", @@ -3323,7 +3855,7 @@ version = "0.6.0" dependencies = [ "bitflags 2.13.1", "cc", - "der", + "der 0.8.1", "dns-lookup", "gethostname", "getrandom 0.4.3", @@ -3339,16 +3871,16 @@ dependencies = [ "num_cpus", "parking_lot", "paste", - "pkcs8", + "pkcs8 0.11.0", "rustix", "rustls", "rustls-native-certs", "rustls-pemfile", - "rustls-webpki", + "rustls-webpki 0.103.13", "rustpython-wtf8", "rustyline", "schannel", - "sha2", + "sha2 0.11.0", "socket2", "system-configuration", "termios", @@ -3512,7 +4044,7 @@ dependencies = [ "openssl-sys", "parking_lot", "paste", - "pem-rfc7468", + "pem-rfc7468 1.0.0", "phf 0.14.0", "pymath", "rand 0.10.2", @@ -3629,8 +4161,14 @@ name = "rustpython_wasm" version = "0.6.0" dependencies = [ "console_error_panic_hook", + "getrandom 0.2.17", + "getrandom 0.4.3", "js-sys", + "rustls", + "rustls-pki-types", + "rustls-rustcrypto", "rustpython-common", + "rustpython-host_env", "rustpython-pylib", "rustpython-ruff_text_size", "rustpython-stdlib", @@ -3691,7 +4229,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f874456e72520ff1375a06c588eaf074b0f01f9e9e1aada45bd9b7954a6e42c" dependencies = [ "cfg-if", - "cipher", + "cipher 0.5.2", ] [[package]] @@ -3727,7 +4265,21 @@ dependencies = [ "cfg-if", "pbkdf2", "salsa20", - "sha2", + "sha2 0.11.0", +] + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der 0.7.10", + "generic-array", + "pkcs8 0.10.2", + "subtle", + "zeroize", ] [[package]] @@ -3753,6 +4305,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -3823,8 +4381,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "digest 0.10.7", ] [[package]] @@ -3834,8 +4403,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures", - "digest", + "cpufeatures 0.3.0", + "digest 0.11.3", ] [[package]] @@ -3844,7 +4413,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bc9bad02c26382724b2d2692c6f179285e4b54eeecd7968f52a50059c3c11759" dependencies = [ - "digest", + "digest 0.11.3", "keccak", "sponge-cursor", ] @@ -3855,7 +4424,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09057cb2149ad4cbd2da1e26b351f9a4c354219421229c69c3063e6f61947c4a" dependencies = [ - "digest", + "digest 0.11.3", "keccak", "sponge-cursor", ] @@ -3880,6 +4449,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" +[[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 = "similar" version = "2.7.0" @@ -3914,6 +4493,22 @@ 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 = "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" @@ -3921,7 +4516,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d9efca8738c78ee9484207732f728b1ef517bbb1833d6fc0879ca898a522f6f" dependencies = [ "base64ct", - "der", + "der 0.8.1", ] [[package]] @@ -4076,10 +4671,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4329,13 +4924,23 @@ dependencies = [ "rand 0.8.6", ] +[[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 = "universal-hash" version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f4987bdc12753382e0bec4a65c50738ffaabc998b9cdd1f952fb5f39b0048a96" dependencies = [ - "crypto-common", + "crypto-common 0.2.2", "ctutils", ] @@ -4497,6 +5102,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "webpki-roots" version = "1.0.9" @@ -4553,7 +5168,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -4684,6 +5299,17 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" +[[package]] +name = "x25519-dalek" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277" +dependencies = [ + "curve25519-dalek", + "rand_core 0.6.4", + "zeroize", +] + [[package]] name = "x509-parser" version = "0.18.1" @@ -4806,6 +5432,20 @@ name = "zeroize" version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c50655cbb0fe3fc43170059e702f1ce5e19b84cec58dc87b037a09935c2f328" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] name = "zerotrie" diff --git a/Lib/test/test_contextlib.py b/Lib/test/test_contextlib.py index a04f785f7..6a3329fa5 100644 --- a/Lib/test/test_contextlib.py +++ b/Lib/test/test_contextlib.py @@ -1014,7 +1014,6 @@ class TestBaseExitStack: else: self.fail("Expected KeyError, but no exception was raised") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_exit_exception_with_correct_context(self): # http://bugs.python.org/issue20317 @contextmanager diff --git a/Lib/test/test_descr.py b/Lib/test/test_descr.py index fc39240fb..7080d7c40 100644 --- a/Lib/test/test_descr.py +++ b/Lib/test/test_descr.py @@ -1553,7 +1553,6 @@ class ClassPropertiesAndMethods(unittest.TestCase): else: self.fail("finding the most derived metaclass should have failed") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_classmethods(self): # Testing class methods... class C(object): @@ -1615,7 +1614,6 @@ class ClassPropertiesAndMethods(unittest.TestCase): del cm.x self.assertNotHasAttr(cm, "x") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_classmethod_staticmethod_annotations(self): for deco in (classmethod, staticmethod): @deco @@ -2355,7 +2353,6 @@ class ClassPropertiesAndMethods(unittest.TestCase): self.assertEqual(r(), None) del r - @unittest.expectedFailure # TODO: RUSTPYTHON def test_properties(self): # Testing property... class C(object): @@ -3329,7 +3326,6 @@ class ClassPropertiesAndMethods(unittest.TestCase): eval("x %s y" % op), "x=%d, y=%d" % (x, y)) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_descrdoc(self): # Testing descriptor doc strings... from _io import FileIO @@ -4084,7 +4080,6 @@ class ClassPropertiesAndMethods(unittest.TestCase): y = x ** 2 self.assertIn('unsupported operand type(s) for **', str(cm.exception)) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_pow_wrapper_error_messages(self): self.assertRaisesRegex(TypeError, 'expected 1 or 2 arguments, got 0', @@ -4654,7 +4649,6 @@ class ClassPropertiesAndMethods(unittest.TestCase): with self.assertRaises(TypeError): a >= b - @unittest.expectedFailure # TODO: RUSTPYTHON def test_method_wrapper(self): # Testing method-wrapper objects... # did not support any reflection before 2.5 @@ -4981,7 +4975,6 @@ class ClassPropertiesAndMethods(unittest.TestCase): self.assertIn("cannot delete '__doc__' attribute of immutable type 'X'", str(cm.exception)) self.assertEqual(X.__doc__, "banana") - @unittest.expectedFailure # TODO: RUSTPYTHON def test_qualname(self): descriptors = [str.lower, complex.real, float.real, int.__add__] types = ['method', 'member', 'getset', 'wrapper'] diff --git a/Lib/test/test_exceptions.py b/Lib/test/test_exceptions.py index ee5af3e39..d881c5941 100644 --- a/Lib/test/test_exceptions.py +++ b/Lib/test/test_exceptions.py @@ -1724,7 +1724,6 @@ class ExceptionTests(unittest.TestCase): os.listdir(__file__) self.assertEqual(cm.exception.errno, errno.ENOTDIR, cm.exception) - @unittest.expectedFailure # TODO: RUSTPYTHON; AssertionError: None != 'Exception ignored while calling dealloca[83 chars]200>' def test_unraisable(self): # Issue #22836: PyErr_WriteUnraisable() should give sensible reports class BrokenDel: diff --git a/Lib/test/test_generators.py b/Lib/test/test_generators.py index cf3288b2c..037c1bed5 100644 --- a/Lib/test/test_generators.py +++ b/Lib/test/test_generators.py @@ -1369,7 +1369,7 @@ From the Iterators list, about the types of these things. >>> [s for s in dir(i) if not s.startswith('_')] ['close', 'gi_code', 'gi_frame', 'gi_running', 'gi_suspended', 'gi_yieldfrom', 'send', 'throw'] >>> from test.support import HAVE_DOCSTRINGS ->>> print(i.__next__.__doc__ if HAVE_DOCSTRINGS else 'Implement next(self).') # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE +>>> print(i.__next__.__doc__ if HAVE_DOCSTRINGS else 'Implement next(self).') Implement next(self). >>> iter(i) is i True @@ -2526,12 +2526,12 @@ Traceback (most recent call last): ... SyntaxError: assignment to yield expression not possible ->>> def f(): (yield bar) = y # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE +>>> def f(): (yield bar) = y Traceback (most recent call last): ... SyntaxError: cannot assign to yield expression here. Maybe you meant '==' instead of '='? ->>> def f(): (yield bar) += y # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE +>>> def f(): (yield bar) += y Traceback (most recent call last): ... SyntaxError: 'yield expression' is an illegal expression for augmented assignment @@ -2727,7 +2727,7 @@ RuntimeError: generator ignored GeneratorExit Our ill-behaved code should be invoked during GC: ->>> with support.catch_unraisable_exception() as cm: # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE +>>> with support.catch_unraisable_exception() as cm: ... g = f() ... next(g) ... gen_repr = repr(g) @@ -2845,7 +2845,7 @@ to test. ... raise RuntimeError(message) ... invoke("del failed") ... ->>> with support.catch_unraisable_exception() as cm: # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE +>>> with support.catch_unraisable_exception() as cm: ... leaker = Leaker() ... del_repr = repr(type(leaker).__del__) ... del leaker diff --git a/Lib/test/test_genexps.py b/Lib/test/test_genexps.py index 17d2d1370..685e3efff 100644 --- a/Lib/test/test_genexps.py +++ b/Lib/test/test_genexps.py @@ -105,7 +105,7 @@ Verify that parenthesis are required in a statement Verify that parenthesis are required when used as a keyword argument value - >>> dict(a = i for i in range(10)) # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE + >>> dict(a = i for i in range(10)) Traceback (most recent call last): ... SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='? @@ -242,7 +242,7 @@ Check that generator attributes are present True >>> from test.support import HAVE_DOCSTRINGS - >>> print(g.__next__.__doc__ if HAVE_DOCSTRINGS else 'Implement next(self).') # TODO: RUSTPYTHON # doctest: +EXPECTED_FAILURE + >>> print(g.__next__.__doc__ if HAVE_DOCSTRINGS else 'Implement next(self).') Implement next(self). >>> import types >>> isinstance(g, types.GeneratorType) diff --git a/Lib/test/test_inspect/test_inspect.py b/Lib/test/test_inspect/test_inspect.py index 2e23be979..ac9a2b1a2 100644 --- a/Lib/test/test_inspect/test_inspect.py +++ b/Lib/test/test_inspect/test_inspect.py @@ -3954,7 +3954,6 @@ class TestSignatureObject(unittest.TestCase): ('b', ..., ..., "positional_or_keyword")), ...)) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_signature_on_class(self): class C: def __init__(self, a): diff --git a/Lib/test/test_pydoc/test_pydoc.py b/Lib/test/test_pydoc/test_pydoc.py index 55825b6bb..966c5fd30 100644 --- a/Lib/test/test_pydoc/test_pydoc.py +++ b/Lib/test/test_pydoc/test_pydoc.py @@ -787,7 +787,6 @@ class PydocDocTest(unittest.TestCase): run_pydoc_for_request(pydoc.Helper.help, 'Help on function help in module pydoc:') # test for pydoc.Helper() instance skipped because it is always meant to be interactive - @unittest.expectedFailure # TODO: RUSTPYTHON @unittest.skipIf(hasattr(sys, 'gettrace') and sys.gettrace(), 'trace function introduces __locals__ unexpectedly') @requires_docstrings diff --git a/Lib/test/test_struct.py b/Lib/test/test_struct.py index b01f002c1..a570ec098 100644 --- a/Lib/test/test_struct.py +++ b/Lib/test/test_struct.py @@ -730,7 +730,6 @@ class StructTest(ComplexesAreIdenticalMixin, unittest.TestCase): s2 = struct.Struct(s.format.encode()) self.assertEqual(s2.format, s.format) - @unittest.expectedFailure # TODO: RUSTPYTHON def test_struct_cleans_up_at_runtime_shutdown(self): code = """if 1: import struct diff --git a/Lib/test/test_syntax.py b/Lib/test/test_syntax.py index 5fab0d3f9..6c492e96e 100644 --- a/Lib/test/test_syntax.py +++ b/Lib/test/test_syntax.py @@ -1264,22 +1264,22 @@ Missing ':' before suites: Traceback (most recent call last): SyntaxError: cannot assign to __debug__ - >>> if x = 3: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> if x = 3: ... pass Traceback (most recent call last): SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='? - >>> while x = 3: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> while x = 3: ... pass Traceback (most recent call last): SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='? - >>> if x.a = 3: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> if x.a = 3: ... pass Traceback (most recent call last): SyntaxError: cannot assign to attribute here. Maybe you meant '==' instead of '='? - >>> while x.a = 3: # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE + >>> while x.a = 3: ... pass Traceback (most recent call last): SyntaxError: cannot assign to attribute here. Maybe you meant '==' instead of '='? @@ -1885,19 +1885,19 @@ Traceback (most recent call last): ... SyntaxError: keyword argument repeated: a ->>> {1, 2, 3} = 42 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> {1, 2, 3} = 42 Traceback (most recent call last): SyntaxError: cannot assign to set display here. Maybe you meant '==' instead of '='? ->>> {1: 2, 3: 4} = 42 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> {1: 2, 3: 4} = 42 Traceback (most recent call last): SyntaxError: cannot assign to dict literal here. Maybe you meant '==' instead of '='? ->>> f'{x}' = 42 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f'{x}' = 42 Traceback (most recent call last): SyntaxError: cannot assign to f-string expression here. Maybe you meant '==' instead of '='? ->>> f'{x}-{y}' = 42 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> f'{x}-{y}' = 42 Traceback (most recent call last): SyntaxError: cannot assign to f-string expression here. Maybe you meant '==' instead of '='? @@ -1973,11 +1973,11 @@ SyntaxError: 'u' and 'b' prefixes are incompatible Traceback (most recent call last): SyntaxError: 'b' and 'f' prefixes are incompatible ->>> t'{x}' = 42 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> t'{x}' = 42 Traceback (most recent call last): SyntaxError: cannot assign to t-string expression here. Maybe you meant '==' instead of '='? ->>> t'{x}-{y}' = 42 # TODO: RUSTPYTHON; Wrong error message # doctest: +EXPECTED_FAILURE +>>> t'{x}-{y}' = 42 Traceback (most recent call last): SyntaxError: cannot assign to t-string expression here. Maybe you meant '==' instead of '='? diff --git a/Lib/test/test_types.py b/Lib/test/test_types.py index 6a3a58a1b..e07fa568a 100644 --- a/Lib/test/test_types.py +++ b/Lib/test/test_types.py @@ -635,7 +635,6 @@ class TypesTests(unittest.TestCase): self.assertIsInstance(object.__lt__, types.WrapperDescriptorType) self.assertIsInstance(int.__lt__, types.WrapperDescriptorType) - @unittest.expectedFailure # TODO: RUSTPYTHON; ValueError: no signature found for builtin "cannot assign to expression".into(), ast::Expr::If(_) => "cannot assign to conditional expression".into(), ast::Expr::Generator(_) => "cannot assign to generator expression".into(), + ast::Expr::Yield(_) | ast::Expr::YieldFrom(_) => { + "cannot assign to yield expression here. Maybe you meant '==' instead of '='?" + .into() + } ast::Expr::FString(_) => "invalid syntax".into(), ast::Expr::StringLiteral(_) | ast::Expr::BytesLiteral(_) @@ -1893,6 +1911,14 @@ fn invalid_call_argument_assignment_error( } } if is_simple_keyword_name(bytes, target_start, target_end) { + // NAME '=' expression for_if_clauses + if unparenthesized_comprehension(bytes, equal + 1) { + return Some(CpythonDiagnostic::new( + "invalid syntax. Maybe you meant '==' or ':=' instead of '='?".to_owned(), + target_start, + equal + 1, + )); + } return None; } Some(CpythonDiagnostic::new( @@ -1902,6 +1928,37 @@ fn invalid_call_argument_assignment_error( )) } +fn unparenthesized_comprehension(bytes: &[u8], mut index: usize) -> bool { + index = next_non_horizontal_whitespace(bytes, index); + if index >= bytes.len() { + return false; + } + let start = index; + let mut level = 0usize; + while index < bytes.len() { + match bytes[index] { + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + if level == 0 { + return false; + } + level -= 1; + index += 1; + } + b',' | b':' if level == 0 => return false, + _ if level == 0 && index > start && starts_identifier(bytes, index, b"for") => { + return true; + } + _ => index += 1, + } + } + false +} + fn invalid_call_star_expression_error( bytes: &[u8], arg_start: usize, @@ -2711,6 +2768,30 @@ fn parenthesized_single_starred_delete_target(bytes: &[u8], start: usize, end: u false } +fn assignment_target_expr_range(source: &str, start: usize, end: usize) -> Option<(usize, usize)> { + let bytes = source.as_bytes(); + let (target_start, target_end) = trim_target_range(bytes, start, end); + if target_start >= target_end { + return None; + } + if parser::parse( + &source[target_start..target_end], + parser::Mode::Expression.into(), + ) + .is_ok() + { + return Some((target_start, target_end)); + } + // `def f(): (yield bar)` — skip the suite header so the remaining + // text is the assignment target expression. + let colon = top_level_colon(bytes, target_start, target_end)?; + let after = skip_horizontal_whitespace(bytes, colon + 1); + if after >= target_end { + return None; + } + Some((after, target_end)) +} + fn trim_target_range(bytes: &[u8], mut start: usize, mut end: usize) -> (usize, usize) { while start < end && matches!( @@ -2745,10 +2826,7 @@ fn assignment_target_error_for_slice( end: usize, ) -> Option { let bytes = source.as_bytes(); - let (target_start, target_end) = trim_target_range(bytes, start, end); - if target_start >= target_end { - return None; - } + let (target_start, target_end) = assignment_target_expr_range(source, start, end)?; if starts_identifier(bytes, target_start, b"yield") { return Some(CpythonDiagnostic::new( "assignment to yield expression not possible".to_owned(), @@ -2766,13 +2844,6 @@ fn assignment_target_error_for_slice( let invalid_target = invalid_assignment_target(&expression.body)?; let invalid_start = target_start + invalid_target.range().start().to_usize(); let invalid_end = target_start + invalid_target.range().end().to_usize(); - if matches!(invalid_target, ast::Expr::FString(_)) { - return Some(CpythonDiagnostic::new( - "invalid syntax".to_owned(), - invalid_start, - invalid_end, - )); - } let name = delete_target_expr_name(invalid_target); let top_level = invalid_target.range() == expression.body.range(); let bitwise_like = matches!( @@ -2785,6 +2856,12 @@ fn assignment_target_error_for_slice( | ast::Expr::StringLiteral(_) | ast::Expr::BytesLiteral(_) | ast::Expr::EllipsisLiteral(_) + | ast::Expr::Yield(_) + | ast::Expr::YieldFrom(_) + | ast::Expr::Set(_) + | ast::Expr::Dict(_) + | ast::Expr::FString(_) + | ast::Expr::TString(_) ); Some(CpythonDiagnostic::new( invalid_assignment_message(name, top_level && bitwise_like), @@ -2919,6 +2996,161 @@ fn top_level_plain_assignment_offsets(bytes: &[u8]) -> Vec { offsets } +fn invalid_condition_assignment_error( + source: &str, + parse_error_offset: usize, +) -> Option { + let bytes = source.as_bytes(); + let mut index = 0usize; + let mut line_start = 0usize; + let mut level = 0usize; + while index < bytes.len() { + match bytes[index] { + b'#' if level == 0 => { + while index < bytes.len() && bytes[index] != b'\n' { + index += 1; + } + } + b'\n' => { + line_start = index + 1; + index += 1; + } + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' | b'[' | b'{' => { + level += 1; + index += 1; + } + b')' | b']' | b'}' => { + level = level.saturating_sub(1); + index += 1; + } + _ if level == 0 + && index == skip_horizontal_whitespace(bytes, line_start) + && (starts_identifier(bytes, index, b"if") + || starts_identifier(bytes, index, b"elif") + || starts_identifier(bytes, index, b"while")) => + { + let keyword_len = if starts_identifier(bytes, index, b"while") { + 5 + } else if starts_identifier(bytes, index, b"elif") { + 4 + } else { + 2 + }; + let cond_start = skip_horizontal_whitespace(bytes, index + keyword_len); + let Some(colon) = top_level_colon(bytes, cond_start, bytes.len()) else { + index += keyword_len; + continue; + }; + if parse_error_offset < index || parse_error_offset > colon { + index += keyword_len; + continue; + } + let Some(equal) = condition_plain_assignment(bytes, cond_start, colon) else { + index += keyword_len; + continue; + }; + let (target_start, target_end) = trim_target_range(bytes, cond_start, equal); + if target_start >= target_end { + index += keyword_len; + continue; + } + if is_simple_keyword_name(bytes, target_start, target_end) { + return Some(CpythonDiagnostic::new( + "invalid syntax. Maybe you meant '==' or ':=' instead of '='?".to_owned(), + target_start, + equal + 1, + )); + } + if let Some((expr_name, start, end, _)) = + expression_name_and_range(&source[target_start..target_end]) + { + return Some(CpythonDiagnostic::new( + format!( + "cannot assign to {expr_name} here. Maybe you meant '==' instead of '='?" + ), + target_start + start, + target_start + end, + )); + } + return Some(CpythonDiagnostic::new( + "invalid syntax. Maybe you meant '==' or ':=' instead of '='?".to_owned(), + target_start, + equal + 1, + )); + } + _ => index += 1, + } + } + None +} + +fn condition_plain_assignment(bytes: &[u8], start: usize, end: usize) -> Option { + let mut index = start; + let mut nest = Vec::new(); + while index < end { + match bytes[index] { + b'\'' | b'"' => index = skip_quoted_string(bytes, index), + b'(' => { + nest.push(if is_call_open(bytes, start, index) { + b'c' + } else { + b'g' + }); + index += 1; + } + b'[' => { + nest.push(b'['); + index += 1; + } + b'{' => { + nest.push(b'{'); + index += 1; + } + b')' | b']' | b'}' => { + nest.pop(); + index += 1; + } + b'=' if is_plain_assignment_operator(bytes, index) && !nest.contains(&b'c') => { + return Some(index); + } + _ => index += 1, + } + } + None +} + +fn is_call_open(bytes: &[u8], start: usize, open: usize) -> bool { + let mut index = open; + while index > start { + index -= 1; + match bytes[index] { + b' ' | b'\t' | b'\n' | b'\r' | b'\x0c' => {} + b')' | b']' => return true, + byte if byte >= 0x80 || is_ascii_identifier_char(byte) => { + let mut ident_start = index; + while ident_start > start + && bytes + .get(ident_start - 1) + .is_some_and(|b| *b >= 0x80 || is_ascii_identifier_char(*b)) + { + ident_start -= 1; + } + return !is_condition_keyword(&bytes[ident_start..=index]); + } + _ => return false, + } + } + false +} + +fn is_condition_keyword(word: &[u8]) -> bool { + matches!( + word, + b"not" | b"and" | b"or" | b"in" | b"is" | b"if" | b"elif" | b"while" | b"await" | b"lambda" + ) +} + fn invalid_assignment_target_error(source: &str) -> Option { let bytes = source.as_bytes(); let offsets = top_level_plain_assignment_offsets(bytes); @@ -2987,10 +3219,7 @@ fn top_level_augassign_offset(bytes: &[u8]) -> Option<(usize, usize)> { fn invalid_augassign_target_error(source: &str) -> Option { let bytes = source.as_bytes(); let (operator, _) = top_level_augassign_offset(bytes)?; - let (target_start, target_end) = trim_target_range(bytes, 0, operator); - if target_start >= target_end { - return None; - } + let (target_start, target_end) = assignment_target_expr_range(source, 0, operator)?; let target_text = &source[target_start..target_end]; let Ok(parsed) = parser::parse(target_text, parser::Mode::Expression.into()) else { return None; @@ -5383,6 +5612,20 @@ fn missing_comma_expression_error(source: &str) -> Option { index = identifier_end(bytes, index, bytes.len()); } byte if !stack.is_empty() && expression_atom_start_byte(byte) => { + // `yield x` / `await x` are prefix expressions, not two + // adjacent atoms missing a comma. + if starts_identifier(bytes, index, b"yield") { + index = identifier_end(bytes, index, bytes.len()); + let after_yield = skip_ascii_whitespace(bytes, index, bytes.len()); + if starts_identifier(bytes, after_yield, b"from") { + index = identifier_end(bytes, after_yield, bytes.len()); + } + continue; + } + if starts_identifier(bytes, index, b"await") { + index = identifier_end(bytes, index, bytes.len()); + continue; + } let atom_end = adjacent_atom_end(bytes, index).unwrap_or(index + 1); let next = skip_ascii_whitespace(bytes, atom_end, bytes.len()); if next > atom_end @@ -7486,6 +7729,155 @@ mod tests { assert_eq!(span("[\u{3b1} \u{3b2}]"), (2, 5)); } + #[test] + fn parenthesized_yield_assignment_uses_invalid_target_message() { + let err = compile( + "def f(): (yield bar) = y\n", + Mode::Exec, + "", + CompileOpts::default(), + ) + .expect_err("parenthesized yield is not an assignment target"); + assert_eq!( + err.to_string(), + "cannot assign to yield expression here. Maybe you meant '==' instead of '='?" + ); + } + + #[test] + fn parenthesized_yield_augassign_uses_illegal_expression_message() { + let err = compile( + "def f(): (yield bar) += y\n", + Mode::Exec, + "", + CompileOpts::default(), + ) + .expect_err("parenthesized yield is not an augmented assignment target"); + assert_eq!( + err.to_string(), + "'yield expression' is an illegal expression for augmented assignment" + ); + } + + #[test] + fn kwarg_unparenthesized_genexp_uses_eq_or_walrus_message() { + let err = compile( + "dict(a = i for i in range(10))\n", + Mode::Exec, + "", + CompileOpts::default(), + ) + .expect_err("unparenthesized genexp after '=' is invalid"); + assert_eq!( + err.to_string(), + "invalid syntax. Maybe you meant '==' or ':=' instead of '='?" + ); + } + + #[test] + fn eval_fstring_assignment_keeps_invalid_syntax() { + for source in ["f'' = 3", "f'{0}' = x", "f'{x}' = x"] { + let err = compile(source, Mode::Eval, "", CompileOpts::default()) + .expect_err("assignment is invalid in eval"); + assert_eq!(err.to_string(), "invalid syntax", "{source}"); + } + let err = compile("f'' = 3", Mode::Exec, "", CompileOpts::default()) + .expect_err("f-string is not an assignment target"); + assert_eq!( + err.to_string(), + "cannot assign to f-string expression here. Maybe you meant '==' instead of '='?" + ); + } + + #[test] + fn if_assignment_uses_eq_or_walrus_message() { + let err = compile( + "if x = 3: pass\n", + Mode::Exec, + "", + CompileOpts::default(), + ) + .expect_err("assignment in if condition is invalid"); + assert_eq!( + err.to_string(), + "invalid syntax. Maybe you meant '==' or ':=' instead of '='?" + ); + } + + #[test] + fn parenthesized_if_assignment_uses_eq_or_walrus_message() { + for source in [ + "if (x = 3): pass\n", + "if ((x = 3)): pass\n", + "if (x = 3) and y: pass\n", + ] { + let err = compile(source, Mode::Exec, "", CompileOpts::default()) + .expect_err("parenthesized assignment in if condition is invalid"); + assert_eq!( + err.to_string(), + "invalid syntax. Maybe you meant '==' or ':=' instead of '='?" + ); + } + } + + #[test] + fn earlier_syntax_error_is_not_replaced_by_later_condition() { + let err = compile( + "@@@\nif x = 3: pass\n", + Mode::Exec, + "", + CompileOpts::default(), + ) + .expect_err("the first invalid token is the syntax error"); + assert_eq!(err.to_string(), "invalid syntax"); + assert_eq!(err.python_location().0, 1); + } + + #[test] + fn if_attribute_assignment_uses_invalid_target_hint() { + let err = compile( + "if x.a = 3: pass\n", + Mode::Exec, + "", + CompileOpts::default(), + ) + .expect_err("attribute assignment in if condition is invalid"); + assert_eq!( + err.to_string(), + "cannot assign to attribute here. Maybe you meant '==' instead of '='?" + ); + } + + #[test] + fn parenthesized_yield_from_assignment_uses_invalid_target_message() { + let err = compile( + "def f(): (yield from value) = target\n", + Mode::Exec, + "", + CompileOpts::default(), + ) + .expect_err("parenthesized yield from is not an assignment target"); + assert_eq!( + err.to_string(), + "cannot assign to yield expression here. Maybe you meant '==' instead of '='?" + ); + } + + #[test] + fn set_display_assignment_uses_invalid_target_hint() { + let err = compile( + "{1, 2, 3} = 42\n", + Mode::Exec, + "", + CompileOpts::default(), + ) + .expect_err("set display is not an assignment target"); + assert_eq!( + err.to_string(), + "cannot assign to set display here. Maybe you meant '==' instead of '='?" + ); + } + #[test] fn obsolete_not_equal_diagnostic_spans_the_whole_operator() { let err = compile("2 <> 3\n", Mode::Exec, "", CompileOpts::default()) diff --git a/crates/derive-impl/src/pyclass.rs b/crates/derive-impl/src/pyclass.rs index 686c59094..320a39feb 100644 --- a/crates/derive-impl/src/pyclass.rs +++ b/crates/derive-impl/src/pyclass.rs @@ -168,6 +168,8 @@ pub(crate) fn impl_pyclass_impl(attr: PunctuatedNestedMeta, item: Item) -> Resul itemsize, } = extract_impl_attrs(attr, &impl_ty)?; let payload_ty = attr_payload.unwrap_or(payload_guess); + context.getset_items.type_name = Some(payload_ty.to_string()); + context.member_items.type_name = Some(payload_ty.to_string()); let method_def = &context.method_items; let getset_impl = &context.getset_items; let member_impl = &context.member_items; @@ -265,6 +267,8 @@ pub(crate) fn impl_pyclass_impl(attr: PunctuatedNestedMeta, item: Item) -> Resul .. } = extract_impl_attrs(attr, &trai.ident)?; + context.getset_items.type_name = Some(trai.ident.to_string()); + context.member_items.type_name = Some(trai.ident.to_string()); let method_def = &context.method_items; let getset_impl = &context.getset_items; let member_impl = &context.member_items; @@ -415,6 +419,21 @@ fn type_matches_path(ty: &syn::Type, path: &syn::Path) -> bool { type_last.ident == path_last.ident } +fn cpython_attr_doc(rust_type: &str, attr: &str) -> Option { + let stripped = rust_type.strip_prefix("Py").unwrap_or(rust_type); + let lower = stripped.to_ascii_lowercase(); + let underscored = format!("_{stripped}"); + let class_names = [rust_type, stripped, lower.as_str(), underscored.as_str()]; + for (key, doc) in &DB { + if class_names.iter().any(|class| { + *key == format!("{class}.{attr}") || key.ends_with(&format!(".{class}.{attr}")) + }) { + return Some((*doc).to_owned()); + } + } + None +} + fn generate_class_def( ident: &Ident, name: &str, @@ -978,10 +997,12 @@ where let item_meta = MethodItemMeta::from_attr(ident.clone(), &item_attr)?; let py_name = item_meta.method_name()?; + let coexist = item_meta.coexist()?; // Disallow slot methods - they should be defined via trait implementations // These are exposed as wrapper_descriptor via add_operators from SLOT_DEFS - if !args.context.is_trait { + // unless the method is METH_COEXIST. + if !args.context.is_trait && !coexist { const FORBIDDEN_SLOT_METHODS: &[(&str, &str)] = &[ // Constructor/Initializer traits ("__new__", "Constructor"), @@ -1123,6 +1144,7 @@ where ident: ident.to_owned(), doc, raw, + coexist, attr_name: self.inner.attr_name, call_flags, }); @@ -1328,11 +1350,13 @@ where args.attrs.push(allow_attr); } + let doc = args.attrs.doc(); args.context.member_items.add_item( &py_name, member_item_kind, member_kind, ident.clone(), + doc, )?; Ok(()) } @@ -1348,6 +1372,7 @@ struct MethodNurseryItem { cfgs: Vec, ident: Ident, raw: bool, + coexist: bool, doc: Option, attr_name: AttrName, call_flags: TokenStream, @@ -1394,9 +1419,14 @@ impl ToTokens for MethodNursery { _ => unreachable!(), }; let call_flags = &item.call_flags; + let coexist_flags = if item.coexist { + quote! { | rustpython_vm::function::PyMethodFlags::COEXIST.bits() } + } else { + quote! {} + }; let flags = quote! { rustpython_vm::function::PyMethodFlags::from_bits_retain( - (#binding_flags).bits() | (#call_flags).bits() + (#binding_flags).bits() | (#call_flags).bits() #coexist_flags ) }; // TODO: intern @@ -1437,6 +1467,7 @@ struct GetSetEntry { struct GetSetNursery { map: HashMap<(String, Vec), GetSetEntry>, validated: bool, + type_name: Option, } #[derive(Clone, Copy, Eq, PartialEq)] @@ -1510,7 +1541,12 @@ impl ToTokens for GetSetNursery { Some(setter) => quote_spanned! { setter.span() => .with_set(Self::#setter)}, None => quote! {}, }; - let doc = match &entry.doc { + let doc = entry.doc.clone().or_else(|| { + self.type_name + .as_deref() + .and_then(|ty| cpython_attr_doc(ty, name)) + }); + let doc = match &doc { Some(doc) => quote! { .with_doc(#doc) }, None => quote! {}, }; @@ -1540,12 +1576,14 @@ type MemberKindStr = Option; struct MemberNursery { map: HashMap, validated: bool, + type_name: Option, } struct MemberNurseryEntry { kind: MemberKindStr, getter: Option, setter: Option, + doc: Option, } #[derive(Clone, Copy, Eq, PartialEq)] @@ -1561,6 +1599,7 @@ impl MemberNursery { kind: MemberItemKind, member_kind: MemberKindStr, item_ident: Ident, + doc: Option, ) -> Result<()> { assert!(!self.validated, "new item is not allowed after validation"); let entry = self @@ -1570,6 +1609,7 @@ impl MemberNursery { kind: member_kind, getter: None, setter: None, + doc: None, }); let func = match kind { MemberItemKind::Get => &mut entry.getter, @@ -1579,6 +1619,11 @@ impl MemberNursery { bail_span!(item_ident, "Multiple member accessors with name '{}'", name); } *func = Some(item_ident); + if matches!(kind, MemberItemKind::Get) + && let Some(doc) = doc + { + entry.doc = Some(doc); + } Ok(()) } @@ -1625,10 +1670,19 @@ impl ToTokens for MemberNursery { } }; let getter = entry.getter.as_ref().unwrap(); + let doc = entry.doc.clone().or_else(|| { + self.type_name + .as_deref() + .and_then(|ty| cpython_attr_doc(ty, name)) + }); + let doc = match &doc { + Some(doc) => quote! { Some(#doc) }, + None => quote! { None }, + }; quote_spanned! { getter.span() => class.set_str_attr( #name, - ctx.new_member(#name, #member_kind, Self::#getter, #setter, class), + ctx.new_member(#name, #member_kind, Self::#getter, #setter, class, #doc), ctx, ); } @@ -1640,7 +1694,7 @@ impl ToTokens for MemberNursery { struct MethodItemMeta(ItemMetaInner); impl ItemMeta for MethodItemMeta { - const ALLOWED_NAMES: &'static [&'static str] = &["name", "raw"]; + const ALLOWED_NAMES: &'static [&'static str] = &["name", "raw", "coexist"]; fn from_inner(inner: ItemMetaInner) -> Self { Self(inner) @@ -1656,6 +1710,10 @@ impl MethodItemMeta { self.inner()._bool("raw") } + fn coexist(&self) -> Result { + self.inner()._bool("coexist") + } + fn method_name(&self) -> Result { let inner = self.inner(); let name = inner._optional_str("name")?; diff --git a/crates/host_env/src/crt_fd_unsupported.rs b/crates/host_env/src/crt_fd_unsupported.rs index b6a7918b3..7fb8a406e 100644 --- a/crates/host_env/src/crt_fd_unsupported.rs +++ b/crates/host_env/src/crt_fd_unsupported.rs @@ -96,7 +96,7 @@ impl Drop for Owned { fn drop(&mut self) {} } -impl<'fd> Borrowed<'fd> { +impl Borrowed<'_> { /// Create a `crt_fd::Borrowed` from a raw file descriptor. /// /// # Safety diff --git a/crates/host_env/src/lib.rs b/crates/host_env/src/lib.rs index aea559b45..f285771de 100644 --- a/crates/host_env/src/lib.rs +++ b/crates/host_env/src/lib.rs @@ -26,11 +26,10 @@ pub mod crt_fd; #[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))] pub mod fileutils; -#[cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))] pub mod fs; #[cfg(any(unix, windows))] pub mod locale; -#[cfg(all(feature = "native-certs", not(target_arch = "wasm32")))] +#[cfg(feature = "native-certs")] pub mod native_certs; pub mod readline; #[cfg(feature = "ssl")] diff --git a/crates/host_env/src/native_certs.rs b/crates/host_env/src/native_certs.rs index ace37e6da..02b9ad9b5 100644 --- a/crates/host_env/src/native_certs.rs +++ b/crates/host_env/src/native_certs.rs @@ -42,6 +42,7 @@ pub fn default_verify_paths() -> (String, String) { (cafile, capath) } +#[cfg(not(target_arch = "wasm32"))] pub fn load() -> LoadResult { let result = rustls_native_certs::load_native_certs(); LoadResult { @@ -58,6 +59,14 @@ pub fn load() -> LoadResult { } } +#[cfg(target_arch = "wasm32")] +pub fn load() -> LoadResult { + LoadResult { + certs: Vec::new(), + errors: vec!["native certificate store is unavailable".to_owned()], + } +} + #[cfg(test)] mod tests { use super::first_existing; diff --git a/crates/host_env/src/ssl/constants.rs b/crates/host_env/src/ssl/constants.rs index 8e24852cf..d06254eae 100644 --- a/crates/host_env/src/ssl/constants.rs +++ b/crates/host_env/src/ssl/constants.rs @@ -1,17 +1,56 @@ //! Protocol, verification, and option values published by `_ssl`. -pub const PROTOCOL_TLS: i32 = 2; -pub const PROTOCOL_TLS_CLIENT: i32 = 16; -pub const PROTOCOL_TLS_SERVER: i32 = 17; -pub const PROTOCOL_TLSV1: i32 = 3; -pub const PROTOCOL_TLSV1_1: i32 = 4; -pub const PROTOCOL_TLSV1_2: i32 = 5; -pub const PROTOCOL_TLSV1_3: i32 = 6; +/// `_SSLContext` constructor protocol. Exclusive selector, not an `SSL_OP_*` mask. +#[repr(i32)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum SslVersion { + Tls = 2, + Tls1 = 3, + Tls1_1 = 4, + Tls1_2 = 5, + Tls1_3 = 6, + TlsClient = 0x10, + TlsServer = 0x11, +} -pub const CERT_NONE: i32 = 0; -pub const CERT_OPTIONAL: i32 = 1; -pub const CERT_REQUIRED: i32 = 2; +pub const PROTOCOL_TLS: i32 = SslVersion::Tls as i32; +pub const PROTOCOL_TLS_CLIENT: i32 = SslVersion::TlsClient as i32; +pub const PROTOCOL_TLS_SERVER: i32 = SslVersion::TlsServer as i32; +pub const PROTOCOL_TLSV1: i32 = SslVersion::Tls1 as i32; +pub const PROTOCOL_TLSV1_1: i32 = SslVersion::Tls1_1 as i32; +pub const PROTOCOL_TLSV1_2: i32 = SslVersion::Tls1_2 as i32; +pub const PROTOCOL_TLSV1_3: i32 = SslVersion::Tls1_3 as i32; +/// `verify_mode`. Exclusive selector, not an `SSL_VERIFY_*` bitmask. +#[repr(i32)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum CertRequirements { + None = 0, + Optional = 1, + Required = 2, +} + +pub const CERT_NONE: i32 = CertRequirements::None as i32; +pub const CERT_OPTIONAL: i32 = CertRequirements::Optional as i32; +pub const CERT_REQUIRED: i32 = CertRequirements::Required as i32; + +/// TLS record version (`SSL3_VERSION` / `TLS1_*_VERSION` in `prov_ssl.h`). +#[repr(i32)] +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum ProtoVersion { + MinSupported = -2, + Ssl3 = 0x0300, + Tls1 = 0x0301, + Tls1_1 = 0x0302, + Tls1_2 = 0x0303, + Tls1_3 = 0x0304, + MaxSupported = -1, +} + +pub const PROTO_TLSV1_2: i32 = ProtoVersion::Tls1_2 as i32; +pub const PROTO_TLSV1_3: i32 = ProtoVersion::Tls1_3 as i32; + +/// `X509_V_FLAG_*` bitmasks from `x509_vfy.h`. pub const VERIFY_DEFAULT: i32 = 0; pub const VERIFY_CRL_CHECK_LEAF: i32 = 4; pub const VERIFY_CRL_CHECK_CHAIN: i32 = 12; @@ -24,7 +63,30 @@ pub const VERIFY_X509_PARTIAL_CHAIN: i32 = 0x80000; /// use SAN only and never fall back to the certificate Common Name. pub const HOSTFLAG_NEVER_CHECK_SUBJECT: i32 = 0x20; -pub const PROTO_TLSV1_2: i32 = 0x0303; -pub const PROTO_TLSV1_3: i32 = 0x0304; +/// `SSL_OP_NO_TLSv1_2` / `SSL_OP_NO_TLSv1_3` from `ssl.h`. pub const OP_NO_TLSV1_2: i32 = 0x0800_0000; pub const OP_NO_TLSV1_3: i32 = 0x2000_0000; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn protocol_and_cert_discriminants_match_published_consts() { + assert_eq!(PROTOCOL_TLS, 2); + assert_eq!(PROTOCOL_TLSV1, 3); + assert_eq!(PROTOCOL_TLSV1_1, 4); + assert_eq!(PROTOCOL_TLSV1_2, 5); + assert_eq!(PROTOCOL_TLSV1_3, 6); + assert_eq!(PROTOCOL_TLS_CLIENT, 0x10); + assert_eq!(PROTOCOL_TLS_SERVER, 0x11); + assert_eq!(CERT_NONE, 0); + assert_eq!(CERT_OPTIONAL, 1); + assert_eq!(CERT_REQUIRED, 2); + assert_eq!(PROTO_TLSV1_2, 0x0303); + assert_eq!(PROTO_TLSV1_3, 0x0304); + assert_eq!(ProtoVersion::Ssl3 as i32, 0x0300); + assert_eq!(ProtoVersion::Tls1 as i32, 0x0301); + assert_eq!(ProtoVersion::Tls1_1 as i32, 0x0302); + } +} diff --git a/crates/host_env/src/ssl/protocol.rs b/crates/host_env/src/ssl/protocol.rs index fd4310e01..62ea97068 100644 --- a/crates/host_env/src/ssl/protocol.rs +++ b/crates/host_env/src/ssl/protocol.rs @@ -2,7 +2,7 @@ use rustls::version::{TLS12, TLS13}; -use super::constants::{OP_NO_TLSV1_2, OP_NO_TLSV1_3, PROTO_TLSV1_2, PROTO_TLSV1_3}; +use super::constants::{OP_NO_TLSV1_2, OP_NO_TLSV1_3, PROTO_TLSV1_2, PROTO_TLSV1_3, ProtoVersion}; /// Why `server_hostname` was rejected. #[derive(Debug, Clone, PartialEq, Eq)] @@ -52,12 +52,12 @@ pub fn rustls_versions( static TLS12_ONLY: &[&rustls::SupportedProtocolVersion] = &[&TLS12]; static TLS13_ONLY: &[&rustls::SupportedProtocolVersion] = &[&TLS13]; - let min = if minimum == -2 { + let min = if minimum == ProtoVersion::MinSupported as i32 { PROTO_TLSV1_2 } else { minimum }; - let max = if maximum == -1 { + let max = if maximum == ProtoVersion::MaxSupported as i32 { PROTO_TLSV1_3 } else { maximum diff --git a/crates/stdlib/Cargo.toml b/crates/stdlib/Cargo.toml index cc282ad66..9950bb4c8 100644 --- a/crates/stdlib/Cargo.toml +++ b/crates/stdlib/Cargo.toml @@ -79,15 +79,14 @@ widestring = { workspace = true, optional = true } [target.'cfg(not(any(target_os = "ios", target_os = "android", target_os = "windows", target_arch = "wasm32", target_os = "redox")))'.dependencies] uuid = { workspace = true, features = ["v1"] } -# mmap + socket dependencies +# mmap + socket + OpenSSL dependencies [target.'cfg(not(target_arch = "wasm32"))'.dependencies] - -# OpenSSL dependencies (optional, for ssl-openssl feature) openssl = { workspace = true, optional = true } openssl-sys = { workspace = true, optional = true } foreign-types-shared = { workspace = true, optional = true } -# Rustls dependencies (optional, for ssl-rustls feature) +# Rustls is the WASI TLS engine as well as the native one. +[target.'cfg(any(not(target_arch = "wasm32"), target_os = "wasi"))'.dependencies] rustls = { workspace = true, default-features = false, features = ["std", "tls12"], optional = true } rustls-pemfile = { workspace = true, optional = true } x509-parser = { workspace = true, optional = true } diff --git a/crates/stdlib/src/lib.rs b/crates/stdlib/src/lib.rs index 89e68d818..08a3c805b 100644 --- a/crates/stdlib/src/lib.rs +++ b/crates/stdlib/src/lib.rs @@ -129,8 +129,8 @@ mod openssl; #[cfg(all( feature = "host_env", - not(target_arch = "wasm32"), - feature = "__ssl-rustls" + feature = "__ssl-rustls", + any(not(target_arch = "wasm32"), target_os = "wasi"), ))] pub mod ssl; @@ -255,8 +255,8 @@ pub fn stdlib_module_defs(ctx: &Context) -> Vec<&'static builtins::PyModuleDef> _sqlite3::module_def(ctx), #[cfg(all( feature = "host_env", - not(target_arch = "wasm32"), - feature = "__ssl-rustls" + feature = "__ssl-rustls", + any(not(target_arch = "wasm32"), target_os = "wasi"), ))] ssl::module_def(ctx), statistics::module_def(ctx), diff --git a/crates/stdlib/src/ssl.rs b/crates/stdlib/src/ssl.rs index 7636f6809..5171764a8 100644 --- a/crates/stdlib/src/ssl.rs +++ b/crates/stdlib/src/ssl.rs @@ -27,6 +27,23 @@ mod msg; pub use rustpython_host_env::ssl::{chain, cipher, oid, providers}; +#[cfg(target_arch = "wasm32")] +mod sock_wait_stub { + use rustpython_vm::{PyRef, VirtualMachine, builtins::PyOSError}; + + #[derive(Copy, Clone)] + pub(crate) enum SockWaitKind { + Read, + Write, + #[allow(dead_code, reason = "kept in lockstep with the native wait kinds")] + Connect, + } + + pub(crate) fn timeout_error_msg(vm: &VirtualMachine, msg: String) -> PyRef { + vm.new_os_subtype_error(vm.ctx.exceptions.timeout_error.to_owned(), None, msg) + } +} + pub(crate) use _ssl::module_def; #[allow(non_snake_case)] @@ -38,7 +55,6 @@ mod _ssl { hash::PyHash, lock::{PyMutex, PyRwLock}, }, - socket::{PySocket, SockWaitKind, sock_wait, timeout_error_msg}, vm::{ AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine, @@ -56,6 +72,11 @@ mod _ssl { }, }; + #[cfg(target_arch = "wasm32")] + use super::sock_wait_stub::{SockWaitKind, timeout_error_msg}; + #[cfg(not(target_arch = "wasm32"))] + use crate::socket::{PySocket, SockWaitKind, sock_wait, timeout_error_msg}; + // Import error types used in this module (others are exposed via pymodule(with(...))) use super::error::{ PySSLError, PySSLWantReadError, PySSLWantWriteError, create_ssl_eof_error, @@ -2484,24 +2505,33 @@ mod _ssl { return Ok(false); } - // Get timeout - let timeout = self.get_socket_timeout(vm)?; - - // Check for non-blocking mode (timeout = 0) - if let Some(t) = timeout - && t.is_zero() + #[cfg(target_arch = "wasm32")] { - // Non-blocking mode - don't use select + let _ = (wait_kind, vm); return Ok(false); } - // Use select with the effective timeout - let py_socket: PyRef = self.io.socket_object(vm).try_into_value(vm)?; - let socket = py_socket - .sock() - .map_err(|e| vm.new_os_error(format!("Failed to get socket: {e}")))?; + #[cfg(not(target_arch = "wasm32"))] + { + // Get timeout + let timeout = self.get_socket_timeout(vm)?; - sock_wait(&socket, wait_kind, timeout, vm) + // Check for non-blocking mode (timeout = 0) + if let Some(t) = timeout + && t.is_zero() + { + // Non-blocking mode - don't use select + return Ok(false); + } + + // Use select with the effective timeout + let py_socket: PyRef = self.io.socket_object(vm).try_into_value(vm)?; + let socket = py_socket + .sock() + .map_err(|e| vm.new_os_error(format!("Failed to get socket: {e}")))?; + + sock_wait(&socket, wait_kind, timeout, vm) + } } // Internal implementation with explicit timeout override @@ -2516,19 +2546,28 @@ mod _ssl { return Ok(false); } - if let Some(t) = timeout - && t.is_zero() + #[cfg(target_arch = "wasm32")] { - // Non-blocking mode - don't use select + let _ = (wait_kind, timeout, vm); return Ok(false); } - let py_socket: PyRef = self.io.socket_object(vm).try_into_value(vm)?; - let socket = py_socket - .sock() - .map_err(|e| vm.new_os_error(format!("Failed to get socket: {e}")))?; + #[cfg(not(target_arch = "wasm32"))] + { + if let Some(t) = timeout + && t.is_zero() + { + // Non-blocking mode - don't use select + return Ok(false); + } - sock_wait(&socket, wait_kind, timeout, vm).map_err(|e| e.into_pyexception(vm)) + let py_socket: PyRef = self.io.socket_object(vm).try_into_value(vm)?; + let socket = py_socket + .sock() + .map_err(|e| vm.new_os_error(format!("Failed to get socket: {e}")))?; + + sock_wait(&socket, wait_kind, timeout, vm).map_err(|e| e.into_pyexception(vm)) + } } fn handshake_completed(&self) -> bool { diff --git a/crates/stdlib/src/ssl/compat.rs b/crates/stdlib/src/ssl/compat.rs index de620e87e..fca79338f 100644 --- a/crates/stdlib/src/ssl/compat.rs +++ b/crates/stdlib/src/ssl/compat.rs @@ -15,6 +15,9 @@ #[path = "../openssl/ssl_data_31.rs"] mod ssl_data; +#[cfg(target_arch = "wasm32")] +use super::sock_wait_stub::{SockWaitKind, timeout_error_msg}; +#[cfg(not(target_arch = "wasm32"))] use crate::socket::{SockWaitKind, timeout_error_msg}; use crate::vm::VirtualMachine; use rustpython_host_env::ssl::{TlsConnection, TlsError}; diff --git a/crates/vm/src/builtins/asyncgenerator.rs b/crates/vm/src/builtins/asyncgenerator.rs index cc2f9bae0..79652be39 100644 --- a/crates/vm/src/builtins/asyncgenerator.rs +++ b/crates/vm/src/builtins/asyncgenerator.rs @@ -144,7 +144,7 @@ impl PyAsyncGen { } #[pygetset] fn ag_running(&self, _vm: &VirtualMachine) -> bool { - self.inner.running() + self.running_async.load() } #[pygetset] fn ag_code(&self, _vm: &VirtualMachine) -> PyRef { @@ -501,12 +501,11 @@ impl PyAsyncGenAThrow { let (ty, val, tb) = self.value.clone(); let ret = self.ag.inner.throw(self.ag.as_object(), ty, val, tb, vm); + if self.aclose && self.ignored_close(&ret) { + return Err(self.yield_close(vm)); + } let ret = if self.aclose { - if self.ignored_close(&ret) { - Err(self.yield_close(vm)) - } else { - ret.and_then(|o| o.into_async_pyresult(vm)) - } + ret.and_then(|o| o.into_async_pyresult(vm)) } else { PyAsyncGenWrappedValue::unbox(&self.ag, ret, vm) }; @@ -572,12 +571,11 @@ impl PyAsyncGenAThrow { exc_tb.unwrap_or_none(vm), vm, ); + if self.aclose && self.ignored_close(&ret) { + return Err(self.yield_close(vm)); + } let res = if self.aclose { - if self.ignored_close(&ret) { - Err(self.yield_close(vm)) - } else { - ret.and_then(|o| o.into_async_pyresult(vm)) - } + ret.and_then(|o| o.into_async_pyresult(vm)) } else { PyAsyncGenWrappedValue::unbox(&self.ag, ret, vm) }; @@ -616,7 +614,6 @@ impl PyAsyncGenAThrow { } fn yield_close(&self, vm: &VirtualMachine) -> PyBaseExceptionRef { self.ag.running_async.store(false); - self.ag.inner.closed.store(true); self.state.store(AwaitableState::Closed); vm.new_runtime_error("async generator ignored GeneratorExit") } @@ -829,14 +826,16 @@ impl IterNext for PyAnextAwaitable { /// _PyGen_Finalize for async generators impl Destructor for PyAsyncGen { fn del(zelf: &Py, vm: &VirtualMachine) -> PyResult<()> { - // Generator is already closed, nothing to do - if zelf.inner.closed.load() { + if zelf.inner.closed() { return Ok(()); } - - // Call the async generator finalizer hook if set. - Self::call_finalizer(zelf, vm); - + if zelf.ag_finalizer.lock().clone().is_some() { + Self::call_finalizer(zelf, vm); + return Ok(()); + } + if let Err(e) = zelf.inner.close(zelf.as_object(), vm) { + crate::coroutine::unraisable_while_closing(zelf.as_object(), &zelf.inner, e, vm); + } Ok(()) } } diff --git a/crates/vm/src/builtins/classmethod.rs b/crates/vm/src/builtins/classmethod.rs index dc3fddb3a..29ab45608 100644 --- a/crates/vm/src/builtins/classmethod.rs +++ b/crates/vm/src/builtins/classmethod.rs @@ -1,6 +1,8 @@ -use super::{PyBoundMethod, PyGenericAlias, PyStr, PyType, PyTypeRef}; +use super::{ + PyBoundMethod, PyGenericAlias, PyStr, PyStrInterned, PyType, PyTypeRef, object_get_dict, +}; use crate::{ - AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, + AsObject, Context, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, class::{PyClassDef, PyClassImpl}, common::lock::PyMutex, function::{FuncArgs, PySetterValue}, @@ -88,20 +90,7 @@ impl Initializer for PyClassMethod { fn init(zelf: PyRef, callable: Self::Args, vm: &VirtualMachine) -> PyResult<()> { *zelf.callable.lock() = callable.clone(); - // Copy wrapper attributes from the callable, mirroring functools.wraps. - let dict = zelf.as_object().dict().expect("classmethod has __dict__"); - for attr in [ - identifier!(vm.ctx, __doc__), - identifier!(vm.ctx, __name__), - identifier!(vm.ctx, __qualname__), - identifier!(vm.ctx, __module__), - identifier!(vm.ctx, __annotations__), - ] { - if let Ok(value) = callable.get_attr(attr, vm) { - dict.set_item(attr, value, vm)?; - } - } - Ok(()) + functools_wraps(zelf.as_object(), &callable, vm) } } @@ -117,55 +106,68 @@ impl PyClassMethod { flags(BASETYPE, HAS_DICT, HAS_WEAKREF) )] impl PyClassMethod { - #[pygetset] - fn __func__(&self) -> PyObjectRef { - self.callable.lock().clone() + #[pymember] + fn __func__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { + let zelf: &Py = zelf.try_to_value(vm)?; + Ok(zelf.callable.lock().clone()) + } + + #[pymember] + fn __wrapped__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { + let zelf: &Py = zelf.try_to_value(vm)?; + Ok(zelf.callable.lock().clone()) } #[pygetset] - fn __wrapped__(&self) -> PyObjectRef { - self.callable.lock().clone() - } - - #[pygetset] - fn __module__(&self, vm: &VirtualMachine) -> PyResult { - self.callable.lock().get_attr("__module__", vm) - } - - #[pygetset] - fn __qualname__(&self, vm: &VirtualMachine) -> PyResult { - self.callable.lock().get_attr("__qualname__", vm) - } - - #[pygetset] - fn __name__(&self, vm: &VirtualMachine) -> PyResult { - self.callable.lock().get_attr("__name__", vm) - } - - #[pygetset] - fn __annotations__(&self, vm: &VirtualMachine) -> PyResult { - self.callable.lock().get_attr("__annotations__", vm) + fn __annotations__(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let callable = zelf.callable.lock().clone(); + descriptor_get_wrapped_attribute( + callable, + zelf.as_object(), + identifier!(vm.ctx, __annotations__), + vm, + ) } #[pygetset(setter)] - fn set___annotations__(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> { - match value { - PySetterValue::Assign(v) => self.callable.lock().set_attr("__annotations__", v, vm), - PySetterValue::Delete => Ok(()), // Silently ignore delete like CPython - } + fn set___annotations__( + zelf: &Py, + value: PySetterValue, + vm: &VirtualMachine, + ) -> PyResult<()> { + descriptor_set_wrapped_attribute( + zelf.as_object(), + identifier!(vm.ctx, __annotations__), + value, + "classmethod", + vm, + ) } #[pygetset] - fn __annotate__(&self, vm: &VirtualMachine) -> PyResult { - self.callable.lock().get_attr("__annotate__", vm) + fn __annotate__(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let callable = zelf.callable.lock().clone(); + descriptor_get_wrapped_attribute( + callable, + zelf.as_object(), + identifier!(vm.ctx, __annotate__), + vm, + ) } #[pygetset(setter)] - fn set___annotate__(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> { - match value { - PySetterValue::Assign(v) => self.callable.lock().set_attr("__annotate__", v, vm), - PySetterValue::Delete => Ok(()), // Silently ignore delete like CPython - } + fn set___annotate__( + zelf: &Py, + value: PySetterValue, + vm: &VirtualMachine, + ) -> PyResult<()> { + descriptor_set_wrapped_attribute( + zelf.as_object(), + identifier!(vm.ctx, __annotate__), + value, + "classmethod", + vm, + ) } #[pygetset] @@ -225,3 +227,58 @@ impl Representable for PyClassMethod { pub(crate) fn init(context: &'static Context) { PyClassMethod::extend_class(context, context.types.classmethod_type); } + +pub(crate) fn functools_wraps( + wrapper: &PyObject, + wrapped: &PyObject, + vm: &VirtualMachine, +) -> PyResult<()> { + for attr in [ + identifier!(vm.ctx, __module__), + identifier!(vm.ctx, __name__), + identifier!(vm.ctx, __qualname__), + identifier!(vm.ctx, __doc__), + ] { + if let Some(value) = vm.get_attribute_opt(wrapped.to_owned(), attr)? { + wrapper.set_attr(attr, value, vm)?; + } + } + Ok(()) +} + +pub(crate) fn descriptor_get_wrapped_attribute( + wrapped: PyObjectRef, + obj: &PyObject, + name: &'static PyStrInterned, + vm: &VirtualMachine, +) -> PyResult { + let dict = object_get_dict(obj.to_owned(), vm)?; + if let Some(res) = dict.get_item_opt(name, vm)? { + return Ok(res); + } + let res = wrapped.get_attr(name, vm)?; + dict.set_item(name, res.clone(), vm)?; + Ok(res) +} + +pub(crate) fn descriptor_set_wrapped_attribute( + obj: &PyObject, + name: &'static PyStrInterned, + value: PySetterValue, + type_name: &str, + vm: &VirtualMachine, +) -> PyResult<()> { + let dict = object_get_dict(obj.to_owned(), vm)?; + match value { + PySetterValue::Delete => match dict.del_item(name, vm) { + Ok(()) => Ok(()), + Err(e) if e.fast_isinstance(vm.ctx.exceptions.key_error) => Err(vm + .new_attribute_error(format!( + "'{type_name}' object has no attribute '{}'", + name.as_str() + ))), + Err(e) => Err(e), + }, + PySetterValue::Assign(value) => dict.set_item(name, value, vm), + } +} diff --git a/crates/vm/src/builtins/complex.rs b/crates/vm/src/builtins/complex.rs index 3ba816ffc..c98a44a3e 100644 --- a/crates/vm/src/builtins/complex.rs +++ b/crates/vm/src/builtins/complex.rs @@ -539,14 +539,16 @@ impl PyComplex { with(PyRef, Comparable, Hashable, Constructor, AsNumber, Representable) )] impl PyComplex { - #[pygetset] - const fn real(&self) -> f64 { - self.value.re + #[pymember] + fn real(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { + let zelf: &Py = zelf.try_to_value(vm)?; + Ok(vm.ctx.new_float(zelf.value.re).into()) } - #[pygetset] - const fn imag(&self) -> f64 { - self.value.im + #[pymember] + fn imag(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { + let zelf: &Py = zelf.try_to_value(vm)?; + Ok(vm.ctx.new_float(zelf.value.im).into()) } #[pymethod] diff --git a/crates/vm/src/builtins/coroutine.rs b/crates/vm/src/builtins/coroutine.rs index c61103c7b..52cfb3937 100644 --- a/crates/vm/src/builtins/coroutine.rs +++ b/crates/vm/src/builtins/coroutine.rs @@ -185,7 +185,7 @@ impl Destructor for PyCoroutine { return Ok(()); } if let Err(e) = zelf.inner.close(zelf.as_object(), vm) { - vm.run_unraisable(e, None, zelf.as_object().to_owned()); + crate::coroutine::unraisable_while_closing(zelf.as_object(), &zelf.inner, e, vm); } Ok(()) } diff --git a/crates/vm/src/builtins/descriptor.rs b/crates/vm/src/builtins/descriptor.rs index 96bd67cce..49fc4c865 100644 --- a/crates/vm/src/builtins/descriptor.rs +++ b/crates/vm/src/builtins/descriptor.rs @@ -762,16 +762,12 @@ impl SlotFunc { func(&other, &obj, vm) // Swapped: other op obj } Self::NumTernary(func) => { - let (y, z): (PyObjectRef, crate::function::OptionalArg) = - args.bind(vm)?; - let z = z.unwrap_or_else(|| vm.ctx.none()); + let (y, z) = pow_args(args, vm)?; func(&obj, &y, &z, vm) } Self::NumTernaryRight(func) => { - let (y, z): (PyObjectRef, crate::function::OptionalArg) = - args.bind(vm)?; - let z = z.unwrap_or_else(|| vm.ctx.none()); - func(&y, &obj, &z, vm) // Swapped: y ** obj % z + let (y, z) = pow_args(args, vm)?; + func(&y, &obj, &z, vm) } // Buffer protocol Self::GetBuffer(func) => { @@ -792,6 +788,24 @@ impl SlotFunc { } } +/// wrap_ternaryfunc / check_pow_args +fn pow_args(args: FuncArgs, vm: &VirtualMachine) -> PyResult<(PyObjectRef, PyObjectRef)> { + if let Some(err) = args.check_kwargs_empty(vm) { + return Err(err); + } + let size = args.args.len(); + if !(1..=2).contains(&size) { + return Err(vm.new_type_error(format!("expected 1 or 2 arguments, got {size}"))); + } + let y = args.args[0].clone(); + let z = if size == 2 { + args.args[1].clone() + } else { + vm.ctx.none() + }; + Ok((y, z)) +} + /// Parse the `flags` argument of `__buffer__`. wrap_buffer fn parse_buffer_flags( arg: PyObjectRef, @@ -884,7 +898,16 @@ impl PyWrapper { #[pygetset] fn __doc__(&self) -> Option<&'static str> { - self.doc + let doc = self.doc?; + type_::get_doc_from_internal_doc(self.name.as_str(), doc) + } + + #[pygetset] + fn __text_signature__(&self) -> Option { + self.doc.and_then(|doc| { + type_::get_text_signature_from_internal_doc(self.name.as_str(), doc) + .map(|signature| signature.to_string()) + }) } } @@ -954,6 +977,25 @@ impl PyMethodWrapper { self.wrapper.typ.to_owned() } + #[pygetset] + fn __qualname__(&self) -> String { + format!("{}.{}", self.wrapper.typ.name(), self.wrapper.name) + } + + #[pygetset] + fn __doc__(&self) -> Option<&'static str> { + let doc = self.wrapper.doc?; + type_::get_doc_from_internal_doc(self.wrapper.name.as_str(), doc) + } + + #[pygetset] + fn __text_signature__(&self) -> Option { + self.wrapper.doc.and_then(|doc| { + type_::get_text_signature_from_internal_doc(self.wrapper.name.as_str(), doc) + .map(|signature| signature.to_string()) + }) + } + #[pymethod] fn __reduce__(zelf: PyRef, vm: &VirtualMachine) -> PyResult { let builtins_getattr = vm.builtins.get_attr("getattr", vm)?; @@ -985,10 +1027,14 @@ impl Representable for PyMethodWrapper { } impl Hashable for PyMethodWrapper { - fn hash(zelf: &Py, vm: &VirtualMachine) -> PyResult { - let obj_hash = zelf.obj.hash(vm)?; - let wrapper_hash = zelf.wrapper.as_object().get_id() as PyHash; - Ok(obj_hash ^ wrapper_hash) + fn hash(zelf: &Py, _vm: &VirtualMachine) -> PyResult { + // wrapperobject_hash: pointer hash of descr xor self + let mut hash = + (zelf.wrapper.as_object().get_id() as PyHash) ^ (zelf.obj.get_id() as PyHash); + if hash == -1 { + hash = -2; + } + Ok(hash) } } @@ -997,11 +1043,11 @@ impl Comparable for PyMethodWrapper { zelf: &Py, other: &PyObject, op: PyComparisonOp, - vm: &VirtualMachine, + _vm: &VirtualMachine, ) -> PyResult { op.eq_only(|| { let other = class_or_notimplemented!(Self, other); - let eq = zelf.wrapper.is(&other.wrapper) && vm.bool_eq(&zelf.obj, &other.obj)?; + let eq = zelf.wrapper.is(&other.wrapper) && zelf.obj.is(&other.obj); Ok(eq.into()) }) } diff --git a/crates/vm/src/builtins/dict.rs b/crates/vm/src/builtins/dict.rs index 9837c7c86..3e8c1d4d5 100644 --- a/crates/vm/src/builtins/dict.rs +++ b/crates/vm/src/builtins/dict.rs @@ -579,7 +579,9 @@ impl Py { Ok(PyArithmeticValue::Implemented(true)) } + /// Return self[key]. #[cfg_attr(feature = "flame-it", flame("PyDictRef"))] + #[pymethod(coexist)] fn __getitem__(&self, key: PyObjectRef, vm: &VirtualMachine) -> PyResult { self.inner_getitem(&*key, vm) } diff --git a/crates/vm/src/builtins/generator.rs b/crates/vm/src/builtins/generator.rs index 9f99a8378..d929e6d23 100644 --- a/crates/vm/src/builtins/generator.rs +++ b/crates/vm/src/builtins/generator.rs @@ -160,7 +160,7 @@ impl Destructor for PyGenerator { return Ok(()); } if let Err(e) = zelf.inner.close(zelf.as_object(), vm) { - vm.run_unraisable(e, None, zelf.as_object().to_owned()); + crate::coroutine::unraisable_while_closing(zelf.as_object(), &zelf.inner, e, vm); } Ok(()) } diff --git a/crates/vm/src/builtins/list.rs b/crates/vm/src/builtins/list.rs index 3ba537a81..d80eec86d 100644 --- a/crates/vm/src/builtins/list.rs +++ b/crates/vm/src/builtins/list.rs @@ -288,8 +288,10 @@ impl PyList { } } - fn __getitem__(&self, needle: PyObjectRef, vm: &VirtualMachine) -> PyResult { - self._getitem(&needle, vm) + /// Return self[index]. + #[pymethod(coexist)] + fn __getitem__(&self, index: PyObjectRef, vm: &VirtualMachine) -> PyResult { + self._getitem(&index, vm) } fn _setitem(&self, needle: &PyObject, value: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { diff --git a/crates/vm/src/builtins/property.rs b/crates/vm/src/builtins/property.rs index 65ae48222..d4bd6cdd5 100644 --- a/crates/vm/src/builtins/property.rs +++ b/crates/vm/src/builtins/property.rs @@ -135,22 +135,33 @@ impl PyProperty { // Access functions - #[pygetset] - fn fget(&self) -> Option { - self.getter.read().clone() + #[pymember] + fn fget(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { + let zelf: &Py = zelf.try_to_value(vm)?; + Ok(vm.unwrap_or_none(zelf.getter.read().clone())) } pub(crate) fn get_fget(&self) -> Option { self.getter.read().clone() } - #[pygetset] - fn fset(&self) -> Option { + #[pymember] + fn fset(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { + let zelf: &Py = zelf.try_to_value(vm)?; + Ok(vm.unwrap_or_none(zelf.setter.read().clone())) + } + + fn get_fset(&self) -> Option { self.setter.read().clone() } - #[pygetset] - fn fdel(&self) -> Option { + #[pymember] + fn fdel(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { + let zelf: &Py = zelf.try_to_value(vm)?; + Ok(vm.unwrap_or_none(zelf.deleter.read().clone())) + } + + fn get_fdel(&self) -> Option { self.deleter.read().clone() } @@ -215,9 +226,9 @@ impl PyProperty { // Create property args with updated values let args = PropertyArgs { - fget: new_getter.or_else(|| zelf.fget()), - fset: new_setter.or_else(|| zelf.fset()), - fdel: new_deleter.or_else(|| zelf.fdel()), + fget: new_getter.or_else(|| zelf.get_fget()), + fset: new_setter.or_else(|| zelf.get_fset()), + fdel: new_deleter.or_else(|| zelf.get_fdel()), doc, }; diff --git a/crates/vm/src/builtins/staticmethod.rs b/crates/vm/src/builtins/staticmethod.rs index 6838e6ccc..b069675a3 100644 --- a/crates/vm/src/builtins/staticmethod.rs +++ b/crates/vm/src/builtins/staticmethod.rs @@ -1,4 +1,9 @@ -use super::{PyGenericAlias, PyStr, PyType, PyTypeRef}; +use super::{ + PyGenericAlias, PyStr, PyType, PyTypeRef, + classmethod::{ + descriptor_get_wrapped_attribute, descriptor_set_wrapped_attribute, functools_wraps, + }, +}; use crate::{ AsObject, Context, Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, class::{PyClassDef, PyClassImpl}, @@ -80,10 +85,7 @@ impl Initializer for PyStaticMethod { fn init(zelf: PyRef, callable: Self::Args, vm: &VirtualMachine) -> PyResult<()> { *zelf.callable.lock() = callable.clone(); - if let Ok(doc) = callable.get_attr("__doc__", vm) { - zelf.as_object().set_attr("__doc__", doc, vm)?; - } - Ok(()) + functools_wraps(zelf.as_object(), &callable, vm) } } @@ -92,55 +94,68 @@ impl Initializer for PyStaticMethod { flags(BASETYPE, HAS_DICT, HAS_WEAKREF) )] impl PyStaticMethod { - #[pygetset] - fn __func__(&self) -> PyObjectRef { - self.callable.lock().clone() + #[pymember] + fn __func__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { + let zelf: &Py = zelf.try_to_value(vm)?; + Ok(zelf.callable.lock().clone()) + } + + #[pymember] + fn __wrapped__(vm: &VirtualMachine, zelf: PyObjectRef) -> PyResult { + let zelf: &Py = zelf.try_to_value(vm)?; + Ok(zelf.callable.lock().clone()) } #[pygetset] - fn __wrapped__(&self) -> PyObjectRef { - self.callable.lock().clone() - } - - #[pygetset] - fn __module__(&self, vm: &VirtualMachine) -> PyResult { - self.callable.lock().get_attr("__module__", vm) - } - - #[pygetset] - fn __qualname__(&self, vm: &VirtualMachine) -> PyResult { - self.callable.lock().get_attr("__qualname__", vm) - } - - #[pygetset] - fn __name__(&self, vm: &VirtualMachine) -> PyResult { - self.callable.lock().get_attr("__name__", vm) - } - - #[pygetset] - fn __annotations__(&self, vm: &VirtualMachine) -> PyResult { - self.callable.lock().get_attr("__annotations__", vm) + fn __annotations__(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let callable = zelf.callable.lock().clone(); + descriptor_get_wrapped_attribute( + callable, + zelf.as_object(), + identifier!(vm.ctx, __annotations__), + vm, + ) } #[pygetset(setter)] - fn set___annotations__(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> { - match value { - PySetterValue::Assign(v) => self.callable.lock().set_attr("__annotations__", v, vm), - PySetterValue::Delete => Ok(()), // Silently ignore delete like CPython - } + fn set___annotations__( + zelf: &Py, + value: PySetterValue, + vm: &VirtualMachine, + ) -> PyResult<()> { + descriptor_set_wrapped_attribute( + zelf.as_object(), + identifier!(vm.ctx, __annotations__), + value, + "staticmethod", + vm, + ) } #[pygetset] - fn __annotate__(&self, vm: &VirtualMachine) -> PyResult { - self.callable.lock().get_attr("__annotate__", vm) + fn __annotate__(zelf: &Py, vm: &VirtualMachine) -> PyResult { + let callable = zelf.callable.lock().clone(); + descriptor_get_wrapped_attribute( + callable, + zelf.as_object(), + identifier!(vm.ctx, __annotate__), + vm, + ) } #[pygetset(setter)] - fn set___annotate__(&self, value: PySetterValue, vm: &VirtualMachine) -> PyResult<()> { - match value { - PySetterValue::Assign(v) => self.callable.lock().set_attr("__annotate__", v, vm), - PySetterValue::Delete => Ok(()), // Silently ignore delete like CPython - } + fn set___annotate__( + zelf: &Py, + value: PySetterValue, + vm: &VirtualMachine, + ) -> PyResult<()> { + descriptor_set_wrapped_attribute( + zelf.as_object(), + identifier!(vm.ctx, __annotate__), + value, + "staticmethod", + vm, + ) } #[pygetset] diff --git a/crates/vm/src/coroutine.rs b/crates/vm/src/coroutine.rs index 1fde7c6f5..8aa93c519 100644 --- a/crates/vm/src/coroutine.rs +++ b/crates/vm/src/coroutine.rs @@ -1,6 +1,6 @@ use crate::{ - AsObject, Py, PyObject, PyObjectRef, PyRef, PyResult, TryFromObject, VirtualMachine, - builtins::{PyCode, PyStrRef, PyTupleRef}, + AsObject, Py, PyObject, PyObjectRef, PyPayload, PyRef, PyResult, TryFromObject, VirtualMachine, + builtins::{PyCode, PyStrRef, PyTraceback, PyTupleRef}, common::lock::PyMutex, exceptions::types::PyBaseException, frame::{ExecutionResult, FrameObject, FrameObjectRef, FrameOwner, InterpreterFrame}, @@ -8,6 +8,7 @@ use crate::{ object::{PyAtomicRef, Traverse, TraverseFn}, protocol::PyIterReturn, }; +use core::sync::atomic::Ordering; use crossbeam_utils::atomic::AtomicCell; impl ExecutionResult { @@ -86,6 +87,7 @@ fn gen_name(jen: &PyObject, vm: &VirtualMachine) -> &'static str { impl Coro { pub fn new(frame: FrameObjectRef, name: PyStrRef, qualname: PyStrRef) -> Self { let code = frame.iframe().code().to_owned(); + frame.as_object().mark_cache_published(); Self { frame: Some(frame).into(), code, @@ -97,30 +99,21 @@ impl Coro { } } - /// `_PyFrame_ClearExceptCode`. Clear locals unless another reference - /// still holds the frame object; then take_ownership instead. + /// `_PyFrame_ClearExceptCode`. Steal the frame slot first, then + /// clear locals only if this was the last reference. fn clear_except_code(&self) { - let Some(frame) = self.frame.deref() else { - return; - }; - if frame.as_object().strong_count() == 1 { - frame.clear_locals_and_stack(); - } else { - self.take_ownership(); - } - } - - /// Steal the iframe's `frame_obj` pointer (`take_ownership`). The live - /// locals stay on that frame object; this generator no longer roots it. - fn take_ownership(&self) { let Some(frame) = (unsafe { self.frame.swap(None) }) else { return; }; - frame.iframe().owner.store( - FrameOwner::FrameObject as i8, - core::sync::atomic::Ordering::Release, - ); frame.clear_generator(); + if frame.as_object().strong_count() == 1 { + frame.clear_locals_and_stack(); + } else { + frame.iframe().owner.store( + FrameOwner::FrameObject as i8, + core::sync::atomic::Ordering::Release, + ); + } } /// Retire the generator if the frame it just ran came to an end. The claim @@ -331,20 +324,18 @@ impl Coro { vm.ctx.none(), ) }); + if !matches!(&result, Ok(ExecutionResult::Yield(_))) { + self.closed.store(true); + self.clear_except_code(); + } drop(claim); match result { Ok(ExecutionResult::Yield(_)) => { Err(vm.new_runtime_error(format!("{} ignored GeneratorExit", gen_name(jen, vm)))) } - other => { - self.closed.store(true); - self.clear_except_code(); - match other { - Err(e) if !is_gen_exit(&e, vm) => Err(e), - Ok(ExecutionResult::Return(value)) => Ok(value), - _ => Ok(vm.ctx.none()), - } - } + Err(e) if !is_gen_exit(&e, vm) => Err(e), + Ok(ExecutionResult::Return(value)) => Ok(value), + _ => Ok(vm.ctx.none()), } } @@ -367,7 +358,7 @@ impl Coro { } pub fn frame_opt(&self) -> Option { - self.frame.to_owned() + self.frame.try_to_owned(Ordering::Acquire) } pub fn code(&self) -> PyRef { @@ -494,6 +485,30 @@ pub(crate) fn get_awaitable_iter(obj: PyObjectRef, vm: &VirtualMachine) -> PyRes Err(vm.new_type_error(format!("'{}' object can't be awaited", obj.class().name()))) } +pub(crate) fn unraisable_while_closing( + jen: &PyObject, + coro: &Coro, + e: crate::builtins::PyBaseExceptionRef, + vm: &VirtualMachine, +) { + // Explicit close() leaves the traceback to the caller frame. + // Finalize has no caller frame, so attach the generator site here. + if e.__traceback__().is_none() + && let Some(frame) = coro.frame_opt() + { + let lasti = frame.lasti().saturating_mul(2); + let lineno = rustpython_compiler_core::OneIndexed::new(frame.f_lineno().max(1)) + .unwrap_or(rustpython_compiler_core::OneIndexed::MIN); + let tb = PyTraceback::new(None, frame, lasti, lineno); + e.set_traceback_typed(Some(tb.into_ref(&vm.ctx))); + } + let msg = jen + .repr(vm) + .ok() + .map(|r| format!("Exception ignored while closing generator {r}")); + vm.run_unraisable(e, msg, vm.ctx.none()); +} + /// Emit DeprecationWarning for the deprecated 3-argument throw() signature. pub(crate) fn warn_deprecated_throw_signature( exc_val: &OptionalArg, diff --git a/crates/vm/src/frame.rs b/crates/vm/src/frame.rs index 282b29649..34d026d01 100644 --- a/crates/vm/src/frame.rs +++ b/crates/vm/src/frame.rs @@ -3390,13 +3390,24 @@ impl ExecutingFrame<'_> { }; if is_gen_exit { - // gen_close_iter: close the sub-iterator + // gen_close_iter: close the sub-iterator. A failed + // lookup of close is unraisable; a failed close() + // call is raised into this generator. let close_result = if let Some(coro) = self.builtin_coro(jen) { coro.close(jen, vm).map(|_| ()) - } else if let Some(close_meth) = vm.get_attribute_opt(jen.to_owned(), "close")? { - close_meth.call((), vm).map(|_| ()) } else { - Ok(()) + match vm.get_attribute_opt(jen.to_owned(), "close") { + Ok(Some(close_meth)) => close_meth.call((), vm).map(|_| ()), + Ok(None) => Ok(()), + Err(e) => { + let msg = jen + .repr(vm) + .ok() + .map(|r| format!("Exception ignored while closing generator {r}")); + vm.run_unraisable(e, msg, vm.ctx.none()); + Ok(()) + } + } }; if let Err(err) = close_result { let idx = self.lasti().saturating_sub(1) as usize; @@ -3409,7 +3420,7 @@ impl ExecutingFrame<'_> { } self.push_value(vm.ctx.none()); - vm.contextualize_exception(&err); + vm.chain_stack_item(&err); return match self.unwind_blocks(vm, UnwindReason::Raising { exception: err }) { Ok(None) => { self.prev_line.set(0); @@ -3454,7 +3465,7 @@ impl ExecutingFrame<'_> { } self.push_value(vm.ctx.none()); - vm.contextualize_exception(&err); + vm.chain_stack_item(&err); match self.unwind_blocks(vm, UnwindReason::Raising { exception: err }) { Ok(None) => { self.prev_line.set(0); @@ -3518,9 +3529,9 @@ impl ExecutingFrame<'_> { } }; - // when raising an exception, set __context__ to the current exception - // This is done in _PyErr_SetObject - vm.contextualize_exception(&exception); + // PyErr_Restore: do not touch __context__. Chain only when this + // generator's own exc_info slot is occupied (_PyErr_ChainStackItem). + vm.chain_stack_item(&exception); // always pushes Py_None before calling gen_send_ex with exc=1 // This is needed for exception handler to have correct stack state diff --git a/crates/vm/src/function/method.rs b/crates/vm/src/function/method.rs index 5c19aa112..c1a947c8d 100644 --- a/crates/vm/src/function/method.rs +++ b/crates/vm/src/function/method.rs @@ -29,7 +29,7 @@ bitflags::bitflags! { // already filled the entry. When defined, the flag allows a separate // method, "__contains__" for example, to coexist with a defined // slot like sq_contains. - // const COEXIST = 0x0040; + const COEXIST = 0x0040; // if not Py_LIMITED_API const FASTCALL = 0x0080; diff --git a/crates/vm/src/object/core.rs b/crates/vm/src/object/core.rs index 6d1588c25..e386f2b8f 100644 --- a/crates/vm/src/object/core.rs +++ b/crates/vm/src/object/core.rs @@ -1951,7 +1951,11 @@ impl PyObject { if let Err(e) = slot_del(zelf, vm) { let del_method = zelf.get_class_attr(identifier!(vm, __del__)).unwrap(); - vm.run_unraisable(e, None, del_method); + let msg = del_method + .repr(vm) + .ok() + .map(|r| format!("Exception ignored while calling deallocator {r}")); + vm.run_unraisable(e, msg, del_method); } // Undo the temporary resurrection. Always remove both @@ -2047,7 +2051,11 @@ impl PyObject { if let Err(e) = slot_del(self, vm) && let Some(del_method) = self.get_class_attr(identifier!(vm, __del__)) { - vm.run_unraisable(e, None, del_method); + let msg = del_method + .repr(vm) + .ok() + .map(|r| format!("Exception ignored while calling deallocator {r}")); + vm.run_unraisable(e, msg, del_method); } }); let _ = result; diff --git a/crates/vm/src/stdlib/errno.rs b/crates/vm/src/stdlib/errno.rs index 4b9f4a4bf..8327a3a98 100644 --- a/crates/vm/src/stdlib/errno.rs +++ b/crates/vm/src/stdlib/errno.rs @@ -26,6 +26,12 @@ mod errno_mod { #[cfg(any(unix, windows, target_os = "wasi"))] pub use rustpython_host_env::errno::errors; +#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] +pub mod errors { + pub const ECONNABORTED: i32 = 103; + pub const ECONNRESET: i32 = 104; +} + #[cfg(any(unix, windows, target_os = "wasi"))] macro_rules! e { ($name:ident) => { diff --git a/crates/vm/src/types/slot_defs.rs b/crates/vm/src/types/slot_defs.rs index 300ee3199..9d1f2dd21 100644 --- a/crates/vm/src/types/slot_defs.rs +++ b/crates/vm/src/types/slot_defs.rs @@ -1021,149 +1021,149 @@ pub static SLOT_DEFS: &[SlotDef] = &[ name: "__buffer__", accessor: SlotAccessor::BfGetBuffer, op: None, - doc: "Return a buffer object that exposes the underlying memory of the object.", + doc: "__buffer__($self, flags, /)\n--\n\nReturn a buffer object that exposes the underlying memory of the object.", }, SlotDef { name: "__release_buffer__", accessor: SlotAccessor::BfReleaseBuffer, op: None, - doc: "Release the buffer object that exposes the underlying memory of the object.", + doc: "__release_buffer__($self, buffer, /)\n--\n\nRelease the buffer object that exposes the underlying memory of the object.", }, // Type slots (tp_*) SlotDef { name: "__init__", accessor: SlotAccessor::TpInit, op: None, - doc: "Initialize self. See help(type(self)) for accurate signature.", + doc: "__init__($self, /, *args, **kwargs)\n--\n\nInitialize self. See help(type(self)) for accurate signature.", }, SlotDef { name: "__new__", accessor: SlotAccessor::TpNew, op: None, - doc: "Create and return a new object. See help(type) for accurate signature.", + doc: "__new__($type, /, *args, **kwargs)\n--\n\nCreate and return a new object. See help(type) for accurate signature.", }, SlotDef { name: "__del__", accessor: SlotAccessor::TpDel, op: None, - doc: "Called when the instance is about to be destroyed.", + doc: "__del__($self, /)\n--\n\nCalled when the instance is about to be destroyed.", }, SlotDef { name: "__repr__", accessor: SlotAccessor::TpRepr, op: None, - doc: "Return repr(self).", + doc: "__repr__($self, /)\n--\n\nReturn repr(self).", }, SlotDef { name: "__str__", accessor: SlotAccessor::TpStr, op: None, - doc: "Return str(self).", + doc: "__str__($self, /)\n--\n\nReturn str(self).", }, SlotDef { name: "__hash__", accessor: SlotAccessor::TpHash, op: None, - doc: "Return hash(self).", + doc: "__hash__($self, /)\n--\n\nReturn hash(self).", }, SlotDef { name: "__call__", accessor: SlotAccessor::TpCall, op: None, - doc: "Call self as a function.", + doc: "__call__($self, /, *args, **kwargs)\n--\n\nCall self as a function.", }, SlotDef { name: "__iter__", accessor: SlotAccessor::TpIter, op: None, - doc: "Implement iter(self).", + doc: "__iter__($self, /)\n--\n\nImplement iter(self).", }, SlotDef { name: "__next__", accessor: SlotAccessor::TpIternext, op: None, - doc: "Implement next(self).", + doc: "__next__($self, /)\n--\n\nImplement next(self).", }, // Attribute access SlotDef { name: "__getattribute__", accessor: SlotAccessor::TpGetattro, op: None, - doc: "Return getattr(self, name).", + doc: "__getattribute__($self, name, /)\n--\n\nReturn getattr(self, name).", }, SlotDef { name: "__getattr__", accessor: SlotAccessor::TpGetattro, op: None, - doc: "Implement getattr(self, name).", + doc: "__getattr__($self, name, /)\n--\n\nImplement getattr(self, name).", }, SlotDef { name: "__setattr__", accessor: SlotAccessor::TpSetattro, op: None, - doc: "Implement setattr(self, name, value).", + doc: "__setattr__($self, name, value, /)\n--\n\nImplement setattr(self, name, value).", }, SlotDef { name: "__delattr__", accessor: SlotAccessor::TpSetattro, op: Some(SlotOp::Delete), - doc: "Implement delattr(self, name).", + doc: "__delattr__($self, name, /)\n--\n\nImplement delattr(self, name).", }, // Rich comparison - all map to TpRichcompare with different op SlotDef { name: "__eq__", accessor: SlotAccessor::TpRichcompare, op: Some(SlotOp::Eq), - doc: "Return self==value.", + doc: "__eq__($self, value, /)\n--\n\nReturn self==value.", }, SlotDef { name: "__ne__", accessor: SlotAccessor::TpRichcompare, op: Some(SlotOp::Ne), - doc: "Return self!=value.", + doc: "__ne__($self, value, /)\n--\n\nReturn self!=value.", }, SlotDef { name: "__lt__", accessor: SlotAccessor::TpRichcompare, op: Some(SlotOp::Lt), - doc: "Return selfvalue.", + doc: "__gt__($self, value, /)\n--\n\nReturn self>value.", }, SlotDef { name: "__ge__", accessor: SlotAccessor::TpRichcompare, op: Some(SlotOp::Ge), - doc: "Return self>=value.", + doc: "__ge__($self, value, /)\n--\n\nReturn self>=value.", }, // Descriptor protocol SlotDef { name: "__get__", accessor: SlotAccessor::TpDescrGet, op: None, - doc: "Return an attribute of instance, which is of type owner.", + doc: "__get__($self, instance, owner=None, /)\n--\n\nReturn an attribute of instance, which is of type owner.", }, SlotDef { name: "__set__", accessor: SlotAccessor::TpDescrSet, op: None, - doc: "Set an attribute of instance to value.", + doc: "__set__($self, instance, value, /)\n--\n\nSet an attribute of instance to value.", }, SlotDef { name: "__delete__", accessor: SlotAccessor::TpDescrSet, op: Some(SlotOp::Delete), - doc: "Delete an attribute of instance.", + doc: "__delete__($self, instance, /)\n--\n\nDelete an attribute of instance.", }, // Mapping protocol (mp_*) - must come before Sequence protocol // so that mp_subscript wins over sq_item for __getitem__ @@ -1172,383 +1172,383 @@ pub static SLOT_DEFS: &[SlotDef] = &[ name: "__len__", accessor: SlotAccessor::MpLength, op: None, - doc: "Return len(self).", + doc: "__len__($self, /)\n--\n\nReturn len(self).", }, SlotDef { name: "__getitem__", accessor: SlotAccessor::MpSubscript, op: None, - doc: "Return self[key].", + doc: "__getitem__($self, key, /)\n--\n\nReturn self[key].", }, SlotDef { name: "__setitem__", accessor: SlotAccessor::MpAssSubscript, op: None, - doc: "Set self[key] to value.", + doc: "__setitem__($self, key, value, /)\n--\n\nSet self[key] to value.", }, SlotDef { name: "__delitem__", accessor: SlotAccessor::MpAssSubscript, op: Some(SlotOp::Delete), - doc: "Delete self[key].", + doc: "__delitem__($self, key, /)\n--\n\nDelete self[key].", }, // Sequence protocol (sq_*) SlotDef { name: "__len__", accessor: SlotAccessor::SqLength, op: None, - doc: "Return len(self).", + doc: "__len__($self, /)\n--\n\nReturn len(self).", }, SlotDef { name: "__getitem__", accessor: SlotAccessor::SqItem, op: None, - doc: "Return self[key].", + doc: "__getitem__($self, key, /)\n--\n\nReturn self[key].", }, SlotDef { name: "__setitem__", accessor: SlotAccessor::SqAssItem, op: None, - doc: "Set self[key] to value.", + doc: "__setitem__($self, key, value, /)\n--\n\nSet self[key] to value.", }, SlotDef { name: "__delitem__", accessor: SlotAccessor::SqAssItem, op: Some(SlotOp::Delete), - doc: "Delete self[key].", + doc: "__delitem__($self, key, /)\n--\n\nDelete self[key].", }, SlotDef { name: "__contains__", accessor: SlotAccessor::SqContains, op: None, - doc: "Return key in self.", + doc: "__contains__($self, key, /)\n--\n\nReturn key in self.", }, // Number protocol - binary ops with left/right variants SlotDef { name: "__add__", accessor: SlotAccessor::NbAdd, op: Some(SlotOp::Left), - doc: "Return self+value.", + doc: "__add__($self, value, /)\n--\n\nReturn self+value.", }, SlotDef { name: "__radd__", accessor: SlotAccessor::NbAdd, op: Some(SlotOp::Right), - doc: "Return value+self.", + doc: "__radd__($self, value, /)\n--\n\nReturn value+self.", }, SlotDef { name: "__iadd__", accessor: SlotAccessor::NbInplaceAdd, op: None, - doc: "Implement self+=value.", + doc: "__iadd__($self, value, /)\n--\n\nImplement self+=value.", }, SlotDef { name: "__sub__", accessor: SlotAccessor::NbSubtract, op: Some(SlotOp::Left), - doc: "Return self-value.", + doc: "__sub__($self, value, /)\n--\n\nReturn self-value.", }, SlotDef { name: "__rsub__", accessor: SlotAccessor::NbSubtract, op: Some(SlotOp::Right), - doc: "Return value-self.", + doc: "__rsub__($self, value, /)\n--\n\nReturn value-self.", }, SlotDef { name: "__isub__", accessor: SlotAccessor::NbInplaceSubtract, op: None, - doc: "Implement self-=value.", + doc: "__isub__($self, value, /)\n--\n\nImplement self-=value.", }, SlotDef { name: "__mul__", accessor: SlotAccessor::NbMultiply, op: Some(SlotOp::Left), - doc: "Return self*value.", + doc: "__mul__($self, value, /)\n--\n\nReturn self*value.", }, SlotDef { name: "__rmul__", accessor: SlotAccessor::NbMultiply, op: Some(SlotOp::Right), - doc: "Return value*self.", + doc: "__rmul__($self, value, /)\n--\n\nReturn value*self.", }, SlotDef { name: "__imul__", accessor: SlotAccessor::NbInplaceMultiply, op: None, - doc: "Implement self*=value.", + doc: "__imul__($self, value, /)\n--\n\nImplement self*=value.", }, SlotDef { name: "__mod__", accessor: SlotAccessor::NbRemainder, op: Some(SlotOp::Left), - doc: "Return self%value.", + doc: "__mod__($self, value, /)\n--\n\nReturn self%value.", }, SlotDef { name: "__rmod__", accessor: SlotAccessor::NbRemainder, op: Some(SlotOp::Right), - doc: "Return value%self.", + doc: "__rmod__($self, value, /)\n--\n\nReturn value%self.", }, SlotDef { name: "__imod__", accessor: SlotAccessor::NbInplaceRemainder, op: None, - doc: "Implement self%=value.", + doc: "__imod__($self, value, /)\n--\n\nImplement self%=value.", }, SlotDef { name: "__divmod__", accessor: SlotAccessor::NbDivmod, op: Some(SlotOp::Left), - doc: "Return divmod(self, value).", + doc: "__divmod__($self, value, /)\n--\n\nReturn divmod(self, value).", }, SlotDef { name: "__rdivmod__", accessor: SlotAccessor::NbDivmod, op: Some(SlotOp::Right), - doc: "Return divmod(value, self).", + doc: "__rdivmod__($self, value, /)\n--\n\nReturn divmod(value, self).", }, SlotDef { name: "__pow__", accessor: SlotAccessor::NbPower, op: Some(SlotOp::Left), - doc: "Return pow(self, value, mod).", + doc: "__pow__($self, value, mod=None, /)\n--\n\nReturn pow(self, value, mod).", }, SlotDef { name: "__rpow__", accessor: SlotAccessor::NbPower, op: Some(SlotOp::Right), - doc: "Return pow(value, self, mod).", + doc: "__rpow__($self, value, mod=None, /)\n--\n\nReturn pow(value, self, mod).", }, SlotDef { name: "__ipow__", accessor: SlotAccessor::NbInplacePower, op: None, - doc: "Implement self**=value.", + doc: "__ipow__($self, value, /)\n--\n\nImplement self**=value.", }, SlotDef { name: "__lshift__", accessor: SlotAccessor::NbLshift, op: Some(SlotOp::Left), - doc: "Return self<>value.", + doc: "__rshift__($self, value, /)\n--\n\nReturn self>>value.", }, SlotDef { name: "__rrshift__", accessor: SlotAccessor::NbRshift, op: Some(SlotOp::Right), - doc: "Return value>>self.", + doc: "__rrshift__($self, value, /)\n--\n\nReturn value>>self.", }, SlotDef { name: "__irshift__", accessor: SlotAccessor::NbInplaceRshift, op: None, - doc: "Implement self>>=value.", + doc: "__irshift__($self, value, /)\n--\n\nImplement self>>=value.", }, SlotDef { name: "__and__", accessor: SlotAccessor::NbAnd, op: Some(SlotOp::Left), - doc: "Return self&value.", + doc: "__and__($self, value, /)\n--\n\nReturn self&value.", }, SlotDef { name: "__rand__", accessor: SlotAccessor::NbAnd, op: Some(SlotOp::Right), - doc: "Return value&self.", + doc: "__rand__($self, value, /)\n--\n\nReturn value&self.", }, SlotDef { name: "__iand__", accessor: SlotAccessor::NbInplaceAnd, op: None, - doc: "Implement self&=value.", + doc: "__iand__($self, value, /)\n--\n\nImplement self&=value.", }, SlotDef { name: "__xor__", accessor: SlotAccessor::NbXor, op: Some(SlotOp::Left), - doc: "Return self^value.", + doc: "__xor__($self, value, /)\n--\n\nReturn self^value.", }, SlotDef { name: "__rxor__", accessor: SlotAccessor::NbXor, op: Some(SlotOp::Right), - doc: "Return value^self.", + doc: "__rxor__($self, value, /)\n--\n\nReturn value^self.", }, SlotDef { name: "__ixor__", accessor: SlotAccessor::NbInplaceXor, op: None, - doc: "Implement self^=value.", + doc: "__ixor__($self, value, /)\n--\n\nImplement self^=value.", }, SlotDef { name: "__or__", accessor: SlotAccessor::NbOr, op: Some(SlotOp::Left), - doc: "Return self|value.", + doc: "__or__($self, value, /)\n--\n\nReturn self|value.", }, SlotDef { name: "__ror__", accessor: SlotAccessor::NbOr, op: Some(SlotOp::Right), - doc: "Return value|self.", + doc: "__ror__($self, value, /)\n--\n\nReturn value|self.", }, SlotDef { name: "__ior__", accessor: SlotAccessor::NbInplaceOr, op: None, - doc: "Implement self|=value.", + doc: "__ior__($self, value, /)\n--\n\nImplement self|=value.", }, SlotDef { name: "__floordiv__", accessor: SlotAccessor::NbFloorDivide, op: Some(SlotOp::Left), - doc: "Return self//value.", + doc: "__floordiv__($self, value, /)\n--\n\nReturn self//value.", }, SlotDef { name: "__rfloordiv__", accessor: SlotAccessor::NbFloorDivide, op: Some(SlotOp::Right), - doc: "Return value//self.", + doc: "__rfloordiv__($self, value, /)\n--\n\nReturn value//self.", }, SlotDef { name: "__ifloordiv__", accessor: SlotAccessor::NbInplaceFloorDivide, op: None, - doc: "Implement self//=value.", + doc: "__ifloordiv__($self, value, /)\n--\n\nImplement self//=value.", }, SlotDef { name: "__truediv__", accessor: SlotAccessor::NbTrueDivide, op: Some(SlotOp::Left), - doc: "Return self/value.", + doc: "__truediv__($self, value, /)\n--\n\nReturn self/value.", }, SlotDef { name: "__rtruediv__", accessor: SlotAccessor::NbTrueDivide, op: Some(SlotOp::Right), - doc: "Return value/self.", + doc: "__rtruediv__($self, value, /)\n--\n\nReturn value/self.", }, SlotDef { name: "__itruediv__", accessor: SlotAccessor::NbInplaceTrueDivide, op: None, - doc: "Implement self/=value.", + doc: "__itruediv__($self, value, /)\n--\n\nImplement self/=value.", }, SlotDef { name: "__matmul__", accessor: SlotAccessor::NbMatrixMultiply, op: Some(SlotOp::Left), - doc: "Return self@value.", + doc: "__matmul__($self, value, /)\n--\n\nReturn self@value.", }, SlotDef { name: "__rmatmul__", accessor: SlotAccessor::NbMatrixMultiply, op: Some(SlotOp::Right), - doc: "Return value@self.", + doc: "__rmatmul__($self, value, /)\n--\n\nReturn value@self.", }, SlotDef { name: "__imatmul__", accessor: SlotAccessor::NbInplaceMatrixMultiply, op: None, - doc: "Implement self@=value.", + doc: "__imatmul__($self, value, /)\n--\n\nImplement self@=value.", }, // Number unary operations SlotDef { name: "__neg__", accessor: SlotAccessor::NbNegative, op: None, - doc: "Return -self.", + doc: "__neg__($self, /)\n--\n\nReturn -self.", }, SlotDef { name: "__pos__", accessor: SlotAccessor::NbPositive, op: None, - doc: "Return +self.", + doc: "__pos__($self, /)\n--\n\nReturn +self.", }, SlotDef { name: "__abs__", accessor: SlotAccessor::NbAbsolute, op: None, - doc: "Return abs(self).", + doc: "__abs__($self, /)\n--\n\nReturn abs(self).", }, SlotDef { name: "__invert__", accessor: SlotAccessor::NbInvert, op: None, - doc: "Return ~self.", + doc: "__invert__($self, /)\n--\n\nReturn ~self.", }, SlotDef { name: "__bool__", accessor: SlotAccessor::NbBool, op: None, - doc: "Return self != 0.", + doc: "__bool__($self, /)\n--\n\nTrue if self else False", }, SlotDef { name: "__int__", accessor: SlotAccessor::NbInt, op: None, - doc: "Return int(self).", + doc: "__int__($self, /)\n--\n\nReturn int(self).", }, SlotDef { name: "__float__", accessor: SlotAccessor::NbFloat, op: None, - doc: "Return float(self).", + doc: "__float__($self, /)\n--\n\nReturn float(self).", }, SlotDef { name: "__index__", accessor: SlotAccessor::NbIndex, op: None, - doc: "Return self converted to an integer, if self is suitable for use as an index into a list.", + doc: "__index__($self, /)\n--\n\nReturn self converted to an integer, if self is suitable for use as an index into a list.", }, // Sequence inplace operations (also map to number slots for some types) SlotDef { name: "__add__", accessor: SlotAccessor::SqConcat, op: None, - doc: "Return self+value.", + doc: "__add__($self, value, /)\n--\n\nReturn self+value.", }, SlotDef { name: "__mul__", accessor: SlotAccessor::SqRepeat, op: None, - doc: "Return self*value.", + doc: "__mul__($self, value, /)\n--\n\nReturn self*value.", }, SlotDef { name: "__rmul__", accessor: SlotAccessor::SqRepeat, op: None, - doc: "Return value*self.", + doc: "__rmul__($self, value, /)\n--\n\nReturn value*self.", }, SlotDef { name: "__iadd__", accessor: SlotAccessor::SqInplaceConcat, op: None, - doc: "Implement self+=value.", + doc: "__iadd__($self, value, /)\n--\n\nImplement self+=value.", }, SlotDef { name: "__imul__", accessor: SlotAccessor::SqInplaceRepeat, op: None, - doc: "Implement self*=value.", + doc: "__imul__($self, value, /)\n--\n\nImplement self*=value.", }, ]; diff --git a/crates/vm/src/vm/context.rs b/crates/vm/src/vm/context.rs index d886dbaf2..53c743931 100644 --- a/crates/vm/src/vm/context.rs +++ b/crates/vm/src/vm/context.rs @@ -628,13 +628,14 @@ impl Context { getter: fn(&VirtualMachine, PyObjectRef) -> PyResult, setter: MemberSetterFunc, class: &'static Py, + doc: Option<&str>, ) -> PyRef { let member_def = PyMemberDef { name: name.to_owned(), kind: member_kind, getter: MemberGetter::Getter(getter), setter: MemberSetter::Setter(setter), - doc: None, + doc: doc.map(str::to_owned), }; let member_descriptor = PyMemberDescriptor { common: PyDescriptorOwned { diff --git a/crates/vm/src/vm/mod.rs b/crates/vm/src/vm/mod.rs index d608ccc5e..7168789bd 100644 --- a/crates/vm/src/vm/mod.rs +++ b/crates/vm/src/vm/mod.rs @@ -3123,6 +3123,15 @@ impl VirtualMachine { } } + /// `_PyErr_ChainStackItem`: if the current `exc_info` slot is occupied, + /// set that handled exception as `__context__` of `exception`. A vacant + /// current slot must not walk to an outer frame's exception. + pub(crate) fn chain_stack_item(&self, exception: &Py) { + if self.current_exception().is_some() { + self.contextualize_exception(exception); + } + } + pub(crate) fn contextualize_exception(&self, exception: &Py) { if let Some(context_exc) = self.topmost_exception() && !context_exc.is(exception) diff --git a/crates/wasm/Cargo.toml b/crates/wasm/Cargo.toml index aa7a95984..547f84d24 100644 --- a/crates/wasm/Cargo.toml +++ b/crates/wasm/Cargo.toml @@ -42,6 +42,14 @@ web-sys = { workspace = true, features = [ "Response" ] } +[target.'cfg(all(target_arch = "wasm32", any(target_os = "unknown", target_os = "none")))'.dependencies] +getrandom = { workspace = true, features = ["wasm_js"] } +getrandom_02 = { package = "getrandom", version = "0.2", features = ["js"] } +rustls = { workspace = true, default-features = false, features = ["std", "tls12", "custom-provider"] } +rustls-pki-types = { version = "1", default-features = false, features = ["alloc", "std", "web"] } +rustls-rustcrypto = "0.0.2-alpha" +rustpython-host_env = { workspace = true, features = ["ssl"] } + [package.metadata.wasm-pack.profile.release] wasm-opt = false#["-O1"] @@ -49,4 +57,4 @@ wasm-opt = false#["-O1"] workspace = true [package.metadata.cargo-shear] -ignored = ["serde"] +ignored = ["serde", "getrandom", "rustls-pki-types"] diff --git a/crates/wasm/src/lib.rs b/crates/wasm/src/lib.rs index 041cb864e..2461d9a9d 100644 --- a/crates/wasm/src/lib.rs +++ b/crates/wasm/src/lib.rs @@ -3,6 +3,10 @@ extern crate alloc; pub mod browser_module; pub mod convert; pub mod js_module; +#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] +mod socket; +#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] +mod ssl; pub mod vm_class; pub mod wasm_builtins; diff --git a/crates/wasm/src/socket.rs b/crates/wasm/src/socket.rs new file mode 100644 index 000000000..efbb53c97 --- /dev/null +++ b/crates/wasm/src/socket.rs @@ -0,0 +1,457 @@ +pub(crate) use _socket::module_def; + +#[pymodule] +mod _socket { + use rustpython_vm::{ + Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, + builtins::{PyType, PyTypeRef}, + common::lock::PyMutex, + function::{OptionalArg, OptionalOption}, + types::{Constructor, Initializer}, + }; + use std::net::{Ipv4Addr, Ipv6Addr}; + use std::sync::atomic::{AtomicI32, Ordering}; + + #[pyattr] + const AF_UNSPEC: i32 = 0; + #[pyattr] + const AF_UNIX: i32 = 1; + #[pyattr] + const AF_INET: i32 = 2; + #[pyattr] + const AF_INET6: i32 = 10; + + #[pyattr] + const SOCK_STREAM: i32 = 1; + #[pyattr] + const SOCK_DGRAM: i32 = 2; + #[pyattr] + const SOCK_RAW: i32 = 3; + + #[pyattr] + const SOL_SOCKET: i32 = 1; + #[pyattr] + const SO_REUSEADDR: i32 = 2; + #[pyattr] + const SO_TYPE: i32 = 3; + #[pyattr] + const SO_ERROR: i32 = 4; + #[pyattr] + const SO_BROADCAST: i32 = 6; + #[pyattr] + const SO_KEEPALIVE: i32 = 9; + #[pyattr] + const SO_RCVBUF: i32 = 8; + #[pyattr] + const SO_SNDBUF: i32 = 7; + + #[pyattr] + const IPPROTO_IP: i32 = 0; + #[pyattr] + const IPPROTO_TCP: i32 = 6; + #[pyattr] + const IPPROTO_UDP: i32 = 17; + #[pyattr] + const IPPROTO_IPV6: i32 = 41; + #[pyattr] + const SOL_TCP: i32 = IPPROTO_TCP; + + #[pyattr] + const SHUT_RD: i32 = 0; + #[pyattr] + const SHUT_WR: i32 = 1; + #[pyattr] + const SHUT_RDWR: i32 = 2; + + #[pyattr] + const AI_PASSIVE: i32 = 1; + #[pyattr] + const AI_CANONNAME: i32 = 2; + #[pyattr] + const AI_NUMERICHOST: i32 = 4; + #[pyattr] + const AI_NUMERICSERV: i32 = 8; + #[pyattr] + const AI_ADDRCONFIG: i32 = 32; + + #[pyattr] + const NI_NUMERICHOST: i32 = 1; + #[pyattr] + const NI_NUMERICSERV: i32 = 2; + #[pyattr] + const NI_NOFQDN: i32 = 4; + #[pyattr] + const NI_NAMEREQD: i32 = 8; + #[pyattr] + const NI_DGRAM: i32 = 16; + + #[pyattr] + const INADDR_ANY: u32 = 0; + #[pyattr] + const INADDR_LOOPBACK: u32 = 0x7f00_0001; + #[pyattr] + const INADDR_BROADCAST: u32 = 0xffff_ffff; + #[pyattr] + const INADDR_NONE: u32 = 0xffff_ffff; + #[pyattr] + const IPPORT_RESERVED: i32 = 1024; + #[pyattr] + const IPPORT_USERRESERVED: i32 = 5000; + #[pyattr] + const TCP_NODELAY: i32 = 1; + #[pyattr(name = "has_ipv6")] + const HAS_IPV6: bool = true; + + static DEFAULT_TIMEOUT: AtomicI32 = AtomicI32::new(-1); + + fn unsupported( + vm: &VirtualMachine, + op: &str, + ) -> PyRef { + vm.new_os_error(format!("{op} is not available")) + } + + #[pyattr] + fn error(vm: &VirtualMachine) -> PyTypeRef { + vm.ctx.exceptions.os_error.to_owned() + } + + #[pyattr] + fn timeout(vm: &VirtualMachine) -> PyTypeRef { + vm.ctx.exceptions.timeout_error.to_owned() + } + + #[pyattr(once)] + fn herror(vm: &VirtualMachine) -> PyTypeRef { + vm.ctx.new_exception_type( + "socket", + "herror", + Some(vec![vm.ctx.exceptions.os_error.to_owned()]), + ) + } + + #[pyattr(once)] + fn gaierror(vm: &VirtualMachine) -> PyTypeRef { + vm.ctx.new_exception_type( + "socket", + "gaierror", + Some(vec![vm.ctx.exceptions.os_error.to_owned()]), + ) + } + + #[pyfunction] + const fn htonl(x: u32) -> u32 { + u32::to_be(x) + } + + #[pyfunction] + const fn htons(x: u16) -> u16 { + u16::to_be(x) + } + + #[pyfunction] + const fn ntohl(x: u32) -> u32 { + u32::from_be(x) + } + + #[pyfunction] + const fn ntohs(x: u16) -> u16 { + u16::from_be(x) + } + + #[pyfunction] + fn inet_aton( + ip: rustpython_vm::builtins::PyUtf8StrRef, + vm: &VirtualMachine, + ) -> PyResult> { + ip.as_str() + .parse::() + .map(|addr| addr.octets().to_vec()) + .map_err(|_| vm.new_os_error("illegal IP address string passed to inet_aton")) + } + + #[pyfunction] + fn inet_ntoa( + packed: rustpython_vm::function::ArgBytesLike, + vm: &VirtualMachine, + ) -> PyResult { + let buf = packed.borrow_buf(); + let octets: [u8; 4] = (&*buf) + .try_into() + .map_err(|_| vm.new_os_error("packed IP wrong length for inet_ntoa"))?; + Ok(Ipv4Addr::from(octets).to_string()) + } + + #[pyfunction] + fn inet_pton( + af: i32, + ip: rustpython_vm::builtins::PyUtf8StrRef, + vm: &VirtualMachine, + ) -> PyResult> { + match af { + AF_INET => ip + .as_str() + .parse::() + .map(|addr| addr.octets().to_vec()) + .map_err(|_| vm.new_os_error("illegal IP address string passed to inet_pton")), + AF_INET6 => ip + .as_str() + .parse::() + .map(|addr| addr.octets().to_vec()) + .map_err(|_| vm.new_os_error("illegal IP address string passed to inet_pton")), + _ => Err(vm.new_os_error("Address family not supported")), + } + } + + #[pyfunction] + fn inet_ntop( + af: i32, + packed: rustpython_vm::function::ArgBytesLike, + vm: &VirtualMachine, + ) -> PyResult { + let buf = packed.borrow_buf(); + match af { + AF_INET => { + let octets: [u8; 4] = (&*buf).try_into().map_err(|_| { + vm.new_value_error("invalid length of packed IP address string") + })?; + Ok(Ipv4Addr::from(octets).to_string()) + } + AF_INET6 => { + let octets: [u8; 16] = (&*buf).try_into().map_err(|_| { + vm.new_value_error("invalid length of packed IP address string") + })?; + Ok(Ipv6Addr::from(octets).to_string()) + } + _ => Err(vm.new_value_error("unknown address family")), + } + } + + #[pyfunction] + fn getdefaulttimeout() -> Option { + let timeout = DEFAULT_TIMEOUT.load(Ordering::Relaxed); + (timeout >= 0).then_some(f64::from(timeout)) + } + + #[pyfunction] + fn setdefaulttimeout(timeout: OptionalOption, vm: &VirtualMachine) -> PyResult<()> { + match timeout.flatten() { + None => DEFAULT_TIMEOUT.store(-1, Ordering::Relaxed), + Some(value) if value >= 0.0 => { + DEFAULT_TIMEOUT.store(value as i32, Ordering::Relaxed); + } + Some(_) => return Err(vm.new_value_error("Timeout value out of range")), + } + Ok(()) + } + + #[derive(FromArgs)] + struct SocketInitArgs { + #[pyarg(any, optional)] + family: OptionalArg, + #[pyarg(any, optional)] + r#type: OptionalArg, + #[pyarg(any, optional)] + proto: OptionalArg, + #[pyarg(any, optional)] + fileno: OptionalArg>, + } + + #[pyattr(name = "socket")] + #[pyattr(name = "SocketType")] + #[pyclass(name = "socket")] + #[derive(Debug, PyPayload)] + struct PySocket { + family: AtomicI32, + kind: AtomicI32, + proto: AtomicI32, + timeout: PyMutex>, + closed: PyMutex, + } + + impl Default for PySocket { + fn default() -> Self { + Self { + family: AtomicI32::new(AF_INET), + kind: AtomicI32::new(SOCK_STREAM), + proto: AtomicI32::new(0), + timeout: PyMutex::new(None), + closed: PyMutex::new(false), + } + } + } + + #[pyclass(with(Constructor, Initializer), flags(BASETYPE))] + impl PySocket { + fn ensure_open(&self, vm: &VirtualMachine) -> PyResult<()> { + if *self.closed.lock() { + return Err(vm.new_os_error("Bad file descriptor")); + } + Ok(()) + } + + #[pygetset] + fn family(&self) -> i32 { + self.family.load(Ordering::Relaxed) + } + + #[pygetset] + fn r#type(&self) -> i32 { + self.kind.load(Ordering::Relaxed) + } + + #[pygetset] + fn proto(&self) -> i32 { + self.proto.load(Ordering::Relaxed) + } + + #[pymethod] + fn fileno(&self) -> i32 { + if *self.closed.lock() { -1 } else { 0 } + } + + #[pymethod] + fn close(&self) { + *self.closed.lock() = true; + } + + #[pymethod] + fn detach(&self) -> i32 { + *self.closed.lock() = true; + -1 + } + + #[pymethod] + fn gettimeout(&self) -> Option { + *self.timeout.lock() + } + + #[pymethod] + fn settimeout(&self, timeout: OptionalOption, vm: &VirtualMachine) -> PyResult<()> { + *self.timeout.lock() = match timeout.flatten() { + None => None, + Some(value) if value >= 0.0 => Some(value), + Some(_) => return Err(vm.new_value_error("Timeout value out of range")), + }; + Ok(()) + } + + #[pymethod] + fn setblocking(&self, blocking: bool) { + *self.timeout.lock() = if blocking { None } else { Some(0.0) }; + } + + #[pymethod] + fn getblocking(&self) -> bool { + !matches!(*self.timeout.lock(), Some(t) if t == 0.0) + } + + #[pymethod] + fn getsockopt(&self, level: i32, optname: i32, vm: &VirtualMachine) -> PyResult { + self.ensure_open(vm)?; + if level == SOL_SOCKET && optname == SO_TYPE { + return Ok(self.kind.load(Ordering::Relaxed)); + } + if level == SOL_SOCKET && optname == SO_ERROR { + return Ok(0); + } + Err(unsupported(vm, "getsockopt").into()) + } + + #[pymethod] + fn setsockopt( + &self, + _level: i32, + _optname: i32, + _value: OptionalArg, + vm: &VirtualMachine, + ) -> PyResult<()> { + self.ensure_open(vm)?; + Ok(()) + } + + #[pymethod] + fn bind(&self, _address: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + self.ensure_open(vm)?; + Err(unsupported(vm, "bind").into()) + } + + #[pymethod] + fn connect(&self, _address: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + self.ensure_open(vm)?; + Err(unsupported(vm, "connect").into()) + } + + #[pymethod] + fn listen(&self, _backlog: OptionalArg, vm: &VirtualMachine) -> PyResult<()> { + self.ensure_open(vm)?; + Err(unsupported(vm, "listen").into()) + } + + #[pymethod] + fn accept(&self, vm: &VirtualMachine) -> PyResult<(PyObjectRef, PyObjectRef)> { + self.ensure_open(vm)?; + Err(unsupported(vm, "accept").into()) + } + + #[pymethod] + fn send( + &self, + _data: rustpython_vm::function::ArgBytesLike, + vm: &VirtualMachine, + ) -> PyResult { + self.ensure_open(vm)?; + Err(unsupported(vm, "send").into()) + } + + #[pymethod] + fn recv(&self, _bufsize: i32, vm: &VirtualMachine) -> PyResult> { + self.ensure_open(vm)?; + Err(unsupported(vm, "recv").into()) + } + + #[pymethod] + fn shutdown(&self, _how: i32, vm: &VirtualMachine) -> PyResult<()> { + self.ensure_open(vm)?; + Err(unsupported(vm, "shutdown").into()) + } + + #[pymethod] + fn getsockname(&self, vm: &VirtualMachine) -> PyResult<(String, i32)> { + self.ensure_open(vm)?; + Ok(("0.0.0.0".to_owned(), 0)) + } + + #[pymethod] + fn getpeername(&self, vm: &VirtualMachine) -> PyResult<(String, i32)> { + self.ensure_open(vm)?; + Err(unsupported(vm, "getpeername").into()) + } + } + + impl Constructor for PySocket { + type Args = (); + + fn py_new(_cls: &Py, _args: Self::Args, _vm: &VirtualMachine) -> PyResult { + Ok(Self::default()) + } + } + + impl Initializer for PySocket { + type Args = SocketInitArgs; + + fn init(zelf: PyRef, args: Self::Args, _vm: &VirtualMachine) -> PyResult<()> { + let family = args.family.unwrap_or(AF_INET); + let kind = args.r#type.unwrap_or(SOCK_STREAM); + let proto = args.proto.unwrap_or(0); + let _ = args.fileno; + zelf.family.store(family, Ordering::Relaxed); + zelf.kind.store(kind, Ordering::Relaxed); + zelf.proto.store(proto, Ordering::Relaxed); + *zelf.closed.lock() = false; + let default = DEFAULT_TIMEOUT.load(Ordering::Relaxed); + *zelf.timeout.lock() = (default >= 0).then_some(f64::from(default)); + Ok(()) + } + } +} diff --git a/crates/wasm/src/ssl.rs b/crates/wasm/src/ssl.rs new file mode 100644 index 000000000..cc3a09e4c --- /dev/null +++ b/crates/wasm/src/ssl.rs @@ -0,0 +1,1004 @@ +pub(crate) use _ssl::module_def; + +#[allow(non_snake_case, non_upper_case_globals)] +#[pymodule] +mod _ssl { + use rustls::pki_types::ServerName; + use rustls::{ClientConfig, ClientConnection, Connection}; + use rustpython_host_env::ssl::{ + self as host_ssl, MemoryBio, ProtoVersion, TlsConnection, TlsError, cipher, oid, + providers::CryptoExt, rustls_versions, validate_hostname, verify::NoVerifier, + }; + use rustpython_vm::{ + Py, PyObjectRef, PyPayload, PyRef, PyResult, VirtualMachine, + builtins::{ + PyBaseException, PyBaseExceptionRef, PyBytesRef, PyOSError, PyStrRef, PyType, + PyUtf8StrRef, + }, + common::lock::{PyMutex, PyRwLock}, + function::{ArgBytesLike, OptionalArg, OptionalOption}, + stdlib::_warnings, + types::Constructor, + }; + use std::sync::Arc; + + #[pyattr] + const PROTOCOL_TLS: i32 = host_ssl::PROTOCOL_TLS; + #[pyattr] + const PROTOCOL_SSLv23: i32 = PROTOCOL_TLS; + #[pyattr] + const PROTOCOL_TLS_CLIENT: i32 = host_ssl::PROTOCOL_TLS_CLIENT; + #[pyattr] + const PROTOCOL_TLS_SERVER: i32 = host_ssl::PROTOCOL_TLS_SERVER; + #[pyattr] + const PROTOCOL_TLSv1: i32 = host_ssl::PROTOCOL_TLSV1; + #[pyattr] + const PROTOCOL_TLSv1_1: i32 = host_ssl::PROTOCOL_TLSV1_1; + #[pyattr] + const PROTOCOL_TLSv1_2: i32 = host_ssl::PROTOCOL_TLSV1_2; + #[pyattr] + const PROTOCOL_TLSv1_3: i32 = host_ssl::PROTOCOL_TLSV1_3; + + #[pyattr] + const PROTO_SSLv3: i32 = ProtoVersion::Ssl3 as i32; + #[pyattr] + const PROTO_TLSv1: i32 = ProtoVersion::Tls1 as i32; + #[pyattr] + const PROTO_TLSv1_1: i32 = ProtoVersion::Tls1_1 as i32; + #[pyattr] + const PROTO_TLSv1_2: i32 = host_ssl::PROTO_TLSV1_2; + #[pyattr] + const PROTO_TLSv1_3: i32 = host_ssl::PROTO_TLSV1_3; + #[pyattr] + const PROTO_MINIMUM_SUPPORTED: i32 = ProtoVersion::MinSupported as i32; + #[pyattr] + const PROTO_MAXIMUM_SUPPORTED: i32 = ProtoVersion::MaxSupported as i32; + + #[pyattr] + const CERT_NONE: i32 = host_ssl::CERT_NONE; + #[pyattr] + const CERT_OPTIONAL: i32 = host_ssl::CERT_OPTIONAL; + #[pyattr] + const CERT_REQUIRED: i32 = host_ssl::CERT_REQUIRED; + + #[pyattr] + const VERIFY_DEFAULT: i32 = host_ssl::VERIFY_DEFAULT; + #[pyattr] + const VERIFY_CRL_CHECK_LEAF: i32 = host_ssl::VERIFY_CRL_CHECK_LEAF; + #[pyattr] + const VERIFY_CRL_CHECK_CHAIN: i32 = host_ssl::VERIFY_CRL_CHECK_CHAIN; + #[pyattr] + const VERIFY_X509_STRICT: i32 = host_ssl::VERIFY_X509_STRICT; + #[pyattr] + const VERIFY_ALLOW_PROXY_CERTS: i32 = host_ssl::VERIFY_ALLOW_PROXY_CERTS; + #[pyattr] + const VERIFY_X509_TRUSTED_FIRST: i32 = host_ssl::VERIFY_X509_TRUSTED_FIRST; + #[pyattr] + const VERIFY_X509_PARTIAL_CHAIN: i32 = host_ssl::VERIFY_X509_PARTIAL_CHAIN; + #[pyattr] + const HOSTFLAG_NEVER_CHECK_SUBJECT: i32 = host_ssl::HOSTFLAG_NEVER_CHECK_SUBJECT; + + #[pyattr] + const OP_NO_SSLv2: i32 = 0; + #[pyattr] + const OP_NO_SSLv3: i32 = 0x0200_0000; + #[pyattr] + const OP_NO_TLSv1: i32 = 0x0400_0000; + #[pyattr] + const OP_NO_TLSv1_1: i32 = 0x1000_0000; + #[pyattr] + const OP_NO_TLSv1_2: i32 = host_ssl::OP_NO_TLSV1_2; + #[pyattr] + const OP_NO_TLSv1_3: i32 = host_ssl::OP_NO_TLSV1_3; + #[pyattr] + const OP_NO_COMPRESSION: i32 = 0x0002_0000; + #[pyattr] + const OP_CIPHER_SERVER_PREFERENCE: i32 = 0x0040_0000; + #[pyattr] + const OP_SINGLE_DH_USE: i32 = 0; + #[pyattr] + const OP_SINGLE_ECDH_USE: i32 = 0; + #[pyattr] + const OP_NO_TICKET: i32 = 0x0000_4000; + #[pyattr] + const OP_LEGACY_SERVER_CONNECT: i32 = 0x0000_0004; + #[pyattr] + const OP_NO_RENEGOTIATION: i32 = 0x4000_0000; + #[pyattr] + const OP_IGNORE_UNEXPECTED_EOF: i32 = 0x0000_0080; + #[pyattr] + const OP_ENABLE_MIDDLEBOX_COMPAT: i32 = 0x0010_0000; + #[pyattr] + const OP_ALL: i32 = 0x0000_0BFB; + + #[pyattr] + const ALERT_DESCRIPTION_CLOSE_NOTIFY: i32 = 0; + #[pyattr] + const ALERT_DESCRIPTION_UNEXPECTED_MESSAGE: i32 = 10; + #[pyattr] + const ALERT_DESCRIPTION_BAD_RECORD_MAC: i32 = 20; + #[pyattr] + const ALERT_DESCRIPTION_HANDSHAKE_FAILURE: i32 = 40; + #[pyattr] + const ALERT_DESCRIPTION_BAD_CERTIFICATE: i32 = 42; + #[pyattr] + const ALERT_DESCRIPTION_CERTIFICATE_EXPIRED: i32 = 45; + #[pyattr] + const ALERT_DESCRIPTION_UNKNOWN_CA: i32 = 48; + #[pyattr] + const ALERT_DESCRIPTION_DECODE_ERROR: i32 = 50; + #[pyattr] + const ALERT_DESCRIPTION_PROTOCOL_VERSION: i32 = 70; + #[pyattr] + const ALERT_DESCRIPTION_INTERNAL_ERROR: i32 = 80; + #[pyattr] + const ALERT_DESCRIPTION_UNRECOGNIZED_NAME: i32 = 112; + + #[pyattr] + const SSL_ERROR_NONE: i32 = 0; + #[pyattr] + const SSL_ERROR_SSL: i32 = 1; + #[pyattr] + const SSL_ERROR_WANT_READ: i32 = 2; + #[pyattr] + const SSL_ERROR_WANT_WRITE: i32 = 3; + #[pyattr] + const SSL_ERROR_WANT_X509_LOOKUP: i32 = 4; + #[pyattr] + const SSL_ERROR_SYSCALL: i32 = 5; + #[pyattr] + const SSL_ERROR_ZERO_RETURN: i32 = 6; + #[pyattr] + const SSL_ERROR_WANT_CONNECT: i32 = 7; + #[pyattr] + const SSL_ERROR_EOF: i32 = 8; + #[pyattr] + const SSL_ERROR_INVALID_ERROR_CODE: i32 = 10; + + #[pyattr] + const OPENSSL_VERSION_NUMBER: i32 = 0x3030_0000; + #[pyattr] + const OPENSSL_VERSION: &str = "OpenSSL 3.3.0-compatible (rustls-rustcrypto)"; + #[pyattr] + const OPENSSL_VERSION_INFO: (i32, i32, i32, i32, i32) = (3, 3, 0, 0, 15); + #[pyattr] + const _OPENSSL_API_VERSION: (i32, i32, i32, i32, i32) = (3, 3, 0, 0, 15); + + #[pyattr(once)] + fn _DEFAULT_CIPHERS(_vm: &VirtualMachine) -> String { + cipher::default_cipher_string() + } + + #[pyattr] + const HAS_SNI: bool = true; + #[pyattr] + const HAS_TLS_UNIQUE: bool = false; + #[pyattr] + const HAS_ECDH: bool = true; + #[pyattr] + const HAS_NPN: bool = false; + #[pyattr] + const HAS_ALPN: bool = true; + #[pyattr] + const HAS_PSK: bool = false; + #[pyattr] + const HAS_SSLv2: bool = false; + #[pyattr] + const HAS_SSLv3: bool = false; + #[pyattr] + const HAS_TLSv1: bool = false; + #[pyattr] + const HAS_TLSv1_1: bool = false; + #[pyattr] + const HAS_TLSv1_2: bool = true; + #[pyattr] + const HAS_TLSv1_3: bool = true; + #[pyattr] + const HAS_PHA: bool = false; + + #[pyattr] + const ENCODING_PEM: i32 = 1; + #[pyattr] + const ENCODING_DER: i32 = 2; + #[pyattr] + const ENCODING_PEM_AUX: i32 = 0x101; + + #[pyattr] + #[pyexception(name = "SSLError", base = PyOSError)] + #[derive(Debug)] + #[repr(transparent)] + struct PySSLError(PyOSError); + + #[pyexception] + impl PySSLError { + #[pymethod] + fn __str__(zelf: &Py, vm: &VirtualMachine) -> PyResult { + use rustpython_vm::AsObject; + if let Ok(strerror) = zelf.as_object().get_attr("strerror", vm) + && !vm.is_none(&strerror) + { + return strerror.str(vm); + } + let args = zelf.args(); + if args.len() == 1 { + args.as_slice()[0].str(vm) + } else { + args.as_object().str(vm) + } + } + } + + #[pyattr] + #[pyexception(name = "SSLZeroReturnError", base = PySSLError, impl)] + #[derive(Debug)] + #[repr(transparent)] + struct PySSLZeroReturnError(PySSLError); + + #[pyattr] + #[pyexception(name = "SSLWantReadError", base = PySSLError, impl)] + #[derive(Debug)] + #[repr(transparent)] + struct PySSLWantReadError(PySSLError); + + #[pyattr] + #[pyexception(name = "SSLWantWriteError", base = PySSLError, impl)] + #[derive(Debug)] + #[repr(transparent)] + struct PySSLWantWriteError(PySSLError); + + #[pyattr] + #[pyexception(name = "SSLSyscallError", base = PySSLError, impl)] + #[derive(Debug)] + #[repr(transparent)] + struct PySSLSyscallError(PySSLError); + + #[pyattr] + #[pyexception(name = "SSLEOFError", base = PySSLError, impl)] + #[derive(Debug)] + #[repr(transparent)] + struct PySSLEOFError(PySSLError); + + #[pyattr] + #[pyexception(name = "SSLCertVerificationError", base = PySSLError, impl)] + #[derive(Debug)] + #[repr(transparent)] + struct PySSLCertVerificationError(PySSLError); + + fn ssl_error(vm: &VirtualMachine, msg: impl Into) -> rustpython_vm::PyRef { + vm.new_os_subtype_error(PySSLError::class(&vm.ctx).to_owned(), None, msg.into()) + } + + fn map_tls(vm: &VirtualMachine, err: TlsError) -> PyBaseExceptionRef { + match err { + TlsError::WantRead => vm + .new_os_subtype_error( + PySSLWantReadError::class(&vm.ctx).to_owned(), + Some(SSL_ERROR_WANT_READ), + "The operation did not complete (read)", + ) + .upcast(), + TlsError::WantWrite => vm + .new_os_subtype_error( + PySSLWantWriteError::class(&vm.ctx).to_owned(), + Some(SSL_ERROR_WANT_WRITE), + "The operation did not complete (write)", + ) + .upcast(), + TlsError::ZeroReturn => vm + .new_os_subtype_error( + PySSLZeroReturnError::class(&vm.ctx).to_owned(), + Some(SSL_ERROR_ZERO_RETURN), + "TLS/SSL connection has been closed (EOF)", + ) + .upcast(), + TlsError::Eof => vm + .new_os_subtype_error( + PySSLEOFError::class(&vm.ctx).to_owned(), + Some(SSL_ERROR_EOF), + "EOF occurred in violation of protocol", + ) + .upcast(), + TlsError::CertVerification(err) => vm + .new_os_subtype_error( + PySSLCertVerificationError::class(&vm.ctx).to_owned(), + Some(SSL_ERROR_SSL), + format!("{err}"), + ) + .upcast(), + other => ssl_error(vm, format!("{other:?}")).upcast(), + } + } + + #[pyattr] + #[pyclass(name = "MemoryBIO", module = "ssl")] + #[derive(Debug, PyPayload)] + struct PyMemoryBIO { + inner: PyMutex, + } + + #[pyclass(with(Constructor), flags(BASETYPE))] + impl PyMemoryBIO { + #[pymethod] + fn read(&self, len: OptionalArg, vm: &VirtualMachine) -> PyResult { + let mut bio = self.inner.lock(); + let read_len = match len { + OptionalArg::Present(n) if n >= 0 => n as usize, + OptionalArg::Present(n) => { + return Err(vm.new_value_error(format!("negative read length: {n}"))); + } + OptionalArg::Missing => bio.pending(), + }; + Ok(vm.ctx.new_bytes(bio.read(read_len))) + } + + #[pymethod] + fn write(&self, buf: ArgBytesLike, vm: &VirtualMachine) -> PyResult { + self.inner + .lock() + .write(&buf.borrow_buf()) + .map_err(|err| ssl_error(vm, err.to_string()).upcast()) + } + + #[pymethod] + fn write_eof(&self) { + self.inner.lock().write_eof(); + } + + #[pygetset] + fn pending(&self) -> i32 { + self.inner.lock().pending() as i32 + } + + #[pygetset] + fn eof(&self) -> bool { + self.inner.lock().eof() + } + } + + impl Constructor for PyMemoryBIO { + type Args = (); + + fn py_new(_cls: &Py, _args: Self::Args, _vm: &VirtualMachine) -> PyResult { + Ok(Self { + inner: PyMutex::new(MemoryBio::new()), + }) + } + } + + #[pyattr] + #[pyclass(name = "SSLSession", module = "ssl")] + #[derive(Debug, PyPayload)] + struct PySSLSession {} + + #[pyclass(flags(BASETYPE))] + impl PySSLSession { + #[pygetset] + fn id(&self) -> Vec { + Vec::new() + } + + #[pygetset] + fn time(&self) -> i32 { + 0 + } + + #[pygetset] + fn timeout(&self) -> i32 { + 0 + } + + #[pygetset] + fn ticket_lifetime_hint(&self) -> i32 { + 0 + } + + #[pygetset] + fn has_ticket(&self) -> bool { + false + } + } + + #[pyattr] + #[pyclass(name = "_SSLContext", module = "ssl")] + #[derive(Debug, PyPayload)] + struct PySSLContext { + protocol: i32, + check_hostname: PyRwLock, + host_flags: PyRwLock, + verify_mode: PyRwLock, + verify_flags: PyRwLock, + options: PyRwLock, + minimum_version: PyRwLock, + maximum_version: PyRwLock, + alpn_protocols: PyRwLock>>, + } + + #[derive(FromArgs)] + struct WrapBioArgs { + incoming: PyRef, + outgoing: PyRef, + #[pyarg(named, optional)] + server_side: OptionalArg, + #[pyarg(named, optional)] + server_hostname: OptionalArg>, + #[pyarg(named, optional)] + owner: OptionalArg, + #[pyarg(named, optional)] + session: OptionalArg, + } + + #[derive(FromArgs)] + struct WrapSocketArgs { + sock: PyObjectRef, + server_side: bool, + #[pyarg(positional, optional)] + server_hostname: OptionalArg>, + #[pyarg(named, optional)] + owner: OptionalArg, + #[pyarg(named, optional)] + session: OptionalArg, + } + + #[pyclass(with(Constructor), flags(BASETYPE))] + impl PySSLContext { + #[pygetset] + fn check_hostname(&self) -> bool { + *self.check_hostname.read() + } + + #[pygetset(setter)] + fn set_check_hostname(&self, value: bool) { + *self.check_hostname.write() = value; + } + + #[pygetset] + fn verify_mode(&self) -> i32 { + *self.verify_mode.read() + } + + #[pygetset(setter)] + fn set_verify_mode(&self, mode: i32, vm: &VirtualMachine) -> PyResult<()> { + if !(CERT_NONE..=CERT_REQUIRED).contains(&mode) { + return Err(vm.new_value_error(format!("invalid value for verify_mode"))); + } + if mode == CERT_NONE && *self.check_hostname.read() { + return Err(vm.new_value_error( + "Cannot set verify_mode to CERT_NONE when check_hostname is enabled", + )); + } + *self.verify_mode.write() = mode; + Ok(()) + } + + #[pygetset] + fn verify_flags(&self) -> i32 { + *self.verify_flags.read() + } + + #[pygetset(setter)] + fn set_verify_flags(&self, flags: i32) { + *self.verify_flags.write() = flags; + } + + #[pygetset] + fn options(&self) -> i32 { + *self.options.read() + } + + #[pygetset(setter)] + fn set_options(&self, options: i32) { + *self.options.write() = options; + } + + #[pygetset] + fn minimum_version(&self) -> i32 { + *self.minimum_version.read() + } + + #[pygetset(setter)] + fn set_minimum_version(&self, value: i32) { + *self.minimum_version.write() = value; + } + + #[pygetset] + fn maximum_version(&self) -> i32 { + *self.maximum_version.read() + } + + #[pygetset(setter)] + fn set_maximum_version(&self, value: i32) { + *self.maximum_version.write() = value; + } + + #[pygetset] + fn _host_flags(&self) -> i32 { + *self.host_flags.read() + } + + #[pygetset(setter)] + fn set__host_flags(&self, flags: i32) { + *self.host_flags.write() = flags; + } + + #[pygetset] + fn protocol(&self) -> i32 { + self.protocol + } + + #[pygetset] + fn security_level(&self) -> i32 { + 2 + } + + #[pymethod] + fn set_ciphers(&self, _ciphers: PyUtf8StrRef) {} + + #[pymethod] + fn _set_alpn_protocols(&self, protos: ArgBytesLike, vm: &VirtualMachine) -> PyResult<()> { + let parsed = rustpython_host_env::ssl::parse_length_prefixed_alpn(&protos.borrow_buf()) + .map_err(|err| vm.new_value_error(err.0))?; + *self.alpn_protocols.write() = parsed; + Ok(()) + } + + #[pymethod] + fn set_default_verify_paths(&self) {} + + #[pymethod] + fn load_verify_locations( + &self, + _cafile: OptionalOption, + _capath: OptionalOption, + _cadata: OptionalOption, + ) { + } + + #[pymethod] + fn load_cert_chain( + &self, + _certfile: PyObjectRef, + _keyfile: OptionalOption, + _password: OptionalOption, + vm: &VirtualMachine, + ) -> PyResult<()> { + Err(ssl_error(vm, "certificate files are unavailable").upcast()) + } + + #[pymethod] + fn load_dh_params(&self, _path: PyObjectRef, vm: &VirtualMachine) -> PyResult<()> { + Err(ssl_error(vm, "DH parameters are unavailable").upcast()) + } + + #[pymethod] + fn set_ecdh_curve(&self, _name: PyObjectRef) {} + + #[pymethod] + fn cert_store_stats(&self, vm: &VirtualMachine) -> PyObjectRef { + vm.ctx.new_dict().into() + } + + #[pymethod] + fn get_ca_certs(&self, _binary_form: OptionalArg, vm: &VirtualMachine) -> PyResult { + Ok(vm.ctx.new_list(Vec::new()).into()) + } + + #[pymethod] + fn _wrap_bio( + zelf: PyRef, + args: WrapBioArgs, + vm: &VirtualMachine, + ) -> PyResult> { + let server_side = args.server_side.unwrap_or(false); + let hostname = match args.server_hostname.into_option().flatten() { + Some(name) => { + let hostname = name.as_str(); + validate_hostname(hostname) + .map_err(|err| vm.new_value_error(err.message().to_owned()))?; + Some(hostname.to_owned()) + } + None => None, + }; + if server_side && zelf.protocol == PROTOCOL_TLS_CLIENT { + return Err(ssl_error( + vm, + "Cannot create a server socket with a PROTOCOL_TLS_CLIENT context", + ) + .upcast()); + } + if !server_side && zelf.protocol == PROTOCOL_TLS_SERVER { + return Err(ssl_error( + vm, + "Cannot create a client socket with a PROTOCOL_TLS_SERVER context", + ) + .upcast()); + } + let _ = (args.owner, args.session); + PySSLSocket { + incoming: args.incoming, + outgoing: args.outgoing, + context: PyRwLock::new(zelf), + server_side, + server_hostname: hostname, + connection: PyMutex::new(None), + } + .into_ref_with_type(vm, vm.class("_ssl", "_SSLSocket")) + } + + #[pymethod] + fn _wrap_socket( + zelf: PyRef, + args: WrapSocketArgs, + vm: &VirtualMachine, + ) -> PyResult> { + let _ = args.sock; + Self::_wrap_bio( + zelf, + WrapBioArgs { + incoming: PyMemoryBIO { + inner: PyMutex::new(MemoryBio::new()), + } + .into_ref(&vm.ctx), + outgoing: PyMemoryBIO { + inner: PyMutex::new(MemoryBio::new()), + } + .into_ref(&vm.ctx), + server_side: OptionalArg::Present(args.server_side), + server_hostname: args.server_hostname, + owner: args.owner, + session: args.session, + }, + vm, + ) + } + } + + impl Constructor for PySSLContext { + type Args = (i32,); + + fn py_new( + _cls: &Py, + (protocol,): Self::Args, + vm: &VirtualMachine, + ) -> PyResult { + let deprecated = match protocol { + PROTOCOL_TLS => Some("PROTOCOL_TLS"), + PROTOCOL_TLSv1_2 => Some("PROTOCOL_TLSv1_2"), + PROTOCOL_TLS_CLIENT | PROTOCOL_TLS_SERVER | PROTOCOL_TLSv1_3 => None, + PROTOCOL_TLSv1 | PROTOCOL_TLSv1_1 => { + return Err(vm.new_value_error( + "TLS 1.0 and 1.1 are not supported by rustls for security reasons", + )); + } + _ => { + return Err(vm.new_value_error(format!("invalid protocol version: {protocol}"))); + } + }; + if let Some(name) = deprecated { + _warnings::warn( + vm.ctx.exceptions.deprecation_warning, + format!("ssl.{name} is deprecated"), + 2, + vm, + )?; + } + + let default_options = OP_ALL + | OP_NO_SSLv2 + | OP_NO_SSLv3 + | OP_NO_COMPRESSION + | OP_CIPHER_SERVER_PREFERENCE + | OP_SINGLE_DH_USE + | OP_SINGLE_ECDH_USE + | OP_ENABLE_MIDDLEBOX_COMPAT; + let verify_mode = if protocol == PROTOCOL_TLS_CLIENT { + CERT_REQUIRED + } else { + CERT_NONE + }; + let (minimum_version, maximum_version) = match protocol { + PROTOCOL_TLSv1_2 => (PROTO_TLSv1_2, PROTO_TLSv1_2), + PROTOCOL_TLSv1_3 => (PROTO_TLSv1_3, PROTO_TLSv1_3), + _ => (PROTO_MINIMUM_SUPPORTED, PROTO_MAXIMUM_SUPPORTED), + }; + + Ok(Self { + protocol, + check_hostname: PyRwLock::new(protocol == PROTOCOL_TLS_CLIENT), + host_flags: PyRwLock::new(0), + verify_mode: PyRwLock::new(verify_mode), + verify_flags: PyRwLock::new(VERIFY_DEFAULT | VERIFY_X509_TRUSTED_FIRST), + options: PyRwLock::new(default_options), + minimum_version: PyRwLock::new(minimum_version), + maximum_version: PyRwLock::new(maximum_version), + alpn_protocols: PyRwLock::new(Vec::new()), + }) + } + } + + #[pyattr] + #[pyclass(name = "_SSLSocket", module = "ssl")] + #[derive(Debug, PyPayload)] + struct PySSLSocket { + incoming: PyRef, + outgoing: PyRef, + context: PyRwLock>, + server_side: bool, + server_hostname: Option, + connection: PyMutex>, + } + + #[pyclass] + impl PySSLSocket { + fn ensure_conn(&self, vm: &VirtualMachine) -> PyResult<()> { + if self.connection.lock().is_some() { + return Ok(()); + } + if self.server_side { + return Err(ssl_error(vm, "server certificates are unavailable").upcast()); + } + let context = self.context.read().clone(); + let versions = rustls_versions( + *context.minimum_version.read(), + *context.maximum_version.read(), + *context.options.read(), + ); + let mut builder = + ClientConfig::builder_with_provider(Arc::new(CryptoExt::get_provider().clone())) + .with_protocol_versions(versions) + .map_err(|err| ssl_error(vm, err.to_string()).upcast())? + .dangerous() + .with_custom_certificate_verifier(Arc::new(NoVerifier)) + .with_no_client_auth(); + let alpn = context.alpn_protocols.read().clone(); + if !alpn.is_empty() { + builder.alpn_protocols = alpn; + } + let hostname = self.server_hostname.as_deref().unwrap_or("localhost"); + let server_name = ServerName::try_from(hostname.to_owned()) + .map_err(|_| vm.new_value_error(format!("invalid server hostname: {hostname}")))?; + let conn = ClientConnection::new(Arc::new(builder), server_name) + .map_err(|err| ssl_error(vm, err.to_string()).upcast())?; + *self.connection.lock() = Some(TlsConnection::new(Connection::Client(conn))); + Ok(()) + } + + fn pump(&self, vm: &VirtualMachine) -> PyResult<()> { + self.ensure_conn(vm)?; + let mut connection = self.connection.lock(); + let conn = connection.as_mut().expect("connection created above"); + let incoming = self.incoming.inner.lock().read(usize::MAX); + if !incoming.is_empty() { + conn.feed_tls(&incoming).map_err(|err| map_tls(vm, err))?; + } + conn.process_packets().map_err(|err| map_tls(vm, err))?; + let outgoing = conn.drain_tls().map_err(|err| map_tls(vm, err))?; + if !outgoing.is_empty() { + self.outgoing + .inner + .lock() + .write(&outgoing) + .map_err(|err| ssl_error(vm, err.to_string()).upcast())?; + } + Ok(()) + } + + #[pymethod] + fn do_handshake(&self, vm: &VirtualMachine) -> PyResult<()> { + self.pump(vm)?; + let connected = self + .connection + .lock() + .as_ref() + .is_some_and(|conn| !conn.is_handshaking()); + if connected { + Ok(()) + } else { + Err(map_tls(vm, TlsError::WantRead)) + } + } + + #[pymethod] + fn read(&self, len: OptionalArg, vm: &VirtualMachine) -> PyResult> { + let size = len.unwrap_or(1024); + if size < 0 { + return Err(vm.new_value_error("size should not be negative")); + } + self.pump(vm)?; + let mut connection = self.connection.lock(); + let conn = connection + .as_mut() + .ok_or_else(|| ssl_error(vm, "SSL connection is closed").upcast())?; + let mut buf = vec![0; size as usize]; + match std::io::Read::read(&mut conn.inner_mut().reader(), &mut buf) { + Ok(0) => Err(map_tls(vm, TlsError::ZeroReturn)), + Ok(n) => { + buf.truncate(n); + Ok(buf) + } + Err(err) if err.kind() == std::io::ErrorKind::WouldBlock => { + Err(map_tls(vm, TlsError::WantRead)) + } + Err(err) => Err(ssl_error(vm, err.to_string()).upcast()), + } + } + + #[pymethod] + fn write(&self, data: ArgBytesLike, vm: &VirtualMachine) -> PyResult { + self.ensure_conn(vm)?; + let bytes = data.borrow_buf(); + let mut connection = self.connection.lock(); + let conn = connection + .as_mut() + .ok_or_else(|| ssl_error(vm, "SSL connection is closed").upcast())?; + let n = std::io::Write::write(&mut conn.inner_mut().writer(), &bytes) + .map_err(|err| ssl_error(vm, err.to_string()).upcast())?; + drop(connection); + self.pump(vm)?; + Ok(n) + } + + #[pymethod] + fn pending(&self) -> usize { + self.connection + .lock() + .as_mut() + .map_or(0, TlsConnection::pending_plaintext) + } + + #[pymethod] + fn shutdown(&self, vm: &VirtualMachine) -> PyResult<()> { + if let Some(conn) = self.connection.lock().as_mut() { + conn.send_close_notify(); + } + self.pump(vm) + } + + #[pygetset] + fn context(&self) -> PyRef { + self.context.read().clone() + } + + #[pygetset(setter)] + fn set_context(&self, ctx: PyRef) { + *self.context.write() = ctx; + } + + #[pygetset] + fn server_hostname(&self) -> Option { + self.server_hostname.clone() + } + + #[pygetset] + fn server_side(&self) -> bool { + self.server_side + } + + #[pygetset] + fn session(&self, vm: &VirtualMachine) -> PyObjectRef { + vm.ctx.none() + } + + #[pygetset(setter)] + fn set_session(&self, _session: PyObjectRef) {} + + #[pygetset] + fn session_reused(&self) -> bool { + false + } + + #[pymethod] + fn peer_certificate(&self, _binary: OptionalArg, vm: &VirtualMachine) -> PyObjectRef { + vm.ctx.none() + } + + #[pymethod] + fn cipher(&self, vm: &VirtualMachine) -> PyObjectRef { + vm.ctx.none() + } + + #[pymethod] + fn shared_ciphers(&self, vm: &VirtualMachine) -> PyObjectRef { + vm.ctx.none() + } + + #[pymethod] + fn compression(&self, vm: &VirtualMachine) -> PyObjectRef { + vm.ctx.none() + } + + #[pymethod] + fn version(&self) -> Option<&'static str> { + match self + .connection + .lock() + .as_ref() + .and_then(TlsConnection::protocol_version) + { + Some(rustls::ProtocolVersion::TLSv1_3) => Some("TLSv1.3"), + Some(rustls::ProtocolVersion::TLSv1_2) => Some("TLSv1.2"), + _ => None, + } + } + + #[pymethod] + fn selected_alpn_protocol(&self) -> Option { + self.connection.lock().as_ref().and_then(|conn| { + conn.alpn_protocol() + .map(|proto| String::from_utf8_lossy(proto).into_owned()) + }) + } + } + + #[derive(FromArgs)] + struct Txt2ObjArgs { + txt: PyUtf8StrRef, + #[pyarg(named, default = false)] + name: bool, + } + + fn oid_tuple(entry: &oid::OidEntry, vm: &VirtualMachine) -> PyObjectRef { + vm.new_tuple(( + vm.ctx.new_int(entry.nid), + vm.ctx.new_str(entry.short_name), + vm.ctx.new_str(entry.long_name), + entry + .oid_string() + .map_or_else(|| vm.ctx.none(), |oid| vm.ctx.new_str(oid).into()), + )) + .into() + } + + #[pyfunction] + fn txt2obj(args: Txt2ObjArgs, vm: &VirtualMachine) -> PyResult { + let txt = args.txt.as_str(); + let entry = if txt.chars().next().is_some_and(|c| c.is_ascii_digit()) { + oid::find_by_oid_string(txt) + } else if args.name { + oid::find_by_name(txt) + } else { + None + } + .ok_or_else(|| vm.new_value_error(format!("unknown object '{txt}'")))?; + Ok(oid_tuple(entry, vm)) + } + + #[pyfunction] + fn nid2obj(nid: i32, vm: &VirtualMachine) -> PyResult { + let entry = oid::find_by_nid(nid) + .ok_or_else(|| vm.new_value_error(format!("unknown NID {nid}")))?; + Ok(oid_tuple(entry, vm)) + } + + #[pyfunction] + fn get_default_verify_paths(vm: &VirtualMachine) -> PyObjectRef { + vm.ctx + .new_tuple(vec![ + vm.ctx.new_str("SSL_CERT_FILE").into(), + vm.ctx.new_str("/etc/ssl/cert.pem").into(), + vm.ctx.new_str("SSL_CERT_DIR").into(), + vm.ctx.new_str("/etc/ssl/certs").into(), + ]) + .into() + } + + #[pyfunction] + fn RAND_status() -> i32 { + 1 + } + + #[pyfunction] + fn RAND_add(_string: PyObjectRef, _entropy: f64) {} + + #[pyfunction] + fn RAND_bytes(n: i32, vm: &VirtualMachine) -> PyResult { + if n < 0 { + return Err(vm.new_value_error("num must be positive")); + } + let mut buf = vm.new_zeroed_bytes(n as usize)?; + CryptoExt::get_provider() + .secure_random + .fill(&mut buf) + .map_err(|_| vm.new_os_error("Failed to generate random bytes"))?; + Ok(PyBytesRef::from(vm.ctx.new_bytes(buf))) + } +} diff --git a/crates/wasm/src/vm_class.rs b/crates/wasm/src/vm_class.rs index 6780d4d1b..0b1e5575f 100644 --- a/crates/wasm/src/vm_class.rs +++ b/crates/wasm/src/vm_class.rs @@ -86,10 +86,20 @@ impl StoredVirtualMachine { .add_frozen_modules(rustpython_pylib::FROZEN_STDLIB); } - // Add wasm-specific modules + // Add wasm-specific modules. Browser `_ssl` / `_socket` live here + // rather than rustpython-stdlib: there are no BSD sockets, and rustls + // is driven through MemoryBIO / wrap_bio. let js_def = js_module::module_def(&builder.ctx); builder = builder.add_native_module(js_def); + #[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] + { + install_browser_tls_provider(); + let ssl_def = crate::ssl::module_def(&builder.ctx); + let socket_def = crate::socket::module_def(&builder.ctx); + builder = builder.add_native_modules(&[ssl_def, socket_def]); + } + if inject_browser_module { let window_def = _window::module_def(&builder.ctx); let browser_def = browser_module::module_def(&builder.ctx); @@ -115,6 +125,26 @@ impl StoredVirtualMachine { } } +#[cfg(all(target_arch = "wasm32", not(target_os = "wasi")))] +fn install_browser_tls_provider() { + use rustls::Error; + use rustpython_host_env::ssl::providers::CryptoExt; + use std::sync::Arc; + + fn no_ticketer() -> Result, Error> { + Err(Error::General("TLS session tickets are unavailable".into())) + } + + let ext = CryptoExt { + all_cipher_suites: None, + default_cipher_suites: None, + all_kx_groups: None, + any_supported_key: Some(rustls_rustcrypto::sign::any_supported_type), + ticketer: no_ticketer, + }; + let _ = CryptoExt::set_provider(rustls_rustcrypto::provider(), ext); +} + // It's fine that it's thread local, since WASM doesn't even have threads yet. thread_local! // probably gets compiled down to a normal-ish static variable, like Atomic* types do: // https://rustwasm.github.io/2018/10/24/multithreading-rust-and-wasm.html#atomic-instructions