From d0510ac85b38f69e057c0917e9100f81be700754 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 4 Sep 2026 01:13:24 +0200 Subject: [PATCH] Consume the core stack from CultLib instead of forking it vendor/README.md declared cultcache-rs, cultnet-rs and cultmesh-rs "a locally owned compatible protocol generation". That charter is the defect. A consumer holding its own generation of the wire stack sits outside the only harness that checks cross-runtime agreement, and the fork behaved exactly as that predicts: runtime_authority_contracts.rs drifted to e542225 while this repo's own authority map named f4ab237, and the Rust CultMesh implementation existed nowhere else in GameCult. All three now come from CultLib pinned at 85f7024, and 19,604 lines of forked substrate are deleted. Nothing was lost on the way out. The vendored cultnet tree was compared file by file against CultLib: ten of twelve matched, lib.rs differed only in module declarations, and transport.rs carried one guard that existed nowhere else -- a framed length checked against the advertised maximum before allocating. That guard is now CultLib 85f7024 with a test. The migration this was expected to need does not exist. RuntimePresenceCorrelation changing from enum to struct is Odin's side of the contract and no file here touches it; with line endings normalised the two contract copies differ by 132 lines, and the only production change that reaches a provider is IdunnExpectedRoute requiring rudp:// schemes for rudp transport, which this service does not use. The code compiles unchanged. 89 tests pass across the workspace against the pinned rev. vendor/ still holds the cultcache-py and eve submodules. cultcache-py is the same pattern -- a runtime-specific repo CultLib also carries as packages/cultcache-py -- but it backs the state tooling rather than the daemon, so it is left for its own change. Co-Authored-By: Claude Opus 5 --- Cargo.lock | 519 +--- Cargo.toml | 10 +- vendor/README.md | 11 - vendor/cultcache-rs/.gitignore | 2 - vendor/cultcache-rs/Cargo.toml | 28 - vendor/cultcache-rs/README.md | 332 --- .../cultcache-rs-derive/Cargo.toml | 13 - .../cultcache-rs-derive/src/lib.rs | 243 -- .../examples/cultcache_interop.rs | 121 - vendor/cultcache-rs/src/lib.rs | 1080 -------- vendor/cultcache-rs/src/owned_redb.rs | 564 ---- vendor/cultmesh-rs/Cargo.lock | 1233 --------- vendor/cultmesh-rs/Cargo.toml | 20 - vendor/cultmesh-rs/src/lib.rs | 1363 ---------- vendor/cultnet-rs/.gitignore | 2 - vendor/cultnet-rs/Cargo.toml | 36 - vendor/cultnet-rs/README.md | 279 -- .../cultnet.document-delete.schema.json | 19 - ...net.document-mutation-contract.schema.json | 40 - .../cultnet.document-put-raw.schema.json | 19 - .../cultnet.document-put.schema.json | 19 - .../cultnet.document-record.schema.json | 26 - .../contracts/cultnet.error.schema.json | 15 - .../contracts/cultnet.hello.schema.json | 41 - .../cultnet.login-success.schema.json | 17 - .../contracts/cultnet.login.schema.json | 19 - .../cultnet.raw-document-record.schema.json | 32 - .../contracts/cultnet.register.schema.json | 21 - .../cultnet.sample-change-name.schema.json | 15 - .../contracts/cultnet.sample-chat.schema.json | 15 - ...cultnet.schema-catalog-request.schema.json | 31 - ...ultnet.schema-catalog-response.schema.json | 22 - .../cultnet.schema-descriptor.schema.json | 39 - .../cultnet.shard-catalog-request.schema.json | 19 - ...cultnet.shard-catalog-response.schema.json | 47 - .../cultnet.snapshot-request.schema.json | 23 - .../cultnet.snapshot-response-raw.schema.json | 22 - .../cultnet.snapshot-response.schema.json | 22 - .../cultnet.transport-profile.schema.json | 100 - .../contracts/cultnet.verify.schema.json | 17 - .../ghostlight.agent-state.schema.json | 845 ------ .../examples/cultnet_interop_peer.rs | 1628 ------------ vendor/cultnet-rs/src/contracts.rs | 1406 ---------- vendor/cultnet-rs/src/cultmesh.rs | 525 ---- vendor/cultnet-rs/src/framing.rs | 49 - vendor/cultnet-rs/src/lib.rs | 25 - vendor/cultnet-rs/src/operation_service.rs | 60 - vendor/cultnet-rs/src/provider_session.rs | 625 ----- vendor/cultnet-rs/src/replication.rs | 1014 -------- vendor/cultnet-rs/src/rudp.rs | 2038 --------------- vendor/cultnet-rs/src/schema_discovery.rs | 410 --- vendor/cultnet-rs/src/security.rs | 270 -- vendor/cultnet-rs/src/service_identity.rs | 705 ----- vendor/cultnet-rs/src/shard_catalog.rs | 134 - vendor/cultnet-rs/src/transport.rs | 288 --- vendor/cultnet-rs/tests/cultnet.rs | 2279 ----------------- .../tests/fixtures/cultnet-ts-hello.frame | Bin 160 -> 0 bytes .../fixtures/cultnet-ts-legacy-login.frame | Bin 30 -> 0 bytes vendor/cultnet-rs/tests/provider_session.rs | 365 --- vendor/cultnet-rs/tests/rudp_server_hub.rs | 459 ---- 60 files changed, 17 insertions(+), 19604 deletions(-) delete mode 100644 vendor/README.md delete mode 100644 vendor/cultcache-rs/.gitignore delete mode 100644 vendor/cultcache-rs/Cargo.toml delete mode 100644 vendor/cultcache-rs/README.md delete mode 100644 vendor/cultcache-rs/cultcache-rs-derive/Cargo.toml delete mode 100644 vendor/cultcache-rs/cultcache-rs-derive/src/lib.rs delete mode 100644 vendor/cultcache-rs/examples/cultcache_interop.rs delete mode 100644 vendor/cultcache-rs/src/lib.rs delete mode 100644 vendor/cultcache-rs/src/owned_redb.rs delete mode 100644 vendor/cultmesh-rs/Cargo.lock delete mode 100644 vendor/cultmesh-rs/Cargo.toml delete mode 100644 vendor/cultmesh-rs/src/lib.rs delete mode 100644 vendor/cultnet-rs/.gitignore delete mode 100644 vendor/cultnet-rs/Cargo.toml delete mode 100644 vendor/cultnet-rs/README.md delete mode 100644 vendor/cultnet-rs/contracts/cultnet.document-delete.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.document-mutation-contract.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.document-put-raw.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.document-put.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.document-record.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.error.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.hello.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.login-success.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.login.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.raw-document-record.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.register.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.sample-change-name.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.sample-chat.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.schema-catalog-request.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.schema-catalog-response.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.schema-descriptor.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.shard-catalog-request.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.shard-catalog-response.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.snapshot-request.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.snapshot-response-raw.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.snapshot-response.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.transport-profile.schema.json delete mode 100644 vendor/cultnet-rs/contracts/cultnet.verify.schema.json delete mode 100644 vendor/cultnet-rs/contracts/ghostlight.agent-state.schema.json delete mode 100644 vendor/cultnet-rs/examples/cultnet_interop_peer.rs delete mode 100644 vendor/cultnet-rs/src/contracts.rs delete mode 100644 vendor/cultnet-rs/src/cultmesh.rs delete mode 100644 vendor/cultnet-rs/src/framing.rs delete mode 100644 vendor/cultnet-rs/src/lib.rs delete mode 100644 vendor/cultnet-rs/src/operation_service.rs delete mode 100644 vendor/cultnet-rs/src/provider_session.rs delete mode 100644 vendor/cultnet-rs/src/replication.rs delete mode 100644 vendor/cultnet-rs/src/rudp.rs delete mode 100644 vendor/cultnet-rs/src/schema_discovery.rs delete mode 100644 vendor/cultnet-rs/src/security.rs delete mode 100644 vendor/cultnet-rs/src/service_identity.rs delete mode 100644 vendor/cultnet-rs/src/shard_catalog.rs delete mode 100644 vendor/cultnet-rs/src/transport.rs delete mode 100644 vendor/cultnet-rs/tests/cultnet.rs delete mode 100644 vendor/cultnet-rs/tests/fixtures/cultnet-ts-hello.frame delete mode 100644 vendor/cultnet-rs/tests/fixtures/cultnet-ts-legacy-login.frame delete mode 100644 vendor/cultnet-rs/tests/provider_session.rs delete mode 100644 vendor/cultnet-rs/tests/rudp_server_hub.rs diff --git a/Cargo.lock b/Cargo.lock index 23ba539..3281539 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,20 +37,6 @@ dependencies = [ "subtle", ] -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "serde", - "version_check", - "zerocopy", -] - [[package]] name = "aho-corasick" version = "1.1.5" @@ -60,12 +46,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - [[package]] name = "android_system_properties" version = "0.1.6" @@ -208,21 +188,6 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - [[package]] name = "bitflags" version = "2.13.1" @@ -238,24 +203,12 @@ dependencies = [ "generic-array", ] -[[package]] -name = "borrow-or-share" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c" - [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" -[[package]] -name = "bytecount" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" - [[package]] name = "bytes" version = "1.12.1" @@ -306,7 +259,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -388,18 +341,17 @@ dependencies = [ [[package]] name = "cultcache-rs" version = "0.1.0" +source = "git+https://github.com/GameCult/CultLib.git?rev=85f7024a35ea42cf9f0a0199a064526427150191#85f7024a35ea42cf9f0a0199a064526427150191" dependencies = [ "anyhow", "chrono", "cultcache-rs-derive", "fs2", - "pretty_assertions", "redb", "rmp-serde", "serde", "serde_bytes", "serde_json", - "tempfile", "uuid", "windows-sys 0.59.0", ] @@ -407,6 +359,7 @@ dependencies = [ [[package]] name = "cultcache-rs-derive" version = "0.1.0" +source = "git+https://github.com/GameCult/CultLib.git?rev=85f7024a35ea42cf9f0a0199a064526427150191#85f7024a35ea42cf9f0a0199a064526427150191" dependencies = [ "proc-macro2", "quote", @@ -416,19 +369,18 @@ dependencies = [ [[package]] name = "cultmesh-rs" version = "0.1.0" +source = "git+https://github.com/GameCult/CultLib.git?rev=85f7024a35ea42cf9f0a0199a064526427150191#85f7024a35ea42cf9f0a0199a064526427150191" dependencies = [ "anyhow", "cultcache-rs", "cultnet-rs", - "pretty_assertions", - "rmp-serde", "serde", - "tempfile", ] [[package]] name = "cultnet-rs" version = "0.1.0" +source = "git+https://github.com/GameCult/CultLib.git?rev=85f7024a35ea42cf9f0a0199a064526427150191#85f7024a35ea42cf9f0a0199a064526427150191" dependencies = [ "aes-gcm", "anyhow", @@ -437,7 +389,6 @@ dependencies = [ "cultcache-rs", "ed25519-dalek", "hmac", - "pretty_assertions", "rand 0.9.5", "rmp-serde", "rmpv", @@ -446,9 +397,9 @@ dependencies = [ "serde_json", "sha2", "socket2 0.5.10", - "tempfile", "uuid", "windows-sys 0.61.2", + "zeroize", ] [[package]] @@ -478,12 +429,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "data-encoding" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06" - [[package]] name = "der" version = "0.7.10" @@ -500,12 +445,6 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - [[package]] name = "digest" version = "0.10.7" @@ -528,12 +467,6 @@ dependencies = [ "syn 3.0.3", ] -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - [[package]] name = "ed25519" version = "2.2.3" @@ -558,21 +491,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "email_address" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449" -dependencies = [ - "serde", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - [[package]] name = "errno" version = "0.3.14" @@ -583,17 +501,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "fancy-regex" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "476de73bddf2ef8490aa4ee8f1cf40b430bf1d56c48c22080e5186952cd580e6" -dependencies = [ - "bit-set", - "regex-automata", - "regex-syntax", -] - [[package]] name = "fastrand" version = "2.5.0" @@ -612,23 +519,6 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" -[[package]] -name = "fluent-uri" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e" -dependencies = [ - "borrow-or-share", - "ref-cast", - "serde", -] - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - [[package]] name = "form_urlencoded" version = "1.2.2" @@ -638,16 +528,6 @@ dependencies = [ "percent-encoding", ] -[[package]] -name = "fraction" -version = "0.15.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e076045bb43dac435333ed5f04caf35c7463631d0dae2deb2638d94dd0a5b872" -dependencies = [ - "lazy_static", - "num", -] - [[package]] name = "fs2" version = "0.4.3" @@ -748,11 +628,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", - "js-sys", "libc", "r-efi 5.3.0", "wasip2", - "wasm-bindgen", ] [[package]] @@ -796,13 +674,11 @@ dependencies = [ "cultnet-rs", "ghostlight-persona-projection", "hmac", - "jsonschema", "jsonwebtoken", "libc", "rand 0.9.5", "reqwest", "rmp-serde", - "schemars", "serde", "serde_bytes", "serde_json", @@ -815,7 +691,6 @@ dependencies = [ "tracing", "tracing-subscriber", "uuid", - "windows", "zeroize", ] @@ -827,23 +702,6 @@ dependencies = [ "sha2", ] -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" -dependencies = [ - "allocator-api2", - "equivalent", - "foldhash", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - [[package]] name = "hmac" version = "0.12.1" @@ -976,7 +834,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.62.2", + "windows-core", ] [[package]] @@ -1124,58 +982,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "jsonschema" -version = "0.49.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ec8a241beed129f06114aa68007e905ca350e7baeb6e17a7631bb7978d91b2" -dependencies = [ - "ahash", - "bytecount", - "data-encoding", - "email_address", - "fancy-regex", - "fraction", - "getrandom 0.3.4", - "idna", - "itoa", - "jsonschema-regex", - "jsonschema-value", - "num-cmp", - "num-traits", - "percent-encoding", - "referencing", - "regex", - "serde", - "serde_json", - "strum", - "unicode-general-category", - "uuid-simd", -] - -[[package]] -name = "jsonschema-regex" -version = "0.49.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91994f45017ed5e66aa8e59b8415f4cb033a6380d7200387b7cf117595fbdf85" -dependencies = [ - "regex-syntax", -] - -[[package]] -name = "jsonschema-value" -version = "0.49.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec7637f83e510868ae6ed625f7ebfbbde4554ee8ce49854caa5126a8b9b9ecb" -dependencies = [ - "ahash", - "bytecount", - "fraction", - "num-cmp", - "num-traits", - "serde_json", -] - [[package]] name = "jsonwebtoken" version = "9.3.1" @@ -1257,12 +1063,6 @@ version = "2.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" -[[package]] -name = "micromap" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a86d3146ed3995b5913c414f6664344b9617457320782e64f0bb44afd49d74" - [[package]] name = "mime" version = "0.3.17" @@ -1299,20 +1099,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - [[package]] name = "num-bigint" version = "0.4.8" @@ -1323,21 +1109,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-cmp" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - [[package]] name = "num-conv" version = "0.2.2" @@ -1353,27 +1124,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "num-iter" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint", - "num-integer", - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1395,12 +1145,6 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - [[package]] name = "parking_lot" version = "0.12.5" @@ -1421,7 +1165,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -1492,16 +1236,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "pretty_assertions" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" -dependencies = [ - "diff", - "yansi", -] - [[package]] name = "proc-macro2" version = "1.0.107" @@ -1670,55 +1404,6 @@ dependencies = [ "bitflags", ] -[[package]] -name = "ref-cast" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216e8f773d7923bcba9ceb86a86c93cabb3903a11872fc3f138c49630e50b96d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9283685feec7d69af75fb0e858d5e7378f33fe4fc699383b2916ab9273e03c" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "referencing" -version = "0.49.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6efa2154ea6f5ce0fdecdd2a8d18f2fa1a39a8fbba91564f555a592e4dce8278" -dependencies = [ - "ahash", - "fluent-uri", - "getrandom 0.3.4", - "hashbrown", - "itoa", - "micromap", - "parking_lot", - "percent-encoding", - "serde_json", -] - -[[package]] -name = "regex" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - [[package]] name = "regex-automata" version = "0.4.18" @@ -1896,33 +1581,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" -[[package]] -name = "schemars" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "687274d293b6cdc6e73e0fee520bf2049650090d7164f87672d212a3c530cf4a" -dependencies = [ - "chrono", - "dyn-clone", - "ref-cast", - "schemars_derive", - "serde", - "serde_json", - "uuid", -] - -[[package]] -name = "schemars_derive" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d98c67716b46af2f0b8cf752abc930f6f9aecfbf671ecfb531db8a31dbe4e2ba" -dependencies = [ - "proc-macro2", - "quote", - "serde_derive_internals", - "syn 3.0.3", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -1975,17 +1633,6 @@ dependencies = [ "syn 3.0.3", ] -[[package]] -name = "serde_derive_internals" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f852137cce035d6a4df67ccce505ff6b3e9fd3a10e3e52b24dc71e650bb1a9bd" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - [[package]] name = "serde_json" version = "1.0.151" @@ -2127,27 +1774,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "strum" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.28.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "subtle" version = "2.6.1" @@ -2480,12 +2106,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142" -[[package]] -name = "unicode-general-category" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" - [[package]] name = "unicode-ident" version = "1.0.24" @@ -2538,16 +2158,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "uuid-simd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8" -dependencies = [ - "outref", - "vsimd", -] - [[package]] name = "valuable" version = "0.1.1" @@ -2560,12 +2170,6 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" -[[package]] -name = "vsimd" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" - [[package]] name = "want" version = "0.3.1" @@ -2709,41 +2313,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows" -version = "0.61.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babd3a767a4c1aef6900409f85f5d53ce2544ccdfaa86dad48c91782c6d6893" -dependencies = [ - "windows-collections", - "windows-core 0.61.2", - "windows-future", - "windows-link 0.1.3", - "windows-numerics", -] - -[[package]] -name = "windows-collections" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3beeceb5e5cfd9eb1d76b381630e82c4241ccd0d27f1a39ed41b2760b255c5e8" -dependencies = [ - "windows-core 0.61.2", -] - -[[package]] -name = "windows-core" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link 0.1.3", - "windows-result 0.3.4", - "windows-strings 0.4.2", -] - [[package]] name = "windows-core" version = "0.62.2" @@ -2752,20 +2321,9 @@ checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" dependencies = [ "windows-implement", "windows-interface", - "windows-link 0.2.1", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - -[[package]] -name = "windows-future" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", - "windows-threading", + "windows-link", + "windows-result", + "windows-strings", ] [[package]] @@ -2790,53 +2348,19 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "windows-link" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" - [[package]] name = "windows-link" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" -[[package]] -name = "windows-numerics" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" -dependencies = [ - "windows-core 0.61.2", - "windows-link 0.1.3", -] - -[[package]] -name = "windows-result" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows-result" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" dependencies = [ - "windows-link 0.2.1", -] - -[[package]] -name = "windows-strings" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" -dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -2845,7 +2369,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -2872,7 +2396,7 @@ version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-link 0.2.1", + "windows-link", ] [[package]] @@ -2891,15 +2415,6 @@ dependencies = [ "windows_x86_64_msvc", ] -[[package]] -name = "windows-threading" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" -dependencies = [ - "windows-link 0.1.3", -] - [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" @@ -2960,12 +2475,6 @@ version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - [[package]] name = "yoke" version = "0.8.3" diff --git a/Cargo.toml b/Cargo.toml index ce04448..cdaf955 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,10 +2,6 @@ members = [ "crates/ghostlight-dungeon", "crates/ghostlight-persona-projection", - "vendor/cultcache-rs", - "vendor/cultcache-rs/cultcache-rs-derive", - "vendor/cultnet-rs", - "vendor/cultmesh-rs", ] resolver = "2" @@ -23,9 +19,9 @@ axum = { version = "0.8", features = ["macros"] } chrono = { version = "0.4", features = ["serde"] } base64 = "0.22" codex-connector = { git = "https://github.com/GameCult/CodexConnector.git", rev = "68fe94b9475823de92ed766a97ea962d48723708" } -cultcache-rs = { path = "vendor/cultcache-rs" } -cultnet-rs = { path = "vendor/cultnet-rs" } -cultmesh-rs = { path = "vendor/cultmesh-rs" } +cultcache-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "85f7024a35ea42cf9f0a0199a064526427150191", package = "cultcache-rs" } +cultnet-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "85f7024a35ea42cf9f0a0199a064526427150191", package = "cultnet-rs" } +cultmesh-rs = { git = "https://github.com/GameCult/CultLib.git", rev = "85f7024a35ea42cf9f0a0199a064526427150191", package = "cultmesh-rs" } jsonwebtoken = "9" hmac = "0.12" rand = "0.9" diff --git a/vendor/README.md b/vendor/README.md deleted file mode 100644 index 5b397de..0000000 --- a/vendor/README.md +++ /dev/null @@ -1,11 +0,0 @@ -# Vendored GameCult Rust substrate - -`cultcache-rs`, `cultnet-rs`, and `cultmesh-rs` are a locally owned compatible -protocol generation. Ghostlight vendors the set so an immutable release can be -rebuilt from its exact Git commit. - -The vendored CultCache crate owns both the snapshot store and the pinned redb -store used by application sessions, the world journal, and controller-work -custody. Its redb table, key, and envelope encoding remain compatible with the -existing Ghostlight stores. There is no second CultCache package or persistence -authority in the workspace. diff --git a/vendor/cultcache-rs/.gitignore b/vendor/cultcache-rs/.gitignore deleted file mode 100644 index 96ef6c0..0000000 --- a/vendor/cultcache-rs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target -Cargo.lock diff --git a/vendor/cultcache-rs/Cargo.toml b/vendor/cultcache-rs/Cargo.toml deleted file mode 100644 index ef221d1..0000000 --- a/vendor/cultcache-rs/Cargo.toml +++ /dev/null @@ -1,28 +0,0 @@ -[package] -name = "cultcache-rs" -version = "0.1.0" -edition = "2024" -publish = false - -[lib] -name = "cultcache_rs" -path = "src/lib.rs" - -[dependencies] -anyhow = "1" -chrono = { version = "0.4", features = ["serde"] } -cultcache-rs-derive = { path = "cultcache-rs-derive" } -fs2 = "0.4" -redb = "4.1.0" -rmp-serde = "1.3" -serde = { version = "1", features = ["derive"] } -serde_bytes = "0.11" -serde_json = "1" -uuid = { version = "1", features = ["v4"] } - -[dev-dependencies] -pretty_assertions = "1.4" -tempfile = "3" - -[target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Storage_FileSystem"] } diff --git a/vendor/cultcache-rs/README.md b/vendor/cultcache-rs/README.md deleted file mode 100644 index b21c324..0000000 --- a/vendor/cultcache-rs/README.md +++ /dev/null @@ -1,332 +0,0 @@ -# CultCache Rust - -`cultcache-rs` is the Rust port of the useful part of GameCult's CultCache: -a polymorphism-aware in-memory cache that remembers through pluggable backing -stores. - -Consumer code should think in domain types: - -```rust -let player = cache.get_required::("player:meta")?; -cache.put("player:meta", &player)?; -``` - -The persistence layer should deal with envelopes, routing, MessagePack payload -bytes, and schema identity. Application code should not paw at loose JSON files -like a sad little bureaucrat with a clipboard. - -## What It Is - -CultCache is a domain cache with persistence adapters. - -- `CultCache` is the query and mutation surface. -- Domain structs implement `DatabaseEntry`. -- Entries are stored behind a `type::key` identity, so multiple entry types - can share a logical key without colliding. -- Each payload is encoded directly from the known `DatabaseEntry` type into - MessagePack bytes. -- Backing stores are adapters, not the public data model. -- Writes persist to the resolved backing store before the in-memory cache is - updated. -- Type-specific backing stores beat generic backing stores. -- `SingleFileMessagePackBackingStore` is the first concrete store and guards - file access with a sidecar lock file. - -This is not an ORM, not a database, and not distributed consensus in a novelty -hat. If multiple processes write the same backing file, use an external lock or -a coordinator. - -## Current API - -```rust -use cultcache_rs::{CultCache, DatabaseEntry, SingleFileMessagePackBackingStore}; - -#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache(type = "settings")] -struct Settings { - #[cultcache(key = 0)] - theme: String, - - #[cultcache(key = 1, default)] - retries: u32, -} - -let mut cache = CultCache::new(); -cache.register_entry_type::()?; -cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new("cache.cc")); -cache.pull_all_backing_stores()?; - -cache.put("app", &Settings { - theme: "ash".to_string(), - retries: 3, -})?; - -let settings = cache.get_required::("app")?; -# Ok::<(), anyhow::Error>(()) -``` - -For a closed set of entries, generate a registry instead of repeating yourself: - -```rust -use cultcache_rs::cultcache_registry; - -cultcache_registry!(GameCultEntries { - Settings, - PlayerData, -}); - -let mut cache = CultCache::new(); -cache.register_registry(GameCultEntries)?; -``` - -The important part is that callers retrieve domain values directly: - -```rust -let settings: Settings = cache.get_required("app")?; -let all_settings: Vec = cache.get_all()?; -``` - -The cache is the ergonomic surface. The backing store is the memory prosthetic. - -## How This Maps From Original CultCache - -The original C# CultCache relies on runtime reflection: - -- cacheable models inherit from `DatabaseEntry` -- the cache scans child classes at startup -- `Get(id)` returns a `T` -- optional `INamedEntry` enables name lookup -- optional field/property indexes are registered by member name -- backing stores pull/push/delete `DatabaseEntry` values - -Rust does not have C#-style assembly scanning or runtime subclass discovery. -The closest honest Rust translation is: - -- domain structs implement a marker trait, `DatabaseEntry` -- the derive macro generates a positional array formatter from explicit integer - field slots -- the envelope carries the polymorphic type discriminator -- unknown persisted type ids fail closed instead of constructing arbitrary - runtime types -- a generated registry should eventually replace repetitive manual - registration - -Manual `register_entry_type::()` is still accepted, but entry identity no -longer needs a hand-written impl: `#[derive(DatabaseEntry)]` declares it beside -the domain type. `cultcache_registry!` provides the current generated resolver -surface for a closed entry set. - -## Intended Rust Ergonomics - -The target user-facing shape should be: - -```rust -#[derive(Clone, DatabaseEntry)] -#[cultcache(type = "player")] -pub struct PlayerData { - #[cultcache(key = 0)] - pub id: String, - - #[cultcache(key = 1)] - pub name: String, - - #[cultcache(key = 2)] - pub faction: String, -} - -let mut cache = CultCache::builder() - .with_generated_entries(game_entries()) - .with_generic_store(SingleFileMessagePackBackingStore::new("cache.cc")) - .build()?; - -cache.pull_all_backing_stores()?; -let player = cache.get_required::("player:ari")?; -``` - -The current `cultcache_registry!` macro is deliberately simple. The remaining -ergonomic endpoint is automatic inventory or build-script generation so users -do not have to maintain the registry list by hand. - -The registry is the Rust equivalent of C# reflection and MessagePack resolver -setup. It should know: - -- entry type id -- generated MessagePack/serde formatter path -- Rust type name / schema name -- optional store domain route -- optional name key extractor -- optional secondary indexes -- optional global singleton marker - -In other words: code generation should restore the original CultCache feeling -without pretending Rust has runtime reflection hiding under the floorboards. - -## Current Surface - -- `CultCache::new` -- `register_entry_type::` -- `add_generic_backing_store` -- `add_backing_store` -- `pull_all_backing_stores` -- `get::` -- `get_required::` -- `get_envelope::` -- `get_required_envelope::` -- `get_all::` -- `put::` -- `put_envelope::` -- `update::` -- `delete::` -- `snapshot` -- `SingleFileMessagePackBackingStore` - -## Backing Store Routing - -Generic store: - -```rust -cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new("cache.cc")); -``` - -Type-specific store: - -```rust -cache.add_backing_store( - SingleFileMessagePackBackingStore::new("players.cc"), - ["player"], -); -``` - -When writing a `PlayerData`, the cache checks type-specific stores first. If none -match, it writes to the first generic store. Later matching stores are mirrors. - -This mirrors the C# behavior: - -- specific domain stores own their domain -- the first generic store is the primary generic write target -- later generic stores mirror writes -- this is not multi-master - -## Persistence Semantics - -`put` persists before mutating the in-memory cache. If persistence fails, the -cache does not pretend the write succeeded. - -The envelope is MessagePack, and the payload inside each envelope is also raw -MessagePack bytes encoded from the registered concrete `DatabaseEntry` type. -That avoids the old bootstrap path where payloads were normalized through -`serde_json::Value`. - -For bit-compatible neighbors, that also enables a real fast lane: - -- `get_envelope::()` exports the canonical persisted bytes for a typed entry -- `put_envelope::()` ingests the same envelope into another cache instance - without re-encoding the payload first - -It still decodes once for validation and typed reads. We are not pretending one -`Vec` became metaphysically zero-copy because we believed in it harder. But -the stupid decode/re-encode loop is gone. - -The `DatabaseEntry` derive does not trust Rust source field order as the durable -schema. Every persisted member must declare a stable integer slot: - -```rust -#[derive(Clone, DatabaseEntry)] -#[cultcache(type = "player")] -pub struct PlayerData { - #[cultcache(key = 0)] - pub id: String, - - #[cultcache(key = 1)] - pub name: String, - - #[cultcache(key = 2, default)] - pub level: u32, -} -``` - -The derive emits a tuple/array formatter. Gaps serialize as nil. Missing fields -marked `default` use `Default::default()` when older payloads are read. Deleted -field slots should stay reserved until an explicit store migration rewrites the -data. - -The single-file MessagePack store rewrites an atomic snapshot. That is a sane -starting point for small typed state surfaces, settings, Epiphany agent memory, -heartbeat state, and other compact control-plane data. Large corpora should use -a sharded store or a real database instead of asking one file to become a -warehouse and then acting wounded when physics invoices us. - -The single-file store uses a sidecar lock file for shared reads and exclusive -writes. That protects ordinary multi-process access to the same file. It is -still not a multi-master replication protocol, and it is not a substitute for a -coordinator when higher-level write ordering matters. - -## Near-Term Ergonomic Improvements - -1. **Derive macro** - - `#[derive(DatabaseEntry)]` - - `#[cultcache(type = "...")]` - - `#[cultcache(key = N)]` on every persisted field - - tuple/array formatter generation landed - - optional `#[cultcache(name)]`, `#[cultcache(index)]`, and - `#[cultcache(global)]` are still future work - -2. **Generated registry** - - a `CultCacheRegistry` trait - - `cache.register_registry(GameCultEntries)` - - basic macro registry landed - - automatic inventory/build-script generation and store routes are future work - -3. **Name and index lookups** - - `cache.get_by_name::("Potion")` - - `cache.get_by_index::("faction", "Lucent")` - - generated extractors instead of reflection strings - -4. **Schema/version metadata** - - schema version in the envelope or payload metadata - - explicit migration hooks - - refusal on unknown persisted entry types unless a migration/resolver is - installed - -5. **Projection helpers** - - JSON projection for review and git diffs - - vector stripping / large-field elision for agent memory surfaces - -## Why Not Skip Registration Immediately? - -Rust can only deserialize polymorphic data into concrete types if something -maps the persisted `type` discriminator to the Rust type. In C#, reflection and -MessagePack resolvers can discover a closed `DatabaseEntry` inheritance tree at -runtime and route it through known formatters. In Rust, that map has to come from -somewhere: - -- explicit registration -- a generated registry -- a macro inventory crate -- a hand-written resolver - -Explicit registration is the simplest honest implementation. Generated registry -is the ergonomic destination. Hand-written resolver tables are the punishment -we give ourselves if we get lazy. - -## Security Model - -CultCache should never parse arbitrary structured data into arbitrary runtime -types. It only accepts persisted envelopes whose `type` discriminator resolves -to a known `DatabaseEntry` type registered in the cache or generated registry. - -That is the Rust version of the original trick: - -- C# refuses to store objects outside the `DatabaseEntry` root type. -- Runtime-visible `DatabaseEntry` subclasses get dedicated formatters. -- Unknown or unregistered types are rejected. -- MessagePack payload bytes deserialize through known concrete formatters, not - an open-world type loader or generic structured-value bridge. - -The derive/registry work should preserve that shape. Its job is to remove -manual registration ceremony, not to loosen the closed-world boundary. - -## License - -Private GameCult infrastructure for now. Public packaging can wait until the -API grows enough teeth to deserve strangers. diff --git a/vendor/cultcache-rs/cultcache-rs-derive/Cargo.toml b/vendor/cultcache-rs/cultcache-rs-derive/Cargo.toml deleted file mode 100644 index 74d52a4..0000000 --- a/vendor/cultcache-rs/cultcache-rs-derive/Cargo.toml +++ /dev/null @@ -1,13 +0,0 @@ -[package] -name = "cultcache-rs-derive" -version = "0.1.0" -edition = "2024" -publish = false - -[lib] -proc-macro = true - -[dependencies] -proc-macro2 = "1" -quote = "1" -syn = { version = "2", features = ["derive", "parsing"] } diff --git a/vendor/cultcache-rs/cultcache-rs-derive/src/lib.rs b/vendor/cultcache-rs/cultcache-rs-derive/src/lib.rs deleted file mode 100644 index f05a23f..0000000 --- a/vendor/cultcache-rs/cultcache-rs-derive/src/lib.rs +++ /dev/null @@ -1,243 +0,0 @@ -use proc_macro::TokenStream; -use quote::format_ident; -use quote::quote; -use syn::Data; -use syn::DeriveInput; -use syn::Fields; -use syn::LitInt; -use syn::LitStr; -use syn::Type; -use syn::parse_macro_input; - -#[proc_macro_derive(DatabaseEntry, attributes(cultcache))] -pub fn derive_database_entry(input: TokenStream) -> TokenStream { - let input = parse_macro_input!(input as DeriveInput); - let ident = input.ident.clone(); - let mut entry_type: Option = None; - let mut schema_name: Option = None; - - for attribute in input - .attrs - .iter() - .filter(|attr| attr.path().is_ident("cultcache")) - { - let parse_result = attribute.parse_nested_meta(|meta| { - if meta.path.is_ident("type") { - entry_type = Some(meta.value()?.parse()?); - Ok(()) - } else if meta.path.is_ident("schema") { - schema_name = Some(meta.value()?.parse()?); - Ok(()) - } else { - Err(meta.error("unsupported cultcache attribute")) - } - }); - if let Err(error) = parse_result { - return error.to_compile_error().into(); - } - } - - let Some(entry_type) = entry_type else { - return syn::Error::new_spanned( - ident, - "DatabaseEntry derive requires #[cultcache(type = \"...\")]", - ) - .to_compile_error() - .into(); - }; - let schema_name = schema_name.unwrap_or_else(|| LitStr::new(&ident.to_string(), ident.span())); - - let fields = match input.data { - Data::Struct(data) => match data.fields { - Fields::Named(fields) => fields.named, - _ => { - return syn::Error::new_spanned( - ident, - "DatabaseEntry derive requires a named struct", - ) - .to_compile_error() - .into(); - } - }, - _ => { - return syn::Error::new_spanned(ident, "DatabaseEntry derive requires a struct") - .to_compile_error() - .into(); - } - }; - - let mut slots: Vec = Vec::new(); - for field in fields { - let Some(field_ident) = field.ident else { - return syn::Error::new_spanned(field.ty, "DatabaseEntry derive requires named fields") - .to_compile_error() - .into(); - }; - let mut key: Option = None; - let mut default = false; - for attribute in field - .attrs - .iter() - .filter(|attr| attr.path().is_ident("cultcache")) - { - let parse_result = attribute.parse_nested_meta(|meta| { - if meta.path.is_ident("key") { - let value: LitInt = meta.value()?.parse()?; - key = Some(value.base10_parse()?); - Ok(()) - } else if meta.path.is_ident("default") { - default = true; - Ok(()) - } else { - Err(meta.error("unsupported field cultcache attribute")) - } - }); - if let Err(error) = parse_result { - return error.to_compile_error().into(); - } - } - let Some(key) = key else { - return syn::Error::new_spanned( - field_ident, - "DatabaseEntry fields require #[cultcache(key = N)]", - ) - .to_compile_error() - .into(); - }; - if slots.iter().any(|slot| slot.key == key) { - return syn::Error::new_spanned( - field_ident, - format!("duplicate DatabaseEntry slot {key}"), - ) - .to_compile_error() - .into(); - } - slots.push(CultCacheSlot { - key, - ident: field_ident, - ty: field.ty, - default, - }); - } - slots.sort_by_key(|slot| slot.key); - let max_slot = slots.iter().map(|slot| slot.key).max().unwrap_or(0); - - let serialize_elements = (0..=max_slot).map(|slot| { - if let Some(field) = slots.iter().find(|field| field.key == slot) { - let field_ident = &field.ident; - quote! { - tuple.serialize_element(&self.#field_ident)?; - } - } else { - quote! { - tuple.serialize_element(&())?; - } - } - }); - - let option_declarations = slots.iter().map(|field| { - let variable = format_ident!("slot_{}", field.key); - let ty = &field.ty; - quote! { - let mut #variable: ::std::option::Option<#ty> = ::std::option::Option::None; - } - }); - - let deserialize_match_arms = (0..=max_slot).map(|slot| { - if let Some(field) = slots.iter().find(|field| field.key == slot) { - let variable = format_ident!("slot_{}", field.key); - let ty = &field.ty; - quote! { - #slot => { - #variable = seq.next_element::<#ty>()?; - } - } - } else { - quote! { - #slot => { - let _ = seq.next_element::<::serde::de::IgnoredAny>()?; - } - } - } - }); - - let build_fields = slots.iter().map(|field| { - let field_ident = &field.ident; - let variable = format_ident!("slot_{}", field.key); - if field.default { - quote! { - #field_ident: #variable.unwrap_or_default() - } - } else { - let field_name = field_ident.to_string(); - quote! { - #field_ident: #variable.ok_or_else(|| ::serde::de::Error::missing_field(#field_name))? - } - } - }); - - quote! { - impl ::cultcache_rs::DatabaseEntry for #ident { - const TYPE: &'static str = #entry_type; - const SCHEMA_NAME: &'static str = #schema_name; - } - - impl ::serde::Serialize for #ident { - fn serialize(&self, serializer: S) -> ::std::result::Result - where - S: ::serde::Serializer, - { - use ::serde::ser::SerializeTuple; - let mut tuple = serializer.serialize_tuple(#max_slot + 1)?; - #(#serialize_elements)* - tuple.end() - } - } - - impl<'de> ::serde::Deserialize<'de> for #ident { - fn deserialize(deserializer: D) -> ::std::result::Result - where - D: ::serde::Deserializer<'de>, - { - struct CultCacheVisitor; - - impl<'de> ::serde::de::Visitor<'de> for CultCacheVisitor { - type Value = #ident; - - fn expecting(&self, formatter: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { - formatter.write_str(concat!("a CultCache array for ", stringify!(#ident))) - } - - fn visit_seq(self, mut seq: A) -> ::std::result::Result - where - A: ::serde::de::SeqAccess<'de>, - { - #(#option_declarations)* - let mut index = 0usize; - while index <= #max_slot { - match index { - #(#deserialize_match_arms)* - _ => unreachable!(), - } - index += 1; - } - while let ::std::option::Option::Some(_) = seq.next_element::<::serde::de::IgnoredAny>()? {} - Ok(#ident { - #(#build_fields),* - }) - } - } - - deserializer.deserialize_tuple(#max_slot + 1, CultCacheVisitor) - } - } - } - .into() -} - -struct CultCacheSlot { - key: usize, - ident: syn::Ident, - ty: Type, - default: bool, -} diff --git a/vendor/cultcache-rs/examples/cultcache_interop.rs b/vendor/cultcache-rs/examples/cultcache_interop.rs deleted file mode 100644 index 4049acf..0000000 --- a/vendor/cultcache-rs/examples/cultcache_interop.rs +++ /dev/null @@ -1,121 +0,0 @@ -use anyhow::Result; -use anyhow::anyhow; -use cultcache_rs::CultCache; -use cultcache_rs::DatabaseEntry; -use cultcache_rs::SingleFileMessagePackBackingStore; -use serde_json::json; -use std::collections::BTreeMap; - -const INTEROP_TYPE: &str = "cultcache.interop-note"; -const INTEROP_SCHEMA_VERSION: &str = "cultcache.interop_note.v1"; - -#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache(type = "cultcache.interop-note", schema = "CultCacheInteropNote")] -struct CultCacheInteropNote { - #[cultcache(key = 0)] - schema_version: String, - #[cultcache(key = 1)] - document_id: String, - #[cultcache(key = 2)] - author_runtime_id: String, - #[cultcache(key = 3)] - title: String, - #[cultcache(key = 4)] - body: String, - #[cultcache(key = 5, default)] - tags: Vec, -} - -fn main() -> Result<()> { - let mut args = std::env::args().skip(1); - let mode = args - .next() - .ok_or_else(|| anyhow!("expected mode: write | read"))?; - let options = parse_args(args.collect()); - let file = require_arg(&options, "file")?; - - match mode.as_str() { - "write" => write_note(file, require_arg(&options, "runtime-id")?)?, - "read" => read_note(file)?, - _ => return Err(anyhow!("unknown mode {mode}")), - } - Ok(()) -} - -fn write_note(file: &str, runtime_id: &str) -> Result<()> { - let mut cache = build_cache(file)?; - cache.pull_all_backing_stores()?; - let note = CultCacheInteropNote { - schema_version: INTEROP_SCHEMA_VERSION.to_string(), - document_id: format!("note:{runtime_id}"), - author_runtime_id: runtime_id.to_string(), - title: format!("{runtime_id} wrote a CultCache note"), - body: "The v1 store format is the contract.".to_string(), - tags: vec![ - runtime_id.to_string(), - "rust".to_string(), - "interop".to_string(), - ], - }; - cache.put(¬e.document_id, ¬e)?; - print_note(¬e) -} - -fn read_note(file: &str) -> Result<()> { - let mut cache = build_cache(file)?; - cache.pull_all_backing_stores()?; - let notes = cache.get_all::()?; - let note = notes - .first() - .ok_or_else(|| anyhow!("no {INTEROP_TYPE} records found"))?; - print_note(note) -} - -fn build_cache(file: &str) -> Result { - let mut cache = CultCache::new(); - cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(file)); - Ok(cache) -} - -fn print_note(note: &CultCacheInteropNote) -> Result<()> { - println!( - "{}", - serde_json::to_string(&json!({ - "schemaVersion": note.schema_version, - "documentId": note.document_id, - "authorRuntimeId": note.author_runtime_id, - "title": note.title, - "body": note.body, - "tags": note.tags, - }))? - ); - Ok(()) -} - -fn parse_args(args: Vec) -> BTreeMap { - let mut parsed = BTreeMap::new(); - let mut index = 0; - while index < args.len() { - let token = &args[index]; - if !token.starts_with("--") { - index += 1; - continue; - } - let name = token.trim_start_matches("--").to_string(); - let value = args - .get(index + 1) - .cloned() - .unwrap_or_else(|| panic!("missing value for --{name}")); - parsed.insert(name, value); - index += 2; - } - parsed -} - -fn require_arg<'a>(options: &'a BTreeMap, name: &str) -> Result<&'a str> { - options - .get(name) - .map(String::as_str) - .ok_or_else(|| anyhow!("missing required argument --{name}")) -} diff --git a/vendor/cultcache-rs/src/lib.rs b/vendor/cultcache-rs/src/lib.rs deleted file mode 100644 index 0532ca6..0000000 --- a/vendor/cultcache-rs/src/lib.rs +++ /dev/null @@ -1,1080 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; -use serde::Serialize; -use serde::de::DeserializeOwned; -use std::collections::BTreeMap; -use std::collections::BTreeSet; -use std::fs; -use std::fs::File; -use std::fs::OpenOptions; -use std::path::Path; -use std::path::PathBuf; - -mod owned_redb; - -extern crate self as cultcache_rs; - -pub use cultcache_rs_derive::DatabaseEntry; -pub use owned_redb::OwnedRedbMessagePackBackingStore; - -pub trait DatabaseEntry: Serialize + DeserializeOwned + Clone + Send + 'static { - const TYPE: &'static str; - const SCHEMA_NAME: &'static str = "DatabaseEntry"; -} - -pub trait CultCacheRegistry { - fn register_entries(&self, cache: &mut CultCache) -> Result<()>; -} - -#[macro_export] -macro_rules! cultcache_registry { - ($name:ident { $($entry:ty),* $(,)? }) => { - #[derive(Clone, Copy, Debug, Default)] - pub struct $name; - - impl $crate::CultCacheRegistry for $name { - fn register_entries(&self, cache: &mut $crate::CultCache) -> ::anyhow::Result<()> { - $( - cache.register_entry_type::<$entry>()?; - )* - Ok(()) - } - } - }; -} - -#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultCacheEnvelope { - pub key: String, - #[serde(rename = "type")] - pub r#type: String, - #[serde(with = "serde_bytes")] - pub payload: Vec, - pub stored_at: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub schema_id: Option, -} - -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -struct PersistedStoreSnapshot( - String, - Vec, - Vec, -); - -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -struct PersistedSchemaCatalogEntry( - String, - String, - String, - String, - String, - Vec, - Vec, -); - -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -struct PersistedSchemaCatalogMember( - u32, - String, - String, - bool, - bool, - Option, - bool, - Option, -); - -#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] -struct PersistedRecord( - String, - String, - String, - #[serde(with = "serde_bytes")] Vec, -); - -#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] -pub struct PushAllOptions { - pub soft: bool, -} - -pub trait CacheBackingStore: Send { - fn pull_all(&self) -> Result>; - fn push(&mut self, entry: &CultCacheEnvelope) -> Result<()>; - fn delete(&mut self, entry: &CultCacheEnvelope) -> Result<()>; - - fn push_all(&mut self, entries: &[CultCacheEnvelope], _options: PushAllOptions) -> Result<()> { - let existing = self.pull_all()?; - for entry in existing { - self.delete(&entry)?; - } - for entry in entries { - self.push(entry)?; - } - Ok(()) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct SingleFileMessagePackBackingStore { - path: PathBuf, -} - -impl SingleFileMessagePackBackingStore { - pub fn new(path: impl Into) -> Self { - Self { path: path.into() } - } - - pub fn path(&self) -> &Path { - &self.path - } - - /// Holds the pre-created sibling lock shared while a read-only consumer - /// evaluates and acts on one snapshot. The reader never creates or writes - /// either file, so an authority owner can grant only read access. Writers - /// cannot replace the snapshot until the action returns. - pub fn with_read_only_shared_snapshot( - &self, - action: impl FnOnce(Vec) -> Result, - ) -> Result { - self.with_read_only_shared_snapshot_and_unlock_diagnostic(action, |_| {}) - } - - pub fn with_read_only_shared_snapshot_and_unlock_diagnostic( - &self, - action: impl FnOnce(Vec) -> Result, - on_unlock_failure: impl FnOnce(anyhow::Error), - ) -> Result { - self.with_read_only_shared_snapshot_using( - action, - |lock| { - fs2::FileExt::unlock(lock) - .with_context(|| format!("failed to unlock {}", self.lock_path().display())) - }, - on_unlock_failure, - ) - } - - fn with_read_only_shared_snapshot_using( - &self, - action: impl FnOnce(Vec) -> Result, - unlock: U, - on_unlock_failure: D, - ) -> Result - where - U: FnOnce(&File) -> Result<()>, - D: FnOnce(anyhow::Error), - { - let lock_path = self.lock_path(); - let lock = OpenOptions::new() - .read(true) - .open(&lock_path) - .with_context(|| format!("failed to open pre-created {}", lock_path.display()))?; - fs2::FileExt::lock_shared(&lock) - .with_context(|| format!("failed to lock {}", lock_path.display()))?; - let guard = ReadOnlySharedLockGuard::new(lock, unlock, on_unlock_failure); - let result = self.read_all_unlocked().and_then(action); - drop(guard); - result - } - - fn read_all_unlocked(&self) -> Result> { - if !self.path.exists() { - return Ok(Vec::new()); - } - let bytes = fs::read(&self.path) - .with_context(|| format!("failed to read {}", self.path.display()))?; - if bytes.is_empty() { - return Ok(Vec::new()); - } - decode_store_snapshot(&bytes) - .or_else(|_| rmp_serde::from_slice(&bytes)) - .with_context(|| format!("failed to decode MessagePack {}", self.path.display())) - } - - fn write_all_unlocked(&self, entries: &[CultCacheEnvelope]) -> Result<()> { - if let Some(parent) = self.path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("failed to create {}", parent.display()))?; - } - let bytes = rmp_serde::to_vec(&encode_store_snapshot(entries)) - .context("failed to encode MessagePack")?; - let tmp_path = temporary_path_for(&self.path); - fs::write(&tmp_path, bytes) - .with_context(|| format!("failed to write {}", tmp_path.display()))?; - if self.path.exists() { - fs::remove_file(&self.path) - .with_context(|| format!("failed to replace {}", self.path.display()))?; - } - fs::rename(&tmp_path, &self.path).with_context(|| { - format!( - "failed to move {} to {}", - tmp_path.display(), - self.path.display() - ) - })?; - Ok(()) - } - - fn with_shared_lock(&self, action: impl FnOnce() -> Result) -> Result { - let lock = self.open_lock_file()?; - fs2::FileExt::lock_shared(&lock) - .with_context(|| format!("failed to lock {}", self.lock_path().display()))?; - let result = action(); - fs2::FileExt::unlock(&lock) - .with_context(|| format!("failed to unlock {}", self.lock_path().display()))?; - result - } - - fn with_exclusive_lock(&self, action: impl FnOnce() -> Result) -> Result { - let lock = self.open_lock_file()?; - fs2::FileExt::lock_exclusive(&lock) - .with_context(|| format!("failed to lock {}", self.lock_path().display()))?; - let result = action(); - fs2::FileExt::unlock(&lock) - .with_context(|| format!("failed to unlock {}", self.lock_path().display()))?; - result - } - - fn open_lock_file(&self) -> Result { - let lock_path = self.lock_path(); - if let Some(parent) = lock_path.parent() { - fs::create_dir_all(parent) - .with_context(|| format!("failed to create {}", parent.display()))?; - } - OpenOptions::new() - .create(true) - .read(true) - .truncate(false) - .write(true) - .open(&lock_path) - .with_context(|| format!("failed to open {}", lock_path.display())) - } - - fn lock_path(&self) -> PathBuf { - let mut lock_name = self - .path - .file_name() - .map(|value| value.to_os_string()) - .unwrap_or_else(|| "cultcache.cc".into()); - lock_name.push(".lock"); - self.path.with_file_name(lock_name) - } -} - -struct ReadOnlySharedLockGuard -where - U: FnOnce(&File) -> Result<()>, - D: FnOnce(anyhow::Error), -{ - lock: Option, - unlock: Option, - on_unlock_failure: Option, -} - -impl ReadOnlySharedLockGuard -where - U: FnOnce(&File) -> Result<()>, - D: FnOnce(anyhow::Error), -{ - fn new(lock: File, unlock: U, on_unlock_failure: D) -> Self { - Self { - lock: Some(lock), - unlock: Some(unlock), - on_unlock_failure: Some(on_unlock_failure), - } - } -} - -impl Drop for ReadOnlySharedLockGuard -where - U: FnOnce(&File) -> Result<()>, - D: FnOnce(anyhow::Error), -{ - fn drop(&mut self) { - let (Some(lock), Some(unlock)) = (self.lock.as_ref(), self.unlock.take()) else { - return; - }; - if let Err(error) = unlock(lock) - && let Some(on_unlock_failure) = self.on_unlock_failure.take() - { - on_unlock_failure(error); - } - } -} - -impl CacheBackingStore for SingleFileMessagePackBackingStore { - fn pull_all(&self) -> Result> { - self.with_shared_lock(|| self.read_all_unlocked()) - } - - fn push(&mut self, entry: &CultCacheEnvelope) -> Result<()> { - self.with_exclusive_lock(|| { - let mut entries = self.read_all_unlocked()?; - entries.retain(|candidate| entry_id(candidate) != entry_id(entry)); - entries.push(entry.clone()); - entries.sort_by_key(entry_id); - self.write_all_unlocked(&entries) - }) - } - - fn delete(&mut self, entry: &CultCacheEnvelope) -> Result<()> { - self.with_exclusive_lock(|| { - let mut entries = self.read_all_unlocked()?; - entries.retain(|candidate| entry_id(candidate) != entry_id(entry)); - self.write_all_unlocked(&entries) - }) - } - - fn push_all(&mut self, entries: &[CultCacheEnvelope], _options: PushAllOptions) -> Result<()> { - self.with_exclusive_lock(|| { - let mut entries = entries.to_vec(); - entries.sort_by_key(entry_id); - self.write_all_unlocked(&entries) - }) - } -} - -struct CultCacheStoreRegistration { - store: Box, - types: BTreeSet, -} - -pub struct CultCache { - definitions: BTreeMap, - entries: BTreeMap, - stores: Vec, -} - -impl CultCache { - pub fn new() -> Self { - Self { - definitions: BTreeMap::new(), - entries: BTreeMap::new(), - stores: Vec::new(), - } - } - - pub fn register_entry_type(&mut self) -> Result<()> { - if T::TYPE.trim().is_empty() { - return Err(anyhow!( - "CultCache entry types must declare a non-empty type" - )); - } - if let Some(existing_schema) = self.definitions.get(T::TYPE) - && *existing_schema != T::SCHEMA_NAME - { - return Err(anyhow!( - "CultCache already has a different definition registered for type {:?}", - T::TYPE - )); - } - self.definitions.insert(T::TYPE.to_string(), T::SCHEMA_NAME); - Ok(()) - } - - pub fn register_document_type(&mut self) -> Result<()> { - self.register_entry_type::() - } - - pub fn register_registry(&mut self, registry: R) -> Result<&mut Self> { - registry.register_entries(self)?; - Ok(self) - } - - pub fn add_backing_store( - &mut self, - store: impl CacheBackingStore + 'static, - types: impl IntoIterator>, - ) { - self.stores.push(CultCacheStoreRegistration { - store: Box::new(store), - types: types.into_iter().map(Into::into).collect(), - }); - } - - pub fn add_generic_backing_store(&mut self, store: impl CacheBackingStore + 'static) { - self.add_backing_store(store, Vec::::new()); - } - - pub fn pull_all_backing_stores(&mut self) -> Result<()> { - self.entries.clear(); - let known_types: BTreeSet = self.definitions.keys().cloned().collect(); - for registration in &mut self.stores { - for entry in registration.store.pull_all()? { - if !known_types.contains(&entry.r#type) { - return Err(anyhow!( - "No schema is registered for persisted entry type {:?}", - entry.r#type - )); - } - self.entries.insert(entry_id(&entry), entry); - } - } - Ok(()) - } - - pub fn get(&self, key: &str) -> Result> { - self.require_entry_type::()?; - let Some(entry) = self.entries.get(&entry_id_parts(T::TYPE, key)) else { - return Ok(None); - }; - let payload = rmp_serde::from_slice(&entry.payload).with_context(|| { - format!( - "failed to decode CultCache entry {:?} at key {:?} as {}", - T::TYPE, - key, - T::SCHEMA_NAME - ) - })?; - Ok(Some(payload)) - } - - pub fn get_required(&self, key: &str) -> Result { - self.get::(key)? - .ok_or_else(|| anyhow!("CultCache has no {:?} entry at key {:?}", T::TYPE, key)) - } - - pub fn get_envelope(&self, key: &str) -> Result> { - self.require_entry_type::()?; - Ok(self.entries.get(&entry_id_parts(T::TYPE, key)).cloned()) - } - - pub fn get_required_envelope(&self, key: &str) -> Result { - self.get_envelope::(key)? - .ok_or_else(|| anyhow!("CultCache has no {:?} envelope at key {:?}", T::TYPE, key)) - } - - pub fn get_all(&self) -> Result> { - self.require_entry_type::()?; - let mut values = Vec::new(); - for entry in self.entries.values() { - if entry.r#type != T::TYPE { - continue; - } - values.push(rmp_serde::from_slice(&entry.payload).with_context(|| { - format!( - "failed to decode CultCache entry {:?} at key {:?} as {}", - T::TYPE, - entry.key, - T::SCHEMA_NAME - ) - })?); - } - Ok(values) - } - - pub fn put(&mut self, key: impl Into, value: &T) -> Result { - self.require_entry_type::()?; - let key = key.into(); - let payload = rmp_serde::to_vec(value).with_context(|| { - format!( - "failed to encode CultCache entry {:?} at key {:?} as {}", - T::TYPE, - key, - T::SCHEMA_NAME - ) - })?; - let parsed: T = rmp_serde::from_slice(&payload).with_context(|| { - format!( - "failed to validate CultCache entry {:?} at key {:?} as {}", - T::TYPE, - key, - T::SCHEMA_NAME - ) - })?; - let entry = CultCacheEnvelope { - key: key.clone(), - r#type: T::TYPE.to_string(), - payload, - stored_at: now_utc_second(), - schema_id: Some(T::TYPE.to_string()), - }; - let route = self.resolve_route_indices(T::TYPE); - let Some(primary_index) = route.first().copied() else { - return Err(anyhow!( - "No backing store is registered for entry type {:?}", - T::TYPE - )); - }; - self.stores[primary_index].store.push(&entry)?; - for mirror_index in route.iter().skip(1).copied() { - self.stores[mirror_index].store.push(&entry)?; - } - self.entries.insert(entry_id(&entry), entry); - Ok(parsed) - } - - pub fn put_envelope(&mut self, entry: CultCacheEnvelope) -> Result { - self.require_entry_type::()?; - if entry.r#type != T::TYPE { - return Err(anyhow!( - "CultCache envelope type {:?} does not match registered Rust type {:?}", - entry.r#type, - T::TYPE - )); - } - if entry.key.trim().is_empty() { - return Err(anyhow!( - "CultCache envelope keys for type {:?} must be non-empty", - T::TYPE - )); - } - if entry.stored_at.trim().is_empty() { - return Err(anyhow!( - "CultCache envelope stored_at for type {:?} must be non-empty", - T::TYPE - )); - } - - let parsed: T = rmp_serde::from_slice(&entry.payload).with_context(|| { - format!( - "failed to validate CultCache envelope {:?} at key {:?} as {}", - T::TYPE, - entry.key, - T::SCHEMA_NAME - ) - })?; - let route = self.resolve_route_indices(T::TYPE); - let Some(primary_index) = route.first().copied() else { - return Err(anyhow!( - "No backing store is registered for entry type {:?}", - T::TYPE - )); - }; - self.stores[primary_index].store.push(&entry)?; - for mirror_index in route.iter().skip(1).copied() { - self.stores[mirror_index].store.push(&entry)?; - } - self.entries.insert(entry_id(&entry), entry); - Ok(parsed) - } - - pub fn put_raw_envelope(&mut self, entry: CultCacheEnvelope) -> Result<()> { - if !self.definitions.contains_key(&entry.r#type) { - return Err(anyhow!( - "No entry type registered for CultCache envelope type {:?}", - entry.r#type - )); - } - if entry.key.trim().is_empty() { - return Err(anyhow!( - "CultCache envelope keys for type {:?} must be non-empty", - entry.r#type - )); - } - if entry.stored_at.trim().is_empty() { - return Err(anyhow!( - "CultCache envelope stored_at for type {:?} must be non-empty", - entry.r#type - )); - } - let route = self.resolve_route_indices(&entry.r#type); - let Some(primary_index) = route.first().copied() else { - return Err(anyhow!( - "No backing store is registered for entry type {:?}", - entry.r#type - )); - }; - self.stores[primary_index].store.push(&entry)?; - for mirror_index in route.iter().skip(1).copied() { - self.stores[mirror_index].store.push(&entry)?; - } - self.entries.insert(entry_id(&entry), entry); - Ok(()) - } - - pub fn update(&mut self, key: &str, updater: F) -> Result - where - T: DatabaseEntry, - F: FnOnce(Option) -> T, - { - let current = self.get::(key)?; - self.put::(key.to_string(), &updater(current)) - } - - pub fn delete(&mut self, key: &str) -> Result { - self.require_entry_type::()?; - let id = entry_id_parts(T::TYPE, key); - let Some(entry) = self.entries.get(&id).cloned() else { - return Ok(false); - }; - let route = self.resolve_route_indices(T::TYPE); - let Some(primary_index) = route.first().copied() else { - return Err(anyhow!( - "No backing store is registered for entry type {:?}", - T::TYPE - )); - }; - self.stores[primary_index].store.delete(&entry)?; - for mirror_index in route.iter().skip(1).copied() { - self.stores[mirror_index].store.delete(&entry)?; - } - self.entries.remove(&id); - Ok(true) - } - - pub fn snapshot(&self) -> Vec { - self.entries.values().cloned().collect() - } - - fn require_entry_type(&self) -> Result<()> { - match self.definitions.get(T::TYPE) { - Some(schema_name) if *schema_name == T::SCHEMA_NAME => Ok(()), - _ => Err(anyhow!( - "CultCache entry type {:?} is not registered on this cache instance", - T::TYPE - )), - } - } - - fn resolve_route_indices(&self, type_id: &str) -> Vec { - let type_specific: Vec = self - .stores - .iter() - .enumerate() - .filter_map(|(index, registration)| { - registration.types.contains(type_id).then_some(index) - }) - .collect(); - if !type_specific.is_empty() { - return type_specific; - } - self.stores - .iter() - .enumerate() - .filter_map(|(index, registration)| registration.types.is_empty().then_some(index)) - .collect() - } -} - -impl Default for CultCache { - fn default() -> Self { - Self::new() - } -} - -fn entry_id(entry: &CultCacheEnvelope) -> String { - entry_id_parts(&entry.r#type, &entry.key) -} - -fn entry_id_parts(r#type: &str, key: &str) -> String { - format!("{type}::{key}", type = r#type) -} - -fn now_utc_second() -> String { - chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true) -} - -fn encode_store_snapshot(entries: &[CultCacheEnvelope]) -> PersistedStoreSnapshot { - let mut schema_names = BTreeMap::::new(); - for entry in entries { - schema_names.insert( - entry - .schema_id - .clone() - .unwrap_or_else(|| entry.r#type.clone()), - entry.r#type.clone(), - ); - } - - let catalog = schema_names - .into_iter() - .map(|(schema_id, document_type)| { - PersistedSchemaCatalogEntry( - schema_id.clone(), - document_type, - format!("{schema_id}.v1"), - schema_id.clone(), - format!( - "{{\"schemaName\":\"{}\",\"schemaVersion\":\"{}.v1\",\"members\":[]}}", - escape_json_string(&schema_id), - escape_json_string(&schema_id) - ), - vec![schema_id], - Vec::new(), - ) - }) - .collect(); - let records = entries - .iter() - .map(|entry| { - PersistedRecord( - entry.key.clone(), - entry - .schema_id - .clone() - .unwrap_or_else(|| entry.r#type.clone()), - entry.stored_at.clone(), - entry.payload.clone(), - ) - }) - .collect(); - - PersistedStoreSnapshot("cultcache.store.v1".to_string(), catalog, records) -} - -fn decode_store_snapshot(bytes: &[u8]) -> Result> { - let snapshot: PersistedStoreSnapshot = - rmp_serde::from_slice(bytes).context("failed to decode CultCache v1 snapshot")?; - if snapshot.0 != "cultcache.store.v1" { - return Err(anyhow!("unsupported CultCache snapshot {}", snapshot.0)); - } - - let catalog = snapshot - .1 - .into_iter() - .map(|entry| (entry.0, entry.1)) - .collect::>(); - snapshot - .2 - .into_iter() - .map(|record| { - let r#type = catalog.get(&record.1).cloned().ok_or_else(|| { - anyhow!( - "CultCache record {:?} references missing schema {:?}", - record.0, - record.1 - ) - })?; - Ok(CultCacheEnvelope { - key: record.0, - r#type, - stored_at: record.2, - payload: record.3, - schema_id: Some(record.1), - }) - }) - .collect() -} - -fn escape_json_string(value: &str) -> String { - value.replace('\\', "\\\\").replace('"', "\\\"") -} - -fn temporary_path_for(path: &Path) -> PathBuf { - let mut file_name = path - .file_name() - .map(|value| value.to_os_string()) - .unwrap_or_else(|| "cultcache.cc".into()); - file_name.push(format!(".{}.tmp", uuid::Uuid::new_v4())); - path.with_file_name(file_name) -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - use std::{sync::mpsc, time::Duration}; - - #[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] - #[cultcache(type = "settings")] - struct Settings { - #[cultcache(key = 0)] - theme: String, - #[cultcache(key = 1, default)] - retries: u32, - } - - #[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] - #[cultcache(type = "note")] - struct Note { - #[cultcache(key = 0)] - title: String, - #[cultcache(key = 1)] - body: String, - } - - cultcache_registry!(TestEntries { Settings, Note }); - - #[test] - fn read_only_snapshot_holds_the_precreated_writer_lock() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("authority.cc"); - let mut store = SingleFileMessagePackBackingStore::new(&store_path); - let released = CultCacheEnvelope { - key: "traffic".into(), - r#type: "admission".into(), - payload: b"released".to_vec(), - stored_at: "2026-09-02T00:00:00Z".into(), - schema_id: Some("admission.v1".into()), - }; - store.push(&released)?; - - let writer_path = store_path.clone(); - let (started_tx, started_rx) = mpsc::channel(); - let (finished_tx, finished_rx) = mpsc::channel(); - store.with_read_only_shared_snapshot(|captured| { - assert_eq!(captured, vec![released.clone()]); - std::thread::spawn(move || { - started_tx.send(()).unwrap(); - let engaged = CultCacheEnvelope { - payload: b"engaged".to_vec(), - ..released - }; - let mut writer = SingleFileMessagePackBackingStore::new(writer_path); - writer.push(&engaged).unwrap(); - finished_tx.send(()).unwrap(); - }); - started_rx.recv_timeout(Duration::from_secs(1))?; - assert!( - finished_rx - .recv_timeout(Duration::from_millis(100)) - .is_err() - ); - Ok(()) - })?; - finished_rx.recv_timeout(Duration::from_secs(1))?; - store.with_read_only_shared_snapshot(|captured| { - assert_eq!(captured[0].payload, b"engaged"); - Ok(()) - })?; - Ok(()) - } - - #[test] - fn read_only_snapshot_never_creates_a_missing_lock_or_rewrites_the_store() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("authority.cc"); - let mut store = SingleFileMessagePackBackingStore::new(&store_path); - store.push(&CultCacheEnvelope { - key: "traffic".into(), - r#type: "admission".into(), - payload: b"released".to_vec(), - stored_at: "2026-09-02T00:00:00Z".into(), - schema_id: Some("admission.v1".into()), - })?; - let before = fs::read(&store_path)?; - let lock_path = store.lock_path(); - fs::remove_file(&lock_path)?; - - assert!(store.with_read_only_shared_snapshot(|_| Ok(())).is_err()); - assert!(!lock_path.exists()); - assert_eq!(fs::read(&store_path)?, before); - Ok(()) - } - - #[test] - fn familiar_cultcache_flow_persists_and_reloads_typed_documents() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("cache.cc"); - let settings = Settings { - theme: "ash".to_string(), - retries: 3, - }; - - let mut cache = CultCache::new(); - cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path)); - cache.pull_all_backing_stores()?; - cache.put("app", &settings)?; - assert_eq!(cache.get_required::("app")?, settings); - - let mut reloaded = CultCache::new(); - reloaded.register_entry_type::()?; - reloaded.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path)); - reloaded.pull_all_backing_stores()?; - assert_eq!(reloaded.get_required::("app")?, settings); - Ok(()) - } - - #[test] - fn entry_identity_is_polymorphic_by_type_and_key() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("cache.cc"); - let mut cache = CultCache::new(); - cache.register_registry(TestEntries)?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path)); - - cache.put( - "shared", - &Settings { - theme: "green".to_string(), - retries: 1, - }, - )?; - cache.put( - "shared", - &Note { - title: "same key".to_string(), - body: "different type".to_string(), - }, - )?; - - assert_eq!(cache.snapshot().len(), 2); - assert_eq!(cache.get_required::("shared")?.title, "same key"); - assert_eq!(cache.get_required::("shared")?.theme, "green"); - Ok(()) - } - - #[test] - fn type_specific_store_routes_before_generic_store() -> Result<()> { - let temp = tempfile::tempdir()?; - let generic_path = temp.path().join("generic.cc"); - let settings_path = temp.path().join("settings.cc"); - let mut cache = CultCache::new(); - cache.register_entry_type::()?; - cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&generic_path)); - cache.add_backing_store( - SingleFileMessagePackBackingStore::new(&settings_path), - ["settings"], - ); - - cache.put( - "app", - &Settings { - theme: "ash".to_string(), - retries: 3, - }, - )?; - cache.put( - "memo", - &Note { - title: "hello".to_string(), - body: "world".to_string(), - }, - )?; - - let generic_entries = SingleFileMessagePackBackingStore::new(&generic_path).pull_all()?; - let settings_entries = SingleFileMessagePackBackingStore::new(&settings_path).pull_all()?; - assert_eq!(generic_entries[0].r#type, "note"); - assert_eq!(settings_entries[0].r#type, "settings"); - Ok(()) - } - - #[test] - fn update_and_delete_follow_the_cache_api() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("cache.cc"); - let mut cache = CultCache::new(); - cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path)); - - let updated = cache.update::("app", |current| { - let mut current = current.unwrap_or(Settings { - theme: "ash".to_string(), - retries: 0, - }); - current.retries += 1; - current - })?; - assert_eq!(updated.retries, 1); - assert!(cache.delete::("app")?); - assert!(cache.get::("app")?.is_none()); - Ok(()) - } - - #[test] - fn pull_rejects_unregistered_persisted_entry_type() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("cache.cc"); - let mut store = SingleFileMessagePackBackingStore::new(&store_path); - store.push(&CultCacheEnvelope { - key: "unknown".to_string(), - r#type: "unregistered".to_string(), - payload: rmp_serde::to_vec(&1_u8)?, - stored_at: now_utc_second(), - schema_id: Some("unregistered".to_string()), - })?; - - let mut cache = CultCache::new(); - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path)); - let error = cache.pull_all_backing_stores().unwrap_err(); - assert!( - error - .to_string() - .contains("No schema is registered for persisted entry type") - ); - Ok(()) - } - - #[test] - fn payload_is_binary_messagepack_not_json_value() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("cache.cc"); - let mut cache = CultCache::new(); - cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path)); - cache.put( - "app", - &Settings { - theme: "ash".to_string(), - retries: 3, - }, - )?; - - let entry = cache.snapshot().remove(0); - let decoded: Settings = rmp_serde::from_slice(&entry.payload)?; - assert_eq!(decoded.theme, "ash"); - assert!(!entry.payload.is_empty()); - Ok(()) - } - - #[test] - fn corrupted_payload_fails_during_typed_retrieval() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("cache.cc"); - let mut store = SingleFileMessagePackBackingStore::new(&store_path); - store.push(&CultCacheEnvelope { - key: "app".to_string(), - r#type: "settings".to_string(), - payload: vec![0xc1], - stored_at: now_utc_second(), - schema_id: Some("settings".to_string()), - })?; - - let mut cache = CultCache::new(); - cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&store_path)); - cache.pull_all_backing_stores()?; - let error = cache.get_required::("app").unwrap_err(); - assert!( - error - .to_string() - .contains("failed to decode CultCache entry") - ); - Ok(()) - } - - #[test] - fn put_envelope_reuses_existing_messagepack_payload() -> Result<()> { - let temp = tempfile::tempdir()?; - let origin_store = temp.path().join("origin.cc"); - let target_store = temp.path().join("target.cc"); - - let mut origin = CultCache::new(); - origin.register_entry_type::()?; - origin.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&origin_store)); - origin.put( - "app", - &Settings { - theme: "ash".to_string(), - retries: 3, - }, - )?; - - let envelope = origin.get_required_envelope::("app")?; - - let mut target = CultCache::new(); - target.register_entry_type::()?; - target.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store)); - let applied = target.put_envelope::(envelope.clone())?; - - assert_eq!( - applied, - Settings { - theme: "ash".to_string(), - retries: 3, - } - ); - assert_eq!(target.get_required::("app")?, applied); - assert_eq!( - target.get_required_envelope::("app")?.payload, - envelope.payload - ); - Ok(()) - } -} diff --git a/vendor/cultcache-rs/src/owned_redb.rs b/vendor/cultcache-rs/src/owned_redb.rs deleted file mode 100644 index 0bca612..0000000 --- a/vendor/cultcache-rs/src/owned_redb.rs +++ /dev/null @@ -1,564 +0,0 @@ -use crate::{CacheBackingStore, CultCacheEnvelope, PushAllOptions}; -use anyhow::{Context, Result, anyhow, bail}; -use redb::{Database, ReadableDatabase, ReadableTable, TableDefinition}; -use std::collections::BTreeSet; -use std::fs::{self, File, OpenOptions}; -use std::path::{Path, PathBuf}; -use std::sync::Arc; - -const REDB_ENVELOPES: TableDefinition<&[u8], &[u8]> = TableDefinition::new("cultcache_envelopes"); - -struct OwnedRedbInner { - database: Database, - _owner_lock: File, - file_identity: String, -} - -/// A pinned redb store for one long-lived service owner. This handle holds the -/// CultCache external lock, an open file handle, and the redb database for its -/// entire lifetime. Clones share that exact ownership authority. -#[derive(Clone)] -pub struct OwnedRedbMessagePackBackingStore { - path: PathBuf, - inner: Arc, -} - -impl std::fmt::Debug for OwnedRedbMessagePackBackingStore { - fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - formatter - .debug_struct("OwnedRedbMessagePackBackingStore") - .field("path", &self.path) - .field("file_identity", &self.inner.file_identity) - .finish_non_exhaustive() - } -} - -impl OwnedRedbMessagePackBackingStore { - pub fn new(path: impl Into) -> Result { - let path = path.into(); - if let Some(parent) = path - .parent() - .filter(|parent| !parent.as_os_str().is_empty()) - { - fs::create_dir_all(parent) - .with_context(|| format!("failed to create {}", parent.display()))?; - } - let lock_path = redb_lock_path(&path); - let owner_lock = OpenOptions::new() - .create(true) - .read(true) - .truncate(false) - .write(true) - .open(&lock_path) - .with_context(|| format!("failed to open {}", lock_path.display()))?; - fs2::FileExt::try_lock_exclusive(&owner_lock).with_context(|| { - format!( - "redb CultCache {} already has an active owner", - path.display() - ) - })?; - - // Give redb ownership of the already pinned file, structurally closing - // path substitution during database creation. The post-open identity - // comparison separately verifies that the pathname still names it. - let database_file = OpenOptions::new() - .create(true) - .read(true) - .truncate(false) - .write(true) - .open(&path) - .with_context(|| format!("failed to pin redb CultCache {}", path.display()))?; - let held_identity = file_identity_from_file(&database_file)?; - let database = Database::builder() - .create_file(database_file) - .with_context(|| format!("failed to open redb CultCache {}", path.display()))?; - let path_identity_file = File::open(&path)?; - let path_identity = file_identity_from_file(&path_identity_file)?; - if held_identity != path_identity { - bail!( - "redb CultCache path identity changed while opening: held {held_identity}, path {path_identity}" - ); - } - let write = database.begin_write()?; - { - write.open_table(REDB_ENVELOPES)?; - } - write.commit()?; - Ok(Self { - path, - inner: Arc::new(OwnedRedbInner { - database, - _owner_lock: owner_lock, - file_identity: held_identity, - }), - }) - } - - pub fn path(&self) -> &Path { - &self.path - } - - pub fn file_identity(&self) -> &str { - &self.inner.file_identity - } - - pub fn require_file_identity(&self, expected: &str) -> Result<()> { - if expected != self.file_identity() { - bail!( - "redb CultCache file identity mismatch: expected {expected}, owned {}", - self.file_identity() - ); - } - Ok(()) - } - - pub fn validate_path_identity(&self) -> Result<()> { - let current_file = File::open(&self.path) - .with_context(|| format!("owned redb path {} is missing", self.path.display()))?; - let current = file_identity_from_file(¤t_file)?; - self.require_file_identity(¤t).with_context(|| { - format!( - "owned redb path {} no longer names the pinned file", - self.path.display() - ) - }) - } - - pub fn compare_and_swap_batch( - &self, - expected: &[CultCacheEnvelope], - replacements: Vec, - ) -> Result { - if replacements.is_empty() { - return Err(anyhow!( - "conditional batch requires a non-empty replacement set" - )); - } - let expected_ids = unique_batch_ids(expected, "expected")?; - let replacement_ids = unique_batch_ids(&replacements, "replacement")?; - if !expected_ids.is_subset(&replacement_ids) { - return Err(anyhow!( - "conditional batch must replace every expected identity" - )); - } - let write = self.inner.database.begin_write()?; - let matched = { - let mut table = write.open_table(REDB_ENVELOPES)?; - let mut valid = true; - for row in expected { - if read_redb_entry(&table, row)? != Some(row.clone()) { - valid = false; - break; - } - } - if valid { - for row in &replacements { - if !expected_ids.contains(&entry_id(row)) - && read_redb_entry(&table, row)?.is_some() - { - valid = false; - break; - } - } - } - if valid { - for row in &replacements { - insert_redb_entry(&mut table, row)?; - } - } - valid - }; - if matched { - write.commit()?; - } else { - write.abort()?; - } - Ok(matched) - } - - pub fn append_if_snapshot_unchanged( - &self, - expected_snapshot: &[CultCacheEnvelope], - additions: Vec, - ) -> Result { - if additions.is_empty() { - return Err(anyhow!("conditional snapshot append requires additions")); - } - unique_batch_ids(expected_snapshot, "expected snapshot")?; - let addition_ids = unique_batch_ids(&additions, "snapshot additions")?; - let write = self.inner.database.begin_write()?; - let matched = { - let mut table = write.open_table(REDB_ENVELOPES)?; - let mut current = read_all_redb(&table)?; - let mut expected = expected_snapshot.to_vec(); - current.sort_by_key(entry_id); - expected.sort_by_key(entry_id); - let valid = current == expected - && !current - .iter() - .any(|row| addition_ids.contains(&entry_id(row))); - if valid { - for row in &additions { - insert_redb_entry(&mut table, row)?; - } - } - valid - }; - if matched { - write.commit()?; - } else { - write.abort()?; - } - Ok(matched) - } - - pub fn replace_and_append_if_snapshot_unchanged( - &self, - expected_snapshot: &[CultCacheEnvelope], - replacements: Vec, - ) -> Result { - if replacements.is_empty() { - return Err(anyhow!( - "conditional snapshot replacement requires replacements" - )); - } - unique_batch_ids(expected_snapshot, "expected snapshot")?; - unique_batch_ids(&replacements, "snapshot replacements")?; - let write = self.inner.database.begin_write()?; - let matched = { - let mut table = write.open_table(REDB_ENVELOPES)?; - let mut current = read_all_redb(&table)?; - let mut expected = expected_snapshot.to_vec(); - current.sort_by_key(entry_id); - expected.sort_by_key(entry_id); - let valid = current == expected; - if valid { - for row in &replacements { - insert_redb_entry(&mut table, row)?; - } - } - valid - }; - if matched { - write.commit()?; - } else { - write.abort()?; - } - Ok(matched) - } -} - -impl CacheBackingStore for OwnedRedbMessagePackBackingStore { - fn pull_all(&self) -> Result> { - let read = self.inner.database.begin_read()?; - let table = read.open_table(REDB_ENVELOPES)?; - read_all_redb(&table) - } - - fn push(&mut self, entry: &CultCacheEnvelope) -> Result<()> { - let write = self.inner.database.begin_write()?; - { - let mut table = write.open_table(REDB_ENVELOPES)?; - insert_redb_entry(&mut table, entry)?; - } - write.commit()?; - Ok(()) - } - - fn delete(&mut self, entry: &CultCacheEnvelope) -> Result<()> { - let write = self.inner.database.begin_write()?; - { - let mut table = write.open_table(REDB_ENVELOPES)?; - let key = redb_identity(entry)?; - table.remove(key.as_slice())?; - } - write.commit()?; - Ok(()) - } - - fn push_all(&mut self, entries: &[CultCacheEnvelope], _options: PushAllOptions) -> Result<()> { - unique_batch_ids(entries, "push all")?; - let write = self.inner.database.begin_write()?; - { - let mut table = write.open_table(REDB_ENVELOPES)?; - let keys = table - .iter()? - .map(|row| Ok(row?.0.value().to_vec())) - .collect::>>()?; - for key in keys { - table.remove(key.as_slice())?; - } - for entry in entries { - insert_redb_entry(&mut table, entry)?; - } - } - write.commit()?; - Ok(()) - } -} - -fn unique_batch_ids( - entries: &[CultCacheEnvelope], - label: &str, -) -> Result> { - let ids = entries.iter().map(entry_id).collect::>(); - if ids.len() != entries.len() { - return Err(anyhow!( - "conditional batch {label} set contains duplicate identities" - )); - } - Ok(ids) -} - -fn entry_id(entry: &CultCacheEnvelope) -> (String, String) { - (entry.r#type.clone(), entry.key.clone()) -} - -fn redb_lock_path(path: &Path) -> PathBuf { - let mut lock_name = path - .file_name() - .map(|value| value.to_os_string()) - .unwrap_or_else(|| "cultcache.cc".into()); - lock_name.push(".lock"); - path.with_file_name(lock_name) -} - -#[cfg(unix)] -fn file_identity_from_file(file: &File) -> Result { - use std::os::unix::fs::MetadataExt; - let metadata = file.metadata()?; - Ok(format!( - "unix:{:016x}:{:016x}", - metadata.dev(), - metadata.ino() - )) -} - -#[cfg(windows)] -fn file_identity_from_file(file: &File) -> Result { - use std::mem::zeroed; - use std::os::windows::io::AsRawHandle; - use windows_sys::Win32::Storage::FileSystem::{ - BY_HANDLE_FILE_INFORMATION, GetFileInformationByHandle, - }; - let mut information: BY_HANDLE_FILE_INFORMATION = unsafe { zeroed() }; - let success = - unsafe { GetFileInformationByHandle(file.as_raw_handle() as _, &mut information) }; - if success == 0 { - return Err(std::io::Error::last_os_error()) - .context("failed to read Windows file identity"); - } - let index = ((information.nFileIndexHigh as u64) << 32) | information.nFileIndexLow as u64; - Ok(format!( - "windows:{:08x}:{index:016x}", - information.dwVolumeSerialNumber - )) -} - -fn redb_identity(entry: &CultCacheEnvelope) -> Result> { - rmp_serde::to_vec(&entry_id(entry)).context("failed to encode CultCache identity") -} - -fn read_redb_entry( - table: &impl ReadableTable<&'static [u8], &'static [u8]>, - identity: &CultCacheEnvelope, -) -> Result> { - let key = redb_identity(identity)?; - table - .get(key.as_slice())? - .map(|value| { - rmp_serde::from_slice(value.value()).context("failed to decode redb CultCache envelope") - }) - .transpose() -} - -fn insert_redb_entry( - table: &mut redb::Table<&[u8], &[u8]>, - entry: &CultCacheEnvelope, -) -> Result<()> { - let key = redb_identity(entry)?; - let value = rmp_serde::to_vec(entry).context("failed to encode redb CultCache envelope")?; - table.insert(key.as_slice(), value.as_slice())?; - Ok(()) -} - -fn read_all_redb( - table: &impl ReadableTable<&'static [u8], &'static [u8]>, -) -> Result> { - let mut entries = table - .iter()? - .map(|row| { - let (_, value) = row?; - rmp_serde::from_slice(value.value()).context("failed to decode redb CultCache envelope") - }) - .collect::>>()?; - entries.sort_by_key(entry_id); - Ok(entries) -} - -#[cfg(test)] -mod tests { - use super::*; - - fn test_envelope(r#type: &str, key: &str, payload: &[u8]) -> CultCacheEnvelope { - CultCacheEnvelope { - key: key.to_string(), - r#type: r#type.to_string(), - payload: payload.to_vec(), - stored_at: "2026-07-13T00:00:00Z".to_string(), - schema_id: Some(r#type.to_string()), - } - } - - #[test] - fn legacy_redb_identity_remains_tuple_encoded() -> Result<()> { - let entry = test_envelope("model", "current", b"one"); - assert_eq!( - redb_identity(&entry)?, - vec![ - 0x92, 0xa5, b'm', b'o', b'd', b'e', b'l', 0xa7, b'c', b'u', b'r', b'r', b'e', b'n', - b't', - ] - ); - Ok(()) - } - - #[test] - fn owned_redb_clones_share_authority_and_fresh_owner_is_refused() -> Result<()> { - let temp = tempfile::tempdir()?; - let path = temp.path().join("owned.cc"); - let owner = OwnedRedbMessagePackBackingStore::new(&path)?; - let clone = owner.clone(); - assert_eq!(owner.file_identity(), clone.file_identity()); - assert!(OwnedRedbMessagePackBackingStore::new(&path).is_err()); - drop(owner); - assert!(OwnedRedbMessagePackBackingStore::new(&path).is_err()); - drop(clone); - assert!(OwnedRedbMessagePackBackingStore::new(&path).is_ok()); - Ok(()) - } - - #[test] - fn owned_redb_file_identity_is_stable_across_writes_and_reopen() -> Result<()> { - let temp = tempfile::tempdir()?; - let path = temp.path().join("identity.cc"); - let identity = { - let mut owner = OwnedRedbMessagePackBackingStore::new(&path)?; - let identity = owner.file_identity().to_string(); - owner.push(&test_envelope("model", "current", b"one"))?; - owner.validate_path_identity()?; - owner.require_file_identity(&identity)?; - identity - }; - let reopened = OwnedRedbMessagePackBackingStore::new(&path)?; - assert_eq!(reopened.file_identity(), identity); - assert_eq!(reopened.pull_all()?.len(), 1); - Ok(()) - } - - #[test] - fn owned_redb_identity_helper_refuses_mismatch() -> Result<()> { - let temp = tempfile::tempdir()?; - let owner = OwnedRedbMessagePackBackingStore::new(temp.path().join("mismatch.cc"))?; - assert!(owner.require_file_identity("not-the-owned-file").is_err()); - owner.require_file_identity(owner.file_identity())?; - Ok(()) - } - - #[test] - fn owned_redb_batch_refuses_stale_member_and_commits_success_atomically() -> Result<()> { - let temp = tempfile::tempdir()?; - let mut owner = OwnedRedbMessagePackBackingStore::new(temp.path().join("batch-owned.cc"))?; - let first = test_envelope("model", "first", b"one"); - let second = test_envelope("model", "second", b"one"); - owner.push(&first)?; - owner.push(&second)?; - let first_two = test_envelope("model", "first", b"two"); - let second_two = test_envelope("model", "second", b"two"); - let companion = test_envelope("receipt", "batch", b"committed"); - let stale_second = test_envelope("model", "second", b"stale"); - assert!(!owner.compare_and_swap_batch( - &[first.clone(), stale_second], - vec![first_two.clone(), second_two.clone(), companion.clone()], - )?); - assert_eq!(owner.pull_all()?, vec![first.clone(), second.clone()]); - assert!(owner.compare_and_swap_batch( - &[first, second], - vec![first_two.clone(), second_two.clone(), companion.clone()], - )?); - assert_eq!(owner.pull_all()?, vec![first_two, second_two, companion]); - Ok(()) - } - - #[test] - fn owned_redb_snapshot_append_refuses_stale_or_colliding_state() -> Result<()> { - let temp = tempfile::tempdir()?; - let mut owner = - OwnedRedbMessagePackBackingStore::new(temp.path().join("snapshot-append.cc"))?; - let model = test_envelope("model", "current", b"revision-1"); - owner.push(&model)?; - let stale = owner.pull_all()?; - owner.push(&test_envelope("event", "concurrent", b"live"))?; - assert!(!owner.append_if_snapshot_unchanged( - &stale, - vec![test_envelope("readiness", "proof", b"ready")], - )?); - let exact = owner.pull_all()?; - assert!(owner.append_if_snapshot_unchanged( - &exact, - vec![test_envelope("readiness", "proof", b"ready")], - )?); - let current = owner.pull_all()?; - assert!(!owner.append_if_snapshot_unchanged( - ¤t, - vec![test_envelope("model", "current", b"collision")], - )?); - Ok(()) - } - - #[test] - fn owned_redb_snapshot_replacement_and_append_refuses_concurrent_rows() -> Result<()> { - let temp = tempfile::tempdir()?; - let mut owner = - OwnedRedbMessagePackBackingStore::new(temp.path().join("snapshot-replace.cc"))?; - let current = test_envelope("session", "current", b"active"); - owner.push(¤t)?; - let stale = owner.pull_all()?; - let concurrent = test_envelope("event", "concurrent", b"live"); - owner.push(&concurrent)?; - let completed = test_envelope("session", "current", b"completed"); - let receipt = test_envelope("receipt", "terminal", b"done"); - assert!(!owner.replace_and_append_if_snapshot_unchanged( - &stale, - vec![completed.clone(), receipt.clone()], - )?); - let exact = owner.pull_all()?; - assert!(owner.replace_and_append_if_snapshot_unchanged( - &exact, - vec![completed.clone(), receipt.clone()], - )?); - assert_eq!(owner.pull_all()?, vec![concurrent, receipt, completed]); - Ok(()) - } - - #[test] - fn owned_redb_path_replacement_cannot_redirect_pinned_authority() -> Result<()> { - let temp = tempfile::tempdir()?; - let path = temp.path().join("pinned.cc"); - let displaced = temp.path().join("displaced.cc"); - let mut owner = OwnedRedbMessagePackBackingStore::new(&path)?; - let identity = owner.file_identity().to_string(); - owner.push(&test_envelope("model", "before", b"one"))?; - fs::rename(&path, &displaced)?; - File::create(&path)?; - assert!(owner.validate_path_identity().is_err()); - assert_eq!(owner.file_identity(), identity); - owner.push(&test_envelope("model", "after", b"two"))?; - assert_eq!(owner.pull_all()?.len(), 2); - assert_ne!( - file_identity_from_file(&File::open(&path)?)?, - owner.file_identity() - ); - Ok(()) - } -} diff --git a/vendor/cultmesh-rs/Cargo.lock b/vendor/cultmesh-rs/Cargo.lock deleted file mode 100644 index 328295c..0000000 --- a/vendor/cultmesh-rs/Cargo.lock +++ /dev/null @@ -1,1233 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aead" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" -dependencies = [ - "crypto-common", - "generic-array", -] - -[[package]] -name = "aes" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" -dependencies = [ - "cfg-if", - "cipher", - "cpufeatures", -] - -[[package]] -name = "aes-gcm" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" -dependencies = [ - "aead", - "aes", - "cipher", - "ctr", - "ghash", - "subtle", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "cc" -version = "1.2.62" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dce859f0832a7d088c4f1119888ab94ef4b5d6795d1ce05afb7fe159d79f98" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "cipher" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" -dependencies = [ - "crypto-common", - "inout", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crypto-common" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "typenum", -] - -[[package]] -name = "ctr" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" -dependencies = [ - "cipher", -] - -[[package]] -name = "cultcache-rs" -version = "0.1.0" -dependencies = [ - "anyhow", - "chrono", - "cultcache-rs-derive", - "fs2", - "rmp-serde", - "serde", - "serde_bytes", - "serde_json", - "uuid", -] - -[[package]] -name = "cultcache-rs-derive" -version = "0.1.0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "cultmesh-rs" -version = "0.1.0" -dependencies = [ - "anyhow", - "cultcache-rs", - "cultnet-rs", - "pretty_assertions", - "rmp-serde", - "serde", - "tempfile", -] - -[[package]] -name = "cultnet-rs" -version = "0.1.0" -dependencies = [ - "aes-gcm", - "anyhow", - "base64", - "chrono", - "cultcache-rs", - "hmac", - "rand", - "rmp-serde", - "rmpv", - "serde", - "serde_bytes", - "serde_json", - "sha2", - "socket2", - "uuid", -] - -[[package]] -name = "diff" -version = "0.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer", - "crypto-common", - "subtle", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys 0.61.2", -] - -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - -[[package]] -name = "ghash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" -dependencies = [ - "opaque-debug", - "polyval", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "inout" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" -dependencies = [ - "generic-array", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "js-sys" -version = "0.3.99" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "log" -version = "0.4.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5" - -[[package]] -name = "memchr" -version = "2.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "polyval" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" -dependencies = [ - "cfg-if", - "cpufeatures", - "opaque-debug", - "universal-hash", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "pretty_assertions" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" -dependencies = [ - "diff", - "yansi", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "rmp" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" -dependencies = [ - "num-traits", -] - -[[package]] -name = "rmp-serde" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" -dependencies = [ - "rmp", - "serde", -] - -[[package]] -name = "rmpv" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a4e1d4b9b938a26d2996af33229f0ca0956c652c1375067f0b45291c1df8417" -dependencies = [ - "rmp", - "serde", - "serde_bytes", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.61.2", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - -[[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.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix", - "windows-sys 0.61.2", -] - -[[package]] -name = "typenum" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "universal-hash" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" -dependencies = [ - "crypto-common", - "subtle", -] - -[[package]] -name = "uuid" -version = "1.23.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76" -dependencies = [ - "getrandom 0.4.2", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.122" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap", - "semver", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[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" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -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 = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap", - "prettyplease", - "syn", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap", - "log", - "semver", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "yansi" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" - -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/vendor/cultmesh-rs/Cargo.toml b/vendor/cultmesh-rs/Cargo.toml deleted file mode 100644 index 9a719d3..0000000 --- a/vendor/cultmesh-rs/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "cultmesh-rs" -version = "0.1.0" -edition = "2024" -publish = false - -[lib] -name = "cultmesh_rs" -path = "src/lib.rs" - -[dependencies] -anyhow = "1" -cultcache-rs = { path = "../cultcache-rs" } -cultnet-rs = { path = "../cultnet-rs" } -serde = { version = "1", features = ["derive"] } - -[dev-dependencies] -pretty_assertions = "1.4.1" -rmp-serde = "1" -tempfile = "3" diff --git a/vendor/cultmesh-rs/src/lib.rs b/vendor/cultmesh-rs/src/lib.rs deleted file mode 100644 index 5adbb53..0000000 --- a/vendor/cultmesh-rs/src/lib.rs +++ /dev/null @@ -1,1363 +0,0 @@ -use anyhow::{Context, Result}; -use cultcache_rs::CultCache; -use cultcache_rs::CultCacheEnvelope; -use cultcache_rs::DatabaseEntry; -use cultcache_rs::SingleFileMessagePackBackingStore; -use cultnet_rs::CultNetDocumentPutOptions; -use cultnet_rs::CultNetDocumentRegistry; -use cultnet_rs::CultNetRudpSocketTransportConnection; -use cultnet_rs::CultNetRudpSocketTransportOptions; -use cultnet_rs::CultNetWireContract; -use cultnet_rs::decode_cultnet_message_from_slice; -use cultnet_rs::encode_cultnet_message_to_vec; -use serde::Serialize; -use std::collections::BTreeMap; -use std::env; -use std::net::SocketAddr; -use std::net::UdpSocket; -use std::path::Path; -use std::thread; -use std::time::Duration; -use std::time::Instant; - -pub const CULTMESH_RUDP_DOCUMENT_CATALOG_CONNECTION_ID: u32 = 0x0d1d_0002; - -pub trait CultMeshDocumentSet: Clone + Send + Sync + 'static { - fn register_cache(&self, cache: &mut CultCache) -> Result<()>; - fn register_documents(&self, registry: &mut CultNetDocumentRegistry) -> Result<()>; -} - -#[macro_export] -macro_rules! cultmesh_documents { - ($name:ident { $($entry:ty => $schema_version:expr),* $(,)? }) => { - #[derive(Clone, Copy, Debug, Default)] - pub struct $name; - - impl $crate::CultMeshDocumentSet for $name { - fn register_cache( - &self, - cache: &mut cultcache_rs::CultCache, - ) -> ::anyhow::Result<()> { - $( - cache.register_entry_type::<$entry>()?; - )* - Ok(()) - } - - fn register_documents( - &self, - registry: &mut cultnet_rs::CultNetDocumentRegistry, - ) -> ::anyhow::Result<()> { - $( - registry.register( - cultnet_rs::CultNetDocumentBinding::for_entry_with_schema_id::<$entry>( - $schema_version.to_string(), - $schema_version.to_string(), - ), - ); - )* - Ok(()) - } - } - }; -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshNodeOptions { - pub runtime_id: String, - pub pull_on_start: bool, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshRudpDocumentPublishOptions { - pub target: SocketAddr, - pub runtime_id: String, - pub connection_id: u32, - pub connect_timeout: Duration, - pub flush_timeout: Duration, - pub poll_interval: Duration, - pub resend_delay_ms: u64, - pub source_agent_id: Option, - pub source_role: Option, - pub tags: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshRudpSnapshotOptions { - pub target: SocketAddr, - pub runtime_id: String, - pub connection_id: u32, - pub connect_timeout: Duration, - pub response_timeout: Duration, - pub poll_interval: Duration, - pub resend_delay_ms: u64, - pub schema_ids: Option>, - pub record_keys: Option>, -} - -impl CultMeshRudpSnapshotOptions { - pub fn odin(target: SocketAddr, runtime_id: impl Into) -> Self { - Self { - target, - runtime_id: runtime_id.into(), - ..Self::default() - } - } -} - -impl Default for CultMeshRudpSnapshotOptions { - fn default() -> Self { - Self { - target: SocketAddr::from(([0, 0, 0, 0], 0)), - runtime_id: "cultmesh-rudp-snapshot-client".to_string(), - connection_id: CULTMESH_RUDP_DOCUMENT_CATALOG_CONNECTION_ID, - connect_timeout: Duration::from_secs(3), - response_timeout: Duration::from_secs(3), - poll_interval: Duration::from_millis(10), - resend_delay_ms: 50, - schema_ids: None, - record_keys: None, - } - } -} - -impl CultMeshRudpDocumentPublishOptions { - pub fn odin(target: SocketAddr, runtime_id: impl Into) -> Self { - Self { - target, - runtime_id: runtime_id.into(), - ..Self::default() - } - } -} - -impl Default for CultMeshRudpDocumentPublishOptions { - fn default() -> Self { - Self { - target: SocketAddr::from(([0, 0, 0, 0], 0)), - runtime_id: "cultmesh-rudp-document-publisher".to_string(), - connection_id: CULTMESH_RUDP_DOCUMENT_CATALOG_CONNECTION_ID, - connect_timeout: Duration::from_secs(3), - flush_timeout: Duration::from_secs(30), - poll_interval: Duration::from_millis(10), - resend_delay_ms: 50, - source_agent_id: None, - source_role: None, - tags: Vec::new(), - } - } -} - -impl Default for CultMeshNodeOptions { - fn default() -> Self { - Self { - runtime_id: "cultmesh-local".to_string(), - pull_on_start: true, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshRudpEndpoint { - pub host: String, - pub port: u16, - pub addr: SocketAddr, -} - -pub struct CultMeshNode { - runtime_id: String, - cache: CultCache, - documents: CultNetDocumentRegistry, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CultMeshStreamKind { - Audio, - Video, - Tensor, - Bytes, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CultMeshStreamBodyTransport { - SharedMemory, - SharedD3d12Texture, - SharedD3d11Texture, - DmaBuf, - IoSurface, - AHardwareBuffer, - CultCachePage, - InlineBytes, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CultMeshStreamAccess { - Read, - Write, - ReadWrite, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CultMeshStreamCopyBudget { - ZeroCopyTarget, - OneCopyFallback, - OpaqueRuntime, -} - -#[derive(Clone, Debug, PartialEq)] -pub struct CultMeshStreamClock { - pub clock_domain_id: String, - pub source_id: Option, - pub sample_rate: i32, - pub offset_to_verse_time_ns: i64, - pub confidence: f64, - pub evidence_kind: Option, -} - -impl CultMeshStreamClock { - pub fn new(clock_domain_id: impl Into) -> Result { - let clock_domain_id = require_non_empty(clock_domain_id.into(), "clock_domain_id")?; - Ok(Self { - clock_domain_id, - source_id: None, - sample_rate: 0, - offset_to_verse_time_ns: 0, - confidence: 0.0, - evidence_kind: None, - }) - } - - pub fn source_id(mut self, source_id: impl Into) -> Result { - self.source_id = Some(require_non_empty(source_id.into(), "source_id")?); - Ok(self) - } - - pub fn confidence(mut self, confidence: f64) -> Self { - self.confidence = confidence; - self - } - - pub fn evidence_kind(mut self, evidence_kind: impl Into) -> Result { - self.evidence_kind = Some(require_non_empty(evidence_kind.into(), "evidence_kind")?); - Ok(self) - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct CultMeshStreamDescriptor { - pub stream_id: String, - pub verse_id: String, - pub owner_peer_id: String, - pub kind: CultMeshStreamKind, - pub clock: CultMeshStreamClock, - pub preferred_transports: Vec, - pub label: Option, - pub required_access: CultMeshStreamAccess, - pub max_in_flight_frames: u32, - pub metadata_schema_id: Option, -} - -impl CultMeshStreamDescriptor { - pub fn new( - stream_id: impl Into, - verse_id: impl Into, - owner_peer_id: impl Into, - kind: CultMeshStreamKind, - clock: CultMeshStreamClock, - preferred_transports: Vec, - ) -> Result { - if preferred_transports.is_empty() { - anyhow::bail!("preferred_transports must not be empty"); - } - Ok(Self { - stream_id: require_non_empty(stream_id.into(), "stream_id")?, - verse_id: require_non_empty(verse_id.into(), "verse_id")?, - owner_peer_id: require_non_empty(owner_peer_id.into(), "owner_peer_id")?, - kind, - clock, - preferred_transports, - label: None, - required_access: CultMeshStreamAccess::Read, - max_in_flight_frames: 0, - metadata_schema_id: None, - }) - } - - pub fn label(mut self, label: impl Into) -> Result { - self.label = Some(require_non_empty(label.into(), "label")?); - Ok(self) - } - - pub fn max_in_flight_frames(mut self, max_in_flight_frames: u32) -> Self { - self.max_in_flight_frames = max_in_flight_frames; - self - } - - pub fn metadata_schema_id(mut self, metadata_schema_id: impl Into) -> Result { - self.metadata_schema_id = Some(require_non_empty( - metadata_schema_id.into(), - "metadata_schema_id", - )?); - Ok(self) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshStreamConsumerProfile { - pub peer_id: String, - pub verse_id: String, - pub supported_transports: Vec, - pub accepted_kinds: Vec, - pub max_in_flight_frames: u32, -} - -impl CultMeshStreamConsumerProfile { - pub fn new( - peer_id: impl Into, - verse_id: impl Into, - supported_transports: Vec, - ) -> Result { - if supported_transports.is_empty() { - anyhow::bail!("supported_transports must not be empty"); - } - Ok(Self { - peer_id: require_non_empty(peer_id.into(), "peer_id")?, - verse_id: require_non_empty(verse_id.into(), "verse_id")?, - supported_transports, - accepted_kinds: Vec::new(), - max_in_flight_frames: 0, - }) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshStreamNegotiation { - pub stream_id: String, - pub producer_peer_id: String, - pub consumer_peer_id: String, - pub transport: CultMeshStreamBodyTransport, - pub access: CultMeshStreamAccess, - pub max_in_flight_frames: u32, - pub copy_budget: CultMeshStreamCopyBudget, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshStreamFrameHandle { - pub stream_id: String, - pub sequence: u64, - pub timestamp_ns: i64, - pub duration_ns: i64, - pub transport: CultMeshStreamBodyTransport, - pub byte_length: usize, - pub resource_key: Option, - pub unavoidable_copy_count: u32, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshSharedMemoryFrameRingStats { - pub stream_id: String, - pub slot_count: usize, - pub slot_byte_length: usize, - pub published_frames: u64, - pub dropped_frames: u64, - pub blocked_writes: u64, - pub latest_sequence: u64, - pub unavoidable_copy_count: u64, -} - -pub struct CultMeshFrameReadLease<'a> { - pub handle: CultMeshStreamFrameHandle, - bytes: &'a [u8], -} - -impl CultMeshFrameReadLease<'_> { - pub fn bytes(&self) -> &[u8] { - self.bytes - } -} - -#[derive(Clone, Debug)] -pub struct CultMeshSharedMemoryFrameRing { - stream_id: String, - slot_byte_length: usize, - slots: Vec)>>, - write_cursor: usize, - latest_slot: Option, - next_sequence: u64, - published_frames: u64, - dropped_frames: u64, - blocked_writes: u64, - unavoidable_copy_count: u64, -} - -impl CultMeshSharedMemoryFrameRing { - pub fn new( - stream_id: impl Into, - slot_count: usize, - slot_byte_length: usize, - ) -> Result { - let stream_id = require_non_empty(stream_id.into(), "stream_id")?; - if slot_count == 0 { - anyhow::bail!("slot_count must be greater than zero"); - } - if slot_byte_length == 0 { - anyhow::bail!("slot_byte_length must be greater than zero"); - } - Ok(Self { - stream_id, - slot_byte_length, - slots: vec![None; slot_count], - write_cursor: 0, - latest_slot: None, - next_sequence: 0, - published_frames: 0, - dropped_frames: 0, - blocked_writes: 0, - unavoidable_copy_count: 0, - }) - } - - pub fn try_publish_copy( - &mut self, - bytes: &[u8], - timestamp_ns: i64, - duration_ns: i64, - ) -> Result> { - if bytes.len() > self.slot_byte_length { - anyhow::bail!("frame byte length exceeds slot size"); - } - if self.slots.is_empty() { - self.blocked_writes += 1; - return Ok(None); - } - - let slot_index = self.write_cursor; - if self.slots[slot_index].is_some() { - self.dropped_frames += 1; - } - let handle = CultMeshStreamFrameHandle { - stream_id: self.stream_id.clone(), - sequence: self.next_sequence, - timestamp_ns, - duration_ns, - transport: CultMeshStreamBodyTransport::SharedMemory, - byte_length: bytes.len(), - resource_key: Some(format!("{}:slot:{slot_index}", self.stream_id)), - unavoidable_copy_count: 1, - }; - self.slots[slot_index] = Some((handle.clone(), bytes.to_vec())); - self.latest_slot = Some(slot_index); - self.write_cursor = (slot_index + 1) % self.slots.len(); - self.next_sequence = self.next_sequence.saturating_add(1); - self.published_frames = self.published_frames.saturating_add(1); - self.unavoidable_copy_count = self.unavoidable_copy_count.saturating_add(1); - Ok(Some(handle)) - } - - pub fn try_acquire_latest_read(&self) -> Option> { - let slot_index = self.latest_slot?; - let (handle, bytes) = self.slots[slot_index].as_ref()?; - Some(CultMeshFrameReadLease { - handle: handle.clone(), - bytes, - }) - } - - pub fn stats(&self) -> CultMeshSharedMemoryFrameRingStats { - let latest_sequence = self - .latest_slot - .and_then(|slot| self.slots[slot].as_ref().map(|(handle, _)| handle.sequence)) - .unwrap_or(0); - CultMeshSharedMemoryFrameRingStats { - stream_id: self.stream_id.clone(), - slot_count: self.slots.len(), - slot_byte_length: self.slot_byte_length, - published_frames: self.published_frames, - dropped_frames: self.dropped_frames, - blocked_writes: self.blocked_writes, - latest_sequence, - unavoidable_copy_count: self.unavoidable_copy_count, - } - } -} - -#[derive(Clone, Debug, Default)] -pub struct CultMeshStreamCatalog { - streams: BTreeMap, - latest_frames: BTreeMap, - rings: BTreeMap, -} - -impl CultMeshStreamCatalog { - pub fn declare(&mut self, stream: CultMeshStreamDescriptor) { - self.streams.insert(stream.stream_id.clone(), stream); - } - - pub fn get(&self, stream_id: &str) -> Option<&CultMeshStreamDescriptor> { - self.streams.get(stream_id) - } - - pub fn streams(&self) -> Vec<&CultMeshStreamDescriptor> { - self.streams.values().collect() - } - - pub fn find( - &self, - verse_id: &str, - kind: Option, - ) -> Vec<&CultMeshStreamDescriptor> { - self.streams - .values() - .filter(|stream| stream.verse_id == verse_id && kind.is_none_or(|k| stream.kind == k)) - .collect() - } - - pub fn negotiate( - &self, - stream_id: &str, - consumer: &CultMeshStreamConsumerProfile, - ) -> Result { - let stream = self - .streams - .get(stream_id) - .ok_or_else(|| anyhow::anyhow!("unknown CultMesh stream '{stream_id}'"))?; - if stream.verse_id != consumer.verse_id { - anyhow::bail!("stream and consumer must belong to the same Verse"); - } - if !consumer.accepted_kinds.is_empty() && !consumer.accepted_kinds.contains(&stream.kind) { - anyhow::bail!("consumer does not accept stream kind"); - } - let transport = stream - .preferred_transports - .iter() - .copied() - .find(|candidate| consumer.supported_transports.contains(candidate)) - .ok_or_else(|| { - anyhow::anyhow!("stream and consumer have no compatible body transport") - })?; - let producer_max = if stream.max_in_flight_frames == 0 { - u32::MAX - } else { - stream.max_in_flight_frames - }; - let consumer_max = if consumer.max_in_flight_frames == 0 { - u32::MAX - } else { - consumer.max_in_flight_frames - }; - Ok(CultMeshStreamNegotiation { - stream_id: stream.stream_id.clone(), - producer_peer_id: stream.owner_peer_id.clone(), - consumer_peer_id: consumer.peer_id.clone(), - transport, - access: stream.required_access, - max_in_flight_frames: producer_max.min(consumer_max), - copy_budget: copy_budget_for(transport), - }) - } - - pub fn create_shared_memory_ring( - &mut self, - stream_id: &str, - slot_count: usize, - slot_byte_length: usize, - ) -> Result<&mut CultMeshSharedMemoryFrameRing> { - if !self.streams.contains_key(stream_id) { - anyhow::bail!("unknown CultMesh stream '{stream_id}'"); - } - self.rings.insert( - stream_id.to_string(), - CultMeshSharedMemoryFrameRing::new(stream_id, slot_count, slot_byte_length)?, - ); - Ok(self - .rings - .get_mut(stream_id) - .expect("ring was inserted for stream")) - } - - pub fn ring(&self, stream_id: &str) -> Option<&CultMeshSharedMemoryFrameRing> { - self.rings.get(stream_id) - } - - pub fn ring_mut(&mut self, stream_id: &str) -> Option<&mut CultMeshSharedMemoryFrameRing> { - self.rings.get_mut(stream_id) - } - - pub fn publish_frame(&mut self, handle: CultMeshStreamFrameHandle) -> Result<()> { - if !self.streams.contains_key(&handle.stream_id) { - anyhow::bail!("unknown CultMesh stream '{}'", handle.stream_id); - } - self.latest_frames.insert(handle.stream_id.clone(), handle); - Ok(()) - } - - pub fn latest_frame(&self, stream_id: &str) -> Option<&CultMeshStreamFrameHandle> { - self.latest_frames.get(stream_id) - } -} - -fn copy_budget_for(transport: CultMeshStreamBodyTransport) -> CultMeshStreamCopyBudget { - match transport { - CultMeshStreamBodyTransport::SharedMemory - | CultMeshStreamBodyTransport::SharedD3d12Texture - | CultMeshStreamBodyTransport::SharedD3d11Texture - | CultMeshStreamBodyTransport::DmaBuf - | CultMeshStreamBodyTransport::IoSurface - | CultMeshStreamBodyTransport::AHardwareBuffer => CultMeshStreamCopyBudget::ZeroCopyTarget, - CultMeshStreamBodyTransport::CultCachePage => CultMeshStreamCopyBudget::OneCopyFallback, - CultMeshStreamBodyTransport::InlineBytes => CultMeshStreamCopyBudget::OpaqueRuntime, - } -} - -fn require_non_empty(value: String, name: &str) -> Result { - if value.trim().is_empty() { - anyhow::bail!("{name} must be non-empty"); - } - Ok(value) -} - -impl CultMeshNode { - pub fn runtime_id(&self) -> &str { - &self.runtime_id - } - - pub fn cache(&self) -> &CultCache { - &self.cache - } - - pub fn documents(&self) -> &CultNetDocumentRegistry { - &self.documents - } - - pub fn get(&self, key: &str) -> Result> { - self.cache.get::(key) - } - - pub fn get_required(&self, key: &str) -> Result { - self.cache.get_required::(key) - } - - pub fn put(&mut self, key: impl Into, value: &T) -> Result { - self.cache.put(key, value) - } - - pub fn pull_all_backing_stores(&mut self) -> Result<()> { - self.cache.pull_all_backing_stores() - } - - pub fn delete(&mut self, key: &str) -> Result { - self.cache.delete::(key) - } - - pub fn flush(&mut self) -> Result<()> { - Ok(()) - } - - pub fn publish_document_to_rudp_catalog( - &self, - key: impl Into, - value: &T, - options: CultMeshRudpDocumentPublishOptions, - ) -> Result<()> - where - T: DatabaseEntry + Serialize, - { - let message = self.create_rudp_document_message(key, value, &options)?; - publish_cultnet_messages_to_rudp_catalog(std::slice::from_ref(&message), options) - } - - pub fn create_rudp_document_message( - &self, - key: impl Into, - value: &T, - options: &CultMeshRudpDocumentPublishOptions, - ) -> Result - where - T: DatabaseEntry + Serialize, - { - let key = key.into(); - self.documents.create_raw_document_put_message( - format!("{}:{}:{}", options.runtime_id, T::TYPE, key), - key, - value, - CultNetDocumentPutOptions { - source_runtime_id: Some(options.runtime_id.clone()), - source_agent_id: options.source_agent_id.clone(), - source_role: options.source_role.clone(), - tags: if options.tags.is_empty() { - None - } else { - Some(options.tags.clone()) - }, - ..CultNetDocumentPutOptions::default() - }, - ) - } - - pub fn pull_rudp_catalog_snapshot( - &mut self, - options: CultMeshRudpSnapshotOptions, - ) -> Result { - let response = request_raw_snapshot_from_rudp_catalog(options)?; - let cultnet_rs::CultNetMessage::SnapshotResponseRaw { documents, .. } = response else { - anyhow::bail!("expected cultnet.snapshot_response_raw.v0"); - }; - let mut applied = 0usize; - for document in documents { - let Some(document_type) = - registered_document_type_for_schema(&self.documents, &document.schema_id) - else { - continue; - }; - self.cache.put_raw_envelope(CultCacheEnvelope { - key: document.record_key, - r#type: document_type, - payload: document.payload, - stored_at: document.stored_at, - schema_id: Some(document.schema_id), - })?; - applied += 1; - } - Ok(applied) - } -} - -fn registered_document_type_for_schema( - documents: &CultNetDocumentRegistry, - schema_id: &str, -) -> Option { - let normalized = strip_schema_version_suffix(schema_id); - if normalized != schema_id { - return Some(normalized.to_string()); - } - documents - .binding_by_schema_id(schema_id) - .map(|binding| binding.document_type.clone()) - .or_else(|| documents.binding(schema_id).map(|_| schema_id.to_string())) -} - -fn strip_schema_version_suffix(value: &str) -> &str { - let Some((prefix, suffix)) = value.rsplit_once(".v") else { - return value; - }; - if suffix.chars().all(|character| character.is_ascii_digit()) { - prefix - } else { - value - } -} - -fn request_raw_snapshot_from_rudp_catalog( - options: CultMeshRudpSnapshotOptions, -) -> Result { - if options.runtime_id.trim().is_empty() { - anyhow::bail!("runtime_id must be non-empty"); - } - let bind_addr = if options.target.is_ipv4() { - SocketAddr::from(([0, 0, 0, 0], 0)) - } else { - SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 0], 0)) - }; - let socket = UdpSocket::bind(bind_addr)?; - socket.set_read_timeout(Some(options.poll_interval))?; - let mut client = - CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions { - runtime_id: options.runtime_id.clone(), - socket, - mode: cultnet_rs::CultNetRudpSocketMode::Client, - remote_addr: Some(options.target), - connection_id: options.connection_id, - initial_sequence: 1, - resend_delay_ms: options.resend_delay_ms, - transport_id: Some("cultmesh-rudp-snapshot-client".to_string()), - max_payload_bytes: None, - max_fragment_bytes: Some(1200), - max_pending_reliable_packets: None, - reconnect_policy: None, - })?; - client.connect(Vec::new())?; - let connect_deadline = Instant::now() + options.connect_timeout; - while !client.connected() && Instant::now() < connect_deadline { - let _ = client.receive_once()?; - client.poll_resends()?; - thread::sleep(options.poll_interval); - } - if !client.connected() { - anyhow::bail!( - "timed out connecting CultMesh RUDP snapshot client {} to {}", - options.runtime_id, - options.target - ); - } - - let message_id = format!("{}:snapshot:{}", options.runtime_id, unix_millis()); - let request = cultnet_rs::CultNetMessage::SnapshotRequest { - message_id: message_id.clone(), - schema_ids: options.schema_ids, - record_keys: options.record_keys, - }; - let payload = encode_cultnet_message_to_vec(&request, CultNetWireContract::CultNetSchemaV0)?; - client.send("schema", payload)?; - let response_deadline = Instant::now() + options.response_timeout; - let mut snapshot_response = None; - while Instant::now() < response_deadline { - if let Some(frame) = client.receive_once()? { - if frame.channel_id == "schema" { - let message = decode_cultnet_message_from_slice( - &frame.payload, - CultNetWireContract::CultNetSchemaV0, - )?; - match &message { - cultnet_rs::CultNetMessage::SnapshotResponseRaw { - message_id: received, - .. - } if received == &message_id => { - snapshot_response = Some(message); - break; - } - cultnet_rs::CultNetMessage::Error { error } => { - let _ = client.disconnect(b"snapshot-error".to_vec()); - anyhow::bail!("CultMesh RUDP snapshot failed: {error}"); - } - _ => {} - } - } - } - client.poll_resends()?; - thread::sleep(options.poll_interval); - } - if let Some(message) = snapshot_response { - let _ = client.disconnect(b"snapshot-complete".to_vec()); - return Ok(message); - } - let _ = client.disconnect(b"snapshot-timeout".to_vec()); - anyhow::bail!( - "timed out waiting for CultMesh RUDP snapshot from {}", - options.target - ) -} - -fn unix_millis() -> u128 { - std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .unwrap_or_default() - .as_millis() -} - -pub fn publish_cultnet_messages_to_rudp_catalog( - messages: &[cultnet_rs::CultNetMessage], - options: CultMeshRudpDocumentPublishOptions, -) -> Result<()> { - if options.runtime_id.trim().is_empty() { - anyhow::bail!("runtime_id must be non-empty"); - } - let bind_addr = if options.target.is_ipv4() { - SocketAddr::from(([0, 0, 0, 0], 0)) - } else { - SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 0], 0)) - }; - let socket = UdpSocket::bind(bind_addr)?; - socket.set_read_timeout(Some(options.poll_interval))?; - let mut client = - CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions { - runtime_id: options.runtime_id.clone(), - socket, - mode: cultnet_rs::CultNetRudpSocketMode::Client, - remote_addr: Some(options.target), - connection_id: options.connection_id, - initial_sequence: 1, - resend_delay_ms: options.resend_delay_ms, - transport_id: Some("cultmesh-rudp-document-publisher".to_string()), - max_payload_bytes: None, - max_fragment_bytes: Some(1200), - max_pending_reliable_packets: None, - reconnect_policy: None, - })?; - client.connect(Vec::new())?; - let connect_deadline = Instant::now() + options.connect_timeout; - while !client.connected() && Instant::now() < connect_deadline { - let _ = client.receive_once()?; - client.poll_resends()?; - thread::sleep(options.poll_interval); - } - if !client.connected() { - anyhow::bail!( - "timed out connecting CultMesh RUDP document publisher {} to {}", - options.runtime_id, - options.target - ); - } - - for message in messages { - let payload = encode_cultnet_message_to_vec(message, CultNetWireContract::CultNetSchemaV0)?; - client.send("schema", payload)?; - } - client.flush_reliable(options.flush_timeout) -} - -#[derive(Clone, Copy, Debug, Default)] -pub struct CultMesh; - -impl CultMesh { - pub fn resolve_rudp_endpoint(endpoint: &str) -> Result { - let text = require_non_empty(endpoint.trim().to_string(), "endpoint")?; - if text.starts_with("rudp://") { - return parse_rudp_socket_addr(&text); - } - if text.starts_with("cultmesh://") { - let resolved = default_cultmesh_rudp_endpoint_resolver(&text)?.ok_or_else(|| { - anyhow::anyhow!("CultMesh URI {text} did not resolve to a RUDP endpoint") - })?; - return parse_rudp_socket_addr(&resolved); - } - parse_socket_addr(&text) - } - - pub fn create_node( - store_path: impl AsRef, - documents: D, - options: CultMeshNodeOptions, - ) -> Result - where - D: CultMeshDocumentSet, - { - let mut cache = CultCache::new(); - documents.register_cache(&mut cache)?; - cache - .add_generic_backing_store(SingleFileMessagePackBackingStore::new(store_path.as_ref())); - if options.pull_on_start { - cache.pull_all_backing_stores()?; - } - - let mut registry = CultNetDocumentRegistry::new(); - documents.register_documents(&mut registry)?; - - Ok(CultMeshNode { - runtime_id: options.runtime_id, - cache, - documents: registry, - }) - } - - pub fn start_node( - store_path: impl AsRef, - documents: D, - options: CultMeshNodeOptions, - ) -> Result - where - D: CultMeshDocumentSet, - { - Self::create_node(store_path, documents, options) - } - - pub fn create_stream_catalog() -> CultMeshStreamCatalog { - CultMeshStreamCatalog::default() - } -} - -pub fn create_node( - store_path: impl AsRef, - documents: D, - options: CultMeshNodeOptions, -) -> Result -where - D: CultMeshDocumentSet, -{ - CultMesh::create_node(store_path, documents, options) -} - -pub fn start_node( - store_path: impl AsRef, - documents: D, - options: CultMeshNodeOptions, -) -> Result -where - D: CultMeshDocumentSet, -{ - CultMesh::start_node(store_path, documents, options) -} - -pub fn create_stream_catalog() -> CultMeshStreamCatalog { - CultMesh::create_stream_catalog() -} - -pub fn resolve_rudp_endpoint(endpoint: &str) -> Result { - CultMesh::resolve_rudp_endpoint(endpoint) -} - -fn parse_rudp_socket_addr(value: &str) -> Result { - let text = value.trim().strip_prefix("rudp://").unwrap_or(value.trim()); - parse_socket_addr(text) -} - -fn parse_socket_addr(value: &str) -> Result { - value - .parse() - .with_context(|| format!("RUDP endpoint must be a socket address, got {value:?}")) -} - -fn default_cultmesh_rudp_endpoint_resolver(uri: &str) -> Result> { - let authority = cultmesh_uri_authority(uri)?; - let slug = authority - .chars() - .map(|ch| { - if ch.is_ascii_alphanumeric() { - ch.to_ascii_uppercase() - } else { - '_' - } - }) - .collect::() - .trim_matches('_') - .to_string(); - if slug.is_empty() { - return Ok(None); - } - - for key in [ - format!("CULTMESH_URI_{slug}_RUDP"), - format!("{slug}_CULTMESH_RUDP_ENDPOINT"), - ] { - if let Ok(value) = env::var(&key) { - let text = value.trim(); - if !text.is_empty() { - return Ok(Some(if text.starts_with("rudp://") { - text.to_string() - } else { - format!("rudp://{text}") - })); - } - } - } - Ok(None) -} - -fn cultmesh_uri_authority(uri: &str) -> Result { - let rest = uri - .strip_prefix("cultmesh://") - .ok_or_else(|| anyhow::anyhow!("CultMesh URI must start with cultmesh://"))?; - let authority = rest.split(['/', '?', '#']).next().unwrap_or("").trim(); - if authority.is_empty() { - anyhow::bail!("CultMesh URI must include an authority"); - } - Ok(authority.to_string()) -} - -#[cfg(test)] -mod tests { - use super::*; - use pretty_assertions::assert_eq; - - #[derive(Clone, Debug, PartialEq, Eq, cultcache_rs::DatabaseEntry)] - #[cultcache(type = "cultmesh.test.note", schema = "CultMeshTestNote")] - struct Note { - #[cultcache(key = 0)] - body: String, - } - - cultmesh_documents!(TestDocuments { - Note => "cultmesh.test.note.v0", - }); - - #[test] - fn node_round_trips_registered_documents() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("cultmesh.cc"); - let note = Note { - body: "blessed circuit".to_string(), - }; - - let mut node = CultMesh::create_node( - &store_path, - TestDocuments, - CultMeshNodeOptions { - runtime_id: "test-runtime".to_string(), - ..CultMeshNodeOptions::default() - }, - )?; - assert_eq!(node.runtime_id(), "test-runtime"); - node.put("note", ¬e)?; - node.flush()?; - - let reloaded = CultMesh::create_node(&store_path, TestDocuments, Default::default())?; - assert_eq!(reloaded.get_required::("note")?, note); - assert!( - reloaded - .documents() - .binding_by_schema_id("cultmesh.test.note.v0") - .is_some() - ); - Ok(()) - } - - #[test] - fn node_publishes_registered_document_to_rudp_catalog() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("cultmesh.cc"); - let note = Note { - body: "respect odin once".to_string(), - }; - let node = CultMesh::create_node( - &store_path, - TestDocuments, - CultMeshNodeOptions { - runtime_id: "muninn-test".to_string(), - ..CultMeshNodeOptions::default() - }, - )?; - - let socket = UdpSocket::bind("127.0.0.1:0")?; - socket.set_read_timeout(Some(Duration::from_millis(10)))?; - let target = socket.local_addr()?; - let (sender, receiver) = std::sync::mpsc::channel(); - let server = thread::spawn(move || -> Result<()> { - let mut server = CultNetRudpSocketTransportConnection::new( - CultNetRudpSocketTransportOptions::server( - "odin-test-catalog", - socket, - CULTMESH_RUDP_DOCUMENT_CATALOG_CONNECTION_ID, - ), - )?; - let deadline = Instant::now() + Duration::from_secs(2); - while Instant::now() < deadline { - if let Some(frame) = server.receive_once()? { - let message = cultnet_rs::decode_cultnet_message_from_slice( - &frame.payload, - CultNetWireContract::CultNetSchemaV0, - )?; - if let cultnet_rs::CultNetMessage::DocumentPutRaw { document, .. } = message { - sender - .send(( - document.schema_id, - document.record_key, - document.source_runtime_id, - )) - .ok(); - return Ok(()); - } - } - server.poll_resends()?; - } - anyhow::bail!("timed out waiting for RUDP document put") - }); - - node.publish_document_to_rudp_catalog( - "note", - ¬e, - CultMeshRudpDocumentPublishOptions::odin(target, "muninn-test"), - )?; - let received = receiver.recv_timeout(Duration::from_secs(2))?; - server.join().expect("server thread should not panic")?; - - assert_eq!(received.0, "cultmesh.test.note.v0"); - assert_eq!(received.1, "note"); - assert_eq!(received.2.as_deref(), Some("muninn-test")); - Ok(()) - } - - #[test] - fn large_rudp_publication_waits_for_acknowledged_delivery() -> Result<()> { - let temp = tempfile::tempdir()?; - let node = CultMesh::create_node( - temp.path().join("cultmesh.cc"), - TestDocuments, - CultMeshNodeOptions { - runtime_id: "large-publisher-test".into(), - ..CultMeshNodeOptions::default() - }, - )?; - let socket = UdpSocket::bind("127.0.0.1:0")?; - socket.set_read_timeout(Some(Duration::from_millis(10)))?; - let target = socket.local_addr()?; - let options = CultMeshRudpDocumentPublishOptions::odin(target, "large-publisher-test"); - let messages = [ - node.create_rudp_document_message( - "large-a", - &Note { - body: "a".repeat(256 * 1024), - }, - &options, - )?, - node.create_rudp_document_message( - "large-b", - &Note { - body: "b".repeat(256 * 1024), - }, - &options, - )?, - ]; - - let server = thread::spawn(move || -> Result> { - thread::sleep(Duration::from_millis(500)); - let mut server = CultNetRudpSocketTransportConnection::new( - CultNetRudpSocketTransportOptions::server( - "delayed-odin-test", - socket, - CULTMESH_RUDP_DOCUMENT_CATALOG_CONNECTION_ID, - ), - )?; - let deadline = Instant::now() + Duration::from_secs(5); - let mut record_keys = Vec::new(); - while Instant::now() < deadline && record_keys.len() < 2 { - if let Some(frame) = server.receive_once()? { - let message = decode_cultnet_message_from_slice( - &frame.payload, - CultNetWireContract::CultNetSchemaV0, - )?; - if let cultnet_rs::CultNetMessage::DocumentPutRaw { document, .. } = message { - record_keys.push(document.record_key); - } - } - server.poll_resends()?; - } - let linger_deadline = Instant::now() + Duration::from_millis(250); - while Instant::now() < linger_deadline { - let _ = server.receive_once()?; - server.poll_resends()?; - } - if record_keys.len() != 2 { - anyhow::bail!("timed out waiting for both large RUDP documents"); - } - Ok(record_keys) - }); - - publish_cultnet_messages_to_rudp_catalog(&messages, options)?; - assert_eq!( - server.join().expect("server thread should not panic")?, - vec!["large-a", "large-b"] - ); - Ok(()) - } - - #[test] - fn resolves_rudp_endpoint_from_cultmesh_uri_bootstrap_env() -> Result<()> { - let previous = env::var("CULTMESH_URI_ODIN_RUDP").ok(); - unsafe { - env::set_var("CULTMESH_URI_ODIN_RUDP", "127.0.0.1:17871"); - } - let result = CultMesh::resolve_rudp_endpoint("cultmesh://odin/rendezvous/provider-catalog"); - unsafe { - if let Some(value) = previous { - env::set_var("CULTMESH_URI_ODIN_RUDP", value); - } else { - env::remove_var("CULTMESH_URI_ODIN_RUDP"); - } - } - - assert_eq!(result?, "127.0.0.1:17871".parse()?); - Ok(()) - } - - #[test] - fn rejects_unresolved_cultmesh_rudp_endpoint() { - let previous = env::var("CULTMESH_URI_MISSING_ODIN_RUDP").ok(); - unsafe { - env::remove_var("CULTMESH_URI_MISSING_ODIN_RUDP"); - } - let error = - CultMesh::resolve_rudp_endpoint("cultmesh://missing-odin/rendezvous/provider-catalog") - .expect_err("unresolved CultMesh URI should fail"); - unsafe { - if let Some(value) = previous { - env::set_var("CULTMESH_URI_MISSING_ODIN_RUDP", value); - } - } - assert!(error.to_string().contains("did not resolve")); - } - - #[test] - fn raw_snapshot_application_uses_registered_document_type() -> Result<()> { - let temp = tempfile::tempdir()?; - let store_path = temp.path().join("cultmesh.cc"); - let mut node = CultMesh::create_node( - &store_path, - TestDocuments, - CultMeshNodeOptions { - runtime_id: "snapshot-apply-test".to_string(), - ..CultMeshNodeOptions::default() - }, - )?; - let payload = rmp_serde::to_vec(&Note { - body: "catalog truth".to_string(), - })?; - - node.cache - .put_raw_envelope(cultcache_rs::CultCacheEnvelope { - key: "note".to_string(), - r#type: node - .documents - .binding_by_schema_id("cultmesh.test.note.v0") - .expect("test binding") - .document_type - .clone(), - payload, - stored_at: "2026-07-03T00:00:00Z".to_string(), - schema_id: Some("cultmesh.test.note.v0".to_string()), - })?; - node.flush()?; - - let reloaded = CultMesh::create_node(&store_path, TestDocuments, Default::default())?; - assert_eq!(reloaded.get_required::("note")?.body, "catalog truth"); - assert_eq!(reloaded.cache().snapshot()[0].r#type, Note::TYPE); - Ok(()) - } - - #[test] - fn stream_catalog_negotiates_and_publishes_latest_shared_memory_frame() -> Result<()> { - let mut catalog = CultMesh::create_stream_catalog(); - let stream = CultMeshStreamDescriptor::new( - "muninn:nightwing:move-evidence", - "mimir-live", - "muninn:nightwing", - CultMeshStreamKind::Bytes, - CultMeshStreamClock::new("muninn:nightwing:clock")? - .source_id("muninn:nightwing:move-evidence")? - .confidence(1.0) - .evidence_kind("muninn-move-evidence")?, - vec![ - CultMeshStreamBodyTransport::SharedMemory, - CultMeshStreamBodyTransport::CultCachePage, - ], - )? - .label("Muninn Move evidence")? - .max_in_flight_frames(4) - .metadata_schema_id("mimir.muninn_move_evidence_stream_frame.v1")?; - catalog.declare(stream); - - let consumer = CultMeshStreamConsumerProfile::new( - "mimir:starfire", - "mimir-live", - vec![CultMeshStreamBodyTransport::SharedMemory], - )?; - let negotiation = catalog.negotiate("muninn:nightwing:move-evidence", &consumer)?; - - assert_eq!( - negotiation.transport, - CultMeshStreamBodyTransport::SharedMemory - ); - assert_eq!( - negotiation.copy_budget, - CultMeshStreamCopyBudget::ZeroCopyTarget - ); - - let handle = { - let ring = - catalog.create_shared_memory_ring("muninn:nightwing:move-evidence", 4, 128)?; - ring.try_publish_copy(&[1, 2, 3], 42, 0)? - .expect("frame should publish") - }; - catalog.publish_frame(handle.clone())?; - - assert_eq!( - catalog.latest_frame("muninn:nightwing:move-evidence"), - Some(&handle) - ); - let lease = catalog - .ring("muninn:nightwing:move-evidence") - .and_then(CultMeshSharedMemoryFrameRing::try_acquire_latest_read) - .expect("latest frame should be readable"); - assert_eq!(lease.handle.sequence, 0); - assert_eq!(lease.bytes(), &[1, 2, 3]); - Ok(()) - } -} diff --git a/vendor/cultnet-rs/.gitignore b/vendor/cultnet-rs/.gitignore deleted file mode 100644 index 96ef6c0..0000000 --- a/vendor/cultnet-rs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -/target -Cargo.lock diff --git a/vendor/cultnet-rs/Cargo.toml b/vendor/cultnet-rs/Cargo.toml deleted file mode 100644 index 1334626..0000000 --- a/vendor/cultnet-rs/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "cultnet-rs" -version = "0.1.0" -edition = "2024" -publish = false -license = "MIT" -description = "CultNet-style typed MessagePack messaging, direct-pipe framing, security helpers, and CultCache replication for Rust runtimes." - -[lib] -name = "cultnet_rs" -path = "src/lib.rs" - -[dependencies] -aes-gcm = "0.10" -anyhow = "1" -base64 = "0.22" -chrono = { version = "0.4", features = ["serde"] } -cultcache-rs = { path = "../cultcache-rs" } -ed25519-dalek = "2.2" -hmac = "0.12" -rand = "0.9" -rmp-serde = "1.3" -rmpv = { version = "1.3", features = ["with-serde"] } -serde = { version = "1", features = ["derive"] } -serde_bytes = "0.11" -serde_json = "1" -sha2 = "0.10" -socket2 = "0.5" -uuid = { version = "1", features = ["v4"] } - -[target.'cfg(windows)'.dependencies] -windows-sys = { version = "0.61", features = ["Win32_Foundation", "Win32_Security_Cryptography"] } - -[dev-dependencies] -pretty_assertions = "1.4" -tempfile = "3" diff --git a/vendor/cultnet-rs/README.md b/vendor/cultnet-rs/README.md deleted file mode 100644 index 7ade81d..0000000 --- a/vendor/cultnet-rs/README.md +++ /dev/null @@ -1,279 +0,0 @@ -# CultNet RS - -`cultnet-rs` is the Rust sibling of `cultnet-ts`: typed MessagePack messages, -4-byte length-prefixed direct-pipe framing, CultLib-shaped security helpers, and -CultCache replication without making callers paw through raw envelopes like we -lost a bet. - -The contract is intentionally boring: - -- `cultnet.schema.v0` sends schema-versioned MessagePack objects. -- `gamecult.networking.v0` maps the legacy C# union shape explicitly. -- schema discovery uses explicit catalog request/response messages; no inbound - auto-detect sludge. -- document put/delete/snapshot messages move typed CultCache entries. -- raw document put/snapshot messages preserve canonical MessagePack payload - bytes for bit-compatible neighbors. -- hello messages advertise `supportedMutationContracts`, so callers discover - which document types are read-only, which accept typed intents, which - authority owns the mutation, and which receipt documents prove the outcome. -- payloads are decoded through registered Rust types before entering the cache. - -This crate is not an HTTP wrapper. It is the wire vocabulary Epiphany, -Ghostlight, VoidBot, and the rest of the swarm can share. - -The public API surface is the schema plus its mutation contracts. A runtime does -not expose a pile of bespoke verbs and hope everyone remembers the ritual; it -advertises typed documents, typed intents, and typed receipts. Polite machines -knock on the contract before touching the furniture. - -## Receipts - -```powershell -cargo test -``` - -The initial tests prove: - -- CultLib-compatible AES-GCM string encryption and HMAC session tokens -- 4-byte big-endian MessagePack framing -- schema-versioned message round trips -- legacy `gamecult.networking.v0` login mapping -- schema discovery catalog responses with canonical JSON schema hashes -- shard catalog request/response filtering and response application -- CultCache snapshot replication through registered typed documents -- raw snapshot replication that preserves the original payload bytes -- raw snapshot replication into a local same-schema Rust alias type -- document mutation contract advertisement through hello frames and registries - -## Schema Discovery - -`cultnet-rs` now ships a built-in schema registry for the shared swarm contract -surface: - -- core wire messages -- legacy `gamecult.networking.v0` auth/sample payloads -- schema catalog request/response messages -- shard catalog request/response messages -- the canonical `ghostlight.agent-state` document payload schema - -Use `builtin_schema_registry()` or `CultMesh::create_builtin_schema_registry()` -when you want the standard catalog, or register your own closed-world schema set -with `CultNetSchemaRegistry` / `CultMesh::create_schema_registry()`. Discovery -stays explicit on purpose: peers advertise only the contracts they were compiled -to understand, the same way CultCache only consumes the document types you -registered instead of pretending polymorphism is a public park. - -## Shard Catalogs - -Rust now exposes the same shard topology vocabulary as the other runtimes -without claiming to be the full C# service body: - -```rust -use cultnet_rs::{CultMesh, CultNetMessage, CultNetShardDescriptor}; - -let mut catalog = CultMesh::create_shard_catalog(); -catalog.upsert(CultNetShardDescriptor { - shard_id: "notes-a".to_string(), - owner_runtime_id: "rust-primary".to_string(), - epoch: 3, - is_primary: Some(true), - schema_ids: vec!["note.v0".to_string()], - key_prefix: Some("note:".to_string()), - primary_endpoints: vec!["rudp://127.0.0.1:4100".to_string()], - replica_endpoints: vec![], - read_replica_endpoints: vec![], - region: None, - authority_lease_id: None, -})?; - -let response = catalog.create_catalog_response(&CultNetMessage::ShardCatalogRequest { - message_id: "discover-shards".to_string(), - schema_ids: Some(vec!["note.v0".to_string()]), - record_keys: Some(vec!["note:1".to_string()]), -})?; -``` - -`CultNetShardCatalog::apply_response(...)` imports remote -`cultnet.shard_catalog_response.v0` descriptors into the local catalog. The -catalog owns topology inspection only; shard-log serving, daemon policy, and -game-session authority remain separate responsibilities. - -## Local Fast Lane - -`cultnet-rs` now mirrors the raw replication seam from `cultnet-ts`: - -- `cultnet.document_put_raw.v0` -- `cultnet.snapshot_response_raw.v0` - -Those messages carry the exact persisted MessagePack payload bytes from -CultCache along with the typed envelope metadata. Combined with -`CultCache::put_envelope::()`, that lets a bit-compatible neighbor ingest the -document without re-encoding the payload first. - -The receiving Rust type does not have to be the sender's concrete Rust type. If -the local registry binds a different `DatabaseEntry` to the same schema id, -`sync_raw_document_from_snapshot_response::(...)` hydrates one local -alias type directly: - -```rust -let mut target_registry = CultNetDocumentRegistry::new(); -target_registry.register(CultNetDocumentBinding::for_entry_with_schema_id::( - "aetheria.note.v1".to_string(), - "aetheria.note.v1".to_string(), -)); - -let note = target_registry.sync_raw_document_from_snapshot_response::( - &mut target_cache, - &snapshot_response, - "note:bridge", -)?; -``` - -That still is not zero-copy in the religious sense. Frames allocate, bytes move, -and the receiving cache still decodes once to keep typed reads and validation -honest. The win is narrower and realer: identical payload bytes stop getting -decoded into generic sludge and then encoded right back into the same bytes for -no reason. - -## Reactive Documents - -For local state that should feel owned by the caller, Rust can wrap a registered -CultCache record as a managed reactive document: - -```rust -use std::sync::{Arc, Mutex}; -use cultnet_rs::{CultNetReactiveDocumentOptions, CultNetDocumentRegistry}; - -let cache = Arc::new(Mutex::new(target_cache)); -let note = registry.reactive_document::( - Arc::clone(&cache), - "note:bridge", - CultNetReactiveDocumentOptions::default(), -)?; - -{ - let current = note.current(); - current.lock().unwrap().body = "local prediction".to_string(); -} -``` - -The handle polls the serialized `current` value on the configured debounce -cadence and flushes changed state back through the typed cache. `update(...)`, -`set_current(...)`, `mark_dirty()`, and `flush()` are available when the caller -wants a more explicit Rust shape. - -When a canonical document put arrives while local state is dirty, -`apply_document_put_message(...)` or `apply_raw_document_put_message(...)` -records a `CultNetReactiveDocumentReconciliation`: the canonical value, the -predicted local value, and a delta that can be smoothed by the caller. CultCache -payloads serialize by numeric field slot, so Rust tuple-backed deltas use slot -keys such as `"0"` and `"1"` when field names are not present on the wire. - -## CultMesh Rust Facade - -Rust is still the low-level substrate runtime, not a clone of the C# game -server. It does now expose the small CultMesh entrypoint needed for practical -contact and authority ergonomics: - -```rust -use cultnet_rs::{ - CultMesh, CultMeshAuthorityLease, CultMeshPeerCard, CultMeshRudpClientOptions, - CultMeshRudpSocketOptions, CultNetMessage, CultNetSchemaKind, -}; -use chrono::{Duration, Utc}; - -let schemas = CultMesh::create_builtin_schema_registry()?; -let shared_contracts = schemas.list(&cultnet_rs::CultNetSchemaCatalogOptions { - include_schema_json: false, - schema_ids: None, - kinds: Some(vec![CultNetSchemaKind::SharedContract]), -}); - -let mut server = CultMesh::create_rudp_server( - "rust-server", - 0x1020_3040, - CultMeshRudpSocketOptions::default(), -)?; -let port = server.profile.transports[0].port.unwrap(); - -let peer = CultMeshPeerCard::new( - "rust-server", - "local", - [format!("rudp://127.0.0.1:{port}")], -) -.with_roles(["schema"]) -.with_authority_lease_id("lease:rust-server"); - -let now = Utc::now(); -let mut peers = CultMesh::create_peer_catalog(); -let mut leases = CultMesh::create_authority_lease_catalog(); -peers.upsert(peer.clone())?; -leases.upsert(CultMeshAuthorityLease { - lease_id: "lease:rust-server".to_string(), - verse_id: "local".to_string(), - peer_id: "rust-server".to_string(), - roles: vec!["schema".to_string()], - shard_ids: Vec::new(), - issuer_runtime_id: None, - valid_from: now - Duration::minutes(1), - expires_at: now + Duration::minutes(1), -})?; - -let mut client = CultMesh::connect_rudp_client_for_authorized_peer( - "rust-client", - 0x1020_3040, - &peers, - &leases, - "local", - "schema", - None, - now, - CultMeshRudpClientOptions { - socket_options: CultMeshRudpSocketOptions::default(), - connect_payload: b"join".to_vec(), - ..CultMeshRudpClientOptions::default() - }, -)?; -client.send_schema_message(&CultNetMessage::SchemaCatalogRequest { - message_id: "rust-schema-catalog".to_string(), - include_schema_json: Some(false), - schema_ids: None, - kinds: Some(vec![CultNetSchemaKind::DocumentPayload]), -})?; -``` - -`create_rudp_client...` remains available when the caller intentionally owns the -handshake and polling loop. `connect_rudp_client...` performs the client -handshake before returning the same schema-message-capable RUDP transport. - -For caller-owned reconnect loops, `CultNetRudpReconnectLoop` keeps transport -construction outside the library while sharing the portable retry controller: - -```rust -use cultnet_rs::{ - CultNetReconnectPolicyOptions, CultNetRudpReconnectLoop, create_reconnect_policy, -}; - -let policy = create_reconnect_policy(CultNetReconnectPolicyOptions::default()); -let mut reconnect = CultNetRudpReconnectLoop::new(policy, b"join".to_vec(), || { - CultMesh::create_rudp_client_for_peer( - "rust-client", - 0x1020_3040, - &peer, - CultMeshRudpSocketOptions::default(), - ) -}); - -reconnect.start()?; -let decision = reconnect.handle_closed(now_ms, 0); -if decision.is_some() { - reconnect.reconnect_if_due(now_ms)?; -} -``` - -`CultMesh::parse_rudp_endpoint(...)` handles advertised `rudp://host:port` -contact hints, and `CultMeshAuthorityLeaseCatalog` keeps trust separate from -discovery. A peer card can say where to dial; `peers.first_authorized(...)` and -`CultMesh::create_rudp_client_for_authorized_peer(...)` only use that contact -hint after a lease says the peer may own the requested role or shard. diff --git a/vendor/cultnet-rs/contracts/cultnet.document-delete.schema.json b/vendor/cultnet-rs/contracts/cultnet.document-delete.schema.json deleted file mode 100644 index 265bcaf..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.document-delete.schema.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.document-delete.schema.json", - "title": "CultNet Document Delete Message", - "type": "object", - "required": [ - "schemaVersion", - "messageId", - "schemaId", - "recordKey" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.document_delete.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "schemaId": { "type": "string", "minLength": 1 }, - "recordKey": { "type": "string", "minLength": 1 } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.document-mutation-contract.schema.json b/vendor/cultnet-rs/contracts/cultnet.document-mutation-contract.schema.json deleted file mode 100644 index e444fef..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.document-mutation-contract.schema.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.document-mutation-contract.schema.json", - "title": "CultNet Document Mutation Contract", - "type": "object", - "required": [ - "documentType", - "operations", - "authority" - ], - "additionalProperties": false, - "properties": { - "documentType": { "type": "string", "minLength": 1 }, - "payloadSchemaVersion": { "type": "string", "minLength": 1 }, - "operations": { - "type": "array", - "minItems": 1, - "items": { - "type": "string", - "enum": ["snapshot", "documentPut", "documentDelete", "intentSubmit", "receiptWatch"] - } - }, - "authority": { - "type": "string", - "enum": ["readOnly", "localUser", "coordinator", "runtime", "denied"] - }, - "intentDocumentTypes": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "receiptDocumentTypes": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "notes": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.document-put-raw.schema.json b/vendor/cultnet-rs/contracts/cultnet.document-put-raw.schema.json deleted file mode 100644 index 3c178d0..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.document-put-raw.schema.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.document-put-raw.schema.json", - "title": "CultNet Raw Document Put Message", - "type": "object", - "required": [ - "schemaVersion", - "messageId", - "document" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.document_put_raw.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "document": { - "$ref": "./cultnet.raw-document-record.schema.json" - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.document-put.schema.json b/vendor/cultnet-rs/contracts/cultnet.document-put.schema.json deleted file mode 100644 index a04f5fc..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.document-put.schema.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.document-put.schema.json", - "title": "CultNet Document Put Message", - "type": "object", - "required": [ - "schemaVersion", - "messageId", - "document" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.document_put.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "document": { - "$ref": "./cultnet.document-record.schema.json" - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.document-record.schema.json b/vendor/cultnet-rs/contracts/cultnet.document-record.schema.json deleted file mode 100644 index 94301dc..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.document-record.schema.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.document-record.schema.json", - "title": "CultNet Document Record", - "type": "object", - "required": [ - "schemaId", - "recordKey", - "storedAt", - "payload" - ], - "additionalProperties": false, - "properties": { - "schemaId": { "type": "string", "minLength": 1 }, - "recordKey": { "type": "string", "minLength": 1 }, - "storedAt": { "type": "string", "minLength": 1 }, - "payload": {}, - "sourceRuntimeId": { "type": "string", "minLength": 1 }, - "sourceAgentId": { "type": "string", "minLength": 1 }, - "sourceRole": { "type": "string", "minLength": 1 }, - "tags": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.error.schema.json b/vendor/cultnet-rs/contracts/cultnet.error.schema.json deleted file mode 100644 index af6da9c..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.error.schema.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.error.schema.json", - "title": "CultNet Error Message", - "type": "object", - "required": [ - "schemaVersion", - "error" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.error.v0" }, - "error": { "type": "string", "minLength": 1 } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.hello.schema.json b/vendor/cultnet-rs/contracts/cultnet.hello.schema.json deleted file mode 100644 index a557dc3..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.hello.schema.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.hello.schema.json", - "title": "CultNet Hello Message", - "type": "object", - "required": [ - "schemaVersion", - "runtimeId", - "runtimeKind" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.hello.v0" }, - "runtimeId": { "type": "string", "minLength": 1 }, - "runtimeKind": { "type": "string", "minLength": 1 }, - "agentId": { "type": "string", "minLength": 1 }, - "role": { "type": "string", "minLength": 1 }, - "displayName": { "type": "string", "minLength": 1 }, - "supportedDocumentTypes": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "supportedMutationContracts": { - "type": "array", - "items": { - "$ref": "./cultnet.document-mutation-contract.schema.json" - } - }, - "supportedMessageVersions": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "transportProfiles": { - "type": "array", - "items": { - "$ref": "./cultnet.transport-profile.schema.json" - } - }, - "supportsSchemaCatalog": { "type": "boolean" } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.login-success.schema.json b/vendor/cultnet-rs/contracts/cultnet.login-success.schema.json deleted file mode 100644 index 3d8a459..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.login-success.schema.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.login-success.schema.json", - "title": "CultNet Login Success Message", - "type": "object", - "required": [ - "schemaVersion", - "nonce", - "session" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.login_success.v0" }, - "nonce": { "type": "string", "minLength": 1 }, - "session": { "type": "string", "minLength": 1 } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.login.schema.json b/vendor/cultnet-rs/contracts/cultnet.login.schema.json deleted file mode 100644 index 62f4959..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.login.schema.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.login.schema.json", - "title": "CultNet Login Message", - "type": "object", - "required": [ - "schemaVersion", - "nonce", - "auth", - "password" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.login.v0" }, - "nonce": { "type": "string", "minLength": 1 }, - "auth": { "type": "string", "minLength": 1 }, - "password": { "type": "string", "minLength": 1 } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.raw-document-record.schema.json b/vendor/cultnet-rs/contracts/cultnet.raw-document-record.schema.json deleted file mode 100644 index 3348a5f..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.raw-document-record.schema.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.raw-document-record.schema.json", - "title": "CultNet Raw Document Record", - "type": "object", - "required": [ - "schemaId", - "recordKey", - "storedAt", - "payloadEncoding", - "payload" - ], - "additionalProperties": false, - "properties": { - "schemaId": { "type": "string", "minLength": 1 }, - "recordKey": { "type": "string", "minLength": 1 }, - "storedAt": { "type": "string", "minLength": 1 }, - "payloadEncoding": { "const": "messagepack" }, - "payload": { - "type": "string", - "contentEncoding": "base64", - "x-cult-runtime-type": "bytes" - }, - "sourceRuntimeId": { "type": "string", "minLength": 1 }, - "sourceAgentId": { "type": "string", "minLength": 1 }, - "sourceRole": { "type": "string", "minLength": 1 }, - "tags": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.register.schema.json b/vendor/cultnet-rs/contracts/cultnet.register.schema.json deleted file mode 100644 index 77286f3..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.register.schema.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.register.schema.json", - "title": "CultNet Register Message", - "type": "object", - "required": [ - "schemaVersion", - "nonce", - "email", - "password", - "name" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.register.v0" }, - "nonce": { "type": "string", "minLength": 1 }, - "email": { "type": "string", "minLength": 1 }, - "password": { "type": "string", "minLength": 1 }, - "name": { "type": "string", "minLength": 1 } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.sample-change-name.schema.json b/vendor/cultnet-rs/contracts/cultnet.sample-change-name.schema.json deleted file mode 100644 index fc3fe05..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.sample-change-name.schema.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.sample-change-name.schema.json", - "title": "CultNet Sample Change Name Message", - "type": "object", - "required": [ - "schemaVersion", - "name" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.sample.change_name.v0" }, - "name": { "type": "string", "minLength": 1 } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.sample-chat.schema.json b/vendor/cultnet-rs/contracts/cultnet.sample-chat.schema.json deleted file mode 100644 index 8426fb5..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.sample-chat.schema.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.sample-chat.schema.json", - "title": "CultNet Sample Chat Message", - "type": "object", - "required": [ - "schemaVersion", - "text" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.sample.chat.v0" }, - "text": { "type": "string", "minLength": 1 } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.schema-catalog-request.schema.json b/vendor/cultnet-rs/contracts/cultnet.schema-catalog-request.schema.json deleted file mode 100644 index 611733d..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.schema-catalog-request.schema.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.schema-catalog-request.schema.json", - "title": "CultNet Schema Catalog Request Message", - "type": "object", - "required": [ - "schemaVersion", - "messageId" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.schema_catalog_request.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "includeSchemaJson": { "type": "boolean" }, - "schemaIds": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "kinds": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "wire_message", - "document_payload", - "shared_contract" - ] - } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.schema-catalog-response.schema.json b/vendor/cultnet-rs/contracts/cultnet.schema-catalog-response.schema.json deleted file mode 100644 index fa371e4..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.schema-catalog-response.schema.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.schema-catalog-response.schema.json", - "title": "CultNet Schema Catalog Response Message", - "type": "object", - "required": [ - "schemaVersion", - "messageId", - "schemas" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.schema_catalog_response.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "schemas": { - "type": "array", - "items": { - "$ref": "./cultnet.schema-descriptor.schema.json" - } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.schema-descriptor.schema.json b/vendor/cultnet-rs/contracts/cultnet.schema-descriptor.schema.json deleted file mode 100644 index 2379685..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.schema-descriptor.schema.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.schema-descriptor.schema.json", - "title": "CultNet Schema Descriptor", - "type": "object", - "required": [ - "schemaId", - "kind", - "wireContracts", - "contentHash" - ], - "additionalProperties": false, - "properties": { - "schemaId": { "type": "string", "minLength": 1 }, - "kind": { - "type": "string", - "enum": [ - "wire_message", - "document_payload", - "shared_contract" - ] - }, - "schemaVersion": { "type": "string", "minLength": 1 }, - "documentType": { "type": "string", "minLength": 1 }, - "title": { "type": "string", "minLength": 1 }, - "wireContracts": { - "type": "array", - "items": { - "type": "string", - "enum": [ - "cultnet.schema.v0", - "gamecult.networking.v0" - ] - } - }, - "contentHash": { "type": "string", "minLength": 1 }, - "schemaJson": { "type": "string", "minLength": 1 } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.shard-catalog-request.schema.json b/vendor/cultnet-rs/contracts/cultnet.shard-catalog-request.schema.json deleted file mode 100644 index c91ee09..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.shard-catalog-request.schema.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "cultnet.shard_catalog_request.v0", - "type": "object", - "required": ["schemaVersion", "messageId"], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.shard_catalog_request.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "schemaIds": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "recordKeys": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.shard-catalog-response.schema.json b/vendor/cultnet-rs/contracts/cultnet.shard-catalog-response.schema.json deleted file mode 100644 index b266178..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.shard-catalog-response.schema.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "cultnet.shard_catalog_response.v0", - "type": "object", - "required": ["schemaVersion", "messageId", "shards"], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.shard_catalog_response.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "shards": { - "type": "array", - "items": { "$ref": "#/$defs/shardDescriptor" } - } - }, - "$defs": { - "shardDescriptor": { - "type": "object", - "required": ["shardId", "ownerRuntimeId", "epoch"], - "additionalProperties": false, - "properties": { - "shardId": { "type": "string", "minLength": 1 }, - "ownerRuntimeId": { "type": "string", "minLength": 1 }, - "epoch": { "type": "integer", "minimum": 0 }, - "isPrimary": { "type": "boolean" }, - "schemaIds": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "keyPrefix": { "type": "string", "minLength": 1 }, - "primaryEndpoints": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "replicaEndpoints": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "readReplicaEndpoints": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "region": { "type": "string", "minLength": 1 }, - "authorityLeaseId": { "type": "string", "minLength": 1 } - } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.snapshot-request.schema.json b/vendor/cultnet-rs/contracts/cultnet.snapshot-request.schema.json deleted file mode 100644 index 712d705..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.snapshot-request.schema.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.snapshot-request.schema.json", - "title": "CultNet Snapshot Request Message", - "type": "object", - "required": [ - "schemaVersion", - "messageId" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.snapshot_request.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "schemaIds": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "recordKeys": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.snapshot-response-raw.schema.json b/vendor/cultnet-rs/contracts/cultnet.snapshot-response-raw.schema.json deleted file mode 100644 index a84bc50..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.snapshot-response-raw.schema.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.snapshot-response-raw.schema.json", - "title": "CultNet Raw Snapshot Response Message", - "type": "object", - "required": [ - "schemaVersion", - "messageId", - "documents" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.snapshot_response_raw.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "documents": { - "type": "array", - "items": { - "$ref": "./cultnet.raw-document-record.schema.json" - } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.snapshot-response.schema.json b/vendor/cultnet-rs/contracts/cultnet.snapshot-response.schema.json deleted file mode 100644 index 940822c..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.snapshot-response.schema.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.snapshot-response.schema.json", - "title": "CultNet Snapshot Response Message", - "type": "object", - "required": [ - "schemaVersion", - "messageId", - "documents" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.snapshot_response.v0" }, - "messageId": { "type": "string", "minLength": 1 }, - "documents": { - "type": "array", - "items": { - "$ref": "./cultnet.document-record.schema.json" - } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.transport-profile.schema.json b/vendor/cultnet-rs/contracts/cultnet.transport-profile.schema.json deleted file mode 100644 index 90261c7..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.transport-profile.schema.json +++ /dev/null @@ -1,100 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.transport-profile.schema.json", - "title": "CultNet Transport Profile", - "type": "object", - "required": [ - "schemaVersion", - "runtimeId", - "transports" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.transport_profile.v0" }, - "runtimeId": { "type": "string", "minLength": 1 }, - "transports": { - "type": "array", - "items": { - "type": "object", - "required": [ - "transportId", - "protocol", - "channels" - ], - "additionalProperties": false, - "properties": { - "transportId": { "type": "string", "minLength": 1 }, - "protocol": { - "type": "string", - "enum": [ - "tcp_framed", - "litenetlib", - "websocket", - "rudp" - ] - }, - "host": { "type": "string", "minLength": 1 }, - "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, - "path": { "type": "string", "minLength": 1 }, - "discoveryGroup": { "type": "string", "minLength": 1 }, - "wireContracts": { - "type": "array", - "items": { "type": "string", "minLength": 1 } - }, - "reconnectPolicy": { - "type": "object", - "required": [ - "schemaVersion", - "policyId", - "baseDelayMs", - "maxDelayMs", - "maxJitterMs" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.reconnect_policy.v0" }, - "policyId": { "type": "string", "minLength": 1 }, - "baseDelayMs": { "type": "integer", "minimum": 0 }, - "maxDelayMs": { "type": "integer", "minimum": 0 }, - "maxJitterMs": { "type": "integer", "minimum": 0 }, - "maxAttempts": { "type": "integer", "minimum": 1 } - } - }, - "channels": { - "type": "array", - "items": { - "type": "object", - "required": [ - "channelId", - "delivery", - "ordering" - ], - "additionalProperties": false, - "properties": { - "channelId": { "type": "string", "minLength": 1 }, - "delivery": { - "type": "string", - "enum": [ - "reliable", - "unreliable" - ] - }, - "ordering": { - "type": "string", - "enum": [ - "ordered", - "unordered", - "sequenced" - ] - }, - "maxPayloadBytes": { "type": "integer", "minimum": 1 }, - "maxFragmentBytes": { "type": "integer", "minimum": 1 }, - "maxPendingReliablePackets": { "type": "integer", "minimum": 1 } - } - } - } - } - } - } - } -} diff --git a/vendor/cultnet-rs/contracts/cultnet.verify.schema.json b/vendor/cultnet-rs/contracts/cultnet.verify.schema.json deleted file mode 100644 index 1f992aa..0000000 --- a/vendor/cultnet-rs/contracts/cultnet.verify.schema.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/cultnet-ts/contracts/cultnet.verify.schema.json", - "title": "CultNet Verify Message", - "type": "object", - "required": [ - "schemaVersion", - "nonce", - "session" - ], - "additionalProperties": false, - "properties": { - "schemaVersion": { "const": "cultnet.verify.v0" }, - "nonce": { "type": "string", "minLength": 1 }, - "session": { "type": "string", "minLength": 1 } - } -} diff --git a/vendor/cultnet-rs/contracts/ghostlight.agent-state.schema.json b/vendor/cultnet-rs/contracts/ghostlight.agent-state.schema.json deleted file mode 100644 index 6437cd0..0000000 --- a/vendor/cultnet-rs/contracts/ghostlight.agent-state.schema.json +++ /dev/null @@ -1,845 +0,0 @@ -{ - "$schema": "https://json-schema.org/draft/2020-12/schema", - "$id": "https://github.com/GameCult/Ghostlight/schemas/agent-state.schema.json", - "title": "Ghostlight Agent State Schema", - "description": "First canonical contract for Ghostlight agent, perception, relationship, event, scene, and dialogue-context state.", - "type": "object", - "required": [ - "schema_version", - "world", - "agents", - "relationships", - "events", - "scenes" - ], - "additionalProperties": false, - "properties": { - "schema_version": { - "const": "ghostlight.agent_state.v0" - }, - "world": { - "$ref": "#/$defs/world_state" - }, - "agents": { - "type": "array", - "items": { - "$ref": "#/$defs/agent" - } - }, - "relationships": { - "type": "array", - "items": { - "$ref": "#/$defs/relationship" - } - }, - "events": { - "type": "array", - "items": { - "$ref": "#/$defs/event" - } - }, - "scenes": { - "type": "array", - "items": { - "$ref": "#/$defs/scene" - } - } - }, - "$defs": { - "world_state": { - "type": "object", - "required": [ - "world_id", - "setting", - "time", - "canon_context" - ], - "additionalProperties": false, - "properties": { - "world_id": { - "type": "string" - }, - "setting": { - "type": "string" - }, - "time": { - "type": "object", - "required": [ - "label" - ], - "additionalProperties": true, - "properties": { - "label": { - "type": "string" - } - } - }, - "canon_context": { - "type": "array", - "items": { - "type": "string" - } - }, - "ambient_pressures": { - "type": "array", - "items": { - "$ref": "#/$defs/pressure" - } - } - } - }, - "agent": { - "type": "object", - "required": [ - "agent_id", - "identity", - "canonical_state", - "goals", - "memories", - "perceived_state_overlays" - ], - "additionalProperties": false, - "properties": { - "agent_id": { - "type": "string" - }, - "identity": { - "$ref": "#/$defs/identity" - }, - "canonical_state": { - "$ref": "#/$defs/canonical_state" - }, - "goals": { - "type": "array", - "items": { - "$ref": "#/$defs/goal" - } - }, - "memories": { - "$ref": "#/$defs/memory_bundle" - }, - "perceived_state_overlays": { - "type": "array", - "items": { - "$ref": "#/$defs/perceived_state_overlay" - } - } - } - }, - "identity": { - "type": "object", - "required": [ - "name", - "roles", - "origin", - "public_description" - ], - "additionalProperties": false, - "properties": { - "name": { - "type": "string" - }, - "roles": { - "type": "array", - "items": { - "type": "string" - } - }, - "origin": { - "type": "string" - }, - "public_description": { - "type": "string" - }, - "private_notes": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "canonical_state": { - "type": "object", - "required": [ - "underlying_organization", - "stable_dispositions", - "behavioral_dimensions", - "presentation_strategy", - "voice_style", - "situational_state", - "values" - ], - "additionalProperties": false, - "properties": { - "underlying_organization": { - "$ref": "#/$defs/variable_map" - }, - "stable_dispositions": { - "$ref": "#/$defs/variable_map" - }, - "behavioral_dimensions": { - "$ref": "#/$defs/variable_map" - }, - "presentation_strategy": { - "$ref": "#/$defs/variable_map" - }, - "situational_state": { - "$ref": "#/$defs/variable_map" - }, - "values": { - "type": "array", - "items": { - "$ref": "#/$defs/value_commitment" - } - }, - "voice_style": { - "$ref": "#/$defs/variable_map" - } - } - }, - "variable_map": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/state_variable" - } - }, - "state_variable": { - "type": "object", - "required": [ - "mean", - "plasticity", - "current_activation" - ], - "additionalProperties": false, - "properties": { - "mean": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "plasticity": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "current_activation": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "evidence": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "value_commitment": { - "type": "object", - "required": [ - "value_id", - "label", - "priority", - "unforgivable_if_betrayed" - ], - "additionalProperties": false, - "properties": { - "value_id": { - "type": "string" - }, - "label": { - "type": "string" - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "unforgivable_if_betrayed": { - "type": "boolean" - } - } - }, - "goal": { - "type": "object", - "required": [ - "goal_id", - "description", - "scope", - "priority", - "emotional_stake", - "status" - ], - "additionalProperties": false, - "properties": { - "goal_id": { - "type": "string" - }, - "description": { - "type": "string" - }, - "scope": { - "enum": [ - "immediate", - "scene", - "case", - "arc", - "life" - ] - }, - "priority": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "emotional_stake": { - "type": "string" - }, - "blockers": { - "type": "array", - "items": { - "type": "string" - } - }, - "status": { - "enum": [ - "active", - "blocked", - "dormant", - "resolved", - "abandoned" - ] - } - } - }, - "memory_bundle": { - "type": "object", - "required": [ - "episodic", - "semantic", - "relationship_summaries" - ], - "additionalProperties": false, - "properties": { - "episodic": { - "type": "array", - "items": { - "$ref": "#/$defs/memory" - } - }, - "semantic": { - "type": "array", - "items": { - "$ref": "#/$defs/memory" - } - }, - "relationship_summaries": { - "type": "array", - "items": { - "$ref": "#/$defs/memory" - } - } - } - }, - "memory": { - "type": "object", - "required": [ - "memory_id", - "summary", - "salience", - "confidence" - ], - "additionalProperties": false, - "properties": { - "memory_id": { - "type": "string" - }, - "summary": { - "type": "string" - }, - "salience": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "confidence": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "linked_event_ids": { - "type": "array", - "items": { - "type": "string" - } - }, - "linked_relationship_id": { - "type": "string" - } - } - }, - "perceived_state_overlay": { - "type": "object", - "required": [ - "observer_agent_id", - "target_agent_id", - "perceived_dimensions", - "beliefs", - "distortions" - ], - "additionalProperties": false, - "properties": { - "observer_agent_id": { - "type": "string" - }, - "target_agent_id": { - "type": "string" - }, - "perceived_dimensions": { - "$ref": "#/$defs/inferred_variable_map" - }, - "beliefs": { - "type": "array", - "items": { - "$ref": "#/$defs/belief" - } - }, - "distortions": { - "type": "array", - "items": { - "enum": [ - "hostile_attribution", - "false_reassurance", - "overread_warmth", - "underread_threat", - "false_mask_detection", - "missed_mask_detection", - "status_injury", - "threat_amplification", - "institutional_suspicion_transfer", - "care_underread", - "mask_overfocus" - ] - } - } - } - }, - "belief": { - "type": "object", - "required": [ - "belief_id", - "claim", - "confidence" - ], - "additionalProperties": false, - "properties": { - "belief_id": { - "type": "string" - }, - "claim": { - "type": "string" - }, - "confidence": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "subject_id": { - "type": "string" - }, - "claim_type": { - "enum": [ - "fact", - "identity_inference", - "motive_inference", - "prediction", - "norm", - "value_read", - "relationship_read" - ] - }, - "evidence_event_ids": { - "type": "array", - "items": { - "type": "string" - } - }, - "visibility": { - "enum": [ - "private", - "shared", - "public", - "rumor" - ] - }, - "emotional_charge": { - "type": "number", - "minimum": 0, - "maximum": 1 - } - } - }, - "relationship": { - "type": "object", - "required": [ - "relationship_id", - "source_id", - "target_id", - "stance", - "summary" - ], - "additionalProperties": false, - "properties": { - "relationship_id": { - "type": "string" - }, - "source_id": { - "type": "string" - }, - "target_id": { - "type": "string" - }, - "stance": { - "$ref": "#/$defs/variable_map" - }, - "summary": { - "type": "string" - }, - "unresolved_incident_ids": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "event": { - "type": "object", - "required": [ - "event_id", - "kind", - "summary", - "participants", - "pressure_tags" - ], - "additionalProperties": false, - "properties": { - "event_id": { - "type": "string" - }, - "kind": { - "enum": [ - "dialogue", - "action", - "memory", - "relationship_shift", - "scene_outcome", - "author_note" - ] - }, - "summary": { - "type": "string" - }, - "participants": { - "type": "array", - "items": { - "type": "string" - } - }, - "pressure_tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "observed_exchange": { - "type": "array", - "items": { - "$ref": "#/$defs/event_exchange_turn" - } - }, - "private_interpretations": { - "type": "array", - "items": { - "$ref": "#/$defs/event_private_interpretation" - } - }, - "event_effects": { - "type": "array", - "items": { - "$ref": "#/$defs/event_effect" - } - } - } - }, - "scene": { - "type": "object", - "required": [ - "scene_id", - "location", - "participants", - "public_stakes", - "hidden_stakes", - "dialogue_context_packs" - ], - "additionalProperties": false, - "properties": { - "scene_id": { - "type": "string" - }, - "location": { - "type": "string" - }, - "participants": { - "type": "array", - "items": { - "type": "string" - } - }, - "public_stakes": { - "type": "array", - "items": { - "type": "string" - } - }, - "hidden_stakes": { - "type": "array", - "items": { - "type": "string" - } - }, - "dialogue_context_packs": { - "type": "array", - "items": { - "$ref": "#/$defs/dialogue_context_pack" - } - } - } - }, - "dialogue_context_pack": { - "type": "object", - "required": [ - "speaker_agent_id", - "listener_ids", - "speaker_local_truth", - "speaker_beliefs", - "active_memories", - "active_goals", - "presentation_constraints" - ], - "additionalProperties": false, - "properties": { - "speaker_agent_id": { - "type": "string" - }, - "listener_ids": { - "type": "array", - "items": { - "type": "string" - } - }, - "speaker_local_truth": { - "type": "array", - "items": { - "type": "string" - } - }, - "speaker_beliefs": { - "type": "array", - "items": { - "type": "string" - } - }, - "active_memories": { - "type": "array", - "items": { - "type": "string" - } - }, - "active_goals": { - "type": "array", - "items": { - "type": "string" - } - }, - "presentation_constraints": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "pressure": { - "type": "object", - "required": [ - "pressure_id", - "label", - "intensity" - ], - "additionalProperties": false, - "properties": { - "pressure_id": { - "type": "string" - }, - "label": { - "type": "string" - }, - "intensity": { - "type": "number", - "minimum": 0, - "maximum": 1 - } - } - }, - "inferred_variable_map": { - "type": "object", - "additionalProperties": { - "$ref": "#/$defs/inferred_state_variable" - } - }, - "inferred_state_variable": { - "type": "object", - "required": [ - "observed_activation", - "attributed_disposition", - "confidence" - ], - "additionalProperties": false, - "properties": { - "observed_activation": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "attributed_disposition": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "confidence": { - "type": "number", - "minimum": 0, - "maximum": 1 - }, - "evidence": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "event_exchange_turn": { - "type": "object", - "required": [ - "speaker_id", - "utterance_summary", - "dialogue_function" - ], - "additionalProperties": false, - "properties": { - "speaker_id": { - "type": "string" - }, - "utterance_summary": { - "type": "string" - }, - "dialogue_function": { - "type": "string" - }, - "observable_cues": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "event_private_interpretation": { - "type": "object", - "required": [ - "agent_id", - "interpretation", - "confidence" - ], - "additionalProperties": false, - "properties": { - "agent_id": { - "type": "string" - }, - "interpretation": { - "type": "string" - }, - "attributed_motive": { - "type": "string" - }, - "appraisal_tags": { - "type": "array", - "items": { - "type": "string" - } - }, - "confidence": { - "type": "number", - "minimum": 0, - "maximum": 1 - } - } - }, - "event_effect": { - "type": "object", - "required": [ - "agent_id", - "summary" - ], - "additionalProperties": false, - "properties": { - "agent_id": { - "type": "string" - }, - "summary": { - "type": "string" - }, - "state_deltas": { - "type": "array", - "items": { - "$ref": "#/$defs/state_delta" - } - }, - "new_belief_ids": { - "type": "array", - "items": { - "type": "string" - } - }, - "memory_update_ids": { - "type": "array", - "items": { - "type": "string" - } - } - } - }, - "state_delta": { - "type": "object", - "required": [ - "path", - "delta" - ], - "additionalProperties": false, - "properties": { - "path": { - "type": "string" - }, - "delta": { - "type": "number", - "minimum": -1, - "maximum": 1 - } - } - } - } -} diff --git a/vendor/cultnet-rs/examples/cultnet_interop_peer.rs b/vendor/cultnet-rs/examples/cultnet_interop_peer.rs deleted file mode 100644 index ab00c6c..0000000 --- a/vendor/cultnet-rs/examples/cultnet_interop_peer.rs +++ /dev/null @@ -1,1628 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; -use cultcache_rs::CultCache; -use cultcache_rs::DatabaseEntry; -use cultcache_rs::SingleFileMessagePackBackingStore; -use cultnet_rs::CultNetDocumentBinding; -use cultnet_rs::CultNetDocumentMutationContract; -use cultnet_rs::CultNetDocumentOperation; -use cultnet_rs::CultNetDocumentPutOptions; -use cultnet_rs::CultNetDocumentRegistry; -use cultnet_rs::CultNetMessage; -use cultnet_rs::CultNetMutationAuthority; -use cultnet_rs::CultNetRudpPacketType; -use cultnet_rs::CultNetRudpSendOptions; -use cultnet_rs::CultNetRudpSession; -use cultnet_rs::CultNetRudpSessionOptions; -use cultnet_rs::CultNetRudpSocketTransportConnection; -use cultnet_rs::CultNetRudpSocketTransportOptions; -use cultnet_rs::CultNetSchemaKind; -use cultnet_rs::CultNetSchemaRegistration; -use cultnet_rs::CultNetSchemaRegistry; -use cultnet_rs::CultNetTransportProfile; -use cultnet_rs::CultNetWireContract; -use cultnet_rs::RudpTransportProfileOptions; -use cultnet_rs::TcpFramedTransportConnection; -use cultnet_rs::TcpFramedTransportProfileOptions; -use cultnet_rs::builtin_schema_registry; -use cultnet_rs::create_rudp_transport_profile; -use cultnet_rs::create_tcp_framed_transport_profile; -use cultnet_rs::decode_cultnet_message_from_slice; -use cultnet_rs::decode_rudp_packet; -use cultnet_rs::encode_cultnet_message_to_vec; -use cultnet_rs::encode_rudp_packet; -use serde::Deserialize; -use serde::Serialize; -use socket2::Domain; -use socket2::Protocol; -use socket2::Socket; -use socket2::Type; -use std::collections::BTreeMap; -use std::fs; -use std::net::Ipv4Addr; -use std::net::SocketAddr; -use std::net::SocketAddrV4; -use std::net::TcpListener; -use std::net::TcpStream; -use std::net::UdpSocket; -use std::sync::Arc; -use std::sync::Mutex; -use std::thread; -use std::time::Duration; -use std::time::Instant; - -const INTEROP_DOCUMENT_TYPE: &str = "cultnet.interop-note"; -const INTEROP_SCHEMA_VERSION: &str = "cultnet.interop_note.v0"; -const MUTATION_INTENT_TYPE: &str = "cultnet.interop-note-mutation-intent"; -const MUTATION_INTENT_SCHEMA_ID: &str = "https://github.com/GameCult/cultnet-ts/integration/contracts/cultnet.interop-note-mutation-intent.schema.json"; -const MUTATION_INTENT_SCHEMA_VERSION: &str = "cultnet.interop_note_mutation_intent.v0"; -const MUTATION_RECEIPT_TYPE: &str = "cultnet.interop-note-mutation-receipt"; -const MUTATION_RECEIPT_SCHEMA_ID: &str = "https://github.com/GameCult/cultnet-ts/integration/contracts/cultnet.interop-note-mutation-receipt.schema.json"; -const MUTATION_RECEIPT_SCHEMA_VERSION: &str = "cultnet.interop_note_mutation_receipt.v0"; -const FIRE_COMMAND_TYPE: &str = "cultnet.interop-fire-weapon-command"; -const FIRE_COMMAND_SCHEMA_ID: &str = "https://github.com/GameCult/cultnet-ts/integration/contracts/cultnet.interop-fire-weapon-command.schema.json"; -const FIRE_COMMAND_SCHEMA_VERSION: &str = "cultnet.interop_fire_weapon_command.v0"; -const FIRE_RECEIPT_TYPE: &str = "cultnet.interop-fire-weapon-receipt"; -const FIRE_RECEIPT_SCHEMA_ID: &str = "https://github.com/GameCult/cultnet-ts/integration/contracts/cultnet.interop-fire-weapon-receipt.schema.json"; -const FIRE_RECEIPT_SCHEMA_VERSION: &str = "cultnet.interop_fire_weapon_receipt.v0"; -const DISCOVERY_ANNOUNCE_SCHEMA_VERSION: &str = "cultnet.discovery_announce.v0"; -const RUDP_INTEROP_CONNECTION_ID: u32 = 0x43554C54; -const RUDP_INTEROP_MAX_FRAGMENT_BYTES: usize = 1024; -const RUDP_INTEROP_RESEND_DELAY_MS: u64 = 25; -const RUDP_INTEROP_READ_TIMEOUT: Duration = Duration::from_secs(15); - -#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache(type = "cultnet.interop-note", schema = "CultNetInteropNote")] -struct CultNetInteropNote { - #[cultcache(key = 0)] - schema_version: String, - #[cultcache(key = 1)] - document_id: String, - #[cultcache(key = 2)] - author_runtime_id: String, - #[cultcache(key = 3)] - title: String, - #[cultcache(key = 4)] - body: String, - #[cultcache(key = 5)] - tags: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache( - type = "cultnet.interop-note-mutation-intent", - schema = "CultNetInteropMutationIntent" -)] -struct CultNetInteropMutationIntent { - #[cultcache(key = 0)] - schema_version: String, - #[cultcache(key = 1)] - intent_id: String, - #[cultcache(key = 2)] - target_document_id: String, - #[cultcache(key = 3)] - append_body: String, - #[cultcache(key = 4)] - append_tag: String, -} - -#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache( - type = "cultnet.interop-note-mutation-receipt", - schema = "CultNetInteropMutationReceipt" -)] -struct CultNetInteropMutationReceipt { - #[cultcache(key = 0)] - schema_version: String, - #[cultcache(key = 1)] - intent_id: String, - #[cultcache(key = 2)] - accepted: bool, - #[cultcache(key = 3)] - document_id: String, - #[cultcache(key = 4)] - body: String, - #[cultcache(key = 5)] - tags: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache( - type = "cultnet.interop-fire-weapon-command", - schema = "CultNetInteropFireCommand" -)] -struct CultNetInteropFireCommand { - #[cultcache(key = 0)] - schema_version: String, - #[cultcache(key = 1)] - command_id: String, - #[cultcache(key = 2)] - character_id: String, - #[cultcache(key = 3)] - weapon_id: String, -} - -#[derive(Clone, Debug, PartialEq, Eq, DatabaseEntry)] -#[cultcache( - type = "cultnet.interop-fire-weapon-receipt", - schema = "CultNetInteropFireReceipt" -)] -struct CultNetInteropFireReceipt { - #[cultcache(key = 0)] - schema_version: String, - #[cultcache(key = 1)] - command_id: String, - #[cultcache(key = 2)] - accepted: bool, - #[cultcache(key = 3)] - character_id: String, - #[cultcache(key = 4)] - weapon_id: String, - #[cultcache(key = 5)] - shots_fired: u32, - #[cultcache(key = 6)] - ammo_remaining: u32, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(tag = "schemaVersion", rename_all = "camelCase")] -enum DiscoveryMessage { - #[serde(rename = "cultnet.discovery_probe.v0", rename_all = "camelCase")] - Probe { - message_id: String, - requester_runtime_id: String, - }, - #[serde(rename = "cultnet.discovery_announce.v0", rename_all = "camelCase")] - Announce { - message_id: String, - runtime_id: String, - runtime_kind: String, - display_name: String, - agent_id: Option, - tcp_host: String, - tcp_port: u16, - wire_contract: String, - supported_document_types: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - transport_profiles: Option>, - supports_schema_catalog: bool, - }, -} - -#[derive(Clone, Debug)] -struct PeerConfig { - runtime_id: String, - runtime_kind: String, - display_name: String, - agent_id: String, - bind_host: String, - advertise_host: String, - tcp_port: u16, - rudp_port: u16, - discovery_port: u16, - discovery_group: Ipv4Addr, - schema_path: String, -} - -#[derive(Clone, Debug)] -struct DialConfig { - runtime_id: String, - runtime_kind: String, - display_name: String, - agent_id: String, - target_host: String, - target_port: Option, - target_rudp_port: Option, - schema_path: String, -} - -trait SchemaMessageSender { - fn send_schema_message(&mut self, message: &CultNetMessage) -> Result<()>; -} - -trait SchemaMessageTransport: SchemaMessageSender { - fn read_schema_message(&mut self) -> Result; -} - -fn main() -> Result<()> { - let mut args = std::env::args().skip(1); - let mode = args.next().ok_or_else(|| { - anyhow!("expected mode: serve | probe | dial | rudp-serve-once | rudp-dial-once | rudp-serve-message-once | rudp-dial-message-once") - })?; - let options = parse_args(args.collect()); - - match mode.as_str() { - "serve" => serve(parse_peer_config(&options)?)?, - "probe" => probe(&options)?, - "dial" => dial(parse_dial_config(&options)?)?, - "rudp-serve-once" => rudp_serve_once(&options)?, - "rudp-dial-once" => rudp_dial_once(&options)?, - "rudp-serve-message-once" => rudp_serve_message_once(&options)?, - "rudp-dial-message-once" => rudp_dial_message_once(&options)?, - _ => return Err(anyhow!("unknown mode {mode}")), - } - - Ok(()) -} - -fn serve(config: PeerConfig) -> Result<()> { - let schema_registration = load_schema_registration(&config.schema_path)?; - let mut schema_registry = builtin_schema_registry()?; - schema_registry.register(schema_registration.clone())?; - - let mut cache = CultCache::new(); - cache.register_entry_type::()?; - register_capability_entry_types(&mut cache)?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(runtime_store_path( - &config.runtime_id, - ))); - cache.pull_all_backing_stores()?; - let note = build_note(&config.runtime_id, &config.display_name); - cache.put(¬e.document_id, ¬e)?; - - let mut document_registry = CultNetDocumentRegistry::new(); - register_capability_bindings(&mut document_registry, &schema_registration.schema_id); - - let cache = Arc::new(Mutex::new(cache)); - let document_registry = Arc::new(document_registry); - let schema_registry = Arc::new(schema_registry); - let config = Arc::new(config); - - start_udp_discovery_server(config.clone())?; - start_tcp_server( - config.clone(), - cache.clone(), - document_registry.clone(), - schema_registry.clone(), - )?; - start_rudp_server(config.clone(), cache, document_registry, schema_registry)?; - - print_json(&serde_json::json!({ - "status": "ready", - "mode": "serve", - "runtimeId": config.runtime_id, - "runtimeKind": config.runtime_kind, - "tcpPort": config.tcp_port, - "rudpPort": config.rudp_port, - "discoveryPort": config.discovery_port, - "discoveryGroup": config.discovery_group.to_string(), - }))?; - - loop { - thread::sleep(Duration::from_secs(3600)); - } -} - -fn probe(options: &BTreeMap) -> Result<()> { - let runtime_id = require_arg(options, "runtime-id")?.to_string(); - let discovery_port = parse_u16_arg(options, "discovery-port")?; - let discovery_group = parse_ipv4_arg(options, "discovery-group")?; - let timeout_ms = parse_u64_arg(options, "timeout-ms").unwrap_or(1_500); - let message_id = format!("{runtime_id}-{}", chrono::Utc::now().timestamp_millis()); - - let socket = create_discovery_socket(0, false)?; - socket.set_read_timeout(Some(Duration::from_millis(timeout_ms)))?; - - let probe_message = DiscoveryMessage::Probe { - message_id: message_id.clone(), - requester_runtime_id: runtime_id.clone(), - }; - let payload = rmp_serde::to_vec_named(&probe_message)?; - socket.send_to( - &payload, - SocketAddr::V4(SocketAddrV4::new(discovery_group, discovery_port)), - )?; - - let mut buffer = vec![0_u8; 4096]; - let mut found = BTreeMap::::new(); - loop { - match socket.recv_from(&mut buffer) { - Ok((len, _)) => { - if let Ok(DiscoveryMessage::Announce { - message_id: response_message_id, - runtime_id, - runtime_kind, - display_name, - agent_id, - tcp_host, - tcp_port, - wire_contract, - supported_document_types, - transport_profiles, - supports_schema_catalog, - }) = rmp_serde::from_slice::(&buffer[..len]) - { - if response_message_id == message_id { - found.insert( - runtime_id.clone(), - serde_json::json!({ - "schemaVersion": DISCOVERY_ANNOUNCE_SCHEMA_VERSION, - "messageId": response_message_id, - "runtimeId": runtime_id, - "runtimeKind": runtime_kind, - "displayName": display_name, - "agentId": agent_id, - "tcpHost": tcp_host, - "tcpPort": tcp_port, - "wireContract": wire_contract, - "supportedDocumentTypes": supported_document_types, - "transportProfiles": transport_profiles, - "supportsSchemaCatalog": supports_schema_catalog, - }), - ); - } - } - } - Err(error) - if error.kind() == std::io::ErrorKind::WouldBlock - || error.kind() == std::io::ErrorKind::TimedOut => - { - break; - } - Err(error) => return Err(error.into()), - } - } - - print_json(&serde_json::json!({ - "mode": "probe", - "runtimeId": runtime_id, - "peers": found.into_values().collect::>(), - }))?; - Ok(()) -} - -fn dial(config: DialConfig) -> Result<()> { - let schema_registration = load_schema_registration(&config.schema_path)?; - - let mut cache = CultCache::new(); - cache.register_entry_type::()?; - register_capability_entry_types(&mut cache)?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(runtime_store_path( - &format!("{}-dial", config.runtime_id), - ))); - cache.pull_all_backing_stores()?; - - let mut document_registry = CultNetDocumentRegistry::new(); - register_capability_bindings(&mut document_registry, &schema_registration.schema_id); - - let transport_name = if config.target_rudp_port.is_some() { - "rudp" - } else { - "tcp" - }; - let mut transport = open_dial_transport(&config)?; - - transport.send_schema_message(&CultNetMessage::Hello { - runtime_id: config.runtime_id.clone(), - runtime_kind: config.runtime_kind.clone(), - agent_id: Some(config.agent_id.clone()), - role: None, - display_name: Some(config.display_name.clone()), - supported_document_types: Some(vec![INTEROP_DOCUMENT_TYPE.to_string()]), - supported_mutation_contracts: Some(interaction_contracts()), - supported_message_versions: Some(vec![INTEROP_SCHEMA_VERSION.to_string()]), - transport_profiles: Some(dial_transport_profiles(&config)), - supports_schema_catalog: Some(true), - })?; - - let remote_hello = transport - .read_schema_message() - .context("waiting for remote hello")?; - let remote_runtime_id = match &remote_hello { - CultNetMessage::Hello { runtime_id, .. } => runtime_id.clone(), - other => return Err(anyhow!("expected hello response, got {other:?}")), - }; - - transport.send_schema_message(&CultNetMessage::SchemaCatalogRequest { - message_id: format!("{}-catalog", config.runtime_id), - include_schema_json: Some(true), - schema_ids: None, - kinds: None, - })?; - let catalog_response = transport - .read_schema_message() - .context("waiting for schema catalog response")?; - let has_interop_schema = match &catalog_response { - CultNetMessage::SchemaCatalogResponse { schemas, .. } => schemas.iter().any(|schema| { - schema.schema_id == schema_registration.schema_id - && schema.document_type.as_deref() == Some(INTEROP_DOCUMENT_TYPE) - }), - other => return Err(anyhow!("expected catalog response, got {other:?}")), - }; - - transport.send_schema_message(&CultNetMessage::SnapshotRequest { - message_id: format!("{}-snapshot", config.runtime_id), - schema_ids: Some(vec![schema_registration.schema_id.clone()]), - record_keys: None, - })?; - let snapshot_response = transport - .read_schema_message() - .context("waiting for snapshot response")?; - let applied = document_registry - .apply_raw_snapshot_response::(&mut cache, &snapshot_response)?; - let note = applied - .into_iter() - .find(|candidate| candidate.author_runtime_id == remote_runtime_id) - .ok_or_else(|| anyhow!("did not receive an interop note from {remote_runtime_id}"))?; - - print_json(&serde_json::json!({ - "mode": "dial", - "transport": transport_name, - "runtimeId": config.runtime_id, - "targetHost": config.target_host, - "targetPort": config.target_rudp_port.or(config.target_port), - "remoteHello": { - "schemaVersion": "cultnet.hello.v0", - "runtimeId": remote_runtime_id, - }, - "hasInteropSchema": has_interop_schema, - "retrievedNote": { - "schemaVersion": note.schema_version, - "documentId": note.document_id, - "authorRuntimeId": note.author_runtime_id, - "title": note.title, - "body": note.body, - "tags": note.tags, - }, - "mutatedNote": mutate_remote_note(&mut *transport, &mut cache, &document_registry, &schema_registration.schema_id, &config.runtime_id, ¬e)?, - "fireReceipt": fire_remote_weapon(&mut *transport, &mut cache, &document_registry, &config.runtime_id, &remote_runtime_id)?, - }))?; - Ok(()) -} - -fn rudp_serve_once(options: &BTreeMap) -> Result<()> { - let bind_host = options - .get("bind-host") - .cloned() - .unwrap_or_else(|| "127.0.0.1".to_string()); - let bind_port = options - .get("bind-port") - .map(|value| value.parse::()) - .transpose() - .with_context(|| "argument --bind-port must be a u16")? - .unwrap_or(0); - let expected_client_payload = options - .get("client-payload") - .map(|value| value.as_bytes().to_vec()) - .unwrap_or_else(|| b"ts-rust-client-state".to_vec()); - let server_payload = options - .get("server-payload") - .map(|value| value.as_bytes().to_vec()) - .unwrap_or_else(|| b"rust-server-state".to_vec()); - let server_extra_payload = options - .get("server-extra-payload") - .map(|value| value.as_bytes().to_vec()); - let disconnect_reason = options - .get("disconnect-reason") - .map(|value| value.as_bytes().to_vec()); - let max_fragment_bytes = options - .get("max-fragment-bytes") - .map(|value| value.parse::()) - .transpose() - .with_context(|| "argument --max-fragment-bytes must be a u32")?; - let socket = UdpSocket::bind((bind_host.as_str(), bind_port)) - .with_context(|| format!("failed to bind RUDP server on {bind_host}:{bind_port}"))?; - socket.set_read_timeout(Some(Duration::from_millis(20)))?; - let local_port = socket.local_addr()?.port(); - let mut transport_options = - CultNetRudpSocketTransportOptions::server("rust-rudp-interop", socket, 0x22446688); - transport_options.initial_sequence = 100; - transport_options.resend_delay_ms = 25; - transport_options.max_fragment_bytes = max_fragment_bytes; - let mut transport = CultNetRudpSocketTransportConnection::new(transport_options)?; - - print_json(&serde_json::json!({ - "status": "ready", - "port": local_port, - }))?; - - let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline { - if let Some(frame) = transport.receive_once()? { - if frame.channel_id != "schema" || frame.payload != expected_client_payload { - return Err(anyhow!( - "unexpected RUDP frame: channel={} payload={:?}", - frame.channel_id, - frame.payload - )); - } - transport.send("schema", server_payload.clone())?; - if let Some(extra_payload) = &server_extra_payload { - transport.send("schema", extra_payload.clone())?; - } - if let Some(reason) = &disconnect_reason { - transport.disconnect(reason.clone())?; - } - poll_rudp_after_send(&mut transport, Duration::from_millis(250))?; - print_json(&serde_json::json!({ "status": "ok" }))?; - return Ok(()); - } - transport.poll_resends()?; - thread::sleep(Duration::from_millis(5)); - } - - Err(anyhow!("timed out waiting for TypeScript RUDP frame")) -} - -fn rudp_dial_once(options: &BTreeMap) -> Result<()> { - let target_host = require_arg(options, "target-host")?; - let target_port = parse_u16_arg(options, "target-port")?; - let remote_addr: SocketAddr = format!("{target_host}:{target_port}") - .parse() - .with_context(|| { - format!("failed to parse RUDP remote endpoint {target_host}:{target_port}") - })?; - let socket = UdpSocket::bind(("127.0.0.1", 0))?; - socket.set_read_timeout(Some(Duration::from_millis(20)))?; - let mut transport_options = CultNetRudpSocketTransportOptions::client( - "rust-rudp-client-interop", - socket, - remote_addr, - 0x88664422, - ); - transport_options.resend_delay_ms = 25; - let mut transport = CultNetRudpSocketTransportConnection::new(transport_options)?; - transport.connect(b"rust-join".to_vec())?; - - let mut sent = false; - let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline { - if let Some(frame) = transport.receive_once()? { - if frame.channel_id != "schema" || frame.payload != b"ts-rust-server-state" { - return Err(anyhow!( - "unexpected RUDP frame: channel={} payload={:?}", - frame.channel_id, - frame.payload - )); - } - print_json(&serde_json::json!({ "status": "ok" }))?; - return Ok(()); - } - transport.poll_resends()?; - if transport.connected() && !sent { - transport.send("schema", b"rust-client-state".to_vec())?; - sent = true; - } - thread::sleep(Duration::from_millis(5)); - } - - Err(anyhow!("timed out waiting for TypeScript RUDP response")) -} - -fn rudp_serve_message_once(options: &BTreeMap) -> Result<()> { - let bind_host = options - .get("bind-host") - .cloned() - .unwrap_or_else(|| "127.0.0.1".to_string()); - let bind_port = options - .get("bind-port") - .map(|value| value.parse::()) - .transpose() - .with_context(|| "argument --bind-port must be a u16")? - .unwrap_or(0); - let socket = UdpSocket::bind((bind_host.as_str(), bind_port)).with_context(|| { - format!("failed to bind RUDP message server on {bind_host}:{bind_port}") - })?; - socket.set_read_timeout(Some(Duration::from_millis(20)))?; - let local_port = socket.local_addr()?.port(); - let mut transport_options = - CultNetRudpSocketTransportOptions::server("rust-rudp-message-interop", socket, 0x22446689); - transport_options.initial_sequence = 100; - transport_options.resend_delay_ms = 25; - let mut transport = CultNetRudpSocketTransportConnection::new(transport_options)?; - - print_json(&serde_json::json!({ - "status": "ready", - "port": local_port, - }))?; - - let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline { - if let Some(frame) = transport.receive_once()? { - if frame.channel_id != "schema" { - return Err(anyhow!("unexpected RUDP channel: {}", frame.channel_id)); - } - let message = decode_cultnet_message_from_slice( - &frame.payload, - CultNetWireContract::CultNetSchemaV0, - )?; - let CultNetMessage::SchemaCatalogRequest { .. } = message else { - return Err(anyhow!("unexpected schema message: {message:?}")); - }; - let response = CultNetMessage::Hello { - runtime_id: "rust-rudp-message-interop".to_string(), - runtime_kind: "rust".to_string(), - agent_id: None, - role: None, - display_name: Some("Rust RUDP Message Interop".to_string()), - supported_document_types: Some(vec![]), - supported_mutation_contracts: Some(vec![]), - supported_message_versions: Some(vec![ - "cultnet.hello.v0".to_string(), - "cultnet.schema_catalog_request.v0".to_string(), - ]), - transport_profiles: Some(vec![create_rudp_transport_profile( - "rust-rudp-message-interop", - RudpTransportProfileOptions { - host: Some("127.0.0.1".to_string()), - port: Some(local_port), - ..RudpTransportProfileOptions::default() - }, - )]), - supports_schema_catalog: Some(true), - }; - transport.send( - "schema", - encode_cultnet_message_to_vec(&response, CultNetWireContract::CultNetSchemaV0)?, - )?; - poll_rudp_after_send(&mut transport, Duration::from_millis(250))?; - print_json(&serde_json::json!({ "status": "ok" }))?; - return Ok(()); - } - transport.poll_resends()?; - thread::sleep(Duration::from_millis(5)); - } - - Err(anyhow!( - "timed out waiting for TypeScript RUDP schema-v0 message" - )) -} - -fn rudp_dial_message_once(options: &BTreeMap) -> Result<()> { - let target_host = require_arg(options, "target-host")?; - let target_port = parse_u16_arg(options, "target-port")?; - let remote_addr: SocketAddr = format!("{target_host}:{target_port}") - .parse() - .with_context(|| { - format!("failed to parse RUDP remote endpoint {target_host}:{target_port}") - })?; - let socket = UdpSocket::bind(("127.0.0.1", 0))?; - socket.set_read_timeout(Some(Duration::from_millis(20)))?; - let mut transport_options = CultNetRudpSocketTransportOptions::client( - "rust-rudp-message-client-interop", - socket, - remote_addr, - 0x88664423, - ); - transport_options.resend_delay_ms = 25; - let mut transport = CultNetRudpSocketTransportConnection::new(transport_options)?; - transport.connect(b"rust-message-join".to_vec())?; - - let request = CultNetMessage::SchemaCatalogRequest { - message_id: "rust-ts-schema-message".to_string(), - include_schema_json: Some(false), - schema_ids: Some(vec![]), - kinds: Some(vec![CultNetSchemaKind::WireMessage]), - }; - - let mut sent = false; - let deadline = Instant::now() + Duration::from_secs(5); - while Instant::now() < deadline { - if let Some(frame) = transport.receive_once()? { - if frame.channel_id != "schema" { - return Err(anyhow!("unexpected RUDP channel: {}", frame.channel_id)); - } - let message = decode_cultnet_message_from_slice( - &frame.payload, - CultNetWireContract::CultNetSchemaV0, - )?; - match message { - CultNetMessage::Hello { runtime_id, .. } - if runtime_id == "ts-rust-rudp-message-server" => - { - print_json(&serde_json::json!({ "status": "ok" }))?; - return Ok(()); - } - other => return Err(anyhow!("unexpected schema message: {other:?}")), - } - } - transport.poll_resends()?; - if transport.connected() && !sent { - transport.send( - "schema", - encode_cultnet_message_to_vec(&request, CultNetWireContract::CultNetSchemaV0)?, - )?; - sent = true; - } - thread::sleep(Duration::from_millis(5)); - } - - Err(anyhow!( - "timed out waiting for TypeScript RUDP schema-v0 response" - )) -} - -fn poll_rudp_after_send( - transport: &mut CultNetRudpSocketTransportConnection, - duration: Duration, -) -> Result<()> { - let deadline = Instant::now() + duration; - while Instant::now() < deadline { - let _ = transport.receive_once()?; - transport.poll_resends()?; - thread::sleep(Duration::from_millis(5)); - } - Ok(()) -} - -fn mutate_remote_note( - transport: &mut dyn SchemaMessageTransport, - cache: &mut CultCache, - document_registry: &CultNetDocumentRegistry, - note_schema_id: &str, - runtime_id: &str, - note: &CultNetInteropNote, -) -> Result { - let intent = CultNetInteropMutationIntent { - schema_version: MUTATION_INTENT_SCHEMA_VERSION.to_string(), - intent_id: format!("{runtime_id}-decorate"), - target_document_id: note.document_id.clone(), - append_body: format!(" Decorated by {runtime_id}."), - append_tag: format!("decorated:{runtime_id}"), - }; - let message = document_registry.create_raw_document_put_message( - format!("{runtime_id}-decorate-put"), - intent.intent_id.clone(), - &intent, - CultNetDocumentPutOptions::default(), - )?; - transport.send_schema_message(&message)?; - - let receipt_message = transport - .read_schema_message() - .context("waiting for mutation receipt")?; - let _receipt = document_registry - .apply_raw_document_put_message::(cache, &receipt_message)?; - let mutated_message = transport - .read_schema_message() - .context("waiting for mutated note")?; - let mutated = document_registry - .apply_raw_document_put_message::(cache, &mutated_message)?; - if mutated_message_schema_id(&mutated_message) != Some(note_schema_id) { - return Err(anyhow!("mutation response used the wrong schema id")); - } - Ok(serde_json::json!({ - "schemaVersion": mutated.schema_version, - "documentId": mutated.document_id, - "authorRuntimeId": mutated.author_runtime_id, - "title": mutated.title, - "body": mutated.body, - "tags": mutated.tags, - })) -} - -fn fire_remote_weapon( - transport: &mut dyn SchemaMessageTransport, - cache: &mut CultCache, - document_registry: &CultNetDocumentRegistry, - runtime_id: &str, - remote_runtime_id: &str, -) -> Result { - let command = CultNetInteropFireCommand { - schema_version: FIRE_COMMAND_SCHEMA_VERSION.to_string(), - command_id: format!("{runtime_id}-fire"), - character_id: remote_runtime_id.to_string(), - weapon_id: "interop-rifle".to_string(), - }; - let message = document_registry.create_raw_document_put_message( - format!("{runtime_id}-fire-put"), - command.command_id.clone(), - &command, - CultNetDocumentPutOptions::default(), - )?; - transport.send_schema_message(&message)?; - let receipt_message = transport - .read_schema_message() - .context("waiting for fire receipt")?; - let receipt = document_registry - .apply_raw_document_put_message::(cache, &receipt_message)?; - Ok(serde_json::json!({ - "schemaVersion": receipt.schema_version, - "commandId": receipt.command_id, - "accepted": receipt.accepted, - "characterId": receipt.character_id, - "weaponId": receipt.weapon_id, - "shotsFired": receipt.shots_fired, - "ammoRemaining": receipt.ammo_remaining, - })) -} - -fn start_udp_discovery_server(config: Arc) -> Result<()> { - let socket = create_discovery_socket(config.discovery_port, true)?; - socket.join_multicast_v4(&config.discovery_group, &Ipv4Addr::UNSPECIFIED)?; - socket.set_read_timeout(Some(Duration::from_millis(250)))?; - - thread::spawn(move || { - let mut buffer = vec![0_u8; 4096]; - loop { - match socket.recv_from(&mut buffer) { - Ok((len, remote)) => { - if let Ok(DiscoveryMessage::Probe { - message_id, - requester_runtime_id: _, - }) = rmp_serde::from_slice::(&buffer[..len]) - { - let announce = DiscoveryMessage::Announce { - message_id, - runtime_id: config.runtime_id.clone(), - runtime_kind: config.runtime_kind.clone(), - display_name: config.display_name.clone(), - agent_id: Some(config.agent_id.clone()), - tcp_host: config.advertise_host.clone(), - tcp_port: config.tcp_port, - wire_contract: "cultnet.schema.v0".to_string(), - supported_document_types: vec![INTEROP_DOCUMENT_TYPE.to_string()], - transport_profiles: Some(interop_transport_profiles( - &config.runtime_id, - &config.advertise_host, - config.tcp_port, - config.rudp_port, - )), - supports_schema_catalog: true, - }; - if let Ok(payload) = rmp_serde::to_vec_named(&announce) { - let _ = socket.send_to(&payload, remote); - } - } - } - Err(error) - if error.kind() == std::io::ErrorKind::WouldBlock - || error.kind() == std::io::ErrorKind::TimedOut => {} - Err(_) => break, - } - } - }); - - Ok(()) -} - -fn start_tcp_server( - config: Arc, - cache: Arc>, - document_registry: Arc, - schema_registry: Arc, -) -> Result<()> { - let listener = - TcpListener::bind((config.bind_host.as_str(), config.tcp_port)).with_context(|| { - format!( - "failed to bind TCP listener on {}:{}", - config.bind_host, config.tcp_port - ) - })?; - listener.set_nonblocking(false)?; - - thread::spawn(move || { - for stream in listener.incoming() { - let Ok(stream) = stream else { - continue; - }; - let config = config.clone(); - let cache = cache.clone(); - let document_registry = document_registry.clone(); - let schema_registry = schema_registry.clone(); - thread::spawn(move || { - if let Err(error) = - handle_connection(stream, config, cache, document_registry, schema_registry) - { - eprintln!("{error:#}"); - } - }); - } - }); - - Ok(()) -} - -fn start_rudp_server( - config: Arc, - cache: Arc>, - document_registry: Arc, - schema_registry: Arc, -) -> Result<()> { - let socket = - UdpSocket::bind((config.bind_host.as_str(), config.rudp_port)).with_context(|| { - format!( - "failed to bind RUDP listener on {}:{}", - config.bind_host, config.rudp_port - ) - })?; - socket.set_read_timeout(Some(Duration::from_millis(20)))?; - - thread::spawn(move || { - if let Err(error) = - run_rudp_server(socket, config, cache, document_registry, schema_registry) - { - eprintln!("{error:#}"); - } - }); - - Ok(()) -} - -struct RudpPeerSession { - session: CultNetRudpSession, -} - -fn run_rudp_server( - socket: UdpSocket, - config: Arc, - cache: Arc>, - document_registry: Arc, - schema_registry: Arc, -) -> Result<()> { - let mut peers = BTreeMap::::new(); - let mut buffer = vec![0_u8; 65_535]; - loop { - match socket.recv_from(&mut buffer) { - Ok((len, remote_addr)) => { - let packet = match decode_rudp_packet(&buffer[..len]) { - Ok(packet) if packet.connection_id == RUDP_INTEROP_CONNECTION_ID => packet, - Ok(_) => continue, - Err(error) => { - eprintln!("{error:#}"); - continue; - } - }; - let peer = peers.entry(remote_addr).or_insert_with(|| RudpPeerSession { - session: CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: RUDP_INTEROP_CONNECTION_ID, - initial_sequence: 100, - resend_delay_ms: RUDP_INTEROP_RESEND_DELAY_MS, - max_pending_reliable_packets: None, - }), - }); - if packet.packet_type == CultNetRudpPacketType::Connect { - let accept = peer.session.accept_connect( - &packet, - rudp_now_ms(), - b"cultnet-interop-rudp".to_vec(), - )?; - send_rudp_packet(&socket, remote_addr, &accept)?; - continue; - } - - let result = peer.session.receive(&packet, rudp_now_ms())?; - if let Some(reply) = result.reply { - send_rudp_packet(&socket, remote_addr, &reply)?; - } - if packet.packet_type == CultNetRudpPacketType::Data { - let ack = peer.session.create_ack(); - send_rudp_packet(&socket, remote_addr, &ack)?; - } - for frame in result.delivered { - if frame.channel_id != "schema" { - continue; - } - let message = decode_cultnet_message_from_slice( - &frame.payload, - CultNetWireContract::CultNetSchemaV0, - )?; - let mut transport = RudpSessionSender { - socket: &socket, - remote_addr, - session: &mut peer.session, - }; - handle_server_message( - &mut transport, - message, - &config, - &cache, - &document_registry, - &schema_registry, - )?; - } - } - Err(error) - if error.kind() == std::io::ErrorKind::WouldBlock - || error.kind() == std::io::ErrorKind::TimedOut - || error.kind() == std::io::ErrorKind::ConnectionReset => - { - let now = rudp_now_ms(); - for (remote_addr, peer) in peers.iter_mut() { - for packet in peer.session.due_resends(now) { - send_rudp_packet(&socket, *remote_addr, &packet)?; - } - } - } - Err(error) => return Err(error.into()), - } - } -} - -fn handle_connection( - stream: TcpStream, - config: Arc, - cache: Arc>, - document_registry: Arc, - schema_registry: Arc, -) -> Result<()> { - let mut transport = TcpFramedTransportConnection::new( - stream, - tcp_transport_profile(&config.runtime_id, &config.advertise_host, config.tcp_port), - ); - loop { - let message = match transport.read_schema_message() { - Ok(message) => message, - Err(error) if is_eof_like(&error) => break, - Err(error) => return Err(error), - }; - - handle_server_message( - &mut transport, - message, - &config, - &cache, - &document_registry, - &schema_registry, - )?; - } - - Ok(()) -} - -fn handle_server_message( - transport: &mut dyn SchemaMessageSender, - message: CultNetMessage, - config: &PeerConfig, - cache: &Arc>, - document_registry: &CultNetDocumentRegistry, - schema_registry: &CultNetSchemaRegistry, -) -> Result<()> { - match message { - CultNetMessage::Hello { .. } => { - transport.send_schema_message(&CultNetMessage::Hello { - runtime_id: config.runtime_id.clone(), - runtime_kind: config.runtime_kind.clone(), - agent_id: Some(config.agent_id.clone()), - role: None, - display_name: Some(config.display_name.clone()), - supported_document_types: Some(vec![INTEROP_DOCUMENT_TYPE.to_string()]), - supported_mutation_contracts: Some(interaction_contracts()), - supported_message_versions: Some(vec![INTEROP_SCHEMA_VERSION.to_string()]), - transport_profiles: Some(interop_transport_profiles( - &config.runtime_id, - &config.advertise_host, - config.tcp_port, - config.rudp_port, - )), - supports_schema_catalog: Some(true), - })?; - } - request @ CultNetMessage::SchemaCatalogRequest { .. } => { - let response = schema_registry.create_catalog_response(&request)?; - transport.send_schema_message(&response)?; - } - CultNetMessage::SnapshotRequest { - message_id, - schema_ids, - record_keys, - } => { - let mut response = document_registry.create_raw_snapshot_response( - &cache.lock().expect("cache poisoned"), - message_id, - schema_ids.as_deref(), - record_keys.as_deref(), - )?; - if let CultNetMessage::SnapshotResponseRaw { documents, .. } = &mut response { - for document in documents.iter_mut() { - document.source_runtime_id = Some(config.runtime_id.clone()); - document.source_agent_id = Some(config.agent_id.clone()); - document.source_role = Some("peer".to_string()); - document.tags = Some(vec!["interop".to_string(), config.runtime_id.clone()]); - } - } - transport.send_schema_message(&response)?; - } - message @ CultNetMessage::DocumentPutRaw { .. } => { - handle_raw_put(transport, config, cache, document_registry, &message)?; - } - _ => {} - } - Ok(()) -} - -fn handle_raw_put( - transport: &mut dyn SchemaMessageSender, - config: &PeerConfig, - cache: &Arc>, - document_registry: &CultNetDocumentRegistry, - message: &CultNetMessage, -) -> Result<()> { - let CultNetMessage::DocumentPutRaw { document, .. } = message else { - return Ok(()); - }; - if document.schema_id == MUTATION_INTENT_SCHEMA_ID { - let mut cache = cache.lock().expect("cache poisoned"); - let intent = document_registry - .apply_raw_document_put_message::(&mut cache, message)?; - let mut note = cache.get_required::(&intent.target_document_id)?; - note.body = format!("{}{}", note.body, intent.append_body); - note.tags.push(intent.append_tag); - cache.put(¬e.document_id, ¬e)?; - let receipt = CultNetInteropMutationReceipt { - schema_version: MUTATION_RECEIPT_SCHEMA_VERSION.to_string(), - intent_id: intent.intent_id.clone(), - accepted: true, - document_id: note.document_id.clone(), - body: note.body.clone(), - tags: note.tags.clone(), - }; - let options = response_options(config, "mutation"); - let receipt_message = document_registry.create_raw_document_put_message( - format!("{}-mutation-receipt", config.runtime_id), - receipt.intent_id.clone(), - &receipt, - options.clone(), - )?; - let note_message = document_registry.create_raw_document_put_message( - format!("{}-mutated-note", config.runtime_id), - note.document_id.clone(), - ¬e, - options, - )?; - transport.send_schema_message(&receipt_message)?; - transport.send_schema_message(¬e_message)?; - } else if document.schema_id == FIRE_COMMAND_SCHEMA_ID { - let mut cache = cache.lock().expect("cache poisoned"); - let command = document_registry - .apply_raw_document_put_message::(&mut cache, message)?; - let receipt = CultNetInteropFireReceipt { - schema_version: FIRE_RECEIPT_SCHEMA_VERSION.to_string(), - command_id: command.command_id, - accepted: true, - character_id: command.character_id, - weapon_id: command.weapon_id, - shots_fired: 1, - ammo_remaining: 29, - }; - let receipt_message = document_registry.create_raw_document_put_message( - format!("{}-fire-receipt", config.runtime_id), - receipt.command_id.clone(), - &receipt, - response_options(config, "side-effect"), - )?; - transport.send_schema_message(&receipt_message)?; - } - Ok(()) -} - -fn load_schema_registration(schema_path: &str) -> Result { - let schema_json = fs::read_to_string(schema_path) - .with_context(|| format!("failed to read schema {}", schema_path))?; - let parsed: serde_json::Value = serde_json::from_str(&schema_json) - .with_context(|| format!("failed to parse schema {}", schema_path))?; - let schema_id = parsed - .get("$id") - .and_then(serde_json::Value::as_str) - .ok_or_else(|| anyhow!("schema {} is missing $id", schema_path))? - .to_string(); - let title = parsed - .get("title") - .and_then(serde_json::Value::as_str) - .map(ToString::to_string); - - Ok(CultNetSchemaRegistration { - schema_id, - kind: CultNetSchemaKind::DocumentPayload, - wire_contracts: vec![CultNetWireContract::CultNetSchemaV0], - schema_version: Some(INTEROP_SCHEMA_VERSION.to_string()), - document_type: Some(INTEROP_DOCUMENT_TYPE.to_string()), - title, - schema_json: Some(schema_json), - }) -} - -fn register_capability_entry_types(cache: &mut CultCache) -> Result<()> { - cache.register_entry_type::()?; - cache.register_entry_type::()?; - cache.register_entry_type::()?; - cache.register_entry_type::()?; - Ok(()) -} - -fn register_capability_bindings( - document_registry: &mut CultNetDocumentRegistry, - note_schema_id: &str, -) { - document_registry - .register(CultNetDocumentBinding::for_entry_with_schema_id::< - CultNetInteropNote, - >( - note_schema_id.to_string(), - INTEROP_SCHEMA_VERSION.to_string(), - )) - .register(CultNetDocumentBinding::for_entry_with_schema_id::< - CultNetInteropMutationIntent, - >( - MUTATION_INTENT_SCHEMA_ID.to_string(), - MUTATION_INTENT_SCHEMA_VERSION.to_string(), - )) - .register(CultNetDocumentBinding::for_entry_with_schema_id::< - CultNetInteropMutationReceipt, - >( - MUTATION_RECEIPT_SCHEMA_ID.to_string(), - MUTATION_RECEIPT_SCHEMA_VERSION.to_string(), - )) - .register(CultNetDocumentBinding::for_entry_with_schema_id::< - CultNetInteropFireCommand, - >( - FIRE_COMMAND_SCHEMA_ID.to_string(), - FIRE_COMMAND_SCHEMA_VERSION.to_string(), - )) - .register(CultNetDocumentBinding::for_entry_with_schema_id::< - CultNetInteropFireReceipt, - >( - FIRE_RECEIPT_SCHEMA_ID.to_string(), - FIRE_RECEIPT_SCHEMA_VERSION.to_string(), - )); -} - -fn interaction_contracts() -> Vec { - vec![CultNetDocumentMutationContract { - document_type: INTEROP_DOCUMENT_TYPE.to_string(), - payload_schema_version: Some(INTEROP_SCHEMA_VERSION.to_string()), - operations: vec![ - CultNetDocumentOperation::Snapshot, - CultNetDocumentOperation::DocumentPut, - CultNetDocumentOperation::IntentSubmit, - CultNetDocumentOperation::ReceiptWatch, - ], - authority: CultNetMutationAuthority::Runtime, - intent_document_types: Some(vec![ - MUTATION_INTENT_TYPE.to_string(), - FIRE_COMMAND_TYPE.to_string(), - ]), - receipt_document_types: Some(vec![ - MUTATION_RECEIPT_TYPE.to_string(), - FIRE_RECEIPT_TYPE.to_string(), - ]), - notes: None, - }] -} - -fn tcp_transport_profile(runtime_id: &str, host: &str, port: u16) -> CultNetTransportProfile { - create_tcp_framed_transport_profile( - runtime_id, - TcpFramedTransportProfileOptions { - transport_id: Some("interop-tcp".to_string()), - host: Some(host.to_string()), - port: Some(port), - ..TcpFramedTransportProfileOptions::default() - }, - ) -} - -fn rudp_transport_profile(runtime_id: &str, host: &str, port: u16) -> CultNetTransportProfile { - create_rudp_transport_profile( - runtime_id, - RudpTransportProfileOptions { - transport_id: Some("interop-rudp".to_string()), - host: Some(host.to_string()), - port: Some(port), - max_fragment_bytes: Some(RUDP_INTEROP_MAX_FRAGMENT_BYTES as u32), - ..RudpTransportProfileOptions::default() - }, - ) -} - -fn interop_transport_profiles( - runtime_id: &str, - host: &str, - tcp_port: u16, - rudp_port: u16, -) -> Vec { - vec![ - tcp_transport_profile(runtime_id, host, tcp_port), - rudp_transport_profile(runtime_id, host, rudp_port), - ] -} - -fn dial_transport_profiles(config: &DialConfig) -> Vec { - let mut profiles = Vec::new(); - if let Some(target_port) = config.target_port { - profiles.push(tcp_transport_profile( - &config.runtime_id, - &config.target_host, - target_port, - )); - } - if let Some(target_rudp_port) = config.target_rudp_port { - profiles.push(rudp_transport_profile( - &config.runtime_id, - &config.target_host, - target_rudp_port, - )); - } - profiles -} - -fn open_dial_transport(config: &DialConfig) -> Result> { - if let Some(target_rudp_port) = config.target_rudp_port { - let remote_addr: SocketAddr = format!("{}:{}", config.target_host, target_rudp_port) - .parse() - .with_context(|| { - format!( - "failed to parse RUDP remote endpoint {}:{}", - config.target_host, target_rudp_port - ) - })?; - let socket = UdpSocket::bind(("127.0.0.1", 0))?; - socket.set_read_timeout(Some(Duration::from_millis(20)))?; - let mut options = CultNetRudpSocketTransportOptions::client( - format!("{}-interop-rudp-dial", config.runtime_id), - socket, - remote_addr, - RUDP_INTEROP_CONNECTION_ID, - ); - options.transport_id = Some("interop-rudp".to_string()); - options.resend_delay_ms = RUDP_INTEROP_RESEND_DELAY_MS; - options.max_fragment_bytes = Some(RUDP_INTEROP_MAX_FRAGMENT_BYTES as u32); - let mut transport = CultNetRudpSocketTransportConnection::new(options)?; - transport.connect(b"cultnet-interop-rudp".to_vec())?; - wait_for_rudp_connected(&mut transport, Duration::from_secs(5))?; - return Ok(Box::new(transport)); - } - - let target_port = config - .target_port - .ok_or_else(|| anyhow!("dial requires --target-port or --target-rudp-port"))?; - let stream = - TcpStream::connect((config.target_host.as_str(), target_port)).with_context(|| { - format!( - "failed to connect to {}:{}", - config.target_host, target_port - ) - })?; - Ok(Box::new(TcpFramedTransportConnection::new( - stream, - tcp_transport_profile(&config.runtime_id, &config.target_host, target_port), - ))) -} - -fn response_options(config: &PeerConfig, tag: &str) -> CultNetDocumentPutOptions { - CultNetDocumentPutOptions { - source_runtime_id: Some(config.runtime_id.clone()), - source_agent_id: Some(config.agent_id.clone()), - source_role: Some("peer".to_string()), - tags: Some(vec![tag.to_string(), config.runtime_id.clone()]), - ..CultNetDocumentPutOptions::default() - } -} - -fn mutated_message_schema_id(message: &CultNetMessage) -> Option<&str> { - match message { - CultNetMessage::DocumentPutRaw { document, .. } => Some(document.schema_id.as_str()), - _ => None, - } -} - -fn build_note(runtime_id: &str, display_name: &str) -> CultNetInteropNote { - CultNetInteropNote { - schema_version: INTEROP_SCHEMA_VERSION.to_string(), - document_id: format!("note:{runtime_id}"), - author_runtime_id: runtime_id.to_string(), - title: format!("{display_name} keeps a little note"), - body: format!( - "{runtime_id} can move CultNet state without begging the gods for translation." - ), - tags: vec![ - runtime_id.to_string(), - "interop".to_string(), - "cultnet".to_string(), - ], - } -} - -fn is_eof_like(error: &anyhow::Error) -> bool { - error - .downcast_ref::() - .is_some_and(|io| io.kind() == std::io::ErrorKind::UnexpectedEof) -} - -impl SchemaMessageSender for TcpFramedTransportConnection { - fn send_schema_message(&mut self, message: &CultNetMessage) -> Result<()> { - let payload = encode_cultnet_message_to_vec(message, CultNetWireContract::CultNetSchemaV0)?; - self.send("schema", &payload) - } -} - -impl SchemaMessageTransport for TcpFramedTransportConnection { - fn read_schema_message(&mut self) -> Result { - let frame = self.receive()?; - decode_cultnet_message_from_slice(&frame.payload, CultNetWireContract::CultNetSchemaV0) - } -} - -impl SchemaMessageSender for CultNetRudpSocketTransportConnection { - fn send_schema_message(&mut self, message: &CultNetMessage) -> Result<()> { - CultNetRudpSocketTransportConnection::send_schema_message(self, message) - } -} - -impl SchemaMessageTransport for CultNetRudpSocketTransportConnection { - fn read_schema_message(&mut self) -> Result { - let deadline = Instant::now() + RUDP_INTEROP_READ_TIMEOUT; - while Instant::now() < deadline { - if let Some(message) = self.receive_schema_message_once()? { - return Ok(message); - } - self.poll_resends()?; - thread::sleep(Duration::from_millis(5)); - } - Err(anyhow!("timed out waiting for RUDP schema message")) - } -} - -struct RudpSessionSender<'a> { - socket: &'a UdpSocket, - remote_addr: SocketAddr, - session: &'a mut CultNetRudpSession, -} - -impl SchemaMessageSender for RudpSessionSender<'_> { - fn send_schema_message(&mut self, message: &CultNetMessage) -> Result<()> { - let payload = encode_cultnet_message_to_vec(message, CultNetWireContract::CultNetSchemaV0)?; - let packets = self.session.send_many( - "schema", - payload, - CultNetRudpSendOptions { - reliable: true, - ordered: true, - sequenced: false, - now_ms: rudp_now_ms(), - }, - Some(RUDP_INTEROP_MAX_FRAGMENT_BYTES), - )?; - for packet in packets { - send_rudp_packet(self.socket, self.remote_addr, &packet)?; - } - Ok(()) - } -} - -fn send_rudp_packet( - socket: &UdpSocket, - remote_addr: SocketAddr, - packet: &cultnet_rs::CultNetRudpPacket, -) -> Result<()> { - socket.send_to(&encode_rudp_packet(packet)?, remote_addr)?; - Ok(()) -} - -fn wait_for_rudp_connected( - transport: &mut CultNetRudpSocketTransportConnection, - timeout: Duration, -) -> Result<()> { - let deadline = Instant::now() + timeout; - while Instant::now() < deadline { - if transport.connected() { - return Ok(()); - } - let _ = transport.receive_once()?; - transport.poll_resends()?; - thread::sleep(Duration::from_millis(5)); - } - Err(anyhow!("timed out waiting for RUDP connect")) -} - -fn rudp_now_ms() -> u64 { - use std::time::SystemTime; - use std::time::UNIX_EPOCH; - - SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or_default() - .as_millis() as u64 -} - -fn create_discovery_socket(port: u16, _join_group: bool) -> Result { - let socket = Socket::new(Domain::IPV4, Type::DGRAM, Some(Protocol::UDP))?; - socket.set_reuse_address(true)?; - socket.bind(&SocketAddrV4::new(Ipv4Addr::UNSPECIFIED, port).into())?; - let socket = UdpSocket::from(socket); - socket.set_multicast_loop_v4(true)?; - socket.set_multicast_ttl_v4(1)?; - socket.set_nonblocking(false)?; - Ok(socket) -} - -fn parse_peer_config(options: &BTreeMap) -> Result { - let tcp_port = parse_u16_arg(options, "tcp-port")?; - Ok(PeerConfig { - runtime_id: require_arg(options, "runtime-id")?.to_string(), - runtime_kind: require_arg(options, "runtime-kind")?.to_string(), - display_name: require_arg(options, "display-name")?.to_string(), - agent_id: require_arg(options, "agent-id")?.to_string(), - bind_host: options - .get("bind-host") - .cloned() - .unwrap_or_else(|| "127.0.0.1".to_string()), - advertise_host: require_arg(options, "advertise-host")?.to_string(), - tcp_port, - rudp_port: options - .get("rudp-port") - .map(|value| value.parse::()) - .transpose() - .with_context(|| "argument --rudp-port must be a u16")? - .unwrap_or(tcp_port), - discovery_port: parse_u16_arg(options, "discovery-port")?, - discovery_group: parse_ipv4_arg(options, "discovery-group")?, - schema_path: require_arg(options, "schema-path")?.to_string(), - }) -} - -fn parse_dial_config(options: &BTreeMap) -> Result { - Ok(DialConfig { - runtime_id: require_arg(options, "runtime-id")?.to_string(), - runtime_kind: require_arg(options, "runtime-kind")?.to_string(), - display_name: require_arg(options, "display-name")?.to_string(), - agent_id: require_arg(options, "agent-id")?.to_string(), - target_host: require_arg(options, "target-host")?.to_string(), - target_port: options - .get("target-port") - .map(|value| value.parse::()) - .transpose() - .with_context(|| "argument --target-port must be a u16")?, - target_rudp_port: options - .get("target-rudp-port") - .map(|value| value.parse::()) - .transpose() - .with_context(|| "argument --target-rudp-port must be a u16")?, - schema_path: require_arg(options, "schema-path")?.to_string(), - }) -} - -fn parse_args(args: Vec) -> BTreeMap { - let mut parsed = BTreeMap::new(); - let mut index = 0; - while index < args.len() { - let token = &args[index]; - if !token.starts_with("--") { - index += 1; - continue; - } - let name = token.trim_start_matches("--").to_string(); - let value = args - .get(index + 1) - .cloned() - .unwrap_or_else(|| panic!("missing value for --{name}")); - parsed.insert(name, value); - index += 2; - } - parsed -} - -fn require_arg<'a>(options: &'a BTreeMap, name: &str) -> Result<&'a str> { - options - .get(name) - .map(String::as_str) - .ok_or_else(|| anyhow!("missing required argument --{name}")) -} - -fn parse_u16_arg(options: &BTreeMap, name: &str) -> Result { - require_arg(options, name)? - .parse::() - .with_context(|| format!("argument --{name} must be a u16")) -} - -fn parse_u64_arg(options: &BTreeMap, name: &str) -> Option { - options - .get(name) - .map(|value| value.parse::().expect("u64 arg")) -} - -fn parse_ipv4_arg(options: &BTreeMap, name: &str) -> Result { - require_arg(options, name)? - .parse::() - .with_context(|| format!("argument --{name} must be an IPv4 address")) -} - -fn runtime_store_path(runtime_id: &str) -> std::path::PathBuf { - std::env::temp_dir().join(format!("cultnet-rs-interop-{runtime_id}.msgpack")) -} - -fn print_json(value: &serde_json::Value) -> Result<()> { - println!("{}", serde_json::to_string(value)?); - Ok(()) -} diff --git a/vendor/cultnet-rs/src/contracts.rs b/vendor/cultnet-rs/src/contracts.rs deleted file mode 100644 index e637883..0000000 --- a/vendor/cultnet-rs/src/contracts.rs +++ /dev/null @@ -1,1406 +0,0 @@ -use anyhow::Result; -use anyhow::anyhow; -use base64::Engine; -use base64::engine::general_purpose::URL_SAFE_NO_PAD; -use serde::Deserialize; -use serde::Serialize; -use serde_json::Value; - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub enum CultNetWireContract { - #[serde(rename = "cultnet.schema.v0")] - CultNetSchemaV0, - #[serde(rename = "gamecult.networking.v0")] - GameCultNetworkingV0, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum CultNetSchemaKind { - WireMessage, - DocumentPayload, - SharedContract, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum CultNetRawPayloadEncoding { - Messagepack, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultNetDocumentRecord { - pub schema_id: String, - pub record_key: String, - pub stored_at: String, - pub payload: TPayload, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_runtime_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_agent_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_role: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tags: Option>, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum CultNetDocumentOperation { - Snapshot, - DocumentPut, - DocumentDelete, - IntentSubmit, - ReceiptWatch, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub enum CultNetMutationAuthority { - ReadOnly, - LocalUser, - Coordinator, - Runtime, - Denied, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultNetDocumentMutationContract { - pub document_type: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub payload_schema_version: Option, - pub operations: Vec, - pub authority: CultNetMutationAuthority, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub intent_document_types: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub receipt_document_types: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub notes: Option>, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultNetTransportProfile { - pub schema_version: String, - pub runtime_id: String, - pub transports: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultNetTransportDescriptor { - pub transport_id: String, - pub protocol: CultNetTransportProtocol, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub host: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub port: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub path: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub discovery_group: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub wire_contracts: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub reconnect_policy: Option, - pub channels: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultNetReconnectPolicy { - pub schema_version: String, - pub policy_id: String, - pub base_delay_ms: u64, - pub max_delay_ms: u64, - pub max_jitter_ms: u64, - #[serde(skip_serializing_if = "Option::is_none")] - pub max_attempts: Option, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum CultNetTransportProtocol { - TcpFramed, - Litenetlib, - Websocket, - Rudp, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultNetTransportChannel { - pub channel_id: String, - pub delivery: CultNetTransportDelivery, - pub ordering: CultNetTransportOrdering, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_payload_bytes: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_fragment_bytes: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub max_pending_reliable_packets: Option, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum CultNetTransportDelivery { - Reliable, - Unreliable, -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum CultNetTransportOrdering { - Ordered, - Unordered, - Sequenced, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultNetRawDocumentRecord { - pub schema_id: String, - pub record_key: String, - pub stored_at: String, - pub payload_encoding: CultNetRawPayloadEncoding, - #[serde(with = "serde_bytes")] - pub payload: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_runtime_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_agent_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub source_role: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub tags: Option>, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultNetSchemaDescriptor { - pub schema_id: String, - pub kind: CultNetSchemaKind, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub schema_version: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub document_type: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub title: Option, - pub wire_contracts: Vec, - pub content_hash: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub schema_json: Option, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultNetShardDescriptor { - pub shard_id: String, - pub owner_runtime_id: String, - pub epoch: u64, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub is_primary: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub schema_ids: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub key_prefix: Option, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub primary_endpoints: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub replica_endpoints: Vec, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - pub read_replica_endpoints: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub region: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub authority_lease_id: Option, -} - -impl CultNetShardDescriptor { - pub fn serves(&self, schema_id: Option<&str>, record_key: Option<&str>) -> bool { - if let Some(schema_id) = schema_id - && !self.schema_ids.is_empty() - && !self - .schema_ids - .iter() - .any(|candidate| candidate == schema_id) - { - return false; - } - - if let Some(record_key) = record_key - && let Some(prefix) = self.key_prefix.as_deref() - && !record_key.starts_with(prefix) - { - return false; - } - - true - } -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(tag = "schemaVersion")] -pub enum CultNetMessage { - #[serde(rename = "cultnet.hello.v0", rename_all = "camelCase")] - Hello { - runtime_id: String, - runtime_kind: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - agent_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - role: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - display_name: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - supported_document_types: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - supported_mutation_contracts: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - supported_message_versions: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - transport_profiles: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - supports_schema_catalog: Option, - }, - #[serde(rename = "cultnet.login.v0", rename_all = "camelCase")] - Login { - nonce: String, - auth: String, - password: String, - }, - #[serde(rename = "cultnet.register.v0", rename_all = "camelCase")] - Register { - nonce: String, - email: String, - password: String, - name: String, - }, - #[serde(rename = "cultnet.verify.v0", rename_all = "camelCase")] - Verify { nonce: String, session: String }, - #[serde(rename = "cultnet.login_success.v0", rename_all = "camelCase")] - LoginSuccess { nonce: String, session: String }, - #[serde(rename = "cultnet.error.v0", rename_all = "camelCase")] - Error { error: String }, - #[serde(rename = "cultnet.sample.change_name.v0", rename_all = "camelCase")] - SampleChangeName { name: String }, - #[serde(rename = "cultnet.sample.chat.v0", rename_all = "camelCase")] - SampleChat { text: String }, - #[serde(rename = "cultnet.document_put.v0", rename_all = "camelCase")] - DocumentPut { - message_id: String, - document: CultNetDocumentRecord, - }, - #[serde(rename = "cultnet.document_delete.v0", rename_all = "camelCase")] - DocumentDelete { - message_id: String, - schema_id: String, - record_key: String, - }, - #[serde(rename = "cultnet.document_put_raw.v0", rename_all = "camelCase")] - DocumentPutRaw { - message_id: String, - document: CultNetRawDocumentRecord, - }, - #[serde(rename = "cultnet.snapshot_request.v0", rename_all = "camelCase")] - SnapshotRequest { - message_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - schema_ids: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - record_keys: Option>, - }, - #[serde(rename = "cultnet.snapshot_response.v0", rename_all = "camelCase")] - SnapshotResponse { - message_id: String, - documents: Vec>, - }, - #[serde(rename = "cultnet.snapshot_response_raw.v0", rename_all = "camelCase")] - SnapshotResponseRaw { - message_id: String, - documents: Vec, - }, - #[serde(rename = "cultnet.schema_catalog_request.v0", rename_all = "camelCase")] - SchemaCatalogRequest { - message_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - include_schema_json: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - schema_ids: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - kinds: Option>, - }, - #[serde( - rename = "cultnet.schema_catalog_response.v0", - rename_all = "camelCase" - )] - SchemaCatalogResponse { - message_id: String, - schemas: Vec, - }, - #[serde(rename = "cultnet.shard_catalog_request.v0", rename_all = "camelCase")] - ShardCatalogRequest { - message_id: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - schema_ids: Option>, - #[serde(default, skip_serializing_if = "Option::is_none")] - record_keys: Option>, - }, - #[serde(rename = "cultnet.shard_catalog_response.v0", rename_all = "camelCase")] - ShardCatalogResponse { - message_id: String, - shards: Vec, - }, - #[serde(rename = "cultnet.operation_request.v0", rename_all = "camelCase")] - OperationRequest { - message_id: String, - service_id: String, - operation: String, - payload_schema: String, - #[serde(default = "messagepack_base64_encoding")] - payload_encoding: String, - payload: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - source_runtime_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - target_runtime_id: Option, - }, - #[serde(rename = "cultnet.operation_response.v0", rename_all = "camelCase")] - OperationResponse { - message_id: String, - service_id: String, - operation: String, - status: String, - payload_schema: String, - #[serde(default = "messagepack_base64_encoding")] - payload_encoding: String, - payload: String, - #[serde(default, skip_serializing_if = "Vec::is_empty")] - diagnostics: Vec, - #[serde(default, skip_serializing_if = "Option::is_none")] - source_runtime_id: Option, - }, -} - -fn messagepack_base64_encoding() -> String { - "messagepack-base64".to_string() -} - -pub fn parse_cultnet_message( - wire_value: &rmpv::Value, - contract: CultNetWireContract, -) -> Result { - match contract { - CultNetWireContract::CultNetSchemaV0 => { - let message = match schema_version_from_wire_value(wire_value)? { - Some("cultnet.document_put_raw.v0" | "cultnet.snapshot_response_raw.v0") => { - parse_raw_cultnet_schema_message(wire_value)? - } - _ => { - let json_value: Value = rmp_serde::from_slice(&rmp_serde::to_vec(wire_value)?)?; - serde_json::from_value(json_value)? - } - }; - validate_message(&message)?; - Ok(message) - } - CultNetWireContract::GameCultNetworkingV0 => parse_gamecult_networking_message(wire_value), - } -} - -pub fn encode_cultnet_message_for_wire( - message: &CultNetMessage, - contract: CultNetWireContract, -) -> Result { - validate_message(message)?; - match contract { - CultNetWireContract::CultNetSchemaV0 => match message { - CultNetMessage::DocumentPutRaw { .. } | CultNetMessage::SnapshotResponseRaw { .. } => { - encode_raw_cultnet_schema_message(message) - } - _ => Ok(rmp_serde::from_slice(&rmp_serde::to_vec( - &serde_json::to_value(message)?, - )?)?), - }, - CultNetWireContract::GameCultNetworkingV0 => encode_gamecult_networking_message(message), - } -} - -pub fn encode_cultnet_message_to_vec( - message: &CultNetMessage, - contract: CultNetWireContract, -) -> Result> { - let wire_value = encode_cultnet_message_for_wire(message, contract)?; - Ok(rmp_serde::to_vec(&wire_value)?) -} - -pub fn decode_cultnet_message_from_slice( - bytes: &[u8], - contract: CultNetWireContract, -) -> Result { - if contract == CultNetWireContract::CultNetSchemaV0 { - let wire_value: rmpv::Value = rmp_serde::from_slice(bytes)?; - return parse_cultnet_message(&wire_value, contract); - } - let wire_value: rmpv::Value = rmp_serde::from_slice(bytes)?; - parse_cultnet_message(&wire_value, contract) -} - -fn validate_message(message: &CultNetMessage) -> Result<()> { - match message { - CultNetMessage::Hello { - runtime_id, - runtime_kind, - agent_id, - role, - display_name, - supported_document_types, - supported_mutation_contracts, - supported_message_versions, - transport_profiles: _, - supports_schema_catalog: _, - } => { - require_non_empty(runtime_id, "runtimeId")?; - require_non_empty(runtime_kind, "runtimeKind")?; - require_optional_non_empty(agent_id.as_deref(), "agentId")?; - require_optional_non_empty(role.as_deref(), "role")?; - require_optional_non_empty(display_name.as_deref(), "displayName")?; - require_optional_string_vec( - supported_document_types.as_deref(), - "supportedDocumentTypes", - )?; - if let Some(contracts) = supported_mutation_contracts { - for contract in contracts { - validate_mutation_contract(contract)?; - } - } - require_optional_string_vec( - supported_message_versions.as_deref(), - "supportedMessageVersions", - )?; - } - CultNetMessage::Login { - nonce, - auth, - password, - } => { - require_non_empty(nonce, "nonce")?; - require_non_empty(auth, "auth")?; - require_non_empty(password, "password")?; - } - CultNetMessage::Register { - nonce, - email, - password, - name, - } => { - require_non_empty(nonce, "nonce")?; - require_non_empty(email, "email")?; - require_non_empty(password, "password")?; - require_non_empty(name, "name")?; - } - CultNetMessage::Verify { nonce, session } - | CultNetMessage::LoginSuccess { nonce, session } => { - require_non_empty(nonce, "nonce")?; - require_non_empty(session, "session")?; - } - CultNetMessage::Error { error } => require_non_empty(error, "error")?, - CultNetMessage::SampleChangeName { name } => require_non_empty(name, "name")?, - CultNetMessage::SampleChat { text } => require_non_empty(text, "text")?, - CultNetMessage::DocumentPut { - message_id, - document, - } => { - require_non_empty(message_id, "messageId")?; - validate_document_record(document)?; - } - CultNetMessage::DocumentDelete { - message_id, - schema_id, - record_key, - } => { - require_non_empty(message_id, "messageId")?; - require_non_empty(schema_id, "schemaId")?; - require_non_empty(record_key, "recordKey")?; - } - CultNetMessage::DocumentPutRaw { - message_id, - document, - } => { - require_non_empty(message_id, "messageId")?; - validate_raw_document_record(document)?; - } - CultNetMessage::SnapshotRequest { - message_id, - schema_ids, - record_keys, - } => { - require_non_empty(message_id, "messageId")?; - require_optional_string_vec(schema_ids.as_deref(), "schemaIds")?; - require_optional_string_vec(record_keys.as_deref(), "recordKeys")?; - } - CultNetMessage::SnapshotResponse { - message_id, - documents, - } => { - require_non_empty(message_id, "messageId")?; - for document in documents { - validate_document_record(document)?; - } - } - CultNetMessage::SnapshotResponseRaw { - message_id, - documents, - } => { - require_non_empty(message_id, "messageId")?; - for document in documents { - validate_raw_document_record(document)?; - } - } - CultNetMessage::SchemaCatalogRequest { - message_id, - include_schema_json: _, - schema_ids, - kinds: _, - } => { - require_non_empty(message_id, "messageId")?; - require_optional_string_vec(schema_ids.as_deref(), "schemaIds")?; - } - CultNetMessage::SchemaCatalogResponse { - message_id, - schemas, - } => { - require_non_empty(message_id, "messageId")?; - for schema in schemas { - validate_schema_descriptor(schema)?; - } - } - CultNetMessage::ShardCatalogRequest { - message_id, - schema_ids, - record_keys, - } => { - require_non_empty(message_id, "messageId")?; - require_optional_string_vec(schema_ids.as_deref(), "schemaIds")?; - require_optional_string_vec(record_keys.as_deref(), "recordKeys")?; - } - CultNetMessage::ShardCatalogResponse { message_id, shards } => { - require_non_empty(message_id, "messageId")?; - for shard in shards { - validate_shard_descriptor(shard)?; - } - } - CultNetMessage::OperationRequest { - message_id, - service_id, - operation, - payload_schema, - payload_encoding, - payload, - source_runtime_id, - target_runtime_id, - } => { - require_non_empty(message_id, "messageId")?; - require_non_empty(service_id, "serviceId")?; - require_non_empty(operation, "operation")?; - require_non_empty(payload_schema, "payloadSchema")?; - require_exact(payload_encoding, "messagepack-base64", "payloadEncoding")?; - require_non_empty(payload, "payload")?; - require_optional_non_empty(source_runtime_id.as_deref(), "sourceRuntimeId")?; - require_optional_non_empty(target_runtime_id.as_deref(), "targetRuntimeId")?; - } - CultNetMessage::OperationResponse { - message_id, - service_id, - operation, - status, - payload_schema, - payload_encoding, - payload, - diagnostics: _, - source_runtime_id, - } => { - require_non_empty(message_id, "messageId")?; - require_non_empty(service_id, "serviceId")?; - require_non_empty(operation, "operation")?; - require_non_empty(status, "status")?; - require_non_empty(payload_schema, "payloadSchema")?; - require_exact(payload_encoding, "messagepack-base64", "payloadEncoding")?; - require_non_empty(payload, "payload")?; - require_optional_non_empty(source_runtime_id.as_deref(), "sourceRuntimeId")?; - } - } - Ok(()) -} - -fn validate_mutation_contract(contract: &CultNetDocumentMutationContract) -> Result<()> { - require_non_empty(&contract.document_type, "documentType")?; - require_optional_non_empty( - contract.payload_schema_version.as_deref(), - "payloadSchemaVersion", - )?; - if contract.operations.is_empty() { - return Err(anyhow!( - "CultNet mutation contract for {:?} must advertise at least one operation", - contract.document_type - )); - } - require_optional_string_vec( - contract.intent_document_types.as_deref(), - "intentDocumentTypes", - )?; - require_optional_string_vec( - contract.receipt_document_types.as_deref(), - "receiptDocumentTypes", - )?; - require_optional_string_vec(contract.notes.as_deref(), "notes")?; - Ok(()) -} - -fn validate_document_record(document: &CultNetDocumentRecord) -> Result<()> { - require_non_empty(&document.schema_id, "schemaId")?; - require_non_empty(&document.record_key, "recordKey")?; - require_non_empty(&document.stored_at, "storedAt")?; - require_optional_non_empty(document.source_runtime_id.as_deref(), "sourceRuntimeId")?; - require_optional_non_empty(document.source_agent_id.as_deref(), "sourceAgentId")?; - require_optional_non_empty(document.source_role.as_deref(), "sourceRole")?; - require_optional_string_vec(document.tags.as_deref(), "tags")?; - Ok(()) -} - -fn validate_raw_document_record(document: &CultNetRawDocumentRecord) -> Result<()> { - require_non_empty(&document.schema_id, "schemaId")?; - require_non_empty(&document.record_key, "recordKey")?; - require_non_empty(&document.stored_at, "storedAt")?; - if document.payload.is_empty() { - return Err(anyhow!( - "CultNet field payload must contain non-empty MessagePack bytes" - )); - } - require_optional_non_empty(document.source_runtime_id.as_deref(), "sourceRuntimeId")?; - require_optional_non_empty(document.source_agent_id.as_deref(), "sourceAgentId")?; - require_optional_non_empty(document.source_role.as_deref(), "sourceRole")?; - require_optional_string_vec(document.tags.as_deref(), "tags")?; - Ok(()) -} - -fn validate_schema_descriptor(schema: &CultNetSchemaDescriptor) -> Result<()> { - require_non_empty(&schema.schema_id, "schemaId")?; - require_optional_non_empty(schema.schema_version.as_deref(), "schemaVersion")?; - require_optional_non_empty(schema.document_type.as_deref(), "documentType")?; - require_optional_non_empty(schema.title.as_deref(), "title")?; - require_non_empty(&schema.content_hash, "contentHash")?; - require_optional_non_empty(schema.schema_json.as_deref(), "schemaJson")?; - - if schema.wire_contracts.is_empty() { - return Err(anyhow!( - "CultNet field wireContracts must contain at least one supported contract" - )); - } - - Ok(()) -} - -fn validate_shard_descriptor(shard: &CultNetShardDescriptor) -> Result<()> { - require_non_empty(&shard.shard_id, "shardId")?; - require_non_empty(&shard.owner_runtime_id, "ownerRuntimeId")?; - require_optional_string_vec(Some(shard.schema_ids.as_slice()), "schemaIds")?; - require_optional_non_empty(shard.key_prefix.as_deref(), "keyPrefix")?; - require_optional_string_vec(Some(shard.primary_endpoints.as_slice()), "primaryEndpoints")?; - require_optional_string_vec(Some(shard.replica_endpoints.as_slice()), "replicaEndpoints")?; - require_optional_string_vec( - Some(shard.read_replica_endpoints.as_slice()), - "readReplicaEndpoints", - )?; - require_optional_non_empty(shard.region.as_deref(), "region")?; - require_optional_non_empty(shard.authority_lease_id.as_deref(), "authorityLeaseId")?; - Ok(()) -} - -fn require_non_empty(value: &str, field: &str) -> Result<()> { - if value.trim().is_empty() { - return Err(anyhow!("CultNet field {field} must be non-empty")); - } - Ok(()) -} - -fn require_exact(value: &str, expected: &str, field: &str) -> Result<()> { - if value != expected { - return Err(anyhow!("{field} must be {expected}")); - } - Ok(()) -} - -fn require_optional_non_empty(value: Option<&str>, field: &str) -> Result<()> { - if let Some(value) = value { - require_non_empty(value, field)?; - } - Ok(()) -} - -fn require_optional_string_vec(value: Option<&[String]>, field: &str) -> Result<()> { - if let Some(values) = value { - for item in values { - require_non_empty(item, field)?; - } - } - Ok(()) -} - -fn schema_version_from_wire_value(input: &rmpv::Value) -> Result> { - let Some(object) = input.as_map() else { - return Ok(None); - }; - - Ok(object.iter().find_map(|(key, value)| { - key.as_str() - .filter(|candidate| *candidate == "schemaVersion") - .and_then(|_| value.as_str()) - })) -} - -fn parse_raw_cultnet_schema_message(input: &rmpv::Value) -> Result { - let object = input - .as_map() - .ok_or_else(|| anyhow!("cultnet.schema.v0 raw messages must be objects"))?; - - let get = |name: &str| -> Option<&rmpv::Value> { - object.iter().find_map(|(key, value)| { - key.as_str() - .filter(|candidate| *candidate == name) - .map(|_| value) - }) - }; - - let schema_version = require_legacy_string(get("schemaVersion"), "schemaVersion")?; - match schema_version.as_str() { - "cultnet.document_put_raw.v0" => Ok(CultNetMessage::DocumentPutRaw { - message_id: require_legacy_string(get("messageId"), "messageId")?, - document: require_raw_document_record(get("document"), "document")?, - }), - "cultnet.snapshot_response_raw.v0" => { - let documents = get("documents") - .and_then(rmpv::Value::as_array) - .ok_or_else(|| anyhow!("documents must be an array"))? - .iter() - .enumerate() - .map(|(index, value)| { - require_raw_document_record(value.into(), &format!("documents[{index}]")) - }) - .collect::>>()?; - - Ok(CultNetMessage::SnapshotResponseRaw { - message_id: require_legacy_string(get("messageId"), "messageId")?, - documents, - }) - } - _ => Err(anyhow!( - "Unsupported raw cultnet.schema.v0 schemaVersion {schema_version}" - )), - } -} - -fn encode_raw_cultnet_schema_message(message: &CultNetMessage) -> Result { - Ok(rmpv::Value::Map(match message { - CultNetMessage::DocumentPutRaw { - message_id, - document, - } => vec![ - ( - rmpv::Value::from("schemaVersion"), - rmpv::Value::from("cultnet.document_put_raw.v0"), - ), - ( - rmpv::Value::from("messageId"), - rmpv::Value::from(message_id.as_str()), - ), - ( - rmpv::Value::from("document"), - encode_raw_document_record(document), - ), - ], - CultNetMessage::SnapshotResponseRaw { - message_id, - documents, - } => vec![ - ( - rmpv::Value::from("schemaVersion"), - rmpv::Value::from("cultnet.snapshot_response_raw.v0"), - ), - ( - rmpv::Value::from("messageId"), - rmpv::Value::from(message_id.as_str()), - ), - ( - rmpv::Value::from("documents"), - rmpv::Value::Array(documents.iter().map(encode_raw_document_record).collect()), - ), - ], - _ => { - return Err(anyhow!( - "Message is not a raw cultnet.schema.v0 binary replication message" - )); - } - })) -} - -fn require_raw_document_record( - value: Option<&rmpv::Value>, - field_name: &str, -) -> Result { - let object = value - .and_then(rmpv::Value::as_map) - .ok_or_else(|| anyhow!("{field_name} must be an object"))?; - - let get = |name: &str| -> Option<&rmpv::Value> { - object.iter().find_map(|(key, value)| { - key.as_str() - .filter(|candidate| *candidate == name) - .map(|_| value) - }) - }; - - let payload_encoding = require_legacy_string( - get("payloadEncoding"), - &format!("{field_name}.payloadEncoding"), - )?; - - Ok(CultNetRawDocumentRecord { - schema_id: require_legacy_string(get("schemaId"), &format!("{field_name}.schemaId"))?, - record_key: require_legacy_string(get("recordKey"), &format!("{field_name}.recordKey"))?, - stored_at: require_legacy_string(get("storedAt"), &format!("{field_name}.storedAt"))?, - payload_encoding: match payload_encoding.as_str() { - "messagepack" => CultNetRawPayloadEncoding::Messagepack, - _ => { - return Err(anyhow!( - "{field_name}.payloadEncoding has unsupported value {payload_encoding}" - )); - } - }, - payload: get("payload") - .and_then(rmpv::Value::as_slice) - .map(|bytes| bytes.to_vec()) - .ok_or_else(|| anyhow!("{field_name}.payload must be binary MessagePack bytes"))?, - source_runtime_id: require_legacy_optional_string( - get("sourceRuntimeId"), - &format!("{field_name}.sourceRuntimeId"), - )?, - source_agent_id: require_legacy_optional_string( - get("sourceAgentId"), - &format!("{field_name}.sourceAgentId"), - )?, - source_role: require_legacy_optional_string( - get("sourceRole"), - &format!("{field_name}.sourceRole"), - )?, - tags: require_legacy_optional_string_array(get("tags"), &format!("{field_name}.tags"))?, - }) -} - -fn encode_raw_document_record(document: &CultNetRawDocumentRecord) -> rmpv::Value { - rmpv::Value::Map(vec![ - ( - rmpv::Value::from("schemaId"), - rmpv::Value::from(document.schema_id.as_str()), - ), - ( - rmpv::Value::from("recordKey"), - rmpv::Value::from(document.record_key.as_str()), - ), - ( - rmpv::Value::from("storedAt"), - rmpv::Value::from(document.stored_at.as_str()), - ), - ( - rmpv::Value::from("payloadEncoding"), - rmpv::Value::from(match document.payload_encoding { - CultNetRawPayloadEncoding::Messagepack => "messagepack", - }), - ), - ( - rmpv::Value::from("payload"), - rmpv::Value::Binary(document.payload.clone()), - ), - ( - rmpv::Value::from("sourceRuntimeId"), - document - .source_runtime_id - .as_deref() - .map(rmpv::Value::from) - .unwrap_or(rmpv::Value::Nil), - ), - ( - rmpv::Value::from("sourceAgentId"), - document - .source_agent_id - .as_deref() - .map(rmpv::Value::from) - .unwrap_or(rmpv::Value::Nil), - ), - ( - rmpv::Value::from("sourceRole"), - document - .source_role - .as_deref() - .map(rmpv::Value::from) - .unwrap_or(rmpv::Value::Nil), - ), - ( - rmpv::Value::from("tags"), - legacy_optional_string_array(document.tags.as_deref()), - ), - ]) -} - -fn parse_gamecult_networking_message(input: &rmpv::Value) -> Result { - let items = input.as_array().ok_or_else(|| { - anyhow!("gamecult.networking.v0 messages must be a 2-element union array") - })?; - if items.len() != 2 { - return Err(anyhow!( - "gamecult.networking.v0 messages must be a 2-element union array" - )); - } - let tag = items[0] - .as_i64() - .ok_or_else(|| anyhow!("gamecult.networking.v0 message tag must be an integer"))?; - let payload = items[1] - .as_array() - .ok_or_else(|| anyhow!("gamecult.networking.v0 message payload must be an array"))?; - match tag { - 0 => Ok(CultNetMessage::Login { - nonce: require_legacy_bytes(payload.first(), "LoginMessage.Nonce")?, - auth: require_legacy_bytes(payload.get(1), "LoginMessage.Auth")?, - password: require_legacy_bytes(payload.get(2), "LoginMessage.Password")?, - }), - 1 => Ok(CultNetMessage::Register { - nonce: require_legacy_bytes(payload.first(), "RegisterMessage.Nonce")?, - email: require_legacy_bytes(payload.get(1), "RegisterMessage.Email")?, - password: require_legacy_bytes(payload.get(2), "RegisterMessage.Password")?, - name: require_legacy_bytes(payload.get(3), "RegisterMessage.Name")?, - }), - 2 => Ok(CultNetMessage::Verify { - nonce: require_legacy_bytes(payload.first(), "VerifyMessage.Nonce")?, - session: require_legacy_bytes(payload.get(1), "VerifyMessage.Session")?, - }), - 3 => Ok(CultNetMessage::LoginSuccess { - nonce: require_legacy_bytes(payload.first(), "LoginSuccessMessage.Nonce")?, - session: require_legacy_bytes(payload.get(1), "LoginSuccessMessage.Session")?, - }), - 4 => Ok(CultNetMessage::Error { - error: require_legacy_string(payload.first(), "ErrorMessage.Error")?, - }), - 5 => Ok(CultNetMessage::SampleChangeName { - name: require_legacy_string(payload.first(), "ChangeNameMessage.Name")?, - }), - 6 => Ok(CultNetMessage::SampleChat { - text: require_legacy_string(payload.first(), "ChatMessage.Text")?, - }), - 7 => Ok(CultNetMessage::SchemaCatalogRequest { - message_id: require_legacy_string( - payload.first(), - "SchemaCatalogRequestMessage.MessageId", - )?, - include_schema_json: Some(require_legacy_bool( - payload.get(1), - "SchemaCatalogRequestMessage.IncludeSchemaJson", - )?), - schema_ids: require_legacy_optional_string_array( - payload.get(2), - "SchemaCatalogRequestMessage.SchemaIds", - )?, - kinds: require_legacy_optional_schema_kind_array( - payload.get(3), - "SchemaCatalogRequestMessage.Kinds", - )?, - }), - 8 => Ok(CultNetMessage::SchemaCatalogResponse { - message_id: require_legacy_string( - payload.first(), - "SchemaCatalogResponseMessage.MessageId", - )?, - schemas: require_legacy_schema_descriptor_array( - payload.get(1), - "SchemaCatalogResponseMessage.Schemas", - )?, - }), - _ => Err(anyhow!( - "Unsupported gamecult.networking.v0 union tag {tag}" - )), - } -} - -fn encode_gamecult_networking_message(message: &CultNetMessage) -> Result { - let pair = match message { - CultNetMessage::Login { - nonce, - auth, - password, - } => vec![ - rmpv::Value::from(0), - rmpv::Value::Array(vec![ - legacy_bytes(nonce, "LoginMessage.Nonce")?, - legacy_bytes(auth, "LoginMessage.Auth")?, - legacy_bytes(password, "LoginMessage.Password")?, - ]), - ], - CultNetMessage::Register { - nonce, - email, - password, - name, - } => vec![ - rmpv::Value::from(1), - rmpv::Value::Array(vec![ - legacy_bytes(nonce, "RegisterMessage.Nonce")?, - legacy_bytes(email, "RegisterMessage.Email")?, - legacy_bytes(password, "RegisterMessage.Password")?, - legacy_bytes(name, "RegisterMessage.Name")?, - ]), - ], - CultNetMessage::Verify { nonce, session } => vec![ - rmpv::Value::from(2), - rmpv::Value::Array(vec![ - legacy_bytes(nonce, "VerifyMessage.Nonce")?, - legacy_bytes(session, "VerifyMessage.Session")?, - ]), - ], - CultNetMessage::LoginSuccess { nonce, session } => vec![ - rmpv::Value::from(3), - rmpv::Value::Array(vec![ - legacy_bytes(nonce, "LoginSuccessMessage.Nonce")?, - legacy_bytes(session, "LoginSuccessMessage.Session")?, - ]), - ], - CultNetMessage::Error { error } => { - vec![ - rmpv::Value::from(4), - rmpv::Value::Array(vec![error.as_str().into()]), - ] - } - CultNetMessage::SampleChangeName { name } => { - vec![ - rmpv::Value::from(5), - rmpv::Value::Array(vec![name.as_str().into()]), - ] - } - CultNetMessage::SampleChat { text } => { - vec![ - rmpv::Value::from(6), - rmpv::Value::Array(vec![text.as_str().into()]), - ] - } - CultNetMessage::SchemaCatalogRequest { - message_id, - include_schema_json, - schema_ids, - kinds, - } => vec![ - rmpv::Value::from(7), - rmpv::Value::Array(vec![ - message_id.as_str().into(), - rmpv::Value::from(include_schema_json.unwrap_or(false)), - legacy_optional_string_array(schema_ids.as_deref()), - legacy_optional_schema_kind_array(kinds.as_deref()), - ]), - ], - CultNetMessage::SchemaCatalogResponse { - message_id, - schemas, - } => vec![ - rmpv::Value::from(8), - rmpv::Value::Array(vec![ - message_id.as_str().into(), - rmpv::Value::Array( - schemas - .iter() - .map(legacy_schema_descriptor) - .collect::>>()?, - ), - ]), - ], - _ => { - return Err(anyhow!( - "Message is not defined in the gamecult.networking.v0 contract" - )); - } - }; - Ok(rmpv::Value::Array(pair)) -} - -fn require_legacy_bytes(value: Option<&rmpv::Value>, field_name: &str) -> Result { - let bytes = value - .and_then(rmpv::Value::as_slice) - .ok_or_else(|| anyhow!("{field_name} must be binary data in gamecult.networking.v0"))?; - Ok(URL_SAFE_NO_PAD.encode(bytes)) -} - -fn require_legacy_string(value: Option<&rmpv::Value>, field_name: &str) -> Result { - let value = value - .and_then(rmpv::Value::as_str) - .ok_or_else(|| anyhow!("{field_name} must be a string in gamecult.networking.v0"))?; - Ok(value.to_string()) -} - -fn require_legacy_bool(value: Option<&rmpv::Value>, field_name: &str) -> Result { - value - .and_then(rmpv::Value::as_bool) - .ok_or_else(|| anyhow!("{field_name} must be a boolean in gamecult.networking.v0")) -} - -fn require_legacy_optional_string_array( - value: Option<&rmpv::Value>, - field_name: &str, -) -> Result>> { - let Some(value) = value else { - return Ok(None); - }; - if value.is_nil() { - return Ok(None); - } - let items = value - .as_array() - .ok_or_else(|| anyhow!("{field_name} must be an array in gamecult.networking.v0"))?; - let values = items - .iter() - .map(|item| { - item.as_str() - .map(ToString::to_string) - .ok_or_else(|| anyhow!("{field_name} must contain only strings")) - }) - .collect::>>()?; - Ok(Some(values)) -} - -fn require_legacy_optional_schema_kind_array( - value: Option<&rmpv::Value>, - field_name: &str, -) -> Result>> { - let Some(values) = require_legacy_optional_string_array(value, field_name)? else { - return Ok(None); - }; - values - .into_iter() - .map(|value| match value.as_str() { - "wire_message" => Ok(CultNetSchemaKind::WireMessage), - "document_payload" => Ok(CultNetSchemaKind::DocumentPayload), - "shared_contract" => Ok(CultNetSchemaKind::SharedContract), - _ => Err(anyhow!("{field_name} has unsupported schema kind {value}")), - }) - .collect::>>() - .map(Some) -} - -fn require_legacy_schema_descriptor_array( - value: Option<&rmpv::Value>, - field_name: &str, -) -> Result> { - let items = value - .and_then(rmpv::Value::as_array) - .ok_or_else(|| anyhow!("{field_name} must be an array in gamecult.networking.v0"))?; - items - .iter() - .enumerate() - .map(|(index, item)| { - require_legacy_schema_descriptor(item, &format!("{field_name}[{index}]")) - }) - .collect() -} - -fn require_legacy_schema_descriptor( - value: &rmpv::Value, - field_name: &str, -) -> Result { - let object = value - .as_map() - .ok_or_else(|| anyhow!("{field_name} must be an object in gamecult.networking.v0"))?; - - let get = |name: &str| -> Option<&rmpv::Value> { - object.iter().find_map(|(key, value)| { - key.as_str() - .filter(|candidate| *candidate == name) - .map(|_| value) - }) - }; - - let schema_id = require_legacy_string(get("schemaId"), &format!("{field_name}.SchemaId"))?; - let kind = require_legacy_string(get("kind"), &format!("{field_name}.Kind"))?; - let schema_version = require_legacy_optional_string( - get("schemaVersion"), - &format!("{field_name}.SchemaVersion"), - )?; - let document_type = - require_legacy_optional_string(get("documentType"), &format!("{field_name}.DocumentType"))?; - let title = require_legacy_optional_string(get("title"), &format!("{field_name}.Title"))?; - let wire_contracts = require_legacy_optional_wire_contracts( - get("wireContracts"), - &format!("{field_name}.WireContracts"), - )?; - let content_hash = - require_legacy_string(get("contentHash"), &format!("{field_name}.ContentHash"))?; - let schema_json = - require_legacy_optional_string(get("schemaJson"), &format!("{field_name}.SchemaJson"))?; - - let kind = match kind.as_str() { - "wire_message" => CultNetSchemaKind::WireMessage, - "document_payload" => CultNetSchemaKind::DocumentPayload, - "shared_contract" => CultNetSchemaKind::SharedContract, - _ => { - return Err(anyhow!( - "{field_name}.Kind has unsupported schema kind {kind}" - )); - } - }; - - Ok(CultNetSchemaDescriptor { - schema_id, - kind, - schema_version, - document_type, - title, - wire_contracts, - content_hash, - schema_json, - }) -} - -fn require_legacy_optional_string( - value: Option<&rmpv::Value>, - field_name: &str, -) -> Result> { - let Some(value) = value else { - return Ok(None); - }; - if value.is_nil() { - return Ok(None); - } - Ok(Some(require_legacy_string(Some(value), field_name)?)) -} - -fn require_legacy_optional_wire_contracts( - value: Option<&rmpv::Value>, - field_name: &str, -) -> Result> { - let Some(values) = require_legacy_optional_string_array(value, field_name)? else { - return Ok(Vec::new()); - }; - - values - .into_iter() - .map(|value| match value.as_str() { - "cultnet.schema.v0" => Ok(CultNetWireContract::CultNetSchemaV0), - "gamecult.networking.v0" => Ok(CultNetWireContract::GameCultNetworkingV0), - _ => Err(anyhow!( - "{field_name} contains unsupported wire contract {value}" - )), - }) - .collect() -} - -fn legacy_bytes(input: &str, field_name: &str) -> Result { - if input.trim().is_empty() { - return Err(anyhow!("{field_name} must be a non-empty base64url string")); - } - Ok(rmpv::Value::Binary(URL_SAFE_NO_PAD.decode(input)?)) -} - -fn legacy_optional_string_array(input: Option<&[String]>) -> rmpv::Value { - match input { - Some(values) => rmpv::Value::Array( - values - .iter() - .map(|value| rmpv::Value::from(value.as_str())) - .collect(), - ), - None => rmpv::Value::Nil, - } -} - -fn legacy_optional_schema_kind_array(input: Option<&[CultNetSchemaKind]>) -> rmpv::Value { - match input { - Some(values) => rmpv::Value::Array( - values - .iter() - .map(|value| { - rmpv::Value::from(match value { - CultNetSchemaKind::WireMessage => "wire_message", - CultNetSchemaKind::DocumentPayload => "document_payload", - CultNetSchemaKind::SharedContract => "shared_contract", - }) - }) - .collect(), - ), - None => rmpv::Value::Nil, - } -} - -fn legacy_schema_descriptor(schema: &CultNetSchemaDescriptor) -> Result { - Ok(rmpv::Value::Map(vec![ - ( - rmpv::Value::from("schemaId"), - rmpv::Value::from(schema.schema_id.as_str()), - ), - ( - rmpv::Value::from("kind"), - rmpv::Value::from(match schema.kind { - CultNetSchemaKind::WireMessage => "wire_message", - CultNetSchemaKind::DocumentPayload => "document_payload", - CultNetSchemaKind::SharedContract => "shared_contract", - }), - ), - ( - rmpv::Value::from("schemaVersion"), - schema - .schema_version - .as_deref() - .map(rmpv::Value::from) - .unwrap_or(rmpv::Value::Nil), - ), - ( - rmpv::Value::from("documentType"), - schema - .document_type - .as_deref() - .map(rmpv::Value::from) - .unwrap_or(rmpv::Value::Nil), - ), - ( - rmpv::Value::from("title"), - schema - .title - .as_deref() - .map(rmpv::Value::from) - .unwrap_or(rmpv::Value::Nil), - ), - ( - rmpv::Value::from("wireContracts"), - rmpv::Value::Array( - schema - .wire_contracts - .iter() - .map(|value| { - rmpv::Value::from(match value { - CultNetWireContract::CultNetSchemaV0 => "cultnet.schema.v0", - CultNetWireContract::GameCultNetworkingV0 => "gamecult.networking.v0", - }) - }) - .collect(), - ), - ), - ( - rmpv::Value::from("contentHash"), - rmpv::Value::from(schema.content_hash.as_str()), - ), - ( - rmpv::Value::from("schemaJson"), - schema - .schema_json - .as_deref() - .map(rmpv::Value::from) - .unwrap_or(rmpv::Value::Nil), - ), - ])) -} diff --git a/vendor/cultnet-rs/src/cultmesh.rs b/vendor/cultnet-rs/src/cultmesh.rs deleted file mode 100644 index 20d2123..0000000 --- a/vendor/cultnet-rs/src/cultmesh.rs +++ /dev/null @@ -1,525 +0,0 @@ -use anyhow::Result; -use anyhow::anyhow; -use chrono::DateTime; -use chrono::Utc; -use std::collections::BTreeMap; -use std::net::SocketAddr; -use std::net::ToSocketAddrs; -use std::net::UdpSocket; -use std::thread; -use std::time::Duration; -use std::time::Instant; - -use crate::CultNetRudpSocketMode; -use crate::CultNetRudpSocketTransportConnection; -use crate::CultNetRudpSocketTransportOptions; -use crate::CultNetSchemaRegistry; -use crate::CultNetShardCatalog; -use crate::builtin_schema_registry; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshPeerCard { - pub peer_id: String, - pub verse_id: String, - pub endpoints: Vec, - pub roles: Vec, - pub shard_ids: Vec, - pub authority_lease_id: Option, -} - -impl CultMeshPeerCard { - pub fn new( - peer_id: impl Into, - verse_id: impl Into, - endpoints: impl IntoIterator>, - ) -> Self { - Self { - peer_id: peer_id.into(), - verse_id: verse_id.into(), - endpoints: endpoints.into_iter().map(Into::into).collect(), - roles: Vec::new(), - shard_ids: Vec::new(), - authority_lease_id: None, - } - } - - pub fn with_roles(mut self, roles: impl IntoIterator>) -> Self { - self.roles = roles.into_iter().map(Into::into).collect(); - self - } - - pub fn with_shard_ids( - mut self, - shard_ids: impl IntoIterator>, - ) -> Self { - self.shard_ids = shard_ids.into_iter().map(Into::into).collect(); - self - } - - pub fn with_authority_lease_id(mut self, authority_lease_id: impl Into) -> Self { - self.authority_lease_id = Some(authority_lease_id.into()); - self - } - - pub fn rudp_endpoint(&self) -> Result { - let endpoint = self - .endpoints - .iter() - .find(|endpoint| endpoint.to_ascii_lowercase().starts_with("rudp://")) - .ok_or_else(|| anyhow!("Peer {} does not advertise a RUDP endpoint", self.peer_id))?; - CultMesh::parse_rudp_endpoint(endpoint) - } -} - -#[derive(Clone, Debug, Default)] -pub struct CultMeshPeerCatalog { - peers: BTreeMap, -} - -impl CultMeshPeerCatalog { - pub fn new() -> Self { - Self::default() - } - - pub fn upsert(&mut self, peer: CultMeshPeerCard) -> Result<()> { - require_non_empty(&peer.peer_id, "peer.peer_id")?; - require_non_empty(&peer.verse_id, "peer.verse_id")?; - self.peers.insert(peer.peer_id.clone(), peer); - Ok(()) - } - - pub fn get(&self, peer_id: &str) -> Option<&CultMeshPeerCard> { - self.peers.get(peer_id) - } - - pub fn peers(&self) -> Vec { - self.peers.values().cloned().collect() - } - - pub fn find(&self, verse_id: &str, role: Option<&str>) -> Vec { - self.peers - .values() - .filter(|peer| { - peer.verse_id == verse_id - && role.is_none_or(|role| peer.roles.iter().any(|value| value == role)) - }) - .cloned() - .collect() - } - - pub fn find_authorized( - &self, - verse_id: &str, - role: &str, - leases: &CultMeshAuthorityLeaseCatalog, - shard_id: Option<&str>, - at: DateTime, - ) -> Vec { - self.find(verse_id, Some(role)) - .into_iter() - .filter(|peer| leases.is_authorized(peer, role, shard_id, at)) - .collect() - } - - pub fn first_authorized( - &self, - verse_id: &str, - role: &str, - leases: &CultMeshAuthorityLeaseCatalog, - shard_id: Option<&str>, - at: DateTime, - ) -> Option { - self.find_authorized(verse_id, role, leases, shard_id, at) - .into_iter() - .next() - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultMeshAuthorityLease { - pub lease_id: String, - pub verse_id: String, - pub peer_id: String, - pub roles: Vec, - pub shard_ids: Vec, - pub issuer_runtime_id: Option, - pub valid_from: DateTime, - pub expires_at: DateTime, -} - -impl CultMeshAuthorityLease { - pub fn covers( - &self, - peer: &CultMeshPeerCard, - role: &str, - shard_id: Option<&str>, - at: DateTime, - ) -> bool { - at >= self.valid_from - && at < self.expires_at - && self.verse_id == peer.verse_id - && self.peer_id == peer.peer_id - && self.roles.iter().any(|value| value == role) - && peer.roles.iter().any(|value| value == role) - && shard_id.is_none_or(|shard_id| { - self.shard_ids.is_empty() || self.shard_ids.iter().any(|value| value == shard_id) - }) - } -} - -#[derive(Clone, Debug, Default)] -pub struct CultMeshAuthorityLeaseCatalog { - leases: BTreeMap, -} - -impl CultMeshAuthorityLeaseCatalog { - pub fn new() -> Self { - Self::default() - } - - pub fn upsert(&mut self, lease: CultMeshAuthorityLease) -> Result<()> { - require_non_empty(&lease.lease_id, "lease.lease_id")?; - require_non_empty(&lease.verse_id, "lease.verse_id")?; - require_non_empty(&lease.peer_id, "lease.peer_id")?; - if lease.expires_at <= lease.valid_from { - return Err(anyhow!( - "CultMesh authority lease expiry must be after valid_from" - )); - } - self.leases.insert(lease.lease_id.clone(), lease); - Ok(()) - } - - pub fn get(&self, lease_id: &str) -> Option<&CultMeshAuthorityLease> { - self.leases.get(lease_id) - } - - pub fn leases(&self) -> Vec { - self.leases.values().cloned().collect() - } - - pub fn is_authorized( - &self, - peer: &CultMeshPeerCard, - role: &str, - shard_id: Option<&str>, - at: DateTime, - ) -> bool { - let Some(lease_id) = peer.authority_lease_id.as_deref() else { - return false; - }; - self.leases - .get(lease_id) - .is_some_and(|lease| lease.covers(peer, role, shard_id, at)) - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetRudpEndpoint { - pub host: String, - pub port: u16, -} - -impl CultNetRudpEndpoint { - pub fn uri(&self) -> String { - if self.host.contains(':') && !self.host.starts_with('[') { - format!("rudp://[{}]:{}", self.host, self.port) - } else { - format!("rudp://{}:{}", self.host, self.port) - } - } - - pub fn socket_addr(&self) -> Result { - format!("{}:{}", socket_host(&self.host), self.port) - .to_socket_addrs() - .map_err(|error| anyhow!("Invalid RUDP socket endpoint {}: {error}", self.uri()))? - .next() - .ok_or_else(|| anyhow!("RUDP socket endpoint {} did not resolve", self.uri())) - } -} - -#[derive(Clone, Debug)] -pub struct CultMeshRudpSocketOptions { - pub bind_host: String, - pub bind_port: u16, - pub read_timeout: Option, - pub initial_sequence: u32, - pub resend_delay_ms: u64, - pub max_payload_bytes: Option, - pub max_fragment_bytes: Option, - pub max_pending_reliable_packets: Option, -} - -impl Default for CultMeshRudpSocketOptions { - fn default() -> Self { - Self { - bind_host: "127.0.0.1".to_string(), - bind_port: 0, - read_timeout: Some(Duration::from_millis(20)), - initial_sequence: 1, - resend_delay_ms: 250, - max_payload_bytes: None, - max_fragment_bytes: None, - max_pending_reliable_packets: None, - } - } -} - -#[derive(Clone, Debug)] -pub struct CultMeshRudpClientOptions { - pub socket_options: CultMeshRudpSocketOptions, - pub connect_payload: Vec, - pub connect_timeout: Duration, - pub poll_interval: Duration, -} - -impl Default for CultMeshRudpClientOptions { - fn default() -> Self { - Self { - socket_options: CultMeshRudpSocketOptions::default(), - connect_payload: Vec::new(), - connect_timeout: Duration::from_secs(1), - poll_interval: Duration::from_millis(5), - } - } -} - -pub struct CultMesh; - -impl CultMesh { - pub fn create_schema_registry() -> CultNetSchemaRegistry { - CultNetSchemaRegistry::new() - } - - pub fn create_builtin_schema_registry() -> Result { - builtin_schema_registry() - } - - pub fn create_shard_catalog() -> CultNetShardCatalog { - CultNetShardCatalog::new() - } - - pub fn create_peer_catalog() -> CultMeshPeerCatalog { - CultMeshPeerCatalog::new() - } - - pub fn create_authority_lease_catalog() -> CultMeshAuthorityLeaseCatalog { - CultMeshAuthorityLeaseCatalog::new() - } - - pub fn parse_rudp_endpoint(endpoint: &str) -> Result { - cultnet_rudp_endpoint(endpoint) - } - - pub fn create_rudp_server( - runtime_id: impl Into, - connection_id: u32, - options: CultMeshRudpSocketOptions, - ) -> Result { - let socket = bind_rudp_socket(&options)?; - CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions { - runtime_id: runtime_id.into(), - socket, - mode: CultNetRudpSocketMode::Server, - remote_addr: None, - connection_id, - initial_sequence: options.initial_sequence, - resend_delay_ms: options.resend_delay_ms, - transport_id: None, - max_payload_bytes: options.max_payload_bytes, - max_fragment_bytes: options.max_fragment_bytes, - max_pending_reliable_packets: options.max_pending_reliable_packets, - reconnect_policy: None, - }) - } - - pub fn create_rudp_client( - runtime_id: impl Into, - connection_id: u32, - endpoint: &CultNetRudpEndpoint, - options: CultMeshRudpSocketOptions, - ) -> Result { - let socket = bind_rudp_socket(&options)?; - CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions { - runtime_id: runtime_id.into(), - socket, - mode: CultNetRudpSocketMode::Client, - remote_addr: Some(endpoint.socket_addr()?), - connection_id, - initial_sequence: options.initial_sequence, - resend_delay_ms: options.resend_delay_ms, - transport_id: None, - max_payload_bytes: options.max_payload_bytes, - max_fragment_bytes: options.max_fragment_bytes, - max_pending_reliable_packets: options.max_pending_reliable_packets, - reconnect_policy: None, - }) - } - - pub fn create_rudp_client_for_endpoint( - runtime_id: impl Into, - connection_id: u32, - endpoint: &str, - options: CultMeshRudpSocketOptions, - ) -> Result { - let endpoint = Self::parse_rudp_endpoint(endpoint)?; - Self::create_rudp_client(runtime_id, connection_id, &endpoint, options) - } - - pub fn create_rudp_client_for_peer( - runtime_id: impl Into, - connection_id: u32, - peer: &CultMeshPeerCard, - options: CultMeshRudpSocketOptions, - ) -> Result { - let endpoint = peer.rudp_endpoint()?; - Self::create_rudp_client(runtime_id, connection_id, &endpoint, options) - } - - pub fn create_rudp_client_for_authorized_peer( - runtime_id: impl Into, - connection_id: u32, - peers: &CultMeshPeerCatalog, - leases: &CultMeshAuthorityLeaseCatalog, - verse_id: &str, - role: &str, - shard_id: Option<&str>, - at: DateTime, - options: CultMeshRudpSocketOptions, - ) -> Result { - let peer = peers - .first_authorized(verse_id, role, leases, shard_id, at) - .ok_or_else(|| { - anyhow!("No authorized RUDP peer for role {role} in Verse {verse_id}") - })?; - Self::create_rudp_client_for_peer(runtime_id, connection_id, &peer, options) - } - - pub fn connect_rudp_client( - runtime_id: impl Into, - connection_id: u32, - endpoint: &CultNetRudpEndpoint, - options: CultMeshRudpClientOptions, - ) -> Result { - let runtime_id = runtime_id.into(); - let mut client = Self::create_rudp_client( - runtime_id.clone(), - connection_id, - endpoint, - options.socket_options, - )?; - client.connect(options.connect_payload)?; - let deadline = Instant::now() + options.connect_timeout; - while Instant::now() < deadline { - let _ = client.receive_once()?; - client.poll_resends()?; - if client.connected() { - return Ok(client); - } - thread::sleep(options.poll_interval); - } - if client.connected() { - return Ok(client); - } - Err(anyhow!( - "Timed out waiting for RUDP client {runtime_id} to connect" - )) - } - - pub fn connect_rudp_client_for_endpoint( - runtime_id: impl Into, - connection_id: u32, - endpoint: &str, - options: CultMeshRudpClientOptions, - ) -> Result { - let endpoint = Self::parse_rudp_endpoint(endpoint)?; - Self::connect_rudp_client(runtime_id, connection_id, &endpoint, options) - } - - pub fn connect_rudp_client_for_peer( - runtime_id: impl Into, - connection_id: u32, - peer: &CultMeshPeerCard, - options: CultMeshRudpClientOptions, - ) -> Result { - let endpoint = peer.rudp_endpoint()?; - Self::connect_rudp_client(runtime_id, connection_id, &endpoint, options) - } - - pub fn connect_rudp_client_for_authorized_peer( - runtime_id: impl Into, - connection_id: u32, - peers: &CultMeshPeerCatalog, - leases: &CultMeshAuthorityLeaseCatalog, - verse_id: &str, - role: &str, - shard_id: Option<&str>, - at: DateTime, - options: CultMeshRudpClientOptions, - ) -> Result { - let peer = peers - .first_authorized(verse_id, role, leases, shard_id, at) - .ok_or_else(|| { - anyhow!("No authorized RUDP peer for role {role} in Verse {verse_id}") - })?; - Self::connect_rudp_client_for_peer(runtime_id, connection_id, &peer, options) - } -} - -pub fn cultnet_rudp_endpoint(endpoint: &str) -> Result { - let scheme = "rudp://"; - if endpoint.len() < scheme.len() || !endpoint[..scheme.len()].eq_ignore_ascii_case(scheme) { - return Err(anyhow!("RUDP endpoint must use the rudp scheme")); - } - let rest = &endpoint[scheme.len()..]; - let (host, port_text) = if let Some(rest) = rest.strip_prefix('[') { - let (host, port_rest) = rest - .split_once(']') - .ok_or_else(|| anyhow!("RUDP IPv6 endpoint must close the host bracket"))?; - let port_text = port_rest - .strip_prefix(':') - .ok_or_else(|| anyhow!("RUDP endpoint must include a valid port"))?; - (host.to_string(), port_text) - } else { - let (host, port_text) = rest - .rsplit_once(':') - .ok_or_else(|| anyhow!("RUDP endpoint must include a host and port"))?; - if host.contains(':') { - return Err(anyhow!("RUDP IPv6 endpoint host must be bracketed")); - } - (host.to_string(), port_text) - }; - require_non_empty(&host, "rudp host")?; - let port = port_text - .parse::() - .map_err(|error| anyhow!("RUDP endpoint must include a valid port: {error}"))?; - if port == 0 { - return Err(anyhow!("RUDP endpoint port must be greater than zero")); - } - Ok(CultNetRudpEndpoint { host, port }) -} - -fn bind_rudp_socket(options: &CultMeshRudpSocketOptions) -> Result { - let socket = UdpSocket::bind(format!( - "{}:{}", - socket_host(&options.bind_host), - options.bind_port - ))?; - socket.set_read_timeout(options.read_timeout)?; - Ok(socket) -} - -fn socket_host(host: &str) -> String { - if host.contains(':') && !host.starts_with('[') { - format!("[{host}]") - } else { - host.to_string() - } -} - -fn require_non_empty(value: &str, field: &str) -> Result<()> { - if value.trim().is_empty() { - return Err(anyhow!("CultMesh {field} must not be empty")); - } - Ok(()) -} diff --git a/vendor/cultnet-rs/src/framing.rs b/vendor/cultnet-rs/src/framing.rs deleted file mode 100644 index 0157881..0000000 --- a/vendor/cultnet-rs/src/framing.rs +++ /dev/null @@ -1,49 +0,0 @@ -use anyhow::Result; -use anyhow::anyhow; - -pub const FRAME_HEADER_BYTES: usize = 4; - -pub fn encode_frame(payload: &[u8]) -> Result> { - let len: u32 = payload - .len() - .try_into() - .map_err(|_| anyhow!("CultNet frame payload is too large"))?; - let mut frame = Vec::with_capacity(FRAME_HEADER_BYTES + payload.len()); - frame.extend_from_slice(&len.to_be_bytes()); - frame.extend_from_slice(payload); - Ok(frame) -} - -#[derive(Clone, Debug, Default)] -pub struct LengthPrefixedMessageFramer { - buffer: Vec, -} - -impl LengthPrefixedMessageFramer { - pub fn new() -> Self { - Self::default() - } - - pub fn push(&mut self, chunk: impl AsRef<[u8]>) -> Vec> { - self.buffer.extend_from_slice(chunk.as_ref()); - let mut frames = Vec::new(); - loop { - if self.buffer.len() < FRAME_HEADER_BYTES { - break; - } - let payload_len = u32::from_be_bytes([ - self.buffer[0], - self.buffer[1], - self.buffer[2], - self.buffer[3], - ]) as usize; - let total_len = FRAME_HEADER_BYTES + payload_len; - if self.buffer.len() < total_len { - break; - } - frames.push(self.buffer[FRAME_HEADER_BYTES..total_len].to_vec()); - self.buffer.drain(0..total_len); - } - frames - } -} diff --git a/vendor/cultnet-rs/src/lib.rs b/vendor/cultnet-rs/src/lib.rs deleted file mode 100644 index c13c4e8..0000000 --- a/vendor/cultnet-rs/src/lib.rs +++ /dev/null @@ -1,25 +0,0 @@ -mod contracts; -mod cultmesh; -mod framing; -mod operation_service; -mod provider_session; -mod replication; -mod rudp; -mod schema_discovery; -mod security; -mod service_identity; -mod shard_catalog; -mod transport; - -pub use contracts::*; -pub use cultmesh::*; -pub use framing::*; -pub use operation_service::*; -pub use provider_session::*; -pub use replication::*; -pub use rudp::*; -pub use schema_discovery::*; -pub use security::*; -pub use service_identity::*; -pub use shard_catalog::*; -pub use transport::*; diff --git a/vendor/cultnet-rs/src/operation_service.rs b/vendor/cultnet-rs/src/operation_service.rs deleted file mode 100644 index 84d60cf..0000000 --- a/vendor/cultnet-rs/src/operation_service.rs +++ /dev/null @@ -1,60 +0,0 @@ -use std::net::UdpSocket; -use std::time::Duration; - -use anyhow::{Result, anyhow}; - -use crate::{ - CultNetMessage, CultNetRudpSocketTransportConnection, CultNetRudpSocketTransportOptions, -}; - -pub const CULTNET_OPERATION_CONNECTION_ID: u32 = 0x4355_4c54; - -pub struct CultNetOperationServer { - transport: CultNetRudpSocketTransportConnection, - endpoint: String, -} - -impl CultNetOperationServer { - pub fn bind(runtime_id: impl Into, host: &str, port: u16) -> Result { - let socket = UdpSocket::bind((host, port))?; - socket.set_read_timeout(Some(Duration::from_millis(25)))?; - let address = socket.local_addr()?; - let mut options = CultNetRudpSocketTransportOptions::server( - runtime_id, - socket, - CULTNET_OPERATION_CONNECTION_ID, - ); - options.max_fragment_bytes = Some(2048); - Ok(Self { - transport: CultNetRudpSocketTransportConnection::new(options)?, - endpoint: format!("rudp://{}:{}", address.ip(), address.port()), - }) - } - - pub fn endpoint(&self) -> &str { - &self.endpoint - } - - pub fn serve_once( - &mut self, - mut handler: impl FnMut(CultNetMessage) -> Result, - ) -> Result { - let Some(request) = self.transport.receive_schema_message_once()? else { - self.transport.poll_resends()?; - return Ok(false); - }; - if !matches!(request, CultNetMessage::OperationRequest { .. }) { - return Err(anyhow!( - "CultNet operation service received a non-operation message" - )); - } - let response = handler(request)?; - if !matches!(response, CultNetMessage::OperationResponse { .. }) { - return Err(anyhow!( - "CultNet operation handler returned a non-operation response" - )); - } - self.transport.send_schema_message(&response)?; - Ok(true) - } -} diff --git a/vendor/cultnet-rs/src/provider_session.rs b/vendor/cultnet-rs/src/provider_session.rs deleted file mode 100644 index 9965b0e..0000000 --- a/vendor/cultnet-rs/src/provider_session.rs +++ /dev/null @@ -1,625 +0,0 @@ -use anyhow::{Result, anyhow}; -use base64::Engine; -use base64::engine::general_purpose::STANDARD; -use chrono::DateTime; -use rmpv::Value; -use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; - -use crate::{CultNetMessage, CultNetRawDocumentRecord}; - -pub const CULTMESH_PROVIDER_SESSION_SERVICE_ID: &str = "gamecult.mesh.provider_session"; -pub const CULTMESH_PROVIDER_SESSION_PAYLOAD_ENCODING: &str = "messagepack-base64"; - -pub const CULTMESH_PROVIDER_REGISTER_OPERATION: &str = "provider.register"; -pub const CULTMESH_PROVIDER_RENEW_OPERATION: &str = "provider.renew"; -pub const CULTMESH_PROVIDER_PUBLICATION_PUT_OPERATION: &str = "provider.publication.put"; -pub const CULTMESH_PROVIDER_PUBLICATION_DELETE_OPERATION: &str = "provider.publication.delete"; -pub const CULTMESH_PROVIDER_RECEIPT_PUT_OPERATION: &str = "provider.receipt.put"; -pub const CULTMESH_PROVIDER_WITHDRAW_OPERATION: &str = "provider.withdraw"; - -pub const CULTMESH_PROVIDER_REGISTRATION_SCHEMA: &str = "gamecult.mesh.provider_registration.v1"; -pub const CULTMESH_PROVIDER_LEASE_SCHEMA: &str = "gamecult.mesh.provider_lease.v1"; -pub const CULTMESH_PROVIDER_LEASE_RENEWAL_SCHEMA: &str = "gamecult.mesh.provider_lease_renewal.v1"; -pub const CULTMESH_PROVIDER_PUBLICATION_PUT_SCHEMA: &str = - "gamecult.mesh.provider_publication_put.v1"; -pub const CULTMESH_PROVIDER_PUBLICATION_DELETE_SCHEMA: &str = - "gamecult.mesh.provider_publication_delete.v1"; -pub const CULTMESH_PROVIDER_COMMAND_SCHEMA: &str = "gamecult.mesh.provider_command.v1"; -pub const CULTMESH_PROVIDER_RECEIPT_PUT_SCHEMA: &str = "gamecult.mesh.provider_receipt_put.v1"; -pub const CULTMESH_PROVIDER_WITHDRAWAL_SCHEMA: &str = "gamecult.mesh.provider_withdrawal.v1"; -pub const CULTMESH_PROVIDER_MUTATION_ACCEPTANCE_SCHEMA: &str = - "gamecult.mesh.provider_mutation_acceptance.v1"; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CultMeshProviderOperationStatus { - Ok, - Conflict, - Expired, - Denied, - Invalid, -} - -impl CultMeshProviderOperationStatus { - pub fn as_str(self) -> &'static str { - match self { - Self::Ok => "ok", - Self::Conflict => "conflict", - Self::Expired => "expired", - Self::Denied => "denied", - Self::Invalid => "invalid", - } - } - - pub fn parse(value: &str) -> Result { - match value { - "ok" => Ok(Self::Ok), - "conflict" => Ok(Self::Conflict), - "expired" => Ok(Self::Expired), - "denied" => Ok(Self::Denied), - "invalid" => Ok(Self::Invalid), - _ => Err(anyhow!("Unsupported provider-session status {value:?}")), - } - } -} - -pub trait CultMeshProviderSessionPayload { - fn validate(&self) -> Result<()>; -} - -/// Provider-owned evidence carried verbatim in the RUDP Connect payload. -/// `client_session_id` distinguishes process/session generations; the optional -/// token remains opaque authorization evidence for the receiving service. -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderConnectEvidenceWire { - pub client_session_id: String, - #[serde(default)] - pub session_token: Option, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderConnectEvidenceWire { - fn validate(&self) -> Result<()> { - require_text(&self.client_session_id, "clientSessionId")?; - require_optional_text(self.session_token.as_deref(), "sessionToken") - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderRegistrationWire { - pub provider_id: String, - pub service_instance_id: String, - pub endpoint_id: String, - pub verse_id: String, - pub requested_lease_duration_ms: i32, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub authority_lease_id: Option, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderRegistrationWire { - fn validate(&self) -> Result<()> { - require_identity( - &self.provider_id, - &self.service_instance_id, - &self.endpoint_id, - &self.verse_id, - )?; - require_positive(self.requested_lease_duration_ms, "requestedLeaseDurationMs")?; - require_optional_text(self.authority_lease_id.as_deref(), "authorityLeaseId") - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderLeaseWire { - pub provider_id: String, - pub service_instance_id: String, - pub endpoint_id: String, - pub verse_id: String, - pub lease_id: String, - pub valid_from_utc: String, - pub expires_at_utc: String, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderLeaseWire { - fn validate(&self) -> Result<()> { - require_identity( - &self.provider_id, - &self.service_instance_id, - &self.endpoint_id, - &self.verse_id, - )?; - require_text(&self.lease_id, "leaseId")?; - let valid_from = require_timestamp(&self.valid_from_utc, "validFromUtc")?; - let expires_at = require_timestamp(&self.expires_at_utc, "expiresAtUtc")?; - if expires_at <= valid_from { - return Err(anyhow!("Provider lease must expire after validFromUtc")); - } - Ok(()) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderLeaseRenewalWire { - pub lease_id: String, - pub requested_lease_duration_ms: i32, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderLeaseRenewalWire { - fn validate(&self) -> Result<()> { - require_text(&self.lease_id, "leaseId")?; - require_positive(self.requested_lease_duration_ms, "requestedLeaseDurationMs") - } -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderPublicationPutWire { - pub lease_id: String, - pub publication_id: String, - pub document: CultNetRawDocumentRecord, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderPublicationPutWire { - fn validate(&self) -> Result<()> { - require_text(&self.lease_id, "leaseId")?; - require_text(&self.publication_id, "publicationId")?; - validate_raw_document(&self.document) - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderPublicationDeleteWire { - pub lease_id: String, - pub publication_id: String, - pub schema_id: String, - pub record_key: String, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderPublicationDeleteWire { - fn validate(&self) -> Result<()> { - require_text(&self.lease_id, "leaseId")?; - require_text(&self.publication_id, "publicationId")?; - require_text(&self.schema_id, "schemaId")?; - require_text(&self.record_key, "recordKey") - } -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderCommandWire { - pub command_id: String, - pub command_kind: String, - pub provider_id: String, - pub service_instance_id: String, - pub payload: Value, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderCommandWire { - fn validate(&self) -> Result<()> { - require_text(&self.command_id, "commandId")?; - require_text(&self.command_kind, "commandKind")?; - require_text(&self.provider_id, "providerId")?; - require_text(&self.service_instance_id, "serviceInstanceId") - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum CultMeshProviderReceiptStateWire { - Applied, - Rejected, - Failed, -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderCommandReceiptWire { - pub receipt_id: String, - pub command_id: String, - pub command_kind: String, - pub provider_id: String, - pub service_instance_id: String, - pub state: CultMeshProviderReceiptStateWire, - pub completed_at_utc: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub result: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub error: Option, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderCommandReceiptWire { - fn validate(&self) -> Result<()> { - require_text(&self.receipt_id, "receiptId")?; - require_text(&self.command_id, "commandId")?; - require_text(&self.command_kind, "commandKind")?; - require_text(&self.provider_id, "providerId")?; - require_text(&self.service_instance_id, "serviceInstanceId")?; - require_timestamp(&self.completed_at_utc, "completedAtUtc")?; - require_optional_text(self.error.as_deref(), "error") - } -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderReceiptPutWire { - pub lease_id: String, - pub receipt: CultMeshProviderCommandReceiptWire, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderReceiptPutWire { - fn validate(&self) -> Result<()> { - require_text(&self.lease_id, "leaseId")?; - self.receipt.validate() - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderWithdrawalWire { - pub lease_id: String, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderWithdrawalWire { - fn validate(&self) -> Result<()> { - require_text(&self.lease_id, "leaseId") - } -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CultMeshProviderMutationAcceptanceWire { - pub accepted_at_utc: String, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub lease_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub publication_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub command_id: Option, - #[serde(default, skip_serializing_if = "Option::is_none")] - pub receipt_id: Option, -} - -impl CultMeshProviderSessionPayload for CultMeshProviderMutationAcceptanceWire { - fn validate(&self) -> Result<()> { - require_timestamp(&self.accepted_at_utc, "acceptedAtUtc")?; - require_optional_text(self.lease_id.as_deref(), "leaseId")?; - require_optional_text(self.publication_id.as_deref(), "publicationId")?; - require_optional_text(self.command_id.as_deref(), "commandId")?; - require_optional_text(self.receipt_id.as_deref(), "receiptId") - } -} - -#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] -pub struct CultMeshProviderErrorWire {} - -impl CultMeshProviderSessionPayload for CultMeshProviderErrorWire { - fn validate(&self) -> Result<()> { - Ok(()) - } -} - -pub fn encode_provider_session_payload(value: &T) -> Result -where - T: CultMeshProviderSessionPayload + Serialize, -{ - value.validate()?; - Ok(STANDARD.encode(rmp_serde::to_vec_named(value)?)) -} - -pub fn encode_provider_connect_evidence( - evidence: &CultMeshProviderConnectEvidenceWire, -) -> Result> { - evidence.validate()?; - Ok(rmp_serde::to_vec_named(evidence)?) -} - -pub fn decode_provider_connect_evidence( - payload: &[u8], -) -> Result { - if payload.is_empty() { - return Err(anyhow!( - "Provider Connect evidence must be a non-empty MessagePack map" - )); - } - let evidence: CultMeshProviderConnectEvidenceWire = rmp_serde::from_slice(payload)?; - evidence.validate()?; - Ok(evidence) -} - -pub fn decode_provider_session_payload(payload: &str) -> Result -where - T: CultMeshProviderSessionPayload + DeserializeOwned, -{ - require_text(payload, "payload")?; - let bytes = STANDARD - .decode(payload) - .map_err(|error| anyhow!("Provider-session payload must be MessagePack base64: {error}"))?; - let value: T = rmp_serde::from_slice(&bytes)?; - value.validate()?; - Ok(value) -} - -pub fn decode_provider_command_document( - document: &CultNetRawDocumentRecord, -) -> Result { - if document.schema_id != CULTMESH_PROVIDER_COMMAND_SCHEMA { - return Err(anyhow!( - "Expected provider command schema {:?}, received {:?}", - CULTMESH_PROVIDER_COMMAND_SCHEMA, - document.schema_id - )); - } - let command: CultMeshProviderCommandWire = rmp_serde::from_slice(&document.payload)?; - command.validate()?; - Ok(command) -} - -pub fn create_provider_session_request( - message_id: impl Into, - operation: impl Into, - payload_schema: impl Into, - payload: &T, - source_runtime_id: Option, - target_runtime_id: Option, -) -> Result -where - T: CultMeshProviderSessionPayload + Serialize, -{ - let message_id = message_id.into(); - let operation = operation.into(); - let payload_schema = payload_schema.into(); - require_text(&message_id, "messageId")?; - validate_operation_schema(&operation, &payload_schema)?; - Ok(CultNetMessage::OperationRequest { - message_id, - service_id: CULTMESH_PROVIDER_SESSION_SERVICE_ID.to_string(), - operation, - payload_schema, - payload_encoding: CULTMESH_PROVIDER_SESSION_PAYLOAD_ENCODING.to_string(), - payload: encode_provider_session_payload(payload)?, - source_runtime_id, - target_runtime_id, - }) -} - -pub fn decode_provider_session_request( - request: &CultNetMessage, - expected_operation: &str, - expected_payload_schema: &str, -) -> Result -where - T: CultMeshProviderSessionPayload + DeserializeOwned, -{ - let CultNetMessage::OperationRequest { - service_id, - operation, - payload_schema, - payload_encoding, - payload, - .. - } = request - else { - return Err(anyhow!("Expected a CultNet operation request")); - }; - require_envelope( - service_id, - operation, - payload_schema, - payload_encoding, - expected_operation, - expected_payload_schema, - )?; - decode_provider_session_payload(payload) -} - -pub fn create_provider_session_response( - request: &CultNetMessage, - status: CultMeshProviderOperationStatus, - payload_schema: impl Into, - payload: &T, - source_runtime_id: Option, -) -> Result -where - T: CultMeshProviderSessionPayload + Serialize, -{ - let CultNetMessage::OperationRequest { - message_id, - operation, - .. - } = request - else { - return Err(anyhow!("Expected a CultNet operation request")); - }; - require_text(message_id, "messageId")?; - require_text(operation, "operation")?; - let payload_schema = payload_schema.into(); - require_text(&payload_schema, "payloadSchema")?; - Ok(CultNetMessage::OperationResponse { - message_id: message_id.clone(), - service_id: CULTMESH_PROVIDER_SESSION_SERVICE_ID.to_string(), - operation: operation.clone(), - status: status.as_str().to_string(), - payload_schema, - payload_encoding: CULTMESH_PROVIDER_SESSION_PAYLOAD_ENCODING.to_string(), - payload: encode_provider_session_payload(payload)?, - diagnostics: Vec::new(), - source_runtime_id, - }) -} - -pub fn create_provider_session_error_response( - request: &CultNetMessage, - status: CultMeshProviderOperationStatus, - diagnostics: Vec, - source_runtime_id: Option, -) -> Result { - if status == CultMeshProviderOperationStatus::Ok { - return Err(anyhow!( - "Provider-session error responses require a non-ok status" - )); - } - if diagnostics.is_empty() { - return Err(anyhow!( - "Provider-session error responses require diagnostics" - )); - } - for diagnostic in &diagnostics { - require_text(diagnostic, "diagnostics[]")?; - } - let mut response = create_provider_session_response( - request, - status, - CULTMESH_PROVIDER_MUTATION_ACCEPTANCE_SCHEMA, - &CultMeshProviderErrorWire {}, - source_runtime_id, - )?; - let CultNetMessage::OperationResponse { - diagnostics: response_diagnostics, - .. - } = &mut response - else { - unreachable!("provider-session response helper returned a request") - }; - *response_diagnostics = diagnostics; - Ok(response) -} - -pub fn decode_provider_session_response( - response: &CultNetMessage, - expected_operation: &str, - expected_payload_schema: &str, -) -> Result<(CultMeshProviderOperationStatus, T)> -where - T: CultMeshProviderSessionPayload + DeserializeOwned, -{ - let CultNetMessage::OperationResponse { - service_id, - operation, - status, - payload_schema, - payload_encoding, - payload, - .. - } = response - else { - return Err(anyhow!("Expected a CultNet operation response")); - }; - require_envelope( - service_id, - operation, - payload_schema, - payload_encoding, - expected_operation, - expected_payload_schema, - )?; - Ok(( - CultMeshProviderOperationStatus::parse(status)?, - decode_provider_session_payload(payload)?, - )) -} - -fn validate_operation_schema(operation: &str, payload_schema: &str) -> Result<()> { - let expected = match operation { - CULTMESH_PROVIDER_REGISTER_OPERATION => CULTMESH_PROVIDER_REGISTRATION_SCHEMA, - CULTMESH_PROVIDER_RENEW_OPERATION => CULTMESH_PROVIDER_LEASE_RENEWAL_SCHEMA, - CULTMESH_PROVIDER_PUBLICATION_PUT_OPERATION => CULTMESH_PROVIDER_PUBLICATION_PUT_SCHEMA, - CULTMESH_PROVIDER_PUBLICATION_DELETE_OPERATION => { - CULTMESH_PROVIDER_PUBLICATION_DELETE_SCHEMA - } - CULTMESH_PROVIDER_RECEIPT_PUT_OPERATION => CULTMESH_PROVIDER_RECEIPT_PUT_SCHEMA, - CULTMESH_PROVIDER_WITHDRAW_OPERATION => CULTMESH_PROVIDER_WITHDRAWAL_SCHEMA, - _ => { - return Err(anyhow!( - "Unsupported provider-session operation {operation:?}" - )); - } - }; - if payload_schema != expected { - return Err(anyhow!( - "Provider-session operation {operation:?} requires payload schema {expected:?}" - )); - } - Ok(()) -} - -fn require_envelope( - service_id: &str, - operation: &str, - payload_schema: &str, - payload_encoding: &str, - expected_operation: &str, - expected_payload_schema: &str, -) -> Result<()> { - if service_id != CULTMESH_PROVIDER_SESSION_SERVICE_ID { - return Err(anyhow!( - "Unexpected provider-session service id {service_id:?}" - )); - } - if operation != expected_operation { - return Err(anyhow!( - "Unexpected provider-session operation {operation:?}" - )); - } - if payload_schema != expected_payload_schema { - return Err(anyhow!( - "Unexpected provider-session payload schema {payload_schema:?}" - )); - } - if payload_encoding != CULTMESH_PROVIDER_SESSION_PAYLOAD_ENCODING { - return Err(anyhow!( - "Unexpected provider-session payload encoding {payload_encoding:?}" - )); - } - Ok(()) -} - -fn validate_raw_document(document: &CultNetRawDocumentRecord) -> Result<()> { - require_text(&document.schema_id, "document.schemaId")?; - require_text(&document.record_key, "document.recordKey")?; - Ok(()) -} - -fn require_identity( - provider_id: &str, - service_instance_id: &str, - endpoint_id: &str, - verse_id: &str, -) -> Result<()> { - require_text(provider_id, "providerId")?; - require_text(service_instance_id, "serviceInstanceId")?; - require_text(endpoint_id, "endpointId")?; - require_text(verse_id, "verseId") -} - -fn require_text(value: &str, field: &str) -> Result<()> { - if value.trim().is_empty() { - return Err(anyhow!("Provider-session {field} must not be empty")); - } - Ok(()) -} - -fn require_optional_text(value: Option<&str>, field: &str) -> Result<()> { - if let Some(value) = value { - require_text(value, field)?; - } - Ok(()) -} - -fn require_positive(value: i32, field: &str) -> Result<()> { - if value <= 0 { - return Err(anyhow!( - "Provider-session {field} must be greater than zero" - )); - } - Ok(()) -} - -fn require_timestamp(value: &str, field: &str) -> Result> { - require_text(value, field)?; - DateTime::parse_from_rfc3339(value) - .map_err(|error| anyhow!("Provider-session {field} must be RFC3339: {error}")) -} diff --git a/vendor/cultnet-rs/src/replication.rs b/vendor/cultnet-rs/src/replication.rs deleted file mode 100644 index d02a141..0000000 --- a/vendor/cultnet-rs/src/replication.rs +++ /dev/null @@ -1,1014 +0,0 @@ -use anyhow::Context; -use anyhow::Result; -use anyhow::anyhow; -use cultcache_rs::CultCache; -use cultcache_rs::CultCacheEnvelope; -use cultcache_rs::DatabaseEntry; -use serde::Serialize; -use serde::de::DeserializeOwned; -use serde_json::Value; -use std::collections::BTreeMap; -use std::collections::BTreeSet; -use std::sync::Arc; -use std::sync::Mutex; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering; -use std::thread; -use std::thread::JoinHandle; -use std::time::Duration; - -use crate::CultNetDocumentMutationContract; -use crate::CultNetDocumentRecord; -use crate::CultNetMessage; -use crate::CultNetRawDocumentRecord; -use crate::CultNetRawPayloadEncoding; - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct CultNetDocumentPutOptions { - pub stored_at: Option, - pub source_runtime_id: Option, - pub source_agent_id: Option, - pub source_role: Option, - pub tags: Option>, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetDocumentBinding { - pub schema_id: String, - pub document_type: String, - pub mutation_contract: Option, - pub payload_schema_version: Option, -} - -impl CultNetDocumentBinding { - pub fn for_entry(payload_schema_version: impl Into>) -> Self { - Self { - document_type: T::TYPE.to_string(), - schema_id: T::TYPE.to_string(), - mutation_contract: None, - payload_schema_version: payload_schema_version.into(), - } - } - - pub fn for_entry_with_schema_id( - schema_id: impl Into, - payload_schema_version: impl Into>, - ) -> Self { - Self { - document_type: T::TYPE.to_string(), - schema_id: schema_id.into(), - mutation_contract: None, - payload_schema_version: payload_schema_version.into(), - } - } - - pub fn with_mutation_contract(mut self, contract: CultNetDocumentMutationContract) -> Self { - self.mutation_contract = Some(contract); - self - } -} - -#[derive(Clone, Debug, Default)] -pub struct CultNetDocumentRegistry { - bindings_by_type: BTreeMap, - bindings_by_schema_id: BTreeMap, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetReactiveDocumentOptions { - pub flush_delay: Duration, - pub detect_local_changes: bool, - pub replace_dirty_current_on_canonical_snapshot: bool, -} - -impl Default for CultNetReactiveDocumentOptions { - fn default() -> Self { - Self { - flush_delay: Duration::from_millis(16), - detect_local_changes: true, - replace_dirty_current_on_canonical_snapshot: false, - } - } -} - -#[derive(Clone, Debug, PartialEq)] -pub struct CultNetReactiveDocumentReconciliation { - pub canonical: T, - pub predicted: T, - pub delta: BTreeMap, - pub version: u64, - pub received_at: String, -} - -struct CultNetReactiveDocumentState { - dirty: bool, - flushing: bool, - flush_queued: bool, - last_clean_payload: Vec, - reconciliation: Option>, - reconciliation_version: u64, - last_error: Option, -} - -pub struct CultNetReactiveDocument -where - T: DatabaseEntry + Serialize + DeserializeOwned, -{ - registry: CultNetDocumentRegistry, - cache: Arc>, - record_key: String, - current: Arc>, - state: Arc>>, - options: CultNetReactiveDocumentOptions, - disposed: Arc, - detect_worker: Option>, -} - -impl CultNetDocumentRegistry { - pub fn new() -> Self { - Self::default() - } - - pub fn register(&mut self, binding: CultNetDocumentBinding) -> &mut Self { - self.bindings_by_schema_id - .insert(binding.schema_id.clone(), binding.clone()); - self.bindings_by_type - .insert(binding.document_type.clone(), binding); - self - } - - pub fn binding(&self, document_type: &str) -> Option<&CultNetDocumentBinding> { - self.bindings_by_type.get(document_type) - } - - pub fn binding_by_schema_id(&self, schema_id: &str) -> Option<&CultNetDocumentBinding> { - self.bindings_by_schema_id.get(schema_id) - } - - pub fn mutation_contracts(&self) -> Vec { - self.bindings_by_type - .values() - .filter_map(|binding| binding.mutation_contract.clone()) - .collect() - } - - pub fn create_document_put_message( - &self, - message_id: impl Into, - record_key: impl Into, - value: &T, - options: CultNetDocumentPutOptions, - ) -> Result - where - T: DatabaseEntry + Serialize + DeserializeOwned, - { - let binding = self.require_binding(T::TYPE)?; - let payload = serde_json::to_value(round_trip_typed(value)?)?; - Ok(CultNetMessage::DocumentPut { - message_id: message_id.into(), - document: CultNetDocumentRecord { - schema_id: binding.schema_id.clone(), - record_key: record_key.into(), - stored_at: options.stored_at.unwrap_or_else(now_utc_second), - payload, - source_runtime_id: options.source_runtime_id, - source_agent_id: options.source_agent_id, - source_role: options.source_role, - tags: options.tags, - }, - }) - } - - pub fn create_document_delete_message( - &self, - message_id: impl Into, - schema_id: impl Into, - record_key: impl Into, - ) -> CultNetMessage { - CultNetMessage::DocumentDelete { - message_id: message_id.into(), - schema_id: schema_id.into(), - record_key: record_key.into(), - } - } - - pub fn create_raw_document_put_message_from_envelope( - &self, - message_id: impl Into, - envelope: &CultCacheEnvelope, - ) -> Result { - Ok(CultNetMessage::DocumentPutRaw { - message_id: message_id.into(), - document: self.raw_document_record_from_envelope(envelope)?, - }) - } - - pub fn create_raw_document_put_message( - &self, - message_id: impl Into, - record_key: impl Into, - value: &T, - options: CultNetDocumentPutOptions, - ) -> Result - where - T: DatabaseEntry + Serialize, - { - let binding = self.require_binding(T::TYPE)?; - Ok(CultNetMessage::DocumentPutRaw { - message_id: message_id.into(), - document: CultNetRawDocumentRecord { - schema_id: binding.schema_id.clone(), - record_key: record_key.into(), - stored_at: options.stored_at.unwrap_or_else(now_utc_second), - payload_encoding: CultNetRawPayloadEncoding::Messagepack, - payload: rmp_serde::to_vec(value)?, - source_runtime_id: options.source_runtime_id, - source_agent_id: options.source_agent_id, - source_role: options.source_role, - tags: options.tags, - }, - }) - } - - pub fn create_snapshot_response( - &self, - cache: &CultCache, - message_id: impl Into, - schema_ids: Option<&[String]>, - record_keys: Option<&[String]>, - ) -> Result { - let requested_schema_ids = schema_ids.map(|items| items.iter().collect::>()); - let requested_record_keys = record_keys.map(|items| items.iter().collect::>()); - let mut documents = Vec::new(); - for envelope in cache.snapshot() { - let binding = self.require_binding(&envelope.r#type)?; - if requested_schema_ids - .as_ref() - .is_some_and(|ids| !ids.contains(&binding.schema_id)) - { - continue; - } - if requested_record_keys - .as_ref() - .is_some_and(|keys| !keys.contains(&envelope.key)) - { - continue; - } - documents.push(self.document_record_from_envelope(&envelope)?); - } - Ok(CultNetMessage::SnapshotResponse { - message_id: message_id.into(), - documents, - }) - } - - pub fn create_raw_snapshot_response( - &self, - cache: &CultCache, - message_id: impl Into, - schema_ids: Option<&[String]>, - record_keys: Option<&[String]>, - ) -> Result { - let requested_schema_ids = schema_ids.map(|items| items.iter().collect::>()); - let requested_record_keys = record_keys.map(|items| items.iter().collect::>()); - let mut documents = Vec::new(); - for envelope in cache.snapshot() { - let binding = self.require_binding(&envelope.r#type)?; - if requested_schema_ids - .as_ref() - .is_some_and(|ids| !ids.contains(&binding.schema_id)) - { - continue; - } - if requested_record_keys - .as_ref() - .is_some_and(|keys| !keys.contains(&envelope.key)) - { - continue; - } - documents.push(self.raw_document_record_from_envelope(&envelope)?); - } - Ok(CultNetMessage::SnapshotResponseRaw { - message_id: message_id.into(), - documents, - }) - } - - pub fn apply_document_put_message( - &self, - cache: &mut CultCache, - message: &CultNetMessage, - ) -> Result - where - T: DatabaseEntry + Serialize + DeserializeOwned, - { - let CultNetMessage::DocumentPut { document, .. } = message else { - return Err(anyhow!("expected cultnet.document_put.v0")); - }; - let binding = self.require_binding(T::TYPE)?; - if document.schema_id != binding.schema_id { - return Err(anyhow!( - "schema id {:?} does not match registered Rust type {:?} schema {:?}", - document.schema_id, - T::TYPE, - binding.schema_id - )); - } - let value: T = serde_json::from_value(document.payload.clone()).with_context(|| { - format!( - "failed to decode CultNet payload schema {:?} as {}", - binding.schema_id, - T::SCHEMA_NAME - ) - })?; - cache.put(&document.record_key, &value) - } - - pub fn apply_document_delete_message( - &self, - cache: &mut CultCache, - message: &CultNetMessage, - ) -> Result - where - T: DatabaseEntry, - { - let CultNetMessage::DocumentDelete { - schema_id, - record_key, - .. - } = message - else { - return Err(anyhow!("expected cultnet.document_delete.v0")); - }; - let binding = self.require_binding(T::TYPE)?; - if schema_id != &binding.schema_id { - return Err(anyhow!( - "schema id {:?} does not match registered Rust type {:?} schema {:?}", - schema_id, - T::TYPE, - binding.schema_id - )); - } - cache.delete::(record_key) - } - - pub fn apply_raw_document_put_message( - &self, - cache: &mut CultCache, - message: &CultNetMessage, - ) -> Result - where - T: DatabaseEntry + Serialize + DeserializeOwned, - { - let CultNetMessage::DocumentPutRaw { document, .. } = message else { - return Err(anyhow!("expected cultnet.document_put_raw.v0")); - }; - let binding = self.require_binding(T::TYPE)?; - if document.schema_id != binding.schema_id { - return Err(anyhow!( - "schema id {:?} does not match registered Rust type {:?} schema {:?}", - document.schema_id, - T::TYPE, - binding.schema_id - )); - } - cache.put_envelope::(CultCacheEnvelope { - key: document.record_key.clone(), - r#type: binding.document_type.clone(), - payload: document.payload.clone(), - stored_at: document.stored_at.clone(), - schema_id: Some(binding.schema_id.clone()), - }) - } - - pub fn apply_snapshot_response( - &self, - cache: &mut CultCache, - response: &CultNetMessage, - ) -> Result> - where - T: DatabaseEntry + Serialize + DeserializeOwned, - { - let CultNetMessage::SnapshotResponse { documents, .. } = response else { - return Err(anyhow!("expected cultnet.snapshot_response.v0")); - }; - let mut applied = Vec::new(); - let binding = self.require_binding(T::TYPE)?; - for document in documents { - if document.schema_id != binding.schema_id { - continue; - } - applied.push(self.apply_document_put_message::( - cache, - &CultNetMessage::DocumentPut { - message_id: "snapshot-apply".to_string(), - document: document.clone(), - }, - )?); - } - Ok(applied) - } - - pub fn sync_document_from_snapshot_response( - &self, - cache: &mut CultCache, - response: &CultNetMessage, - record_key: &str, - ) -> Result - where - T: DatabaseEntry + Serialize + DeserializeOwned, - { - let CultNetMessage::SnapshotResponse { documents, .. } = response else { - return Err(anyhow!("expected cultnet.snapshot_response.v0")); - }; - let binding = self.require_binding(T::TYPE)?; - let document = documents - .iter() - .find(|document| { - document.schema_id == binding.schema_id && document.record_key == record_key - }) - .ok_or_else(|| { - anyhow!( - "No CultNet snapshot document for schema {:?} and record key {:?}", - binding.schema_id, - record_key - ) - })?; - self.apply_document_put_message::( - cache, - &CultNetMessage::DocumentPut { - message_id: "snapshot-sync".to_string(), - document: document.clone(), - }, - ) - } - - pub fn apply_raw_snapshot_response( - &self, - cache: &mut CultCache, - response: &CultNetMessage, - ) -> Result> - where - T: DatabaseEntry + Serialize + DeserializeOwned, - { - let CultNetMessage::SnapshotResponseRaw { documents, .. } = response else { - return Err(anyhow!("expected cultnet.snapshot_response_raw.v0")); - }; - let mut applied = Vec::new(); - let binding = self.require_binding(T::TYPE)?; - for document in documents { - if document.schema_id != binding.schema_id { - continue; - } - applied.push(self.apply_raw_document_put_message::( - cache, - &CultNetMessage::DocumentPutRaw { - message_id: "snapshot-apply-raw".to_string(), - document: document.clone(), - }, - )?); - } - Ok(applied) - } - - pub fn sync_raw_document_from_snapshot_response( - &self, - cache: &mut CultCache, - response: &CultNetMessage, - record_key: &str, - ) -> Result - where - T: DatabaseEntry + Serialize + DeserializeOwned, - { - let CultNetMessage::SnapshotResponseRaw { documents, .. } = response else { - return Err(anyhow!("expected cultnet.snapshot_response_raw.v0")); - }; - let binding = self.require_binding(T::TYPE)?; - let document = documents - .iter() - .find(|document| { - document.schema_id == binding.schema_id && document.record_key == record_key - }) - .ok_or_else(|| { - anyhow!( - "No raw CultNet snapshot document for schema {:?} and record key {:?}", - binding.schema_id, - record_key - ) - })?; - self.apply_raw_document_put_message::( - cache, - &CultNetMessage::DocumentPutRaw { - message_id: "snapshot-sync-raw".to_string(), - document: document.clone(), - }, - ) - } - - pub fn reactive_document( - &self, - cache: Arc>, - record_key: impl Into, - options: CultNetReactiveDocumentOptions, - ) -> Result> - where - T: DatabaseEntry + Serialize + DeserializeOwned, - { - CultNetReactiveDocument::new(self.clone(), cache, record_key, options) - } - - fn document_record_from_envelope( - &self, - envelope: &CultCacheEnvelope, - ) -> Result> { - let binding = self.require_binding(&envelope.r#type)?; - let payload: Value = rmp_serde::from_slice(&envelope.payload).with_context(|| { - format!( - "failed to decode CultCache envelope {:?} at {:?} as generic CultNet payload", - envelope.r#type, envelope.key - ) - })?; - Ok(CultNetDocumentRecord { - schema_id: binding.schema_id.clone(), - record_key: envelope.key.clone(), - stored_at: envelope.stored_at.clone(), - payload, - source_runtime_id: None, - source_agent_id: None, - source_role: None, - tags: None, - }) - } - - fn raw_document_record_from_envelope( - &self, - envelope: &CultCacheEnvelope, - ) -> Result { - let binding = self.require_binding(&envelope.r#type)?; - Ok(CultNetRawDocumentRecord { - schema_id: binding.schema_id.clone(), - record_key: envelope.key.clone(), - stored_at: envelope.stored_at.clone(), - payload_encoding: CultNetRawPayloadEncoding::Messagepack, - payload: envelope.payload.clone(), - source_runtime_id: None, - source_agent_id: None, - source_role: None, - tags: None, - }) - } - - fn require_binding(&self, document_type: &str) -> Result<&CultNetDocumentBinding> { - self.binding(document_type).ok_or_else(|| { - anyhow!("No CultNet document binding is registered for {document_type:?}") - }) - } -} - -impl CultNetReactiveDocument -where - T: DatabaseEntry + Serialize + DeserializeOwned, -{ - pub fn new( - registry: CultNetDocumentRegistry, - cache: Arc>, - record_key: impl Into, - options: CultNetReactiveDocumentOptions, - ) -> Result { - let record_key = record_key.into(); - if record_key.trim().is_empty() { - return Err(anyhow!("record_key must be non-empty")); - } - registry.require_binding(T::TYPE)?; - let current = { - let cache = cache - .lock() - .map_err(|_| anyhow!("CultCache mutex poisoned"))?; - cache.get_required::(&record_key)? - }; - let last_clean_payload = serialize_typed(¤t)?; - let current = Arc::new(Mutex::new(current)); - let state = Arc::new(Mutex::new(CultNetReactiveDocumentState { - dirty: false, - flushing: false, - flush_queued: false, - last_clean_payload, - reconciliation: None, - reconciliation_version: 0, - last_error: None, - })); - let disposed = Arc::new(AtomicBool::new(false)); - let detect_worker = if options.detect_local_changes { - Some(start_reactive_detection_worker::( - Arc::clone(&cache), - Arc::clone(¤t), - Arc::clone(&state), - Arc::clone(&disposed), - options.flush_delay, - record_key.clone(), - )) - } else { - None - }; - Ok(Self { - registry, - cache, - record_key, - current, - state, - options, - disposed, - detect_worker, - }) - } - - pub fn current(&self) -> Arc> { - Arc::clone(&self.current) - } - - pub fn record_key(&self) -> &str { - &self.record_key - } - - pub fn is_dirty(&self) -> bool { - self.state.lock().map(|state| state.dirty).unwrap_or(false) - } - - pub fn reconciliation(&self) -> Option> { - self.state - .lock() - .ok() - .and_then(|state| state.reconciliation.clone()) - } - - pub fn last_error(&self) -> Option { - self.state - .lock() - .ok() - .and_then(|state| state.last_error.clone()) - } - - pub fn update(&self, update: impl FnOnce(&mut T)) -> Result { - let predicted = { - let mut current = self - .current - .lock() - .map_err(|_| anyhow!("reactive document mutex poisoned"))?; - update(&mut current); - current.clone() - }; - { - let mut state = self - .state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - state.dirty = true; - } - if self.options.flush_delay.is_zero() { - self.flush()?; - } - Ok(predicted) - } - - pub fn set_current(&self, value: T) -> Result { - { - let mut current = self - .current - .lock() - .map_err(|_| anyhow!("reactive document mutex poisoned"))?; - *current = value.clone(); - } - { - let mut state = self - .state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - state.dirty = true; - } - if self.options.flush_delay.is_zero() { - self.flush()?; - } - Ok(value) - } - - pub fn mark_dirty(&self) -> Result<()> { - let mut state = self - .state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - state.dirty = true; - Ok(()) - } - - pub fn refresh(&self) -> Result { - let canonical = { - let cache = self - .cache - .lock() - .map_err(|_| anyhow!("CultCache mutex poisoned"))?; - cache.get_required::(&self.record_key)? - }; - { - let mut current = self - .current - .lock() - .map_err(|_| anyhow!("reactive document mutex poisoned"))?; - *current = canonical.clone(); - } - { - let mut state = self - .state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - state.last_clean_payload = serialize_typed(&canonical)?; - state.dirty = false; - state.flush_queued = false; - state.reconciliation = None; - } - Ok(canonical) - } - - pub fn flush(&self) -> Result<()> { - flush_reactive_document::(&self.cache, &self.current, &self.state, &self.record_key) - } - - pub fn apply_document_put_message(&self, message: &CultNetMessage) -> Result { - let canonical = { - let mut cache = self - .cache - .lock() - .map_err(|_| anyhow!("CultCache mutex poisoned"))?; - self.registry - .apply_document_put_message::(&mut cache, message)? - }; - self.apply_canonical_value(canonical) - } - - pub fn apply_raw_document_put_message(&self, message: &CultNetMessage) -> Result { - let canonical = { - let mut cache = self - .cache - .lock() - .map_err(|_| anyhow!("CultCache mutex poisoned"))?; - self.registry - .apply_raw_document_put_message::(&mut cache, message)? - }; - self.apply_canonical_value(canonical) - } - - pub fn clear_reconciliation(&self) -> Result<()> { - let mut state = self - .state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - state.reconciliation = None; - Ok(()) - } - - fn apply_canonical_value(&self, canonical: T) -> Result { - let predicted = self - .current - .lock() - .map_err(|_| anyhow!("reactive document mutex poisoned"))? - .clone(); - let mut state = self - .state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - if state.dirty || state.flushing { - let delta = create_reconciliation_delta(&predicted, &canonical)?; - if delta.is_empty() { - state.reconciliation = None; - } else { - state.reconciliation_version += 1; - state.reconciliation = Some(CultNetReactiveDocumentReconciliation { - canonical: canonical.clone(), - predicted, - delta, - version: state.reconciliation_version, - received_at: now_utc_second(), - }); - } - if !self.options.replace_dirty_current_on_canonical_snapshot { - return Ok(canonical); - } - } - drop(state); - { - let mut current = self - .current - .lock() - .map_err(|_| anyhow!("reactive document mutex poisoned"))?; - *current = canonical.clone(); - } - let mut state = self - .state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - state.last_clean_payload = serialize_typed(&canonical)?; - state.reconciliation = None; - Ok(canonical) - } -} - -impl Drop for CultNetReactiveDocument -where - T: DatabaseEntry + Serialize + DeserializeOwned, -{ - fn drop(&mut self) { - self.disposed.store(true, Ordering::SeqCst); - let _ = self.detect_worker.take(); - } -} - -fn start_reactive_detection_worker( - cache: Arc>, - current: Arc>, - state: Arc>>, - disposed: Arc, - flush_delay: Duration, - record_key: String, -) -> JoinHandle<()> -where - T: DatabaseEntry + Serialize + DeserializeOwned, -{ - thread::spawn(move || { - let delay = if flush_delay.is_zero() { - Duration::from_millis(1) - } else { - flush_delay - }; - while !disposed.load(Ordering::SeqCst) { - thread::sleep(delay); - match detect_reactive_document_change::(¤t, &state).and_then(|dirty| { - if dirty { - flush_reactive_document::(&cache, ¤t, &state, &record_key)?; - } - Ok(()) - }) { - Ok(()) => {} - Err(error) => { - if let Ok(mut state) = state.lock() { - state.last_error = Some(error.to_string()); - } - } - } - } - }) -} - -fn detect_reactive_document_change( - current: &Arc>, - state: &Arc>>, -) -> Result -where - T: DatabaseEntry + Serialize + DeserializeOwned, -{ - let payload = { - let current = current - .lock() - .map_err(|_| anyhow!("reactive document mutex poisoned"))?; - serialize_typed(&*current)? - }; - let mut state = state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - if !state.dirty && !state.flushing && payload != state.last_clean_payload { - state.dirty = true; - } - Ok(state.dirty && !state.flushing) -} - -fn flush_reactive_document( - cache: &Arc>, - current: &Arc>, - state: &Arc>>, - record_key: &str, -) -> Result<()> -where - T: DatabaseEntry + Serialize + DeserializeOwned, -{ - let predicted = { - let mut state_guard = state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - if !state_guard.dirty { - drop(state_guard); - if !detect_reactive_document_change::(current, state)? { - return Ok(()); - } - state_guard = state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - } - if state_guard.flushing { - state_guard.flush_queued = true; - return Ok(()); - } - state_guard.flushing = true; - state_guard.dirty = false; - current - .lock() - .map_err(|_| anyhow!("reactive document mutex poisoned"))? - .clone() - }; - let payload = serialize_typed(&predicted)?; - { - let mut cache = cache - .lock() - .map_err(|_| anyhow!("CultCache mutex poisoned"))?; - cache.put::(record_key.to_string(), &predicted)?; - } - let should_flush_again = { - let mut state = state - .lock() - .map_err(|_| anyhow!("reactive document state mutex poisoned"))?; - state.flushing = false; - state.last_clean_payload = payload; - state.last_error = None; - state.reconciliation = None; - let changed_after_flush = { - let current = current - .lock() - .map_err(|_| anyhow!("reactive document mutex poisoned"))?; - serialize_typed(&*current)? != state.last_clean_payload - }; - if changed_after_flush { - state.dirty = true; - } - let should_flush_again = state.flush_queued || state.dirty; - state.flush_queued = false; - should_flush_again - }; - if should_flush_again { - flush_reactive_document::(cache, current, state, record_key)?; - } - Ok(()) -} - -fn serialize_typed(value: &T) -> Result> -where - T: Serialize, -{ - Ok(rmp_serde::to_vec(value)?) -} - -fn create_reconciliation_delta(predicted: &T, canonical: &T) -> Result> -where - T: Serialize, -{ - let predicted = serde_json::to_value(predicted)?; - let canonical = serde_json::to_value(canonical)?; - let mut delta = BTreeMap::new(); - match (predicted, canonical) { - (Value::Object(predicted), Value::Object(canonical)) => { - for (key, predicted_value) in predicted { - let canonical_value = canonical.get(&key).cloned().unwrap_or(Value::Null); - if predicted_value == canonical_value { - continue; - } - delta.insert( - key, - numeric_delta(&predicted_value, &canonical_value).unwrap_or(predicted_value), - ); - } - } - (Value::Array(predicted), Value::Array(canonical)) => { - for (index, predicted_value) in predicted.into_iter().enumerate() { - let canonical_value = canonical.get(index).cloned().unwrap_or(Value::Null); - if predicted_value == canonical_value { - continue; - } - delta.insert( - index.to_string(), - numeric_delta(&predicted_value, &canonical_value).unwrap_or(predicted_value), - ); - } - } - (predicted, canonical) if predicted != canonical => { - delta.insert( - "value".to_string(), - numeric_delta(&predicted, &canonical).unwrap_or(predicted), - ); - } - _ => {} - } - Ok(delta) -} - -fn numeric_delta(predicted: &Value, canonical: &Value) -> Option { - let predicted = predicted.as_f64()?; - let canonical = canonical.as_f64()?; - serde_json::Number::from_f64(predicted - canonical).map(Value::Number) -} - -fn round_trip_typed(value: &T) -> Result -where - T: Serialize + DeserializeOwned, -{ - Ok(rmp_serde::from_slice(&rmp_serde::to_vec(value)?)?) -} - -fn now_utc_second() -> String { - chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true) -} diff --git a/vendor/cultnet-rs/src/rudp.rs b/vendor/cultnet-rs/src/rudp.rs deleted file mode 100644 index f487d14..0000000 --- a/vendor/cultnet-rs/src/rudp.rs +++ /dev/null @@ -1,2038 +0,0 @@ -use anyhow::Result; -use anyhow::anyhow; -use std::collections::BTreeMap; -use std::collections::BTreeSet; -use std::collections::VecDeque; -use std::io::ErrorKind; -use std::net::SocketAddr; -use std::net::UdpSocket; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; - -use crate::CultNetMessage; -use crate::CultNetReconnectController; -use crate::CultNetReconnectDecision; -use crate::CultNetReconnectPolicy; -use crate::CultNetTransportChannel; -use crate::CultNetTransportDelivery; -use crate::CultNetTransportDescriptor; -use crate::CultNetTransportFrame; -use crate::CultNetTransportOrdering; -use crate::CultNetTransportProfile; -use crate::CultNetTransportProtocol; -use crate::CultNetTransportStats; -use crate::CultNetWireContract; -use crate::create_reconnect_policy; -use crate::decode_cultnet_message_from_slice; -use crate::encode_cultnet_message_to_vec; - -const RUDP_MAGIC: [u8; 4] = [0x43, 0x4e, 0x52, 0x30]; -const RUDP_VERSION: u8 = 0; -const RUDP_FIXED_HEADER_BYTES: usize = 36; -pub const CULTNET_RUDP_DEFAULT_MAX_PAYLOAD_BYTES: usize = 16 * 1024 * 1024; -pub const CULTNET_RUDP_RELIABLE_SEND_WINDOW_PACKETS: usize = 32; -const RUDP_RECEIVED_SEQUENCE_WINDOW: usize = 4_096; - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CultNetRudpPacketType { - Connect, - Accept, - Data, - Ack, - Ping, - Pong, - Disconnect, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetRudpPacket { - pub packet_type: CultNetRudpPacketType, - pub connection_id: u32, - pub sequence: u32, - pub ack: u32, - pub ack_mask: u32, - pub channel_id: String, - pub reliable: bool, - pub ordered: bool, - pub sequenced: bool, - pub fragment_id: u16, - pub fragment_index: u16, - pub fragment_count: u16, - pub payload: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetRudpDeliveredFrame { - pub channel_id: String, - pub payload: Vec, - pub sequence: u32, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetRudpReceiveResult { - pub delivered: Vec, - pub ready_to_send: Vec, - pub reply: Option, - pub pong: bool, - pub pong_payload: Vec, - pub disconnected: bool, - pub disconnect_reason: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetRudpSessionOptions { - pub connection_id: u32, - pub initial_sequence: u32, - pub resend_delay_ms: u64, - pub max_pending_reliable_packets: Option, -} - -impl Default for CultNetRudpSessionOptions { - fn default() -> Self { - Self { - connection_id: 0, - initial_sequence: 1, - resend_delay_ms: 250, - max_pending_reliable_packets: None, - } - } -} - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct CultNetRudpSendOptions { - pub reliable: bool, - pub ordered: bool, - pub sequenced: bool, - pub now_ms: u64, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -struct PendingReliablePacket { - packet: CultNetRudpPacket, - last_sent_at_ms: u64, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -struct PendingOrderedFrame { - frame: CultNetRudpDeliveredFrame, - next_sequence: u32, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -struct FragmentBuffer { - channel_id: String, - ordered: bool, - fragment_count: u16, - payloads: BTreeMap>, - sequences: BTreeMap, -} - -pub struct CultNetRudpSession { - connection_id: u32, - resend_delay_ms: u64, - max_pending_reliable_packets: Option, - max_payload_bytes: Option, - max_pending_fragment_sets: usize, - initial_sequence: u32, - next_sequence: u32, - next_sequenced_by_channel: BTreeMap, - next_fragment_id: u16, - connected: bool, - last_received_at_ms: Option, - highest_received_sequence: Option, - received_sequences: BTreeSet, - latest_sequenced_by_channel: BTreeMap, - pending_reliable: BTreeMap, - queued_reliable: VecDeque, - ordered_next_sequence_by_channel: BTreeMap, - ordered_buffers: BTreeMap>, - fragment_buffers: BTreeMap<(String, u16), FragmentBuffer>, -} - -impl CultNetRudpSession { - pub fn new(options: CultNetRudpSessionOptions) -> Self { - Self { - connection_id: options.connection_id, - resend_delay_ms: options.resend_delay_ms, - max_pending_reliable_packets: options.max_pending_reliable_packets, - max_payload_bytes: None, - max_pending_fragment_sets: 64, - initial_sequence: options.initial_sequence, - next_sequence: options.initial_sequence, - next_sequenced_by_channel: BTreeMap::new(), - next_fragment_id: 1, - connected: false, - last_received_at_ms: None, - highest_received_sequence: None, - received_sequences: BTreeSet::new(), - latest_sequenced_by_channel: BTreeMap::new(), - pending_reliable: BTreeMap::new(), - queued_reliable: VecDeque::new(), - ordered_next_sequence_by_channel: BTreeMap::new(), - ordered_buffers: BTreeMap::new(), - fragment_buffers: BTreeMap::new(), - } - } - - pub fn connection_id(&self) -> u32 { - self.connection_id - } - - pub fn resend_delay_ms(&self) -> u64 { - self.resend_delay_ms - } - - pub fn connected(&self) -> bool { - self.connected - } - - pub fn pending_reliable_sequences(&self) -> Vec { - self.pending_reliable.keys().copied().collect() - } - - pub fn queued_reliable_packet_count(&self) -> usize { - self.queued_reliable.len() - } - - pub fn outstanding_reliable_packet_count(&self) -> usize { - self.pending_reliable.len() + self.queued_reliable.len() - } - - fn pending_accept_for_resend(&mut self, now_ms: u64) -> Option { - let pending = self - .pending_reliable - .values_mut() - .find(|pending| pending.packet.packet_type == CultNetRudpPacketType::Accept)?; - pending.last_sent_at_ms = now_ms; - Some(pending.packet.clone()) - } - - pub fn last_received_at_ms(&self) -> Option { - self.last_received_at_ms - } - - pub fn set_max_payload_bytes(&mut self, max_payload_bytes: Option) { - self.max_payload_bytes = max_payload_bytes; - } - - pub fn set_max_pending_fragment_sets( - &mut self, - max_pending_fragment_sets: usize, - ) -> Result<()> { - if max_pending_fragment_sets == 0 { - return Err(anyhow!( - "RUDP max_pending_fragment_sets must be greater than zero" - )); - } - self.max_pending_fragment_sets = max_pending_fragment_sets; - Ok(()) - } - - pub fn reset_peer_state(&mut self) { - self.next_sequence = self.initial_sequence; - self.next_sequenced_by_channel.clear(); - self.next_fragment_id = 1; - self.connected = false; - self.last_received_at_ms = None; - self.highest_received_sequence = None; - self.received_sequences.clear(); - self.latest_sequenced_by_channel.clear(); - self.pending_reliable.clear(); - self.queued_reliable.clear(); - self.ordered_next_sequence_by_channel.clear(); - self.ordered_buffers.clear(); - self.fragment_buffers.clear(); - } - - pub fn create_connect(&mut self, now_ms: u64, payload: Vec) -> Result { - self.ensure_reliable_capacity(1)?; - let packet = self.create_packet( - CultNetRudpPacketType::Connect, - "control", - payload, - true, - true, - false, - ); - self.track_reliable(packet.clone(), now_ms); - Ok(packet) - } - - pub fn accept_connect( - &mut self, - packet: &CultNetRudpPacket, - now_ms: u64, - payload: Vec, - ) -> Result { - self.require_connection(packet)?; - if packet.packet_type != CultNetRudpPacketType::Connect { - return Err(anyhow!( - "Expected RUDP connect packet, got {:?}", - packet.packet_type - )); - } - - self.ensure_reliable_capacity(1)?; - self.remember_received(packet.sequence); - self.last_received_at_ms = Some(now_ms); - self.connected = true; - let response = self.create_packet( - CultNetRudpPacketType::Accept, - "control", - payload, - true, - true, - false, - ); - self.track_reliable(response.clone(), now_ms); - Ok(response) - } - - pub fn send( - &mut self, - channel_id: &str, - payload: Vec, - options: CultNetRudpSendOptions, - ) -> Result { - if options.reliable - && self.pending_reliable.len() >= CULTNET_RUDP_RELIABLE_SEND_WINDOW_PACKETS - { - return Err(anyhow!( - "RUDP reliable send window is full; receive acknowledgements before sending" - )); - } - self.send_many(channel_id, payload, options, None)? - .into_iter() - .next() - .ok_or_else(|| anyhow!("RUDP send produced no packets")) - } - - pub fn send_many( - &mut self, - channel_id: &str, - payload: Vec, - options: CultNetRudpSendOptions, - max_fragment_bytes: Option, - ) -> Result> { - if !self.connected { - return Err(anyhow!( - "Cannot send RUDP data before the session is connected" - )); - } - if options.ordered && !options.reliable { - return Err(anyhow!("RUDP ordered delivery requires reliability")); - } - self.require_payload_size(payload.len())?; - - if let Some(max_fragment_bytes) = max_fragment_bytes { - if max_fragment_bytes == 0 { - return Err(anyhow!("RUDP max_fragment_bytes must be greater than zero")); - } - if payload.len() > max_fragment_bytes { - let fragment_count = payload.len().div_ceil(max_fragment_bytes); - if fragment_count > u16::MAX as usize { - return Err(anyhow!("RUDP payload requires more than 65535 fragments")); - } - self.ensure_reliable_capacity(if options.reliable { fragment_count } else { 0 })?; - let fragment_id = self.allocate_fragment_id(); - let mut packets = Vec::new(); - for index in 0..fragment_count { - let start = index * max_fragment_bytes; - let end = (start + max_fragment_bytes).min(payload.len()); - let packet = self.create_packet_with_fragments( - CultNetRudpPacketType::Data, - channel_id, - payload[start..end].to_vec(), - options.reliable, - options.ordered, - options.sequenced, - fragment_id, - index as u16, - fragment_count as u16, - ); - packets.push(packet); - } - return Ok(if options.reliable { - self.admit_reliable_packets(packets, options.now_ms) - } else { - packets - }); - } - } - - self.ensure_reliable_capacity(if options.reliable { 1 } else { 0 })?; - let packet = self.create_packet( - CultNetRudpPacketType::Data, - channel_id, - payload, - options.reliable, - options.ordered, - options.sequenced, - ); - if packet.reliable { - return Ok(self.admit_reliable_packets(vec![packet], options.now_ms)); - } - Ok(vec![packet]) - } - - pub fn receive( - &mut self, - packet: &CultNetRudpPacket, - now_ms: u64, - ) -> Result { - self.require_connection(packet)?; - self.apply_acknowledgements(packet); - let ready_to_send = self.promote_queued_reliable(now_ms); - self.last_received_at_ms = Some(now_ms); - let expected_sequence_if_uninitialized = self - .highest_received_sequence - .map(|sequence| sequence + 1) - .unwrap_or(packet.sequence); - - if packet.packet_type == CultNetRudpPacketType::Accept { - self.remember_received(packet.sequence); - self.connected = true; - return Ok(CultNetRudpReceiveResult { - delivered: Vec::new(), - ready_to_send, - reply: None, - pong: false, - pong_payload: Vec::new(), - disconnected: false, - disconnect_reason: Vec::new(), - }); - } - - if packet.packet_type == CultNetRudpPacketType::Ping { - return Ok(CultNetRudpReceiveResult { - delivered: Vec::new(), - ready_to_send, - reply: Some(self.create_packet( - CultNetRudpPacketType::Pong, - "control", - packet.payload.clone(), - false, - false, - false, - )), - pong: false, - pong_payload: Vec::new(), - disconnected: false, - disconnect_reason: Vec::new(), - }); - } - - if packet.packet_type == CultNetRudpPacketType::Ack - || packet.packet_type == CultNetRudpPacketType::Pong - { - return Ok(CultNetRudpReceiveResult { - delivered: Vec::new(), - ready_to_send, - reply: None, - pong: packet.packet_type == CultNetRudpPacketType::Pong, - pong_payload: if packet.packet_type == CultNetRudpPacketType::Pong { - packet.payload.clone() - } else { - Vec::new() - }, - disconnected: false, - disconnect_reason: Vec::new(), - }); - } - - if packet.packet_type == CultNetRudpPacketType::Disconnect { - self.connected = false; - return Ok(CultNetRudpReceiveResult { - delivered: Vec::new(), - ready_to_send, - reply: None, - pong: false, - pong_payload: Vec::new(), - disconnected: true, - disconnect_reason: packet.payload.clone(), - }); - } - - if packet.packet_type != CultNetRudpPacketType::Data { - return Ok(CultNetRudpReceiveResult { - delivered: Vec::new(), - ready_to_send, - reply: None, - pong: false, - pong_payload: Vec::new(), - disconnected: false, - disconnect_reason: Vec::new(), - }); - } - - let duplicate = packet.reliable - && (self.received_sequences.contains(&packet.sequence) - || self.highest_received_sequence.is_some_and(|highest| { - packet.sequence < highest - && highest - packet.sequence >= RUDP_RECEIVED_SEQUENCE_WINDOW as u32 - })); - if packet.reliable { - self.remember_received(packet.sequence); - } - if duplicate { - return Ok(CultNetRudpReceiveResult { - delivered: Vec::new(), - ready_to_send, - reply: None, - pong: false, - pong_payload: Vec::new(), - disconnected: false, - disconnect_reason: Vec::new(), - }); - } - - let Some((frame, ordered, next_sequence)) = self.reassemble(packet)? else { - return Ok(CultNetRudpReceiveResult { - delivered: Vec::new(), - ready_to_send, - reply: None, - pong: false, - pong_payload: Vec::new(), - disconnected: false, - disconnect_reason: Vec::new(), - }); - }; - let delivered = if ordered { - self.deliver_ordered(frame, next_sequence, expected_sequence_if_uninitialized) - } else if packet.sequenced { - let latest = self - .latest_sequenced_by_channel - .get(&frame.channel_id) - .copied(); - let newest_sequence = next_sequence.saturating_sub(1); - if latest.is_some_and(|latest| newest_sequence <= latest) { - Vec::new() - } else { - self.latest_sequenced_by_channel - .insert(frame.channel_id.clone(), newest_sequence); - vec![frame] - } - } else { - vec![frame] - }; - Ok(CultNetRudpReceiveResult { - delivered, - ready_to_send, - reply: None, - pong: false, - pong_payload: Vec::new(), - disconnected: false, - disconnect_reason: Vec::new(), - }) - } - - pub fn create_ack(&mut self) -> CultNetRudpPacket { - let (ack, ack_mask) = self.ack_state(); - CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Ack, - connection_id: self.connection_id, - sequence: 0, - ack, - ack_mask, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - } - } - - pub fn create_ack_for(&self, sequence: u32) -> CultNetRudpPacket { - CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Ack, - connection_id: self.connection_id, - sequence: 0, - ack: sequence, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - } - } - - pub fn create_ack_for_received(&mut self, sequence: u32) -> CultNetRudpPacket { - let (ack, _) = self.ack_state(); - if ack >= sequence && ack - sequence <= 32 { - self.create_ack() - } else { - self.create_ack_for(sequence) - } - } - - pub fn create_ping(&mut self, payload: Vec) -> CultNetRudpPacket { - self.create_packet( - CultNetRudpPacketType::Ping, - "control", - payload, - false, - false, - false, - ) - } - - pub fn create_disconnect(&mut self, reason: Vec) -> CultNetRudpPacket { - self.connected = false; - self.create_packet( - CultNetRudpPacketType::Disconnect, - "control", - reason, - false, - false, - false, - ) - } - - pub fn check_timeout(&mut self, now_ms: u64, timeout_ms: u64) -> bool { - if !self.connected { - return false; - } - let Some(last_received_at_ms) = self.last_received_at_ms else { - return false; - }; - if now_ms.saturating_sub(last_received_at_ms) <= timeout_ms { - return false; - } - self.connected = false; - true - } - - pub fn due_resends(&mut self, now_ms: u64) -> Vec { - let mut due = Vec::new(); - for pending in self.pending_reliable.values_mut() { - if now_ms.saturating_sub(pending.last_sent_at_ms) >= self.resend_delay_ms { - pending.last_sent_at_ms = now_ms; - due.push(pending.packet.clone()); - } - } - due.sort_by_key(|packet| packet.sequence); - due - } - - fn create_packet( - &mut self, - packet_type: CultNetRudpPacketType, - channel_id: &str, - payload: Vec, - reliable: bool, - ordered: bool, - sequenced: bool, - ) -> CultNetRudpPacket { - self.create_packet_with_fragments( - packet_type, - channel_id, - payload, - reliable, - ordered, - sequenced, - 0, - 0, - 0, - ) - } - - #[allow(clippy::too_many_arguments)] - fn create_packet_with_fragments( - &mut self, - packet_type: CultNetRudpPacketType, - channel_id: &str, - payload: Vec, - reliable: bool, - ordered: bool, - sequenced: bool, - fragment_id: u16, - fragment_index: u16, - fragment_count: u16, - ) -> CultNetRudpPacket { - let sequence = if reliable { - let sequence = self.next_sequence; - self.next_sequence = self - .next_sequence - .checked_add(1) - .expect("reliable sequence overflow"); - sequence - } else if sequenced { - let next = self - .next_sequenced_by_channel - .entry(channel_id.to_string()) - .or_insert(1); - let sequence = *next; - *next = next.checked_add(1).expect("sequenced channel overflow"); - sequence - } else { - 0 - }; - let (ack, ack_mask) = self.ack_state(); - CultNetRudpPacket { - packet_type, - connection_id: self.connection_id, - sequence, - ack, - ack_mask, - channel_id: channel_id.to_string(), - reliable, - ordered, - sequenced, - fragment_id, - fragment_index, - fragment_count, - payload, - } - } - - fn track_reliable(&mut self, packet: CultNetRudpPacket, now_ms: u64) { - self.pending_reliable.insert( - packet.sequence, - PendingReliablePacket { - packet, - last_sent_at_ms: now_ms, - }, - ); - } - - fn admit_reliable_packets( - &mut self, - packets: Vec, - now_ms: u64, - ) -> Vec { - let available = - CULTNET_RUDP_RELIABLE_SEND_WINDOW_PACKETS.saturating_sub(self.pending_reliable.len()); - let mut ready = Vec::with_capacity(available.min(packets.len())); - for packet in packets { - if ready.len() < available { - self.track_reliable(packet.clone(), now_ms); - ready.push(packet); - } else { - self.queued_reliable.push_back(packet); - } - } - ready - } - - fn promote_queued_reliable(&mut self, now_ms: u64) -> Vec { - let available = - CULTNET_RUDP_RELIABLE_SEND_WINDOW_PACKETS.saturating_sub(self.pending_reliable.len()); - let mut ready = Vec::with_capacity(available.min(self.queued_reliable.len())); - for _ in 0..available { - let Some(packet) = self.queued_reliable.pop_front() else { - break; - }; - self.track_reliable(packet.clone(), now_ms); - ready.push(packet); - } - ready - } - - fn ensure_reliable_capacity(&self, packet_count: usize) -> Result<()> { - if packet_count == 0 { - return Ok(()); - } - if let Some(limit) = self.max_pending_reliable_packets { - if self.outstanding_reliable_packet_count() + packet_count > limit { - return Err(anyhow!("RUDP reliable send queue is full")); - } - } - Ok(()) - } - - fn apply_acknowledgements(&mut self, packet: &CultNetRudpPacket) { - self.pending_reliable.remove(&packet.ack); - for bit in 0..32 { - if (packet.ack_mask & (1_u32 << bit)) != 0 && packet.ack > bit { - self.pending_reliable.remove(&(packet.ack - bit - 1)); - } - } - } - - fn remember_received(&mut self, sequence: u32) { - self.received_sequences.insert(sequence); - if self - .highest_received_sequence - .is_none_or(|highest| sequence > highest) - { - self.highest_received_sequence = Some(sequence); - } - if self.received_sequences.len() > RUDP_RECEIVED_SEQUENCE_WINDOW { - let keep_from = self - .highest_received_sequence - .unwrap_or(sequence) - .saturating_sub(RUDP_RECEIVED_SEQUENCE_WINDOW as u32 - 1); - self.received_sequences = self.received_sequences.split_off(&keep_from); - } - } - - fn ack_state(&self) -> (u32, u32) { - let ack = self.highest_received_sequence.unwrap_or(0); - let mut ack_mask = 0_u32; - for bit in 0..32 { - if ack > bit && self.received_sequences.contains(&(ack - bit - 1)) { - ack_mask |= 1_u32 << bit; - } - } - (ack, ack_mask) - } - - fn reassemble( - &mut self, - packet: &CultNetRudpPacket, - ) -> Result> { - if packet.fragment_count == 0 { - self.require_payload_size(packet.payload.len())?; - return Ok(Some(( - CultNetRudpDeliveredFrame { - channel_id: packet.channel_id.clone(), - payload: packet.payload.clone(), - sequence: packet.sequence, - }, - packet.ordered, - packet.sequence + 1, - ))); - } - if packet.fragment_id == 0 { - return Err(anyhow!( - "RUDP fragmented packet must have a non-zero fragment id" - )); - } - if packet.fragment_index >= packet.fragment_count { - return Err(anyhow!( - "RUDP fragment index must be lower than fragment count" - )); - } - - let key = (packet.channel_id.clone(), packet.fragment_id); - if !self.fragment_buffers.contains_key(&key) - && self.fragment_buffers.len() >= self.max_pending_fragment_sets - { - return Err(anyhow!("RUDP pending fragment-set limit reached")); - } - let buffered_bytes = self - .fragment_buffers - .get(&key) - .map(|buffer| { - buffer - .payloads - .iter() - .filter(|(index, _)| **index != packet.fragment_index) - .map(|(_, payload)| payload.len()) - .sum::() - }) - .unwrap_or(0); - if let Err(error) = self.require_payload_size(buffered_bytes + packet.payload.len()) { - self.fragment_buffers.remove(&key); - return Err(error); - } - let buffer = self - .fragment_buffers - .entry(key.clone()) - .or_insert_with(|| FragmentBuffer { - channel_id: packet.channel_id.clone(), - ordered: packet.ordered, - fragment_count: packet.fragment_count, - payloads: BTreeMap::new(), - sequences: BTreeMap::new(), - }); - if buffer.fragment_count != packet.fragment_count || buffer.ordered != packet.ordered { - return Err(anyhow!( - "RUDP fragment metadata changed within a fragment set" - )); - } - buffer - .payloads - .insert(packet.fragment_index, packet.payload.clone()); - buffer - .sequences - .insert(packet.fragment_index, packet.sequence); - if buffer.payloads.len() < packet.fragment_count as usize { - return Ok(None); - } - - let mut payload = Vec::new(); - let mut sequences = Vec::new(); - for index in 0..packet.fragment_count { - let Some(chunk) = buffer.payloads.get(&index) else { - return Ok(None); - }; - let Some(sequence) = buffer.sequences.get(&index) else { - return Ok(None); - }; - payload.extend_from_slice(chunk); - sequences.push(*sequence); - } - let channel_id = buffer.channel_id.clone(); - let ordered = buffer.ordered; - self.fragment_buffers.remove(&key); - Ok(Some(( - CultNetRudpDeliveredFrame { - channel_id, - payload, - sequence: *sequences.iter().min().unwrap(), - }, - ordered, - sequences.iter().max().unwrap() + 1, - ))) - } - - fn require_payload_size(&self, payload_bytes: usize) -> Result<()> { - if self - .max_payload_bytes - .is_some_and(|max_payload_bytes| payload_bytes > max_payload_bytes) - { - return Err(anyhow!("RUDP payload exceeds max_payload_bytes")); - } - Ok(()) - } - - fn deliver_ordered( - &mut self, - frame: CultNetRudpDeliveredFrame, - next_sequence_after_frame: u32, - expected_sequence_if_uninitialized: u32, - ) -> Vec { - let channel_id = frame.channel_id.clone(); - let mut next = if let Some(next) = self - .ordered_next_sequence_by_channel - .get(&channel_id) - .copied() - { - next - } else { - self.ordered_next_sequence_by_channel.insert( - channel_id.clone(), - expected_sequence_if_uninitialized.min(frame.sequence), - ); - expected_sequence_if_uninitialized.min(frame.sequence) - }; - - while frame.sequence > next - && self.received_sequences.contains(&next) - && !self - .ordered_buffers - .get(&channel_id) - .is_some_and(|buffer| buffer.contains_key(&next)) - { - next = next.saturating_add(1); - self.ordered_next_sequence_by_channel - .insert(channel_id.clone(), next); - } - - if frame.sequence < next { - return Vec::new(); - } - - if frame.sequence > next { - self.ordered_buffers.entry(channel_id).or_default().insert( - frame.sequence, - PendingOrderedFrame { - frame, - next_sequence: next_sequence_after_frame, - }, - ); - return Vec::new(); - } - - self.ordered_next_sequence_by_channel - .insert(channel_id.clone(), next_sequence_after_frame); - let mut delivered = vec![frame]; - delivered.extend(self.drain_ordered(&channel_id)); - delivered - } - - fn drain_ordered(&mut self, channel_id: &str) -> Vec { - let mut delivered = Vec::new(); - loop { - let Some(next) = self - .ordered_next_sequence_by_channel - .get(channel_id) - .copied() - else { - break; - }; - let Some(buffer) = self.ordered_buffers.get_mut(channel_id) else { - break; - }; - let Some(pending) = buffer.remove(&next) else { - break; - }; - delivered.push(pending.frame); - self.ordered_next_sequence_by_channel - .insert(channel_id.to_string(), pending.next_sequence); - self.skip_received_non_channel_sequences(channel_id); - } - delivered - } - - fn skip_received_non_channel_sequences(&mut self, channel_id: &str) { - let Some(mut next) = self - .ordered_next_sequence_by_channel - .get(channel_id) - .copied() - else { - return; - }; - while self.received_sequences.contains(&next) - && !self - .ordered_buffers - .get(channel_id) - .is_some_and(|buffer| buffer.contains_key(&next)) - { - next = next.saturating_add(1); - self.ordered_next_sequence_by_channel - .insert(channel_id.to_string(), next); - } - } - - fn allocate_fragment_id(&mut self) -> u16 { - let fragment_id = self.next_fragment_id; - self.next_fragment_id = self.next_fragment_id.saturating_add(1); - if self.next_fragment_id == 0 { - self.next_fragment_id = 1; - } - fragment_id - } - - fn require_connection(&self, packet: &CultNetRudpPacket) -> Result<()> { - if packet.connection_id != self.connection_id { - return Err(anyhow!( - "RUDP packet connection id {} does not match {}", - packet.connection_id, - self.connection_id - )); - } - Ok(()) - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum CultNetRudpSocketMode { - Client, - Server, -} - -pub struct CultNetRudpSocketTransportOptions { - pub runtime_id: String, - pub socket: UdpSocket, - pub mode: CultNetRudpSocketMode, - pub remote_addr: Option, - pub connection_id: u32, - pub initial_sequence: u32, - pub resend_delay_ms: u64, - pub transport_id: Option, - pub max_payload_bytes: Option, - pub max_fragment_bytes: Option, - pub max_pending_reliable_packets: Option, - pub reconnect_policy: Option, -} - -impl CultNetRudpSocketTransportOptions { - pub fn client( - runtime_id: impl Into, - socket: UdpSocket, - remote_addr: SocketAddr, - connection_id: u32, - ) -> Self { - Self { - runtime_id: runtime_id.into(), - socket, - mode: CultNetRudpSocketMode::Client, - remote_addr: Some(remote_addr), - connection_id, - initial_sequence: 1, - resend_delay_ms: 250, - transport_id: None, - max_payload_bytes: None, - max_fragment_bytes: None, - max_pending_reliable_packets: None, - reconnect_policy: None, - } - } - - pub fn server(runtime_id: impl Into, socket: UdpSocket, connection_id: u32) -> Self { - Self { - runtime_id: runtime_id.into(), - socket, - mode: CultNetRudpSocketMode::Server, - remote_addr: None, - connection_id, - initial_sequence: 1, - resend_delay_ms: 250, - transport_id: None, - max_payload_bytes: None, - max_fragment_bytes: None, - max_pending_reliable_packets: None, - reconnect_policy: None, - } - } -} - -pub struct CultNetRudpSocketTransportConnection { - socket: UdpSocket, - session: CultNetRudpSession, - mode: CultNetRudpSocketMode, - remote_addr: Option, - pub profile: CultNetTransportProfile, - stats: CultNetTransportStats, - delivered_frames: VecDeque, - max_fragment_bytes: Option, - disconnect_reason: Option>, - pong_payloads: VecDeque>, -} - -pub struct CultNetRudpServerHubOptions { - pub runtime_id: String, - pub socket: UdpSocket, - pub connection_id: u32, - pub initial_sequence: u32, - pub resend_delay_ms: u64, - pub transport_id: Option, - pub max_payload_bytes: Option, - pub max_fragment_bytes: Option, - pub max_pending_reliable_packets: Option, - pub max_peers: usize, -} - -impl CultNetRudpServerHubOptions { - pub fn new(runtime_id: impl Into, socket: UdpSocket, connection_id: u32) -> Self { - Self { - runtime_id: runtime_id.into(), - socket, - connection_id, - initial_sequence: 1, - resend_delay_ms: 250, - transport_id: None, - max_payload_bytes: None, - max_fragment_bytes: None, - max_pending_reliable_packets: None, - max_peers: 256, - } - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetRudpServerSessionContext { - pub remote_addr: SocketAddr, - pub connection_id: u32, - /// Server-minted generation used to fence work from an older session at - /// the same UDP endpoint. - pub session_generation: u64, - /// Exact bytes supplied by the peer's most recently accepted Connect packet. - /// CultNet does not interpret these bytes; higher layers may use them as - /// transport evidence when making their own authorization decision. - pub connect_payload: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub enum CultNetRudpServerEvent { - Connected { - session: CultNetRudpServerSessionContext, - }, - Frame { - session: CultNetRudpServerSessionContext, - frame: CultNetTransportFrame, - }, - Pong { - session: CultNetRudpServerSessionContext, - payload: Vec, - }, - Disconnected { - session: CultNetRudpServerSessionContext, - reason: Vec, - }, -} - -struct CultNetRudpServerPeer { - session: CultNetRudpSession, - context: CultNetRudpServerSessionContext, -} - -/// One UDP listener with an independent reliable session for every remote peer. -/// -/// The hub owns packet/session mechanics only. Registration, leases, commands, -/// and all other application policy belong to the service using the hub. -pub struct CultNetRudpServerHub { - socket: UdpSocket, - connection_id: u32, - initial_sequence: u32, - resend_delay_ms: u64, - max_pending_reliable_packets: Option, - max_payload_bytes: usize, - max_fragment_bytes: Option, - max_peers: usize, - next_session_generation: u64, - peers: BTreeMap, - pending_events: VecDeque, - pub profile: CultNetTransportProfile, - stats: CultNetTransportStats, -} - -impl CultNetRudpServerHub { - pub fn new(options: CultNetRudpServerHubOptions) -> Result { - let local_addr = options.socket.local_addr()?; - if options.max_peers == 0 { - return Err(anyhow!( - "RUDP server hub max_peers must be greater than zero" - )); - } - let max_payload_bytes = options - .max_payload_bytes - .or(Some(CULTNET_RUDP_DEFAULT_MAX_PAYLOAD_BYTES as u32)); - let profile = create_rudp_transport_profile( - options.runtime_id, - RudpTransportProfileOptions { - transport_id: options.transport_id, - host: Some(local_addr.ip().to_string()), - port: Some(local_addr.port()), - max_payload_bytes, - max_fragment_bytes: options.max_fragment_bytes, - max_pending_reliable_packets: options.max_pending_reliable_packets, - reconnect_policy: None, - }, - ); - Ok(Self { - socket: options.socket, - connection_id: options.connection_id, - initial_sequence: options.initial_sequence, - resend_delay_ms: options.resend_delay_ms, - max_pending_reliable_packets: options - .max_pending_reliable_packets - .map(|value| value as usize), - max_payload_bytes: max_payload_bytes.expect("RUDP hub has an effective payload limit") - as usize, - max_fragment_bytes: options.max_fragment_bytes.map(|value| value as usize), - max_peers: options.max_peers, - next_session_generation: 1, - peers: BTreeMap::new(), - pending_events: VecDeque::new(), - profile, - stats: CultNetTransportStats::default(), - }) - } - - pub fn local_addr(&self) -> Result { - Ok(self.socket.local_addr()?) - } - - pub fn stats(&self) -> CultNetTransportStats { - self.stats.clone() - } - - pub fn sessions(&self) -> Vec { - self.peers - .values() - .map(|peer| peer.context.clone()) - .collect() - } - - pub fn session(&self, remote_addr: SocketAddr) -> Option<&CultNetRudpServerSessionContext> { - self.peers.get(&remote_addr).map(|peer| &peer.context) - } - - pub fn send( - &mut self, - session: &CultNetRudpServerSessionContext, - channel_id: &str, - payload: Vec, - ) -> Result<()> { - let remote_addr = session.remote_addr; - let packets = { - let peer = self - .peers - .get_mut(&remote_addr) - .ok_or_else(|| anyhow!("Unknown RUDP server peer {remote_addr}"))?; - if peer.context.session_generation != session.session_generation { - return Err(anyhow!( - "RUDP server session generation {} is no longer active", - session.session_generation - )); - } - peer.session.send_many( - channel_id, - payload, - channel_send_options(channel_id, now_ms()), - self.max_fragment_bytes, - )? - }; - for packet in packets { - self.send_packet(remote_addr, &packet)?; - } - self.stats.frames_sent += 1; - Ok(()) - } - - pub fn send_schema_message( - &mut self, - session: &CultNetRudpServerSessionContext, - message: &CultNetMessage, - ) -> Result<()> { - let payload = encode_cultnet_message_to_vec(message, CultNetWireContract::CultNetSchemaV0)?; - self.send(session, "schema", payload) - } - - pub fn disconnect( - &mut self, - session: &CultNetRudpServerSessionContext, - reason: Vec, - ) -> Result { - let Some(peer) = self.peers.get(&session.remote_addr) else { - return Ok(false); - }; - if peer.context.session_generation != session.session_generation { - return Ok(false); - } - let mut peer = self - .peers - .remove(&session.remote_addr) - .expect("validated RUDP peer exists"); - let packet = peer.session.create_disconnect(reason); - self.send_packet(session.remote_addr, &packet)?; - Ok(true) - } - - pub fn receive_event_once(&mut self) -> Result> { - if let Some(event) = self.pending_events.pop_front() { - return Ok(Some(event)); - } - - let mut wire = vec![0_u8; 65_535]; - let (received, remote_addr) = match self.socket.recv_from(&mut wire) { - Ok(value) => value, - Err(error) - if error.kind() == ErrorKind::WouldBlock - || error.kind() == ErrorKind::TimedOut - || error.kind() == ErrorKind::ConnectionReset => - { - return Ok(None); - } - Err(error) => return Err(error.into()), - }; - wire.truncate(received); - self.stats.bytes_received += received as u64; - let packet = decode_rudp_packet(&wire)?; - if packet.connection_id != self.connection_id { - return Ok(None); - } - - if packet.packet_type == CultNetRudpPacketType::Connect { - if let Some(peer) = self.peers.get_mut(&remote_addr) - && peer.context.connect_payload == packet.payload - { - let _ = peer.session.receive(&packet, now_ms())?; - let reply = peer - .session - .pending_accept_for_resend(now_ms()) - .unwrap_or_else(|| peer.session.create_ack()); - self.send_packet(remote_addr, &reply)?; - return Ok(None); - } - if !self.peers.contains_key(&remote_addr) && self.peers.len() >= self.max_peers { - return Err(anyhow!("RUDP server hub peer limit reached")); - } - let mut session = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: self.connection_id, - initial_sequence: self.initial_sequence, - resend_delay_ms: self.resend_delay_ms, - max_pending_reliable_packets: self.max_pending_reliable_packets, - }); - session.set_max_payload_bytes(Some(self.max_payload_bytes)); - let generation = self.next_session_generation; - self.next_session_generation = self - .next_session_generation - .checked_add(1) - .ok_or_else(|| anyhow!("RUDP server session generation exhausted"))?; - let context = CultNetRudpServerSessionContext { - remote_addr, - connection_id: packet.connection_id, - session_generation: generation, - connect_payload: packet.payload.clone(), - }; - let accept = session.accept_connect(&packet, now_ms(), Vec::new())?; - self.send_packet(remote_addr, &accept)?; - let replaced = self.peers.insert( - remote_addr, - CultNetRudpServerPeer { - session, - context: context.clone(), - }, - ); - if let Some(replaced) = replaced { - self.pending_events - .push_back(CultNetRudpServerEvent::Disconnected { - session: replaced.context, - reason: b"replaced by a new Connect generation".to_vec(), - }); - } - self.pending_events - .push_back(CultNetRudpServerEvent::Connected { session: context }); - return Ok(self.pending_events.pop_front()); - } - - let Some(peer) = self.peers.get_mut(&remote_addr) else { - return Ok(None); - }; - let result = peer.session.receive(&packet, now_ms())?; - let context = peer.context.clone(); - let ack = if packet.reliable { - Some(peer.session.create_ack_for_received(packet.sequence)) - } else { - None - }; - if let Some(reply) = result.reply { - self.send_packet(remote_addr, &reply)?; - } - for ready in result.ready_to_send { - self.send_packet(remote_addr, &ready)?; - } - if let Some(ack) = ack { - self.send_packet(remote_addr, &ack)?; - } - if result.pong { - self.pending_events.push_back(CultNetRudpServerEvent::Pong { - session: context.clone(), - payload: result.pong_payload, - }); - } - for delivered in result.delivered { - self.stats.frames_received += 1; - self.pending_events - .push_back(CultNetRudpServerEvent::Frame { - session: context.clone(), - frame: CultNetTransportFrame { - channel_id: delivered.channel_id, - payload: delivered.payload, - }, - }); - } - if result.disconnected { - self.pending_events - .push_back(CultNetRudpServerEvent::Disconnected { - session: context, - reason: result.disconnect_reason, - }); - self.peers.remove(&remote_addr); - } - Ok(self.pending_events.pop_front()) - } - - pub fn poll_resends(&mut self) -> Result<()> { - let now = now_ms(); - let mut packets = Vec::new(); - for (remote_addr, peer) in &mut self.peers { - packets.extend( - peer.session - .due_resends(now) - .into_iter() - .map(|packet| (*remote_addr, packet)), - ); - } - for (remote_addr, packet) in packets { - self.send_packet(remote_addr, &packet)?; - } - Ok(()) - } - - pub fn remove_timed_out_sessions( - &mut self, - timeout_ms: u64, - ) -> Vec { - let now = now_ms(); - let timed_out = self - .peers - .iter_mut() - .filter_map(|(remote_addr, peer)| { - peer.session - .check_timeout(now, timeout_ms) - .then_some((*remote_addr, peer.context.clone())) - }) - .collect::>(); - for (remote_addr, _) in &timed_out { - self.peers.remove(remote_addr); - } - timed_out.into_iter().map(|(_, context)| context).collect() - } - - fn send_packet(&mut self, remote_addr: SocketAddr, packet: &CultNetRudpPacket) -> Result<()> { - let wire = encode_rudp_packet(packet)?; - let sent = self.socket.send_to(&wire, remote_addr)?; - self.stats.bytes_sent += sent as u64; - Ok(()) - } -} - -impl CultNetRudpSocketTransportConnection { - pub fn new(options: CultNetRudpSocketTransportOptions) -> Result { - let local_addr = options.socket.local_addr()?; - let max_payload_bytes = options - .max_payload_bytes - .or(Some(CULTNET_RUDP_DEFAULT_MAX_PAYLOAD_BYTES as u32)); - let profile = create_rudp_transport_profile( - options.runtime_id, - RudpTransportProfileOptions { - transport_id: options.transport_id, - host: Some(local_addr.ip().to_string()), - port: Some(local_addr.port()), - max_payload_bytes, - max_fragment_bytes: options.max_fragment_bytes, - max_pending_reliable_packets: options.max_pending_reliable_packets, - reconnect_policy: options.reconnect_policy, - }, - ); - let max_pending_reliable_packets = options - .max_pending_reliable_packets - .map(|value| value as usize); - let mut session = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: options.connection_id, - initial_sequence: options.initial_sequence, - resend_delay_ms: options.resend_delay_ms, - max_pending_reliable_packets, - }); - session.set_max_payload_bytes(max_payload_bytes.map(|value| value as usize)); - Ok(Self { - socket: options.socket, - session, - mode: options.mode, - remote_addr: options.remote_addr, - profile, - stats: CultNetTransportStats::default(), - delivered_frames: VecDeque::new(), - max_fragment_bytes: options.max_fragment_bytes.map(|value| value as usize), - disconnect_reason: None, - pong_payloads: VecDeque::new(), - }) - } - - pub fn connected(&self) -> bool { - self.session.connected() - } - - pub fn stats(&self) -> CultNetTransportStats { - self.stats.clone() - } - - pub fn disconnect_reason(&self) -> Option<&[u8]> { - self.disconnect_reason.as_deref() - } - - pub fn pop_pong_payload(&mut self) -> Option> { - self.pong_payloads.pop_front() - } - - pub fn connect(&mut self, payload: Vec) -> Result<()> { - if self.mode != CultNetRudpSocketMode::Client { - return Err(anyhow!( - "Only a client RUDP socket transport can initiate connect" - )); - } - let packet = self.session.create_connect(now_ms(), payload)?; - self.send_packet(&packet) - } - - pub fn send(&mut self, channel_id: &str, payload: Vec) -> Result<()> { - let options = channel_send_options(channel_id, now_ms()); - let packets = - self.session - .send_many(channel_id, payload, options, self.max_fragment_bytes)?; - for packet in packets { - self.send_packet(&packet)?; - } - self.stats.frames_sent += 1; - Ok(()) - } - - pub fn send_schema_message(&mut self, message: &CultNetMessage) -> Result<()> { - let payload = encode_cultnet_message_to_vec(message, CultNetWireContract::CultNetSchemaV0)?; - self.send("schema", payload) - } - - pub fn receive_schema_message_once(&mut self) -> Result> { - let Some(frame) = self.receive_once()? else { - return Ok(None); - }; - if frame.channel_id != "schema" { - return Err(anyhow!( - "Expected RUDP schema frame, received channel {}", - frame.channel_id - )); - } - Ok(Some(decode_cultnet_message_from_slice( - &frame.payload, - CultNetWireContract::CultNetSchemaV0, - )?)) - } - - pub fn disconnect(&mut self, reason: Vec) -> Result<()> { - let packet = self.session.create_disconnect(reason); - self.send_packet(&packet) - } - - pub fn ping(&mut self, payload: Vec) -> Result<()> { - let packet = self.session.create_ping(payload); - self.send_packet(&packet) - } - - pub fn check_timeout(&mut self, timeout_ms: u64) -> bool { - self.session.check_timeout(now_ms(), timeout_ms) - } - - pub fn pending_reliable_packet_count(&self) -> usize { - self.session.pending_reliable_sequences().len() - } - - pub fn outstanding_reliable_packet_count(&self) -> usize { - self.session.outstanding_reliable_packet_count() - } - - pub fn flush_reliable(&mut self, timeout: Duration) -> Result<()> { - let original_timeout = self.socket.read_timeout()?; - let poll_timeout = original_timeout - .map(|configured| configured.min(Duration::from_millis(10))) - .unwrap_or(Duration::from_millis(10)); - self.socket.set_read_timeout(Some(poll_timeout))?; - - let deadline = Instant::now() + timeout; - let mut preserved_frames = VecDeque::new(); - let result = (|| { - while self.outstanding_reliable_packet_count() > 0 { - if Instant::now() >= deadline { - return Err(anyhow!( - "RUDP reliable flush timed out with {} packets outstanding", - self.outstanding_reliable_packet_count() - )); - } - if let Some(frame) = self.receive_once()? { - preserved_frames.push_back(frame); - } - self.poll_resends()?; - } - Ok(()) - })(); - - preserved_frames.append(&mut self.delivered_frames); - self.delivered_frames = preserved_frames; - let restore = self.socket.set_read_timeout(original_timeout); - match (result, restore) { - (Err(error), _) => Err(error), - (Ok(()), Err(error)) => Err(error.into()), - (Ok(()), Ok(())) => Ok(()), - } - } - - pub fn receive_once(&mut self) -> Result> { - if let Some(frame) = self.delivered_frames.pop_front() { - return Ok(Some(frame)); - } - - let mut wire = vec![0_u8; 65_535]; - let (received, remote_addr) = match self.socket.recv_from(&mut wire) { - Ok(value) => value, - Err(error) - if error.kind() == ErrorKind::WouldBlock - || error.kind() == ErrorKind::TimedOut - || error.kind() == ErrorKind::ConnectionReset => - { - return Ok(None); - } - Err(error) => return Err(error.into()), - }; - wire.truncate(received); - self.stats.bytes_received += received as u64; - - let packet = decode_rudp_packet(&wire)?; - if let Some(expected) = self.remote_addr { - if expected != remote_addr { - if self.mode == CultNetRudpSocketMode::Server - && packet.packet_type == CultNetRudpPacketType::Connect - { - self.remote_addr = Some(remote_addr); - } else { - return Ok(None); - } - } - } else { - if self.mode == CultNetRudpSocketMode::Server - && packet.packet_type != CultNetRudpPacketType::Connect - { - return Ok(None); - } - self.remote_addr = Some(remote_addr); - } - if self.mode == CultNetRudpSocketMode::Server - && packet.packet_type == CultNetRudpPacketType::Connect - { - self.session.reset_peer_state(); - let accept = self.session.accept_connect(&packet, now_ms(), Vec::new())?; - self.send_packet(&accept)?; - return Ok(None); - } - - let result = self.session.receive(&packet, now_ms())?; - if let Some(reply) = result.reply { - self.send_packet(&reply)?; - } - for ready in result.ready_to_send { - self.send_packet(&ready)?; - } - if result.pong { - self.pong_payloads.push_back(result.pong_payload); - } - if result.disconnected { - self.disconnect_reason = Some(result.disconnect_reason); - return Ok(None); - } - - for frame in result.delivered { - self.delivered_frames.push_back(CultNetTransportFrame { - channel_id: frame.channel_id, - payload: frame.payload, - }); - self.stats.frames_received += 1; - } - let frame = self.delivered_frames.pop_front(); - if packet.reliable || packet.packet_type == CultNetRudpPacketType::Accept || frame.is_some() - { - let ack = self.session.create_ack_for_received(packet.sequence); - self.send_packet(&ack)?; - } - Ok(frame) - } - - pub fn poll_resends(&mut self) -> Result<()> { - for packet in self.session.due_resends(now_ms()) { - self.send_packet(&packet)?; - } - Ok(()) - } - - fn send_packet(&mut self, packet: &CultNetRudpPacket) -> Result<()> { - let Some(remote_addr) = self.remote_addr else { - return Err(anyhow!( - "RUDP socket transport does not have a remote endpoint" - )); - }; - let wire = encode_rudp_packet(packet)?; - let sent = self.socket.send_to(&wire, remote_addr)?; - self.stats.bytes_sent += sent as u64; - Ok(()) - } -} - -pub struct CultNetRudpReconnectLoop -where - F: FnMut() -> Result, -{ - pub reconnect_controller: CultNetReconnectController, - create_transport: F, - connect_payload: Vec, - transport: Option, - stopped: bool, -} - -impl CultNetRudpReconnectLoop -where - F: FnMut() -> Result, -{ - pub fn new( - reconnect_policy: CultNetReconnectPolicy, - connect_payload: Vec, - create_transport: F, - ) -> Self { - Self { - reconnect_controller: CultNetReconnectController::new(reconnect_policy), - create_transport, - connect_payload, - transport: None, - stopped: true, - } - } - - pub fn with_default_policy(connect_payload: Vec, create_transport: F) -> Self { - Self::new( - create_reconnect_policy(Default::default()), - connect_payload, - create_transport, - ) - } - - pub fn transport(&self) -> Option<&CultNetRudpSocketTransportConnection> { - self.transport.as_ref() - } - - pub fn transport_mut(&mut self) -> Option<&mut CultNetRudpSocketTransportConnection> { - self.transport.as_mut() - } - - pub fn start(&mut self) -> Result<&mut CultNetRudpSocketTransportConnection> { - self.stopped = false; - self.reconnect_controller.reset(); - self.open_transport() - } - - pub fn stop(&mut self) { - self.stopped = true; - self.transport = None; - self.reconnect_controller.reset(); - } - - pub fn mark_connected(&mut self) { - self.reconnect_controller.reset(); - } - - pub fn handle_closed( - &mut self, - now_ms: u64, - jitter_ms: u64, - ) -> Option { - self.transport = None; - if self.stopped { - return None; - } - Some(self.reconnect_controller.record_failure(now_ms, jitter_ms)) - } - - pub fn reconnect_if_due(&mut self, now_ms: u64) -> Result { - if self.stopped || !self.reconnect_controller.can_attempt(now_ms) { - return Ok(false); - } - self.open_transport()?; - Ok(true) - } - - fn open_transport(&mut self) -> Result<&mut CultNetRudpSocketTransportConnection> { - let mut transport = (self.create_transport)()?; - transport.connect(self.connect_payload.clone())?; - self.transport = Some(transport); - Ok(self - .transport - .as_mut() - .expect("RUDP reconnect loop opened a transport")) - } -} - -#[derive(Clone, Debug, Default)] -pub struct RudpTransportProfileOptions { - pub transport_id: Option, - pub host: Option, - pub port: Option, - pub max_payload_bytes: Option, - pub max_fragment_bytes: Option, - pub max_pending_reliable_packets: Option, - pub reconnect_policy: Option, -} - -pub fn create_rudp_transport_profile( - runtime_id: impl Into, - options: RudpTransportProfileOptions, -) -> CultNetTransportProfile { - CultNetTransportProfile { - schema_version: "cultnet.transport_profile.v0".to_string(), - runtime_id: runtime_id.into(), - transports: vec![CultNetTransportDescriptor { - transport_id: options.transport_id.unwrap_or_else(|| "rudp".to_string()), - protocol: CultNetTransportProtocol::Rudp, - host: options.host, - port: options.port, - path: None, - discovery_group: None, - wire_contracts: Some(vec!["cultnet.schema.v0".to_string()]), - reconnect_policy: Some( - options - .reconnect_policy - .unwrap_or_else(|| create_reconnect_policy(Default::default())), - ), - channels: vec![ - CultNetTransportChannel { - channel_id: "schema".to_string(), - delivery: CultNetTransportDelivery::Reliable, - ordering: CultNetTransportOrdering::Ordered, - max_payload_bytes: options.max_payload_bytes, - max_fragment_bytes: options.max_fragment_bytes, - max_pending_reliable_packets: options.max_pending_reliable_packets, - }, - CultNetTransportChannel { - channel_id: "latest".to_string(), - delivery: CultNetTransportDelivery::Unreliable, - ordering: CultNetTransportOrdering::Sequenced, - max_payload_bytes: options.max_payload_bytes, - max_fragment_bytes: options.max_fragment_bytes, - max_pending_reliable_packets: options.max_pending_reliable_packets, - }, - CultNetTransportChannel { - channel_id: "realtime".to_string(), - delivery: CultNetTransportDelivery::Unreliable, - ordering: CultNetTransportOrdering::Unordered, - max_payload_bytes: options.max_payload_bytes, - max_fragment_bytes: options.max_fragment_bytes, - max_pending_reliable_packets: options.max_pending_reliable_packets, - }, - CultNetTransportChannel { - channel_id: "media".to_string(), - delivery: CultNetTransportDelivery::Reliable, - ordering: CultNetTransportOrdering::Unordered, - max_payload_bytes: options.max_payload_bytes, - max_fragment_bytes: options.max_fragment_bytes, - max_pending_reliable_packets: options.max_pending_reliable_packets, - }, - ], - }], - } -} - -pub fn encode_rudp_packet(packet: &CultNetRudpPacket) -> Result> { - let channel_id = packet.channel_id.as_bytes(); - if channel_id.len() > u8::MAX as usize { - return Err(anyhow!( - "CultNet RUDP channel id cannot exceed 255 UTF-8 bytes" - )); - } - - let header_bytes = RUDP_FIXED_HEADER_BYTES + channel_id.len(); - let mut wire = vec![0_u8; header_bytes + packet.payload.len()]; - wire[..4].copy_from_slice(&RUDP_MAGIC); - wire[4] = RUDP_VERSION; - wire[5] = packet_type_to_code(packet.packet_type); - wire[6] = encode_flags(packet); - wire[7] = header_bytes as u8; - wire[8..12].copy_from_slice(&packet.connection_id.to_be_bytes()); - wire[12..16].copy_from_slice(&packet.sequence.to_be_bytes()); - wire[16..20].copy_from_slice(&packet.ack.to_be_bytes()); - wire[20..24].copy_from_slice(&packet.ack_mask.to_be_bytes()); - wire[24..26].copy_from_slice(&packet.fragment_id.to_be_bytes()); - wire[26..28].copy_from_slice(&packet.fragment_index.to_be_bytes()); - wire[28..30].copy_from_slice(&packet.fragment_count.to_be_bytes()); - wire[30..34].copy_from_slice(&(packet.payload.len() as u32).to_be_bytes()); - wire[34] = channel_id.len() as u8; - wire[35] = 0; - wire[RUDP_FIXED_HEADER_BYTES..header_bytes].copy_from_slice(channel_id); - wire[header_bytes..].copy_from_slice(&packet.payload); - Ok(wire) -} - -pub fn decode_rudp_packet(wire: &[u8]) -> Result { - if wire.len() < RUDP_FIXED_HEADER_BYTES { - return Err(anyhow!( - "CultNet RUDP packet is shorter than the fixed header" - )); - } - if wire[..4] != RUDP_MAGIC { - return Err(anyhow!("CultNet RUDP packet has the wrong magic")); - } - if wire[4] != RUDP_VERSION { - return Err(anyhow!( - "Unsupported CultNet RUDP packet version {}", - wire[4] - )); - } - - let packet_type = packet_type_from_code(wire[5])?; - let header_bytes = wire[7] as usize; - let channel_id_len = wire[34] as usize; - if header_bytes != RUDP_FIXED_HEADER_BYTES + channel_id_len { - return Err(anyhow!( - "CultNet RUDP packet header length does not match the channel id length" - )); - } - let payload_len = u32::from_be_bytes(wire[30..34].try_into()?) as usize; - if wire.len() != header_bytes + payload_len { - return Err(anyhow!( - "CultNet RUDP packet payload length does not match the packet size" - )); - } - - let flags = wire[6]; - Ok(CultNetRudpPacket { - packet_type, - reliable: (flags & 0b0000_0001) != 0, - ordered: (flags & 0b0000_0010) != 0, - sequenced: (flags & 0b0000_0100) != 0, - connection_id: u32::from_be_bytes(wire[8..12].try_into()?), - sequence: u32::from_be_bytes(wire[12..16].try_into()?), - ack: u32::from_be_bytes(wire[16..20].try_into()?), - ack_mask: u32::from_be_bytes(wire[20..24].try_into()?), - fragment_id: u16::from_be_bytes(wire[24..26].try_into()?), - fragment_index: u16::from_be_bytes(wire[26..28].try_into()?), - fragment_count: u16::from_be_bytes(wire[28..30].try_into()?), - channel_id: String::from_utf8(wire[RUDP_FIXED_HEADER_BYTES..header_bytes].to_vec())?, - payload: wire[header_bytes..].to_vec(), - }) -} - -fn encode_flags(packet: &CultNetRudpPacket) -> u8 { - (if packet.reliable { 0b0000_0001 } else { 0 }) - | (if packet.ordered { 0b0000_0010 } else { 0 }) - | (if packet.sequenced { 0b0000_0100 } else { 0 }) - | (if packet.fragment_count > 0 { - 0b0000_1000 - } else { - 0 - }) -} - -fn packet_type_to_code(packet_type: CultNetRudpPacketType) -> u8 { - match packet_type { - CultNetRudpPacketType::Connect => 1, - CultNetRudpPacketType::Accept => 2, - CultNetRudpPacketType::Data => 3, - CultNetRudpPacketType::Ack => 4, - CultNetRudpPacketType::Ping => 5, - CultNetRudpPacketType::Pong => 6, - CultNetRudpPacketType::Disconnect => 7, - } -} - -fn channel_send_options(channel_id: &str, now_ms: u64) -> CultNetRudpSendOptions { - match channel_id { - "schema" => CultNetRudpSendOptions { - reliable: true, - ordered: true, - sequenced: false, - now_ms, - }, - "latest" => CultNetRudpSendOptions { - reliable: false, - ordered: false, - sequenced: true, - now_ms, - }, - "media" => CultNetRudpSendOptions { - reliable: true, - ordered: false, - sequenced: false, - now_ms, - }, - _ => CultNetRudpSendOptions { - reliable: false, - ordered: false, - sequenced: false, - now_ms, - }, - } -} - -fn now_ms() -> u64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_millis() as u64) - .unwrap_or(0) -} - -fn packet_type_from_code(code: u8) -> Result { - match code { - 1 => Ok(CultNetRudpPacketType::Connect), - 2 => Ok(CultNetRudpPacketType::Accept), - 3 => Ok(CultNetRudpPacketType::Data), - 4 => Ok(CultNetRudpPacketType::Ack), - 5 => Ok(CultNetRudpPacketType::Ping), - 6 => Ok(CultNetRudpPacketType::Pong), - 7 => Ok(CultNetRudpPacketType::Disconnect), - _ => Err(anyhow!("Unsupported CultNet RUDP packet type {code}")), - } -} diff --git a/vendor/cultnet-rs/src/schema_discovery.rs b/vendor/cultnet-rs/src/schema_discovery.rs deleted file mode 100644 index 272323b..0000000 --- a/vendor/cultnet-rs/src/schema_discovery.rs +++ /dev/null @@ -1,410 +0,0 @@ -use anyhow::Result; -use anyhow::anyhow; -use serde_json::Value; -use sha2::Digest; -use sha2::Sha256; -use std::collections::BTreeMap; -use std::collections::BTreeSet; - -use crate::CultNetMessage; -use crate::CultNetSchemaDescriptor; -use crate::CultNetSchemaKind; -use crate::CultNetWireContract; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetSchemaRegistration { - pub schema_id: String, - pub kind: CultNetSchemaKind, - pub wire_contracts: Vec, - pub schema_version: Option, - pub document_type: Option, - pub title: Option, - pub schema_json: Option, -} - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct CultNetSchemaCatalogOptions { - pub include_schema_json: bool, - pub schema_ids: Option>, - pub kinds: Option>, -} - -#[derive(Clone, Debug, Default)] -pub struct CultNetSchemaRegistry { - entries: BTreeMap, -} - -impl CultNetSchemaRegistry { - pub fn new() -> Self { - Self::default() - } - - pub fn register(&mut self, registration: CultNetSchemaRegistration) -> Result<()> { - if registration.schema_id.trim().is_empty() { - return Err(anyhow!("schema_id must be non-empty")); - } - if registration.wire_contracts.is_empty() { - return Err(anyhow!("wire_contracts must contain at least one entry")); - } - - let content_hash = if let Some(schema_json) = registration.schema_json.as_deref() { - canonical_schema_hash(schema_json)? - } else { - return Err(anyhow!( - "schema_json is required so discovery can advertise canonical hashes" - )); - }; - - self.entries.insert( - registration.schema_id.clone(), - CultNetSchemaDescriptor { - schema_id: registration.schema_id, - kind: registration.kind, - schema_version: registration.schema_version, - document_type: registration.document_type, - title: registration.title, - wire_contracts: registration.wire_contracts, - content_hash, - schema_json: registration.schema_json, - }, - ); - - Ok(()) - } - - pub fn get( - &self, - schema_id: &str, - include_schema_json: bool, - ) -> Option { - self.entries - .get(schema_id) - .map(|entry| sanitize_descriptor(entry, include_schema_json)) - } - - pub fn list(&self, options: &CultNetSchemaCatalogOptions) -> Vec { - let requested_ids = options - .schema_ids - .as_ref() - .map(|values| values.iter().cloned().collect::>()); - let requested_kinds = options - .kinds - .as_ref() - .map(|values| values.iter().copied().collect::>()); - - self.entries - .values() - .filter(|entry| { - if let Some(requested_ids) = &requested_ids - && !requested_ids.contains(&entry.schema_id) - { - return false; - } - - if let Some(requested_kinds) = &requested_kinds - && !requested_kinds.contains(&entry.kind) - { - return false; - } - - true - }) - .map(|entry| sanitize_descriptor(entry, options.include_schema_json)) - .collect() - } - - pub fn create_catalog_response(&self, request: &CultNetMessage) -> Result { - let CultNetMessage::SchemaCatalogRequest { - message_id, - include_schema_json, - schema_ids, - kinds, - } = request - else { - return Err(anyhow!( - "expected cultnet.schema_catalog_request.v0 for schema discovery" - )); - }; - - Ok(CultNetMessage::SchemaCatalogResponse { - message_id: message_id.clone(), - schemas: self.list(&CultNetSchemaCatalogOptions { - include_schema_json: include_schema_json.unwrap_or(false), - schema_ids: schema_ids.clone(), - kinds: kinds.clone(), - }), - }) - } -} - -fn sanitize_descriptor( - descriptor: &CultNetSchemaDescriptor, - include_schema_json: bool, -) -> CultNetSchemaDescriptor { - let mut clone = descriptor.clone(); - if !include_schema_json { - clone.schema_json = None; - } - clone -} - -fn canonical_schema_hash(schema_json: &str) -> Result { - let value: Value = serde_json::from_str(schema_json)?; - let canonical = stable_stringify(&value); - Ok(format!("{:x}", Sha256::digest(canonical.as_bytes()))) -} - -fn stable_stringify(value: &Value) -> String { - match value { - Value::Null => "null".to_string(), - Value::Bool(value) => value.to_string(), - Value::Number(value) => value.to_string(), - Value::String(value) => serde_json::to_string(value).expect("string serialization"), - Value::Array(values) => format!( - "[{}]", - values - .iter() - .map(stable_stringify) - .collect::>() - .join(",") - ), - Value::Object(values) => { - let mut entries = values.iter().collect::>(); - entries.sort_by(|(left, _), (right, _)| left.cmp(right)); - format!( - "{{{}}}", - entries - .into_iter() - .map(|(key, value)| { - format!( - "{}:{}", - serde_json::to_string(key).expect("key serialization"), - stable_stringify(value) - ) - }) - .collect::>() - .join(",") - ) - } - } -} - -pub fn builtin_schema_registry() -> Result { - let mut registry = CultNetSchemaRegistry::new(); - - for registration in [ - schema_registration( - include_str!("../contracts/cultnet.hello.schema.json"), - CultNetSchemaKind::WireMessage, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.hello.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.document-mutation-contract.schema.json"), - CultNetSchemaKind::SharedContract, - vec![CultNetWireContract::CultNetSchemaV0], - None, - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.transport-profile.schema.json"), - CultNetSchemaKind::SharedContract, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.transport_profile.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.login.schema.json"), - CultNetSchemaKind::WireMessage, - vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - Some("cultnet.login.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.register.schema.json"), - CultNetSchemaKind::WireMessage, - vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - Some("cultnet.register.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.verify.schema.json"), - CultNetSchemaKind::WireMessage, - vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - Some("cultnet.verify.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.login-success.schema.json"), - CultNetSchemaKind::WireMessage, - vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - Some("cultnet.login_success.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.error.schema.json"), - CultNetSchemaKind::WireMessage, - vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - Some("cultnet.error.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.sample-change-name.schema.json"), - CultNetSchemaKind::WireMessage, - vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - Some("cultnet.sample.change_name.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.sample-chat.schema.json"), - CultNetSchemaKind::WireMessage, - vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - Some("cultnet.sample.chat.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.document-put.schema.json"), - CultNetSchemaKind::WireMessage, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.document_put.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.document-delete.schema.json"), - CultNetSchemaKind::WireMessage, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.document_delete.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.raw-document-record.schema.json"), - CultNetSchemaKind::SharedContract, - vec![CultNetWireContract::CultNetSchemaV0], - None, - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.document-put-raw.schema.json"), - CultNetSchemaKind::WireMessage, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.document_put_raw.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.snapshot-request.schema.json"), - CultNetSchemaKind::WireMessage, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.snapshot_request.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.snapshot-response.schema.json"), - CultNetSchemaKind::WireMessage, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.snapshot_response.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.snapshot-response-raw.schema.json"), - CultNetSchemaKind::WireMessage, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.snapshot_response_raw.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.schema-catalog-request.schema.json"), - CultNetSchemaKind::WireMessage, - vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - Some("cultnet.schema_catalog_request.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.schema-catalog-response.schema.json"), - CultNetSchemaKind::WireMessage, - vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - Some("cultnet.schema_catalog_response.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.shard-catalog-request.schema.json"), - CultNetSchemaKind::WireMessage, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.shard_catalog_request.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/cultnet.shard-catalog-response.schema.json"), - CultNetSchemaKind::WireMessage, - vec![CultNetWireContract::CultNetSchemaV0], - Some("cultnet.shard_catalog_response.v0"), - None, - )?, - schema_registration( - include_str!("../contracts/ghostlight.agent-state.schema.json"), - CultNetSchemaKind::DocumentPayload, - vec![CultNetWireContract::CultNetSchemaV0], - Some("ghostlight.agent_state.v0"), - Some("ghostlight.agent-state"), - )?, - ] { - registry.register(registration)?; - } - - Ok(registry) -} - -fn schema_registration( - schema_json: &str, - kind: CultNetSchemaKind, - wire_contracts: Vec, - schema_version: Option<&str>, - document_type: Option<&str>, -) -> Result { - let value: Value = serde_json::from_str(schema_json)?; - let schema_id = value - .get("$id") - .and_then(Value::as_str) - .ok_or_else(|| anyhow!("schema is missing $id"))?; - let title = value - .get("title") - .and_then(Value::as_str) - .map(str::to_string); - - Ok(CultNetSchemaRegistration { - schema_id: schema_id.to_string(), - kind, - wire_contracts, - schema_version: schema_version.map(str::to_string), - document_type: document_type.map(str::to_string), - title, - schema_json: Some(schema_json.to_string()), - }) -} diff --git a/vendor/cultnet-rs/src/security.rs b/vendor/cultnet-rs/src/security.rs deleted file mode 100644 index 30aa841..0000000 --- a/vendor/cultnet-rs/src/security.rs +++ /dev/null @@ -1,270 +0,0 @@ -use aes_gcm::Aes256Gcm; -use aes_gcm::KeyInit; -use aes_gcm::Nonce; -use aes_gcm::aead::Aead; -use aes_gcm::aead::Payload; -use anyhow::Result; -use anyhow::anyhow; -use base64::Engine; -use base64::engine::general_purpose::URL_SAFE_NO_PAD; -use chrono::DateTime; -use chrono::TimeZone; -use chrono::Utc; -use hmac::Hmac; -use hmac::Mac; -use rand::RngCore; -use sha2::Digest; -use sha2::Sha256; - -const NONCE_LENGTH: usize = 12; -type HmacSha256 = Hmac; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetClientSecurityOptions { - pub connection_key: String, - encryption_key: [u8; 32], -} - -impl CultNetClientSecurityOptions { - pub fn new(connection_key: impl Into) -> Result { - let connection_key = connection_key.into(); - if connection_key.trim().is_empty() { - return Err(anyhow!("Connection key must be provided")); - } - let encryption_key = sha256(connection_key.as_bytes()); - Ok(Self { - connection_key, - encryption_key, - }) - } - - pub fn development() -> Self { - Self::new("gamecult-dev-connection-key").expect("development key is valid") - } - - pub fn encryption_key(&self) -> [u8; 32] { - self.encryption_key - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetServerSecurityOptions { - client: CultNetClientSecurityOptions, - pub session_signing_secret: String, - session_signing_key: [u8; 32], - pub is_development: bool, -} - -impl CultNetServerSecurityOptions { - pub fn new( - connection_key: impl Into, - session_signing_secret: impl Into, - is_development: bool, - ) -> Result { - let session_signing_secret = session_signing_secret.into(); - if session_signing_secret.trim().is_empty() { - return Err(anyhow!("Session signing secret must be provided")); - } - Ok(Self { - client: CultNetClientSecurityOptions::new(connection_key)?, - session_signing_key: sha256(session_signing_secret.as_bytes()), - session_signing_secret, - is_development, - }) - } - - pub fn development() -> Self { - Self::new( - "gamecult-dev-connection-key", - "gamecult-dev-session-signing-secret", - true, - ) - .expect("development security config is valid") - } - - pub fn to_client_options(&self) -> CultNetClientSecurityOptions { - self.client.clone() - } - - pub fn encryption_key(&self) -> [u8; 32] { - self.client.encryption_key() - } - - pub fn session_signing_key(&self) -> [u8; 32] { - self.session_signing_key - } -} - -pub trait CultNetEncryptionOptions { - fn encryption_key(&self) -> [u8; 32]; -} - -impl CultNetEncryptionOptions for CultNetClientSecurityOptions { - fn encryption_key(&self) -> [u8; 32] { - self.encryption_key() - } -} - -impl CultNetEncryptionOptions for CultNetServerSecurityOptions { - fn encryption_key(&self) -> [u8; 32] { - self.encryption_key() - } -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ValidatedCultNetSessionToken { - pub user_id: String, - pub expires_at_utc: DateTime, -} - -pub struct CultNetSecret; - -impl CultNetSecret { - pub fn new_nonce() -> [u8; NONCE_LENGTH] { - let mut nonce = [0_u8; NONCE_LENGTH]; - rand::rng().fill_bytes(&mut nonce); - nonce - } - - pub fn encrypt_string( - input: Option<&str>, - nonce: &[u8], - options: &impl CultNetEncryptionOptions, - ) -> Result>> { - let Some(input) = input.filter(|value| !value.is_empty()) else { - return Ok(None); - }; - Ok(Some(Self::encrypt_bytes(input.as_bytes(), nonce, options)?)) - } - - pub fn decrypt_string( - encrypted: Option<&[u8]>, - nonce: Option<&[u8]>, - options: &impl CultNetEncryptionOptions, - ) -> Result> { - let (Some(encrypted), Some(nonce)) = (encrypted, nonce) else { - return Ok(None); - }; - let bytes = Self::decrypt_bytes(encrypted, nonce, options)?; - Ok(Some(String::from_utf8(bytes)?)) - } - - pub fn encrypt_bytes( - input: &[u8], - nonce: &[u8], - options: &impl CultNetEncryptionOptions, - ) -> Result> { - let nonce = validate_nonce(nonce)?; - let cipher = Aes256Gcm::new_from_slice(&options.encryption_key())?; - let encrypted = cipher - .encrypt( - Nonce::from_slice(nonce), - Payload { - msg: input, - aad: &[], - }, - ) - .map_err(|_| anyhow!("AES-GCM encryption failed"))?; - Ok(encrypted) - } - - pub fn decrypt_bytes( - encrypted: &[u8], - nonce: &[u8], - options: &impl CultNetEncryptionOptions, - ) -> Result> { - let nonce = validate_nonce(nonce)?; - let cipher = Aes256Gcm::new_from_slice(&options.encryption_key())?; - cipher - .decrypt( - Nonce::from_slice(nonce), - Payload { - msg: encrypted, - aad: &[], - }, - ) - .map_err(|_| anyhow!("AES-GCM decryption failed")) - } - - pub fn create_session_token( - user_id: &str, - expires_at_utc: DateTime, - options: &CultNetServerSecurityOptions, - ) -> Result { - let payload = format!("{}|{}", user_id, expires_at_utc.timestamp()); - let signature = hmac_sha256(&options.session_signing_key(), payload.as_bytes())?; - Ok(format!( - "{}.{}", - URL_SAFE_NO_PAD.encode(payload.as_bytes()), - URL_SAFE_NO_PAD.encode(signature) - )) - } - - pub fn try_validate_session_token( - token: Option<&str>, - options: &CultNetServerSecurityOptions, - ) -> Result> { - let Some(token) = token.filter(|value| !value.trim().is_empty()) else { - return Ok(None); - }; - let Some((payload_part, signature_part)) = token.split_once('.') else { - return Ok(None); - }; - let payload = URL_SAFE_NO_PAD.decode(payload_part)?; - let signature = URL_SAFE_NO_PAD.decode(signature_part)?; - let expected = hmac_sha256(&options.session_signing_key(), &payload)?; - if !constant_time_eq(&signature, &expected) { - return Ok(None); - } - let payload = String::from_utf8(payload)?; - let Some((user_id, expires_at)) = payload.split_once('|') else { - return Ok(None); - }; - let expires_at_seconds: i64 = expires_at.parse()?; - let Some(expires_at_utc) = Utc.timestamp_opt(expires_at_seconds, 0).single() else { - return Ok(None); - }; - if expires_at_utc <= Utc::now() { - return Ok(None); - } - Ok(Some(ValidatedCultNetSessionToken { - user_id: user_id.to_string(), - expires_at_utc, - })) - } - - pub fn to_base64_url(input: &[u8]) -> String { - URL_SAFE_NO_PAD.encode(input) - } - - pub fn from_base64_url(input: &str) -> Result> { - Ok(URL_SAFE_NO_PAD.decode(input)?) - } -} - -fn validate_nonce(nonce: &[u8]) -> Result<&[u8]> { - if nonce.len() != NONCE_LENGTH { - return Err(anyhow!("Invalid nonce")); - } - Ok(nonce) -} - -fn sha256(input: &[u8]) -> [u8; 32] { - Sha256::digest(input).into() -} - -fn hmac_sha256(key: &[u8], input: &[u8]) -> Result> { - let mut mac = ::new_from_slice(key)?; - mac.update(input); - Ok(mac.finalize().into_bytes().to_vec()) -} - -fn constant_time_eq(left: &[u8], right: &[u8]) -> bool { - if left.len() != right.len() { - return false; - } - left.iter() - .zip(right) - .fold(0_u8, |acc, (left, right)| acc | (left ^ right)) - == 0 -} diff --git a/vendor/cultnet-rs/src/service_identity.rs b/vendor/cultnet-rs/src/service_identity.rs deleted file mode 100644 index 9160b43..0000000 --- a/vendor/cultnet-rs/src/service_identity.rs +++ /dev/null @@ -1,705 +0,0 @@ -use anyhow::{Context, Result, anyhow, bail}; -use cultcache_rs::{CacheBackingStore, CultCacheEnvelope, SingleFileMessagePackBackingStore}; -use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey}; -use rand::RngCore; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use std::io::Write; -use std::marker::PhantomData; -use std::path::Path; - -pub const WINDOWS_SERVICE_IDENTITY_ASSURANCE: &str = "os_user_installation_bound_best_effort"; -pub const LINUX_SERVICE_IDENTITY_ASSURANCE: &str = "os_installation_file_bound_cloneable_baseline"; - -/// Fixes every authority-bearing name at the call site. A service cannot open -/// another service's identity by supplying a convenient runtime string. -pub trait ServiceIdentityProfile: Send + Sync + 'static { - const PRIVATE_TYPE: &'static str; - const PRIVATE_SCHEMA: &'static str; - const PRIVATE_KEY: &'static str; - const TRUST_ANCHOR_TYPE: &'static str; - const TRUST_ANCHOR_SCHEMA: &'static str; - const TRUST_ANCHOR_KEY: &'static str; - const ID_DOMAIN: &'static [u8]; - const SIGNATURE_DOMAIN: &'static [u8]; - const PROTECTOR_CONTEXT: &'static str; -} - -/// A purpose is a type, not caller-provided prose. Protocol implementations -/// define one zero-sized type per signed statement kind. -pub trait ServiceSignaturePurpose: Send + Sync + 'static { - const PURPOSE: &'static [u8]; -} - -pub enum IdunnServiceIdentity {} - -/// Root-operated identity dedicated to deployment-brake release grants. The -/// Idunn daemon receives only this profile's public trust anchor; opening the -/// private store is a provisioning operation, never daemon startup work. -pub enum IdunnDeploymentBrakeOperatorIdentity {} - -pub struct IdunnDeploymentBrakeReleasePurpose; - -impl ServiceSignaturePurpose - for IdunnDeploymentBrakeReleasePurpose -{ - const PURPOSE: &'static [u8] = b"idunn.deployment-release.v1"; -} - -impl ServiceIdentityProfile for IdunnDeploymentBrakeOperatorIdentity { - const PRIVATE_TYPE: &'static str = "idunn.deployment_brake_operator.private.v1"; - const PRIVATE_SCHEMA: &'static str = "idunn.deployment_brake_operator.private.v1"; - const PRIVATE_KEY: &'static str = "idunn-deployment-brake-operator"; - const TRUST_ANCHOR_TYPE: &'static str = "idunn.deployment_brake_operator.trust_anchor.v1"; - const TRUST_ANCHOR_SCHEMA: &'static str = "idunn.deployment_brake_operator.trust_anchor.v1"; - const TRUST_ANCHOR_KEY: &'static str = "idunn-deployment-brake-operator-public"; - const ID_DOMAIN: &'static [u8] = b"idunn.deployment-brake-operator.id.v1\0"; - const SIGNATURE_DOMAIN: &'static [u8] = b"idunn.deployment-brake-operator.signature.v1\0"; - const PROTECTOR_CONTEXT: &'static str = "idunn-deployment-brake-operator-v1"; -} - -/// Dedicated provider-owned identity for generic daemon-health statements. -/// It is intentionally independent of any one provider runtime or repository. -pub enum GameCultProviderHealthIdentity {} - -/// The only generic health statement purpose accepted from provider-health -/// identities by Idunn admission. -pub struct IdunnSignedDaemonHealthPurpose; - -impl ServiceSignaturePurpose for IdunnSignedDaemonHealthPurpose { - const PURPOSE: &'static [u8] = b"idunn.signed_daemon_health.v1"; -} - -impl ServiceIdentityProfile for GameCultProviderHealthIdentity { - const PRIVATE_TYPE: &'static str = "gamecult.provider_health_identity.private.v1"; - const PRIVATE_SCHEMA: &'static str = "gamecult.provider_health_identity.private.v1"; - const PRIVATE_KEY: &'static str = "gamecult-provider-health-identity"; - const TRUST_ANCHOR_TYPE: &'static str = "gamecult.provider_health_identity.trust_anchor.v1"; - const TRUST_ANCHOR_SCHEMA: &'static str = "gamecult.provider_health_identity.trust_anchor.v1"; - const TRUST_ANCHOR_KEY: &'static str = "gamecult-provider-health-identity-public"; - const ID_DOMAIN: &'static [u8] = b"gamecult.provider-health.identity.v1\0"; - const SIGNATURE_DOMAIN: &'static [u8] = b"gamecult.provider-health.signature.v1\0"; - const PROTECTOR_CONTEXT: &'static str = "gamecult-provider-health-identity-v1"; -} - -/// The only signing purpose accepted for Idunn's public projection of a -/// provider-authenticated health admission. Keeping this profile here makes -/// purpose selection a compile-time protocol choice rather than caller text. -pub struct IdunnAuthenticatedProviderHealthProjectionPurpose; - -impl ServiceSignaturePurpose - for IdunnAuthenticatedProviderHealthProjectionPurpose -{ - const PURPOSE: &'static [u8] = b"idunn.authenticated-provider-health-projection.v1"; -} - -impl ServiceIdentityProfile for IdunnServiceIdentity { - const PRIVATE_TYPE: &'static str = "idunn.service_identity.private.v1"; - const PRIVATE_SCHEMA: &'static str = "idunn.service_identity.private.v1"; - const PRIVATE_KEY: &'static str = "idunn-service-identity"; - const TRUST_ANCHOR_TYPE: &'static str = "idunn.service_identity.trust_anchor.v1"; - const TRUST_ANCHOR_SCHEMA: &'static str = "idunn.service_identity.trust_anchor.v1"; - const TRUST_ANCHOR_KEY: &'static str = "idunn-service-identity-public"; - const ID_DOMAIN: &'static [u8] = b"idunn.service-identity.id.v1\0"; - const SIGNATURE_DOMAIN: &'static [u8] = b"idunn.service-identity.signature.v1\0"; - const PROTECTOR_CONTEXT: &'static str = "idunn-service-identity-v1"; -} - -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] -pub struct ServiceIdentityPrivateEntry { - pub schema_version: String, - pub identity_id: String, - pub public_key: Vec, - pub protected_private_seed: Vec, - pub protector_kind: String, - pub protector_binding: String, - pub protector_version: String, - pub assurance: String, - pub created_at: String, - pub enrollment_nonce: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct ServiceIdentityTrustAnchor { - pub schema_version: String, - pub identity_id: String, - pub public_key: Vec, - pub assurance: String, - pub identity_created_at: String, - pub source_identity_record_sha256: String, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct ServiceIdentitySignature { - pub identity_id: String, - pub signature: Vec, -} - -/// The only value holding unprotected key material. It exposes signing, never -/// seed export or access to the underlying signing key. -pub struct ServiceIdentitySigner { - entry: ServiceIdentityPrivateEntry, - signing_key: SigningKey, - profile: PhantomData

, -} - -impl ServiceIdentitySigner

{ - pub fn entry(&self) -> &ServiceIdentityPrivateEntry { - &self.entry - } - - pub fn trust_anchor(&self) -> Result { - Ok(ServiceIdentityTrustAnchor { - schema_version: P::TRUST_ANCHOR_SCHEMA.into(), - identity_id: self.entry.identity_id.clone(), - public_key: self.entry.public_key.clone(), - assurance: self.entry.assurance.clone(), - identity_created_at: self.entry.created_at.clone(), - source_identity_record_sha256: format!( - "sha256-{}", - hex(&Sha256::digest(rmp_serde::to_vec(&self.entry)?)) - ), - }) - } - - pub fn sign>(&self, payload: &[u8]) -> ServiceIdentitySignature { - ServiceIdentitySignature { - identity_id: self.entry.identity_id.clone(), - signature: self - .signing_key - .sign(&signing_message::(payload)) - .to_bytes() - .to_vec(), - } - } -} - -pub fn enroll_service_identity_at( - path: &Path, -) -> Result> { - if path.exists() { - bail!( - "service identity store {} already exists; enrollment is immutable", - path.display() - ); - } - prepare_parent(path)?; - let mut seed = [0u8; 32]; - rand::rng().fill_bytes(&mut seed); - let signing_key = SigningKey::from_bytes(&seed); - let mut nonce = [0u8; 32]; - rand::rng().fill_bytes(&mut nonce); - let public_key = signing_key.verifying_key().to_bytes(); - let binding = platform_binding::

()?; - let entry = ServiceIdentityPrivateEntry { - schema_version: P::PRIVATE_SCHEMA.into(), - identity_id: identity_id::

(&public_key), - public_key: public_key.to_vec(), - protected_private_seed: protect_seed::

(&seed, &binding)?, - protector_kind: platform_protector_kind().into(), - protector_binding: binding, - protector_version: "v1".into(), - assurance: platform_assurance().into(), - created_at: chrono::Utc::now().to_rfc3339(), - enrollment_nonce: nonce.to_vec(), - }; - validate_private::

(&entry)?; - let envelope = private_envelope::

(&entry)?; - atomic_create_private_store(path, &envelope)?; - harden_store_permissions(path)?; - open_service_identity_at::

(path) -} - -pub fn open_service_identity_at( - path: &Path, -) -> Result> { - if !path.is_file() { - bail!("service identity store {} does not exist", path.display()); - } - let entries = SingleFileMessagePackBackingStore::new(path).pull_all()?; - if entries.len() != 1 { - bail!("service identity store must contain exactly one immutable envelope"); - } - let envelope = &entries[0]; - if envelope.r#type != P::PRIVATE_TYPE - || envelope.key != P::PRIVATE_KEY - || envelope.schema_id.as_deref() != Some(P::PRIVATE_SCHEMA) - { - bail!("service identity store belongs to a different profile or schema"); - } - let entry: ServiceIdentityPrivateEntry = rmp_serde::from_slice(&envelope.payload) - .context("service identity private payload is malformed MessagePack")?; - validate_private::

(&entry)?; - if entry.protector_kind != platform_protector_kind() || entry.assurance != platform_assurance() - { - bail!("service identity protector does not belong to this platform implementation"); - } - let binding = platform_binding::

()?; - if entry.protector_binding != binding { - bail!("service identity protector binding does not match this OS installation or profile"); - } - let seed: [u8; 32] = unprotect_seed::

(&entry.protected_private_seed, &binding)? - .try_into() - .map_err(|_| anyhow!("unprotected service identity seed has invalid length"))?; - let signing_key = SigningKey::from_bytes(&seed); - if signing_key.verifying_key().to_bytes().as_slice() != entry.public_key.as_slice() { - bail!("service identity private seed does not match enrolled public key"); - } - Ok(ServiceIdentitySigner { - entry, - signing_key, - profile: PhantomData, - }) -} - -pub fn export_service_identity_trust_anchor( - signer: &ServiceIdentitySigner

, - path: &Path, -) -> Result { - let anchor = signer.trust_anchor()?; - prepare_parent(path)?; - let envelope = CultCacheEnvelope { - key: P::TRUST_ANCHOR_KEY.into(), - r#type: P::TRUST_ANCHOR_TYPE.into(), - payload: rmp_serde::to_vec(&anchor)?, - stored_at: anchor.identity_created_at.clone(), - schema_id: Some(P::TRUST_ANCHOR_SCHEMA.into()), - }; - let mut backing = SingleFileMessagePackBackingStore::new(path); - match backing.pull_all()?.as_slice() { - [] => backing.push(&envelope)?, - [current] if current == &envelope => {} - _ => bail!("service identity trust anchor output already contains different state"), - } - Ok(anchor) -} - -pub fn verify_service_identity_signature( - anchor: &ServiceIdentityTrustAnchor, - payload: &[u8], - proof: &ServiceIdentitySignature, -) -> Result<()> -where - P: ServiceIdentityProfile, - S: ServiceSignaturePurpose

, -{ - validate_anchor::

(anchor)?; - if proof.identity_id != anchor.identity_id { - bail!("service identity signature names a different identity"); - } - let key: [u8; 32] = anchor - .public_key - .as_slice() - .try_into() - .map_err(|_| anyhow!("service identity public key has invalid length"))?; - let signature = Signature::from_slice(&proof.signature) - .map_err(|_| anyhow!("service identity signature has invalid length"))?; - VerifyingKey::from_bytes(&key)? - .verify(&signing_message::(payload), &signature) - .map_err(|_| anyhow!("service identity signature verification failed")) -} - -/// Verify against a public key already admitted by an owning trust store. -/// The profile derives the identity, so callers cannot substitute a convenient -/// identity string while retaining the same signature bytes. -pub fn verify_service_identity_signature_with_public_key( - public_key: &[u8], - payload: &[u8], - proof: &ServiceIdentitySignature, -) -> Result<()> -where - P: ServiceIdentityProfile, - S: ServiceSignaturePurpose

, -{ - if proof.identity_id != derive_service_identity_id::

(public_key)? { - bail!("service identity signature names a different identity"); - } - let key: [u8; 32] = public_key - .try_into() - .map_err(|_| anyhow!("service identity public key has invalid length"))?; - let signature = Signature::from_slice(&proof.signature) - .map_err(|_| anyhow!("service identity signature has invalid length"))?; - VerifyingKey::from_bytes(&key)? - .verify(&signing_message::(payload), &signature) - .map_err(|_| anyhow!("service identity signature verification failed")) -} - -fn validate_private(entry: &ServiceIdentityPrivateEntry) -> Result<()> { - if entry.schema_version != P::PRIVATE_SCHEMA - || entry.public_key.len() != 32 - || entry.enrollment_nonce.len() != 32 - || entry.protected_private_seed.is_empty() - || entry.protector_version != "v1" - { - bail!("service identity private entry violates its profile schema"); - } - chrono::DateTime::parse_from_rfc3339(&entry.created_at) - .map_err(|_| anyhow!("service identity created_at is not RFC3339"))?; - if identity_id::

(&entry.public_key) != entry.identity_id { - bail!("service identity id does not match public key"); - } - Ok(()) -} - -fn validate_anchor(anchor: &ServiceIdentityTrustAnchor) -> Result<()> { - if anchor.schema_version != P::TRUST_ANCHOR_SCHEMA - || anchor.public_key.len() != 32 - || derive_service_identity_id::

(&anchor.public_key)? != anchor.identity_id - { - bail!("service identity trust anchor violates its profile schema"); - } - Ok(()) -} - -/// Derives the profile-bound identity id from an exact Ed25519 public key. -/// Consumers use this instead of accepting a caller-supplied identity string. -pub fn derive_service_identity_id(public_key: &[u8]) -> Result { - if public_key.len() != 32 { - bail!("service identity public key has invalid length"); - } - Ok(hex(&Sha256::digest([P::ID_DOMAIN, public_key].concat()))) -} - -fn identity_id(key: &[u8]) -> String { - derive_service_identity_id::

(key).expect("internal service identity key is Ed25519") -} - -fn signing_message>( - payload: &[u8], -) -> Vec { - let mut out = - Vec::with_capacity(P::SIGNATURE_DOMAIN.len() + S::PURPOSE.len() + payload.len() + 16); - out.extend_from_slice(P::SIGNATURE_DOMAIN); - out.extend_from_slice(&(S::PURPOSE.len() as u64).to_be_bytes()); - out.extend_from_slice(S::PURPOSE); - out.extend_from_slice(&(payload.len() as u64).to_be_bytes()); - out.extend_from_slice(payload); - out -} - -fn private_envelope( - entry: &ServiceIdentityPrivateEntry, -) -> Result { - Ok(CultCacheEnvelope { - key: P::PRIVATE_KEY.into(), - r#type: P::PRIVATE_TYPE.into(), - payload: rmp_serde::to_vec(entry)?, - stored_at: entry.created_at.clone(), - schema_id: Some(P::PRIVATE_SCHEMA.into()), - }) -} - -fn prepare_parent(path: &Path) -> Result<()> { - let parent = path - .parent() - .ok_or_else(|| anyhow!("service identity path has no parent"))?; - std::fs::create_dir_all(parent)?; - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))?; - } - Ok(()) -} - -/// Enrollment is a one-shot authority transition. `create_new` makes two -/// simultaneous enrollments unable to overwrite one another, while the encoded -/// envelope vector remains a supported CultCache single-file representation. -fn atomic_create_private_store(path: &Path, envelope: &CultCacheEnvelope) -> Result<()> { - let bytes = rmp_serde::to_vec(&vec![envelope.clone()]) - .context("failed to encode immutable service identity store")?; - let mut file = std::fs::OpenOptions::new() - .create_new(true) - .write(true) - .open(path) - .with_context(|| { - format!( - "service identity store {} already exists or cannot be created; enrollment is immutable", - path.display() - ) - })?; - if let Err(error) = file.write_all(&bytes).and_then(|()| file.sync_all()) { - drop(file); - let _ = std::fs::remove_file(path); - return Err(error).context("failed to persist immutable service identity store"); - } - Ok(()) -} -fn harden_store_permissions(_path: &Path) -> Result<()> { - #[cfg(unix)] - { - use std::os::unix::fs::PermissionsExt; - std::fs::set_permissions(_path, std::fs::Permissions::from_mode(0o600))?; - } - Ok(()) -} - -#[cfg(windows)] -fn platform_protector_kind() -> &'static str { - "windows_dpapi_current_user" -} -#[cfg(target_os = "linux")] -fn platform_protector_kind() -> &'static str { - "linux_file_mode_machine_id_binding" -} -#[cfg(windows)] -fn platform_assurance() -> &'static str { - WINDOWS_SERVICE_IDENTITY_ASSURANCE -} -#[cfg(target_os = "linux")] -fn platform_assurance() -> &'static str { - LINUX_SERVICE_IDENTITY_ASSURANCE -} - -#[cfg(windows)] -fn platform_binding() -> Result { - Ok(format!("dpapi-current-user:{}", P::PROTECTOR_CONTEXT)) -} -#[cfg(target_os = "linux")] -fn platform_binding() -> Result { - let raw = std::fs::read_to_string("/etc/machine-id") - .or_else(|_| std::fs::read_to_string("/var/lib/dbus/machine-id")) - .context("Linux machine-id is unavailable")?; - let id = raw.trim(); - if id.is_empty() { - bail!("Linux machine-id is empty"); - } - Ok(format!( - "{}:machine-id-sha256:{}", - P::PROTECTOR_CONTEXT, - hex(&Sha256::digest(id.as_bytes())) - )) -} - -#[cfg(windows)] -fn protect_seed(seed: &[u8; 32], binding: &str) -> Result> { - dpapi::

(seed, binding, true) -} -#[cfg(windows)] -fn unprotect_seed(seed: &[u8], binding: &str) -> Result> { - dpapi::

(seed, binding, false) -} -#[cfg(windows)] -fn dpapi(input: &[u8], binding: &str, protect: bool) -> Result> { - use windows_sys::Win32::Foundation::LocalFree; - use windows_sys::Win32::Security::Cryptography::{ - CRYPT_INTEGER_BLOB, CRYPTPROTECT_UI_FORBIDDEN, CryptProtectData, CryptUnprotectData, - }; - let mut input = input.to_vec(); - let mut entropy = Sha256::digest( - [ - b"gamecult-service-identity-dpapi-v1\0".as_slice(), - P::PROTECTOR_CONTEXT.as_bytes(), - binding.as_bytes(), - ] - .concat(), - ) - .to_vec(); - let ib = CRYPT_INTEGER_BLOB { - cbData: input.len() as u32, - pbData: input.as_mut_ptr(), - }; - let eb = CRYPT_INTEGER_BLOB { - cbData: entropy.len() as u32, - pbData: entropy.as_mut_ptr(), - }; - let mut output = CRYPT_INTEGER_BLOB { - cbData: 0, - pbData: std::ptr::null_mut(), - }; - let ok = unsafe { - if protect { - CryptProtectData( - &ib, - std::ptr::null(), - &eb, - std::ptr::null(), - std::ptr::null(), - CRYPTPROTECT_UI_FORBIDDEN, - &mut output, - ) - } else { - CryptUnprotectData( - &ib, - std::ptr::null_mut(), - &eb, - std::ptr::null(), - std::ptr::null(), - CRYPTPROTECT_UI_FORBIDDEN, - &mut output, - ) - } - }; - if ok == 0 { - return Err(std::io::Error::last_os_error()) - .context("DPAPI service identity operation failed"); - } - let bytes = - unsafe { std::slice::from_raw_parts(output.pbData, output.cbData as usize).to_vec() }; - unsafe { - LocalFree(output.pbData.cast()); - } - Ok(bytes) -} - -#[cfg(target_os = "linux")] -fn protect_seed(seed: &[u8; 32], binding: &str) -> Result> { - xor_linux::

(seed, binding) -} -#[cfg(target_os = "linux")] -fn unprotect_seed(seed: &[u8], binding: &str) -> Result> { - if seed.len() != 32 { - bail!("protected Linux service seed has invalid length"); - } - xor_linux::

(seed, binding) -} -#[cfg(target_os = "linux")] -fn xor_linux(seed: &[u8], binding: &str) -> Result> { - let mask = Sha256::digest( - [ - b"gamecult-linux-service-seed-v1\0".as_slice(), - P::PROTECTOR_CONTEXT.as_bytes(), - binding.as_bytes(), - ] - .concat(), - ); - Ok(seed.iter().zip(mask).map(|(a, b)| a ^ b).collect()) -} - -fn hex(bytes: &[u8]) -> String { - const D: &[u8; 16] = b"0123456789abcdef"; - let mut s = String::with_capacity(bytes.len() * 2); - for b in bytes { - s.push(D[(b >> 4) as usize] as char); - s.push(D[(b & 15) as usize] as char); - } - s -} - -#[cfg(test)] -mod tests { - use super::*; - enum OtherPurpose {} - impl ServiceSignaturePurpose for OtherPurpose { - const PURPOSE: &'static [u8] = b"idunn.other.v1"; - } - - #[test] - fn immutable_enrollment_reopens_and_is_purpose_and_payload_bound() -> Result<()> { - let temp = tempfile::tempdir()?; - let path = temp.path().join("idunn.ccmp"); - let signer = enroll_service_identity_at::(&path)?; - let proof = signer.sign::(b"healthy"); - let anchor = signer.trust_anchor()?; - verify_service_identity_signature::< - IdunnServiceIdentity, - IdunnAuthenticatedProviderHealthProjectionPurpose, - >(&anchor, b"healthy", &proof)?; - assert!( - verify_service_identity_signature::< - IdunnServiceIdentity, - IdunnAuthenticatedProviderHealthProjectionPurpose, - >(&anchor, b"unhealthy", &proof) - .is_err() - ); - assert!( - verify_service_identity_signature::( - &anchor, b"healthy", &proof - ) - .is_err() - ); - assert!(enroll_service_identity_at::(&path).is_err()); - assert_eq!( - derive_service_identity_id::(&anchor.public_key)?, - anchor.identity_id - ); - assert!(derive_service_identity_id::(&[0; 31]).is_err()); - assert_eq!( - open_service_identity_at::(&path)?.entry(), - signer.entry() - ); - Ok(()) - } - - #[test] - fn malformed_or_profile_substituted_state_fails_closed_without_replacement() -> Result<()> { - enum Alien {} - impl ServiceIdentityProfile for Alien { - const PRIVATE_TYPE: &'static str = "alien.private.v1"; - const PRIVATE_SCHEMA: &'static str = "alien.private.v1"; - const PRIVATE_KEY: &'static str = "alien"; - const TRUST_ANCHOR_TYPE: &'static str = "alien.anchor.v1"; - const TRUST_ANCHOR_SCHEMA: &'static str = "alien.anchor.v1"; - const TRUST_ANCHOR_KEY: &'static str = "alien-public"; - const ID_DOMAIN: &'static [u8] = b"alien.id\0"; - const SIGNATURE_DOMAIN: &'static [u8] = b"alien.sig\0"; - const PROTECTOR_CONTEXT: &'static str = "alien-v1"; - } - let temp = tempfile::tempdir()?; - let malformed = temp.path().join("bad.ccmp"); - std::fs::write(&malformed, b"rot")?; - let before = std::fs::read(&malformed)?; - assert!(open_service_identity_at::(&malformed).is_err()); - assert!(enroll_service_identity_at::(&malformed).is_err()); - assert_eq!(before, std::fs::read(&malformed)?); - let good = temp.path().join("good.ccmp"); - enroll_service_identity_at::(&good)?; - assert!(open_service_identity_at::(&good).is_err()); - Ok(()) - } - - #[test] - fn exported_anchor_is_public_only_immutable_and_rejects_identity_substitution() -> Result<()> { - let temp = tempfile::tempdir()?; - let signer = - enroll_service_identity_at::(&temp.path().join("private.ccmp"))?; - let output = temp.path().join("public.ccmp"); - let anchor = export_service_identity_trust_anchor(&signer, &output)?; - let bytes = std::fs::read(&output)?; - assert!( - !bytes - .windows(signer.entry().protected_private_seed.len()) - .any(|w| w == signer.entry().protected_private_seed) - ); - export_service_identity_trust_anchor(&signer, &output)?; - assert_eq!(bytes, std::fs::read(&output)?); - let other = - enroll_service_identity_at::(&temp.path().join("other.ccmp"))?; - let proof = other.sign::(b"healthy"); - assert!( - verify_service_identity_signature::< - IdunnServiceIdentity, - IdunnAuthenticatedProviderHealthProjectionPurpose, - >(&anchor, b"healthy", &proof) - .is_err() - ); - Ok(()) - } - - #[test] - fn provider_health_profile_is_domain_separated_and_purpose_bound() -> Result<()> { - let temp = tempfile::tempdir()?; - let signer = enroll_service_identity_at::( - &temp.path().join("provider-health.cc"), - )?; - let proof = signer.sign::(b"signed-health-record"); - let anchor = signer.trust_anchor()?; - verify_service_identity_signature::< - GameCultProviderHealthIdentity, - IdunnSignedDaemonHealthPurpose, - >(&anchor, b"signed-health-record", &proof)?; - verify_service_identity_signature_with_public_key::< - GameCultProviderHealthIdentity, - IdunnSignedDaemonHealthPurpose, - >(&anchor.public_key, b"signed-health-record", &proof)?; - assert_eq!( - anchor.schema_version, - "gamecult.provider_health_identity.trust_anchor.v1" - ); - assert_ne!( - derive_service_identity_id::(&anchor.public_key)?, - derive_service_identity_id::(&anchor.public_key)? - ); - Ok(()) - } -} diff --git a/vendor/cultnet-rs/src/shard_catalog.rs b/vendor/cultnet-rs/src/shard_catalog.rs deleted file mode 100644 index a112246..0000000 --- a/vendor/cultnet-rs/src/shard_catalog.rs +++ /dev/null @@ -1,134 +0,0 @@ -use anyhow::Result; -use anyhow::anyhow; -use std::collections::BTreeMap; -use std::collections::BTreeSet; - -use crate::CultNetMessage; -use crate::CultNetShardDescriptor; - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct CultNetShardCatalogOptions { - pub schema_ids: Option>, - pub record_keys: Option>, -} - -#[derive(Clone, Debug, Default)] -pub struct CultNetShardCatalog { - shards: BTreeMap, -} - -impl CultNetShardCatalog { - pub fn new() -> Self { - Self::default() - } - - pub fn upsert(&mut self, descriptor: CultNetShardDescriptor) -> Result<()> { - validate_descriptor(&descriptor)?; - self.shards.insert(descriptor.shard_id.clone(), descriptor); - Ok(()) - } - - pub fn get(&self, shard_id: &str) -> Option<&CultNetShardDescriptor> { - self.shards.get(shard_id) - } - - pub fn list(&self, options: &CultNetShardCatalogOptions) -> Vec { - let schema_ids = options - .schema_ids - .as_ref() - .map(|values| values.iter().cloned().collect::>()); - let record_keys = options - .record_keys - .as_ref() - .map(|values| values.iter().cloned().collect::>()); - - self.shards - .values() - .filter(|shard| { - let schema_match = schema_ids.as_ref().is_none_or(|requested| { - requested - .iter() - .any(|schema_id| shard.serves(Some(schema_id), None)) - }); - let key_match = record_keys.as_ref().is_none_or(|requested| { - requested - .iter() - .any(|record_key| shard.serves(None, Some(record_key))) - }); - schema_match && key_match - }) - .cloned() - .collect() - } - - pub fn create_catalog_response(&self, request: &CultNetMessage) -> Result { - let CultNetMessage::ShardCatalogRequest { - message_id, - schema_ids, - record_keys, - } = request - else { - return Err(anyhow!( - "expected cultnet.shard_catalog_request.v0 for shard discovery" - )); - }; - - Ok(CultNetMessage::ShardCatalogResponse { - message_id: message_id.clone(), - shards: self.list(&CultNetShardCatalogOptions { - schema_ids: schema_ids.clone(), - record_keys: record_keys.clone(), - }), - }) - } - - pub fn apply_response( - &mut self, - response: &CultNetMessage, - ) -> Result> { - let CultNetMessage::ShardCatalogResponse { shards, .. } = response else { - return Err(anyhow!( - "expected cultnet.shard_catalog_response.v0 for shard discovery" - )); - }; - - let mut applied = Vec::with_capacity(shards.len()); - for shard in shards { - self.upsert(shard.clone())?; - applied.push(shard.clone()); - } - Ok(applied) - } -} - -fn validate_descriptor(descriptor: &CultNetShardDescriptor) -> Result<()> { - if descriptor.shard_id.trim().is_empty() { - return Err(anyhow!("shard_id must be non-empty")); - } - if descriptor.owner_runtime_id.trim().is_empty() { - return Err(anyhow!("owner_runtime_id must be non-empty")); - } - for schema_id in &descriptor.schema_ids { - if schema_id.trim().is_empty() { - return Err(anyhow!("schema_ids must not contain empty values")); - } - } - if descriptor - .key_prefix - .as_deref() - .is_some_and(|value| value.trim().is_empty()) - { - return Err(anyhow!("key_prefix must be non-empty when present")); - } - for endpoint in descriptor - .primary_endpoints - .iter() - .chain(descriptor.replica_endpoints.iter()) - .chain(descriptor.read_replica_endpoints.iter()) - { - if endpoint.trim().is_empty() { - return Err(anyhow!("shard endpoints must not contain empty values")); - } - } - Ok(()) -} diff --git a/vendor/cultnet-rs/src/transport.rs b/vendor/cultnet-rs/src/transport.rs deleted file mode 100644 index 7bb203c..0000000 --- a/vendor/cultnet-rs/src/transport.rs +++ /dev/null @@ -1,288 +0,0 @@ -use anyhow::Result; -use anyhow::anyhow; -use std::io::Read; -use std::io::Write; - -use crate::CultNetReconnectPolicy; -use crate::CultNetTransportChannel; -use crate::CultNetTransportDelivery; -use crate::CultNetTransportDescriptor; -use crate::CultNetTransportOrdering; -use crate::CultNetTransportProfile; -use crate::CultNetTransportProtocol; -use crate::FRAME_HEADER_BYTES; -use crate::encode_frame; - -#[derive(Clone, Debug, Default, PartialEq, Eq)] -pub struct CultNetTransportStats { - pub bytes_received: u64, - pub bytes_sent: u64, - pub frames_received: u64, - pub frames_sent: u64, -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetTransportFrame { - pub channel_id: String, - pub payload: Vec, -} - -#[derive(Clone, Debug)] -pub struct CultNetReconnectPolicyOptions { - pub policy_id: String, - pub base_delay_ms: u64, - pub max_delay_ms: u64, - pub max_jitter_ms: u64, - pub max_attempts: Option, -} - -impl Default for CultNetReconnectPolicyOptions { - fn default() -> Self { - Self { - policy_id: "default".to_string(), - base_delay_ms: 1_000, - max_delay_ms: 30_000, - max_jitter_ms: 250, - max_attempts: None, - } - } -} - -pub fn create_reconnect_policy(options: CultNetReconnectPolicyOptions) -> CultNetReconnectPolicy { - CultNetReconnectPolicy { - schema_version: "cultnet.reconnect_policy.v0".to_string(), - policy_id: if options.policy_id.trim().is_empty() { - "default".to_string() - } else { - options.policy_id - }, - base_delay_ms: options.base_delay_ms, - max_delay_ms: options.max_delay_ms, - max_jitter_ms: options.max_jitter_ms, - max_attempts: options.max_attempts, - } -} - -pub fn compute_reconnect_delay_ms( - policy: &CultNetReconnectPolicy, - attempt: u32, - jitter_ms: u64, -) -> u64 { - let normalized_attempt = attempt.max(1); - let multiplier = 2_u64.saturating_pow(normalized_attempt.saturating_sub(1)); - let capped_base_delay = policy - .base_delay_ms - .saturating_mul(multiplier) - .min(policy.max_delay_ms); - capped_base_delay + jitter_ms.min(policy.max_jitter_ms) -} - -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct CultNetReconnectDecision { - pub attempt: u32, - pub should_retry: bool, - pub delay_ms: u64, - pub next_attempt_at_ms: Option, - pub exhausted: bool, -} - -#[derive(Clone, Debug)] -pub struct CultNetReconnectController { - pub policy: CultNetReconnectPolicy, - attempt: u32, - next_attempt_at_ms: Option, - exhausted: bool, -} - -impl Default for CultNetReconnectController { - fn default() -> Self { - Self::new(create_reconnect_policy(Default::default())) - } -} - -impl CultNetReconnectController { - pub fn new(policy: CultNetReconnectPolicy) -> Self { - Self { - policy, - attempt: 0, - next_attempt_at_ms: None, - exhausted: false, - } - } - - pub fn attempt(&self) -> u32 { - self.attempt - } - - pub fn next_attempt_at_ms(&self) -> Option { - self.next_attempt_at_ms - } - - pub fn exhausted(&self) -> bool { - self.exhausted - } - - pub fn reset(&mut self) { - self.attempt = 0; - self.next_attempt_at_ms = None; - self.exhausted = false; - } - - pub fn can_attempt(&self, now_ms: u64) -> bool { - !self.exhausted - && self - .next_attempt_at_ms - .is_none_or(|next_attempt_at_ms| now_ms >= next_attempt_at_ms) - } - - pub fn record_failure(&mut self, now_ms: u64, jitter_ms: u64) -> CultNetReconnectDecision { - let next_attempt = self.attempt.saturating_add(1); - if self - .policy - .max_attempts - .is_some_and(|max_attempts| next_attempt > max_attempts) - { - self.exhausted = true; - self.next_attempt_at_ms = None; - return CultNetReconnectDecision { - attempt: self.attempt, - should_retry: false, - delay_ms: 0, - next_attempt_at_ms: None, - exhausted: true, - }; - } - - self.attempt = next_attempt; - let delay_ms = compute_reconnect_delay_ms(&self.policy, self.attempt, jitter_ms); - let next_attempt_at_ms = now_ms.saturating_add(delay_ms); - self.next_attempt_at_ms = Some(next_attempt_at_ms); - CultNetReconnectDecision { - attempt: self.attempt, - should_retry: true, - delay_ms, - next_attempt_at_ms: Some(next_attempt_at_ms), - exhausted: false, - } - } -} - -#[derive(Clone, Debug, Default)] -pub struct TcpFramedTransportProfileOptions { - pub transport_id: Option, - pub host: Option, - pub port: Option, - pub max_payload_bytes: Option, - pub max_fragment_bytes: Option, -} - -pub fn create_tcp_framed_transport_profile( - runtime_id: impl Into, - options: TcpFramedTransportProfileOptions, -) -> CultNetTransportProfile { - CultNetTransportProfile { - schema_version: "cultnet.transport_profile.v0".to_string(), - runtime_id: runtime_id.into(), - transports: vec![CultNetTransportDescriptor { - transport_id: options - .transport_id - .unwrap_or_else(|| "tcp-framed".to_string()), - protocol: CultNetTransportProtocol::TcpFramed, - host: options.host, - port: options.port, - path: None, - discovery_group: None, - wire_contracts: Some(vec!["cultnet.schema.v0".to_string()]), - reconnect_policy: None, - channels: vec![CultNetTransportChannel { - channel_id: "schema".to_string(), - delivery: CultNetTransportDelivery::Reliable, - ordering: CultNetTransportOrdering::Ordered, - max_payload_bytes: options.max_payload_bytes, - max_fragment_bytes: options.max_fragment_bytes, - max_pending_reliable_packets: None, - }], - }], - } -} - -pub struct TcpFramedTransportConnection { - stream: TStream, - pub profile: CultNetTransportProfile, - stats: CultNetTransportStats, -} - -impl TcpFramedTransportConnection { - pub fn new(stream: TStream, profile: CultNetTransportProfile) -> Self { - Self { - stream, - profile, - stats: CultNetTransportStats::default(), - } - } - - pub fn stats(&self) -> CultNetTransportStats { - self.stats.clone() - } - - pub fn into_inner(self) -> TStream { - self.stream - } -} - -impl TcpFramedTransportConnection -where - TStream: Write, -{ - pub fn send(&mut self, channel_id: &str, payload: &[u8]) -> Result<()> { - if channel_id != "schema" { - return Err(anyhow!( - "tcp_framed transport only supports the schema channel, got {channel_id:?}" - )); - } - - let frame = encode_frame(payload)?; - self.stream.write_all(&frame)?; - self.stream.flush()?; - self.stats.bytes_sent += frame.len() as u64; - self.stats.frames_sent += 1; - Ok(()) - } -} - -impl TcpFramedTransportConnection -where - TStream: Read, -{ - pub fn receive(&mut self) -> Result { - let mut header = [0_u8; FRAME_HEADER_BYTES]; - self.stream.read_exact(&mut header)?; - let payload_len = u32::from_be_bytes(header) as usize; - if let Some(max_payload_bytes) = self - .profile - .transports - .iter() - .find(|transport| transport.protocol == CultNetTransportProtocol::TcpFramed) - .and_then(|transport| { - transport - .channels - .iter() - .find(|channel| channel.channel_id == "schema") - }) - .and_then(|channel| channel.max_payload_bytes) - && payload_len > max_payload_bytes as usize - { - return Err(anyhow!( - "tcp_framed payload length {payload_len} exceeds advertised maximum {max_payload_bytes}" - )); - } - let mut payload = vec![0_u8; payload_len]; - self.stream.read_exact(&mut payload)?; - self.stats.bytes_received += (FRAME_HEADER_BYTES + payload_len) as u64; - self.stats.frames_received += 1; - Ok(CultNetTransportFrame { - channel_id: "schema".to_string(), - payload, - }) - } -} diff --git a/vendor/cultnet-rs/tests/cultnet.rs b/vendor/cultnet-rs/tests/cultnet.rs deleted file mode 100644 index 598c383..0000000 --- a/vendor/cultnet-rs/tests/cultnet.rs +++ /dev/null @@ -1,2279 +0,0 @@ -use anyhow::Result; -use chrono::Duration; -use chrono::Utc; -use cultcache_rs::CultCache; -use cultcache_rs::DatabaseEntry; -use cultcache_rs::SingleFileMessagePackBackingStore; -use cultnet_rs::CultMesh; -use cultnet_rs::CultMeshAuthorityLease; -use cultnet_rs::CultMeshPeerCard; -use cultnet_rs::CultMeshRudpClientOptions; -use cultnet_rs::CultMeshRudpSocketOptions; -use cultnet_rs::CultNetClientSecurityOptions; -use cultnet_rs::CultNetDocumentBinding; -use cultnet_rs::CultNetDocumentMutationContract; -use cultnet_rs::CultNetDocumentOperation; -use cultnet_rs::CultNetDocumentPutOptions; -use cultnet_rs::CultNetDocumentRegistry; -use cultnet_rs::CultNetMessage; -use cultnet_rs::CultNetMutationAuthority; -use cultnet_rs::CultNetReactiveDocumentOptions; -use cultnet_rs::CultNetReconnectController; -use cultnet_rs::CultNetReconnectPolicyOptions; -use cultnet_rs::CultNetRudpPacket; -use cultnet_rs::CultNetRudpPacketType; -use cultnet_rs::CultNetRudpReconnectLoop; -use cultnet_rs::CultNetRudpSendOptions; -use cultnet_rs::CultNetRudpSession; -use cultnet_rs::CultNetRudpSessionOptions; -use cultnet_rs::CultNetRudpSocketMode; -use cultnet_rs::CultNetRudpSocketTransportConnection; -use cultnet_rs::CultNetRudpSocketTransportOptions; -use cultnet_rs::CultNetSchemaKind; -use cultnet_rs::CultNetSchemaRegistration; -use cultnet_rs::CultNetSchemaRegistry; -use cultnet_rs::CultNetSecret; -use cultnet_rs::CultNetServerSecurityOptions; -use cultnet_rs::CultNetShardCatalog; -use cultnet_rs::CultNetShardDescriptor; -use cultnet_rs::CultNetTransportChannel; -use cultnet_rs::CultNetTransportDelivery; -use cultnet_rs::CultNetTransportDescriptor; -use cultnet_rs::CultNetTransportFrame; -use cultnet_rs::CultNetTransportOrdering; -use cultnet_rs::CultNetTransportProfile; -use cultnet_rs::CultNetTransportProtocol; -use cultnet_rs::CultNetWireContract; -use cultnet_rs::LengthPrefixedMessageFramer; -use cultnet_rs::TcpFramedTransportConnection; -use cultnet_rs::TcpFramedTransportProfileOptions; -use cultnet_rs::builtin_schema_registry; -use cultnet_rs::compute_reconnect_delay_ms; -use cultnet_rs::create_reconnect_policy; -use cultnet_rs::create_rudp_transport_profile; -use cultnet_rs::create_tcp_framed_transport_profile; -use cultnet_rs::decode_cultnet_message_from_slice; -use cultnet_rs::decode_rudp_packet; -use cultnet_rs::encode_cultnet_message_for_wire; -use cultnet_rs::encode_cultnet_message_to_vec; -use cultnet_rs::encode_frame; -use cultnet_rs::encode_rudp_packet; -use pretty_assertions::assert_eq; -use std::cell::RefCell; -use std::net::UdpSocket; -use std::rc::Rc; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex}; -use std::thread; -use std::time::Duration as StdDuration; - -const TS_HELLO_FRAME: &[u8] = include_bytes!("fixtures/cultnet-ts-hello.frame"); -const TS_LEGACY_LOGIN_FRAME: &[u8] = include_bytes!("fixtures/cultnet-ts-legacy-login.frame"); - -fn bind_udp_socket() -> Result { - let socket = UdpSocket::bind("127.0.0.1:0")?; - socket.set_read_timeout(Some(StdDuration::from_millis(20)))?; - Ok(socket) -} - -fn pump_rudp_handshake( - client: &mut CultNetRudpSocketTransportConnection, - server: &mut CultNetRudpSocketTransportConnection, -) -> Result<()> { - for _ in 0..20 { - let _ = server.receive_once()?; - let _ = client.receive_once()?; - let _ = server.receive_once()?; - if client.connected() && server.connected() { - return Ok(()); - } - thread::sleep(StdDuration::from_millis(5)); - } - anyhow::bail!("RUDP socket handshake did not complete"); -} - -fn receive_rudp_frame( - transport: &mut CultNetRudpSocketTransportConnection, -) -> Result { - for _ in 0..20 { - if let Some(frame) = transport.receive_once()? { - return Ok(frame); - } - thread::sleep(StdDuration::from_millis(5)); - } - anyhow::bail!("RUDP socket frame was not delivered") -} - -fn receive_rudp_schema_message( - transport: &mut CultNetRudpSocketTransportConnection, -) -> Result { - for _ in 0..20 { - if let Some(message) = transport.receive_schema_message_once()? { - return Ok(message); - } - thread::sleep(StdDuration::from_millis(5)); - } - anyhow::bail!("RUDP schema message was not delivered") -} - -fn pump_rudp_server_until_connected( - server: Arc>, - done: Arc, -) -> thread::JoinHandle> { - thread::spawn(move || { - while !done.load(Ordering::SeqCst) { - { - let mut server = server.lock().expect("RUDP server mutex poisoned"); - let _ = server.receive_once()?; - server.poll_resends()?; - if server.connected() { - return Ok(()); - } - } - thread::sleep(StdDuration::from_millis(5)); - } - Ok(()) - }) -} - -#[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache( - type = "ghostlight.agent-state", - schema = "GhostlightAgentStateFixture" -)] -struct GhostlightAgentStateFixture { - #[cultcache(key = 0)] - schema_version: String, - #[cultcache(key = 1)] - agent_id: String, - #[cultcache(key = 2)] - display_name: String, -} - -#[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache( - type = "ghostlight.agent-state.ui", - schema = "GhostlightAgentStateUiFixture" -)] -struct GhostlightAgentStateUiFixture { - #[cultcache(key = 0)] - schema_version: String, - #[cultcache(key = 1)] - agent_id: String, - #[cultcache(key = 2)] - display_name: String, -} - -#[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache( - type = "ghostlight.reactive-note", - schema = "GhostlightReactiveNoteFixture" -)] -struct GhostlightReactiveNoteFixture { - #[cultcache(key = 0)] - body: String, - #[cultcache(key = 1)] - revision: i32, -} - -#[derive(Clone, Debug, PartialEq, DatabaseEntry)] -#[cultcache( - type = "ghostlight.reactive-note.ui", - schema = "GhostlightReactiveNoteUiFixture" -)] -struct GhostlightReactiveNoteUiFixture { - #[cultcache(key = 0)] - body: String, - #[cultcache(key = 1)] - revision: i32, -} - -#[test] -fn security_helpers_round_trip_encrypted_strings_and_validate_sessions() -> Result<()> { - let server_security = CultNetServerSecurityOptions::development(); - let client_security = server_security.to_client_options(); - let nonce = CultNetSecret::new_nonce(); - let encrypted = CultNetSecret::encrypt_string(Some("hello"), &nonce, &client_security)? - .expect("non-empty input encrypts"); - assert_eq!( - CultNetSecret::decrypt_string(Some(&encrypted), Some(&nonce), &server_security)?, - Some("hello".to_string()) - ); - - let token = CultNetSecret::create_session_token( - "runtime-face", - chrono::Utc::now() + Duration::minutes(1), - &server_security, - )?; - let validated = CultNetSecret::try_validate_session_token(Some(&token), &server_security)? - .expect("token validates before expiry"); - assert_eq!(validated.user_id, "runtime-face"); - Ok(()) -} - -#[test] -fn cultnet_schema_messages_round_trip_through_messagepack_frames() -> Result<()> { - let message = CultNetMessage::Hello { - runtime_id: "voidbot-main".to_string(), - runtime_kind: "rust-worker".to_string(), - agent_id: Some("void".to_string()), - role: None, - display_name: Some("Void".to_string()), - supported_document_types: Some(vec!["ghostlight.agent-state".to_string()]), - supported_mutation_contracts: Some(vec![CultNetDocumentMutationContract { - document_type: "ghostlight.agent-state".to_string(), - payload_schema_version: Some("ghostlight.agent_state.v0".to_string()), - operations: vec![ - CultNetDocumentOperation::Snapshot, - CultNetDocumentOperation::DocumentPut, - ], - authority: CultNetMutationAuthority::Coordinator, - intent_document_types: Some(vec!["ghostlight.agent-state.intent.v0".to_string()]), - receipt_document_types: Some(vec!["ghostlight.agent-state.receipt.v0".to_string()]), - notes: Some(vec![ - "Persona may request bounded memory mutation; coordinator reviews authority." - .to_string(), - ]), - }]), - supported_message_versions: None, - transport_profiles: Some(vec![CultNetTransportProfile { - schema_version: "cultnet.transport_profile.v0".to_string(), - runtime_id: "voidbot-main".to_string(), - transports: vec![CultNetTransportDescriptor { - transport_id: "direct-pipe".to_string(), - protocol: CultNetTransportProtocol::TcpFramed, - host: None, - port: None, - path: None, - discovery_group: None, - wire_contracts: Some(vec!["cultnet.schema.v0".to_string()]), - reconnect_policy: None, - channels: vec![CultNetTransportChannel { - channel_id: "schema".to_string(), - delivery: CultNetTransportDelivery::Reliable, - ordering: CultNetTransportOrdering::Ordered, - max_payload_bytes: None, - max_fragment_bytes: None, - max_pending_reliable_packets: None, - }], - }], - }]), - supports_schema_catalog: Some(true), - }; - let payload = encode_cultnet_message_to_vec(&message, CultNetWireContract::CultNetSchemaV0)?; - let frame = encode_frame(&payload)?; - assert_eq!(&frame[..4], &(payload.len() as u32).to_be_bytes()); - - let mut framer = LengthPrefixedMessageFramer::new(); - assert!(framer.push(&frame[..2]).is_empty()); - let frames = framer.push(&frame[2..]); - assert_eq!(frames.len(), 1); - let decoded = - decode_cultnet_message_from_slice(&frames[0], CultNetWireContract::CultNetSchemaV0)?; - assert_eq!(decoded, message); - Ok(()) -} - -#[test] -fn tcp_framed_transport_carries_schema_payloads_with_stats() -> Result<()> { - let payload = b"cultnet-payload".to_vec(); - let profile = create_tcp_framed_transport_profile( - "rust-transport", - TcpFramedTransportProfileOptions { - transport_id: Some("test-tcp".to_string()), - ..TcpFramedTransportProfileOptions::default() - }, - ); - let mut sender = TcpFramedTransportConnection::new(Vec::::new(), profile.clone()); - sender.send("schema", &payload)?; - assert_eq!(sender.stats().frames_sent, 1); - assert_eq!(sender.stats().bytes_sent, (payload.len() + 4) as u64); - assert!(sender.send("unreliable", &[]).is_err()); - - let bytes = sender.into_inner(); - let mut receiver = TcpFramedTransportConnection::new(std::io::Cursor::new(bytes), profile); - let frame = receiver.receive()?; - assert_eq!(frame.channel_id, "schema"); - assert_eq!(frame.payload, payload); - assert_eq!(receiver.stats().frames_received, 1); - assert_eq!( - receiver.stats().bytes_received, - (frame.payload.len() + 4) as u64 - ); - assert_eq!( - receiver.profile.transports[0].protocol, - CultNetTransportProtocol::TcpFramed - ); - Ok(()) -} - -#[test] -fn tcp_framed_transport_rejects_oversized_payload_before_reading_it() { - let profile = create_tcp_framed_transport_profile( - "bounded-rust-transport", - TcpFramedTransportProfileOptions { - max_payload_bytes: Some(8), - ..TcpFramedTransportProfileOptions::default() - }, - ); - let mut bytes = Vec::new(); - bytes.extend_from_slice(&9_u32.to_be_bytes()); - let mut receiver = TcpFramedTransportConnection::new(std::io::Cursor::new(bytes), profile); - let error = receiver.receive().expect_err("oversized frame must fail"); - assert!(error.to_string().contains("exceeds advertised maximum 8")); - assert_eq!(receiver.stats().bytes_received, 0); -} - -#[test] -fn rudp_packet_codec_uses_deterministic_reliable_ordered_fixture() -> Result<()> { - let encoded = encode_rudp_packet(&CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Data, - connection_id: 0x01020304, - sequence: 0x0000002a, - ack: 0x00000029, - ack_mask: 0x80000001, - channel_id: "schema".to_string(), - reliable: true, - ordered: true, - sequenced: false, - fragment_id: 7, - fragment_index: 1, - fragment_count: 3, - payload: b"hello".to_vec(), - })?; - - assert_eq!( - encoded, - vec![ - 67, 78, 82, 48, 0, 3, 11, 42, 1, 2, 3, 4, 0, 0, 0, 42, 0, 0, 0, 41, 128, 0, 0, 1, 0, 7, - 0, 1, 0, 3, 0, 0, 0, 5, 6, 0, 115, 99, 104, 101, 109, 97, 104, 101, 108, 108, 111, - ] - ); - - let decoded = decode_rudp_packet(&encoded)?; - assert_eq!(decoded.packet_type, CultNetRudpPacketType::Data); - assert_eq!(decoded.connection_id, 0x01020304); - assert_eq!(decoded.sequence, 0x0000002a); - assert_eq!(decoded.ack, 0x00000029); - assert_eq!(decoded.ack_mask, 0x80000001); - assert_eq!(decoded.channel_id, "schema"); - assert!(decoded.reliable); - assert!(decoded.ordered); - assert!(!decoded.sequenced); - assert_eq!(decoded.fragment_id, 7); - assert_eq!(decoded.fragment_index, 1); - assert_eq!(decoded.fragment_count, 3); - assert_eq!(decoded.payload, b"hello"); - Ok(()) -} - -#[test] -fn rudp_transport_profile_advertises_state_and_realtime_channels() { - let profile = create_rudp_transport_profile( - "rust-rudp", - cultnet_rs::RudpTransportProfileOptions { - transport_id: Some("public-rudp".to_string()), - host: Some("127.0.0.1".to_string()), - port: Some(7777), - max_payload_bytes: Some(1200), - max_fragment_bytes: Some(1000), - max_pending_reliable_packets: Some(64), - reconnect_policy: None, - }, - ); - - assert_eq!( - profile.transports[0].protocol, - CultNetTransportProtocol::Rudp - ); - assert_eq!( - profile.transports[0] - .reconnect_policy - .as_ref() - .map(|policy| policy.schema_version.as_str()), - Some("cultnet.reconnect_policy.v0") - ); - assert_eq!( - profile.transports[0] - .reconnect_policy - .as_ref() - .map(|policy| policy.base_delay_ms), - Some(1_000) - ); - let channels = profile.transports[0] - .channels - .iter() - .map(|channel| { - ( - channel.channel_id.as_str(), - channel.delivery, - channel.ordering, - channel.max_pending_reliable_packets, - ) - }) - .collect::>(); - assert_eq!( - channels, - vec![ - ( - "schema", - CultNetTransportDelivery::Reliable, - CultNetTransportOrdering::Ordered, - Some(64) - ), - ( - "latest", - CultNetTransportDelivery::Unreliable, - CultNetTransportOrdering::Sequenced, - Some(64) - ), - ( - "realtime", - CultNetTransportDelivery::Unreliable, - CultNetTransportOrdering::Unordered, - Some(64) - ), - ( - "media", - CultNetTransportDelivery::Reliable, - CultNetTransportOrdering::Unordered, - Some(64) - ), - ] - ); -} - -#[test] -fn reconnect_policy_exposes_portable_delay_contract() { - let policy = create_reconnect_policy(CultNetReconnectPolicyOptions { - policy_id: "rudp-default".to_string(), - max_attempts: Some(8), - ..CultNetReconnectPolicyOptions::default() - }); - - assert_eq!(policy.schema_version, "cultnet.reconnect_policy.v0"); - assert_eq!(policy.policy_id, "rudp-default"); - assert_eq!(policy.max_attempts, Some(8)); - assert_eq!(compute_reconnect_delay_ms(&policy, 1, 0), 1_000); - assert_eq!(compute_reconnect_delay_ms(&policy, 3, 17), 4_017); - assert_eq!(compute_reconnect_delay_ms(&policy, 9, 999), 30_250); - assert_eq!(compute_reconnect_delay_ms(&policy, 0, 0), 1_000); -} - -#[test] -fn reconnect_controller_schedules_attempts_and_reset() { - let policy = create_reconnect_policy(CultNetReconnectPolicyOptions { - max_attempts: Some(2), - ..CultNetReconnectPolicyOptions::default() - }); - let mut controller = CultNetReconnectController::new(policy); - - let first = controller.record_failure(10_000, 0); - assert_eq!(first.attempt, 1); - assert!(first.should_retry); - assert_eq!(first.delay_ms, 1_000); - assert_eq!(first.next_attempt_at_ms, Some(11_000)); - assert!(!first.exhausted); - assert!(!controller.can_attempt(10_999)); - assert!(controller.can_attempt(11_000)); - - let second = controller.record_failure(11_000, 17); - assert_eq!(second.attempt, 2); - assert_eq!(second.delay_ms, 2_017); - assert_eq!(second.next_attempt_at_ms, Some(13_017)); - assert!(second.should_retry); - - let exhausted = controller.record_failure(13_017, 0); - assert_eq!(exhausted.attempt, 2); - assert!(!exhausted.should_retry); - assert_eq!(exhausted.delay_ms, 0); - assert_eq!(exhausted.next_attempt_at_ms, None); - assert!(exhausted.exhausted); - assert!(!controller.can_attempt(99_000)); - - controller.reset(); - assert_eq!(controller.attempt(), 0); - assert_eq!(controller.next_attempt_at_ms(), None); - assert!(!controller.exhausted()); - assert!(controller.can_attempt(99_000)); -} - -#[test] -fn rudp_reconnect_loop_consumes_shared_controller() -> Result<()> { - let server_socket = bind_udp_socket()?; - let remote_addr = server_socket.local_addr()?; - let opened_local_addrs = Rc::new(RefCell::new(Vec::new())); - let opened_for_factory = Rc::clone(&opened_local_addrs); - let connection_id = 0x2233_4455; - let policy = create_reconnect_policy(CultNetReconnectPolicyOptions { - max_attempts: Some(2), - ..CultNetReconnectPolicyOptions::default() - }); - - let mut loop_ = CultNetRudpReconnectLoop::new(policy, b"join".to_vec(), move || { - let socket = bind_udp_socket()?; - opened_for_factory.borrow_mut().push(socket.local_addr()?); - CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions::client( - "rust-rudp-reconnect", - socket, - remote_addr, - connection_id, - )) - }); - - { - let first = loop_.start()?; - assert!(first.stats().bytes_sent > 0); - } - assert!(loop_.transport().is_some()); - assert_eq!(opened_local_addrs.borrow().len(), 1); - - let decision = loop_.handle_closed(10_000, 17).expect("retry decision"); - assert_eq!(decision.attempt, 1); - assert!(decision.should_retry); - assert_eq!(decision.delay_ms, 1_017); - assert_eq!(decision.next_attempt_at_ms, Some(11_017)); - assert_eq!(loop_.reconnect_controller.attempt(), 1); - assert_eq!( - loop_.reconnect_controller.next_attempt_at_ms(), - Some(11_017) - ); - - assert!(!loop_.reconnect_if_due(11_016)?); - assert_eq!(opened_local_addrs.borrow().len(), 1); - assert!(loop_.reconnect_if_due(11_017)?); - assert_eq!(opened_local_addrs.borrow().len(), 2); - assert!(loop_.transport().is_some()); - - loop_.mark_connected(); - assert_eq!(loop_.reconnect_controller.attempt(), 0); - - loop_.stop(); - assert!(loop_.transport().is_none()); - assert_eq!(loop_.reconnect_controller.attempt(), 0); - - Ok(()) -} - -#[test] -fn rudp_session_handshake_acks_reliable_connect_and_accept_packets() -> Result<()> { - let mut client = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 0x0a0b0c0d, - initial_sequence: 1, - resend_delay_ms: 50, - ..CultNetRudpSessionOptions::default() - }); - let mut server = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 0x0a0b0c0d, - initial_sequence: 100, - resend_delay_ms: 50, - ..CultNetRudpSessionOptions::default() - }); - - let connect = client.create_connect(0, b"join".to_vec())?; - assert_eq!(connect.packet_type, CultNetRudpPacketType::Connect); - assert_eq!(connect.sequence, 1); - assert_eq!(client.pending_reliable_sequences(), vec![1]); - - let accept = server.accept_connect(&connect, 10, b"ok".to_vec())?; - assert_eq!(accept.packet_type, CultNetRudpPacketType::Accept); - assert_eq!(accept.ack, 1); - assert!(server.connected()); - assert_eq!(server.pending_reliable_sequences(), vec![100]); - - client.receive(&accept, 20)?; - assert!(client.connected()); - assert!(client.pending_reliable_sequences().is_empty()); - - let ack = client.create_ack(); - assert_eq!(ack.ack, 100); - server.receive(&ack, 30)?; - assert!(server.pending_reliable_sequences().is_empty()); - Ok(()) -} - -#[test] -fn rudp_session_computes_ack_masks_and_clears_pending_reliable_packets() -> Result<()> { - let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 7, - initial_sequence: 10, - resend_delay_ms: 100, - ..CultNetRudpSessionOptions::default() - }); - let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 7, - initial_sequence: 200, - resend_delay_ms: 100, - ..CultNetRudpSessionOptions::default() - }); - sender.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 7, - sequence: 1, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - receiver.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 7, - sequence: 2, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - - let options = CultNetRudpSendOptions { - reliable: true, - ordered: true, - ..CultNetRudpSendOptions::default() - }; - let first = sender.send("schema", b"first".to_vec(), options.clone())?; - let second = sender.send("schema", b"second".to_vec(), options.clone())?; - let third = sender.send("schema", b"third".to_vec(), options)?; - assert_eq!(sender.pending_reliable_sequences(), vec![10, 11, 12]); - - receiver.receive(&first, 0)?; - receiver.receive(&third, 0)?; - let ack_with_gap = receiver.create_ack(); - assert_eq!(ack_with_gap.ack, 12); - assert_eq!(ack_with_gap.ack_mask, 0b10 | (1 << 9)); - sender.receive(&ack_with_gap, 0)?; - assert_eq!(sender.pending_reliable_sequences(), vec![11]); - - receiver.receive(&second, 0)?; - let full_ack = receiver.create_ack(); - assert_eq!(full_ack.ack, 12); - assert_eq!(full_ack.ack_mask, 0b11 | (1 << 9)); - sender.receive(&full_ack, 0)?; - assert!(sender.pending_reliable_sequences().is_empty()); - Ok(()) -} - -#[test] -fn rudp_session_schedules_reliable_resends_until_acked() -> Result<()> { - let mut session = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 99, - initial_sequence: 1, - resend_delay_ms: 100, - ..CultNetRudpSessionOptions::default() - }); - session.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 99, - sequence: 50, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - let sent = session.send( - "schema", - b"payload".to_vec(), - CultNetRudpSendOptions { - reliable: true, - ordered: true, - now_ms: 10, - ..CultNetRudpSendOptions::default() - }, - )?; - - assert!(session.due_resends(90).is_empty()); - assert_eq!( - session - .due_resends(110) - .iter() - .map(|packet| packet.sequence) - .collect::>(), - vec![sent.sequence] - ); - assert!(session.due_resends(150).is_empty()); - - session.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Ack, - connection_id: 99, - sequence: 51, - ack: sent.sequence, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - assert!(session.due_resends(250).is_empty()); - Ok(()) -} - -#[test] -fn rudp_session_pings_and_detects_receive_timeout() -> Result<()> { - let mut client = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 101, - initial_sequence: 1, - ..CultNetRudpSessionOptions::default() - }); - let mut server = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 101, - initial_sequence: 100, - ..CultNetRudpSessionOptions::default() - }); - let connect = client.create_connect(0, b"join".to_vec())?; - let accept = server.accept_connect(&connect, 10, Vec::new())?; - client.receive(&accept, 20)?; - - let ping = client.create_ping(b"pulse".to_vec()); - let ping_result = server.receive(&ping, 30)?; - let pong = ping_result.reply.expect("ping should produce pong"); - assert_eq!(pong.packet_type, CultNetRudpPacketType::Pong); - assert_eq!(pong.payload, b"pulse"); - - let pong_result = client.receive(&pong, 40)?; - assert!(pong_result.pong); - assert_eq!(pong_result.pong_payload, b"pulse"); - assert!(!client.check_timeout(90, 50)); - assert!(client.check_timeout(91, 50)); - assert!(!client.connected()); - Ok(()) -} - -#[test] -fn rudp_lossy_packets_cannot_create_reliable_ordered_gaps() -> Result<()> { - let mut client = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 198, - initial_sequence: 1, - ..CultNetRudpSessionOptions::default() - }); - let mut server = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 198, - initial_sequence: 100, - ..CultNetRudpSessionOptions::default() - }); - let connect = client.create_connect(0, Vec::new())?; - let accept = server.accept_connect(&connect, 1, Vec::new())?; - client.receive(&accept, 2)?; - - let ping = client.create_ping(b"pulse".to_vec()); - let pong = server - .receive(&ping, 3)? - .reply - .expect("ping should produce pong"); - let realtime = server.send( - "realtime", - b"discarded realtime".to_vec(), - CultNetRudpSendOptions::default(), - )?; - let latest = server.send( - "latest", - b"discarded latest state".to_vec(), - CultNetRudpSendOptions { - sequenced: true, - ..CultNetRudpSendOptions::default() - }, - )?; - let schema = server.send( - "schema", - b"committed response".to_vec(), - CultNetRudpSendOptions { - reliable: true, - ordered: true, - ..CultNetRudpSendOptions::default() - }, - )?; - - assert_eq!(ping.sequence, 0); - assert_eq!(pong.sequence, 0); - assert_eq!(realtime.sequence, 0); - assert_eq!(latest.sequence, 1); - assert_eq!(schema.sequence, accept.sequence + 1); - assert_eq!( - client.receive(&schema, 4)?.delivered[0].payload, - b"committed response" - ); - Ok(()) -} - -#[test] -fn rudp_unreliable_sequenced_delivery_is_scoped_to_its_channel() -> Result<()> { - let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 197, - initial_sequence: 50, - ..CultNetRudpSessionOptions::default() - }); - let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 197, - initial_sequence: 100, - ..CultNetRudpSessionOptions::default() - }); - let connect = sender.create_connect(0, Vec::new())?; - let accept = receiver.accept_connect(&connect, 1, Vec::new())?; - sender.receive(&accept, 2)?; - let options = CultNetRudpSendOptions { - sequenced: true, - ..CultNetRudpSendOptions::default() - }; - let older = sender.send("latest", b"older".to_vec(), options.clone())?; - let newer = sender.send("latest", b"newer".to_vec(), options)?; - - assert_eq!(older.sequence, 1); - assert_eq!(newer.sequence, 2); - assert_eq!(receiver.receive(&newer, 3)?.delivered[0].payload, b"newer"); - assert!(receiver.receive(&older, 4)?.delivered.is_empty()); - Ok(()) -} - -#[test] -fn rudp_rejects_unreliable_ordered_delivery() -> Result<()> { - let mut session = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 196, - ..CultNetRudpSessionOptions::default() - }); - session.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 196, - sequence: 50, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - let error = session - .send( - "schema", - b"cannot order what will not retransmit".to_vec(), - CultNetRudpSendOptions { - ordered: true, - ..CultNetRudpSendOptions::default() - }, - ) - .unwrap_err(); - assert!( - error - .to_string() - .contains("ordered delivery requires reliability") - ); - Ok(()) -} - -#[test] -fn rudp_session_bounds_pending_reliable_packets_before_enqueue() -> Result<()> { - let mut session = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 102, - initial_sequence: 1, - max_pending_reliable_packets: Some(2), - ..CultNetRudpSessionOptions::default() - }); - session.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 102, - sequence: 50, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - session.send( - "schema", - b"first".to_vec(), - CultNetRudpSendOptions { - reliable: true, - ordered: true, - ..CultNetRudpSendOptions::default() - }, - )?; - session.send( - "schema", - b"second".to_vec(), - CultNetRudpSendOptions { - reliable: true, - ordered: true, - ..CultNetRudpSendOptions::default() - }, - )?; - let error = session - .send( - "schema", - b"third".to_vec(), - CultNetRudpSendOptions { - reliable: true, - ordered: true, - ..CultNetRudpSendOptions::default() - }, - ) - .unwrap_err(); - assert!(error.to_string().contains("reliable send queue is full")); - assert_eq!(session.pending_reliable_sequences(), vec![1, 2]); - - let mut fragmented = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 103, - initial_sequence: 1, - max_pending_reliable_packets: Some(3), - ..CultNetRudpSessionOptions::default() - }); - fragmented.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 103, - sequence: 50, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - let error = fragmented - .send_many( - "schema", - b"fragment-me".to_vec(), - CultNetRudpSendOptions { - reliable: true, - ordered: true, - ..CultNetRudpSendOptions::default() - }, - Some(3), - ) - .unwrap_err(); - assert!(error.to_string().contains("reliable send queue is full")); - assert!(fragmented.pending_reliable_sequences().is_empty()); - Ok(()) -} - -#[test] -fn rudp_session_suppresses_duplicates_and_delivers_reliable_ordered_payloads_in_sequence() --> Result<()> { - let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 123, - initial_sequence: 1, - ..CultNetRudpSessionOptions::default() - }); - let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 123, - initial_sequence: 100, - ..CultNetRudpSessionOptions::default() - }); - sender.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 123, - sequence: 90, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - receiver.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 123, - sequence: 91, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - - let options = CultNetRudpSendOptions { - reliable: true, - ordered: true, - ..CultNetRudpSendOptions::default() - }; - let first = sender.send("schema", b"first".to_vec(), options.clone())?; - let second = sender.send("schema", b"second".to_vec(), options.clone())?; - let third = sender.send("schema", b"third".to_vec(), options)?; - - assert_eq!( - receiver - .receive(&first, 0)? - .delivered - .iter() - .map(|frame| String::from_utf8(frame.payload.clone()).unwrap()) - .collect::>(), - vec!["first"] - ); - assert!(receiver.receive(&third, 0)?.delivered.is_empty()); - assert!(receiver.receive(&first, 0)?.delivered.is_empty()); - assert_eq!( - receiver - .receive(&second, 0)? - .delivered - .iter() - .map(|frame| String::from_utf8(frame.payload.clone()).unwrap()) - .collect::>(), - vec!["second", "third"] - ); - Ok(()) -} - -#[test] -fn rudp_session_skips_control_packets_while_ordering_schema_payloads() -> Result<()> { - let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 124, - initial_sequence: 1, - ..CultNetRudpSessionOptions::default() - }); - let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 124, - initial_sequence: 100, - ..CultNetRudpSessionOptions::default() - }); - sender.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 124, - sequence: 90, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - receiver.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 124, - sequence: 91, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - - let options = CultNetRudpSendOptions { - reliable: true, - ordered: true, - ..CultNetRudpSendOptions::default() - }; - let first = sender.send("schema", b"first".to_vec(), options.clone())?; - let control = sender.create_ack(); - let second = sender.send("schema", b"second".to_vec(), options)?; - assert_eq!(control.sequence, 0); - assert_eq!(second.sequence, first.sequence + 1); - - assert_eq!( - receiver - .receive(&first, 0)? - .delivered - .iter() - .map(|frame| String::from_utf8(frame.payload.clone()).unwrap()) - .collect::>(), - vec!["first"] - ); - assert!(receiver.receive(&control, 0)?.delivered.is_empty()); - assert_eq!( - receiver - .receive(&second, 0)? - .delivered - .iter() - .map(|frame| String::from_utf8(frame.payload.clone()).unwrap()) - .collect::>(), - vec!["second"] - ); - Ok(()) -} - -#[test] -fn rudp_session_fragments_and_reassembles_reliable_ordered_payloads() -> Result<()> { - let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 456, - initial_sequence: 1, - ..CultNetRudpSessionOptions::default() - }); - let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: 456, - initial_sequence: 100, - ..CultNetRudpSessionOptions::default() - }); - sender.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 456, - sequence: 90, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - receiver.receive( - &CultNetRudpPacket { - packet_type: CultNetRudpPacketType::Accept, - connection_id: 456, - sequence: 91, - ack: 0, - ack_mask: 0, - channel_id: "control".to_string(), - reliable: false, - ordered: false, - sequenced: false, - fragment_id: 0, - fragment_index: 0, - fragment_count: 0, - payload: Vec::new(), - }, - 0, - )?; - - let packets = sender.send_many( - "schema", - b"fragment-me-please".to_vec(), - CultNetRudpSendOptions { - reliable: true, - ordered: true, - now_ms: 10, - ..CultNetRudpSendOptions::default() - }, - Some(5), - )?; - assert_eq!(packets.len(), 4); - assert_eq!( - packets - .iter() - .map(|packet| packet.fragment_count) - .collect::>(), - vec![4, 4, 4, 4] - ); - assert_eq!( - packets - .iter() - .map(|packet| packet.fragment_index) - .collect::>(), - vec![0, 1, 2, 3] - ); - assert!( - packets - .iter() - .all(|packet| packet.fragment_id == packets[0].fragment_id) - ); - - assert!(receiver.receive(&packets[0], 0)?.delivered.is_empty()); - assert!(receiver.receive(&packets[1], 0)?.delivered.is_empty()); - assert!(receiver.receive(&packets[2], 0)?.delivered.is_empty()); - let delivered = receiver.receive(&packets[3], 0)?.delivered; - assert_eq!(delivered.len(), 1); - assert_eq!(delivered[0].payload, b"fragment-me-please"); - assert_eq!(delivered[0].sequence, packets[0].sequence); - Ok(()) -} - -#[test] -fn rudp_socket_transport_handshakes_and_carries_reliable_ordered_schema_frames() -> Result<()> { - let server_socket = bind_udp_socket()?; - let client_socket = bind_udp_socket()?; - let server_addr = server_socket.local_addr()?; - let connection_id = 0x1020_3040; - let mut server = - CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions { - runtime_id: "rust-rudp-server".to_string(), - socket: server_socket, - mode: CultNetRudpSocketMode::Server, - remote_addr: None, - connection_id, - initial_sequence: 100, - resend_delay_ms: 25, - transport_id: None, - max_payload_bytes: None, - max_fragment_bytes: None, - max_pending_reliable_packets: None, - reconnect_policy: None, - })?; - let mut client = - CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions { - runtime_id: "rust-rudp-client".to_string(), - socket: client_socket, - mode: CultNetRudpSocketMode::Client, - remote_addr: Some(server_addr), - connection_id, - initial_sequence: 1, - resend_delay_ms: 25, - transport_id: None, - max_payload_bytes: None, - max_fragment_bytes: None, - max_pending_reliable_packets: None, - reconnect_policy: None, - })?; - - client.connect(b"join".to_vec())?; - pump_rudp_handshake(&mut client, &mut server)?; - assert!(client.connected()); - assert!(server.connected()); - - client.send("schema", b"client-state".to_vec())?; - let server_frame = receive_rudp_frame(&mut server)?; - assert_eq!(server_frame.channel_id, "schema"); - assert_eq!(server_frame.payload, b"client-state"); - - server.send("schema", b"server-state".to_vec())?; - let client_frame = receive_rudp_frame(&mut client)?; - assert_eq!(client_frame.channel_id, "schema"); - assert_eq!(client_frame.payload, b"server-state"); - - client.send_schema_message(&CultNetMessage::Hello { - runtime_id: "rust-rudp-client".to_string(), - runtime_kind: "rust-worker".to_string(), - agent_id: None, - role: Some("schema".to_string()), - display_name: None, - supported_document_types: None, - supported_mutation_contracts: None, - supported_message_versions: None, - transport_profiles: Some(vec![client.profile.clone()]), - supports_schema_catalog: Some(true), - })?; - let message = receive_rudp_schema_message(&mut server)?; - let CultNetMessage::Hello { - runtime_id, - transport_profiles, - supports_schema_catalog, - .. - } = message - else { - anyhow::bail!("RUDP schema message did not decode as hello"); - }; - assert_eq!(runtime_id, "rust-rudp-client"); - assert_eq!(supports_schema_catalog, Some(true)); - assert_eq!( - transport_profiles.expect("hello advertises transport")[0].transports[0].protocol, - CultNetTransportProtocol::Rudp - ); - assert_eq!( - server.profile.transports[0].protocol, - CultNetTransportProtocol::Rudp - ); - assert_eq!(client.stats().frames_sent, 2); - assert_eq!(server.stats().frames_received, 2); - - Ok(()) -} - -#[test] -fn rudp_socket_transport_carries_fragmented_reliable_ordered_schema_frames() -> Result<()> { - let server_socket = bind_udp_socket()?; - let client_socket = bind_udp_socket()?; - let server_addr = server_socket.local_addr()?; - let connection_id = 0x1020_3041; - let mut server = - CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions { - runtime_id: "rust-rudp-fragment-server".to_string(), - socket: server_socket, - mode: CultNetRudpSocketMode::Server, - remote_addr: None, - connection_id, - initial_sequence: 100, - resend_delay_ms: 25, - transport_id: None, - max_payload_bytes: None, - max_fragment_bytes: Some(8), - max_pending_reliable_packets: None, - reconnect_policy: None, - })?; - let mut client = - CultNetRudpSocketTransportConnection::new(CultNetRudpSocketTransportOptions { - runtime_id: "rust-rudp-fragment-client".to_string(), - socket: client_socket, - mode: CultNetRudpSocketMode::Client, - remote_addr: Some(server_addr), - connection_id, - initial_sequence: 1, - resend_delay_ms: 25, - transport_id: None, - max_payload_bytes: None, - max_fragment_bytes: Some(8), - max_pending_reliable_packets: None, - reconnect_policy: None, - })?; - - client.connect(b"join".to_vec())?; - pump_rudp_handshake(&mut client, &mut server)?; - - let payload = b"this-schema-frame-is-larger-than-one-rudp-fragment".to_vec(); - client.send("schema", payload.clone())?; - let server_frame = receive_rudp_frame(&mut server)?; - assert_eq!(server_frame.channel_id, "schema"); - assert_eq!(server_frame.payload, payload); - assert_eq!(client.stats().frames_sent, 1); - assert_eq!(server.stats().frames_received, 1); - Ok(()) -} - -#[test] -fn cultmesh_facade_creates_rudp_client_from_peer_endpoint() -> Result<()> { - let connection_id = 0x2030_4050; - let options = CultMeshRudpSocketOptions { - resend_delay_ms: 25, - max_fragment_bytes: Some(8), - max_pending_reliable_packets: Some(16), - ..CultMeshRudpSocketOptions::default() - }; - let mut server = - CultMesh::create_rudp_server("rust-cultmesh-server", connection_id, options.clone())?; - let server_port = server.profile.transports[0] - .port - .expect("RUDP server profile advertises its local port"); - let endpoint = CultMesh::parse_rudp_endpoint(&format!("rudp://127.0.0.1:{server_port}"))?; - assert_eq!(endpoint.host, "127.0.0.1"); - assert_eq!(endpoint.port, server_port); - assert_eq!( - CultMesh::parse_rudp_endpoint("RuDp://localhost:4100")?.host, - "localhost" - ); - assert_eq!( - CultMesh::parse_rudp_endpoint("rudp://[::1]:4100")?.uri(), - "rudp://[::1]:4100" - ); - - let peer = CultMeshPeerCard::new("rust-cultmesh-server", "local", [endpoint.uri()]) - .with_roles(["schema"]) - .with_authority_lease_id("lease:rust-cultmesh-server"); - let mut peers = CultMesh::create_peer_catalog(); - peers.upsert(peer.clone())?; - assert_eq!(peers.find("local", Some("schema")), vec![peer.clone()]); - let now = Utc::now(); - let mut leases = CultMesh::create_authority_lease_catalog(); - assert!( - CultMesh::create_rudp_client_for_authorized_peer( - "rust-cultmesh-client", - connection_id, - &peers, - &leases, - "local", - "schema", - None, - now, - options.clone(), - ) - .is_err() - ); - leases.upsert(CultMeshAuthorityLease { - lease_id: "lease:rust-cultmesh-server".to_string(), - verse_id: "local".to_string(), - peer_id: "rust-cultmesh-server".to_string(), - roles: vec!["schema".to_string()], - shard_ids: Vec::new(), - issuer_runtime_id: Some("odin".to_string()), - valid_from: now - Duration::minutes(1), - expires_at: now + Duration::minutes(1), - })?; - - let mut client = CultMesh::create_rudp_client_for_authorized_peer( - "rust-cultmesh-client", - connection_id, - &peers, - &leases, - "local", - "schema", - None, - now, - options, - )?; - client.connect(b"join".to_vec())?; - pump_rudp_handshake(&mut client, &mut server)?; - - client.send("schema", b"client-state".to_vec())?; - let server_frame = receive_rudp_frame(&mut server)?; - assert_eq!(server_frame.channel_id, "schema"); - assert_eq!(server_frame.payload, b"client-state"); - Ok(()) -} - -#[test] -fn cultmesh_facade_connects_authorized_rudp_client_for_schema_messages() -> Result<()> { - let connection_id = 0x2030_4051; - let options = CultMeshRudpSocketOptions { - resend_delay_ms: 25, - max_fragment_bytes: Some(1024), - max_pending_reliable_packets: Some(16), - ..CultMeshRudpSocketOptions::default() - }; - let server = Arc::new(Mutex::new(CultMesh::create_rudp_server( - "rust-cultmesh-connected-server", - connection_id, - options.clone(), - )?)); - let server_port = server - .lock() - .expect("RUDP server mutex poisoned") - .profile - .transports[0] - .port - .expect("RUDP server profile advertises its local port"); - let endpoint = CultMesh::parse_rudp_endpoint(&format!("rudp://127.0.0.1:{server_port}"))?; - let peer = CultMeshPeerCard::new("rust-cultmesh-connected-server", "local", [endpoint.uri()]) - .with_roles(["schema"]) - .with_authority_lease_id("lease:rust-cultmesh-connected-server"); - let mut peers = CultMesh::create_peer_catalog(); - peers.upsert(peer)?; - let now = Utc::now(); - let mut leases = CultMesh::create_authority_lease_catalog(); - leases.upsert(CultMeshAuthorityLease { - lease_id: "lease:rust-cultmesh-connected-server".to_string(), - verse_id: "local".to_string(), - peer_id: "rust-cultmesh-connected-server".to_string(), - roles: vec!["schema".to_string()], - shard_ids: Vec::new(), - issuer_runtime_id: Some("odin".to_string()), - valid_from: now - Duration::minutes(1), - expires_at: now + Duration::minutes(1), - })?; - - let done = Arc::new(AtomicBool::new(false)); - let pump = pump_rudp_server_until_connected(server.clone(), done.clone()); - let mut client = CultMesh::connect_rudp_client_for_authorized_peer( - "rust-cultmesh-connected-client", - connection_id, - &peers, - &leases, - "local", - "schema", - None, - now, - CultMeshRudpClientOptions { - socket_options: options, - connect_payload: b"join".to_vec(), - connect_timeout: StdDuration::from_secs(1), - poll_interval: StdDuration::from_millis(5), - }, - )?; - done.store(true, Ordering::SeqCst); - pump.join() - .map_err(|_| anyhow::anyhow!("RUDP server pump panicked"))??; - - assert!(client.connected()); - assert!( - server - .lock() - .expect("RUDP server mutex poisoned") - .connected() - ); - client.send_schema_message(&CultNetMessage::SchemaCatalogRequest { - message_id: "rust-cultmesh-rudp-schema-catalog".to_string(), - include_schema_json: Some(true), - schema_ids: None, - kinds: Some(vec![CultNetSchemaKind::DocumentPayload]), - })?; - let request = { - let mut server = server.lock().expect("RUDP server mutex poisoned"); - receive_rudp_schema_message(&mut server)? - }; - let CultNetMessage::SchemaCatalogRequest { - message_id, kinds, .. - } = request - else { - anyhow::bail!("RUDP schema message did not decode as schema catalog request"); - }; - assert_eq!(message_id, "rust-cultmesh-rudp-schema-catalog"); - assert_eq!(kinds, Some(vec![CultNetSchemaKind::DocumentPayload])); - assert_eq!( - client.profile.transports[0].protocol, - CultNetTransportProtocol::Rudp - ); - Ok(()) -} - -#[test] -fn cultmesh_facade_exposes_schema_registry_and_shard_catalog_owners() -> Result<()> { - let mut schemas = CultMesh::create_schema_registry(); - schemas.register(CultNetSchemaRegistration { - schema_id: "rust.cultmesh.note.v1".to_string(), - kind: CultNetSchemaKind::DocumentPayload, - wire_contracts: vec![CultNetWireContract::CultNetSchemaV0], - schema_version: Some("rust.cultmesh.note.v1".to_string()), - document_type: Some("rust.cultmesh.note".to_string()), - title: Some("Rust CultMesh Note".to_string()), - schema_json: Some( - r#"{"$id":"rust.cultmesh.note.v1","type":"object","properties":{"body":{"type":"string"}}}"# - .to_string(), - ), - })?; - assert_eq!( - schemas - .get("rust.cultmesh.note.v1", false) - .expect("registered schema is discoverable") - .document_type - .as_deref(), - Some("rust.cultmesh.note") - ); - - let builtins = CultMesh::create_builtin_schema_registry()?; - let shared_contracts = builtins.list(&cultnet_rs::CultNetSchemaCatalogOptions { - include_schema_json: false, - schema_ids: None, - kinds: Some(vec![CultNetSchemaKind::SharedContract]), - }); - assert!(shared_contracts.iter().any(|schema| { - schema.schema_id - == "https://github.com/GameCult/cultnet-ts/contracts/cultnet.transport-profile.schema.json" - && schema.schema_version.as_deref() == Some("cultnet.transport_profile.v0") - })); - - let mut shards = CultMesh::create_shard_catalog(); - shards.upsert(CultNetShardDescriptor { - shard_id: "rust-notes-a".to_string(), - owner_runtime_id: "rust-cultmesh".to_string(), - epoch: 7, - is_primary: Some(true), - schema_ids: vec!["rust.cultmesh.note.v1".to_string()], - key_prefix: Some("note:".to_string()), - primary_endpoints: vec!["rudp://127.0.0.1:4100".to_string()], - replica_endpoints: Vec::new(), - read_replica_endpoints: Vec::new(), - region: None, - authority_lease_id: Some("lease:rust-notes-a".to_string()), - })?; - let matching = shards.list(&cultnet_rs::CultNetShardCatalogOptions { - schema_ids: Some(vec!["rust.cultmesh.note.v1".to_string()]), - record_keys: Some(vec!["note:1".to_string()]), - }); - assert_eq!(matching.len(), 1); - assert_eq!(matching[0].shard_id, "rust-notes-a"); - assert!(shards.get("rust-notes-a").is_some()); - Ok(()) -} - -#[test] -fn cultmesh_authority_leases_gate_peer_contact_hints() -> Result<()> { - let now = Utc::now(); - let peer = CultMeshPeerCard::new("rust-peer", "public", ["rudp://127.0.0.1:4100"]) - .with_roles(["shard-primary"]) - .with_authority_lease_id("lease:rust-peer"); - let mut leases = CultMesh::create_authority_lease_catalog(); - let mut peers = CultMesh::create_peer_catalog(); - peers.upsert(peer.clone())?; - - assert!(!leases.is_authorized(&peer, "shard-primary", Some("players"), now)); - assert!( - peers - .find_authorized("public", "shard-primary", &leases, Some("players"), now) - .is_empty() - ); - leases.upsert(CultMeshAuthorityLease { - lease_id: "lease:rust-peer".to_string(), - verse_id: "public".to_string(), - peer_id: "rust-peer".to_string(), - roles: vec!["shard-primary".to_string()], - shard_ids: vec!["players".to_string()], - issuer_runtime_id: Some("odin".to_string()), - valid_from: now - Duration::minutes(1), - expires_at: now + Duration::minutes(1), - })?; - - assert!(leases.is_authorized(&peer, "shard-primary", Some("players"), now)); - assert_eq!( - peers.find_authorized("public", "shard-primary", &leases, Some("players"), now), - vec![peer.clone()] - ); - assert_eq!( - peers.first_authorized("public", "shard-primary", &leases, Some("players"), now), - Some(peer.clone()) - ); - assert!(!leases.is_authorized(&peer, "schema", Some("players"), now)); - assert!( - peers - .first_authorized("public", "schema", &leases, Some("players"), now) - .is_none() - ); - assert!(!leases.is_authorized(&peer, "shard-primary", Some("inventory"), now)); - assert!(!leases.is_authorized( - &peer, - "shard-primary", - Some("players"), - now + Duration::minutes(1) - )); - Ok(()) -} - -#[test] -fn legacy_gamecult_networking_contract_round_trips_login_union() -> Result<()> { - let message = CultNetMessage::Login { - nonce: CultNetSecret::to_base64_url(b"nonce"), - auth: CultNetSecret::to_base64_url(b"auth"), - password: CultNetSecret::to_base64_url(b"password"), - }; - let wire = - encode_cultnet_message_for_wire(&message, CultNetWireContract::GameCultNetworkingV0)?; - let items = wire.as_array().expect("legacy union is an array"); - assert_eq!(items[0].as_i64(), Some(0)); - assert_eq!( - items[1].as_array().unwrap()[0].as_slice(), - Some(&b"nonce"[..]) - ); - - let bytes = rmp_serde::to_vec(&wire)?; - let decoded = - decode_cultnet_message_from_slice(&bytes, CultNetWireContract::GameCultNetworkingV0)?; - assert_eq!(decoded, message); - Ok(()) -} - -#[test] -fn rust_decodes_typescript_generated_cultnet_frames() -> Result<()> { - let mut framer = LengthPrefixedMessageFramer::new(); - let hello_frames = framer.push(TS_HELLO_FRAME); - assert_eq!(hello_frames.len(), 1); - let hello = - decode_cultnet_message_from_slice(&hello_frames[0], CultNetWireContract::CultNetSchemaV0)?; - assert_eq!( - hello, - CultNetMessage::Hello { - runtime_id: "voidbot-main".to_string(), - runtime_kind: "node-worker".to_string(), - agent_id: Some("void".to_string()), - role: None, - display_name: Some("Void".to_string()), - supported_document_types: Some(vec!["ghostlight.agent-state".to_string()]), - supported_mutation_contracts: None, - supported_message_versions: None, - transport_profiles: None, - supports_schema_catalog: None, - } - ); - - let mut framer = LengthPrefixedMessageFramer::new(); - let login_frames = framer.push(TS_LEGACY_LOGIN_FRAME); - assert_eq!(login_frames.len(), 1); - let login = decode_cultnet_message_from_slice( - &login_frames[0], - CultNetWireContract::GameCultNetworkingV0, - )?; - assert_eq!( - login, - CultNetMessage::Login { - nonce: "bm9uY2U".to_string(), - auth: "YXV0aA".to_string(), - password: "cGFzc3dvcmQ".to_string(), - } - ); - Ok(()) -} - -#[test] -fn document_registry_advertises_mutation_contracts_with_bindings() -> Result<()> { - let mut registry = CultNetDocumentRegistry::new(); - let contract = CultNetDocumentMutationContract { - document_type: "ghostlight.agent-state".to_string(), - payload_schema_version: Some("ghostlight.agent_state.v0".to_string()), - operations: vec![ - CultNetDocumentOperation::Snapshot, - CultNetDocumentOperation::IntentSubmit, - CultNetDocumentOperation::ReceiptWatch, - ], - authority: CultNetMutationAuthority::Coordinator, - intent_document_types: Some(vec!["ghostlight.agent-state.intent.v0".to_string()]), - receipt_document_types: Some(vec!["ghostlight.agent-state.receipt.v0".to_string()]), - notes: Some(vec![ - "Intent documents are reviewed before cache mutation.".to_string(), - ]), - }; - registry.register( - CultNetDocumentBinding::for_entry::( - "ghostlight.agent_state.v0".to_string(), - ) - .with_mutation_contract(contract.clone()), - ); - - assert_eq!(registry.mutation_contracts(), vec![contract]); - Ok(()) -} - -#[test] -fn document_registry_replicates_typed_cultcache_state() -> Result<()> { - let temp = tempfile::tempdir()?; - let origin_store = temp.path().join("origin.msgpack"); - let target_store = temp.path().join("target.msgpack"); - let payload = GhostlightAgentStateFixture { - schema_version: "ghostlight.agent_state.v0".to_string(), - agent_id: "epiphany.persona".to_string(), - display_name: "Persona".to_string(), - }; - - let mut registry = CultNetDocumentRegistry::new(); - registry.register(CultNetDocumentBinding::for_entry::< - GhostlightAgentStateFixture, - >("ghostlight.agent_state.v0".to_string())); - - let mut origin = CultCache::new(); - origin.register_entry_type::()?; - origin.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&origin_store)); - origin.pull_all_backing_stores()?; - origin.put("epiphany.persona", &payload)?; - - let snapshot = registry.create_snapshot_response(&origin, "snapshot-1", None, None)?; - - let mut target = CultCache::new(); - target.register_entry_type::()?; - target.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store)); - target.pull_all_backing_stores()?; - let applied = - registry.apply_snapshot_response::(&mut target, &snapshot)?; - let synced = registry.sync_document_from_snapshot_response::( - &mut target, - &snapshot, - "epiphany.persona", - )?; - assert_eq!(applied, vec![payload.clone()]); - assert_eq!(synced, payload); - assert_eq!( - target.get_required::("epiphany.persona")?, - payload - ); - - let direct_put = registry.create_document_put_message( - "put-1", - "epiphany.persona", - &GhostlightAgentStateFixture { - display_name: "Persona Prime".to_string(), - ..payload - }, - CultNetDocumentPutOptions::default(), - )?; - let updated = registry - .apply_document_put_message::(&mut target, &direct_put)?; - assert_eq!(updated.display_name, "Persona Prime"); - Ok(()) -} - -#[test] -fn raw_snapshot_replication_preserves_messagepack_payload_bytes() -> Result<()> { - let temp = tempfile::tempdir()?; - let origin_store = temp.path().join("origin-raw.msgpack"); - let target_store = temp.path().join("target-raw.msgpack"); - let payload = GhostlightAgentStateFixture { - schema_version: "ghostlight.agent_state.v0".to_string(), - agent_id: "epiphany.persona".to_string(), - display_name: "Persona".to_string(), - }; - - let mut registry = CultNetDocumentRegistry::new(); - registry.register(CultNetDocumentBinding::for_entry::< - GhostlightAgentStateFixture, - >("ghostlight.agent_state.v0".to_string())); - - let mut origin = CultCache::new(); - origin.register_entry_type::()?; - origin.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&origin_store)); - origin.pull_all_backing_stores()?; - origin.put("epiphany.persona", &payload)?; - - let raw_snapshot = - registry.create_raw_snapshot_response(&origin, "raw-snapshot-1", None, None)?; - let source_envelope = - origin.get_required_envelope::("epiphany.persona")?; - - let mut target = CultCache::new(); - target.register_entry_type::()?; - target.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store)); - target.pull_all_backing_stores()?; - let applied = registry - .apply_raw_snapshot_response::(&mut target, &raw_snapshot)?; - let target_envelope = - target.get_required_envelope::("epiphany.persona")?; - - assert_eq!(applied, vec![payload.clone()]); - assert_eq!(target_envelope.payload, source_envelope.payload); - assert_eq!( - target.get_required::("epiphany.persona")?, - payload - ); - Ok(()) -} - -#[test] -fn raw_snapshot_replication_hydrates_same_schema_rust_aliases() -> Result<()> { - let temp = tempfile::tempdir()?; - let origin_store = temp.path().join("origin-alias.msgpack"); - let target_store = temp.path().join("target-alias.msgpack"); - let canonical = GhostlightAgentStateFixture { - schema_version: "ghostlight.agent_state.v0".to_string(), - agent_id: "epiphany.persona".to_string(), - display_name: "Persona".to_string(), - }; - - let mut origin_registry = CultNetDocumentRegistry::new(); - origin_registry.register(CultNetDocumentBinding::for_entry_with_schema_id::< - GhostlightAgentStateFixture, - >( - "ghostlight.agent_state.v0".to_string(), - "ghostlight.agent_state.v0".to_string(), - )); - let mut origin = CultCache::new(); - origin.register_entry_type::()?; - origin.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&origin_store)); - origin.pull_all_backing_stores()?; - origin.put("epiphany.persona", &canonical)?; - let raw_snapshot = origin_registry.create_raw_snapshot_response( - &origin, - "raw-snapshot-alias", - Some(&["ghostlight.agent_state.v0".to_string()]), - Some(&["epiphany.persona".to_string()]), - )?; - - let mut alias_registry = CultNetDocumentRegistry::new(); - alias_registry.register(CultNetDocumentBinding::for_entry_with_schema_id::< - GhostlightAgentStateUiFixture, - >( - "ghostlight.agent_state.v0".to_string(), - "ghostlight.agent_state.v0".to_string(), - )); - let mut target = CultCache::new(); - target.register_entry_type::()?; - target.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store)); - target.pull_all_backing_stores()?; - - let applied = alias_registry - .sync_raw_document_from_snapshot_response::( - &mut target, - &raw_snapshot, - "epiphany.persona", - )?; - - assert_eq!( - applied, - GhostlightAgentStateUiFixture { - schema_version: "ghostlight.agent_state.v0".to_string(), - agent_id: "epiphany.persona".to_string(), - display_name: "Persona".to_string(), - } - ); - assert_eq!( - target.get_required::("epiphany.persona")?, - applied - ); - Ok(()) -} - -#[test] -fn reactive_document_coalesces_direct_same_schema_alias_member_writes() -> Result<()> { - let temp = tempfile::tempdir()?; - let target_store = temp.path().join("target-reactive-alias.msgpack"); - let mut registry = CultNetDocumentRegistry::new(); - registry.register(CultNetDocumentBinding::for_entry_with_schema_id::< - GhostlightReactiveNoteUiFixture, - >( - "ghostlight.reactive_note.v0".to_string(), - "ghostlight.reactive_note.v0".to_string(), - )); - let cache = Arc::new(Mutex::new(CultCache::new())); - { - let mut cache = cache.lock().expect("cache mutex"); - cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store)); - cache.pull_all_backing_stores()?; - cache.put( - "note:reactive", - &GhostlightReactiveNoteUiFixture { - body: "initial".to_string(), - revision: 1, - }, - )?; - } - - let reactive = registry.reactive_document::( - Arc::clone(&cache), - "note:reactive", - CultNetReactiveDocumentOptions { - flush_delay: StdDuration::from_millis(5), - ..CultNetReactiveDocumentOptions::default() - }, - )?; - { - let current = reactive.current(); - let mut current = current.lock().expect("reactive current mutex"); - current.body = "first-local-edit".to_string(); - current.body = "second-local-edit".to_string(); - current.revision = 2; - } - - for _ in 0..30 { - let body = cache - .lock() - .expect("cache mutex") - .get_required::("note:reactive")? - .body; - if body == "second-local-edit" { - break; - } - thread::sleep(StdDuration::from_millis(5)); - } - - let stored = cache - .lock() - .expect("cache mutex") - .get_required::("note:reactive")?; - assert_eq!(stored.body, "second-local-edit"); - assert_eq!(stored.revision, 2); - assert!(!reactive.is_dirty()); - assert!(reactive.last_error().is_none()); - Ok(()) -} - -#[test] -fn reactive_document_tracks_canonical_reconciliation_delta() -> Result<()> { - let temp = tempfile::tempdir()?; - let target_store = temp.path().join("target-reactive-reconcile.msgpack"); - let mut registry = CultNetDocumentRegistry::new(); - registry.register(CultNetDocumentBinding::for_entry_with_schema_id::< - GhostlightReactiveNoteFixture, - >( - "ghostlight.reactive_note.v0".to_string(), - "ghostlight.reactive_note.v0".to_string(), - )); - let cache = Arc::new(Mutex::new(CultCache::new())); - { - let mut cache = cache.lock().expect("cache mutex"); - cache.register_entry_type::()?; - cache.add_generic_backing_store(SingleFileMessagePackBackingStore::new(&target_store)); - cache.pull_all_backing_stores()?; - cache.put( - "note:reconcile", - &GhostlightReactiveNoteFixture { - body: "initial".to_string(), - revision: 1, - }, - )?; - } - - let reactive = registry.reactive_document::( - Arc::clone(&cache), - "note:reconcile", - CultNetReactiveDocumentOptions { - flush_delay: StdDuration::from_secs(60), - detect_local_changes: false, - ..CultNetReactiveDocumentOptions::default() - }, - )?; - reactive.update(|note| { - note.body = "local-prediction".to_string(); - note.revision = 2; - })?; - let authoritative = registry.create_document_put_message( - "put-authoritative", - "note:reconcile", - &GhostlightReactiveNoteFixture { - body: "authoritative".to_string(), - revision: 7, - }, - CultNetDocumentPutOptions::default(), - )?; - - reactive.apply_document_put_message(&authoritative)?; - - let current = reactive.current(); - let current = current.lock().expect("reactive current mutex").clone(); - assert_eq!(current.body, "local-prediction"); - let reconciliation = reactive - .reconciliation() - .expect("dirty canonical apply records reconciliation"); - assert_eq!(reconciliation.canonical.body, "authoritative"); - assert_eq!(reconciliation.predicted.body, "local-prediction"); - assert_eq!( - reconciliation.delta.get("0"), - Some(&serde_json::json!("local-prediction")) - ); - assert_eq!( - reconciliation.delta.get("1"), - Some(&serde_json::json!(-5.0)) - ); - - reactive.flush()?; - let stored = cache - .lock() - .expect("cache mutex") - .get_required::("note:reconcile")?; - assert_eq!(stored.body, "local-prediction"); - assert!(reactive.reconciliation().is_none()); - Ok(()) -} - -#[test] -fn client_security_keeps_the_connection_key_visible_without_exposing_cipher_logic() -> Result<()> { - let options = CultNetClientSecurityOptions::development(); - assert_eq!(options.connection_key, "gamecult-dev-connection-key"); - assert_ne!(options.encryption_key(), [0_u8; 32]); - Ok(()) -} - -#[test] -fn builtin_schema_registry_advertises_canonical_ghostlight_schema_without_inline_body_by_default() --> Result<()> { - let registry = builtin_schema_registry()?; - let response = registry.create_catalog_response(&CultNetMessage::SchemaCatalogRequest { - message_id: "catalog-1".to_string(), - include_schema_json: None, - schema_ids: None, - kinds: None, - })?; - - let CultNetMessage::SchemaCatalogResponse { schemas, .. } = response else { - panic!("expected catalog response"); - }; - - let ghostlight = schemas - .iter() - .find(|schema| schema.document_type.as_deref() == Some("ghostlight.agent-state")) - .expect("ghostlight agent-state schema is advertised"); - - assert_eq!(ghostlight.kind, CultNetSchemaKind::DocumentPayload); - assert_eq!( - ghostlight.schema_version.as_deref(), - Some("ghostlight.agent_state.v0") - ); - assert_eq!(ghostlight.schema_json, None); - assert!(!ghostlight.content_hash.is_empty()); - - let transport_profile = schemas - .iter() - .find(|schema| schema.schema_version.as_deref() == Some("cultnet.transport_profile.v0")) - .expect("transport profile schema is advertised"); - - assert_eq!(transport_profile.kind, CultNetSchemaKind::SharedContract); - assert_eq!( - transport_profile.schema_id, - "https://github.com/GameCult/cultnet-ts/contracts/cultnet.transport-profile.schema.json" - ); - assert_eq!( - transport_profile.wire_contracts, - vec![CultNetWireContract::CultNetSchemaV0] - ); - Ok(()) -} - -#[test] -fn builtin_schema_registry_advertises_shard_catalog_wire_messages() -> Result<()> { - let registry = builtin_schema_registry()?; - let response = registry.create_catalog_response(&CultNetMessage::SchemaCatalogRequest { - message_id: "catalog-shards".to_string(), - include_schema_json: None, - schema_ids: None, - kinds: Some(vec![CultNetSchemaKind::WireMessage]), - })?; - - let CultNetMessage::SchemaCatalogResponse { schemas, .. } = response else { - panic!("expected catalog response"); - }; - - assert!(schemas.iter().any(|schema| { - schema.schema_version.as_deref() == Some("cultnet.shard_catalog_request.v0") - })); - assert!(schemas.iter().any(|schema| { - schema.schema_version.as_deref() == Some("cultnet.shard_catalog_response.v0") - })); - Ok(()) -} - -#[test] -fn shard_catalog_filters_descriptors_and_applies_remote_responses() -> Result<()> { - let mut catalog = CultNetShardCatalog::new(); - catalog.upsert(CultNetShardDescriptor { - shard_id: "notes-a".to_string(), - owner_runtime_id: "rust-primary".to_string(), - epoch: 3, - is_primary: Some(true), - schema_ids: vec!["note.v0".to_string()], - key_prefix: Some("note:".to_string()), - primary_endpoints: vec!["rudp://127.0.0.1:4100".to_string()], - replica_endpoints: vec![], - read_replica_endpoints: vec![], - region: Some("local".to_string()), - authority_lease_id: Some("lease:notes-a".to_string()), - })?; - catalog.upsert(CultNetShardDescriptor { - shard_id: "players-a".to_string(), - owner_runtime_id: "rust-primary".to_string(), - epoch: 1, - is_primary: Some(false), - schema_ids: vec!["player.v0".to_string()], - key_prefix: Some("player:".to_string()), - primary_endpoints: vec!["cultnet://127.0.0.1:3075".to_string()], - replica_endpoints: vec![], - read_replica_endpoints: vec![], - region: None, - authority_lease_id: None, - })?; - - let response = catalog.create_catalog_response(&CultNetMessage::ShardCatalogRequest { - message_id: "shards".to_string(), - schema_ids: Some(vec!["note.v0".to_string()]), - record_keys: Some(vec!["note:1".to_string()]), - })?; - - let CultNetMessage::ShardCatalogResponse { message_id, shards } = &response else { - panic!("expected shard catalog response"); - }; - assert_eq!(message_id, "shards"); - assert_eq!(shards.len(), 1); - assert_eq!(shards[0].shard_id, "notes-a"); - - let bytes = encode_cultnet_message_to_vec(&response, CultNetWireContract::CultNetSchemaV0)?; - let decoded = decode_cultnet_message_from_slice(&bytes, CultNetWireContract::CultNetSchemaV0)?; - assert_eq!(decoded, response); - - let mut applied = CultNetShardCatalog::new(); - let applied_shards = applied.apply_response(&decoded)?; - assert_eq!(applied_shards.len(), 1); - assert!(applied.get("notes-a").is_some_and(|shard| { - shard.serves(Some("note.v0"), Some("note:2")) - && !shard.serves(Some("note.v0"), Some("player:2")) - })); - Ok(()) -} - -#[test] -fn schema_discovery_round_trips_over_legacy_gamecult_contract_when_inline_schemas_are_requested() --> Result<()> { - let registry = { - let mut registry = CultNetSchemaRegistry::new(); - registry.register(cultnet_rs::CultNetSchemaRegistration { - schema_id: "https://example.test/contracts/example.schema.json".to_string(), - kind: CultNetSchemaKind::SharedContract, - wire_contracts: vec![ - CultNetWireContract::CultNetSchemaV0, - CultNetWireContract::GameCultNetworkingV0, - ], - schema_version: None, - document_type: None, - title: Some("Example Schema".to_string()), - schema_json: Some( - r#"{ - "$schema":"https://json-schema.org/draft/2020-12/schema", - "$id":"https://example.test/contracts/example.schema.json", - "title":"Example Schema", - "type":"object", - "properties":{"value":{"type":"string"}}, - "required":["value"], - "additionalProperties":false - }"# - .to_string(), - ), - })?; - registry - }; - - let response = registry.create_catalog_response(&CultNetMessage::SchemaCatalogRequest { - message_id: "catalog-legacy".to_string(), - include_schema_json: Some(true), - schema_ids: None, - kinds: None, - })?; - let wire = - encode_cultnet_message_for_wire(&response, CultNetWireContract::GameCultNetworkingV0)?; - let bytes = rmp_serde::to_vec(&wire)?; - let decoded = - decode_cultnet_message_from_slice(&bytes, CultNetWireContract::GameCultNetworkingV0)?; - - let CultNetMessage::SchemaCatalogResponse { - message_id, - schemas, - } = decoded - else { - panic!("expected legacy schema catalog response"); - }; - - assert_eq!(message_id, "catalog-legacy"); - assert_eq!( - schemas[0].schema_id, - "https://example.test/contracts/example.schema.json" - ); - assert!( - schemas[0] - .schema_json - .as_deref() - .is_some_and(|schema| schema.contains("\"value\"")) - ); - Ok(()) -} diff --git a/vendor/cultnet-rs/tests/fixtures/cultnet-ts-hello.frame b/vendor/cultnet-rs/tests/fixtures/cultnet-ts-hello.frame deleted file mode 100644 index e2b22d50048ca9f140927ecaa1a9f5ec70affe1d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 160 zcmXZUu?oU45P;#nP>T=YQUoXI9-E^%OzuMNQt1o0yE_&{qTtl$vx@7_$6r;|?edb| zK`9voKBGLW&qOG&R<+C oO7v#hn*ejShDCCWoD}Sq`y5tzm?Fq^861mx9Go`4K`m+&KGeNK7ytkO diff --git a/vendor/cultnet-rs/tests/fixtures/cultnet-ts-legacy-login.frame b/vendor/cultnet-rs/tests/fixtures/cultnet-ts-legacy-login.frame deleted file mode 100644 index c6f335d37783e6ab66bf1cdfa02137995209a860..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30 lcmZQzV33-`F!=~;UVdJ3>JgU2(vplL90iHR#pU@$DFBis3e^Ar diff --git a/vendor/cultnet-rs/tests/provider_session.rs b/vendor/cultnet-rs/tests/provider_session.rs deleted file mode 100644 index 4ad7c23..0000000 --- a/vendor/cultnet-rs/tests/provider_session.rs +++ /dev/null @@ -1,365 +0,0 @@ -use anyhow::Result; -use cultnet_rs::*; - -const REGISTRATION_GOLDEN: &str = "hqpwcm92aWRlcklkqGFldGhlcmlhsXNlcnZpY2VJbnN0YW5jZUlkq2FldGhlcmlhLTQyqmVuZHBvaW50SWSvYWV0aGVyaWEtcHVibGljp3ZlcnNlSWSmcHVibGljuHJlcXVlc3RlZExlYXNlRHVyYXRpb25Nc811MLBhdXRob3JpdHlMZWFzZUlkq2F1dGhvcml0eS03"; -const CONNECT_EVIDENCE_GOLDEN: &str = - "gq9jbGllbnRTZXNzaW9uSWSyYWV0aGVyaWEtY2xpZW50LTQyrHNlc3Npb25Ub2tlbrJvZGluLXNlc3Npb24tdG9rZW4="; -const TOKENLESS_CONNECT_EVIDENCE_GOLDEN: &str = - "gq9jbGllbnRTZXNzaW9uSWSwYW5vbnltb3VzLWNsaWVudKxzZXNzaW9uVG9rZW7A"; - -fn registration() -> CultMeshProviderRegistrationWire { - CultMeshProviderRegistrationWire { - provider_id: "aetheria".to_string(), - service_instance_id: "aetheria-42".to_string(), - endpoint_id: "aetheria-public".to_string(), - verse_id: "public".to_string(), - requested_lease_duration_ms: 30_000, - authority_lease_id: Some("authority-7".to_string()), - } -} - -#[test] -fn registration_matches_csharp_and_typescript_golden_payload() -> Result<()> { - let encoded = encode_provider_session_payload(®istration())?; - assert_eq!(encoded, REGISTRATION_GOLDEN); - assert_eq!( - decode_provider_session_payload::(&encoded)?, - registration() - ); - Ok(()) -} - -#[test] -fn connect_evidence_is_a_validated_named_messagepack_map() -> Result<()> { - let evidence = CultMeshProviderConnectEvidenceWire { - client_session_id: "aetheria-client-42".to_string(), - session_token: Some("odin-session-token".to_string()), - }; - let encoded = encode_provider_connect_evidence(&evidence)?; - assert_eq!( - base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &encoded), - CONNECT_EVIDENCE_GOLDEN - ); - assert_eq!(decode_provider_connect_evidence(&encoded)?, evidence); - let value: rmpv::Value = rmp_serde::from_slice(&encoded)?; - let map = value.as_map().expect("Connect evidence is a named map"); - assert_eq!( - map.iter() - .find_map(|(key, value)| (key.as_str() == Some("clientSessionId")).then_some(value)) - .and_then(rmpv::Value::as_str), - Some("aetheria-client-42") - ); - assert_eq!( - map.iter() - .find_map(|(key, value)| (key.as_str() == Some("sessionToken")).then_some(value)) - .and_then(rmpv::Value::as_str), - Some("odin-session-token") - ); - - assert!( - encode_provider_connect_evidence(&CultMeshProviderConnectEvidenceWire { - client_session_id: String::new(), - session_token: None, - }) - .is_err() - ); - assert!( - encode_provider_connect_evidence(&CultMeshProviderConnectEvidenceWire { - client_session_id: "client".to_string(), - session_token: Some(" ".to_string()), - }) - .is_err() - ); - assert!(decode_provider_connect_evidence(&[]).is_err()); - let tokenless = CultMeshProviderConnectEvidenceWire { - client_session_id: "anonymous-client".to_string(), - session_token: None, - }; - let tokenless_encoded = encode_provider_connect_evidence(&tokenless)?; - assert_eq!( - base64::Engine::encode( - &base64::engine::general_purpose::STANDARD, - &tokenless_encoded - ), - TOKENLESS_CONNECT_EVIDENCE_GOLDEN - ); - assert_eq!( - decode_provider_connect_evidence(&tokenless_encoded)?, - tokenless - ); - Ok(()) -} - -#[test] -fn registration_round_trips_inside_canonical_operation_envelope() -> Result<()> { - let request = create_provider_session_request( - "register-1", - CULTMESH_PROVIDER_REGISTER_OPERATION, - CULTMESH_PROVIDER_REGISTRATION_SCHEMA, - ®istration(), - Some("aetheria-42".to_string()), - Some("odin".to_string()), - )?; - let bytes = encode_cultnet_message_to_vec(&request, CultNetWireContract::CultNetSchemaV0)?; - let decoded_request = - decode_cultnet_message_from_slice(&bytes, CultNetWireContract::CultNetSchemaV0)?; - let decoded: CultMeshProviderRegistrationWire = decode_provider_session_request( - &decoded_request, - CULTMESH_PROVIDER_REGISTER_OPERATION, - CULTMESH_PROVIDER_REGISTRATION_SCHEMA, - )?; - assert_eq!(decoded, registration()); - - let lease = CultMeshProviderLeaseWire { - provider_id: "aetheria".to_string(), - service_instance_id: "aetheria-42".to_string(), - endpoint_id: "aetheria-public".to_string(), - verse_id: "public".to_string(), - lease_id: "lease-2".to_string(), - valid_from_utc: "2026-07-14T12:00:00Z".to_string(), - expires_at_utc: "2026-07-14T12:00:10Z".to_string(), - }; - let response = create_provider_session_response( - &decoded_request, - CultMeshProviderOperationStatus::Ok, - CULTMESH_PROVIDER_LEASE_SCHEMA, - &lease, - Some("odin".to_string()), - )?; - let (status, decoded_lease) = decode_provider_session_response::( - &response, - CULTMESH_PROVIDER_REGISTER_OPERATION, - CULTMESH_PROVIDER_LEASE_SCHEMA, - )?; - assert_eq!(status, CultMeshProviderOperationStatus::Ok); - assert_eq!(decoded_lease, lease); - Ok(()) -} - -#[test] -fn publication_and_command_preserve_typed_messagepack_values() -> Result<()> { - let publication = CultMeshProviderPublicationPutWire { - lease_id: "lease-1".to_string(), - publication_id: "surface:pilot".to_string(), - document: CultNetRawDocumentRecord { - schema_id: "gamecult.eve.surface.v1".to_string(), - record_key: "eve:surface:pilot".to_string(), - stored_at: String::new(), - payload_encoding: CultNetRawPayloadEncoding::Messagepack, - payload: vec![0x81, 0xa2, 0x69, 0x64, 0x01], - source_runtime_id: Some("aetheria-42".to_string()), - source_agent_id: None, - source_role: None, - tags: None, - }, - }; - let publication_encoded = encode_provider_session_payload(&publication)?; - assert_eq!( - decode_provider_session_payload::( - &publication_encoded - )?, - publication - ); - - let command = CultMeshProviderCommandWire { - command_id: "cmd-7".to_string(), - command_kind: "pilot.thrust".to_string(), - provider_id: "aetheria".to_string(), - service_instance_id: "aetheria-42".to_string(), - payload: rmpv::Value::Map(vec![(rmpv::Value::from("axis"), rmpv::Value::F64(0.75))]), - }; - let command_bytes = base64::Engine::decode( - &base64::engine::general_purpose::STANDARD, - encode_provider_session_payload(&command)?, - )?; - let decoded_command = decode_provider_command_document(&CultNetRawDocumentRecord { - schema_id: CULTMESH_PROVIDER_COMMAND_SCHEMA.to_string(), - record_key: "provider-command:aetheria:aetheria-42:cmd-7".to_string(), - stored_at: String::new(), - payload_encoding: CultNetRawPayloadEncoding::Messagepack, - payload: command_bytes, - source_runtime_id: Some("odin".to_string()), - source_agent_id: None, - source_role: Some("provider-session-broker".to_string()), - tags: None, - })?; - assert_eq!(decoded_command, command); - Ok(()) -} - -#[test] -fn lifecycle_validation_rejects_split_or_malformed_authority() { - let mismatched = create_provider_session_request( - "bad-schema", - CULTMESH_PROVIDER_REGISTER_OPERATION, - CULTMESH_PROVIDER_PUBLICATION_PUT_SCHEMA, - ®istration(), - None, - None, - ); - assert!(mismatched.is_err()); - - let malformed_lease = CultMeshProviderLeaseWire { - provider_id: "provider".to_string(), - service_instance_id: "instance".to_string(), - endpoint_id: "endpoint".to_string(), - verse_id: "verse".to_string(), - lease_id: "lease".to_string(), - valid_from_utc: "2026-07-14T12:00:10Z".to_string(), - expires_at_utc: "2026-07-14T12:00:00Z".to_string(), - }; - assert!(encode_provider_session_payload(&malformed_lease).is_err()); - - let malformed_request = CultNetMessage::OperationRequest { - message_id: String::new(), - service_id: CULTMESH_PROVIDER_SESSION_SERVICE_ID.to_string(), - operation: String::new(), - payload_schema: CULTMESH_PROVIDER_REGISTRATION_SCHEMA.to_string(), - payload_encoding: CULTMESH_PROVIDER_SESSION_PAYLOAD_ENCODING.to_string(), - payload: REGISTRATION_GOLDEN.to_string(), - source_runtime_id: None, - target_runtime_id: None, - }; - assert!( - create_provider_session_response( - &malformed_request, - CultMeshProviderOperationStatus::Invalid, - CULTMESH_PROVIDER_MUTATION_ACCEPTANCE_SCHEMA, - &CultMeshProviderMutationAcceptanceWire { - accepted_at_utc: "2026-07-14T12:00:00Z".to_string(), - lease_id: None, - publication_id: None, - command_id: None, - receipt_id: None, - }, - None, - ) - .is_err() - ); -} - -#[test] -fn remaining_lifecycle_payloads_round_trip_as_named_maps() -> Result<()> { - let renewal = CultMeshProviderLeaseRenewalWire { - lease_id: "lease-1".to_string(), - requested_lease_duration_ms: 30_000, - }; - assert_eq!( - decode_provider_session_payload::( - &encode_provider_session_payload(&renewal)? - )?, - renewal - ); - - let deletion = CultMeshProviderPublicationDeleteWire { - lease_id: "lease-1".to_string(), - publication_id: "surface:pilot".to_string(), - schema_id: "gamecult.eve.surface.v1".to_string(), - record_key: "eve:surface:pilot".to_string(), - }; - assert_eq!( - decode_provider_session_payload::( - &encode_provider_session_payload(&deletion)? - )?, - deletion - ); - - let receipt = CultMeshProviderReceiptPutWire { - lease_id: "lease-1".to_string(), - receipt: CultMeshProviderCommandReceiptWire { - receipt_id: "receipt-7".to_string(), - command_id: "cmd-7".to_string(), - command_kind: "pilot.thrust".to_string(), - provider_id: "aetheria".to_string(), - service_instance_id: "aetheria-42".to_string(), - state: CultMeshProviderReceiptStateWire::Applied, - completed_at_utc: "2026-07-14T12:01:02.345Z".to_string(), - result: Some(rmpv::Value::Map(vec![( - rmpv::Value::from("accepted"), - rmpv::Value::Boolean(true), - )])), - error: None, - }, - }; - assert_eq!( - decode_provider_session_payload::( - &encode_provider_session_payload(&receipt)? - )?, - receipt - ); - - let withdrawal = CultMeshProviderWithdrawalWire { - lease_id: "lease-1".to_string(), - }; - assert_eq!( - decode_provider_session_payload::( - &encode_provider_session_payload(&withdrawal)? - )?, - withdrawal - ); - - let acceptance = CultMeshProviderMutationAcceptanceWire { - accepted_at_utc: "2026-07-14T12:01:03Z".to_string(), - lease_id: Some("lease-1".to_string()), - publication_id: Some("surface:pilot".to_string()), - command_id: None, - receipt_id: None, - }; - assert_eq!( - decode_provider_session_payload::( - &encode_provider_session_payload(&acceptance)? - )?, - acceptance - ); - Ok(()) -} - -#[test] -fn non_ok_response_uses_correlated_empty_map_and_diagnostics() -> Result<()> { - let request = create_provider_session_request( - "register-denied", - CULTMESH_PROVIDER_REGISTER_OPERATION, - CULTMESH_PROVIDER_REGISTRATION_SCHEMA, - ®istration(), - Some("aetheria-42".to_string()), - Some("odin".to_string()), - )?; - let response = create_provider_session_error_response( - &request, - CultMeshProviderOperationStatus::Denied, - vec!["registration authority rejected the session".to_string()], - Some("odin".to_string()), - )?; - let CultNetMessage::OperationResponse { - message_id, - operation, - status, - payload_schema, - payload, - diagnostics, - .. - } = response - else { - panic!("expected operation response"); - }; - assert_eq!(message_id, "register-denied"); - assert_eq!(operation, CULTMESH_PROVIDER_REGISTER_OPERATION); - assert_eq!(status, "denied"); - assert_eq!(payload_schema, CULTMESH_PROVIDER_MUTATION_ACCEPTANCE_SCHEMA); - assert_eq!(diagnostics.len(), 1); - let bytes = base64::Engine::decode(&base64::engine::general_purpose::STANDARD, payload)?; - assert_eq!( - rmp_serde::from_slice::(&bytes)?, - rmpv::Value::Map(Vec::new()) - ); - assert!( - create_provider_session_error_response( - &request, - CultMeshProviderOperationStatus::Ok, - vec!["not an error".to_string()], - None, - ) - .is_err() - ); - Ok(()) -} diff --git a/vendor/cultnet-rs/tests/rudp_server_hub.rs b/vendor/cultnet-rs/tests/rudp_server_hub.rs deleted file mode 100644 index a8a2c11..0000000 --- a/vendor/cultnet-rs/tests/rudp_server_hub.rs +++ /dev/null @@ -1,459 +0,0 @@ -use std::net::UdpSocket; -use std::time::Duration; - -use anyhow::{Result, anyhow}; -use cultnet_rs::*; - -const CONNECTION_ID: u32 = 0x4355_4c54; - -fn socket() -> Result { - let socket = UdpSocket::bind("127.0.0.1:0")?; - socket.set_read_timeout(Some(Duration::from_millis(20)))?; - Ok(socket) -} - -fn client(remote: std::net::SocketAddr) -> Result { - let mut options = - CultNetRudpSocketTransportOptions::client("provider", socket()?, remote, CONNECTION_ID); - options.max_fragment_bytes = Some(2_048); - CultNetRudpSocketTransportConnection::new(options) -} - -fn connect( - hub: &mut CultNetRudpServerHub, - client: &mut CultNetRudpSocketTransportConnection, - evidence: &[u8], -) -> Result { - client.connect(evidence.to_vec())?; - let mut connected = None; - for _ in 0..20 { - if let Some(CultNetRudpServerEvent::Connected { session }) = hub.receive_event_once()? { - connected = Some(session); - break; - } - } - let session = connected.ok_or_else(|| anyhow!("hub did not receive Connect"))?; - let _ = client.receive_once()?; - assert!(client.connected()); - Ok(session) -} - -fn receive_frame(hub: &mut CultNetRudpServerHub) -> Result { - for _ in 0..20 { - if let Some(event @ CultNetRudpServerEvent::Frame { .. }) = hub.receive_event_once()? { - return Ok(event); - } - } - Err(anyhow!("hub did not receive a frame")) -} - -fn receive_message(client: &mut CultNetRudpSocketTransportConnection) -> Result { - for _ in 0..20 { - if let Some(message) = client.receive_schema_message_once()? { - return Ok(message); - } - } - Err(anyhow!("client did not receive a schema message")) -} - -#[test] -fn hub_keeps_independent_peer_sessions_and_exposes_connect_evidence() -> Result<()> { - let server_socket = socket()?; - let server_addr = server_socket.local_addr()?; - let mut hub = CultNetRudpServerHub::new(CultNetRudpServerHubOptions::new( - "odin", - server_socket, - CONNECTION_ID, - ))?; - let mut first = client(server_addr)?; - let mut second = client(server_addr)?; - - let first_session = connect(&mut hub, &mut first, b"session-token-one")?; - let second_session = connect(&mut hub, &mut second, b"session-token-two")?; - assert_ne!(first_session.remote_addr, second_session.remote_addr); - assert_eq!(first_session.connect_payload, b"session-token-one"); - assert_eq!(second_session.connect_payload, b"session-token-two"); - assert_eq!(hub.sessions().len(), 2); - - let first_request = create_provider_session_request( - "register-first", - CULTMESH_PROVIDER_REGISTER_OPERATION, - CULTMESH_PROVIDER_REGISTRATION_SCHEMA, - &CultMeshProviderRegistrationWire { - provider_id: "first".to_string(), - service_instance_id: "first-1".to_string(), - endpoint_id: "first-public".to_string(), - verse_id: "public".to_string(), - requested_lease_duration_ms: 30_000, - authority_lease_id: None, - }, - Some("first-1".to_string()), - Some("odin".to_string()), - )?; - let second_request = create_provider_session_request( - "register-second", - CULTMESH_PROVIDER_REGISTER_OPERATION, - CULTMESH_PROVIDER_REGISTRATION_SCHEMA, - &CultMeshProviderRegistrationWire { - provider_id: "second".to_string(), - service_instance_id: "second-1".to_string(), - endpoint_id: "second-public".to_string(), - verse_id: "public".to_string(), - requested_lease_duration_ms: 30_000, - authority_lease_id: None, - }, - Some("second-1".to_string()), - Some("odin".to_string()), - )?; - first.send_schema_message(&first_request)?; - second.send_schema_message(&second_request)?; - - for _ in 0..2 { - let CultNetRudpServerEvent::Frame { session, frame } = receive_frame(&mut hub)? else { - unreachable!(); - }; - assert_eq!(frame.channel_id, "schema"); - let request = decode_cultnet_message_from_slice( - &frame.payload, - CultNetWireContract::CultNetSchemaV0, - )?; - let CultNetMessage::OperationRequest { message_id, .. } = &request else { - return Err(anyhow!("expected operation request")); - }; - let provider_id = if message_id == "register-first" { - "first" - } else { - "second" - }; - let lease = CultMeshProviderLeaseWire { - provider_id: provider_id.to_string(), - service_instance_id: format!("{provider_id}-1"), - endpoint_id: format!("{provider_id}-public"), - verse_id: "public".to_string(), - lease_id: format!("lease-{provider_id}"), - valid_from_utc: "2026-07-14T12:00:00Z".to_string(), - expires_at_utc: "2026-07-14T12:00:30Z".to_string(), - }; - let response = create_provider_session_response( - &request, - CultMeshProviderOperationStatus::Ok, - CULTMESH_PROVIDER_LEASE_SCHEMA, - &lease, - Some("odin".to_string()), - )?; - hub.send_schema_message(&session, &response)?; - } - - let first_response = receive_message(&mut first)?; - let second_response = receive_message(&mut second)?; - let (_, first_lease) = decode_provider_session_response::( - &first_response, - CULTMESH_PROVIDER_REGISTER_OPERATION, - CULTMESH_PROVIDER_LEASE_SCHEMA, - )?; - let (_, second_lease) = decode_provider_session_response::( - &second_response, - CULTMESH_PROVIDER_REGISTER_OPERATION, - CULTMESH_PROVIDER_LEASE_SCHEMA, - )?; - assert_eq!(first_lease.provider_id, "first"); - assert_eq!(second_lease.provider_id, "second"); - assert_eq!(hub.session(first_session.remote_addr), Some(&first_session)); - assert_eq!( - hub.session(second_session.remote_addr), - Some(&second_session) - ); - Ok(()) -} - -#[test] -fn hub_fences_replaced_generations_and_does_not_replace_connect_retransmits() -> Result<()> { - let server_socket = socket()?; - let server_addr = server_socket.local_addr()?; - let mut hub = CultNetRudpServerHub::new(CultNetRudpServerHubOptions::new( - "odin", - server_socket, - CONNECTION_ID, - ))?; - let mut provider = client(server_addr)?; - let original_evidence = - encode_provider_connect_evidence(&CultMeshProviderConnectEvidenceWire { - client_session_id: "provider-client-1".to_string(), - session_token: Some("shared-session-token".to_string()), - })?; - let replacement_evidence = - encode_provider_connect_evidence(&CultMeshProviderConnectEvidenceWire { - client_session_id: "provider-client-2".to_string(), - session_token: Some("shared-session-token".to_string()), - })?; - let original = connect(&mut hub, &mut provider, &original_evidence)?; - - provider.connect(original_evidence.clone())?; - for _ in 0..3 { - assert!(hub.receive_event_once()?.is_none()); - } - assert_eq!(hub.session(original.remote_addr), Some(&original)); - - provider.connect(replacement_evidence.clone())?; - let mut disconnected = None; - for _ in 0..20 { - if let Some(event @ CultNetRudpServerEvent::Disconnected { .. }) = - hub.receive_event_once()? - { - disconnected = Some(event); - break; - } - } - let disconnected = - disconnected.ok_or_else(|| anyhow!("replacement did not disconnect old generation"))?; - let CultNetRudpServerEvent::Disconnected { session, .. } = disconnected else { - return Err(anyhow!("expected old generation Disconnected event")); - }; - assert_eq!(session, original); - let connected = hub - .receive_event_once()? - .ok_or_else(|| anyhow!("replacement did not connect new generation"))?; - let CultNetRudpServerEvent::Connected { - session: replacement, - } = connected - else { - return Err(anyhow!("expected replacement Connected event")); - }; - assert_ne!(replacement.session_generation, original.session_generation); - let original_decoded = decode_provider_connect_evidence(&original.connect_payload)?; - let replacement_decoded = decode_provider_connect_evidence(&replacement.connect_payload)?; - assert_eq!( - original_decoded.session_token, - replacement_decoded.session_token - ); - assert_ne!( - original_decoded.client_session_id, - replacement_decoded.client_session_id - ); - assert!( - hub.send(&original, "schema", vec![0x80]) - .unwrap_err() - .to_string() - .contains("no longer active") - ); - assert_eq!(hub.session(replacement.remote_addr), Some(&replacement)); - Ok(()) -} - -#[test] -fn identical_connect_retransmit_reuses_pending_accept_after_loss() -> Result<()> { - let server_socket = socket()?; - let server_addr = server_socket.local_addr()?; - let mut hub = CultNetRudpServerHub::new(CultNetRudpServerHubOptions::new( - "odin", - server_socket, - CONNECTION_ID, - ))?; - let client_socket = socket()?; - let mut client_session = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: CONNECTION_ID, - initial_sequence: 1, - resend_delay_ms: 250, - max_pending_reliable_packets: None, - }); - let evidence = encode_provider_connect_evidence(&CultMeshProviderConnectEvidenceWire { - client_session_id: "provider-client-lost-accept".to_string(), - session_token: Some("shared-session-token".to_string()), - })?; - let connect_packet = client_session.create_connect(0, evidence)?; - let connect_wire = encode_rudp_packet(&connect_packet)?; - - client_socket.send_to(&connect_wire, server_addr)?; - let CultNetRudpServerEvent::Connected { session } = hub - .receive_event_once()? - .ok_or_else(|| anyhow!("hub did not accept first Connect"))? - else { - return Err(anyhow!("hub did not emit Connected")); - }; - let mut buffer = vec![0_u8; 65_535]; - let (first_len, _) = client_socket.recv_from(&mut buffer)?; - let lost_accept = decode_rudp_packet(&buffer[..first_len])?; - assert_eq!(lost_accept.packet_type, CultNetRudpPacketType::Accept); - - client_socket.send_to(&connect_wire, server_addr)?; - assert!(hub.receive_event_once()?.is_none()); - let (resent_len, _) = client_socket.recv_from(&mut buffer)?; - let resent_accept = decode_rudp_packet(&buffer[..resent_len])?; - assert_eq!(resent_accept, lost_accept); - assert_eq!(hub.session(session.remote_addr), Some(&session)); - - client_session.receive(&resent_accept, 1)?; - assert!(client_session.connected()); - Ok(()) -} - -#[test] -fn exact_reliable_acks_clear_more_than_one_ack_window_of_fragments() -> Result<()> { - let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: CONNECTION_ID, - initial_sequence: 1, - resend_delay_ms: 250, - max_pending_reliable_packets: None, - }); - let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: CONNECTION_ID, - initial_sequence: 1, - resend_delay_ms: 250, - max_pending_reliable_packets: None, - }); - let connect_packet = sender.create_connect(0, Vec::new())?; - let accept = receiver.accept_connect(&connect_packet, 0, Vec::new())?; - sender.receive(&accept, 0)?; - receiver.receive(&sender.create_ack_for(accept.sequence), 0)?; - - let packets = sender.send_many( - "schema", - vec![0x5a; 40], - CultNetRudpSendOptions { - reliable: true, - ordered: true, - sequenced: false, - now_ms: 1, - }, - Some(1), - )?; - assert_eq!(packets.len(), 40); - let mut delivered = Vec::new(); - for packet in &packets { - let result = receiver.receive(packet, 2)?; - delivered.extend(result.delivered); - sender.receive(&receiver.create_ack_for(packet.sequence), 3)?; - } - assert_eq!(delivered.len(), 1); - assert_eq!(delivered[0].payload, vec![0x5a; 40]); - assert!(sender.pending_reliable_sequences().is_empty()); - Ok(()) -} - -#[test] -fn session_and_hub_reject_configured_memory_bounds() -> Result<()> { - let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: CONNECTION_ID, - initial_sequence: 1, - resend_delay_ms: 250, - max_pending_reliable_packets: None, - }); - receiver.set_max_payload_bytes(Some(4)); - receiver.set_max_pending_fragment_sets(1)?; - let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: CONNECTION_ID, - initial_sequence: 1, - resend_delay_ms: 250, - max_pending_reliable_packets: None, - }); - let connect_packet = sender.create_connect(0, Vec::new())?; - let accept = receiver.accept_connect(&connect_packet, 0, Vec::new())?; - sender.receive(&accept, 0)?; - - let oversized = sender.send_many( - "schema", - vec![1, 2, 3, 4, 5], - CultNetRudpSendOptions { - reliable: true, - ordered: true, - sequenced: false, - now_ms: 1, - }, - Some(2), - )?; - receiver.receive(&oversized[0], 1)?; - receiver.receive(&oversized[1], 1)?; - assert!(receiver.receive(&oversized[2], 1).is_err()); - - let fragment_options = CultNetRudpSendOptions { - reliable: true, - ordered: true, - sequenced: false, - now_ms: 2, - }; - let first_set = sender.send_many( - "schema", - vec![1, 2, 3, 4], - fragment_options.clone(), - Some(2), - )?; - let second_set = sender.send_many("schema", vec![5, 6, 7, 8], fragment_options, Some(2))?; - receiver.receive(&first_set[0], 2)?; - assert!(receiver.receive(&second_set[0], 2).is_err()); - - let server_socket = socket()?; - let server_addr = server_socket.local_addr()?; - let mut options = CultNetRudpServerHubOptions::new("odin", server_socket, CONNECTION_ID); - options.max_peers = 1; - let mut hub = CultNetRudpServerHub::new(options)?; - let mut first = client(server_addr)?; - let mut second = client(server_addr)?; - connect(&mut hub, &mut first, b"first")?; - second.connect(b"second".to_vec())?; - for _ in 0..20 { - match hub.receive_event_once() { - Err(error) => { - assert!(error.to_string().contains("peer limit")); - return Ok(()); - } - Ok(_) => continue, - } - } - Err(anyhow!("hub did not enforce peer limit")) -} - -#[test] -fn replay_history_is_bounded_and_idle_hub_sessions_expire() -> Result<()> { - let mut sender = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: CONNECTION_ID, - initial_sequence: 1, - resend_delay_ms: 250, - max_pending_reliable_packets: None, - }); - let mut receiver = CultNetRudpSession::new(CultNetRudpSessionOptions { - connection_id: CONNECTION_ID, - initial_sequence: 1, - resend_delay_ms: 250, - max_pending_reliable_packets: None, - }); - let connect_packet = sender.create_connect(0, Vec::new())?; - let accept = receiver.accept_connect(&connect_packet, 0, Vec::new())?; - sender.receive(&accept, 0)?; - let mut oldest = None; - for sequence in 0..4_100 { - let packet = sender.send( - "realtime", - vec![(sequence % 251) as u8], - CultNetRudpSendOptions { - reliable: false, - ordered: false, - sequenced: false, - now_ms: sequence, - }, - )?; - if oldest.is_none() { - oldest = Some(packet.clone()); - } - assert_eq!(receiver.receive(&packet, sequence)?.delivered.len(), 1); - } - assert!( - receiver - .receive(&oldest.expect("oldest packet"), 4_101)? - .delivered - .is_empty() - ); - - let server_socket = socket()?; - let server_addr = server_socket.local_addr()?; - let mut hub = CultNetRudpServerHub::new(CultNetRudpServerHubOptions::new( - "odin", - server_socket, - CONNECTION_ID, - ))?; - let mut provider = client(server_addr)?; - let context = connect(&mut hub, &mut provider, b"expiring-session")?; - std::thread::sleep(Duration::from_millis(2)); - assert_eq!(hub.remove_timed_out_sessions(0), vec![context]); - assert!(hub.sessions().is_empty()); - Ok(()) -}