diff --git a/Cargo.lock b/Cargo.lock index 6bc8cc8..c127eba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,11 +2,177 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "accesskit" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3b7f7f85a7e5f68090000ed7622545829afd484d210358702ae4cb97dd0c320" +dependencies = [ + "uuid", +] + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.21", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "approx" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6" +dependencies = [ + "num-traits", +] + +[[package]] +name = "arrayvec" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" + +[[package]] +name = "assert_type_match" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f548ad2c4031f2902e3edc1f29c29e835829437de49562d8eb5dc5584d3a1043" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +dependencies = [ + "portable-atomic", +] + [[package]] name = "atomic-waker" version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "atomicow" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301801c08259e328a1c7da556608c0c22687708831b22024dbd3a57ea741e6de" +dependencies = [ + "portable-atomic", + "portable-atomic-util", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" [[package]] name = "aws-lc-rs" @@ -83,6 +249,12 @@ dependencies = [ "tracing", ] +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64" version = "0.23.1" @@ -90,165 +262,1078 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" [[package]] -name = "bitflags" -version = "2.13.2" +name = "bevy" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +checksum = "8bd0e4ea5ec677698955f9f285221eab78606d8c27b57b889def199c16ce36e6" +dependencies = [ + "bevy_internal", +] [[package]] -name = "block-buffer" -version = "0.10.4" +name = "bevy_a11y" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +checksum = "f1070b9b7f087a5073be2da713367f29a831c85cd3391afcfc95bc0498125704" dependencies = [ - "generic-array", + "accesskit", + "bevy_app", + "bevy_derive", + "bevy_ecs", + "bevy_reflect", ] [[package]] -name = "bumpalo" -version = "3.20.3" +name = "bevy_android" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +checksum = "17fd56a4708085b36f8aa1a011e347f8bd9334f0343bb3d5d9c3742897968a55" +dependencies = [ + "android-activity", +] [[package]] -name = "bytes" -version = "1.12.1" +name = "bevy_app" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" +checksum = "a2489da2972d57cccd19060d7faa44c4c4cfb296a657c84ee6ab8bde2b1c0115" +dependencies = [ + "bevy_derive", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "console_error_panic_hook", + "ctrlc", + "downcast-rs", + "log", + "thiserror 2.0.21", + "variadics_please", + "wasm-bindgen", + "web-sys", +] [[package]] -name = "cc" -version = "1.10.0" +name = "bevy_asset" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" +checksum = "b88b3767a2c3f5a017b2febca08a670819ea2acb46846e4d8cd02980aeb0ed3e" dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", + "async-broadcast", + "async-channel", + "async-fs", + "async-io", + "async-lock", + "atomicow", + "bevy_android", + "bevy_app", + "bevy_asset_macros", + "bevy_diagnostic", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "bitflags", + "blake3", + "crossbeam-channel", + "derive_more", + "disqualified", + "downcast-rs", + "either", + "futures-io", + "futures-lite", + "futures-util", + "js-sys", + "ron", + "serde", + "stackfuture", + "thiserror 2.0.21", + "tracing", + "uuid", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] -name = "cfg-if" -version = "1.0.5" +name = "bevy_asset_macros" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" +checksum = "875b4fd236612744dbd61fb0a77ffde0aee37b8b780cf8a89caa308511c70646" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn 2.0.119", +] [[package]] -name = "cfg_aliases" -version = "0.2.2" +name = "bevy_camera" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" +checksum = "f32c0d8436fb9373dc3b64fa30c3dec51d6a7cfda61de3c70fd1f62f8a903887" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_color", + "bevy_derive", + "bevy_ecs", + "bevy_image", + "bevy_math", + "bevy_mesh", + "bevy_reflect", + "bevy_shape", + "bevy_transform", + "bevy_utils", + "bevy_window", + "derive_more", + "downcast-rs", + "serde", + "smallvec", + "thiserror 2.0.21", + "wgpu-types", +] [[package]] -name = "chacha20" -version = "0.10.2" +name = "bevy_color" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +checksum = "c0e57bfc1a0da1c018235fb98bb0e15ab61f1d0253122020ab6e1450e6cfbcf9" dependencies = [ - "cfg-if", - "cpufeatures 0.3.1", - "rand_core", + "bevy_curve", + "bevy_math", + "bevy_reflect", + "bytemuck", + "derive_more", + "encase", + "serde", + "thiserror 2.0.21", + "wgpu-types", +] + +[[package]] +name = "bevy_curve" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21ae28ed95c1c8a1bfc8930471840ee5a4b99b6b31138b17598e7ee6f4291b64" +dependencies = [ + "approx", + "bevy_math", + "bevy_reflect", + "itertools", + "serde", + "thiserror 2.0.21", + "variadics_please", +] + +[[package]] +name = "bevy_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd59e9e600d792e60df17b4d52719b28c288bf047884ac89ad29d9c7a6592d2c" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_diagnostic" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f8851dc1b253b350bf941695457007e6e44ad4b8f075bf52a1ef202e6866b6a" +dependencies = [ + "atomic-waker", + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_tasks", + "bevy_time", + "const-fnv1a-hash", + "log", + "serde", +] + +[[package]] +name = "bevy_ecs" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5541a1658c5029b39e3ec59c6ecb47ad0a6d85fa083312a5ee2a95d6982a926" +dependencies = [ + "arrayvec", + "bevy_ecs_macros", + "bevy_platform", + "bevy_ptr", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "bitflags", + "bumpalo", + "concurrent-queue", + "derive_more", + "fixedbitset", + "indexmap", + "log", + "nonmax", + "serde", + "slotmap", + "smallvec", + "thiserror 2.0.21", + "variadics_please", +] + +[[package]] +name = "bevy_ecs_macro_logic" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5197fea7643d2733e8b6d81c3dfa99adad3a754cdfd159c00255cea157b795d7" +dependencies = [ + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_ecs_macros" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3dde5305169503c9101404bf8cf17e8351f56fccf5152b8e5d7d7c32dcf1c486" +dependencies = [ + "bevy_ecs_macro_logic", + "bevy_macro_utils", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_encase_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e4c4183c39ae8d00e53afeeab66aa71c41b08d82638149f9d485a2a62df569" +dependencies = [ + "bevy_macro_utils", + "encase_derive_impl", +] + +[[package]] +name = "bevy_image" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb818dd8d705809d54adea08e346edce81a51ddbf06406ddf12a72ee467a3855" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_color", + "bevy_ecs", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_utils", + "bitflags", + "bytemuck", + "futures-lite", + "guillotiere", + "half", + "image", + "rectangle-pack", + "serde", + "thiserror 2.0.21", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_input" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "261ca5cc8a0bf228bfe4e841570db18a3ab1d034ce089772b8ec49c163ae74ea" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "derive_more", + "log", + "thiserror 2.0.21", +] + +[[package]] +name = "bevy_input_focus" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83c81d369d0ab3ec17ba14bab1e07da7cacf3751a3104a93b1a3cd99d887887b" +dependencies = [ + "bevy_app", + "bevy_camera", + "bevy_ecs", + "bevy_input", + "bevy_math", + "bevy_reflect", + "bevy_window", + "log", + "thiserror 2.0.21", +] + +[[package]] +name = "bevy_internal" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4ed1cb58d1761426a077a8aff55e6a2e0cdfd19e411211d4b0217409330bb84" +dependencies = [ + "bevy_a11y", + "bevy_android", + "bevy_app", + "bevy_color", + "bevy_curve", + "bevy_derive", + "bevy_diagnostic", + "bevy_ecs", + "bevy_input", + "bevy_input_focus", + "bevy_math", + "bevy_platform", + "bevy_ptr", + "bevy_reflect", + "bevy_shape", + "bevy_state", + "bevy_tasks", + "bevy_time", + "bevy_transform", + "bevy_utils", + "bevy_window", +] + +[[package]] +name = "bevy_macro_utils" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0d03a948df65138e30917d91237797d7e38763135ebacd0f28c891e1a3b2b52" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", + "toml_edit", +] + +[[package]] +name = "bevy_math" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598cbdcec4d0f0d966905996240ac76f77a644c583f95fb612d4f59f5f649038" +dependencies = [ + "approx", + "bevy_reflect", + "derive_more", + "glam", + "libm", + "rand", + "serde", + "thiserror 2.0.21", + "variadics_please", ] [[package]] -name = "cmake" -version = "0.1.58" +name = "bevy_mesh" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09358ac9a3ede5855568c10eaba6bcf126bf86639e786fb21b413457ebec5f26" +dependencies = [ + "bevy_app", + "bevy_asset", + "bevy_derive", + "bevy_ecs", + "bevy_encase_derive", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "bevy_shape", + "bevy_transform", + "bitflags", + "bytemuck", + "derive_more", + "encase", + "half", + "hexasphere", + "thiserror 2.0.21", + "tracing", + "wgpu-types", +] + +[[package]] +name = "bevy_platform" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000bc467d1b344b6609fcf217a9b08d349d49c8e85dc4179fb7656f2b6e3db47" +dependencies = [ + "critical-section", + "foldhash", + "futures-channel", + "futures-lite", + "hashbrown 0.16.1", + "js-sys", + "portable-atomic", + "portable-atomic-util", + "serde", + "spin", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-time", + "windows-sys 0.61.2", +] + +[[package]] +name = "bevy_ptr" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41935a00c02004753bd7ed5094754397800b3f673bf15553bb007b06216872d5" + +[[package]] +name = "bevy_reflect" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46eecc733575b873deedcc37639e36290092388657091b3321fa6e073e7818e6" +dependencies = [ + "assert_type_match", + "bevy_platform", + "bevy_ptr", + "bevy_reflect_derive", + "bevy_utils", + "derive_more", + "disqualified", + "downcast-rs", + "erased-serde", + "foldhash", + "glam", + "indexmap", + "inventory", + "serde", + "smallvec", + "smol_str", + "thiserror 2.0.21", + "uuid", + "variadics_please", + "wgpu-types", +] + +[[package]] +name = "bevy_reflect_derive" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfaa1e315676c625ed8d30ba8a671ebf917cad6355320295b9802a8cbc06853b" +dependencies = [ + "bevy_macro_utils", + "indexmap", + "proc-macro2", + "quote", + "syn 2.0.119", + "uuid", +] + +[[package]] +name = "bevy_shape" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa8b6096ad1ec7dfe1af8e51d7cfa7de1a9e0a3bd02be58a061092c5e1ff1eb1" +dependencies = [ + "arrayvec", + "bevy_math", + "bevy_reflect", + "derive_more", + "rand", + "rand_distr", + "serde", + "thiserror 2.0.21", +] + +[[package]] +name = "bevy_state" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09292e08e1eb34d2dd915630fb3c04ba75220c7819943c9f213d3a5f9a8be1e8" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "bevy_state_macros", + "bevy_utils", + "log", + "variadics_please", +] + +[[package]] +name = "bevy_state_macros" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fec1a8f6a253e39ddf7e447b1b9286caacb205ed8e619e6fb9a2e010d58dd04b" +dependencies = [ + "bevy_macro_utils", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "bevy_tasks" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fbd8116f12dc50bde3758a72fba85e1f4c00e6c66d0ad58bdc46c0c0690829a8" +dependencies = [ + "async-executor", + "async-task", + "atomic-waker", + "bevy_platform", + "crossbeam-queue", + "derive_more", + "futures-lite", + "heapless", + "web-task", +] + +[[package]] +name = "bevy_time" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13901174b792bb7848d8e088075f0009ea409b99179934c0d5bdd2b95622694e" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_platform", + "bevy_reflect", + "crossbeam-channel", + "log", + "serde", +] + +[[package]] +name = "bevy_transform" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83379023d77a0739569a5bcb859eed652781a1b3271e94a86dfbb56fc5d067e0" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_math", + "bevy_reflect", + "bevy_tasks", + "bevy_utils", + "derive_more", + "serde", + "thiserror 2.0.21", +] + +[[package]] +name = "bevy_utils" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cbd58495039d4e63e6fcfef1242fecab33ffe62b4a5e010467e69e790879812" +dependencies = [ + "async-channel", + "bevy_platform", + "disqualified", + "indexmap", + "thread_local", +] + +[[package]] +name = "bevy_window" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc9a83f3cbaa04155b5dc9b247819c0e215aee754451a4324b82ddeb80dc53f1" +dependencies = [ + "bevy_app", + "bevy_ecs", + "bevy_input", + "bevy_math", + "bevy_platform", + "bevy_reflect", + "log", + "raw-window-handle", + "serde", +] + +[[package]] +name = "bitflags" +version = "2.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06" +dependencies = [ + "serde_core", +] + +[[package]] +name = "blake3" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9e454fc11f76977dc803893aff6304ed33d6a26efae8696573bea74baa27ae" +dependencies = [ + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.1", +] + +[[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 = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + +[[package]] +name = "blocking" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a70e4329df6cb94385eed412ec92375c3cdd8a6e502493d1229b6414e4036dfa" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a1f896587b6f2c069c73d2f0913e2d590c3990285cd2f0b6aa02b786b4c679c" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cc" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86e055fa6e3e40ad6187ba25b6855eeb635d5271f8e0f0e1e9a5826b8d75bea0" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600" + +[[package]] +name = "cfg_aliases" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" + +[[package]] +name = "chacha20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.1", + "rand_core", +] + +[[package]] +name = "cmake" +version = "0.1.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" +dependencies = [ + "cc", +] + +[[package]] +name = "combine" +version = "4.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", + "portable-atomic", +] + +[[package]] +name = "console_error_panic_hook" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" +dependencies = [ + "cfg-if", + "wasm-bindgen", +] + +[[package]] +name = "const-fnv1a-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b13ea120a812beba79e34316b3942a857c86ec1593cb34f27bb28272ce2cca" + +[[package]] +name = "const_panic" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9603f79528ece8163c496f8932121cb36cfe46259e9c907bb3d8205139d7caa3" +dependencies = [ + "typewit", +] + +[[package]] +name = "const_soft_float" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ca1caa64ef4ed453e68bb3db612e51cf1b2f5b871337f0fcab1c8f87cc3dff" + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "constgebra" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1aaf9b65849a68662ac6c0810c8893a765c960b907dd7cfab9c4a50bf764fbc" +dependencies = [ + "const_soft_float", +] + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" +dependencies = [ + "libc", +] + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-channel" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98b0cc327b5bc766e7fda9c9260cc0fa81b43a8e240440422dff70788e3f9ef1" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-queue" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e8bd762f7479489c70ed6c768ddca99d7296857de437a68dcb2a94365b3fae" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a31eee39dddec8330830986fcd7625edb5a24ec90ea038215273bbc3adb08ac6" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "ctrlc" +version = "3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0b1fab2ae45819af2d0731d60f2afe17227ebb1a1538a236da84c93e9a60162" +dependencies = [ + "dispatch2", + "nix", + "windows-sys 0.61.2", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.119", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + +[[package]] +name = "displaydoc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + +[[package]] +name = "disqualified" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e90a5dd66beeca277ae954f8055b02a712792b6b6b43f2bc2f92b0b7d8291614" + +[[package]] +name = "downcast-rs" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "117240f60069e65410b3ae1bb213295bd828f707b5bec6596a1afc8793ce0cbc" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678" -dependencies = [ - "cc", -] +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" [[package]] -name = "combine" -version = "4.6.9" +name = "either" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4356eba6976435498d05a311ee9fc486da3bc81c200d3f0d7dd9703b41913f92" -dependencies = [ - "bytes", - "memchr", -] +checksum = "0e9c71c2167ca323c882b99918929403426e2373ea17242ff5653e0d5e1058be" [[package]] -name = "core-foundation" -version = "0.10.1" +name = "encase" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +checksum = "b1acbc46b960480c4959213714511e22f9a2e0d361520693bcf5057b5924f53e" dependencies = [ - "core-foundation-sys", - "libc", + "const_panic", + "encase_derive", + "thiserror 2.0.21", ] [[package]] -name = "core-foundation-sys" -version = "0.8.7" +name = "encase_derive" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +checksum = "1e30d38c47336a0afbdfde297b442944041cf6dccbb1a79eceea16abfb27a92a" +dependencies = [ + "encase_derive_impl", +] [[package]] -name = "cpufeatures" -version = "0.2.17" +name = "encase_derive_impl" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "495d24abe406f090f2ba8fc362ecaa8a001e192522aa75eae7b144163c3235ff" dependencies = [ - "libc", + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] -name = "cpufeatures" -version = "0.3.1" +name = "equivalent" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566" -dependencies = [ - "libc", -] +checksum = "00d174d5400e5e8fd687ad1049e2f578285fa914201b1af7e8b112a4546bd826" [[package]] -name = "crypto-common" -version = "0.1.7" +name = "erased-serde" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +checksum = "1e59b668157f32d3459d4bd634861a06ffa2503a461ca5f0916b9f0252605e66" dependencies = [ - "generic-array", - "typenum", + "serde", + "serde_core", + "typeid", ] [[package]] -name = "digest" -version = "0.10.7" +name = "errno" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ - "block-buffer", - "crypto-common", + "libc", + "windows-sys 0.61.2", ] [[package]] -name = "displaydoc" -version = "0.2.7" +name = "euclid" +version = "0.22.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.9", + "num-traits", ] [[package]] -name = "dunce" -version = "1.0.5" +name = "event-listener" +version = "5.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2" +dependencies = [ + "parking", + "pin-project-lite", +] [[package]] -name = "errno" -version = "0.3.14" +name = "event-listener-strategy" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" dependencies = [ - "libc", - "windows-sys 0.61.2", + "event-listener", + "pin-project-lite", ] [[package]] @@ -275,6 +1360,12 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "foldhash" version = "0.2.0" @@ -311,6 +1402,36 @@ version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92d699e522242e69e3003b94ecc1f960f3a5e015aa7c5d7486e65ad01dd94f5e" +[[package]] +name = "futures-io" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53c0fa8157de1303bfffdaa1cc2a673bfffb60102f76b0ef4441659124373fed" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb9654ba8355388abeb8dcb4fc62f511300867002afc858860463bdd9fe0c44" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "futures-task" version = "0.3.34" @@ -324,6 +1445,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0d50a92467f8ba5dd6e3ee5d4bd04d73ab2e4e1c44474a0674821dfce14b79bc" dependencies = [ "futures-core", + "futures-macro", "futures-task", "pin-project-lite", "slab", @@ -366,11 +1488,66 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "glam" +version = "0.33.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37bfe73dc8ec21f54d181e5a60554327ec3f1c600c455a75ccbfc65d2106a6c4" +dependencies = [ + "approx", + "bytemuck", + "libm", + "rand", + "serde_core", +] + +[[package]] +name = "guillotiere" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b17e70c989c36bad147b27a58d148c0741c51448aa5653436547323e524d0ab" +dependencies = [ + "euclid", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "bytemuck", + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "hash32" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "equivalent", + "foldhash", + "serde", + "serde_core", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" dependencies = [ "foldhash", ] @@ -381,7 +1558,35 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "824e001ac4f3012dd16a264bec811403a67ca9deb6c102fc5049b32c4574b35f" dependencies = [ - "hashbrown", + "hashbrown 0.16.1", +] + +[[package]] +name = "heapless" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ba4bd83f9415b58b4ed8dc5714c76e626a105be4646c02630ad730ad3b5aa4" +dependencies = [ + "hash32", + "portable-atomic", + "stable_deref_trait", +] + +[[package]] +name = "hermit-abi" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e17592d60ebacc7d5e169f4663c5f84f9161cc90328abcfe8456f41e4dfcb284" + +[[package]] +name = "hexasphere" +version = "19.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "163385eff5a0cf606ebe2c10b3a44ab02f4306e83fbd068a64f5e1d72a3049ac" +dependencies = [ + "constgebra", + "glam", + "tinyvec", ] [[package]] @@ -470,7 +1675,7 @@ version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ddc03d96684f9226b8a787cdb71488417b53ab5ea8fdb1dac946cb9431cc8bff" dependencies = [ - "base64", + "base64 0.23.1", "bytes", "futures-channel", "futures-util", @@ -592,12 +1797,60 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", +] + +[[package]] +name = "imagesize" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edcd27d72f2f071c64249075f42e205ff93c9a4c5f6c6da53e79ed9f9832c285" + +[[package]] +name = "indexmap" +version = "2.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc4e190f5d26ca7051642629da2c52fc03bde85a03197c99408dcd291734c855" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inventory" +version = "0.3.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6928282826c822ad91bf1c9a1cb90a30ba1c26770749929b4656cd6be829cd7c" +dependencies = [ + "rustversion", +] + [[package]] name = "ipnet" version = "2.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "791930b43c0d5973160d90a8f3894509f2b273430f5c5c73b668636d0287c5c0" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.18" @@ -613,10 +1866,10 @@ dependencies = [ "cfg-if", "combine", "jni-macros", - "jni-sys", + "jni-sys 0.4.1", "log", "simd_cesu8", - "thiserror", + "thiserror 2.0.21", "walkdir", "windows-link", ] @@ -634,6 +1887,15 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + [[package]] name = "jni-sys" version = "0.4.1" @@ -680,6 +1942,12 @@ version = "0.2.191" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "65cd87c9a6d60536580b316ad985726104730a5151c99845f2490428ef91240a" +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libsqlite3-sys" version = "0.36.0" @@ -744,6 +2012,129 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "mutants" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc0287524726960e07b119cebd01678f852f147742ae0d925e6a520dca956126" + +[[package]] +name = "naga-types" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "590afbf58a6f4f62873cd5cff4468061844bafa1cdf399cc954537c22d768d49" +dependencies = [ + "hashbrown 0.17.1", + "indexmap", + "rustc-hash 1.1.0", + "serde", + "thiserror 2.0.21", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.6.0+11769913" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee6cda3051665f1fb8d9e08fc35c96d5a244fb1be711a03b71118828afc9a873" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nix" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + +[[package]] +name = "nonmax" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "objc2" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08849bbd4767dfae9457696856ae1c84fe4e0281bbe4a7abff2d0e06fb7981f8" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + [[package]] name = "once_cell" version = "1.21.4" @@ -756,11 +2147,38 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" +[[package]] +name = "pace_core" +version = "0.1.0" +source = "git+https://github.com/pace-kit/pace_core?rev=1aa2b36270b68ea6fc04c268c3d5661ec09790aa#1aa2b36270b68ea6fc04c268c3d5661ec09790aa" +dependencies = [ + "bevy", + "schemars", + "serde", + "serde_json", + "serde_path_to_error", + "sha2", + "tempfile", +] + +[[package]] +name = "pace_editor_format" +version = "0.1.0" +source = "git+https://github.com/pace-kit/pace_app?rev=1dd8f4d8fcfb84849164a6041895dd15a1e9ef19#1dd8f4d8fcfb84849164a6041895dd15a1e9ef19" +dependencies = [ + "base64 0.22.1", + "imagesize", + "serde", + "serde_json", +] + [[package]] name = "pace_server" version = "0.1.0" dependencies = [ "axum", + "pace_core", + "pace_editor_format", "reqwest", "rusqlite", "serde", @@ -769,9 +2187,17 @@ dependencies = [ "subtle", "tempfile", "tokio", + "tower-http", + "url", "uuid", ] +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + [[package]] name = "percent-encoding" version = "2.3.2" @@ -784,12 +2210,52 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + [[package]] name = "pkg-config" version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix", + "windows-sys 0.61.2", +] + +[[package]] +name = "portable-atomic" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05c8b63e8d9609db387f0324918f81d68fe27748f084ef092fb35954d0539a85" + +[[package]] +name = "portable-atomic-util" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10ab3eb7f3becc3a1cbc4f2c6f20267996cfc1a6467a873763411b136a122715" +dependencies = [ + "portable-atomic", +] + [[package]] name = "potential_utf" version = "0.1.6" @@ -799,6 +2265,15 @@ dependencies = [ "zerovec", ] +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -808,6 +2283,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "pxfm" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea" + [[package]] name = "quinn" version = "0.11.12" @@ -819,10 +2300,10 @@ dependencies = [ "pin-project-lite", "quinn-proto", "quinn-udp", - "rustc-hash", + "rustc-hash 2.1.3", "rustls", "socket2", - "thiserror", + "thiserror 2.0.21", "tokio", "tracing", "web-time", @@ -841,11 +2322,11 @@ dependencies = [ "rand", "rand_pcg", "ring", - "rustc-hash", + "rustc-hash 2.1.3", "rustls", "rustls-pki-types", "slab", - "thiserror", + "thiserror 2.0.21", "tinyvec", "tracing", "web-time", @@ -897,6 +2378,16 @@ version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69" +[[package]] +name = "rand_distr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d431c2703ccf129de4d45253c03f49ebb22b97d6ad79ee3ecfc7e3f4862c1d8" +dependencies = [ + "num-traits", + "rand", +] + [[package]] name = "rand_pcg" version = "0.10.2" @@ -906,13 +2397,45 @@ dependencies = [ "rand_core", ] +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rectangle-pack" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0d463f2884048e7153449a55166f91028d5b0ea53c79377099ce4e8cf0cf9bb" + +[[package]] +name = "ref-cast" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e440fb4e4b4147295338efb76001ab9e4efc0e5839df2c47fc5ac2381d365c3" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecd8964f8453721699a1ed72037b0db49ce2f5a5138486ee89bed6f67cdf3a" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.9", +] + [[package]] name = "reqwest" version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16a1cfa75cc186dd73d5818e510e042e40927bccc9c236b061cea97e1eb08029" dependencies = [ - "base64", + "base64 0.23.1", "bytes", "futures-core", "http", @@ -957,14 +2480,28 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "ron" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81116b9531d61eabc41aeb228e4b6b2435bcca3233b98cf3b3077d4e6e9debb3" +dependencies = [ + "bitflags", + "once_cell", + "serde", + "serde_derive", + "typeid", + "unicode-ident", +] + [[package]] name = "rsqlite-vfs" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" dependencies = [ - "hashbrown", - "thiserror", + "hashbrown 0.16.1", + "thiserror 2.0.21", ] [[package]] @@ -982,6 +2519,12 @@ dependencies = [ "sqlite-wasm-rs", ] +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + [[package]] name = "rustc-hash" version = "2.1.3" @@ -1115,6 +2658,31 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "schemars" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab508826f74a77ca9d5aba6ff19b522583ee3eaf28a19384ff3d0e5835fadf6e" +dependencies = [ + "dyn-clone", + "ref-cast", + "schemars_derive", + "serde", + "serde_json", +] + +[[package]] +name = "schemars_derive" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1c3a92094fa7d61aa124645844facb6b554dfc797136d0f5fd1f890e2bffc69" +dependencies = [ + "proc-macro2", + "quote", + "serde_derive_internals", + "syn 3.0.9", +] + [[package]] name = "security-framework" version = "3.7.0" @@ -1174,6 +2742,17 @@ dependencies = [ "syn 3.0.9", ] +[[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.9", +] + [[package]] name = "serde_json" version = "1.0.154" @@ -1259,12 +2838,30 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + [[package]] name = "smallvec" version = "1.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0d5d4b1981b8bbbb7aed204cae5726d683d5d6ac7e74ec54122ea28f315e7c8" +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + [[package]] name = "socket2" version = "0.6.5" @@ -1275,6 +2872,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "spin" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0134f9043ed38b087ac4f7d4af44c79e2c9e5094421fe3164f435ce585953b10" +dependencies = [ + "portable-atomic", +] + [[package]] name = "sqlite-wasm-rs" version = "0.5.5" @@ -1293,6 +2899,21 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" +[[package]] +name = "stackfuture" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115beb9c69db2393ff10b75a1b8587a51716e5551d015001e55320ed279d32f9" +dependencies = [ + "const_panic", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "subtle" version = "2.6.1" @@ -1354,13 +2975,33 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + [[package]] name = "thiserror" version = "2.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09e52cb86a36cede5cb101bf8908837b3e4c6e5e59fe7fd85c23fb56200d189e" dependencies = [ - "thiserror-impl", + "thiserror-impl 2.0.21", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", ] [[package]] @@ -1374,6 +3015,15 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + [[package]] name = "tinystr" version = "0.8.5" @@ -1427,6 +3077,36 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml_datetime" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b86d767906c6c42421dcba507eb9d203e779497710a47782a224bb871653053" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.17+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3641d5bbb5349a79e1020a242d251efbc546ad8048d133958323ce9c40a9c9c" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baa693a8032d7e1cada7d0041e96126df243179ff061456783ac7f12bda4744c" +dependencies = [ + "winnow", +] + [[package]] name = "tower" version = "0.5.3" @@ -1499,18 +3179,53 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "typeid" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" + [[package]] name = "typenum" version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" +[[package]] +name = "typewit" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "214ca0b2191785cbc06209b9ca1861e048e39b5ba33574b3cedd58363d5bb5f6" + [[package]] name = "unicode-ident" version = "1.0.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2c754d6c33795a1c324727428e5a7dedb5b06195f9890bdbcba760d3e246563" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unsynn" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501a7adf1a4bd9951501e5c66621e972ef8874d787628b7f90e64f936ef7ec0a" +dependencies = [ + "mutants", + "proc-macro2", + "rustc-hash 2.1.3", +] + [[package]] name = "untrusted" version = "0.9.0" @@ -1543,9 +3258,20 @@ checksum = "4a0e2d11620c50695fe12bbbfde96b3477fe606e7284c22d505c12ce51214514" dependencies = [ "getrandom 0.4.3", "js-sys", + "serde_core", "wasm-bindgen", ] +[[package]] +name = "variadics_please" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b53d0f7f2d0182759a549f1f313ce16b07d8f9ba93098157b5fa10ee3e61117f" +dependencies = [ + "quote", + "unsynn", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -1649,6 +3375,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-task" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cdc136a53ccd64a1211f107ccc34404769fbcc0f165f1afa065f5d88ab93538" +dependencies = [ + "async-task", + "cfg-if", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "web-time" version = "1.1.0" @@ -1668,6 +3406,23 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "wgpu-types" +version = "30.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99dad6f1fbdbbdb4c278a6508b059d44688f5cebddf78d005a46a31340269286" +dependencies = [ + "bitflags", + "bytemuck", + "js-sys", + "log", + "naga-types", + "raw-window-handle", + "serde", + "static_assertions", + "web-sys", +] + [[package]] name = "winapi-util" version = "0.1.11" @@ -1765,6 +3520,15 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81" +dependencies = [ + "memchr", +] + [[package]] name = "writeable" version = "0.6.4" @@ -1794,6 +3558,26 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zerocopy" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5fe1f8f1b06191a00962174c61aa5005e0bb391a6d80d07e24d115c01a92ed8" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "863ad3ac83293fb4d740aedbfdc9240dd8d1a50c1099acd76ce80ce7c7230c7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + [[package]] name = "zerofrom" version = "0.1.8" diff --git a/Cargo.toml b/Cargo.toml index 76982ed..9654653 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,16 +2,21 @@ name = "pace_server" version = "0.1.0" edition = "2021" +rust-version = "1.97.1" [dependencies] axum = "0.8" rusqlite = { version = "0.38", features = ["bundled"] } serde = { version = "1", features = ["derive"] } -serde_json = { version = "1", features = ["raw_value"] } +serde_json = "1" +pace_core = { git = "https://github.com/pace-kit/pace_core", rev = "1aa2b36270b68ea6fc04c268c3d5661ec09790aa" } +pace_editor_format = { git = "https://github.com/pace-kit/pace_app", rev = "1dd8f4d8fcfb84849164a6041895dd15a1e9ef19" } sha2 = "0.10" subtle = "2" tokio = { version = "1", features = ["macros", "rt-multi-thread", "net", "signal"] } uuid = { version = "1", features = ["v4"] } +tower-http = { version = "0.6", features = ["cors"] } +url = "2" [dev-dependencies] reqwest = { version = "0.13", default-features = false, features = ["json", "rustls"] } diff --git a/README.md b/README.md index 33edc9f..cdd09cd 100644 --- a/README.md +++ b/README.md @@ -7,24 +7,37 @@ Work locally, download a revision, edit, and explicitly upload against that revi ## Implementation sequence -1. Storage and revision API (this layer): SQLite persistence, unchanged project +1. Storage and revision API: SQLite persistence, unchanged project bytes, bounded requests, strong revision ETags, transactional writes, access control. 2. Authentication: provision bearer identities and enable the server binary. The bare router fails closed without an identity; the binary always uses auth. -3. Client integration: connect the local editor's serializer and expose conflicts - to the user after the local core/editor workflow is available. - -## Scene compatibility prerequisite - -The referenced `pace_core` main at `6ccfd6e` has no scene schema/serializer; -`pace_app` main at `b195b90` is a demo, without project save/load. The server -therefore validates **JSON syntax only**, up to 1 MiB (including whitespace), -and stores/returns the original UTF-8 bytes. Unknown fields, whitespace, key order, -and numeric spelling are preserved. The examples/tests are transport fixtures, -not a proposed Pace scene format. Structurally invalid scenes that are valid JSON -cannot yet be rejected: integrate the core's canonical validator when available. -The transport does not execute scripts, dereference asset URLs, or unpack archives. -External assets are not uploaded by this API. +3. Client integration: the `pace_app` browser editor uploads/downloads its existing + draft format and offers explicit conflict decisions. The server also accepts + core scenes through the same revision API. See the app's project-sharing guide. + +## Supported project formats + +Uploads are limited to **64 MiB**, including whitespace and embedded images, +matching the editor's project-file limit. Validation uses immutable dependencies: + +- **PACE core scene v1** (`format_version: 1`): `Scene::from_json` from + `pace_core` at `1aa2b36270b68ea6fc04c268c3d5661ec09790aa` checks shape, version, + stable IDs, references, geometry and dialogue exits. Unknown fields are rejected. +- **Editor draft v1** (`format: "pace-editor-draft", version: 1`): + `pace_editor_format::Project::from_json` from `pace_app` at + `1dd8f4d8fcfb84849164a6041895dd15a1e9ef19` is the editor's own loader, extracted + without schema changes. It rejects malformed structures, unsupported versions, + unknown fields and duplicate IDs. Incomplete drafts and unresolved references + remain editable, just as with local save/load; this does not certify playability. + +The server stores/returns accepted UTF-8 bytes unchanged: no envelope, conversion, +rewritten IDs or asset extraction. Whitespace, field order and numeric spelling +are preserved. Embedded backgrounds travel inside the draft. Core scenes and +drafts remain distinct contracts; runtime export/conversion is not part of sharing. +Other JSON, runtime save slots and future versions are rejected. Previously stored +arbitrary JSON from the initial API remains downloadable, but new writes must use +a supported format. The transport never executes scripts or fetches asset URLs. +Rust 1.97.1+ is required by the pinned core dependency. ## Run the authenticated server @@ -53,7 +66,19 @@ PACE_AUTH_FILE=./auth.json PACE_DATABASE=./pace-projects.sqlite3 cargo run --loc `PACE_BIND` defaults to `127.0.0.1:3000`. TLS must be terminated by a trusted reverse proxy before exposing the service beyond localhost; bearer tokens confer access to anyone holding them. Do not log authorization headers. There is no -permissive CORS policy or browser login flow in this layer. +browser login flow in this layer. Cross-origin browser access is disabled unless +`PACE_ALLOWED_ORIGINS` lists exact origins separated by commas, for example: + +```sh +PACE_ALLOWED_ORIGINS=http://127.0.0.1:8080,http://localhost:8080 \ +PACE_AUTH_FILE=./auth.json PACE_DATABASE=./pace-projects.sqlite3 cargo run --locked +``` + +Paths, credentials and wildcard origins are rejected. Allowed preflight requests +do not require authentication; actual project requests always do. The policy +allows Authorization, Content-Type and If-Match, and exposes ETag and Location. +Use a same-origin reverse proxy instead if preferred. CORS is a browser policy; +authorization still governs all callers. For token rotation, add a new digest with the **same principal**, distribute that token securely, remove the old digest, and restart. Multiple distinct digests may @@ -66,10 +91,9 @@ granting access. Do not commit token files or operator config. ## Two-client HTTP walkthrough -Use a JSON file produced by the local editor when its serializer is available. -For a transport-only smoke test, `printf '{}\n' > project.json` is sufficient; -this does not establish scene-format compatibility. These commands use `curl` -and `jq`, with the local token files created above: +Use a JSON file saved by the editor, or copy +`tests/fixtures/core-scene-v1.json` to `project.json` to exercise the canonical +core fixture. These commands use `curl` and `jq`, with the tokens created above: ```sh BASE=http://127.0.0.1:3000 @@ -139,7 +163,7 @@ and [ยง15.5.13](https://www.rfc-editor.org/rfc/rfc9110#section-15.5.13), with a deliberately restricted condition syntax to require an explicit revision. Missing/invalid credentials return `401`; unknown and inaccessible projects -both return `404`. Readers attempting writes receive `403`. Invalid JSON returns +both return `404`. Readers attempting writes receive `403`. Invalid JSON/schema returns `400`, unsupported media/encoding `415`, and oversized projects `413`. Revision conflicts never change the stored bytes or revision. ACL changes are owner-only, do not change the content ETag, and are serialized @@ -163,5 +187,7 @@ Integration tests use real HTTP sockets and temporary SQLite files, including independent database connections racing the same revision, two-client exchange, process kill/restart persistence of contents/revisions/ACLs, denied access, reader/writer/owner permissions, malformed/oversized (including chunked) payloads, -and required preconditions. These are transport tests; the editor's actual -save/load round trip remains a prerequisite for client integration. +required preconditions, canonical core/draft fixtures, schema errors and CORS. +The app's browser suite starts this real server and verifies local save โ†’ upload +โ†’ download โ†’ save equality for the complete three-room draft, including images, +as well as two-user conflicts, explicit reconciliation and denied access. diff --git a/src/cors.rs b/src/cors.rs new file mode 100644 index 0000000..84551c8 --- /dev/null +++ b/src/cors.rs @@ -0,0 +1,26 @@ +use axum::http::{header, HeaderValue, Method}; +use tower_http::cors::CorsLayer; + +/// Explicit browser origin allowlist. Authentication remains mandatory on requests. +pub fn cors(origins: &str) -> Result { + let mut allowed = Vec::new(); + for value in origins.split(',').map(str::trim).filter(|s| !s.is_empty()) { + let url = url::Url::parse(value).map_err(|_| "Invalid PACE_ALLOWED_ORIGINS origin")?; + if !matches!(url.scheme(), "http" | "https") || url.origin().ascii_serialization() != value + { + return Err( + "PACE_ALLOWED_ORIGINS requires exact http(s) origins without paths or wildcards", + ); + } + allowed.push(HeaderValue::from_str(value).map_err(|_| "Invalid origin header")?); + } + Ok(CorsLayer::new() + .allow_origin(allowed) + .allow_methods([Method::GET, Method::POST, Method::PUT, Method::DELETE]) + .allow_headers([ + header::AUTHORIZATION, + header::CONTENT_TYPE, + header::IF_MATCH, + ]) + .expose_headers([header::ETAG, header::LOCATION])) +} diff --git a/src/error.rs b/src/error.rs index bce902e..459d87b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -59,7 +59,7 @@ impl ApiError { Self::new( StatusCode::PAYLOAD_TOO_LARGE, "project_too_large", - "Project exceeds the 1 MiB limit", + "Project exceeds the 64 MiB limit", ) } diff --git a/src/lib.rs b/src/lib.rs index 0d9dbf6..481bf16 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,7 @@ //! Durable, explicitly versioned project snapshots. This is not a co-editing service. pub mod api; pub mod auth; +pub mod cors; pub mod error; pub mod project; pub mod storage; diff --git a/src/main.rs b/src/main.rs index cef490c..9502c05 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,6 +8,7 @@ async fn main() -> Result<(), Box> { let config_path = env::var("PACE_AUTH_FILE").map_err(|_| "PACE_AUTH_FILE is required")?; let auth_bytes = std::fs::read(config_path)?; let auth = Auth::from_json(&auth_bytes)?; + let cors = pace_server::cors::cors(&env::var("PACE_ALLOWED_ORIGINS").unwrap_or_default())?; let database = env::var("PACE_DATABASE").unwrap_or_else(|_| "pace-projects.sqlite3".into()); if database.is_empty() || database == ":memory:" { return Err("PACE_DATABASE must name a persistent SQLite file".into()); @@ -17,7 +18,7 @@ async fn main() -> Result<(), Box> { let bind = env::var("PACE_BIND").unwrap_or_else(|_| "127.0.0.1:3000".into()); let listener = TcpListener::bind(bind).await?; println!("Listening on http://{}", listener.local_addr()?); - axum::serve(listener, authenticated_router(store, auth)) + axum::serve(listener, authenticated_router(store, auth).layer(cors)) .with_graceful_shutdown(shutdown()) .await?; Ok(()) diff --git a/src/project.rs b/src/project.rs index be32679..e62d40d 100644 --- a/src/project.rs +++ b/src/project.rs @@ -3,16 +3,51 @@ use serde::{Deserialize, Serialize}; use crate::error::ApiError; /// Limit applies to the complete uncompressed project, including whitespace. -pub const MAX_PROJECT_BYTES: usize = 1024 * 1024; +pub const MAX_PROJECT_BYTES: usize = pace_editor_format::MAX_PROJECT_BYTES; -/// Transport validation only: the upstream core does not yet define a scene schema. -/// RawValue validates JSON without interpreting or normalizing numbers/extension fields. +/// Validate with the pinned upstream loaders, without reserializing stored bytes. +/// Drafts remain editable (including unresolved references); core scenes must be runnable. pub fn validate_project(bytes: &[u8]) -> Result<(), ApiError> { if bytes.len() > MAX_PROJECT_BYTES { return Err(ApiError::too_large()); } - serde_json::from_slice::<&serde_json::value::RawValue>(bytes) - .map_err(|_| ApiError::bad_request("invalid_project", "Project must be valid JSON"))?; + let text = std::str::from_utf8(bytes) + .map_err(|_| ApiError::bad_request("invalid_project", "Project must be UTF-8 JSON"))?; + #[derive(Deserialize)] + struct Format { + format: Option, + format_version: Option, + } + let header: Format = serde_json::from_str(text).map_err(|_| { + ApiError::bad_request( + "invalid_project", + "Project must be a supported JSON document", + ) + })?; + match (header.format.as_deref(), header.format_version) { + (Some(pace_editor_format::FORMAT), None) => { + pace_editor_format::Project::from_json(text).map_err(|_| { + ApiError::bad_request( + "invalid_project", + "Invalid editor draft: check its version, structure and object IDs", + ) + })?; + } + (None, Some(_)) => { + pace_core::scene::Scene::from_json(text).map_err(|_| { + ApiError::bad_request( + "invalid_project", + "Invalid core scene: check its version, structure, IDs, references and bounds", + ) + })?; + } + _ => { + return Err(ApiError::bad_request( + "unsupported_format", + "Expected a pace-editor-draft v1 or PACE core scene v1", + )) + } + } Ok(()) } diff --git a/src/storage.rs b/src/storage.rs index 430dc25..2c14a21 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -65,8 +65,8 @@ impl Store { } pub async fn create(&self, principal: Principal, body: Vec) -> Result { - validate_project(&body)?; self.run(move |connection| { + validate_project(&body)?; let id = Uuid::new_v4().to_string(); connection.execute( "INSERT INTO projects (id, owner, revision, body) VALUES (?1, ?2, 1, ?3)", @@ -99,8 +99,8 @@ impl Store { expected: i64, body: Vec, ) -> Result { - validate_project(&body)?; self.run(move |connection| { + validate_project(&body)?; let tx = connection.transaction_with_behavior(TransactionBehavior::Immediate)?; let (owner, revision): (String, i64) = tx .query_row( diff --git a/tests/authentication.rs b/tests/authentication.rs index a65f62f..4a83423 100644 --- a/tests/authentication.rs +++ b/tests/authentication.rs @@ -146,7 +146,7 @@ async fn owner_grants_reader_writer_access_and_revokes_it() { .status(), 204 ); - let edit = b"{\"edited_by\":\"bob\",\"scene\":{}}"; + let edit = common::edited("Edited by Bob"); let response = bob .put(&url) .header("If-Match", "\"1\"") @@ -233,7 +233,7 @@ async fn each_creator_owns_only_their_own_projects() { let created = bob .post(format!("{}/v1/projects", server.base)) .header("Content-Type", "application/json") - .body("{\"owner\":\"alice\"}") + .body(common::edited("Alice's title does not confer ownership")) .send() .await .unwrap(); @@ -296,7 +296,7 @@ async fn chunked_uploads_cannot_bypass_the_size_limit() { let mut chunk = b"4000\r\n".to_vec(); chunk.extend(vec![b' '; 16 * 1024]); chunk.extend_from_slice(b"\r\n"); - for _ in 0..65 { if socket.write_all(&chunk).is_err() { break; } } + for _ in 0..=pace_server::project::MAX_PROJECT_BYTES / (16 * 1024) { if socket.write_all(&chunk).is_err() { break; } } let _ = socket.write_all(b"2\r\n{}\r\n0\r\n\r\n"); let mut response = [0; 2048]; let size = socket.read(&mut response).unwrap(); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 2f11325..3c532ef 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -7,8 +7,13 @@ use reqwest::{Client, Response}; use sha2::{Digest, Sha256}; use tokio::{net::TcpListener, task::JoinHandle}; -pub const PROJECT: &[u8] = - b"{ \"scene\": {\"nodes\":[]}, \"extension\": {\"large\":123456789012345678901234567890} }\n"; +pub const PROJECT: &[u8] = include_bytes!("../fixtures/core-scene-v1.json"); + +pub fn edited(title: &str) -> Vec { + let mut value: serde_json::Value = serde_json::from_slice(PROJECT).unwrap(); + value["title"] = title.into(); + serde_json::to_vec_pretty(&value).unwrap() +} pub const ALICE: &str = "alice-test-token-00000000000000000000000000000000"; pub const BOB: &str = "bob-test-token-0000000000000000000000000000000000"; @@ -43,7 +48,8 @@ impl Server { let app = authenticated_router( Store::open(path).unwrap(), Auth::from_json(&auth_json()).unwrap(), - ); + ) + .layer(pace_server::cors::cors("https://editor.example").unwrap()); let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); let base = format!("http://{}", listener.local_addr().unwrap()); let task = tokio::spawn(async move { axum::serve(listener, app).await.unwrap() }); diff --git a/tests/fixtures/LICENSE.md b/tests/fixtures/LICENSE.md new file mode 100644 index 0000000..17c86ee --- /dev/null +++ b/tests/fixtures/LICENSE.md @@ -0,0 +1,11 @@ +# Fixture asset license + +The Last Light's story, dialogue, room layouts, palette and fixture data in +`game.json` were created for this repository. These original demo assets are +dedicated to the public domain under CC0 1.0: +https://creativecommons.org/publicdomain/zero/1.0/ + +The demo uses geometric shapes rather than external artwork. The desktop viewer +uses Bevy's bundled Fira Mono font, distributed by Bevy under the SIL Open Font +License 1.1. No PACE mascot, reference-site artwork, music or third-party game +assets are included. diff --git a/tests/fixtures/core-scene-v1.json b/tests/fixtures/core-scene-v1.json new file mode 100644 index 0000000..58c49ca --- /dev/null +++ b/tests/fixtures/core-scene-v1.json @@ -0,0 +1,105 @@ +{ + "format_version": 1, + "game_id": "last_light", + "title": "The Last Light", + "start_room": "quay", + "items": [ + { "id": "fuse", "name": "Spare fuse", "description": "A dry ceramic fuse, rescued from a supply crate." }, + { "id": "key", "name": "Brass key", "description": "The keeper's key to the lantern stairs." } + ], + "flags": ["briefed", "repaired"], + "rooms": [ + { + "id": "quay", + "name": "01 / The Quay", + "description": "Night is falling. A boat waits beyond the reef, but the lighthouse is dark. Search the supply crate, then find the keeper.", + "color": [24, 49, 68], + "hotspots": [ + { + "id": "crate", "label": "Search supply crate", "rect": [0.08, 0.55, 0.28, 0.3], "once": true, + "blocked_text": "The crate is empty.", "text": "Under a folded oilskin, you find a spare fuse.", + "effects": [{ "type": "give_item", "item": "fuse" }] + }, + { + "id": "workshop", "label": "Enter workshop", "rect": [0.62, 0.15, 0.3, 0.7], + "blocked_text": "The workshop door will not open.", "text": "Warm lamplight spills from the workshop.", + "outcome": { "type": "room", "room": "workshop" } + } + ] + }, + { + "id": "workshop", "name": "02 / The Workshop", + "description": "The keeper studies a chart of the reef. Above the workbench, a locked stairway leads to the lantern.", + "color": [71, 48, 42], + "hotspots": [ + { + "id": "quay", "label": "Return to quay", "rect": [0.04, 0.3, 0.22, 0.55], + "blocked_text": "You cannot leave yet.", "text": "You step back into the salt air.", + "outcome": { "type": "room", "room": "quay" } + }, + { + "id": "keeper", "label": "Talk to the keeper", "rect": [0.34, 0.4, 0.28, 0.45], + "blocked_text": "The keeper is busy.", "text": "The keeper looks up from the chart.", + "outcome": { "type": "dialogue", "dialogue": "keeper" } + }, + { + "id": "stairs", "label": "Unlock lantern stairs", "rect": [0.72, 0.12, 0.24, 0.73], + "requires": [{ "type": "has_item", "item": "key" }], + "blocked_text": "The stairway is locked. Ask the keeper for the brass key.", "text": "The brass key turns. You climb to the lantern.", + "outcome": { "type": "room", "room": "lantern" } + } + ] + }, + { + "id": "lantern", "name": "03 / The Lantern", + "description": "The great lens is still. Fit the spare fuse into the dark control panel, then send a signal across the water.", + "color": [39, 60, 68], + "hotspots": [ + { + "id": "workshop", "label": "Down to workshop", "rect": [0.04, 0.3, 0.24, 0.55], + "blocked_text": "The stairs are blocked.", "text": "You descend the narrow stairs.", + "outcome": { "type": "room", "room": "workshop" } + }, + { + "id": "panel", "label": "Fit spare fuse", "rect": [0.35, 0.55, 0.28, 0.3], "once": true, + "requires": [{ "type": "has_item", "item": "fuse" }, { "type": "flag", "flag": "briefed", "value": true }], + "blocked_text": "You need the spare fuse from the quay and the keeper's instructions.", "text": "The fuse clicks into place. The lens begins to turn.", + "effects": [{ "type": "remove_item", "item": "fuse" }, { "type": "set_flag", "flag": "repaired", "value": true }] + }, + { + "id": "beacon", "label": "Light the beacon", "rect": [0.7, 0.1, 0.26, 0.75], + "requires": [{ "type": "flag", "flag": "repaired", "value": true }], + "blocked_text": "The beacon has no power. Repair the control panel first.", "text": "A ribbon of light sweeps the reef.", + "outcome": { "type": "end", "ending": "safe_harbor" } + } + ] + } + ], + "dialogues": [ + { + "id": "keeper", "start": "greeting", + "nodes": [ + { + "id": "greeting", "speaker": "The keeper", "text": "A boat is waiting offshore. Will you help me bring her home?", + "choices": [ + { "id": "help", "text": "Tell me how to fix the light.", "next": "instructions" }, + { "id": "leave", "text": "I'll be back." } + ] + }, + { + "id": "instructions", "speaker": "The keeper", "text": "Take my key. Find the spare fuse in the quay's supply crate and fit it in the lantern panel. Then light the beacon.", + "choices": [ + { + "id": "accept", "text": "Take the key. I'll get the light working.", "once": true, + "effects": [{ "type": "give_item", "item": "key" }, { "type": "set_flag", "flag": "briefed", "value": true }] + }, + { "id": "leave", "text": "I need a moment." } + ] + } + ] + } + ], + "endings": [ + { "id": "safe_harbor", "text": "The boat answers with two soft bells. Guided by your light, she slips safely into harbor. The keeper waves from the quay. You brought them home. / THE END" } + ] +} diff --git a/tests/fixtures/editor-draft-v1.json b/tests/fixtures/editor-draft-v1.json new file mode 100644 index 0000000..cfe3646 --- /dev/null +++ b/tests/fixtures/editor-draft-v1.json @@ -0,0 +1,191 @@ +{ + "format": "pace-editor-draft", + "version": 1, + "title": "The Garden Key", + "start_room": "room-1", + "rooms": [ + { + "id": "room-1", + "name": "Foyer", + "background": { + "name": "foyer.png", + "data_url": "data:image/png;base64,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", + "width": 960, + "height": 600 + }, + "hotspots": [ + { + "id": "hotspot-10", + "name": "Brass key on table", + "rect": { + "x": 0.44, + "y": 0.6, + "width": 0.16, + "height": 0.15 + }, + "action": { + "kind": "take", + "item_id": "item-4" + }, + "requires_item": null + }, + { + "id": "hotspot-11", + "name": "Study door", + "rect": { + "x": 0.72, + "y": 0.2, + "width": 0.2, + "height": 0.48 + }, + "action": { + "kind": "go_to", + "room_id": "room-2" + }, + "requires_item": "item-4" + } + ] + }, + { + "id": "room-2", + "name": "Study", + "background": { + "name": "study.png", + "data_url": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA8AAAAJYCAIAAAAi9hhWAAAWRklEQVR4nO3da4xmd13A8bO7Q7F10wqNFCNYV5DaoG2sYrfGRuuFDZDGEgFJIxoJBS8vgDThFTHGNBpNjOILwRskGm0bbdRqxFapJUW6zRLrBgtdS2naCmULu724pHZLd3wxm+nu7Mwz53ee8z//y/l8XvUynf2d85zn/L/nv093dlx93fUdAADQz87cAwAAQE0ENAAABAhoAAAIENAAABAgoAEAIEBAAwBAgIAGAIAAAQ0AAAECGgAAAgQ0AAAECGgAAAgQ0AAAECCgAQAgQEADAECAgAYAgAABDQAAAQIaAAACBDQAAAQIaAAACBDQAAAQIKABACBAQAMAQICABgCAAAENAAABAhoAAAIENAAABAhoAAAIWEn0ffd8+7de+6a9u3btfP7E6kduvuO7XvGyN1x5Sdd137Pn2+5/6LGu627798++521XvfODf7r29R+94V1rf/3nv/XuLzzyeNetruza9Yl7PvfJA/e/4oKXvPcd+z7wuzevrq52Xfeb73vrh2/6xKNfOZpocgAAWCBVQP/Sz17123/2T0efOnb5Ja/6uat/+EN/cfuB//pi13UfveFdv/Hhv1v7mve87aoz/8NvPH9i7QtefNaLPvDONz57/Ln9Bx989CtHrrj01Z/+zwe+/+ILDx95Sj0DAJBLqo9wnLf7nLNetKvrus/c99Btn/rsgO/w7PHn/vIfP722b33Lv3zmzT/5Azt27LjmJy675fYDI88KAAC9pdqBvvHj+3/9V9987+cf/tR//Pd9X/jSsG/y8GNHXn7+eV3XfenwE488duS6t/zo15449j+Hnxh1UgAACEi1A/3JA/df/zs3HnrosV/46R95y+tfN+yb7Nq58/kTJ9b++pbbD1z1QxfbfgYAIK8kAX3u7rNf850v//ozz9554P4b/ujWn7ritZt+2erq6s6dO7rTQ/lUr/6Olz3y2MmPO3/5q08+8+xzX/7qkykGBgCAnpIE9Orq6vvese/8b9nddd3uc77pa08e2/TLHnz08e97zSu7rrvkold+8ZHHN/zbbz77xde+6Ypb/+3eFBMCAMAwST4D/b9f/78/+Zs73//z+44/940TJ1Y/cvMdm37Zx/72rne/9ceu+fHLuq7747++8+RAu3b+2i9f03Wru3bt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", + "width": 960, + "height": 600 + }, + "hotspots": [ + { + "id": "hotspot-12", + "name": "Return to foyer", + "rect": { + "x": 0.08, + "y": 0.2, + "width": 0.18, + "height": 0.48 + }, + "action": { + "kind": "go_to", + "room_id": "room-1" + }, + "requires_item": null + }, + { + "id": "hotspot-13", + "name": "Talk to gardener", + "rect": { + "x": 0.4, + "y": 0.55, + "width": 0.24, + "height": 0.18 + }, + "action": { + "kind": "talk", + "dialogue_id": "dialogue-7" + }, + "requires_item": null + }, + { + "id": "hotspot-14", + "name": "Garden gate", + "rect": { + "x": 0.72, + "y": 0.2, + "width": 0.2, + "height": 0.48 + }, + "action": { + "kind": "go_to", + "room_id": "room-3" + }, + "requires_item": "item-5" + } + ] + }, + { + "id": "room-3", + "name": "Garden", + "background": { + "name": "garden.png", + "data_url": "data:image/png;base64,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", + "width": 960, + "height": 600 + }, + "hotspots": [ + { + "id": "hotspot-15", + "name": "Return to study", + "rect": { + "x": 0.08, + "y": 0.2, + "width": 0.18, + "height": 0.48 + }, + "action": { + "kind": "go_to", + "room_id": "room-2" + }, + "requires_item": null + }, + { + "id": "hotspot-16", + "name": "Statue", + "rect": { + "x": 0.42, + "y": 0.4, + "width": 0.18, + "height": 0.3 + }, + "action": { + "kind": "look", + "text": "The garden remembers every visitor." + }, + "requires_item": null + } + ] + } + ], + "items": [ + { + "id": "item-4", + "name": "Brass key", + "description": "Opens the study door." + }, + { + "id": "item-5", + "name": "Garden pass", + "description": "A gift from the gardener." + } + ], + "dialogues": [ + { + "id": "dialogue-6", + "name": "Farewell", + "speaker": "Gardener", + "text": "The garden is waiting for you.", + "choices": [] + }, + { + "id": "dialogue-7", + "name": "Meet the gardener", + "speaker": "Gardener", + "text": "Would you like to visit the garden?", + "choices": [ + { + "id": "choice-8", + "text": "May I enter?", + "next": "dialogue-6", + "give_item": "item-5" + }, + { + "id": "choice-9", + "text": "Not yet.", + "next": null, + "give_item": null + } + ] + } + ], + "next_id": 17 +} \ No newline at end of file diff --git a/tests/formats_and_cors.rs b/tests/formats_and_cors.rs new file mode 100644 index 0000000..bfc163e --- /dev/null +++ b/tests/formats_and_cors.rs @@ -0,0 +1,185 @@ +mod common; + +use common::{Server, PROJECT}; +use tempfile::tempdir; + +const DRAFT: &[u8] = include_bytes!("fixtures/editor-draft-v1.json"); + +#[tokio::test] +async fn both_upstream_formats_roundtrip_with_original_bytes_and_assets() { + let dir = tempdir().unwrap(); + let server = Server::start(&dir.path().join("projects.db")).await; + for fixture in [PROJECT, DRAFT] { + let response = server.create(fixture).await; + assert_eq!(response.status(), 201); + let url = format!( + "{}{}", + server.base, + response.headers()["Location"].to_str().unwrap() + ); + let downloaded = server + .client + .get(url) + .send() + .await + .unwrap() + .bytes() + .await + .unwrap(); + assert_eq!(downloaded.as_ref(), fixture); + let text = std::str::from_utf8(&downloaded).unwrap(); + if fixture == PROJECT { + pace_core::scene::Scene::from_json(text).unwrap(); + } else { + assert!(pace_editor_format::Project::from_json(text) + .unwrap() + .validate() + .is_empty()); + } + } +} + +#[tokio::test] +async fn schema_errors_and_future_versions_never_replace_existing_content() { + let dir = tempdir().unwrap(); + let server = Server::start(&dir.path().join("projects.db")).await; + let (url, etag) = server.project().await; + let core: serde_json::Value = serde_json::from_slice(PROJECT).unwrap(); + let draft: serde_json::Value = serde_json::from_slice(DRAFT).unwrap(); + let mut invalid = vec![ + serde_json::json!({}), + serde_json::json!([]), + serde_json::json!(null), + ]; + for (pointer, value) in [ + ("/format_version", serde_json::json!(99)), + ("/start_room", serde_json::json!("missing")), + ("/rooms/0/hotspots/0/rect", serde_json::json!([2, 0, 1, 1])), + ("/rooms/1/id", serde_json::json!("quay")), + ] { + let mut scene = core.clone(); + *scene.pointer_mut(pointer).unwrap() = value; + invalid.push(scene); + } + let mut unknown = core.clone(); + unknown["typo"] = true.into(); + invalid.push(unknown); + for (pointer, value) in [ + ("/version", serde_json::json!(99)), + ("/rooms/1/id", draft["rooms"][0]["id"].clone()), + ( + "/rooms/0/hotspots/0/action/kind", + serde_json::json!("run_script"), + ), + ] { + let mut scene = draft.clone(); + *scene.pointer_mut(pointer).unwrap() = value; + invalid.push(scene); + } + let mut unknown = draft.clone(); + unknown["owner"] = "eve".into(); + invalid.push(unknown); + for value in invalid { + let bytes = serde_json::to_vec(&value).unwrap(); + assert_eq!(server.create(bytes.clone()).await.status(), 400, "{value}"); + assert_eq!( + server + .client + .put(&url) + .header("If-Match", &etag) + .header("Content-Type", "application/json") + .body(bytes) + .send() + .await + .unwrap() + .status(), + 400 + ); + } + let downloaded = server.client.get(url).send().await.unwrap(); + assert_eq!(downloaded.headers()["ETag"], etag); + assert_eq!(downloaded.bytes().await.unwrap().as_ref(), PROJECT); +} + +#[tokio::test] +async fn editable_drafts_preserve_incomplete_references_like_local_save_load() { + let dir = tempdir().unwrap(); + let server = Server::start(&dir.path().join("projects.db")).await; + let mut project = + pace_editor_format::Project::from_json(std::str::from_utf8(DRAFT).unwrap()).unwrap(); + project.start_room = Some("unfinished-room".into()); + let bytes = project.to_json().unwrap().into_bytes(); + assert!(!project.validate().is_empty()); + assert_eq!(server.create(bytes).await.status(), 201); +} + +#[tokio::test] +async fn only_configured_origins_can_read_responses_and_revision_headers() { + let dir = tempdir().unwrap(); + let server = Server::start(&dir.path().join("projects.db")).await; + let client = reqwest::Client::new(); + let url = format!("{}/v1/projects", server.base); + for origin in ["https://editor.example", "https://untrusted.example"] { + let response = client + .request(reqwest::Method::OPTIONS, &url) + .header("Origin", origin) + .header("Access-Control-Request-Method", "PUT") + .header( + "Access-Control-Request-Headers", + "authorization,content-type,if-match", + ) + .send() + .await + .unwrap(); + assert!(response.status().is_success()); + if origin == "https://editor.example" { + assert_eq!(response.headers()["Access-Control-Allow-Origin"], origin); + assert!(response.headers()["Access-Control-Allow-Headers"] + .to_str() + .unwrap() + .contains("if-match")); + } else { + assert!(response + .headers() + .get("Access-Control-Allow-Origin") + .is_none()); + } + } + let response = server + .client + .post(&url) + .header("Origin", "https://editor.example") + .header("Content-Type", "application/json") + .body(PROJECT) + .send() + .await + .unwrap(); + assert_eq!(response.status(), 201); + assert_eq!( + response.headers()["Access-Control-Allow-Origin"], + "https://editor.example" + ); + assert!(response.headers()["Access-Control-Expose-Headers"] + .to_str() + .unwrap() + .contains("etag")); + assert_eq!( + client + .post(url) + .header("Origin", "https://editor.example") + .send() + .await + .unwrap() + .status(), + 401 + ); + for bad in [ + "*", + "null", + "https://editor.example/path", + "https://user:pass@editor.example", + "ftp://editor.example", + ] { + assert!(pace_server::cors::cors(bad).is_err()); + } +} diff --git a/tests/project_api.rs b/tests/project_api.rs index f1e4beb..91c2530 100644 --- a/tests/project_api.rs +++ b/tests/project_api.rs @@ -16,7 +16,7 @@ async fn two_clients_exchange_exact_bytes_and_stale_writes_conflict() { assert_eq!(response.headers()["ETag"], revision); assert_eq!(response.bytes().await.unwrap().as_ref(), PROJECT); - let next = b"{\"scene\":{\"nodes\":[{\"name\":\"edited\"}]},\"unknown\":true}"; + let next = common::edited("Edited title"); let response = second_client .put(&url) .header("If-Match", &revision) @@ -101,14 +101,14 @@ async fn independent_database_connections_cannot_both_win_a_revision() { .put(&url) .header("If-Match", &revision) .header("Content-Type", "application/json") - .body("{\"winner\":1}") + .body(common::edited("First writer")) .send(); let b = second .client .put(&second_url) .header("If-Match", &revision) .header("Content-Type", "application/json") - .body("{\"winner\":2}") + .body(common::edited("Second writer")) .send(); let (a, b) = tokio::join!(a, b); let a = a.unwrap().status(); @@ -186,7 +186,7 @@ async fn malformed_and_oversized_uploads_leave_existing_project_unchanged() { async fn exact_size_limit_is_accepted_and_unsupported_media_is_rejected() { let dir = tempdir().unwrap(); let server = Server::start(&dir.path().join("projects.db")).await; - let mut body = b"{}".to_vec(); + let mut body = PROJECT.to_vec(); body.resize(MAX_PROJECT_BYTES, b' '); assert_eq!(server.create(body).await.status(), 201); let url = format!("{}/v1/projects", server.base);