diff --git a/Cargo.lock b/Cargo.lock index a710707..532fc8f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,35 @@ dependencies = [ "memchr", ] +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "aws-lc-rs" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + [[package]] name = "base64" version = "0.22.1" @@ -79,6 +108,12 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + [[package]] name = "cc" version = "1.4.2" @@ -86,6 +121,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" dependencies = [ "find-msvc-tools", + "jobserver", + "libc", "shlex 2.0.1", ] @@ -104,6 +141,23 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] + [[package]] name = "clang-sys" version = "1.9.1" @@ -115,12 +169,47 @@ dependencies = [ "libloading", ] +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "const-oid" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -188,6 +277,23 @@ dependencies = [ "crypto-common 0.2.2", ] +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + [[package]] name = "either" version = "1.17.0" @@ -207,7 +313,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -236,12 +342,59 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + [[package]] name = "futures-task" version = "0.3.33" @@ -255,7 +408,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] @@ -270,6 +427,19 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -277,8 +447,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", + "rand_core", + "wasm-bindgen", ] [[package]] @@ -299,6 +472,45 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "hybrid-array" version = "0.4.14" @@ -308,6 +520,167 @@ dependencies = [ "typenum", ] +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + [[package]] name = "indexmap" version = "2.14.0" @@ -318,6 +691,12 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + [[package]] name = "itertools" version = "0.13.0" @@ -333,6 +712,65 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.104" @@ -365,9 +803,12 @@ name = "libremetaverse" version = "0.0.1" dependencies = [ "bcdec_rs", + "flate2", + "futures-util", "libremetaverse-imaging", "libremetaverse-structured-data", "libremetaverse-types", + "reqwest", "roxmltree", "tokio", ] @@ -497,7 +938,7 @@ name = "libremetaverse-types" version = "0.0.1" dependencies = [ "base64", - "getrandom", + "getrandom 0.4.3", "md-5", "sha1", "sha2", @@ -538,12 +979,24 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + [[package]] name = "log" version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "md-5" version = "0.10.6" @@ -584,7 +1037,7 @@ checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -603,6 +1056,12 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -621,6 +1080,15 @@ version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + [[package]] name = "prettyplease" version = "0.2.37" @@ -640,6 +1108,63 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "quote" version = "1.0.47" @@ -655,6 +1180,32 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + [[package]] name = "regex" version = "1.13.1" @@ -684,6 +1235,58 @@ version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "roxmltree" version = "0.21.1" @@ -699,6 +1302,15 @@ 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 = "rustix" version = "1.1.4" @@ -709,7 +1321,82 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] @@ -718,6 +1405,53 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "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.229" @@ -810,6 +1544,22 @@ version = "0.3.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "skia-bindings" version = "0.99.0" @@ -857,6 +1607,12 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + [[package]] name = "socket2" version = "0.6.5" @@ -864,15 +1620,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + [[package]] name = "stats_alloc" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c0e04424e733e69714ca1bbb9204c1a57f09f5493439520f9f68c132ad25eec" +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.119" @@ -895,6 +1663,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "tar" version = "0.4.46" @@ -906,18 +1694,64 @@ dependencies = [ "xattr", ] +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ + "bytes", "libc", "mio", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -931,6 +1765,29 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "1.1.4+spec-1.1.0" @@ -970,6 +1827,76 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d56353a2a665ad0f41a421187180aab746c8c325620617ad883a99a1cbe66d2" +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.20.1" @@ -982,13 +1909,37 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "uuid" version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ - "getrandom", + "getrandom 0.4.3", "js-sys", "wasm-bindgen", ] @@ -1005,6 +1956,25 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -1024,6 +1994,16 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.127" @@ -1056,12 +2036,72 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +dependencies = [ + "js-sys", + "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-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -1071,12 +2111,82 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + [[package]] name = "winnow" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + [[package]] name = "xattr" version = "1.6.1" @@ -1087,6 +2197,89 @@ dependencies = [ "rustix", ] +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "zmij" version = "1.0.23" diff --git a/README.md b/README.md index 97e96bf..ce5390f 100644 --- a/README.md +++ b/README.md @@ -282,3 +282,15 @@ snapshotted before invocation, and manager/transport workers retain no dropped client owner. The ownership, dispatch, lifecycle, and fake-server verification contracts are documented in [`docs/network-manager.md`](docs/network-manager.md). +The capability transport is now native Rust as well. `HttpCapsClient` supports +the C# GET/POST/PUT/PATCH/DELETE and LLSD overloads through an injectable +`reqwest`/fake-handler boundary, with HTTPS, bounded redirects, gzip/deflate, +streaming progress, cancellation, explicit request/response/decompression +limits, and rejection of outbound non-HTTP capability URIs. The per-category +oldest-first token buckets retain the reference defaults and cap-name mapping. +The download manager adds a bounded queue and concurrency gate, canonical-URI +deduplication, subscriber progress fanout, shared cancellation, and transient +retry handling while preserving permanent 401/403/404/410 failures. Capability +URLs and tokens are absent from errors and diagnostics. Ownership, limits, +injection, and offline fake-server coverage are documented in +[`docs/caps-http.md`](docs/caps-http.md). diff --git a/api/SHIM-COVERAGE.md b/api/SHIM-COVERAGE.md index dc76d41..2bb51bf 100644 --- a/api/SHIM-COVERAGE.md +++ b/api/SHIM-COVERAGE.md @@ -4,7 +4,7 @@ Generated by `python3 tools/generate_api_shims.py`; do not edit by hand. | Assembly | Types | Members | Status | |---|---:|---:|---| -| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 47 types / 13,615 members; remaining surface is callable failure-only shims | +| `LibreMetaverse` | 2,711 | 27,281 | native implementation: 53 types / 13,673 members; remaining surface is callable failure-only shims | | `LibreMetaverse.Imaging.Abstractions` | 3 | 20 | native implementation: 3 types / 20 members; no generated shims remain | | `LibreMetaverse.Imaging.Skia` | 1 | 3 | native implementation: 1 type / 3 members; no generated shims remain | | `LibreMetaverse.LslTools` | 164 | 768 | callable failure-only shim | diff --git a/crates/libremetaverse-structured-data/src/notation.rs b/crates/libremetaverse-structured-data/src/notation.rs index 4a5dbcc..07fe34a 100644 --- a/crates/libremetaverse-structured-data/src/notation.rs +++ b/crates/libremetaverse-structured-data/src/notation.rs @@ -802,7 +802,7 @@ fn sorted_entries(values: &HashMap) -> Vec<(&String, &OSD)> { } fn decode_base16(value: &str) -> Result, &'static str> { - if value.len() % 2 != 0 { + if !value.len().is_multiple_of(2) { return Err("invalid notation LLSD base16 length"); } value diff --git a/crates/libremetaverse-types/src/compat.rs b/crates/libremetaverse-types/src/compat.rs index 792f9b3..fd6f8c9 100644 --- a/crates/libremetaverse-types/src/compat.rs +++ b/crates/libremetaverse-types/src/compat.rs @@ -356,6 +356,45 @@ impl CancellationToken { } } + /// Registers an executor-neutral callback and removes it when the returned + /// guard is dropped. + #[must_use = "dropping the guard immediately unregisters the callback"] + pub fn register_callback(&self, callback: Arc) -> Subscription { + if self.is_cancellation_requested() { + callback(); + return Subscription::detached(); + } + let id = self.0.next_id.fetch_add(1, Ordering::Relaxed); + self.0 + .callbacks + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .insert(id, callback); + if self.is_cancellation_requested() { + if let Some(callback) = self + .0 + .callbacks + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .remove(&id) + { + callback(); + } + return Subscription::detached(); + } + let state = Arc::downgrade(&self.0); + Subscription::new(move || { + let Some(state) = state.upgrade() else { + return; + }; + state + .callbacks + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .remove(&id); + }) + } + fn register(&self, callback: Arc) -> CancellationRegistration { if self.is_cancellation_requested() { callback(); @@ -528,7 +567,31 @@ pub struct DictionaryKeys(pub Vec, pub PhantomData Self { + Self { acquired: true } + } + + #[must_use] + pub const fn rejected() -> Self { + Self { acquired: false } + } + + #[must_use] + pub const fn is_acquired(self) -> bool { + self.acquired + } +} struct CancellationSourceState { token: CancellationToken, @@ -641,7 +704,160 @@ pub struct Thread; pub struct Task(pub PhantomData T>); -pub struct TaskCompletionSource(pub PhantomData); +struct TaskCompletionState { + next_id: AtomicU64, + result: Mutex>>, + waiters: Mutex>, +} + +/// Cloneable completion source used by mapped task-returning APIs. +#[derive(Clone)] +pub struct TaskCompletionSource(Arc>); + +impl TaskCompletionSource { + #[must_use] + pub fn new() -> Self { + Self(Arc::new(TaskCompletionState { + next_id: AtomicU64::new(1), + result: Mutex::new(None), + waiters: Mutex::new(BTreeMap::new()), + })) + } + + /// Completes the task once and wakes every waiter. + #[must_use] + pub fn try_set_result(&self, value: T) -> bool { + self.try_complete(Ok(value)) + } + + /// Completes the task with a typed failure once. + #[must_use] + pub fn try_set_error(&self, error: crate::Error) -> bool { + self.try_complete(Err(error)) + } + + /// Completes the task with mapped cancellation once. + #[must_use] + pub fn try_set_cancelled(&self) -> bool { + self.try_set_error(crate::Error::Cancelled) + } + + fn try_complete(&self, value: Result) -> bool { + let mut result = self + .0 + .result + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + if result.is_some() { + return false; + } + *result = Some(value); + drop(result); + for (_, waiter) in std::mem::take( + &mut *self + .0 + .waiters + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner), + ) { + waiter.wake(); + } + true + } + + #[must_use] + pub fn future(&self) -> TaskCompletionFuture { + TaskCompletionFuture { + state: Arc::clone(&self.0), + waiter_id: None, + } + } + + #[must_use] + pub fn is_completed(&self) -> bool { + self.0 + .result + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .is_some() + } +} + +impl Default for TaskCompletionSource { + fn default() -> Self { + Self::new() + } +} + +impl fmt::Debug for TaskCompletionSource { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("TaskCompletionSource") + .field("completed", &self.is_completed()) + .finish_non_exhaustive() + } +} + +/// Future associated with a [`TaskCompletionSource`]. +pub struct TaskCompletionFuture { + state: Arc>, + waiter_id: Option, +} + +impl Future for TaskCompletionFuture { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { + if let Some(result) = self + .state + .result + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .clone() + { + return Poll::Ready(result); + } + let id = self.waiter_id.unwrap_or_else(|| { + let id = self.state.next_id.fetch_add(1, Ordering::Relaxed); + self.waiter_id = Some(id); + id + }); + self.state + .waiters + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .insert(id, context.waker().clone()); + let ready = self + .state + .result + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .clone(); + if let Some(result) = ready { + self.state + .waiters + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .remove(&id); + self.waiter_id = None; + Poll::Ready(result) + } else { + Poll::Pending + } + } +} + +impl Drop for TaskCompletionFuture { + fn drop(&mut self) { + if let Some(id) = self.waiter_id { + self.state + .waiters + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .remove(&id); + } + } +} /// Cloneable clock boundary used by runtime-neutral client code. #[derive(Clone)] @@ -741,7 +957,19 @@ pub struct HttpMethod(pub String); pub struct AuthenticationHeaderValue(pub String); -pub trait IProgress: Send + Sync {} +/// Executor-neutral progress sink used by mapped upload/download APIs. +pub trait IProgress: Send + Sync { + fn report(&self, value: T); +} + +impl IProgress for F +where + F: Fn(T) + Send + Sync, +{ + fn report(&self, value: T) { + self(value); + } +} pub struct IEnumerator; diff --git a/crates/libremetaverse/Cargo.toml b/crates/libremetaverse/Cargo.toml index 8b58a7e..e5c9723 100644 --- a/crates/libremetaverse/Cargo.toml +++ b/crates/libremetaverse/Cargo.toml @@ -14,9 +14,12 @@ jpeg2000 = ["libremetaverse-imaging/jpeg2000"] [dependencies] bcdec_rs = { version = "0.2.0", optional = true } +flate2 = "1.1.2" +futures-util = "0.3.31" libremetaverse-imaging = { path = "../libremetaverse-imaging" } libremetaverse-structured-data = { path = "../libremetaverse-structured-data" } libremetaverse-types = { path = "../libremetaverse-types" } +reqwest = { version = "0.13.4", default-features = false, features = ["rustls", "stream"] } roxmltree = "0.21.1" tokio = { version = "1.47.1", features = ["macros", "net", "rt", "sync", "time"] } diff --git a/crates/libremetaverse/src/caps_http.rs b/crates/libremetaverse/src/caps_http.rs new file mode 100644 index 0000000..0a20375 --- /dev/null +++ b/crates/libremetaverse/src/caps_http.rs @@ -0,0 +1,1404 @@ +//! Bounded native HTTP capabilities client and per-category rate limiter. + +#![allow(clippy::missing_errors_doc)] // Public result shapes are fixed by the compatibility map. +#![allow(clippy::needless_pass_by_value)] // Mapped APIs preserve owned CLR argument shapes. +#![allow(clippy::option_option)] // The fixed generated nullable-value mapping is Option>. +#![allow(clippy::too_many_arguments)] // HTTP request boundaries keep explicit policy inputs visible. + +use crate::{CapsCategory, Error}; +use flate2::read::{DeflateDecoder, GzDecoder, ZlibDecoder}; +use futures_util::StreamExt; +use libremetaverse_structured_data::{OSD, OSDFormat, OSDParser}; +use libremetaverse_types::compat::{ + CancellationToken, CancellationTokenSource, HttpMessageHandler, HttpRequest, HttpResponse, + IProgress, MediaTypeHeaderValue, RateLimitLease, TimeProvider, Uri, +}; +use std::collections::{HashMap, VecDeque}; +use std::fmt; +use std::io::Read; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::{Duration, SystemTime}; + +const STREAM_CHUNK_SIZE: usize = 81_920; + +fn mutex(value: &Mutex) -> std::sync::MutexGuard<'_, T> { + value + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + +fn read(value: &RwLock) -> std::sync::RwLockReadGuard<'_, T> { + value + .read() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + +fn write(value: &RwLock) -> std::sync::RwLockWriteGuard<'_, T> { + value + .write() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + +/// Explicit memory and redirect policy for capability HTTP traffic. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CapsHttpLimits { + pub max_request_bytes: usize, + pub max_response_bytes: usize, + pub max_decompressed_bytes: usize, + pub max_redirects: usize, +} + +impl Default for CapsHttpLimits { + fn default() -> Self { + Self { + max_request_bytes: 32 * 1024 * 1024, + max_response_bytes: 64 * 1024 * 1024, + max_decompressed_bytes: 128 * 1024 * 1024, + max_redirects: 10, + } + } +} + +impl CapsHttpLimits { + fn validate(&self) -> Result<(), Error> { + if self.max_request_bytes == 0 + || self.max_response_bytes == 0 + || self.max_decompressed_bytes == 0 + { + return Err(Error::Argument); + } + Ok(()) + } +} + +/// Typed upload/download progress matching the C# nested value type. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct HttpCapsClientProgressReport { + total_bytes: Option>, + bytes_transferred: i64, + percent: Option>, +} + +impl HttpCapsClientProgressReport { + pub fn new( + total_bytes: Option>, + bytes_transferred: i64, + percent: Option>, + ) -> Result { + if bytes_transferred < 0 || total_bytes.flatten().is_some_and(|total| total < 0) { + return Err(Error::Argument); + } + Ok(Self { + total_bytes, + bytes_transferred, + percent, + }) + } + + #[must_use] + pub const fn bytes_transferred(&self) -> i64 { + self.bytes_transferred + } + + #[must_use] + pub const fn percent(&self) -> Option> { + self.percent + } + + #[must_use] + pub const fn total_bytes(&self) -> Option> { + self.total_bytes + } + + #[allow(clippy::cast_precision_loss)] // C# computes the same rounded percentage as double. + fn known(total: usize, transferred: usize) -> Option { + let total = i64::try_from(total).ok()?; + let transferred = i64::try_from(transferred).ok()?; + let percent = if total == 0 { + None + } else { + Some(((transferred as f64 / total as f64) * 10_000.0).round() / 100.0) + }; + Some(Self { + total_bytes: Some(Some(total)), + bytes_transferred: transferred, + percent: Some(percent), + }) + } + + fn unknown(transferred: usize) -> Option { + Some(Self { + total_bytes: Some(None), + bytes_transferred: i64::try_from(transferred).ok()?, + percent: Some(None), + }) + } +} + +/// Mapped token-bucket configuration. +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CapsRateLimiterOptions { + token_limit: i32, + tokens_per_period: i32, + replenishment_period: Duration, + queue_limit: i32, +} + +impl CapsRateLimiterOptions { + pub fn new() -> Result { + Ok(Self { + token_limit: 0, + tokens_per_period: 0, + replenishment_period: Duration::ZERO, + queue_limit: 0, + }) + } + + #[must_use] + pub const fn queue_limit(&self) -> i32 { + self.queue_limit + } + + pub fn set_queue_limit(&mut self, value: i32) { + self.queue_limit = value; + } + + #[must_use] + pub const fn replenishment_period(&self) -> Duration { + self.replenishment_period + } + + pub fn set_replenishment_period(&mut self, value: Duration) { + self.replenishment_period = value; + } + + #[must_use] + pub const fn token_limit(&self) -> i32 { + self.token_limit + } + + pub fn set_token_limit(&mut self, value: i32) { + self.token_limit = value; + } + + #[must_use] + pub const fn tokens_per_period(&self) -> i32 { + self.tokens_per_period + } + + pub fn set_tokens_per_period(&mut self, value: i32) { + self.tokens_per_period = value; + } + + fn configured( + token_limit: i32, + tokens_per_period: i32, + replenishment_period: Duration, + queue_limit: i32, + ) -> Self { + Self { + token_limit, + tokens_per_period, + replenishment_period, + queue_limit, + } + } + + fn validate(&self) -> Result<(), Error> { + if self.token_limit <= 0 + || self.tokens_per_period <= 0 + || self.replenishment_period.is_zero() + || self.queue_limit < 0 + { + return Err(Error::Argument); + } + Ok(()) + } +} + +#[derive(Debug)] +struct BucketState { + tokens: u32, + last_replenishment: SystemTime, + queue: VecDeque, +} + +struct CategoryBucket { + options: CapsRateLimiterOptions, + next_ticket: AtomicU64, + state: Mutex, +} + +impl CategoryBucket { + fn new(options: CapsRateLimiterOptions, now: SystemTime) -> Result { + options.validate()?; + Ok(Self { + state: Mutex::new(BucketState { + tokens: u32::try_from(options.token_limit).map_err(|_| Error::Argument)?, + last_replenishment: now, + queue: VecDeque::new(), + }), + options, + next_ticket: AtomicU64::new(1), + }) + } + + fn replenish(&self, state: &mut BucketState, now: SystemTime) { + let Ok(elapsed) = now.duration_since(state.last_replenishment) else { + state.last_replenishment = now; + return; + }; + let period_nanos = self.options.replenishment_period.as_nanos(); + if period_nanos == 0 { + return; + } + let periods = elapsed.as_nanos() / period_nanos; + if periods == 0 { + return; + } + let added = periods.saturating_mul(u128::from( + u32::try_from(self.options.tokens_per_period).unwrap_or(u32::MAX), + )); + let token_limit = u32::try_from(self.options.token_limit).unwrap_or(u32::MAX); + state.tokens = token_limit.min( + state + .tokens + .saturating_add(u32::try_from(added).unwrap_or(u32::MAX)), + ); + let periods_u32 = u32::try_from(periods).unwrap_or(u32::MAX); + state.last_replenishment += self.options.replenishment_period * periods_u32; + } + + fn remove_ticket(&self, ticket: u64) { + mutex(&self.state).queue.retain(|queued| *queued != ticket); + } +} + +struct CapsRateLimiterInner { + buckets: HashMap>, + uri_categories: RwLock>, + clock: TimeProvider, + shutdown: CancellationTokenSource, + disposed: AtomicBool, +} + +/// Per-capability-category token buckets with a bounded oldest-first queue. +#[derive(Clone)] +pub struct CapsRateLimiter(Arc); + +impl CapsRateLimiter { + pub fn new_with_constructor() -> Result { + Self::new_with_clock_and_overrides(TimeProvider::system(), None) + } + + pub fn new_with_i_read_only_dictionary( + overrides: Option>, + ) -> Result { + Self::new_with_clock_and_overrides(TimeProvider::system(), overrides) + } + + pub fn new_with_clock_and_overrides( + clock: TimeProvider, + overrides: Option>, + ) -> Result { + let mut options = default_rate_options(); + if let Some(overrides) = overrides { + options.extend(overrides); + } + let fallback = options + .get(&CapsCategory::Default) + .cloned() + .ok_or(Error::InvalidOperation)?; + let now = clock.get_utc_now(); + let mut buckets = HashMap::new(); + for category in all_categories() { + let configured = options + .get(&category) + .cloned() + .unwrap_or_else(|| fallback.clone()); + buckets.insert(category, Arc::new(CategoryBucket::new(configured, now)?)); + } + Ok(Self(Arc::new(CapsRateLimiterInner { + buckets, + uri_categories: RwLock::new(HashMap::new()), + clock, + shutdown: CancellationTokenSource::new(), + disposed: AtomicBool::new(false), + }))) + } + + pub async fn acquire( + &self, + uri: Uri, + cancellation_token: Option, + ) -> Result { + let cancellation_token = cancellation_token.unwrap_or_default(); + cancellation_token.throw_if_cancellation_requested()?; + if self.0.disposed.load(Ordering::Acquire) { + return Err(Error::InvalidOperation); + } + let key = parse_http_uri(&uri)?.to_string(); + let category = read(&self.0.uri_categories) + .get(&key) + .copied() + .unwrap_or(CapsCategory::Default); + let bucket = self + .0 + .buckets + .get(&category) + .cloned() + .ok_or(Error::InvalidOperation)?; + let mut ticket = None; + + loop { + if cancellation_token.is_cancellation_requested() { + if let Some(ticket) = ticket { + bucket.remove_ticket(ticket); + } + return Err(Error::Cancelled); + } + if self.0.disposed.load(Ordering::Acquire) { + if let Some(ticket) = ticket { + bucket.remove_ticket(ticket); + } + return Err(Error::InvalidOperation); + } + + let now = self.0.clock.get_utc_now(); + let wait = { + let mut state = mutex(&bucket.state); + bucket.replenish(&mut state, now); + let is_front = ticket.map_or_else( + || state.queue.is_empty(), + |ticket| state.queue.front() == Some(&ticket), + ); + if is_front && state.tokens > 0 { + state.tokens -= 1; + if ticket.is_some() { + state.queue.pop_front(); + } + return Ok(RateLimitLease::acquired()); + } + if ticket.is_none() { + let queue_limit = + usize::try_from(bucket.options.queue_limit).map_err(|_| Error::Argument)?; + if state.queue.len() >= queue_limit { + return Ok(RateLimitLease::rejected()); + } + let next = bucket.next_ticket.fetch_add(1, Ordering::Relaxed); + state.queue.push_back(next); + ticket = Some(next); + } + let elapsed = now + .duration_since(state.last_replenishment) + .unwrap_or_default(); + bucket + .options + .replenishment_period + .saturating_sub(elapsed) + .max(Duration::from_millis(1)) + }; + if let Err(error) = + wait_executor_neutral(wait, cancellation_token.clone(), self.0.shutdown.token()) + .await + { + if let Some(ticket) = ticket { + bucket.remove_ticket(ticket); + } + return Err(error); + } + } + } + + pub fn register_cap_uri(&self, cap_name: String, uri: Uri) -> Result<(), Error> { + if self.0.disposed.load(Ordering::Acquire) { + return Err(Error::InvalidOperation); + } + let category = category_for_cap_name(&cap_name); + write(&self.0.uri_categories).insert(parse_http_uri(&uri)?.to_string(), category); + Ok(()) + } + + pub fn dispose(&self) -> Result<(), Error> { + if self.0.disposed.swap(true, Ordering::AcqRel) { + return Ok(()); + } + self.0.shutdown.cancel(); + write(&self.0.uri_categories).clear(); + for bucket in self.0.buckets.values() { + mutex(&bucket.state).queue.clear(); + } + Ok(()) + } +} + +impl fmt::Debug for CapsRateLimiter { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("CapsRateLimiter") + .field("categories", &self.0.buckets.len()) + .field("registered_uri_count", &read(&self.0.uri_categories).len()) + .field("disposed", &self.0.disposed.load(Ordering::Acquire)) + .finish() + } +} + +async fn wait_executor_neutral( + duration: Duration, + cancellation_token: CancellationToken, + shutdown: CancellationToken, +) -> Result<(), Error> { + tokio::select! { + () = tokio::time::sleep(duration) => Ok(()), + () = cancellation_token.cancelled() => Err(Error::Cancelled), + () = shutdown.cancelled() => Err(Error::InvalidOperation), + } +} + +fn all_categories() -> [CapsCategory; 8] { + [ + CapsCategory::Default, + CapsCategory::RenderMaterials, + CapsCategory::AssetFetch, + CapsCategory::AssetUpload, + CapsCategory::Inventory, + CapsCategory::EventQueue, + CapsCategory::DisplayName, + CapsCategory::Voice, + ] +} + +fn default_rate_options() -> HashMap { + let second = Duration::from_secs(1); + HashMap::from([ + ( + CapsCategory::Default, + CapsRateLimiterOptions::configured(20, 10, second, 30), + ), + ( + CapsCategory::RenderMaterials, + CapsRateLimiterOptions::configured(4, 2, second, 20), + ), + ( + CapsCategory::AssetFetch, + CapsRateLimiterOptions::configured(24, 12, second, 60), + ), + ( + CapsCategory::AssetUpload, + CapsRateLimiterOptions::configured(4, 2, second, 10), + ), + ( + CapsCategory::Inventory, + CapsRateLimiterOptions::configured(6, 3, second, 20), + ), + ( + CapsCategory::EventQueue, + CapsRateLimiterOptions::configured(3, 2, second, 3), + ), + ( + CapsCategory::DisplayName, + CapsRateLimiterOptions::configured(5, 2, second, 15), + ), + ( + CapsCategory::Voice, + CapsRateLimiterOptions::configured(10, 5, second, 20), + ), + ]) +} + +fn category_for_cap_name(name: &str) -> CapsCategory { + match name.to_ascii_lowercase().as_str() { + "rendermaterials" | "modifymaterialparams" | "modifyregion" => { + CapsCategory::RenderMaterials + } + "gettexture" + | "viewerasset" + | "getmesh" + | "getmesh2" + | "getmetadata" + | "requesttexturedownload" => CapsCategory::AssetFetch, + "newfileagentinventory" + | "newfileagentinventoryvariableprice" + | "uploadbakedtexture" + | "updateavatarappearance" + | "inventorythumbnailupload" + | "updatematerialagentinventory" + | "updatematerialtaskinventory" => CapsCategory::AssetUpload, + "fetchinventory2" + | "fetchinventorydescendents2" + | "fetchlib2" + | "fetchlibdescendents2" + | "inventoryapiv3" + | "libraryapiv3" + | "requesttaskinventory" => CapsCategory::Inventory, + "eventqueueget" => CapsCategory::EventQueue, + "getdisplaynames" | "setdisplayname" | "avatarpickersearch" | "agentprofile" => { + CapsCategory::DisplayName + } + "provisionvoiceaccountrequest" | "voicesignalingrequest" | "parcelvoiceinforequest" => { + CapsCategory::Voice + } + _ => CapsCategory::Default, + } +} + +enum HttpBackend { + Handler(HttpMessageHandler), + Reqwest(reqwest::Client), +} + +struct HttpCapsClientInner { + backend: HttpBackend, + limits: CapsHttpLimits, + rate_limiter: RwLock>, + request_slots: Arc, + shutdown: CancellationTokenSource, + disposed: AtomicBool, +} + +/// Native capability HTTP client with injectable fake and reqwest backends. +#[derive(Clone)] +pub struct HttpCapsClient(Arc); + +impl HttpCapsClient { + pub const LLSD_XML: &'static str = "application/llsd+xml"; + pub const LLSD_BINARY: &'static str = "application/llsd+binary"; + pub const LLSD_JSON: &'static str = "application/llsd+json"; + + #[must_use] + pub fn hdr_llsd_xml() -> MediaTypeHeaderValue { + MediaTypeHeaderValue(Self::LLSD_XML.to_owned()) + } + + #[must_use] + pub fn hdr_llsd_binary() -> MediaTypeHeaderValue { + MediaTypeHeaderValue(Self::LLSD_BINARY.to_owned()) + } + + #[must_use] + pub fn hdr_llsd_json() -> MediaTypeHeaderValue { + MediaTypeHeaderValue(Self::LLSD_JSON.to_owned()) + } + + pub fn new(handler: HttpMessageHandler) -> Result { + Self::with_handler_and_limits(handler, CapsHttpLimits::default()) + } + + pub fn with_handler_and_limits( + handler: HttpMessageHandler, + limits: CapsHttpLimits, + ) -> Result { + Self::with_handler_policy(handler, None, limits, 32) + } + + /// Creates a fully injected fake/backend client with explicit limiter and + /// concurrency policy. This is the deterministic test and embedding seam. + pub fn with_handler_policy( + handler: HttpMessageHandler, + rate_limiter: Option, + limits: CapsHttpLimits, + max_concurrent_requests: usize, + ) -> Result { + limits.validate()?; + if max_concurrent_requests == 0 { + return Err(Error::Argument); + } + Ok(Self(Arc::new(HttpCapsClientInner { + backend: HttpBackend::Handler(handler), + limits, + rate_limiter: RwLock::new(rate_limiter), + request_slots: Arc::new(tokio::sync::Semaphore::new(max_concurrent_requests)), + shutdown: CancellationTokenSource::new(), + disposed: AtomicBool::new(false), + }))) + } + + pub fn with_reqwest_client( + client: reqwest::Client, + rate_limiter: Option, + limits: CapsHttpLimits, + ) -> Result { + Self::with_reqwest_client_and_concurrency(client, rate_limiter, limits, 32) + } + + fn with_reqwest_client_and_concurrency( + client: reqwest::Client, + rate_limiter: Option, + limits: CapsHttpLimits, + max_connections: usize, + ) -> Result { + limits.validate()?; + if max_connections == 0 { + return Err(Error::Argument); + } + Ok(Self(Arc::new(HttpCapsClientInner { + backend: HttpBackend::Reqwest(client), + limits, + rate_limiter: RwLock::new(rate_limiter), + request_slots: Arc::new(tokio::sync::Semaphore::new(max_connections)), + shutdown: CancellationTokenSource::new(), + disposed: AtomicBool::new(false), + }))) + } + + pub(crate) fn production( + user_agent: &str, + timeout: Duration, + max_connections: usize, + rate_limiter: CapsRateLimiter, + ) -> Result { + if max_connections == 0 { + return Err(Error::Argument); + } + let limits = CapsHttpLimits::default(); + let max_redirects = limits.max_redirects; + let redirect = reqwest::redirect::Policy::custom(move |attempt| { + if attempt.previous().len() > max_redirects { + return attempt.stop(); + } + match attempt.url().scheme() { + "http" | "https" => attempt.follow(), + _ => attempt.stop(), + } + }); + let client = reqwest::Client::builder() + .redirect(redirect) + .timeout(timeout) + .connect_timeout(timeout) + .pool_max_idle_per_host(max_connections) + .user_agent(user_agent) + .build() + .map_err(|_| Error::HttpRequest)?; + Self::with_reqwest_client_and_concurrency( + client, + Some(rate_limiter), + limits, + max_connections, + ) + } + + pub(crate) fn set_rate_limiter(&self, rate_limiter: Option) { + *write(&self.0.rate_limiter) = rate_limiter; + } + + pub(crate) fn shutdown(&self) { + if !self.0.disposed.swap(true, Ordering::AcqRel) { + self.0.shutdown.cancel(); + self.0.request_slots.close(); + } + } + + pub async fn get( + &self, + uri: Uri, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(HttpResponse, Vec), Error> { + self.send("GET", uri, None, Vec::new(), cancellation_token, progress) + .await + } + + pub async fn get_request( + &self, + uri: Uri, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(), Error> { + self.get(uri, cancellation_token, progress) + .await + .map(|_| ()) + } + + pub async fn post_with_uri_string_bytes_cancellation_token_i_progress( + &self, + uri: Uri, + content_type: String, + payload: Vec, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(HttpResponse, Vec), Error> { + self.send( + "POST", + uri, + Some(content_type), + payload, + cancellation_token, + progress, + ) + .await + } + + pub async fn put_with_uri_string_bytes_cancellation_token_i_progress( + &self, + uri: Uri, + content_type: String, + payload: Vec, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(HttpResponse, Vec), Error> { + self.send( + "PUT", + uri, + Some(content_type), + payload, + cancellation_token, + progress, + ) + .await + } + + pub async fn patch_with_uri_string_bytes_cancellation_token_i_progress( + &self, + uri: Uri, + content_type: String, + payload: Vec, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(HttpResponse, Vec), Error> { + self.send( + "PATCH", + uri, + Some(content_type), + payload, + cancellation_token, + progress, + ) + .await + } + + pub async fn delete_with_uri_string_bytes_cancellation_token_i_progress( + &self, + uri: Uri, + content_type: String, + payload: Vec, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(HttpResponse, Vec), Error> { + self.send( + "DELETE", + uri, + Some(content_type), + payload, + cancellation_token, + progress, + ) + .await + } + + pub async fn post_with_uri_osd_format_osd_cancellation_token_i_progress( + &self, + uri: Uri, + format: OSDFormat, + payload: OSD, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(HttpResponse, Vec), Error> { + let (content_type, payload) = serialize_osd(format, payload)?; + self.send( + "POST", + uri, + Some(content_type), + payload, + cancellation_token, + progress, + ) + .await + } + + pub async fn put_with_uri_osd_format_osd_cancellation_token_i_progress( + &self, + uri: Uri, + format: OSDFormat, + payload: OSD, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(HttpResponse, Vec), Error> { + let (content_type, payload) = serialize_osd(format, payload)?; + self.send( + "PUT", + uri, + Some(content_type), + payload, + cancellation_token, + progress, + ) + .await + } + + pub async fn patch_with_uri_osd_format_osd_cancellation_token_i_progress( + &self, + uri: Uri, + format: OSDFormat, + payload: OSD, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(HttpResponse, Vec), Error> { + let (content_type, payload) = serialize_osd(format, payload)?; + self.send( + "PATCH", + uri, + Some(content_type), + payload, + cancellation_token, + progress, + ) + .await + } + + pub async fn delete_with_uri_osd_format_osd_cancellation_token_i_progress( + &self, + uri: Uri, + format: OSDFormat, + payload: OSD, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(HttpResponse, Vec), Error> { + let (content_type, payload) = serialize_osd(format, payload)?; + self.send( + "DELETE", + uri, + Some(content_type), + payload, + cancellation_token, + progress, + ) + .await + } + + pub async fn post_request_with_uri_string_bytes_cancellation_token_i_progress( + &self, + uri: Uri, + content_type: String, + payload: Vec, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(), Error> { + self.post_with_uri_string_bytes_cancellation_token_i_progress( + uri, + content_type, + payload, + cancellation_token, + progress, + ) + .await + .map(|_| ()) + } + + pub async fn put_request_with_uri_string_bytes_cancellation_token_i_progress( + &self, + uri: Uri, + content_type: String, + payload: Vec, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(), Error> { + self.put_with_uri_string_bytes_cancellation_token_i_progress( + uri, + content_type, + payload, + cancellation_token, + progress, + ) + .await + .map(|_| ()) + } + + pub async fn patch_request_with_uri_string_bytes_cancellation_token_i_progress( + &self, + uri: Uri, + content_type: String, + payload: Vec, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(), Error> { + self.patch_with_uri_string_bytes_cancellation_token_i_progress( + uri, + content_type, + payload, + cancellation_token, + progress, + ) + .await + .map(|_| ()) + } + + pub async fn delete_request_with_uri_string_bytes_cancellation_token_i_progress( + &self, + uri: Uri, + content_type: String, + payload: Vec, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(), Error> { + self.delete_with_uri_string_bytes_cancellation_token_i_progress( + uri, + content_type, + payload, + cancellation_token, + progress, + ) + .await + .map(|_| ()) + } + + pub async fn post_request_with_uri_osd_format_osd_cancellation_token_i_progress( + &self, + uri: Uri, + format: OSDFormat, + payload: OSD, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(), Error> { + self.post_with_uri_osd_format_osd_cancellation_token_i_progress( + uri, + format, + payload, + cancellation_token, + progress, + ) + .await + .map(|_| ()) + } + + pub async fn put_request_with_uri_osd_format_osd_cancellation_token_i_progress( + &self, + uri: Uri, + format: OSDFormat, + payload: OSD, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(), Error> { + self.put_with_uri_osd_format_osd_cancellation_token_i_progress( + uri, + format, + payload, + cancellation_token, + progress, + ) + .await + .map(|_| ()) + } + + pub async fn patch_request_with_uri_osd_format_osd_cancellation_token_i_progress( + &self, + uri: Uri, + format: OSDFormat, + payload: OSD, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(), Error> { + self.patch_with_uri_osd_format_osd_cancellation_token_i_progress( + uri, + format, + payload, + cancellation_token, + progress, + ) + .await + .map(|_| ()) + } + + pub async fn delete_request_with_uri_osd_format_osd_cancellation_token_i_progress( + &self, + uri: Uri, + format: OSDFormat, + payload: OSD, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(), Error> { + self.delete_with_uri_osd_format_osd_cancellation_token_i_progress( + uri, + format, + payload, + cancellation_token, + progress, + ) + .await + .map(|_| ()) + } + + async fn send( + &self, + method: &str, + uri: Uri, + content_type: Option, + payload: Vec, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(HttpResponse, Vec), Error> { + if self.0.disposed.load(Ordering::Acquire) { + return Err(Error::InvalidOperation); + } + cancellation_token.throw_if_cancellation_requested()?; + let operation_cancellation = + CancellationTokenSource::new_linked(&[cancellation_token, self.0.shutdown.token()]); + let cancellation_token = operation_cancellation.token(); + if payload.len() > self.0.limits.max_request_bytes { + return Err(Error::HttpRequest); + } + let parsed = parse_http_uri(&uri)?; + let limiter = { read(&self.0.rate_limiter).clone() }; + if let Some(limiter) = limiter { + let _ = limiter + .acquire(uri.clone(), Some(cancellation_token.clone())) + .await?; + } + let request_slot = Arc::clone(&self.0.request_slots); + let _permit = tokio::select! { + permit = request_slot.acquire_owned() => permit.map_err(|_| Error::InvalidOperation)?, + () = cancellation_token.cancelled() => return Err(Error::Cancelled), + }; + let progress: Option>> = + progress.map(Arc::from); + let result = match &self.0.backend { + HttpBackend::Handler(handler) => { + self.send_handler( + handler, + method, + uri, + content_type, + payload, + cancellation_token, + progress, + ) + .await + } + HttpBackend::Reqwest(client) => { + self.send_reqwest( + client, + method, + parsed, + content_type, + payload, + cancellation_token, + progress, + ) + .await + } + }; + drop(operation_cancellation); + result + } + + async fn send_handler( + &self, + handler: &HttpMessageHandler, + method: &str, + uri: Uri, + content_type: Option, + payload: Vec, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(HttpResponse, Vec), Error> { + report_buffer_progress(progress.as_ref(), payload.len()); + let request = HttpRequest { + method: method.to_owned(), + uri, + headers: std::collections::BTreeMap::new(), + content_type, + body: payload, + }; + let response = tokio::select! { + response = handler.send(request, cancellation_token.clone()) => response, + () = cancellation_token.cancelled() => return Err(Error::Cancelled), + }; + cancellation_token.throw_if_cancellation_requested()?; + self.finish_response(response, progress.as_ref(), true) + } + + async fn send_reqwest( + &self, + client: &reqwest::Client, + method: &str, + uri: reqwest::Url, + content_type: Option, + payload: Vec, + cancellation_token: CancellationToken, + progress: Option>>, + ) -> Result<(HttpResponse, Vec), Error> { + let method = reqwest::Method::from_bytes(method.as_bytes()).map_err(|_| Error::Argument)?; + let mut request = client.request(method, uri); + if let Some(content_type) = content_type { + request = request.header(reqwest::header::CONTENT_TYPE, content_type); + } + request = request.header(reqwest::header::ACCEPT_ENCODING, "gzip, deflate"); + if !payload.is_empty() { + request = request.header(reqwest::header::CONTENT_LENGTH, payload.len()); + let stream = upload_stream(payload, progress.clone()); + request = request.body(reqwest::Body::wrap_stream(stream)); + } + let response = tokio::select! { + response = request.send() => response.map_err(|_| Error::HttpRequest)?, + () = cancellation_token.cancelled() => return Err(Error::Cancelled), + }; + cancellation_token.throw_if_cancellation_requested()?; + let status_code = response.status().as_u16(); + let headers = response + .headers() + .iter() + .map(|(name, value)| { + ( + name.as_str().to_owned(), + String::from_utf8_lossy(value.as_bytes()).into_owned(), + ) + }) + .collect(); + let content_type = response + .headers() + .get(reqwest::header::CONTENT_TYPE) + .map(|value| String::from_utf8_lossy(value.as_bytes()).into_owned()); + let declared_length = response + .headers() + .get(reqwest::header::CONTENT_LENGTH) + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.parse::().ok()); + if declared_length.is_some_and(|length| length > self.0.limits.max_response_bytes) { + return Err(Error::HttpRequest); + } + let mut stream = response.bytes_stream(); + let mut body = Vec::new(); + while let Some(chunk) = tokio::select! { + chunk = stream.next() => chunk, + () = cancellation_token.cancelled() => return Err(Error::Cancelled), + } { + let chunk = chunk.map_err(|_| Error::HttpRequest)?; + let new_length = body + .len() + .checked_add(chunk.len()) + .ok_or(Error::HttpRequest)?; + if new_length > self.0.limits.max_response_bytes { + return Err(Error::HttpRequest); + } + body.extend_from_slice(&chunk); + report_stream_progress(progress.as_ref(), declared_length, body.len()); + } + if body.is_empty() { + report_stream_progress(progress.as_ref(), declared_length, 0); + } + cancellation_token.throw_if_cancellation_requested()?; + self.finish_response( + HttpResponse { + status_code, + headers, + content_type, + body, + }, + progress.as_ref(), + false, + ) + } + + fn finish_response( + &self, + mut response: HttpResponse, + progress: Option<&Arc>>, + report_progress: bool, + ) -> Result<(HttpResponse, Vec), Error> { + if response.body.len() > self.0.limits.max_response_bytes { + return Err(Error::HttpRequest); + } + if let Some(length) = header_value(&response.headers, "content-length") + .and_then(|value| value.parse::().ok()) + && length > self.0.limits.max_response_bytes + { + return Err(Error::HttpRequest); + } + let encoding = header_value(&response.headers, "content-encoding"); + let encoded = std::mem::take(&mut response.body); + let body = decode_content(encoded, encoding, self.0.limits.max_decompressed_bytes)?; + if report_progress { + report_download_progress( + progress, + &body, + header_value(&response.headers, "content-length"), + ); + } + response.body.clone_from(&body); + Ok((response, body)) + } +} + +impl fmt::Debug for HttpCapsClient { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + let backend = match &self.0.backend { + HttpBackend::Handler(_) => "injected-handler", + HttpBackend::Reqwest(_) => "reqwest", + }; + formatter + .debug_struct("HttpCapsClient") + .field("backend", &backend) + .field("limits", &self.0.limits) + .field("disposed", &self.0.disposed.load(Ordering::Acquire)) + .finish() + } +} + +fn parse_http_uri(uri: &Uri) -> Result { + let parsed = reqwest::Url::parse(&uri.0).map_err(|_| Error::Argument)?; + if !matches!(parsed.scheme(), "http" | "https") || parsed.host().is_none() { + return Err(Error::Argument); + } + Ok(parsed) +} + +fn serialize_osd(format: OSDFormat, payload: OSD) -> Result<(String, Vec), Error> { + match format { + OSDFormat::Xml => Ok(( + HttpCapsClient::LLSD_XML.to_owned(), + OSDParser::serialize_llsd_xml_bytes(payload)?, + )), + OSDFormat::Binary => Ok(( + HttpCapsClient::LLSD_BINARY.to_owned(), + OSDParser::serialize_llsd_binary_with_osd(payload)?, + )), + OSDFormat::Json => Ok(( + HttpCapsClient::LLSD_JSON.to_owned(), + OSDParser::serialize_json_string(payload, None)?.into_bytes(), + )), + } +} + +fn upload_stream( + payload: Vec, + progress: Option>>, +) -> impl futures_util::Stream, std::io::Error>> + Send + 'static { + let payload = Arc::new(payload); + futures_util::stream::unfold( + (payload, 0_usize, progress), + |(payload, offset, progress)| async move { + if offset >= payload.len() { + return None; + } + let end = offset.saturating_add(STREAM_CHUNK_SIZE).min(payload.len()); + if let (Some(progress), Some(report)) = ( + progress.as_ref(), + HttpCapsClientProgressReport::known(payload.len(), end), + ) { + progress.report(report); + } + Some((Ok(payload[offset..end].to_vec()), (payload, end, progress))) + }, + ) +} + +fn report_buffer_progress( + progress: Option<&Arc>>, + length: usize, +) { + let Some(progress) = progress else { + return; + }; + if length == 0 { + if let Some(report) = HttpCapsClientProgressReport::known(0, 0) { + progress.report(report); + } + return; + } + for transferred in (STREAM_CHUNK_SIZE..length) + .step_by(STREAM_CHUNK_SIZE) + .chain(std::iter::once(length)) + { + if let Some(report) = HttpCapsClientProgressReport::known(length, transferred) { + progress.report(report); + } + } +} + +fn report_download_progress( + progress: Option<&Arc>>, + body: &[u8], + declared_length: Option<&str>, +) { + let Some(progress) = progress else { + return; + }; + let total = declared_length + .and_then(|value| value.parse::().ok()) + .filter(|total| *total == body.len()); + if body.is_empty() { + let report = total.and_then(|total| HttpCapsClientProgressReport::known(total, 0)); + if let Some(report) = report.or_else(|| HttpCapsClientProgressReport::unknown(0)) { + progress.report(report); + } + return; + } + for transferred in (STREAM_CHUNK_SIZE..body.len()) + .step_by(STREAM_CHUNK_SIZE) + .chain(std::iter::once(body.len())) + { + let report = total + .and_then(|total| HttpCapsClientProgressReport::known(total, transferred)) + .or_else(|| HttpCapsClientProgressReport::unknown(transferred)); + if let Some(report) = report { + progress.report(report); + } + } +} + +fn report_stream_progress( + progress: Option<&Arc>>, + total: Option, + transferred: usize, +) { + let Some(progress) = progress else { + return; + }; + let report = total + .and_then(|total| HttpCapsClientProgressReport::known(total, transferred)) + .or_else(|| HttpCapsClientProgressReport::unknown(transferred)); + if let Some(report) = report { + progress.report(report); + } +} + +fn header_value<'a>( + headers: &'a std::collections::BTreeMap, + name: &str, +) -> Option<&'a str> { + headers + .iter() + .find_map(|(key, value)| key.eq_ignore_ascii_case(name).then_some(value.as_str())) +} + +fn decode_content( + mut body: Vec, + encoding: Option<&str>, + limit: usize, +) -> Result, Error> { + let Some(encoding) = encoding else { + return Ok(body); + }; + let encodings: Vec<_> = encoding + .split(',') + .map(str::trim) + .filter(|encoding| !encoding.is_empty() && !encoding.eq_ignore_ascii_case("identity")) + .collect(); + for encoding in encodings.into_iter().rev() { + body = if encoding.eq_ignore_ascii_case("gzip") || encoding.eq_ignore_ascii_case("x-gzip") { + read_bounded(GzDecoder::new(body.as_slice()), limit)? + } else if encoding.eq_ignore_ascii_case("deflate") { + match read_bounded(ZlibDecoder::new(body.as_slice()), limit) { + Ok(decoded) => decoded, + Err(_) => read_bounded(DeflateDecoder::new(body.as_slice()), limit)?, + } + } else { + return Err(Error::HttpRequest); + }; + } + Ok(body) +} + +fn read_bounded(mut reader: impl Read, limit: usize) -> Result, Error> { + let take_limit = u64::try_from(limit).unwrap_or(u64::MAX).saturating_add(1); + let mut output = Vec::new(); + reader + .by_ref() + .take(take_limit) + .read_to_end(&mut output) + .map_err(|_| Error::HttpRequest)?; + if output.len() > limit { + return Err(Error::HttpRequest); + } + Ok(output) +} diff --git a/crates/libremetaverse/src/client_core.rs b/crates/libremetaverse/src/client_core.rs index 507e6f7..6d539fd 100644 --- a/crates/libremetaverse/src/client_core.rs +++ b/crates/libremetaverse/src/client_core.rs @@ -114,12 +114,18 @@ struct ClientRuntime { cancellation: CancellationTokenSource, services: Mutex>, agent_throttle_sender: Mutex>>, + http_caps_client: Mutex, + caps_rate_limiter: Mutex, shutdown_complete: Condvar, shutdown_wait: Mutex<()>, } impl ClientRuntime { - fn new(services: Vec>) -> Self { + fn new( + services: Vec>, + http_caps_client: crate::caps_http::HttpCapsClient, + caps_rate_limiter: crate::caps_http::CapsRateLimiter, + ) -> Self { Self { state: AtomicU8::new(ClientLifecycleState::Active as u8), cancellation: CancellationTokenSource::new(), @@ -134,6 +140,8 @@ impl ClientRuntime { .collect(), ), agent_throttle_sender: Mutex::new(None), + http_caps_client: Mutex::new(http_caps_client), + caps_rate_limiter: Mutex::new(caps_rate_limiter), shutdown_complete: Condvar::new(), shutdown_wait: Mutex::new(()), } @@ -206,6 +214,15 @@ impl ClientRuntime { .lock() .unwrap_or_else(std::sync::PoisonError::into_inner) .take(); + self.http_caps_client + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .shutdown(); + let _ = self + .caps_rate_limiter + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .dispose(); let shutdown_wait = self .shutdown_wait .lock() @@ -309,6 +326,43 @@ impl GridClient { .clone() } + pub(crate) fn native_http_caps_client(&self) -> crate::caps_http::HttpCapsClient { + self.runtime + .http_caps_client + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .clone() + } + + pub(crate) fn native_set_http_caps_client(&mut self, value: crate::caps_http::HttpCapsClient) { + *self + .runtime + .http_caps_client + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) = value; + } + + pub(crate) fn native_caps_rate_limiter(&self) -> crate::caps_http::CapsRateLimiter { + self.runtime + .caps_rate_limiter + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .clone() + } + + pub(crate) fn native_set_caps_rate_limiter( + &mut self, + value: crate::caps_http::CapsRateLimiter, + ) { + self.native_http_caps_client() + .set_rate_limiter(Some(value.clone())); + *self + .runtime + .caps_rate_limiter + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) = value; + } + pub(crate) fn shutdown(&self) -> Result<(), ClientCoreError> { self.runtime.shutdown() } @@ -348,6 +402,10 @@ impl Drop for GridClient { } impl crate::IGridClient for GridClient { + fn http_caps_client(&self) -> crate::caps_http::HttpCapsClient { + self.native_http_caps_client() + } + fn settings(&self) -> Settings { self.settings.clone() } @@ -404,10 +462,44 @@ impl GridClientBuilder { /// Returns the first invalid configuration field. pub fn build(self) -> Result { self.settings.validate()?; + let caps_rate_limiter = crate::caps_http::CapsRateLimiter::new_with_clock_and_overrides( + self.time_provider.clone(), + None, + ) + .map_err(|_| ClientCoreError::InvalidConfiguration { + field: "CapsRateLimiter", + reason: "could not construct the configured token buckets", + })?; + let max_connections = usize::try_from(Settings::max_http_connections()).map_err(|_| { + ClientCoreError::InvalidConfiguration { + field: "Settings.MaxHttpConnections", + reason: "must be positive", + } + })?; + let caps_timeout = u64::try_from(self.settings.timing.caps_timeout).map_err(|_| { + ClientCoreError::InvalidConfiguration { + field: "Timing.CapsTimeout", + reason: "must be positive", + } + })?; + let http_caps_client = crate::caps_http::HttpCapsClient::production( + &Settings::user_agent(), + std::time::Duration::from_millis(caps_timeout), + max_connections, + caps_rate_limiter.clone(), + ) + .map_err(|_| ClientCoreError::InvalidConfiguration { + field: "HttpCapsClient", + reason: "could not construct the HTTP transport", + })?; Ok(GridClient { settings: self.settings, time_provider: self.time_provider, - runtime: Arc::new(ClientRuntime::new(self.services)), + runtime: Arc::new(ClientRuntime::new( + self.services, + http_caps_client, + caps_rate_limiter, + )), }) } } diff --git a/crates/libremetaverse/src/download_manager.rs b/crates/libremetaverse/src/download_manager.rs new file mode 100644 index 0000000..1877bdb --- /dev/null +++ b/crates/libremetaverse/src/download_manager.rs @@ -0,0 +1,609 @@ +//! Bounded, deduplicating HTTP download dispatcher. + +#![allow(clippy::missing_errors_doc)] // Public result shapes are fixed by the compatibility map. +#![allow(clippy::needless_pass_by_value)] // Mapped APIs preserve owned CLR argument shapes. + +use crate::{Error, GridClient, HttpCapsClientProgressReport}; +use libremetaverse_types::compat::{ + CancellationToken, CancellationTokenSource, HttpResponse, IProgress, Subscription, + TaskCompletionSource, Uri, +}; +use std::collections::HashMap; +use std::fmt; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex, Weak}; +use std::thread::{self, JoinHandle}; +use std::time::Duration; +use tokio::sync::{Notify, mpsc, oneshot}; +use tokio::task::JoinSet; + +const DEFAULT_PARALLEL_DOWNLOADS: usize = 8; +const MAX_PARALLEL_DOWNLOADS: usize = 32; +const DOWNLOAD_QUEUE_CAPACITY: usize = 256; + +type DownloadValue = (HttpResponse, Vec); +type DownloadCompletion = TaskCompletionSource; +type DownloadResult = Result; + +fn mutex(value: &Mutex) -> std::sync::MutexGuard<'_, T> { + value + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + +/// Mapped request state with the same defaults as the C# constructor. +pub struct DownloadRequest { + pub address: Uri, + pub attempt: i32, + pub cancellation_token: CancellationToken, + pub completion_tcs: Option, + pub content_type: Option, + pub download_progress_callback: Option>>, + pub retries: i32, +} + +impl DownloadRequest { + pub fn new( + address: Uri, + content_type: Option, + download_progress_callback: Option>>, + ) -> Result { + validate_http_uri(&address)?; + Ok(Self { + address, + attempt: 0, + cancellation_token: CancellationToken::default(), + completion_tcs: None, + content_type, + download_progress_callback, + retries: 5, + }) + } +} + +impl fmt::Debug for DownloadRequest { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("DownloadRequest") + .field("address", &"") + .field("attempt", &self.attempt) + .field( + "cancellation_requested", + &self.cancellation_token.is_cancellation_requested(), + ) + .field("has_completion", &self.completion_tcs.is_some()) + .field("has_content_type", &self.content_type.is_some()) + .field("has_progress", &self.download_progress_callback.is_some()) + .field("retries", &self.retries) + .finish() + } +} + +struct ActiveDownload { + cancellation: CancellationTokenSource, + cancellation_guards: Mutex>, + progress: Mutex>>>, + completion_sources: Mutex>, + waiters: Mutex>>, + completed: AtomicBool, +} + +impl ActiveDownload { + fn new() -> Self { + Self { + cancellation: CancellationTokenSource::new(), + cancellation_guards: Mutex::new(Vec::new()), + progress: Mutex::new(Vec::new()), + completion_sources: Mutex::new(Vec::new()), + waiters: Mutex::new(Vec::new()), + completed: AtomicBool::new(false), + } + } + + fn attach_cancellation(&self, token: &CancellationToken) { + let cancellation = self.cancellation.clone(); + let guard = token.register_callback(Arc::new(move || cancellation.cancel())); + mutex(&self.cancellation_guards).push(guard); + } + + fn attach_progress(&self, progress: Option>>) { + if let Some(progress) = progress { + mutex(&self.progress).push(Arc::from(progress)); + } + } + + fn add_waiter(&self) -> oneshot::Receiver { + let (sender, receiver) = oneshot::channel(); + mutex(&self.waiters).push(sender); + receiver + } + + fn attach_completion_source(&self, completion_source: Option) { + if let Some(completion_source) = completion_source { + mutex(&self.completion_sources).push(completion_source); + } + } + + fn complete(&self, result: DownloadResult) { + if self.completed.swap(true, Ordering::AcqRel) { + return; + } + for waiter in std::mem::take(&mut *mutex(&self.waiters)) { + let _ = waiter.send(result.clone()); + } + for completion_source in std::mem::take(&mut *mutex(&self.completion_sources)) { + match result.clone() { + Ok(value) => { + let _ = completion_source.try_set_result(value); + } + Err(Error::Cancelled) => { + let _ = completion_source.try_set_cancelled(); + } + Err(error) => { + let _ = completion_source.try_set_error(error); + } + } + } + mutex(&self.progress).clear(); + mutex(&self.cancellation_guards).clear(); + } + + fn report(&self, report: HttpCapsClientProgressReport) { + let handlers = mutex(&self.progress).clone(); + for handler in handlers { + let _ = catch_unwind(AssertUnwindSafe(|| handler.report(report))); + } + } +} + +struct ProgressFanout(Weak); + +impl IProgress for ProgressFanout { + fn report(&self, report: HttpCapsClientProgressReport) { + if let Some(active) = self.0.upgrade() { + active.report(report); + } + } +} + +struct DownloadJob { + key: String, + address: Uri, + attempt: i32, + retries: i32, + active: Arc, + client: GridClient, +} + +struct GateState { + active: usize, +} + +struct DynamicGate { + limit: AtomicUsize, + state: Mutex, + notify: Notify, +} + +impl DynamicGate { + fn new(limit: usize) -> Self { + Self { + limit: AtomicUsize::new(limit), + state: Mutex::new(GateState { active: 0 }), + notify: Notify::new(), + } + } + + fn set_limit(&self, value: usize) { + self.limit.store(value, Ordering::Release); + self.notify.notify_waiters(); + } + + async fn acquire( + self: &Arc, + cancellation: CancellationToken, + ) -> Result { + loop { + cancellation.throw_if_cancellation_requested()?; + let notified = self.notify.notified(); + { + let mut state = mutex(&self.state); + if state.active < self.limit.load(Ordering::Acquire) { + state.active += 1; + return Ok(GatePermit(Arc::clone(self))); + } + } + tokio::select! { + () = notified => {} + () = cancellation.cancelled() => return Err(Error::Cancelled), + } + } + } +} + +struct GatePermit(Arc); + +impl Drop for GatePermit { + fn drop(&mut self) { + let mut state = mutex(&self.0.state); + state.active = state.active.saturating_sub(1); + drop(state); + self.0.notify.notify_one(); + } +} + +struct DownloadManagerInner { + client: GridClient, + active: Mutex>>, + sender: Mutex>>, + dispatcher: Mutex>>, + shutdown: CancellationTokenSource, + gate: Arc, + parallel_downloads: AtomicUsize, + disposed: AtomicBool, +} + +impl DownloadManagerInner { + fn remove_active(&self, key: &str, active: &Arc) { + let mut downloads = mutex(&self.active); + if downloads + .get(key) + .is_some_and(|candidate| Arc::ptr_eq(candidate, active)) + { + downloads.remove(key); + } + } + + fn shutdown(&self) { + if self.disposed.swap(true, Ordering::AcqRel) { + return; + } + self.shutdown.cancel(); + for active in mutex(&self.active).values() { + active.cancellation.cancel(); + } + mutex(&self.sender).take(); + if let Some(dispatcher) = mutex(&self.dispatcher).take() + && dispatcher.thread().id() != thread::current().id() + { + let _ = dispatcher.join(); + } + let remaining = std::mem::take(&mut *mutex(&self.active)); + for active in remaining.into_values() { + active.complete(Err(Error::Cancelled)); + } + } +} + +impl Drop for DownloadManagerInner { + fn drop(&mut self) { + self.shutdown(); + } +} + +/// Native, bounded download manager with URI-level request deduplication. +#[derive(Clone)] +pub struct DownloadManager(Arc); + +impl DownloadManager { + pub fn new(client: GridClient) -> Result { + let (sender, receiver) = mpsc::channel(DOWNLOAD_QUEUE_CAPACITY); + let gate = Arc::new(DynamicGate::new(DEFAULT_PARALLEL_DOWNLOADS)); + let runtime = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .map_err(|_| Error::InvalidOperation)?; + let inner = Arc::new(DownloadManagerInner { + client, + active: Mutex::new(HashMap::new()), + sender: Mutex::new(Some(sender)), + dispatcher: Mutex::new(None), + shutdown: CancellationTokenSource::new(), + gate: Arc::clone(&gate), + parallel_downloads: AtomicUsize::new(DEFAULT_PARALLEL_DOWNLOADS), + disposed: AtomicBool::new(false), + }); + let weak = Arc::downgrade(&inner); + let shutdown = inner.shutdown.token(); + let dispatcher = thread::Builder::new() + .name("libremetaverse-download-dispatcher".to_owned()) + .spawn(move || download_dispatcher(runtime, receiver, weak, gate, shutdown)) + .map_err(|_| Error::InvalidOperation)?; + *mutex(&inner.dispatcher) = Some(dispatcher); + Ok(Self(inner)) + } + + pub fn dispose(&self) -> Result<(), Error> { + self.0.shutdown(); + Ok(()) + } + + #[must_use] + pub fn parallel_downloads(&self) -> i32 { + i32::try_from(self.0.parallel_downloads.load(Ordering::Acquire)).unwrap_or(i32::MAX) + } + + pub fn set_parallel_downloads(&mut self, value: i32) { + let value = usize::try_from(value) + .unwrap_or(1) + .clamp(1, MAX_PARALLEL_DOWNLOADS); + self.0.parallel_downloads.store(value, Ordering::Release); + self.0.gate.set_limit(value); + } + + pub fn queue_download_with_download_request( + &self, + request: DownloadRequest, + ) -> Result<(), Error> { + self.enqueue(request, false).map(|_| ()) + } + + pub fn queue_download_with_download_request_cancellation_token( + &self, + mut request: DownloadRequest, + cancellation_token: CancellationToken, + ) -> Result<(), Error> { + request.cancellation_token = cancellation_token; + self.queue_download_with_download_request(request) + } + + pub async fn queue_download_with_download_request_ba727387( + &self, + request: DownloadRequest, + ) -> DownloadResult { + let cancellation = request.cancellation_token.clone(); + let (active, receiver) = self.enqueue(request, true)?; + await_download( + active, + receiver.ok_or(Error::InvalidOperation)?, + cancellation, + ) + .await + } + + pub async fn queue_download_with_uri_string_i_progress_cancellation_token_int32( + &self, + address: Uri, + content_type: Option, + progress_callback: Option>>, + cancellation_token: Option, + retries: Option, + ) -> DownloadResult { + let cancellation = cancellation_token.unwrap_or_default(); + let mut request = DownloadRequest::new(address, content_type, progress_callback)?; + request.cancellation_token = cancellation.clone(); + request.retries = retries.unwrap_or(5).max(0); + let (active, receiver) = self.enqueue(request, true)?; + await_download( + active, + receiver.ok_or(Error::InvalidOperation)?, + cancellation, + ) + .await + } + + pub async fn download_with_uri_string_i_progress_cancellation_token( + &self, + address: Uri, + content_type: String, + progress: Box>, + cancellation_token: CancellationToken, + ) -> DownloadResult { + self.queue_download_with_uri_string_i_progress_cancellation_token_int32( + address, + Some(content_type), + Some(progress), + Some(cancellation_token), + Some(5), + ) + .await + } + + pub async fn download_with_uri_i_progress_cancellation_token( + &self, + address: Uri, + progress: Box>, + cancellation_token: CancellationToken, + ) -> DownloadResult { + self.download_with_uri_string_i_progress_cancellation_token( + address, + String::new(), + progress, + cancellation_token, + ) + .await + } + + fn enqueue( + &self, + mut request: DownloadRequest, + with_waiter: bool, + ) -> Result< + ( + Arc, + Option>, + ), + Error, + > { + if self.0.disposed.load(Ordering::Acquire) { + return Err(Error::InvalidOperation); + } + let key = validate_http_uri(&request.address)?; + let selected = { + let mut active = mutex(&self.0.active); + if let Some(existing) = active.get(&key) { + Some((Arc::clone(existing), false)) + } else if request.cancellation_token.is_cancellation_requested() { + None + } else { + let download = Arc::new(ActiveDownload::new()); + active.insert(key.clone(), Arc::clone(&download)); + Some((download, true)) + } + }; + let Some((active, is_new)) = selected else { + if let Some(completion_source) = request.completion_tcs.take() { + let _ = completion_source.try_set_cancelled(); + } + return Err(Error::Cancelled); + }; + active.attach_cancellation(&request.cancellation_token); + active.attach_progress(request.download_progress_callback.take()); + active.attach_completion_source(request.completion_tcs.take()); + let receiver = with_waiter.then(|| active.add_waiter()); + if !is_new { + return Ok((active, receiver)); + } + + let job = DownloadJob { + key: key.clone(), + address: request.address, + attempt: request.attempt.max(0), + retries: request.retries.max(0), + active: Arc::clone(&active), + client: self.0.client.clone(), + }; + let sender = mutex(&self.0.sender) + .clone() + .ok_or(Error::InvalidOperation)?; + if sender.try_send(job).is_err() { + self.0.remove_active(&key, &active); + active.complete(Err(Error::InvalidOperation)); + return Err(Error::InvalidOperation); + } + Ok((active, receiver)) + } +} + +impl fmt::Debug for DownloadManager { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("DownloadManager") + .field("active_downloads", &mutex(&self.0.active).len()) + .field("parallel_downloads", &self.parallel_downloads()) + .field("queue_capacity", &DOWNLOAD_QUEUE_CAPACITY) + .field("disposed", &self.0.disposed.load(Ordering::Acquire)) + .finish() + } +} + +async fn await_download( + active: Arc, + receiver: oneshot::Receiver, + cancellation: CancellationToken, +) -> DownloadResult { + tokio::select! { + result = receiver => result.map_err(|_| Error::Cancelled)?, + () = cancellation.cancelled() => { + active.cancellation.cancel(); + Err(Error::Cancelled) + } + } +} + +fn download_dispatcher( + runtime: tokio::runtime::Runtime, + mut receiver: mpsc::Receiver, + manager: Weak, + gate: Arc, + shutdown: CancellationToken, +) { + runtime.block_on(async move { + let mut jobs = JoinSet::new(); + loop { + tokio::select! { + biased; + () = shutdown.cancelled() => break, + Some(job) = receiver.recv() => { + let manager = manager.clone(); + let gate = Arc::clone(&gate); + jobs.spawn(async move { + run_download_job(job, manager, gate).await; + }); + } + Some(_) = jobs.join_next(), if !jobs.is_empty() => {} + else => break, + } + } + while let Ok(job) = receiver.try_recv() { + job.active.complete(Err(Error::Cancelled)); + if let Some(manager) = manager.upgrade() { + manager.remove_active(&job.key, &job.active); + } + } + while jobs.join_next().await.is_some() {} + }); +} + +async fn run_download_job( + job: DownloadJob, + manager: Weak, + gate: Arc, +) { + let cancellation = job.active.cancellation.token(); + let permit = gate.acquire(cancellation.clone()).await; + let result = match permit { + Ok(_permit) => perform_download(&job, cancellation).await, + Err(error) => Err(error), + }; + if let Some(manager) = manager.upgrade() { + manager.remove_active(&job.key, &job.active); + } + job.active.complete(result); +} + +async fn perform_download(job: &DownloadJob, cancellation: CancellationToken) -> DownloadResult { + let mut attempt = job.attempt; + loop { + cancellation.throw_if_cancellation_requested()?; + let progress: Box> = + Box::new(ProgressFanout(Arc::downgrade(&job.active))); + let result = job + .client + .native_http_caps_client() + .get(job.address.clone(), cancellation.clone(), Some(progress)) + .await; + match result { + Ok((response, data)) if response.is_success_status_code() => { + return Ok((response, data)); + } + Ok((response, _)) => { + if is_permanent_status(response.status_code) || attempt >= job.retries { + return Err(Error::HttpRequest); + } + } + Err(Error::Cancelled) => return Err(Error::Cancelled), + Err(error) if attempt >= job.retries => return Err(error), + Err(_) => {} + } + attempt = attempt.saturating_add(1); + let base_delay = u64::try_from(200_i32.saturating_mul(attempt).min(2_000)).unwrap_or(2_000); + let delay = + Duration::from_millis(base_delay.saturating_add(retry_jitter(&job.key, attempt))); + tokio::select! { + () = tokio::time::sleep(delay) => {} + () = cancellation.cancelled() => return Err(Error::Cancelled), + } + } +} + +fn is_permanent_status(status: u16) -> bool { + matches!(status, 401 | 403 | 404 | 410) +} + +fn retry_jitter(key: &str, attempt: i32) -> u64 { + let hash = key.bytes().fold( + 0xcbf2_9ce4_8422_2325_u64 ^ u64::try_from(attempt).unwrap_or_default(), + |hash, byte| hash.wrapping_mul(0x0000_0100_0000_01b3) ^ u64::from(byte), + ); + hash % 200 +} + +fn validate_http_uri(uri: &Uri) -> Result { + let parsed = reqwest::Url::parse(&uri.0).map_err(|_| Error::Argument)?; + if !matches!(parsed.scheme(), "http" | "https") || parsed.host().is_none() { + return Err(Error::Argument); + } + Ok(parsed.to_string()) +} diff --git a/crates/libremetaverse/src/generated.rs b/crates/libremetaverse/src/generated.rs index 773109b..8c3b712 100644 --- a/crates/libremetaverse/src/generated.rs +++ b/crates/libremetaverse/src/generated.rs @@ -6900,107 +6900,20 @@ pub enum CapsCategory { pub use crate::network_manager::CapsEventDictionary; /// C# type: `T:LibreMetaverse.CapsRateLimiter`. -pub struct CapsRateLimiter; -impl CapsRateLimiter { - /// C# member: `M:LibreMetaverse.CapsRateLimiter.#ctor`. - pub fn new_with_constructor() -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.CapsRateLimiter.#ctor") - } - /// C# member: `M:LibreMetaverse.CapsRateLimiter.#ctor(System.Collections.Generic.IReadOnlyDictionary{LibreMetaverse.CapsCategory,LibreMetaverse.CapsRateLimiterOptions})`. - pub fn new_with_i_read_only_dictionary( - overrides: Option< - std::collections::HashMap< - libremetaverse::CapsCategory, - libremetaverse::CapsRateLimiterOptions, - >, - >, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.CapsRateLimiter.#ctor(System.Collections.Generic.IReadOnlyDictionary{LibreMetaverse.CapsCategory,LibreMetaverse.CapsRateLimiterOptions})", - ) - } - /// C# member: `M:LibreMetaverse.CapsRateLimiter.AcquireAsync(System.Uri,System.Threading.CancellationToken)`. - pub async fn acquire( - &self, - uri: libremetaverse_types::compat::Uri, - cancellation_token: Option, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.CapsRateLimiter.AcquireAsync(System.Uri,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.CapsRateLimiter.Dispose`. - pub fn dispose(&self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented("M:LibreMetaverse.CapsRateLimiter.Dispose") - } - /// C# member: `M:LibreMetaverse.CapsRateLimiter.RegisterCapUri(System.String,System.Uri)`. - pub fn register_cap_uri( - &self, - cap_name: String, - uri: libremetaverse_types::compat::Uri, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.CapsRateLimiter.RegisterCapUri(System.String,System.Uri)", - ) - } -} +/// C# member: `M:LibreMetaverse.CapsRateLimiter.#ctor`. +/// C# member: `M:LibreMetaverse.CapsRateLimiter.#ctor(System.Collections.Generic.IReadOnlyDictionary{LibreMetaverse.CapsCategory,LibreMetaverse.CapsRateLimiterOptions})`. +/// C# member: `M:LibreMetaverse.CapsRateLimiter.AcquireAsync(System.Uri,System.Threading.CancellationToken)`. +/// C# member: `M:LibreMetaverse.CapsRateLimiter.Dispose`. +/// C# member: `M:LibreMetaverse.CapsRateLimiter.RegisterCapUri(System.String,System.Uri)`. +pub use crate::caps_http::CapsRateLimiter; /// C# type: `T:LibreMetaverse.CapsRateLimiterOptions`. -pub struct CapsRateLimiterOptions; -impl CapsRateLimiterOptions { - /// C# member: `M:LibreMetaverse.CapsRateLimiterOptions.#ctor`. - pub fn new() -> Result { - libremetaverse_types::not_implemented("M:LibreMetaverse.CapsRateLimiterOptions.#ctor") - } - /// C# member: `P:LibreMetaverse.CapsRateLimiterOptions.QueueLimit`. - pub fn queue_limit(&self) -> i32 { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.CapsRateLimiterOptions.QueueLimit" - ) - } - /// Setter for C# member: `P:LibreMetaverse.CapsRateLimiterOptions.QueueLimit`. - pub fn set_queue_limit(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.CapsRateLimiterOptions.QueueLimit" - ) - } - /// C# member: `P:LibreMetaverse.CapsRateLimiterOptions.ReplenishmentPeriod`. - pub fn replenishment_period(&self) -> std::time::Duration { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.CapsRateLimiterOptions.ReplenishmentPeriod" - ) - } - /// Setter for C# member: `P:LibreMetaverse.CapsRateLimiterOptions.ReplenishmentPeriod`. - pub fn set_replenishment_period(&mut self, value: std::time::Duration) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.CapsRateLimiterOptions.ReplenishmentPeriod" - ) - } - /// C# member: `P:LibreMetaverse.CapsRateLimiterOptions.TokenLimit`. - pub fn token_limit(&self) -> i32 { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.CapsRateLimiterOptions.TokenLimit" - ) - } - /// Setter for C# member: `P:LibreMetaverse.CapsRateLimiterOptions.TokenLimit`. - pub fn set_token_limit(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.CapsRateLimiterOptions.TokenLimit" - ) - } - /// C# member: `P:LibreMetaverse.CapsRateLimiterOptions.TokensPerPeriod`. - pub fn tokens_per_period(&self) -> i32 { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.CapsRateLimiterOptions.TokensPerPeriod" - ) - } - /// Setter for C# member: `P:LibreMetaverse.CapsRateLimiterOptions.TokensPerPeriod`. - pub fn set_tokens_per_period(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.CapsRateLimiterOptions.TokensPerPeriod" - ) - } -} +/// C# member: `M:LibreMetaverse.CapsRateLimiterOptions.#ctor`. +/// C# member: `P:LibreMetaverse.CapsRateLimiterOptions.QueueLimit`. +/// C# member: `P:LibreMetaverse.CapsRateLimiterOptions.ReplenishmentPeriod`. +/// C# member: `P:LibreMetaverse.CapsRateLimiterOptions.TokenLimit`. +/// C# member: `P:LibreMetaverse.CapsRateLimiterOptions.TokensPerPeriod`. +pub use crate::caps_http::CapsRateLimiterOptions; /// C# type: `T:LibreMetaverse.ChannelType`. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -10839,11 +10752,13 @@ impl GridClient { } /// C# member: `P:LibreMetaverse.GridClient.CapsRateLimiter`. pub fn caps_rate_limiter(&self) -> libremetaverse::CapsRateLimiter { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.GridClient.CapsRateLimiter") + /* native client-core implementation */ + self.native_caps_rate_limiter() } /// Setter for C# member: `P:LibreMetaverse.GridClient.CapsRateLimiter`. pub fn set_caps_rate_limiter(&mut self, value: libremetaverse::CapsRateLimiter) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.GridClient.CapsRateLimiter") + /* native client-core implementation */ + self.native_set_caps_rate_limiter(value) } /// C# member: `P:LibreMetaverse.GridClient.Directory`. pub fn directory(&self) -> libremetaverse::DirectoryManager { @@ -10895,11 +10810,13 @@ impl GridClient { } /// C# member: `P:LibreMetaverse.GridClient.HttpCapsClient`. pub fn http_caps_client(&self) -> libremetaverse::HttpCapsClient { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.GridClient.HttpCapsClient") + /* native client-core implementation */ + self.native_http_caps_client() } /// Setter for C# member: `P:LibreMetaverse.GridClient.HttpCapsClient`. pub fn set_http_caps_client(&mut self, value: libremetaverse::HttpCapsClient) { - libremetaverse_types::unimplemented_api!("P:LibreMetaverse.GridClient.HttpCapsClient") + /* native client-core implementation */ + self.native_set_http_caps_client(value) } /// C# member: `P:LibreMetaverse.GridClient.InterestList`. pub fn interest_list(&self) -> libremetaverse::InterestListManager { @@ -12830,406 +12747,39 @@ impl HomeInfo { } /// C# type: `T:LibreMetaverse.HttpCapsClient`. -pub struct HttpCapsClient; -impl HttpCapsClient { - /// C# member: `F:LibreMetaverse.HttpCapsClient.HDR_LLSD_BINARY`. - pub fn hdr_llsd_binary() -> libremetaverse_types::compat::MediaTypeHeaderValue { - libremetaverse_types::unimplemented_api!("F:LibreMetaverse.HttpCapsClient.HDR_LLSD_BINARY") - } - /// C# member: `F:LibreMetaverse.HttpCapsClient.HDR_LLSD_JSON`. - pub fn hdr_llsd_json() -> libremetaverse_types::compat::MediaTypeHeaderValue { - libremetaverse_types::unimplemented_api!("F:LibreMetaverse.HttpCapsClient.HDR_LLSD_JSON") - } - /// C# member: `F:LibreMetaverse.HttpCapsClient.HDR_LLSD_XML`. - pub fn hdr_llsd_xml() -> libremetaverse_types::compat::MediaTypeHeaderValue { - libremetaverse_types::unimplemented_api!("F:LibreMetaverse.HttpCapsClient.HDR_LLSD_XML") - } - /// C# member: `F:LibreMetaverse.HttpCapsClient.LLSD_BINARY`. - pub const LLSD_BINARY: &'static str = "application/llsd+binary"; - /// C# member: `F:LibreMetaverse.HttpCapsClient.LLSD_JSON`. - pub const LLSD_JSON: &'static str = "application/llsd+json"; - /// C# member: `F:LibreMetaverse.HttpCapsClient.LLSD_XML`. - pub const LLSD_XML: &'static str = "application/llsd+xml"; - /// C# member: `M:LibreMetaverse.HttpCapsClient.#ctor(System.Net.Http.HttpMessageHandler)`. - pub fn new( - handler: libremetaverse_types::compat::HttpMessageHandler, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.#ctor(System.Net.Http.HttpMessageHandler)", - ) - } - /// C# member: `M:LibreMetaverse.HttpCapsClient.DeleteAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub async fn delete_with_uri_osd_format_osd_cancellation_token_i_progress( - &self, - uri: libremetaverse_types::compat::Uri, - format: libremetaverse_structured_data::OSDFormat, - payload: libremetaverse_structured_data::OSD, - cancellation_token: libremetaverse_types::compat::CancellationToken, - progress: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result<(libremetaverse_types::compat::HttpResponse, Vec), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.DeleteAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } - /// C# member: `M:LibreMetaverse.HttpCapsClient.DeleteAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub async fn delete_with_uri_string_bytes_cancellation_token_i_progress( - &self, - uri: libremetaverse_types::compat::Uri, - content_type: String, - payload: Vec, - cancellation_token: libremetaverse_types::compat::CancellationToken, - progress: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result<(libremetaverse_types::compat::HttpResponse, Vec), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.DeleteAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } - /// C# member: `M:LibreMetaverse.HttpCapsClient.DeleteRequestAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub async fn delete_request_with_uri_osd_format_osd_cancellation_token_i_progress( - &self, - uri: libremetaverse_types::compat::Uri, - format: libremetaverse_structured_data::OSDFormat, - payload: libremetaverse_structured_data::OSD, - cancellation_token: libremetaverse_types::compat::CancellationToken, - progress: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.DeleteRequestAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } - /// C# member: `M:LibreMetaverse.HttpCapsClient.DeleteRequestAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub async fn delete_request_with_uri_string_bytes_cancellation_token_i_progress( - &self, - uri: libremetaverse_types::compat::Uri, - content_type: String, - payload: Vec, - cancellation_token: libremetaverse_types::compat::CancellationToken, - progress: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.DeleteRequestAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } - /// C# member: `M:LibreMetaverse.HttpCapsClient.GetAsync(System.Uri,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub async fn get( - &self, - uri: libremetaverse_types::compat::Uri, - cancellation_token: libremetaverse_types::compat::CancellationToken, - progress: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result<(libremetaverse_types::compat::HttpResponse, Vec), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.GetAsync(System.Uri,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } - /// C# member: `M:LibreMetaverse.HttpCapsClient.GetRequestAsync(System.Uri,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub async fn get_request( - &self, - uri: libremetaverse_types::compat::Uri, - cancellation_token: libremetaverse_types::compat::CancellationToken, - progress: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.GetRequestAsync(System.Uri,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } - /// C# member: `M:LibreMetaverse.HttpCapsClient.PatchAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub async fn patch_with_uri_osd_format_osd_cancellation_token_i_progress( - &self, - uri: libremetaverse_types::compat::Uri, - format: libremetaverse_structured_data::OSDFormat, - payload: libremetaverse_structured_data::OSD, - cancellation_token: libremetaverse_types::compat::CancellationToken, - progress: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result<(libremetaverse_types::compat::HttpResponse, Vec), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.PatchAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } - /// C# member: `M:LibreMetaverse.HttpCapsClient.PatchAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub async fn patch_with_uri_string_bytes_cancellation_token_i_progress( - &self, - uri: libremetaverse_types::compat::Uri, - content_type: String, - payload: Vec, - cancellation_token: libremetaverse_types::compat::CancellationToken, - progress: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result<(libremetaverse_types::compat::HttpResponse, Vec), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.PatchAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } - /// C# member: `M:LibreMetaverse.HttpCapsClient.PatchRequestAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub async fn patch_request_with_uri_osd_format_osd_cancellation_token_i_progress( - &self, - uri: libremetaverse_types::compat::Uri, - format: libremetaverse_structured_data::OSDFormat, - payload: libremetaverse_structured_data::OSD, - cancellation_token: libremetaverse_types::compat::CancellationToken, - progress: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.PatchRequestAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } - /// C# member: `M:LibreMetaverse.HttpCapsClient.PatchRequestAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub async fn patch_request_with_uri_string_bytes_cancellation_token_i_progress( - &self, - uri: libremetaverse_types::compat::Uri, - content_type: String, - payload: Vec, - cancellation_token: libremetaverse_types::compat::CancellationToken, - progress: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.PatchRequestAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } - /// C# member: `M:LibreMetaverse.HttpCapsClient.PostAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub async fn post_with_uri_osd_format_osd_cancellation_token_i_progress( - &self, - uri: libremetaverse_types::compat::Uri, - format: libremetaverse_structured_data::OSDFormat, - payload: libremetaverse_structured_data::OSD, - cancellation_token: libremetaverse_types::compat::CancellationToken, - progress: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result<(libremetaverse_types::compat::HttpResponse, Vec), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.PostAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } - /// C# member: `M:LibreMetaverse.HttpCapsClient.PostAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub async fn post_with_uri_string_bytes_cancellation_token_i_progress( - &self, - uri: libremetaverse_types::compat::Uri, - content_type: String, - payload: Vec, - cancellation_token: libremetaverse_types::compat::CancellationToken, - progress: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result<(libremetaverse_types::compat::HttpResponse, Vec), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.PostAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } - /// C# member: `M:LibreMetaverse.HttpCapsClient.PostRequestAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub async fn post_request_with_uri_osd_format_osd_cancellation_token_i_progress( - &self, - uri: libremetaverse_types::compat::Uri, - format: libremetaverse_structured_data::OSDFormat, - payload: libremetaverse_structured_data::OSD, - cancellation_token: libremetaverse_types::compat::CancellationToken, - progress: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.PostRequestAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } - /// C# member: `M:LibreMetaverse.HttpCapsClient.PostRequestAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub async fn post_request_with_uri_string_bytes_cancellation_token_i_progress( - &self, - uri: libremetaverse_types::compat::Uri, - content_type: String, - payload: Vec, - cancellation_token: libremetaverse_types::compat::CancellationToken, - progress: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.PostRequestAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } - /// C# member: `M:LibreMetaverse.HttpCapsClient.PutAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub async fn put_with_uri_osd_format_osd_cancellation_token_i_progress( - &self, - uri: libremetaverse_types::compat::Uri, - format: libremetaverse_structured_data::OSDFormat, - payload: libremetaverse_structured_data::OSD, - cancellation_token: libremetaverse_types::compat::CancellationToken, - progress: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result<(libremetaverse_types::compat::HttpResponse, Vec), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.PutAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } - /// C# member: `M:LibreMetaverse.HttpCapsClient.PutAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub async fn put_with_uri_string_bytes_cancellation_token_i_progress( - &self, - uri: libremetaverse_types::compat::Uri, - content_type: String, - payload: Vec, - cancellation_token: libremetaverse_types::compat::CancellationToken, - progress: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result<(libremetaverse_types::compat::HttpResponse, Vec), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.PutAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } - /// C# member: `M:LibreMetaverse.HttpCapsClient.PutRequestAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub async fn put_request_with_uri_osd_format_osd_cancellation_token_i_progress( - &self, - uri: libremetaverse_types::compat::Uri, - format: libremetaverse_structured_data::OSDFormat, - payload: libremetaverse_structured_data::OSD, - cancellation_token: libremetaverse_types::compat::CancellationToken, - progress: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.PutRequestAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } - /// C# member: `M:LibreMetaverse.HttpCapsClient.PutRequestAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub async fn put_request_with_uri_string_bytes_cancellation_token_i_progress( - &self, - uri: libremetaverse_types::compat::Uri, - content_type: String, - payload: Vec, - cancellation_token: libremetaverse_types::compat::CancellationToken, - progress: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.PutRequestAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } -} +/// C# member: `F:LibreMetaverse.HttpCapsClient.HDR_LLSD_BINARY`. +/// C# member: `F:LibreMetaverse.HttpCapsClient.HDR_LLSD_JSON`. +/// C# member: `F:LibreMetaverse.HttpCapsClient.HDR_LLSD_XML`. +/// C# member: `F:LibreMetaverse.HttpCapsClient.LLSD_BINARY`. +/// C# member: `F:LibreMetaverse.HttpCapsClient.LLSD_JSON`. +/// C# member: `F:LibreMetaverse.HttpCapsClient.LLSD_XML`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.#ctor(System.Net.Http.HttpMessageHandler)`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.DeleteAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.DeleteAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.DeleteRequestAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.DeleteRequestAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.GetAsync(System.Uri,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.GetRequestAsync(System.Uri,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.PatchAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.PatchAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.PatchRequestAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.PatchRequestAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.PostAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.PostAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.PostRequestAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.PostRequestAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.PutAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.PutAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.PutRequestAsync(System.Uri,LibreMetaverse.StructuredData.OSDFormat,LibreMetaverse.StructuredData.OSD,System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. +/// C# member: `M:LibreMetaverse.HttpCapsClient.PutRequestAsync(System.Uri,System.String,System.Byte[],System.Threading.CancellationToken,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. +pub use crate::caps_http::HttpCapsClient; /// C# type: `T:LibreMetaverse.HttpCapsClient.ProgressReport`. -pub struct HttpCapsClientProgressReport; -impl HttpCapsClientProgressReport { - /// C# member: `M:LibreMetaverse.HttpCapsClient.ProgressReport.#ctor(System.Nullable{System.Int64},System.Int64,System.Nullable{System.Double})`. - pub fn new( - total_bytes: Option>, - bytes_transferred: i64, - percent: Option>, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.HttpCapsClient.ProgressReport.#ctor(System.Nullable{System.Int64},System.Int64,System.Nullable{System.Double})", - ) - } - /// C# member: `P:LibreMetaverse.HttpCapsClient.ProgressReport.BytesTransferred`. - pub fn bytes_transferred(&self) -> i64 { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.HttpCapsClient.ProgressReport.BytesTransferred" - ) - } - /// C# member: `P:LibreMetaverse.HttpCapsClient.ProgressReport.Percent`. - pub fn percent(&self) -> Option> { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.HttpCapsClient.ProgressReport.Percent" - ) - } - /// C# member: `P:LibreMetaverse.HttpCapsClient.ProgressReport.TotalBytes`. - pub fn total_bytes(&self) -> Option> { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.HttpCapsClient.ProgressReport.TotalBytes" - ) - } -} +/// C# member: `M:LibreMetaverse.HttpCapsClient.ProgressReport.#ctor(System.Nullable{System.Int64},System.Int64,System.Nullable{System.Double})`. +/// C# member: `P:LibreMetaverse.HttpCapsClient.ProgressReport.BytesTransferred`. +/// C# member: `P:LibreMetaverse.HttpCapsClient.ProgressReport.Percent`. +/// C# member: `P:LibreMetaverse.HttpCapsClient.ProgressReport.TotalBytes`. +pub use crate::caps_http::HttpCapsClientProgressReport; /// C# type: `T:LibreMetaverse.IBakingTextureProvider`. pub trait IBakingTextureProvider: std::any::Any { @@ -32665,156 +32215,27 @@ pub mod formatters { pub mod http { /// C# type: `T:LibreMetaverse.Http.DownloadManager`. - pub struct DownloadManager; - impl DownloadManager { - /// C# member: `M:LibreMetaverse.Http.DownloadManager.#ctor(LibreMetaverse.GridClient)`. - pub fn new(client: libremetaverse::GridClient) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Http.DownloadManager.#ctor(LibreMetaverse.GridClient)", - ) - } - /// C# member: `M:LibreMetaverse.Http.DownloadManager.Dispose`. - pub fn dispose(&self) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented("M:LibreMetaverse.Http.DownloadManager.Dispose") - } - /// C# member: `M:LibreMetaverse.Http.DownloadManager.DownloadAsync(System.Uri,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport},System.Threading.CancellationToken)`. - pub async fn download_with_uri_i_progress_cancellation_token( - &self, - address: libremetaverse_types::compat::Uri, - progress: Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(libremetaverse_types::compat::HttpResponse, Vec), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Http.DownloadManager.DownloadAsync(System.Uri,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport},System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.Http.DownloadManager.DownloadAsync(System.Uri,System.String,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport},System.Threading.CancellationToken)`. - pub async fn download_with_uri_string_i_progress_cancellation_token( - &self, - address: libremetaverse_types::compat::Uri, - content_type: String, - progress: Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(libremetaverse_types::compat::HttpResponse, Vec), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Http.DownloadManager.DownloadAsync(System.Uri,System.String,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport},System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.Http.DownloadManager.QueueDownload(LibreMetaverse.Http.DownloadRequest)`. - pub fn queue_download_with_download_request( - &self, - req: libremetaverse::http::DownloadRequest, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Http.DownloadManager.QueueDownload(LibreMetaverse.Http.DownloadRequest)", - ) - } - /// C# member: `M:LibreMetaverse.Http.DownloadManager.QueueDownload(LibreMetaverse.Http.DownloadRequest,System.Threading.CancellationToken)`. - pub fn queue_download_with_download_request_cancellation_token( - &self, - req: libremetaverse::http::DownloadRequest, - cancellation_token: libremetaverse_types::compat::CancellationToken, - ) -> Result<(), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Http.DownloadManager.QueueDownload(LibreMetaverse.Http.DownloadRequest,System.Threading.CancellationToken)", - ) - } - /// C# member: `M:LibreMetaverse.Http.DownloadManager.QueueDownloadAsync(LibreMetaverse.Http.DownloadRequest)`. - pub async fn queue_download_with_download_request_ba727387( - &self, - req: libremetaverse::http::DownloadRequest, - ) -> Result<(libremetaverse_types::compat::HttpResponse, Vec), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Http.DownloadManager.QueueDownloadAsync(LibreMetaverse.Http.DownloadRequest)", - ) - } - /// C# member: `M:LibreMetaverse.Http.DownloadManager.QueueDownloadAsync(System.Uri,System.String,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport},System.Threading.CancellationToken,System.Int32)`. - pub async fn queue_download_with_uri_string_i_progress_cancellation_token_int32( - &self, - address: libremetaverse_types::compat::Uri, - content_type: Option, - progress_callback: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - cancellation_token: Option, - retries: Option, - ) -> Result<(libremetaverse_types::compat::HttpResponse, Vec), crate::Error> { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Http.DownloadManager.QueueDownloadAsync(System.Uri,System.String,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport},System.Threading.CancellationToken,System.Int32)", - ) - } - /// C# member: `P:LibreMetaverse.Http.DownloadManager.ParallelDownloads`. - pub fn parallel_downloads(&self) -> i32 { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Http.DownloadManager.ParallelDownloads" - ) - } - /// Setter for C# member: `P:LibreMetaverse.Http.DownloadManager.ParallelDownloads`. - pub fn set_parallel_downloads(&mut self, value: i32) { - libremetaverse_types::unimplemented_api!( - "P:LibreMetaverse.Http.DownloadManager.ParallelDownloads" - ) - } - } + /// C# member: `M:LibreMetaverse.Http.DownloadManager.#ctor(LibreMetaverse.GridClient)`. + /// C# member: `M:LibreMetaverse.Http.DownloadManager.Dispose`. + /// C# member: `M:LibreMetaverse.Http.DownloadManager.DownloadAsync(System.Uri,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport},System.Threading.CancellationToken)`. + /// C# member: `M:LibreMetaverse.Http.DownloadManager.DownloadAsync(System.Uri,System.String,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport},System.Threading.CancellationToken)`. + /// C# member: `M:LibreMetaverse.Http.DownloadManager.QueueDownload(LibreMetaverse.Http.DownloadRequest)`. + /// C# member: `M:LibreMetaverse.Http.DownloadManager.QueueDownload(LibreMetaverse.Http.DownloadRequest,System.Threading.CancellationToken)`. + /// C# member: `M:LibreMetaverse.Http.DownloadManager.QueueDownloadAsync(LibreMetaverse.Http.DownloadRequest)`. + /// C# member: `M:LibreMetaverse.Http.DownloadManager.QueueDownloadAsync(System.Uri,System.String,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport},System.Threading.CancellationToken,System.Int32)`. + /// C# member: `P:LibreMetaverse.Http.DownloadManager.ParallelDownloads`. + pub use crate::download_manager::DownloadManager; /// C# type: `T:LibreMetaverse.Http.DownloadRequest`. - pub struct DownloadRequest { - /// C# member: `F:LibreMetaverse.Http.DownloadRequest.Address`. - pub address: libremetaverse_types::compat::Uri, - /// C# member: `F:LibreMetaverse.Http.DownloadRequest.Attempt`. - pub attempt: i32, - /// C# member: `F:LibreMetaverse.Http.DownloadRequest.CancellationToken`. - pub cancellation_token: libremetaverse_types::compat::CancellationToken, - /// C# member: `F:LibreMetaverse.Http.DownloadRequest.CompletionTcs`. - pub completion_tcs: Option< - libremetaverse_types::compat::TaskCompletionSource<( - libremetaverse_types::compat::HttpResponse, - Vec, - )>, - >, - /// C# member: `F:LibreMetaverse.Http.DownloadRequest.ContentType`. - pub content_type: Option, - /// C# member: `F:LibreMetaverse.Http.DownloadRequest.DownloadProgressCallback`. - pub download_progress_callback: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - /// C# member: `F:LibreMetaverse.Http.DownloadRequest.Retries`. - pub retries: i32, - } - impl DownloadRequest { - /// C# member: `M:LibreMetaverse.Http.DownloadRequest.#ctor(System.Uri,System.String,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. - pub fn new( - address: libremetaverse_types::compat::Uri, - content_type: Option, - download_progress_callback: Option< - Box< - dyn libremetaverse_types::compat::IProgress< - libremetaverse::HttpCapsClientProgressReport, - >, - >, - >, - ) -> Result { - libremetaverse_types::not_implemented( - "M:LibreMetaverse.Http.DownloadRequest.#ctor(System.Uri,System.String,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})", - ) - } - } + /// C# member: `F:LibreMetaverse.Http.DownloadRequest.Address`. + /// C# member: `F:LibreMetaverse.Http.DownloadRequest.Attempt`. + /// C# member: `F:LibreMetaverse.Http.DownloadRequest.CancellationToken`. + /// C# member: `F:LibreMetaverse.Http.DownloadRequest.CompletionTcs`. + /// C# member: `F:LibreMetaverse.Http.DownloadRequest.ContentType`. + /// C# member: `F:LibreMetaverse.Http.DownloadRequest.DownloadProgressCallback`. + /// C# member: `F:LibreMetaverse.Http.DownloadRequest.Retries`. + /// C# member: `M:LibreMetaverse.Http.DownloadRequest.#ctor(System.Uri,System.String,System.IProgress{LibreMetaverse.HttpCapsClient.ProgressReport})`. + pub use crate::download_manager::DownloadRequest; /// C# type: `T:LibreMetaverse.Http.EventQueueClient`. pub struct EventQueueClient { diff --git a/crates/libremetaverse/src/lib.rs b/crates/libremetaverse/src/lib.rs index f064040..c8267c3 100644 --- a/crates/libremetaverse/src/lib.rs +++ b/crates/libremetaverse/src/lib.rs @@ -5,7 +5,9 @@ extern crate self as libremetaverse; #[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator. mod attention_catalog; mod bit_pack; +mod caps_http; mod client_core; +mod download_manager; #[rustfmt::skip] // Deterministic machine output is formatted by the pinned generator. mod foliage_catalog; mod generated; @@ -198,6 +200,7 @@ impl imaging::Baker { } } +pub use caps_http::CapsHttpLimits; pub use client_core::{ ClientCoreError, ClientLifecycleState, ClientService, GridClientBuilder, ShutdownPhase, }; diff --git a/crates/libremetaverse/tests/caps_http.rs b/crates/libremetaverse/tests/caps_http.rs new file mode 100644 index 0000000..c763bed --- /dev/null +++ b/crates/libremetaverse/tests/caps_http.rs @@ -0,0 +1,815 @@ +use flate2::Compression; +use flate2::write::{GzEncoder, ZlibEncoder}; +use libremetaverse::http::{DownloadManager, DownloadRequest}; +use libremetaverse::{ + CapsCategory, CapsHttpLimits, CapsRateLimiter, CapsRateLimiterOptions, GridClient, + HttpCapsClient, HttpCapsClientProgressReport, +}; +use libremetaverse_structured_data::{OSD, OSDFormat}; +use libremetaverse_types::Error; +use libremetaverse_types::compat::{ + CancellationToken, CancellationTokenSource, HttpMessageHandler, HttpRequest, HttpResponse, + TaskCompletionSource, TimeProvider, Uri, +}; +use std::collections::{BTreeMap, HashMap}; +use std::io::Write as _; +use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, SystemTime}; +use tokio::io::{AsyncReadExt as _, AsyncWriteExt as _}; + +fn mutex(value: &Mutex) -> std::sync::MutexGuard<'_, T> { + value + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) +} + +fn response(status_code: u16, body: impl Into>) -> HttpResponse { + let body = body.into(); + HttpResponse { + status_code, + headers: BTreeMap::from([("Content-Length".into(), body.len().to_string())]), + content_type: None, + body, + } +} + +fn handler_returning(reply: HttpResponse) -> (Arc>>, HttpMessageHandler) { + let requests = Arc::new(Mutex::new(Vec::new())); + let recorded = Arc::clone(&requests); + let handler = HttpMessageHandler::new(move |request, _cancellation| { + let recorded = Arc::clone(&recorded); + let reply = reply.clone(); + async move { + mutex(&recorded).push(request); + reply + } + }); + (requests, handler) +} + +fn strict_limits(raw: usize, decompressed: usize) -> CapsHttpLimits { + CapsHttpLimits { + max_request_bytes: raw, + max_response_bytes: raw, + max_decompressed_bytes: decompressed, + max_redirects: 3, + } +} + +fn one_token_options(queue_limit: i32) -> CapsRateLimiterOptions { + let mut options = CapsRateLimiterOptions::new().expect("options"); + options.set_token_limit(1); + options.set_tokens_per_period(1); + options.set_replenishment_period(Duration::from_secs(1)); + options.set_queue_limit(queue_limit); + options +} + +#[tokio::test] +async fn byte_and_osd_requests_preserve_methods_content_types_and_payloads() { + let (requests, handler) = handler_returning(response(200, b"reply".to_vec())); + let client = HttpCapsClient::new(handler).expect("client"); + let uri = Uri("https://caps.example.test/secret?token=do-not-log".into()); + + let (_, get_body) = client + .get(uri.clone(), CancellationToken::default(), None) + .await + .expect("GET"); + assert_eq!(get_body, b"reply"); + client + .post_with_uri_string_bytes_cancellation_token_i_progress( + uri.clone(), + "application/octet-stream".into(), + b"post".to_vec(), + CancellationToken::default(), + None, + ) + .await + .expect("POST"); + client + .put_with_uri_string_bytes_cancellation_token_i_progress( + uri.clone(), + "text/plain".into(), + b"put".to_vec(), + CancellationToken::default(), + None, + ) + .await + .expect("PUT"); + client + .patch_with_uri_string_bytes_cancellation_token_i_progress( + uri.clone(), + "text/plain".into(), + b"patch".to_vec(), + CancellationToken::default(), + None, + ) + .await + .expect("PATCH"); + client + .delete_with_uri_string_bytes_cancellation_token_i_progress( + uri.clone(), + "text/plain".into(), + b"delete".to_vec(), + CancellationToken::default(), + None, + ) + .await + .expect("DELETE"); + + for (format, expected_content_type) in [ + (OSDFormat::Xml, HttpCapsClient::LLSD_XML), + (OSDFormat::Binary, HttpCapsClient::LLSD_BINARY), + (OSDFormat::Json, HttpCapsClient::LLSD_JSON), + ] { + client + .post_with_uri_osd_format_osd_cancellation_token_i_progress( + uri.clone(), + format, + OSD::String("payload".into()), + CancellationToken::default(), + None, + ) + .await + .expect("OSD POST"); + assert_eq!( + mutex(&requests) + .last() + .and_then(|request| request.content_type.as_deref()), + Some(expected_content_type) + ); + assert!(!mutex(&requests).last().expect("request").body.is_empty()); + } + + let requests = mutex(&requests); + assert_eq!( + requests + .iter() + .take(5) + .map(|request| request.method.as_str()) + .collect::>(), + ["GET", "POST", "PUT", "PATCH", "DELETE"] + ); + assert_eq!(requests[1].body, b"post"); + assert_eq!( + requests[1].content_type.as_deref(), + Some("application/octet-stream") + ); +} + +#[tokio::test] +async fn outbound_non_http_and_oversize_requests_are_rejected_before_the_handler() { + let (requests, handler) = handler_returning(response(200, Vec::new())); + let client = + HttpCapsClient::with_handler_and_limits(handler, strict_limits(4, 8)).expect("client"); + + let non_http = client + .get( + Uri("slcaps://opaque/secret".into()), + CancellationToken::default(), + None, + ) + .await; + assert_eq!(non_http, Err(Error::Argument)); + let oversized = client + .post_with_uri_string_bytes_cancellation_token_i_progress( + Uri("http://example.test/upload".into()), + "application/octet-stream".into(), + vec![0; 5], + CancellationToken::default(), + None, + ) + .await; + assert_eq!(oversized, Err(Error::HttpRequest)); + assert!(mutex(&requests).is_empty()); + assert!(!format!("{client:?}").contains("opaque")); + assert!(!format!("{client:?}").contains("secret")); + + let download = DownloadRequest::new( + Uri("https://caps.example.test/asset?token=download-secret".into()), + None, + None, + ) + .expect("download request"); + let debug = format!("{download:?}"); + assert!(!debug.contains("caps.example.test")); + assert!(!debug.contains("download-secret")); +} + +#[tokio::test] +async fn gzip_deflate_and_response_limits_are_enforced() { + let payload = b"compressed capability response".repeat(4); + let mut gzip = GzEncoder::new(Vec::new(), Compression::default()); + gzip.write_all(&payload).expect("gzip write"); + let gzip = gzip.finish().expect("gzip finish"); + let mut gzip_reply = response(200, gzip); + gzip_reply + .headers + .insert("Content-Encoding".into(), "gzip".into()); + let (_, gzip_handler) = handler_returning(gzip_reply); + let gzip_client = + HttpCapsClient::with_handler_and_limits(gzip_handler, strict_limits(256, 256)) + .expect("gzip client"); + let (_, decoded) = gzip_client + .get( + Uri("http://example.test/gzip".into()), + CancellationToken::default(), + None, + ) + .await + .expect("gzip response"); + assert_eq!(decoded, payload); + + let mut deflate = ZlibEncoder::new(Vec::new(), Compression::default()); + deflate.write_all(&payload).expect("deflate write"); + let deflate = deflate.finish().expect("deflate finish"); + let mut deflate_reply = response(200, deflate); + deflate_reply + .headers + .insert("Content-Encoding".into(), "deflate".into()); + let (_, deflate_handler) = handler_returning(deflate_reply); + let deflate_client = + HttpCapsClient::with_handler_and_limits(deflate_handler, strict_limits(256, 256)) + .expect("deflate client"); + let (_, decoded) = deflate_client + .get( + Uri("http://example.test/deflate".into()), + CancellationToken::default(), + None, + ) + .await + .expect("deflate response"); + assert_eq!(decoded, payload); + + let bomb = b"x".repeat(2_048); + let mut gzip = GzEncoder::new(Vec::new(), Compression::best()); + gzip.write_all(&bomb).expect("bomb write"); + let mut bomb_reply = response(200, gzip.finish().expect("bomb finish")); + bomb_reply + .headers + .insert("Content-Encoding".into(), "gzip".into()); + let (_, bomb_handler) = handler_returning(bomb_reply); + let bomb_client = + HttpCapsClient::with_handler_and_limits(bomb_handler, strict_limits(128, 128)) + .expect("bomb client"); + assert_eq!( + bomb_client + .get( + Uri("http://example.test/bomb".into()), + CancellationToken::default(), + None, + ) + .await, + Err(Error::HttpRequest) + ); + + let mut declared_too_large = response(200, b"short".to_vec()); + declared_too_large + .headers + .insert("Content-Length".into(), "129".into()); + let (_, length_handler) = handler_returning(declared_too_large); + let length_client = + HttpCapsClient::with_handler_and_limits(length_handler, strict_limits(128, 128)) + .expect("length client"); + assert_eq!( + length_client + .get( + Uri("http://example.test/declared".into()), + CancellationToken::default(), + None, + ) + .await, + Err(Error::HttpRequest) + ); + + let mut unsupported_reply = response(200, b"opaque".to_vec()); + unsupported_reply + .headers + .insert("Content-Encoding".into(), "br".into()); + let (_, unsupported_handler) = handler_returning(unsupported_reply); + let unsupported_client = + HttpCapsClient::with_handler_and_limits(unsupported_handler, strict_limits(128, 128)) + .expect("unsupported-encoding client"); + assert_eq!( + unsupported_client + .get( + Uri("http://example.test/unsupported".into()), + CancellationToken::default(), + None, + ) + .await, + Err(Error::HttpRequest) + ); +} + +#[tokio::test] +async fn upload_and_download_progress_reaches_exact_final_values() { + let body = vec![7_u8; 100_000]; + let (_, handler) = handler_returning(response(200, body.clone())); + let reports = Arc::new(Mutex::new(Vec::new())); + let recorded = Arc::clone(&reports); + let client = HttpCapsClient::new(handler).expect("client"); + client + .post_with_uri_string_bytes_cancellation_token_i_progress( + Uri("http://example.test/progress".into()), + "application/octet-stream".into(), + body.clone(), + CancellationToken::default(), + Some(Box::new(move |report: HttpCapsClientProgressReport| { + mutex(&recorded).push(report); + })), + ) + .await + .expect("request"); + + let reports = mutex(&reports); + assert!( + reports.len() >= 4, + "upload and download should each report chunks" + ); + let final_report = reports.last().expect("final report"); + assert_eq!(final_report.bytes_transferred(), 100_000); + assert_eq!(final_report.total_bytes(), Some(Some(100_000))); + assert_eq!(final_report.percent(), Some(Some(100.0))); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 4)] +async fn http_policy_bounds_concurrency_and_grid_shutdown_cancels_inflight_requests() { + let active = Arc::new(AtomicUsize::new(0)); + let maximum = Arc::new(AtomicUsize::new(0)); + let handler_active = Arc::clone(&active); + let handler_maximum = Arc::clone(&maximum); + let handler = HttpMessageHandler::new(move |_request, _cancellation| { + let active = Arc::clone(&handler_active); + let maximum = Arc::clone(&handler_maximum); + async move { + let current = active.fetch_add(1, Ordering::AcqRel) + 1; + maximum.fetch_max(current, Ordering::AcqRel); + tokio::time::sleep(Duration::from_millis(25)).await; + active.fetch_sub(1, Ordering::AcqRel); + response(200, Vec::new()) + } + }); + let client = HttpCapsClient::with_handler_policy(handler, None, CapsHttpLimits::default(), 2) + .expect("bounded client"); + let mut requests = Vec::new(); + for index in 0..6 { + let client = client.clone(); + requests.push(tokio::spawn(async move { + client + .get( + Uri(format!("http://example.test/bounded/{index}")), + CancellationToken::default(), + None, + ) + .await + })); + } + for request in requests { + request.await.expect("request task").expect("request"); + } + assert_eq!(maximum.load(Ordering::Acquire), 2); + + let started = Arc::new(AtomicUsize::new(0)); + let handler_started = Arc::clone(&started); + let handler = HttpMessageHandler::new(move |_request, cancellation| { + handler_started.fetch_add(1, Ordering::AcqRel); + async move { + cancellation.cancelled().await; + response(200, Vec::new()) + } + }); + let mut grid = GridClient::new().expect("grid client"); + grid.set_http_caps_client(HttpCapsClient::new(handler).expect("HTTP client")); + let inflight_client = grid.http_caps_client(); + let inflight = tokio::spawn(async move { + inflight_client + .get( + Uri("http://example.test/inflight".into()), + CancellationToken::default(), + None, + ) + .await + }); + while started.load(Ordering::Acquire) == 0 { + tokio::task::yield_now().await; + } + grid.dispose_with_method().expect("grid shutdown"); + assert_eq!( + tokio::time::timeout(Duration::from_millis(100), inflight) + .await + .expect("shutdown wake") + .expect("request task"), + Err(Error::Cancelled) + ); +} + +#[tokio::test(start_paused = true)] +async fn rate_limiter_is_bounded_fifo_categorized_and_clock_injected() { + let seconds = Arc::new(AtomicU64::new(0)); + let clock_seconds = Arc::clone(&seconds); + let clock = TimeProvider::from_fn(move || { + SystemTime::UNIX_EPOCH + Duration::from_secs(clock_seconds.load(Ordering::Acquire)) + }); + let options = one_token_options(2); + let limiter = CapsRateLimiter::new_with_clock_and_overrides( + clock, + Some(HashMap::from([ + (CapsCategory::Default, options.clone()), + (CapsCategory::AssetFetch, options), + ])), + ) + .expect("limiter"); + let default_uri = Uri("https://caps.example.test/default".into()); + let asset_uri = Uri("https://caps.example.test/asset".into()); + limiter + .register_cap_uri("GetTexture".into(), asset_uri.clone()) + .expect("register category"); + assert!( + limiter + .acquire(default_uri.clone(), None) + .await + .expect("default token") + .is_acquired() + ); + assert!( + limiter + .acquire(asset_uri, None) + .await + .expect("isolated asset token") + .is_acquired() + ); + + let first_limiter = limiter.clone(); + let first_uri = default_uri.clone(); + let first = tokio::spawn(async move { first_limiter.acquire(first_uri, None).await }); + tokio::task::yield_now().await; + let second_limiter = limiter.clone(); + let second = tokio::spawn(async move { second_limiter.acquire(default_uri, None).await }); + tokio::task::yield_now().await; + + seconds.store(1, Ordering::Release); + tokio::time::advance(Duration::from_secs(1)).await; + tokio::task::yield_now().await; + assert!( + first.is_finished(), + "oldest waiter must receive the first token" + ); + assert!(!second.is_finished(), "newer waiter must remain queued"); + assert!( + first + .await + .expect("first task") + .expect("first lease") + .is_acquired() + ); + + seconds.store(2, Ordering::Release); + tokio::time::advance(Duration::from_secs(1)).await; + assert!( + second + .await + .expect("second task") + .expect("second lease") + .is_acquired() + ); +} + +#[tokio::test] +async fn rate_limiter_rejects_full_queue_and_wakes_cancelled_or_disposed_waiters() { + let no_queue = one_token_options(0); + let limiter = CapsRateLimiter::new_with_i_read_only_dictionary(Some(HashMap::from([( + CapsCategory::Default, + no_queue, + )]))) + .expect("limiter"); + let uri = Uri("http://example.test/limited".into()); + assert!( + limiter + .acquire(uri.clone(), None) + .await + .expect("first") + .is_acquired() + ); + assert!( + !limiter + .acquire(uri, None) + .await + .expect("rejected") + .is_acquired() + ); + + let queued = one_token_options(2); + let limiter = CapsRateLimiter::new_with_i_read_only_dictionary(Some(HashMap::from([( + CapsCategory::Default, + queued, + )]))) + .expect("queued limiter"); + let uri = Uri("http://example.test/queued".into()); + limiter + .acquire(uri.clone(), None) + .await + .expect("consume token"); + let cancellation = CancellationTokenSource::new(); + let cancelled_limiter = limiter.clone(); + let cancelled_uri = uri.clone(); + let cancelled_token = cancellation.token(); + let cancelled = tokio::spawn(async move { + cancelled_limiter + .acquire(cancelled_uri, Some(cancelled_token)) + .await + }); + tokio::task::yield_now().await; + cancellation.cancel(); + assert_eq!( + tokio::time::timeout(Duration::from_millis(100), cancelled) + .await + .expect("cancel wake") + .expect("task"), + Err(Error::Cancelled) + ); + + let disposed_limiter = limiter.clone(); + let disposed = tokio::spawn(async move { disposed_limiter.acquire(uri, None).await }); + tokio::task::yield_now().await; + limiter.dispose().expect("dispose"); + assert_eq!( + tokio::time::timeout(Duration::from_millis(100), disposed) + .await + .expect("dispose wake") + .expect("task"), + Err(Error::InvalidOperation) + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn download_manager_retries_transient_but_not_permanent_statuses() { + let attempts = Arc::new(AtomicUsize::new(0)); + let handler_attempts = Arc::clone(&attempts); + let handler = HttpMessageHandler::new(move |_request, _cancellation| { + let attempt = handler_attempts.fetch_add(1, Ordering::AcqRel); + async move { + if attempt < 2 { + response(503, b"retry".to_vec()) + } else { + response(200, b"done".to_vec()) + } + } + }); + let mut client = GridClient::new().expect("client"); + client.set_http_caps_client(HttpCapsClient::new(handler).expect("HTTP client")); + let downloads = DownloadManager::new(client).expect("downloads"); + let (_, body) = downloads + .queue_download_with_uri_string_i_progress_cancellation_token_int32( + Uri("http://example.test/transient".into()), + None, + None, + None, + Some(2), + ) + .await + .expect("retried download"); + assert_eq!(body, b"done"); + assert_eq!(attempts.load(Ordering::Acquire), 3); + downloads.dispose().expect("dispose"); + + let attempts = Arc::new(AtomicUsize::new(0)); + let handler_attempts = Arc::clone(&attempts); + let handler = HttpMessageHandler::new(move |_request, _cancellation| { + handler_attempts.fetch_add(1, Ordering::AcqRel); + async move { response(404, b"missing".to_vec()) } + }); + let mut client = GridClient::new().expect("client"); + client.set_http_caps_client(HttpCapsClient::new(handler).expect("HTTP client")); + let downloads = DownloadManager::new(client).expect("downloads"); + assert_eq!( + downloads + .queue_download_with_uri_string_i_progress_cancellation_token_int32( + Uri("http://example.test/missing".into()), + None, + None, + None, + Some(5), + ) + .await, + Err(Error::HttpRequest) + ); + assert_eq!(attempts.load(Ordering::Acquire), 1); + downloads.dispose().expect("dispose"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn deduplicated_subscriber_cancellation_cancels_the_shared_download() { + let attempts = Arc::new(AtomicUsize::new(0)); + let handler_attempts = Arc::clone(&attempts); + let handler = HttpMessageHandler::new(move |_request, cancellation| { + handler_attempts.fetch_add(1, Ordering::AcqRel); + async move { + cancellation.cancelled().await; + response(200, b"too late".to_vec()) + } + }); + let mut client = GridClient::new().expect("client"); + client.set_http_caps_client(HttpCapsClient::new(handler).expect("HTTP client")); + let downloads = DownloadManager::new(client).expect("downloads"); + let uri = Uri("http://example.test/shared".into()); + let first_downloads = downloads.clone(); + let first_uri = uri.clone(); + let first = tokio::spawn(async move { + first_downloads + .queue_download_with_uri_string_i_progress_cancellation_token_int32( + first_uri, + None, + None, + None, + Some(0), + ) + .await + }); + let cancellation = CancellationTokenSource::new(); + let second_downloads = downloads.clone(); + let second_token = cancellation.token(); + let second = tokio::spawn(async move { + second_downloads + .queue_download_with_uri_string_i_progress_cancellation_token_int32( + uri, + None, + None, + Some(second_token), + Some(0), + ) + .await + }); + while attempts.load(Ordering::Acquire) == 0 { + tokio::task::yield_now().await; + } + cancellation.cancel(); + assert_eq!(first.await.expect("first task"), Err(Error::Cancelled)); + assert_eq!(second.await.expect("second task"), Err(Error::Cancelled)); + assert_eq!(attempts.load(Ordering::Acquire), 1); + downloads.dispose().expect("dispose"); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn downloads_honor_parallel_limit_progress_and_external_completion_source() { + let active = Arc::new(AtomicUsize::new(0)); + let maximum = Arc::new(AtomicUsize::new(0)); + let handler_active = Arc::clone(&active); + let handler_maximum = Arc::clone(&maximum); + let handler = HttpMessageHandler::new(move |_request, _cancellation| { + let active = Arc::clone(&handler_active); + let maximum = Arc::clone(&handler_maximum); + async move { + let current = active.fetch_add(1, Ordering::AcqRel) + 1; + maximum.fetch_max(current, Ordering::AcqRel); + tokio::time::sleep(Duration::from_millis(30)).await; + active.fetch_sub(1, Ordering::AcqRel); + response(200, vec![1; 100_000]) + } + }); + let mut client = GridClient::new().expect("client"); + client.set_http_caps_client(HttpCapsClient::new(handler).expect("HTTP client")); + let mut downloads = DownloadManager::new(client).expect("downloads"); + downloads.set_parallel_downloads(2); + + let reports = Arc::new(AtomicUsize::new(0)); + let completion = TaskCompletionSource::new(); + let mut request = DownloadRequest::new( + Uri("http://example.test/tcs".into()), + None, + Some(Box::new({ + let reports = Arc::clone(&reports); + move |_report: HttpCapsClientProgressReport| { + reports.fetch_add(1, Ordering::AcqRel); + } + })), + ) + .expect("request"); + request.completion_tcs = Some(completion.clone()); + downloads + .queue_download_with_download_request(request) + .expect("queue TCS request"); + + let mut tasks = Vec::new(); + for index in 0..3 { + let downloads = downloads.clone(); + tasks.push(tokio::spawn(async move { + downloads + .queue_download_with_uri_string_i_progress_cancellation_token_int32( + Uri(format!("http://example.test/{index}")), + None, + None, + None, + Some(0), + ) + .await + })); + } + let (_, body) = completion.future().await.expect("TCS completion"); + assert_eq!(body.len(), 100_000); + for task in tasks { + assert!(task.await.expect("download task").is_ok()); + } + assert!(reports.load(Ordering::Acquire) >= 2); + assert_eq!(maximum.load(Ordering::Acquire), 2); + downloads.dispose().expect("dispose"); +} + +async fn read_http_request(stream: &mut tokio::net::TcpStream) -> Vec { + let mut request = Vec::new(); + let mut scratch = [0_u8; 1024]; + loop { + let count = stream.read(&mut scratch).await.expect("read request"); + assert_ne!(count, 0, "client closed before completing request headers"); + request.extend_from_slice(&scratch[..count]); + if request.windows(4).any(|window| window == b"\r\n\r\n") { + return request; + } + } +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] +async fn injected_reqwest_backend_follows_redirects_streams_and_decompresses() { + let listener = tokio::net::TcpListener::bind((std::net::Ipv4Addr::LOCALHOST, 0)) + .await + .expect("bind fake server"); + let address = listener.local_addr().expect("server address"); + let mut state = 0x1234_5678_u32; + let payload: Vec = (0..180_000) + .map(|_| { + state = state.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); + state.to_be_bytes()[0] + }) + .collect(); + let mut encoder = GzEncoder::new(Vec::new(), Compression::default()); + encoder.write_all(&payload).expect("gzip write"); + let compressed = encoder.finish().expect("gzip finish"); + let server = tokio::spawn(async move { + let (mut first, _) = listener.accept().await.expect("first connection"); + let first_request = read_http_request(&mut first).await; + assert!(first_request.starts_with(b"GET /start HTTP/1.1")); + first + .write_all( + b"HTTP/1.1 302 Found\r\nLocation: /final\r\nContent-Length: 0\r\nConnection: close\r\n\r\n", + ) + .await + .expect("redirect response"); + let (mut second, _) = listener.accept().await.expect("second connection"); + let second_request = read_http_request(&mut second).await; + assert!(second_request.starts_with(b"GET /final HTTP/1.1")); + assert!( + String::from_utf8_lossy(&second_request) + .to_ascii_lowercase() + .contains("accept-encoding: gzip, deflate") + ); + let headers = format!( + "HTTP/1.1 200 OK\r\nContent-Type: application/llsd+binary\r\nContent-Encoding: gzip\r\nContent-Length: {}\r\nConnection: close\r\n\r\n", + compressed.len() + ); + second.write_all(headers.as_bytes()).await.expect("headers"); + for chunk in compressed.chunks(777) { + second.write_all(chunk).await.expect("body chunk"); + tokio::task::yield_now().await; + } + }); + + let reqwest = reqwest::Client::builder() + .redirect(reqwest::redirect::Policy::limited(3)) + .build() + .expect("reqwest client"); + let client = HttpCapsClient::with_reqwest_client( + reqwest, + None, + CapsHttpLimits { + max_request_bytes: 1_024, + max_response_bytes: 256 * 1_024, + max_decompressed_bytes: 512 * 1_024, + max_redirects: 3, + }, + ) + .expect("caps client"); + let reports = Arc::new(AtomicUsize::new(0)); + let report_count = Arc::clone(&reports); + let (reply, body) = client + .get( + Uri(format!("http://{address}/start")), + CancellationToken::default(), + Some(Box::new(move |_report: HttpCapsClientProgressReport| { + report_count.fetch_add(1, Ordering::AcqRel); + })), + ) + .await + .expect("redirected request"); + assert_eq!(reply.status_code, 200); + assert_eq!( + reply.content_type.as_deref(), + Some("application/llsd+binary") + ); + assert_eq!(body, payload); + assert!( + reports.load(Ordering::Acquire) > 1, + "streaming progress reports" + ); + server.await.expect("server task"); +} diff --git a/docs/caps-http.md b/docs/caps-http.md new file mode 100644 index 0000000..b51e9d0 --- /dev/null +++ b/docs/caps-http.md @@ -0,0 +1,85 @@ +# Capability HTTP, rate limiting, and downloads + +`HttpCapsClient`, `CapsRateLimiter`, and `Http::DownloadManager` are native Rust +implementations of the corresponding LibreMetaverse classes. They do not call +.NET code or use a helper process. A normal `GridClient` owns a `reqwest` +client using rustls, its category limiter, and their shutdown order. Tests and +embedders can inject either a `reqwest::Client` or an executor-neutral +`HttpMessageHandler`, plus a `TimeProvider` for deterministic token-bucket time. + +## HTTP policy + +Only absolute `http` and `https` request URIs are accepted. Response headers +are stored as text without interpreting `Location` or `Content-Location`, so a +successful response containing an opaque `slcaps://` value remains usable. +Production redirects are followed only while the next URI is HTTP(S) and the +configured redirect count has not been exhausted. + +The default policy bounds request bodies at 32 MiB, wire response bodies at +64 MiB, decompressed bodies at 128 MiB, redirects at 10, and concurrent HTTP +requests at `Settings::max_http_connections()` (32). Declared and observed +response lengths are checked. Gzip and zlib/raw-deflate decoding reads through +a limiting adapter, preventing a small compressed response from expanding +beyond policy. Unsupported content encodings fail closed. Upload and download +progress follows the mapped C# `ProgressReport`, including two-decimal percent +values when a total length is known. + +`CapsHttpLimits`, `HttpCapsClient::with_reqwest_client`, and +`HttpCapsClient::with_handler_policy` are the explicit injection seams. Fake +handlers receive the exact method, URI, content type, and request bytes and can +return deterministic status, headers, and bytes without external network +access. Grid-client shutdown cancels in-flight operations before disposing the +rate limiter. + +## Rate categories + +Each category owns a bounded, oldest-first token bucket. A full waiting queue +returns a non-acquired lease; the HTTP pipeline then proceeds, matching the C# +handler's overload behavior rather than dropping a request. + +| Category | Burst | Tokens/second | Queue | +| --- | ---: | ---: | ---: | +| Default | 20 | 10 | 30 | +| RenderMaterials | 4 | 2 | 20 | +| AssetFetch | 24 | 12 | 60 | +| AssetUpload | 4 | 2 | 10 | +| Inventory | 6 | 3 | 20 | +| EventQueue | 3 | 2 | 3 | +| DisplayName | 5 | 2 | 15 | +| Voice | 10 | 5 | 20 | + +`RegisterCapUri` applies the complete case-insensitive cap-name mapping from the +reference. Unregistered URIs use `Default`; texture/mesh, upload, inventory, +event queue, display-name/profile, material, and voice endpoints remain +isolated from one another. Cancellation removes queued tickets, and disposal +wakes all waiters immediately. + +## Download behavior + +The manager owns a 256-entry dispatch queue and permits 1 through 32 active +downloads (8 by default). Absolute URIs are canonicalized before deduplication. +All subscribers to the same active URI share one HTTP request, receive progress, +and complete with cloned response bytes. Cancellation by any subscriber cancels +that shared operation, as in the reference. Caller-provided +`TaskCompletionSource` values are completed by the fire-and-forget API. + +HTTP 401, 403, 404, and 410 responses are permanent. Other unsuccessful +statuses and transport failures retry up to the request's configured retry +count with bounded backoff. Disposal cancels active and queued work, wakes every +waiter, joins the dispatcher, and prevents new requests. + +Errors expose only typed categories and `Debug` output substitutes a redacted +marker for capability URIs. No request URL, query token, body, or response body +is logged. + +Run the deterministic issue gate with: + +```sh +cargo test -p libremetaverse --test caps_http +cargo test -p libremetaverse-compat-tests --test network_semantics queue_download +cargo test -p libremetaverse-compat-tests --test network_semantics put_response_with_non_http_location +``` + +The first suite uses injected handlers for all policy and concurrency cases and +a loopback-only fake HTTP server for the actual reqwest redirect/stream path. It +requires no live grid or Internet service. diff --git a/tests/api-compile/Cargo.lock b/tests/api-compile/Cargo.lock index b5c8f2d..ca3ac6b 100644 --- a/tests/api-compile/Cargo.lock +++ b/tests/api-compile/Cargo.lock @@ -2,6 +2,41 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "aws-lc-rs" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2b2dcc879c3bae0d371e77c99f2238400ef24ec001394befa67b6e543add9e" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.44.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f09fae7be8bb3174e05c6afdb34199e6dc0c7c04ba9fa237b1967adfbde27483" +dependencies = [ + "cc", + "cmake", + "dunce", + "fs_extra", + "pkg-config", +] + [[package]] name = "base64" version = "0.22.1" @@ -14,6 +49,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f09c37bc0e9f0924b7dae9988265ef3c76c88538f41a3b06caf4bed07cee5226" +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + [[package]] name = "block-buffer" version = "0.10.4" @@ -38,18 +79,88 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d262e149917187838d5b42777c8253bcb64500067342904e7d429499a6f277e" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d524456ba66e72eb8b115ff89e01e497f8e6d11d78b70b1aa13c0fbd97540a81" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "rand_core", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "const-oid" version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "cpufeatures" version = "0.2.17" @@ -68,6 +179,15 @@ dependencies = [ "libc", ] +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -108,12 +228,92 @@ dependencies = [ "crypto-common 0.2.2", ] +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "find-msvc-tools" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b73573e6edcd2af0cdf47bd6cb58f0b3839491263c314eaad1ccf24430e1de" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "futures-channel" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae" +dependencies = [ + "futures-core", +] + [[package]] name = "futures-core" version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7" +[[package]] +name = "futures-io" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a" + +[[package]] +name = "futures-macro" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d6d3cde68c518367be28956066ddfef33813991b77a55005a69dae04bf3b10b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "futures-sink" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307" + [[package]] name = "futures-task" version = "0.3.33" @@ -127,7 +327,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa" dependencies = [ "futures-core", + "futures-io", + "futures-macro", + "futures-sink", "futures-task", + "memchr", "pin-project-lite", "slab", ] @@ -142,6 +346,19 @@ dependencies = [ "version_check", ] +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -149,10 +366,52 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", + "js-sys", "libc", "r-efi", + "rand_core", + "wasm-bindgen", ] +[[package]] +name = "http" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + [[package]] name = "hybrid-array" version = "0.4.14" @@ -162,12 +421,238 @@ dependencies = [ "typenum", ] +[[package]] +name = "hyper" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" +dependencies = [ + "http", + "hyper", + "hyper-util", + "rustls", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "ipnet" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a756c3fac73139e83f14c2d742155dd2b78d3ee56597b419a0579b7bdd6dd78" + [[package]] name = "itoa" version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys", + "log", + "simd_cesu8", + "thiserror", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.119", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.119", +] + +[[package]] +name = "jobserver" +version = "0.1.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c00acbd29eabad4a2392fa0e921c874934dbbf4194312ad20f04a0ed67a3cb3" +dependencies = [ + "getrandom 0.4.3", + "libc", +] + [[package]] name = "js-sys" version = "0.3.104" @@ -190,9 +675,12 @@ name = "libremetaverse" version = "0.0.1" dependencies = [ "bcdec_rs", + "flate2", + "futures-util", "libremetaverse-imaging", "libremetaverse-structured-data", "libremetaverse-types", + "reqwest", "roxmltree", "tokio", ] @@ -269,7 +757,7 @@ name = "libremetaverse-types" version = "0.0.1" dependencies = [ "base64", - "getrandom", + "getrandom 0.4.3", "md-5", "sha1", "sha2", @@ -303,6 +791,24 @@ dependencies = [ "libremetaverse-types", ] +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lru-slab" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154" + [[package]] name = "md-5" version = "0.10.6" @@ -338,6 +844,16 @@ dependencies = [ "libremetaverse-voice-webrtc", ] +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + [[package]] name = "mio" version = "1.2.2" @@ -346,7 +862,7 @@ checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427" dependencies = [ "libc", "wasi", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -355,12 +871,39 @@ version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + [[package]] name = "pin-project-lite" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -370,6 +913,63 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "quinn" +version = "0.11.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c1a41e437b6bbd489372cd4971de128e85c855f56c57f283d20ff016cf7c0a8" +dependencies = [ + "bytes", + "cfg_aliases", + "pin-project-lite", + "quinn-proto", + "quinn-udp", + "rustc-hash", + "rustls", + "socket2", + "thiserror", + "tokio", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-proto" +version = "0.11.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4bfc015262b9df63c8845072ce59068853ff5872180c2ce2f13038b970e560" +dependencies = [ + "aws-lc-rs", + "bytes", + "getrandom 0.4.3", + "lru-slab", + "rand", + "rand_pcg", + "ring", + "rustc-hash", + "rustls", + "rustls-pki-types", + "slab", + "thiserror", + "tinyvec", + "tracing", + "web-time", +] + +[[package]] +name = "quinn-udp" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35a133f956daabe89a61a685c2649f13d82d5aa4bd5d12d1277e1072a21c0694" +dependencies = [ + "cfg_aliases", + "libc", + "once_cell", + "socket2", + "tracing", + "windows-sys 0.61.2", +] + [[package]] name = "quote" version = "1.0.47" @@ -385,6 +985,84 @@ version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f5fa3a058cd35567ef9bfa5e75732bee0f9e4c55fa90477bef2dfcdbc4be80" +dependencies = [ + "chacha20", + "getrandom 0.4.3", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" + +[[package]] +name = "rand_pcg" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caa0f4137e1c0a72f4c651489402276c8e8e1cf081f3b0ba156d2cbeef09e86a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "reqwest" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3" +dependencies = [ + "base64", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "quinn", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "sync_wrapper", + "tokio", + "tokio-rustls", + "tokio-util", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-streams", + "web-sys", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.17", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "roxmltree" version = "0.21.1" @@ -394,12 +1072,149 @@ dependencies = [ "memchr", ] +[[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 = "rustls" +version = "0.23.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" +dependencies = [ + "web-time", + "zeroize", +] + +[[package]] +name = "rustls-platform-verifier" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" +dependencies = [ + "core-foundation", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework", + "security-framework-sys", + "webpki-root-certs", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + +[[package]] +name = "rustls-webpki" +version = "0.103.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "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.229" @@ -464,12 +1279,46 @@ dependencies = [ "digest 0.11.3", ] +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "simd-adler32" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a219298ac11a56ea9a6d2120044824d6f01aeb034955e7af7bc16858527deea" + +[[package]] +name = "simd_cesu8" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "slab" version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + [[package]] name = "socket2" version = "0.6.5" @@ -477,9 +1326,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.61.2", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + [[package]] name = "syn" version = "2.0.119" @@ -502,18 +1363,84 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinyvec" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + [[package]] name = "tokio" version = "1.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" dependencies = [ + "bytes", "libc", "mio", "pin-project-lite", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -527,6 +1454,99 @@ dependencies = [ "syn 3.0.3", ] +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "494815d09bf52b5548659851081238f0ca39ff638363907596da739561c62c52" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-core", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + [[package]] name = "typenum" version = "1.20.1" @@ -539,13 +1559,37 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "uuid" version = "1.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" dependencies = [ - "getrandom", + "getrandom 0.4.3", "js-sys", "wasm-bindgen", ] @@ -556,6 +1600,25 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + [[package]] name = "wasi" version = "0.11.1+wasi-snapshot-preview1" @@ -575,6 +1638,16 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b7777d5cc23d0e91404e53ce2d5e8ec7acae3026b16233dba62cd3246457950" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.127" @@ -607,12 +1680,72 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-streams" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1ec4f6517c9e11ae630e200b2b65d193279042e28edd4a2cda233e46670bbb" +dependencies = [ + "futures-util", + "js-sys", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "web-sys" +version = "0.3.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c435338968042f4f59a557f690a253676d47ce13ceb55d70100e7facf6620a30" +dependencies = [ + "js-sys", + "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-root-certs" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -622,6 +1755,159 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e" + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "zmij" version = "1.0.23" diff --git a/tools/generate_api_shims.py b/tools/generate_api_shims.py index 213bd38..e37567c 100644 --- a/tools/generate_api_shims.py +++ b/tools/generate_api_shims.py @@ -38,6 +38,12 @@ TARGETS = { # implementations. The generated module keeps catalog markers and re-exports # the hand-written type so coverage remains deterministic. NATIVE_TYPES = { + "T:LibreMetaverse.CapsRateLimiter": "crate::caps_http::CapsRateLimiter", + "T:LibreMetaverse.CapsRateLimiterOptions": "crate::caps_http::CapsRateLimiterOptions", + "T:LibreMetaverse.Http.DownloadManager": "crate::download_manager::DownloadManager", + "T:LibreMetaverse.Http.DownloadRequest": "crate::download_manager::DownloadRequest", + "T:LibreMetaverse.HttpCapsClient": "crate::caps_http::HttpCapsClient", + "T:LibreMetaverse.HttpCapsClient.ProgressReport": "crate::caps_http::HttpCapsClientProgressReport", "T:LibreMetaverse.Caps.EventQueueCallback": "crate::network_manager::CapsEventQueueCallback", "T:LibreMetaverse.CapsEventDictionary": "crate::network_manager::CapsEventDictionary", "T:LibreMetaverse.DisconnectedEventArgs": "crate::network_manager::DisconnectedEventArgs", @@ -178,6 +184,14 @@ NATIVE_MEMBER_BODIES = { "self.shutdown().map_err(Into::into)", "M:LibreMetaverse.GridClient.ToString": "String::new()", + "P:LibreMetaverse.GridClient.CapsRateLimiter": + "self.native_caps_rate_limiter()", + "P:LibreMetaverse.GridClient.CapsRateLimiter#set": + "self.native_set_caps_rate_limiter(value)", + "P:LibreMetaverse.GridClient.HttpCapsClient": + "self.native_http_caps_client()", + "P:LibreMetaverse.GridClient.HttpCapsClient#set": + "self.native_set_http_caps_client(value)", "E:LibreMetaverse.NetworkManager.Disconnected": "self.native_subscribe_disconnected(handler)", "E:LibreMetaverse.NetworkManager.GenericStreamingMessage":