From 60c60a8044f2745ab1b27037d57d3c7842098b18 Mon Sep 17 00:00:00 2001 From: FallenSigh Date: Sun, 7 Jun 2026 19:20:19 +0800 Subject: [PATCH] feat: initial xserial-core with transport, protocol, frame, and pipeline layers - transport: async Serial/TCP/UDP via Connection enum + Transport trait - protocol: TextDecoder (UTF-8/Latin-1/ASCII), HexDecoder, PlotDecoder (binary samples) - frame: LineFramer, FixedLengthFramer, LengthPrefixedFramer, CobsFramer - pipeline: MultiPipeline for parallel multi-protocol parsing - 212 tests (182 unit + 18 integration + 1 doctest), clippy clean --- .gitignore | 1 + Cargo.lock | 5917 +++++++++++++++++++ Cargo.toml | 54 + crates/xserial-client/Cargo.toml | 9 + crates/xserial-client/src/lib.rs | 0 crates/xserial-core/Cargo.toml | 17 + crates/xserial-core/src/error.rs | 27 + crates/xserial-core/src/frame/cobs.rs | 267 + crates/xserial-core/src/frame/fixed.rs | 151 + crates/xserial-core/src/frame/length.rs | 370 ++ crates/xserial-core/src/frame/line.rs | 218 + crates/xserial-core/src/frame/mod.rs | 35 + crates/xserial-core/src/lib.rs | 7 + crates/xserial-core/src/pipeline.rs | 430 ++ crates/xserial-core/src/protocol/hex.rs | 229 + crates/xserial-core/src/protocol/mod.rs | 41 + crates/xserial-core/src/protocol/plot.rs | 508 ++ crates/xserial-core/src/protocol/text.rs | 145 + crates/xserial-core/src/transport/mod.rs | 767 +++ crates/xserial-core/src/transport/serial.rs | 286 + crates/xserial-core/src/transport/tcp.rs | 325 + crates/xserial-core/src/transport/udp.rs | 426 ++ crates/xserial-core/tests/pipeline.rs | 775 +++ crates/xserial-gui/Cargo.toml | 18 + crates/xserial-gui/src/main.rs | 3 + crates/xserial-tui/Cargo.toml | 21 + crates/xserial-tui/src/main.rs | 3 + tools/gen_plot.py | 112 + 28 files changed, 11162 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 crates/xserial-client/Cargo.toml create mode 100644 crates/xserial-client/src/lib.rs create mode 100644 crates/xserial-core/Cargo.toml create mode 100644 crates/xserial-core/src/error.rs create mode 100644 crates/xserial-core/src/frame/cobs.rs create mode 100644 crates/xserial-core/src/frame/fixed.rs create mode 100644 crates/xserial-core/src/frame/length.rs create mode 100644 crates/xserial-core/src/frame/line.rs create mode 100644 crates/xserial-core/src/frame/mod.rs create mode 100644 crates/xserial-core/src/lib.rs create mode 100644 crates/xserial-core/src/pipeline.rs create mode 100644 crates/xserial-core/src/protocol/hex.rs create mode 100644 crates/xserial-core/src/protocol/mod.rs create mode 100644 crates/xserial-core/src/protocol/plot.rs create mode 100644 crates/xserial-core/src/protocol/text.rs create mode 100644 crates/xserial-core/src/transport/mod.rs create mode 100644 crates/xserial-core/src/transport/serial.rs create mode 100644 crates/xserial-core/src/transport/tcp.rs create mode 100644 crates/xserial-core/src/transport/udp.rs create mode 100644 crates/xserial-core/tests/pipeline.rs create mode 100644 crates/xserial-gui/Cargo.toml create mode 100644 crates/xserial-gui/src/main.rs create mode 100644 crates/xserial-tui/Cargo.toml create mode 100644 crates/xserial-tui/src/main.rs create mode 100755 tools/gen_plot.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..b231551 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,5917 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "accesskit" +version = "0.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5351dcebb14b579ccab05f288596b2ae097005be7ee50a7c3d4ca9d0d5a66f6a" +dependencies = [ + "uuid", +] + +[[package]] +name = "accesskit_atspi_common" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8c61bee90b42a772d39d06a740207dc71a4e780004ace1db8d99fb1baaa954" +dependencies = [ + "accesskit", + "accesskit_consumer 0.36.0", + "atspi-common", + "phf 0.13.1", + "serde", + "zvariant", +] + +[[package]] +name = "accesskit_consumer" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_consumer" +version = "0.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25e0d7e25d06f4dc21d1774d67146e9e80d6789216cbd4d1e88185b0095dba60" +dependencies = [ + "accesskit", + "hashbrown 0.16.1", +] + +[[package]] +name = "accesskit_macos" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c5c87e8d94f2ec10cce590aadff24c76f576dab5502d45d0aed9fc3065d4451" +dependencies = [ + "accesskit", + "accesskit_consumer 0.36.0", + "hashbrown 0.16.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "accesskit_unix" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b016ca8db0ea0ea2ceff29a9d6240391492d960716aa471967c00e8cc8cb197c" +dependencies = [ + "accesskit", + "accesskit_atspi_common", + "async-channel", + "async-executor", + "async-task", + "atspi", + "futures-lite", + "futures-util", + "serde", + "zbus", +] + +[[package]] +name = "accesskit_windows" +version = "0.32.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eff7009f1a532e917d66970a1e80c965140c6cfbbabbdde3d64e5431e6c78e21" +dependencies = [ + "accesskit", + "accesskit_consumer 0.35.0", + "hashbrown 0.16.1", + "static_assertions", + "windows", + "windows-core", +] + +[[package]] +name = "accesskit_winit" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fe9a94394896352cc4660ca2288bd4ef883d83238853c038b44070c8f134313" +dependencies = [ + "accesskit", + "accesskit_macos", + "accesskit_unix", + "accesskit_windows", + "raw-window-handle", + "winit", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-activity" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f2a1bb052857d5dd49572219344a7332b31b76405648eabac5bc68978251bcd" +dependencies = [ + "android-properties", + "bitflags 2.12.1", + "cc", + "jni", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 2.0.18", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys 0.61.2", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "image", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ash" +version = "0.38.0+1.3.281" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +dependencies = [ + "libloading", +] + +[[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-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 1.1.4", + "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-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "atomic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89cbf775b137e9b968e67227ef7f775587cde3fd31b0d8599dbd0f598a48340" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "atspi" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c77886257be21c9cd89a4ae7e64860c6f0eefca799bb79127913052bd0eefb3d" +dependencies = [ + "atspi-common", + "atspi-proxies", +] + +[[package]] +name = "atspi-common" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20c5617155740c98003016429ad13fe43ce7a77b007479350a9f8bf95a29f63d" +dependencies = [ + "enumflags2", + "serde", + "static_assertions", + "zbus", + "zbus-lockstep", + "zbus-lockstep-macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "atspi-proxies" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2230e48787ed3eb4088996eab66a32ca20c0b67bbd4fd6cdfe79f04f1f04c9fc" +dependencies = [ + "atspi-common", + "serde", + "zbus", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec 0.6.3", +] + +[[package]] +name = "bit-set" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" +dependencies = [ + "bit-vec 0.9.1", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a" + +[[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.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f" +dependencies = [ + "objc2 0.5.2", +] + +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2 0.6.4", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +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.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "calloop" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec" +dependencies = [ + "bitflags 2.12.1", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.12.1", + "polling", + "rustix 1.1.4", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a66a987056935f7efce4ab5668920b5d0dac4a7c99991a67395f13702ddd20" +dependencies = [ + "calloop 0.13.0", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.4", + "rustix 1.1.4", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "castaway" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" +dependencies = [ + "rustversion", +] + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "clap" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51" +dependencies = [ + "clap_builder", + "clap_derive", +] + +[[package]] +name = "clap_builder" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim", +] + +[[package]] +name = "clap_derive" +version = "4.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "clap_lex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "codespan-reporting" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af491d569909a7e4dee0ad7db7f5341fef5c614d5b8ec8cf765732aba3cff681" +dependencies = [ + "serde", + "termcolor", + "unicode-width", +] + +[[package]] +name = "color" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ec7c5eb7a16992b1904d76c517d170ab353b0e0b3d5a0c81a8a0cd1037893cf" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "compact_str" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" +dependencies = [ + "castaway", + "cfg-if", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "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.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[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 = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.12.1", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.12.1", + "crossterm_winapi", + "derive_more", + "document-features", + "mio", + "parking_lot", + "rustix 1.1.4", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + +[[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 = "csscolorparser" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb2a7d3066da2de787b7f032c736763eb7ae5d355f81a68bab2675a96008b0bf" +dependencies = [ + "lab", + "phf 0.11.3", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "deltae" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5729f5117e208430e437df2f4843f5e5952997175992d1414f94c57d61e270b4" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[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.117", +] + +[[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 = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.12.1", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "dpi" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" + +[[package]] +name = "ecolor" +version = "0.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a05fbfa222ffb51989d5ccf33e5f7aebfcf96c5023413856b0c3618a7f79896e" +dependencies = [ + "bytemuck", + "emath", +] + +[[package]] +name = "eframe" +version = "0.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f98fe83b2589105b69dd25ca1e0fa2135a6e864d502fd8e08978f937e128cfef" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "egui-wgpu", + "egui-winit", + "egui_glow", + "glutin", + "glutin-winit", + "image", + "js-sys", + "log", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-foundation 0.3.2", + "parking_lot", + "percent-encoding", + "pollster", + "profiling", + "raw-window-handle", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "wgpu", + "windows-sys 0.61.2", + "winit", +] + +[[package]] +name = "egui" +version = "0.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42112be0ae157289312b92b3dfaf20e911b5a3c4c65d4aab0e7c47fbc0ce16e3" +dependencies = [ + "accesskit", + "ahash", + "bitflags 2.12.1", + "emath", + "epaint", + "log", + "nohash-hasher", + "profiling", + "smallvec", + "unicode-segmentation", +] + +[[package]] +name = "egui-wgpu" +version = "0.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f0c0559ac5598a1b887a6206dccbab7e3e6246c57cb00ae287262bd44776c9c" +dependencies = [ + "ahash", + "bytemuck", + "document-features", + "egui", + "epaint", + "log", + "profiling", + "thiserror 2.0.18", + "type-map", + "web-time", + "wgpu", + "winit", +] + +[[package]] +name = "egui-winit" +version = "0.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "967c5b323625d46d46a59b5daba3fef742248d27693cc18972458619858c4239" +dependencies = [ + "accesskit_winit", + "arboard", + "bytemuck", + "egui", + "log", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "objc2-ui-kit 0.3.2", + "profiling", + "raw-window-handle", + "smithay-clipboard", + "web-time", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_glow" +version = "0.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b652957fa7e1ab01e8fecbfbf4e35f6e43a53fa98af8a562b50d5403cd44b9" +dependencies = [ + "bytemuck", + "egui", + "glow", + "log", + "memoffset", + "profiling", + "wasm-bindgen", + "web-sys", + "winit", +] + +[[package]] +name = "egui_plot" +version = "0.35.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7bd66213736bf9a9a53dc4888570b9194fc0db906507517a7fcc787e888ac47" +dependencies = [ + "ahash", + "egui", + "emath", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "emath" +version = "0.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b53f0d33a479321da6b0caa71366c9f67e8a2c149762d90bdc0d16e601ee8ecb" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "epaint" +version = "0.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6675898a291ec212fc3df04f537d177fce8496120244590e6359dcaa4c25da79" +dependencies = [ + "ahash", + "bytemuck", + "ecolor", + "emath", + "epaint_default_fonts", + "font-types", + "log", + "nohash-hasher", + "parking_lot", + "profiling", + "self_cell", + "skrifa", + "smallvec", + "vello_cpu", +] + +[[package]] +name = "epaint_default_fonts" +version = "0.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8970033a4282a7bcf899b38b5ed3a58b732fe093d03785d58648515d8d309da" + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "euclid" +version = "0.22.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a05365e3b1c6d1650318537c7460c6923f1abdd272ad6842baa2b509957a06" +dependencies = [ + "num-traits", +] + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set 0.5.3", + "regex", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "fearless_simd" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fb2907d1f08b2b316b9223ced5b0e89d87028ba8deae9764741dba8ff7f3903" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "filedescriptor" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e40758ed24c9b2eeb76c35fb0aebc66c626084edd827e07e1552279814c6682d" +dependencies = [ + "libc", + "thiserror 1.0.69", + "winapi", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "finl_unicode" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9844ddc3a6e533d62bba727eb6c28b5d360921d5175e9ff0f1e621a5c590a4d5" + +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "font-types" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b38ad915f6dadd993ced50848a8291a543bd41ca62bc10740d5e64e2ab4cfd7" +dependencies = [ + "bytemuck", +] + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-executor" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[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.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glow" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29038e1c483364cc6bb3cf78feee1816002e127c331a1eec55a4d202b9e1adb5" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12124de845cacfebedff80e877bb37b5b75c34c5a4c89e47e1cdd67fb6041325" +dependencies = [ + "bitflags 2.12.1", + "cfg_aliases", + "cgl", + "dispatch2", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "libloading", + "objc2 0.6.4", + "objc2-app-kit 0.3.2", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "once_cell", + "raw-window-handle", + "wayland-sys", + "windows-sys 0.52.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85edca7075f8fc728f28cb8fbb111a96c3b89e930574369e3e9c27eb75d3788f" +dependencies = [ + "cfg_aliases", + "glutin", + "raw-window-handle", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4680ba6195f424febdc3ba46e7a42a0e58743f2edb115297b86d7f8ecc02d2" +dependencies = [ + "gl_generator", + "windows-sys 0.52.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7bb2938045a88b612499fbcba375a77198e01306f52272e692f8c1f3751185" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c4ee00b289aba7a9e5306d57c2d05499b2e5dc427f84ac708bd2c090212cf3e" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "gpu-allocator" +version = "0.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51255ea7cfaadb6c5f1528d43e92a82acb2b96c43365989a28b2d44ee38f8795" +dependencies = [ + "ash", + "hashbrown 0.16.1", + "log", + "presser", + "thiserror 2.0.18", + "windows", +] + +[[package]] +name = "gpu-descriptor" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b89c83349105e3732062a895becfc71a8f921bb71ecbbdd8ff99263e3b53a0ca" +dependencies = [ + "bitflags 2.12.1", + "gpu-descriptor-types", + "hashbrown 0.15.5", +] + +[[package]] +name = "gpu-descriptor-types" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdf242682df893b86f33a73828fb09ca4b2d3bb6cc95249707fc684d27484b91" +dependencies = [ + "bitflags 2.12.1", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "num-traits", + "zerocopy", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "allocator-api2", + "equivalent", + "foldhash 0.2.0", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hexf-parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png", + "tiff", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "instability" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb2d60ef19920a3a9193c3e371f726ec1dafc045dac788d0fb3704272458971" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "io-kit-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617ee6cf8e3f66f3b4ea67a4058564628cde41901316e19f559e14c7c72c5e7b" +dependencies = [ + "core-foundation-sys", + "mach2", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" +dependencies = [ + "cfg-if", + "combine", + "jni-macros", + "jni-sys 0.4.1", + "log", + "simd_cesu8", + "thiserror 2.0.18", + "walkdir", + "windows-link", +] + +[[package]] +name = "jni-macros" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", + "simd_cesu8", + "syn 2.0.117", +] + +[[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" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "kasuari" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde5057d6143cc94e861d90f591b9303d6716c6b9602309150bd068853c10899" +dependencies = [ + "hashbrown 0.16.1", + "portable-atomic", + "thiserror 2.0.18", +] + +[[package]] +name = "khronos-egl" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76" +dependencies = [ + "libc", + "libloading", + "pkg-config", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "kurbo" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b60dfc32f652b926df6192e55525b16d186c69d47876c3ead4da5cc9f8450e2" +dependencies = [ + "arrayvec", + "euclid", + "polycool", + "smallvec", +] + +[[package]] +name = "lab" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf36173d4167ed999940f804952e6b08197cae5ad5d572eb4db150ce8ad5d58f" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "bitflags 2.12.1", + "libc", + "plain", + "redox_syscall 0.8.1", +] + +[[package]] +name = "libudev" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b324152da65df7bb95acfcaab55e3097ceaab02fb19b228a9eb74d55f135e0" +dependencies = [ + "libc", + "libudev-sys", +] + +[[package]] +name = "libudev-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8469b4a23b962c1396b9b451dda50ef5b283e8dd309d69033475fa9b334324" +dependencies = [ + "libc", + "pkg-config", +] + +[[package]] +name = "line-clipping" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f50e8f47623268b5407192d26876c4d7f89d686ca130fdc53bced4814cd29f8" +dependencies = [ + "bitflags 2.12.1", +] + +[[package]] +name = "linebender_resource_handle" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "113b30b4cd05f7c06868fdb2854f66a7b9fece9a48425351cd532e810d74024f" + +[[package]] +name = "lru" +version = "0.16.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" +dependencies = [ + "hashbrown 0.16.1", +] + +[[package]] +name = "mac_address" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0aeb26bf5e836cc1c341c8106051b573f1766dfa05aa87f0b98be5e51b02303" +dependencies = [ + "nix 0.29.0", + "winapi", +] + +[[package]] +name = "mach2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d640282b302c0bb0a2a8e0233ead9035e3bed871f0b7e81fe4a1ec829765db44" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "memmem" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a64a92489e2744ce060c349162be1c5f33c6969234104dbd99ddb5feb08b8c15" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "mio-serial" +version = "5.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "029e1f407e261176a983a6599c084efd322d9301028055c87174beac71397ba3" +dependencies = [ + "log", + "mio", + "nix 0.29.0", + "serialport", + "winapi", +] + +[[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 = "naga" +version = "29.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd91265cc2454558f659b3b4b9640f0ddb8cc6521277f166b8a8c181c898079" +dependencies = [ + "arrayvec", + "bit-set 0.9.1", + "bitflags 2.12.1", + "cfg-if", + "cfg_aliases", + "codespan-reporting", + "half", + "hashbrown 0.16.1", + "hexf-parse", + "indexmap", + "libm", + "log", + "num-traits", + "once_cell", + "rustc-hash 1.1.0", + "spirv", + "thiserror 2.0.18", + "unicode-ident", +] + +[[package]] +name = "ndk" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4" +dependencies = [ + "bitflags 2.12.1", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle", + "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.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "598beaf3cc6fdd9a5dfb1630c2800c7acd31df7aaf0f565796fba2b53ca1af1b" +dependencies = [ + "bitflags 1.3.2", + "cfg-if", + "libc", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.12.1", + "cfg-if", + "cfg_aliases", + "libc", + "memoffset", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nom" +version = "8.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405" +dependencies = [ + "memchr", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-derive" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[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.117", +] + +[[package]] +name = "num_threads" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" +dependencies = [ + "libc", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804" +dependencies = [ + "objc-sys", + "objc2-encode", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-app-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" +dependencies = [ + "bitflags 2.12.1", + "block2 0.5.1", + "libc", + "objc2 0.5.2", + "objc2-core-data", + "objc2-core-image", + "objc2-foundation 0.2.2", + "objc2-quartz-core 0.2.2", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.12.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-core-graphics", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-cloud-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" +dependencies = [ + "bitflags 2.12.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-contacts" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-data" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" +dependencies = [ + "bitflags 2.12.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.12.1", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.12.1", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-core-image" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-core-location" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-contacts", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" +dependencies = [ + "bitflags 2.12.1", + "block2 0.5.1", + "dispatch", + "libc", + "objc2 0.5.2", +] + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.12.1", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.12.1", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-link-presentation" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" +dependencies = [ + "bitflags 2.12.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-metal" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0125f776a10d00af4152d74616409f0d4a2053a6f57fa5b7d6aa2854ac04794" +dependencies = [ + "bitflags 2.12.1", + "block2 0.6.2", + "objc2 0.6.4", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" +dependencies = [ + "bitflags 2.12.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", + "objc2-metal 0.2.2", +] + +[[package]] +name = "objc2-quartz-core" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96c1358452b371bf9f104e21ec536d37a650eb10f7ee379fff67d2e08d537f1f" +dependencies = [ + "bitflags 2.12.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", +] + +[[package]] +name = "objc2-symbols" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a684efe3dec1b305badae1a28f6555f6ddd3bb2c2267896782858d5a78404dc" +dependencies = [ + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" +dependencies = [ + "bitflags 2.12.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-cloud-kit", + "objc2-core-data", + "objc2-core-image", + "objc2-core-location", + "objc2-foundation 0.2.2", + "objc2-link-presentation", + "objc2-quartz-core 0.2.2", + "objc2-symbols", + "objc2-uniform-type-identifiers", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-ui-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" +dependencies = [ + "bitflags 2.12.1", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", +] + +[[package]] +name = "objc2-uniform-type-identifiers" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" +dependencies = [ + "block2 0.5.1", + "objc2 0.5.2", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" +dependencies = [ + "bitflags 2.12.1", + "block2 0.5.1", + "objc2 0.5.2", + "objc2-core-location", + "objc2-foundation 0.2.2", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-float" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "peniko" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "839c8299360d2e998bdb106dc0a6cd71dcc5f4df51df1b620361bf50e283cca6" +dependencies = [ + "bytemuck", + "color", + "kurbo", + "linebender_resource_handle", + "smallvec", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "phf" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078" +dependencies = [ + "phf_macros 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros 0.13.1", + "phf_shared 0.13.1", + "serde", +] + +[[package]] +name = "phf_codegen" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", +] + +[[package]] +name = "phf_generator" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d" +dependencies = [ + "phf_shared 0.11.3", + "rand", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared 0.13.1", +] + +[[package]] +name = "phf_macros" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216" +dependencies = [ + "phf_generator 0.11.3", + "phf_shared 0.11.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator 0.13.1", + "phf_shared 0.13.1", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5" +dependencies = [ + "siphasher", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pin-project-lite" +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.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.12.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[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 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3" + +[[package]] +name = "polycool" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50596ddc09eb5ad5f75cacd40209568e66df71baf86e1499a0e99c4cff12a5a6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "presser" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa" + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "profiling" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d595e54a326bc53c1c197b32d295e14b169e3cfeaa8dc82b529f947fba6bcf5" + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", + "serde", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "range-alloc" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08" + +[[package]] +name = "ratatui" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc" +dependencies = [ + "instability", + "ratatui-core", + "ratatui-crossterm", + "ratatui-macros", + "ratatui-termwiz", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293" +dependencies = [ + "bitflags 2.12.1", + "compact_str", + "hashbrown 0.16.1", + "indoc", + "itertools", + "kasuari", + "lru", + "strum", + "thiserror 2.0.18", + "unicode-segmentation", + "unicode-truncate", + "unicode-width", +] + +[[package]] +name = "ratatui-crossterm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3" +dependencies = [ + "cfg-if", + "crossterm 0.29.0", + "instability", + "ratatui-core", +] + +[[package]] +name = "ratatui-macros" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4" +dependencies = [ + "ratatui-core", + "ratatui-widgets", +] + +[[package]] +name = "ratatui-termwiz" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c" +dependencies = [ + "ratatui-core", + "termwiz", +] + +[[package]] +name = "ratatui-widgets" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db" +dependencies = [ + "bitflags 2.12.1", + "hashbrown 0.16.1", + "indoc", + "instability", + "itertools", + "line-clipping", + "ratatui-core", + "strum", + "time", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "raw-window-metal" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40d213455a5f1dc59214213c7330e074ddf8114c9a42411eb890c767357ce135" +dependencies = [ + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-quartz-core 0.3.2", +] + +[[package]] +name = "read-fonts" +version = "0.37.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b634fabf032fab15307ffd272149b622260f55974d9fad689292a5d33df02e5" +dependencies = [ + "bytemuck", + "font-types", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.12.1", +] + +[[package]] +name = "redox_syscall" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7" +dependencies = [ + "bitflags 2.12.1", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "renderdoc-sys" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" + +[[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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.12.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.12.1", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sctk-adwaita" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6277f0217056f77f1d8f49f2950ac6c278c0d607c45f5ee99328d792ede24ec" +dependencies = [ + "ab_glyph", + "log", + "memmap2", + "smithay-client-toolkit 0.19.2", + "tiny-skia", +] + +[[package]] +name = "self_cell" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b12e76d157a900eb52e81bc6e9f3069344290341720e9178cde2407113ac8d89" + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serialport" +version = "4.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4d91116f97173694f1642263b2ff837f80d933aa837e2314969f6728f661df3" +dependencies = [ + "bitflags 2.12.1", + "cfg-if", + "core-foundation 0.10.1", + "core-foundation-sys", + "io-kit-sys", + "libudev", + "mach2", + "nix 0.26.4", + "scopeguard", + "unescaper", + "windows-sys 0.52.0", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version", + "simdutf8", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "skrifa" +version = "0.40.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fbdfe3d2475fbd7ddd1f3e5cf8288a30eb3e5f95832829570cd88115a7434ac" +dependencies = [ + "bytemuck", + "read-fonts", +] + +[[package]] +name = "slab" +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.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smithay-client-toolkit" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3457dea1f0eb631b4034d61d4d8c32074caa6cd1ab2d59f2327bd8461e2c0016" +dependencies = [ + "bitflags 2.12.1", + "calloop 0.13.0", + "calloop-wayland-source 0.3.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.12.1", + "calloop 0.14.4", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.4", + "thiserror 2.0.18", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + +[[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.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "spirv" +version = "0.4.0+sdk-1.4.341.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9571ea910ebd84c86af4b3ed27f9dbdc6ad06f17c5f96146b2b671e2976744f" +dependencies = [ + "bitflags 2.12.1", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strict-num" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "terminfo" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ea810f0692f9f51b382fff5893887bb4580f5fa246fde546e0b13e7fcee662" +dependencies = [ + "fnv", + "nom 7.1.3", + "phf 0.11.3", + "phf_codegen", +] + +[[package]] +name = "termios" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" +dependencies = [ + "libc", +] + +[[package]] +name = "termwiz" +version = "0.23.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7" +dependencies = [ + "anyhow", + "base64", + "bitflags 2.12.1", + "fancy-regex", + "filedescriptor", + "finl_unicode", + "fixedbitset", + "hex", + "lazy_static", + "libc", + "log", + "memmem", + "nix 0.29.0", + "num-derive", + "num-traits", + "ordered-float 4.6.0", + "pest", + "pest_derive", + "phf 0.11.3", + "sha2", + "signal-hook", + "siphasher", + "terminfo", + "termios", + "thiserror 1.0.69", + "ucd-trie", + "unicode-segmentation", + "vtparse", + "wezterm-bidi", + "wezterm-blob-leases", + "wezterm-color-types", + "wezterm-dynamic", + "wezterm-input-types", + "winapi", +] + +[[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.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[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.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "libc", + "num-conv", + "num_threads", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-skia" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab" +dependencies = [ + "arrayref", + "arrayvec", + "bytemuck", + "cfg-if", + "log", + "tiny-skia-path", +] + +[[package]] +name = "tiny-skia-path" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93" +dependencies = [ + "arrayref", + "bytemuck", + "strict-num", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "bytes", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tokio-serial" +version = "5.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa1d5427f11ba7c5e6384521cfd76f2d64572ff29f3f4f7aa0f496282923fdc8" +dependencies = [ + "cfg-if", + "futures", + "log", + "mio-serial", + "serialport", + "tokio", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime", + "toml_parser", + "winnow", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror 2.0.18", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "type-map" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb30dbbd9036155e74adad6812e9898d03ec374946234fbcebd5dfc7b9187b90" +dependencies = [ + "rustc-hash 2.1.2", +] + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unescaper" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4064ed685c487dbc25bd3f0e9548f2e34bab9d18cefc700f9ec2dba74ba1138e" +dependencies = [ + "thiserror 2.0.18", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-truncate" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b380a1238663e5f8a691f9039c73e1cdae598a30e9855f541d29b08b53e9a5" +dependencies = [ + "itertools", + "unicode-segmentation", + "unicode-width", +] + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "uuid" +version = "1.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7" +dependencies = [ + "atomic", + "getrandom 0.4.2", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vello_common" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd1a4c633ce09e7d713df1a6e036644a125e15e0c169cfb5180ddf5836ca04b" +dependencies = [ + "bytemuck", + "fearless_simd", + "hashbrown 0.16.1", + "log", + "peniko", + "skrifa", + "smallvec", +] + +[[package]] +name = "vello_cpu" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162bfe48aabf6a9fdcd401b628c7d9f260c2cbabb343c70a65feba6f7849edc" +dependencies = [ + "bytemuck", + "hashbrown 0.16.1", + "vello_common", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vtparse" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d9b2acfb050df409c972a37d3b8e08cdea3bddb0c09db9d53137e504cfabed0" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.12.1", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.12.1", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.12.1", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.12.1", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.12.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.12.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b6d8cf1eb2c1c31ed1f5643c88a6e53538129d4af80030c8cabd1f9fa884d91" +dependencies = [ + "bitflags 2.12.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.12.1", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fc95580916af1e68ff6a7be07446fc5db73ebf71cf092de939bbf5f7e189f72" +dependencies = [ + "core-foundation 0.10.1", + "jni", + "log", + "ndk-context", + "objc2 0.6.4", + "objc2-foundation 0.3.2", + "url", + "web-sys", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "wezterm-bidi" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0a6e355560527dd2d1cf7890652f4f09bb3433b6aadade4c9b5ed76de5f3ec" +dependencies = [ + "log", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-blob-leases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "692daff6d93d94e29e4114544ef6d5c942a7ed998b37abdc19b17136ea428eb7" +dependencies = [ + "getrandom 0.3.4", + "mac_address", + "sha2", + "thiserror 1.0.69", + "uuid", +] + +[[package]] +name = "wezterm-color-types" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de81ef35c9010270d63772bebef2f2d6d1f2d20a983d27505ac850b8c4b4296" +dependencies = [ + "csscolorparser", + "deltae", + "lazy_static", + "wezterm-dynamic", +] + +[[package]] +name = "wezterm-dynamic" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f2ab60e120fd6eaa68d9567f3226e876684639d22a4219b313ff69ec0ccd5ac" +dependencies = [ + "log", + "ordered-float 4.6.0", + "strsim", + "thiserror 1.0.69", + "wezterm-dynamic-derive", +] + +[[package]] +name = "wezterm-dynamic-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c0cf2d539c645b448eaffec9ec494b8b19bd5077d9e58cb1ae7efece8d575b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "wezterm-input-types" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7012add459f951456ec9d6c7e6fc340b1ce15d6fc9629f8c42853412c029e57e" +dependencies = [ + "bitflags 1.3.2", + "euclid", + "lazy_static", + "serde", + "wezterm-dynamic", +] + +[[package]] +name = "wgpu" +version = "29.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb3feacc458f7bee8bc1737149b42b6c731aa461039a4264a67bb6681646b250" +dependencies = [ + "arrayvec", + "bitflags 2.12.1", + "bytemuck", + "cfg-if", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "js-sys", + "log", + "naga", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "smallvec", + "static_assertions", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "wgpu-core", + "wgpu-hal", + "wgpu-types", +] + +[[package]] +name = "wgpu-core" +version = "29.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02da3ad1b568337f25513b317870960ef87073ea0945502e44b864b67a8c77b7" +dependencies = [ + "arrayvec", + "bit-set 0.9.1", + "bit-vec 0.9.1", + "bitflags 2.12.1", + "bytemuck", + "cfg_aliases", + "document-features", + "hashbrown 0.16.1", + "indexmap", + "log", + "naga", + "once_cell", + "parking_lot", + "portable-atomic", + "profiling", + "raw-window-handle", + "rustc-hash 1.1.0", + "smallvec", + "thiserror 2.0.18", + "wgpu-core-deps-apple", + "wgpu-core-deps-emscripten", + "wgpu-core-deps-wasm", + "wgpu-core-deps-windows-linux-android", + "wgpu-hal", + "wgpu-naga-bridge", + "wgpu-types", +] + +[[package]] +name = "wgpu-core-deps-apple" +version = "29.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62e51b5447e144b3dbba4feb01f80f4fa21696fa0cd99afb2c3df1affd6fdb28" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-emscripten" +version = "29.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3487cd6293a963bc5c0c0396f6a2192043c50003c07f4efdccbad3d90ec9d819" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-wasm" +version = "29.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c2f2fb042f36920771deb0b966543c5751b18f3d327760ffc90f74e20b2dcd4" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-core-deps-windows-linux-android" +version = "29.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb01076d0aa08b0ba9bd741e178b5cc440f5abe99d9581323a4c8b5d1a1916" +dependencies = [ + "wgpu-hal", +] + +[[package]] +name = "wgpu-hal" +version = "29.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8e1a9e7a8512f276f7c62e018c7fa8d60954303fed2e5750114332049193f" +dependencies = [ + "android_system_properties", + "arrayvec", + "ash", + "bit-set 0.9.1", + "bitflags 2.12.1", + "block2 0.6.2", + "bytemuck", + "cfg-if", + "cfg_aliases", + "glow", + "glutin_wgl_sys", + "gpu-allocator", + "gpu-descriptor", + "hashbrown 0.16.1", + "js-sys", + "khronos-egl", + "libc", + "libloading", + "log", + "naga", + "ndk-sys", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-foundation 0.3.2", + "objc2-metal 0.3.2", + "objc2-quartz-core 0.3.2", + "once_cell", + "ordered-float 5.3.0", + "parking_lot", + "portable-atomic", + "portable-atomic-util", + "profiling", + "range-alloc", + "raw-window-handle", + "raw-window-metal", + "renderdoc-sys", + "smallvec", + "thiserror 2.0.18", + "wasm-bindgen", + "wayland-sys", + "web-sys", + "wgpu-naga-bridge", + "wgpu-types", + "windows", + "windows-core", + "windows-result", +] + +[[package]] +name = "wgpu-naga-bridge" +version = "29.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c654c483f058800972c3645e95388a7eca31bf9fe1933bc20e036588a0be02" +dependencies = [ + "naga", + "wgpu-types", +] + +[[package]] +name = "wgpu-types" +version = "29.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9bcc31518a0e9735aefebedb5f7a9ef3ed1c42549c9f4c882fa9060ceaac639" +dependencies = [ + "bitflags 2.12.1", + "bytemuck", + "js-sys", + "log", + "raw-window-handle", + "web-sys", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580" +dependencies = [ + "windows-collections", + "windows-core", + "windows-future", + "windows-numerics", +] + +[[package]] +name = "windows-collections" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610" +dependencies = [ + "windows-core", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-future" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb" +dependencies = [ + "windows-core", + "windows-link", + "windows-threading", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-numerics" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26" +dependencies = [ + "windows-core", + "windows-link", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows-threading" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6755fa58a9f8350bd1e472d4c3fcc25f824ec358933bba33306d0b63df5978d" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.12.1", + "block2 0.5.1", + "bytemuck", + "calloop 0.13.0", + "cfg_aliases", + "concurrent-queue", + "core-foundation 0.9.4", + "core-graphics", + "cursor-icon", + "dpi", + "js-sys", + "libc", + "memmap2", + "ndk", + "objc2 0.5.2", + "objc2-app-kit 0.2.2", + "objc2-foundation 0.2.2", + "objc2-ui-kit 0.2.2", + "orbclient", + "percent-encoding", + "pin-project", + "raw-window-handle", + "redox_syscall 0.4.1", + "rustix 0.38.44", + "sctk-adwaita", + "smithay-client-toolkit 0.19.2", + "smol_str", + "tracing", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.52.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.12.1", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.12.1", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xserial-client" +version = "0.1.0" +dependencies = [ + "tokio", + "tracing", + "xserial-core", +] + +[[package]] +name = "xserial-core" +version = "0.1.0" +dependencies = [ + "async-trait", + "bytes", + "hex", + "nom 8.0.0", + "serde", + "serde_json", + "serialport", + "thiserror 2.0.18", + "tokio", + "tokio-serial", + "tracing", +] + +[[package]] +name = "xserial-gui" +version = "0.1.0" +dependencies = [ + "eframe", + "egui", + "egui_plot", + "tokio", + "tracing", + "tracing-subscriber", + "xserial-client", + "xserial-core", +] + +[[package]] +name = "xserial-tui" +version = "0.1.0" +dependencies = [ + "clap", + "crossterm 0.28.1", + "hex", + "image", + "ratatui", + "tokio", + "tracing", + "tracing-appender", + "tracing-subscriber", + "xserial-client", + "xserial-core", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zbus" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eee682d202a77e4a9f3b2c2bdf48a7b28af5c08c34ddf66f98c93e5e39464285" +dependencies = [ + "async-broadcast", + "async-executor", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-lite", + "hex", + "libc", + "ordered-stream", + "rustix 1.1.4", + "serde", + "serde_repr", + "tracing", + "uds_windows", + "uuid", + "windows-sys 0.61.2", + "winnow", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus-lockstep" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6998de05217a084b7578728a9443d04ea4cd80f2a0839b8d78770b76ccd45863" +dependencies = [ + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus-lockstep-macros" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10da05367f3a7b7553c8cdf8fa91aee6b64afebe32b51c95177957efc47ca3a0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus-lockstep", + "zbus_xml", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "5.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adf1bd45a81a103745b1757754762a26e8cd01e4532e4d6c8ec431624b80d1d6" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", + "zbus_names", + "zvariant", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7074f3e50b894eac91750142016d30d0a89be8e67dbfd9704fb875825760e52d" +dependencies = [ + "serde", + "winnow", + "zvariant", +] + +[[package]] +name = "zbus_xml" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8067892e940ed1727dea64690378601603b31d62dfde019a5335fbb7c0e0ed9" +dependencies = [ + "quick-xml", + "serde", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a192a0bde63360d77a7523c833d4b4ce6070a927e2c53246e4c540b1a3e27be0" +dependencies = [ + "endi", + "enumflags2", + "serde", + "winnow", + "zvariant_derive", + "zvariant_utils", +] + +[[package]] +name = "zvariant_derive" +version = "5.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc6cde9c01c511074be97f7ccb6c19d0da89e3f8662e812e999dcfd4638737" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e8535915cfa75547e559d8c68e8139909a4aeee076831e4ef7fc59d8172c4d6" +dependencies = [ + "proc-macro2", + "quote", + "serde", + "syn 2.0.117", + "winnow", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..89f09b1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,54 @@ +[workspace] +resolver = "2" +members = [ + "crates/xserial-core", + "crates/xserial-client", + "crates/xserial-tui", + "crates/xserial-gui", +] + +[workspace.dependencies] +# ── 异步运行时 ── +tokio = { version = "1", features = ["full"] } +async-trait = "0.1" + +# ── 串口 ── +serialport = "4.9" +tokio-serial = "5.4" + +# ── 协议解析 ── +bytes = "1" +nom = "8" + +# ── 序列化 ── +serde = { version = "1", features = ["derive"] } +serde_json = "1" + +# ── 错误处理 ── +thiserror = "2" + +# ── 日志 ── +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-appender = "0.2" + +# ── 工具 ── +hex = "0.4" + +# ── 图像 ── +image = { version = "0.25", default-features = false, features = ["png"] } + +# ── CLI ── +clap = { version = "4", features = ["derive"] } + +# ── TUI ── +ratatui = "0.30" +crossterm = "0.28" + +# ── GUI ── +egui = "0.34" +eframe = "0.34" +egui_plot = "0.35" + +# ── Lua (Step 6 启用) ── +# mlua = { version = "0.11", features = ["luajit", "async", "serde", "send", "macros", "vendored"] } diff --git a/crates/xserial-client/Cargo.toml b/crates/xserial-client/Cargo.toml new file mode 100644 index 0000000..c257c37 --- /dev/null +++ b/crates/xserial-client/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "xserial-client" +version = "0.1.0" +edition = "2024" + +[dependencies] +xserial-core = { path = "../xserial-core" } +tokio = { workspace = true } +tracing = { workspace = true } diff --git a/crates/xserial-client/src/lib.rs b/crates/xserial-client/src/lib.rs new file mode 100644 index 0000000..e69de29 diff --git a/crates/xserial-core/Cargo.toml b/crates/xserial-core/Cargo.toml new file mode 100644 index 0000000..9943771 --- /dev/null +++ b/crates/xserial-core/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "xserial-core" +version = "0.1.0" +edition = "2024" + +[dependencies] +tokio = { workspace = true } +async-trait = { workspace = true } +serialport = { workspace = true } +tokio-serial = { workspace = true } +bytes = { workspace = true } +nom = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +tracing = { workspace = true } +thiserror = { workspace = true } +hex = "0.4" diff --git a/crates/xserial-core/src/error.rs b/crates/xserial-core/src/error.rs new file mode 100644 index 0000000..9834113 --- /dev/null +++ b/crates/xserial-core/src/error.rs @@ -0,0 +1,27 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum Error { + #[error("I/O error: {0}")] + Io(#[from] std::io::Error), + + #[error("Serial port error: {0}")] + Serial(#[from] serialport::Error), + + #[error("Transport not connected")] + NotConnected, + + #[error("Connection failed: {0}")] + ConnectionFailed(String), + + #[error("Unsupported operation: {0}")] + Unsupported(String), + + #[error("Protocol error: {0}")] + Protocol(String), + + #[error("Script error: {0}")] + Script(String), +} + +pub type Result = std::result::Result; diff --git a/crates/xserial-core/src/frame/cobs.rs b/crates/xserial-core/src/frame/cobs.rs new file mode 100644 index 0000000..51aeb3d --- /dev/null +++ b/crates/xserial-core/src/frame/cobs.rs @@ -0,0 +1,267 @@ +use super::Framer; + +/// COBS (Consistent Overhead Byte Stuffing) framer. +/// +/// Frames are delimited by `0x00`. Everything between two `0x00` bytes +/// is a COBS-encoded packet. The framer accumulates until a `0x00` is +/// seen, decodes the COBS data, and yields the original payload. +/// +/// Reference: +#[derive(Debug, Clone)] +pub struct CobsFramer { + buf: Vec, + max_frame: usize, +} + +impl Default for CobsFramer { + fn default() -> Self { + Self { + buf: Vec::new(), + max_frame: 1024 * 1024, + } + } +} + +impl CobsFramer { + pub fn new(max_frame: usize) -> Self { + Self { + buf: Vec::new(), + max_frame, + } + } +} + +impl Framer for CobsFramer { + fn feed(&mut self, data: &[u8]) -> Vec> { + let mut frames = Vec::new(); + + for &byte in data { + if byte == 0x00 { + if !self.buf.is_empty() { + // Decode failed (corrupt packet) — discard and continue + if let Some(decoded) = cobs_decode(&self.buf) { + frames.push(decoded); + } + self.buf.clear(); + } + // Consecutive 0x00 bytes produce empty frames + // (skip them — no payload to decode) + } else { + if self.buf.len() < self.max_frame { + self.buf.push(byte); + } + // Exceeding max_frame: drop silently (avoid OOM) + } + } + + frames + } + + fn flush(&mut self) -> Option> { + if self.buf.is_empty() { + None + } else { + let data = std::mem::take(&mut self.buf); + cobs_decode(&data).or(Some(data)) + } + } + + fn reset(&mut self) { + self.buf.clear(); + } + + fn pending_len(&self) -> usize { + self.buf.len() + } +} + +/// Decode a COBS-encoded packet (without the trailing 0x00 delimiter). +/// +/// Returns `None` if the encoded data is invalid (e.g. an overhead byte +/// points past the end of the buffer). +fn cobs_decode(encoded: &[u8]) -> Option> { + if encoded.is_empty() { + return Some(Vec::new()); + } + + let mut out = Vec::with_capacity(encoded.len()); + let mut pos = 0; + + while pos < encoded.len() { + let code = encoded[pos] as usize; + if code == 0 { + // Invalid: overhead byte should never be 0 + return None; + } + pos += 1; + + if code > 1 { + let copy_start = pos; + let desired_end = pos + code - 1; + if desired_end > encoded.len() { + return None; + } + out.extend_from_slice(&encoded[copy_start..desired_end]); + pos = desired_end; + } + + // If the code byte was < 0xFF, the next byte (if any) in the + // original stream was 0x00 → insert it. + if code < 0xFF && pos < encoded.len() { + out.push(0x00); + } + } + + Some(out) +} + +#[cfg(test)] +mod tests { + use super::*; + + // ── cobs_decode unit tests ────────────────────────────────────── + + #[test] + fn cobs_decode_no_zeros() { + let decoded = cobs_decode(&[0x06, 0x68, 0x65, 0x6C, 0x6C, 0x6F]).unwrap(); + assert_eq!(decoded, b"hello"); + } + + #[test] + fn cobs_decode_single_zero() { + let decoded = cobs_decode(&[0x03, 0x11, 0x22, 0x02, 0x33]).unwrap(); + assert_eq!(decoded, vec![0x11, 0x22, 0x00, 0x33]); + } + + #[test] + fn cobs_decode_leading_zero() { + let decoded = cobs_decode(&[0x01, 0x01, 0x01, 0x01]).unwrap(); + assert_eq!(decoded, vec![0x00, 0x00, 0x00]); + } + + #[test] + fn cobs_decode_all_zeros() { + // Input: [0x00, 0x00, 0x00] → encoded: [0x01, 0x01, 0x01, 0x01] + let decoded = cobs_decode(&[0x01, 0x01, 0x01, 0x01]).unwrap(); + assert_eq!(decoded, vec![0x00, 0x00, 0x00]); + } + + #[test] + fn cobs_decode_empty() { + let decoded = cobs_decode(&[]).unwrap(); + assert!(decoded.is_empty()); + } + + #[test] + fn cobs_decode_invalid_zero_code() { + assert!(cobs_decode(&[0x00, 0x01]).is_none()); + } + + #[test] + fn cobs_decode_truncated() { + // Claims 5 bytes follow but only 3 remain + assert!(cobs_decode(&[0x06, 0x01, 0x02, 0x03]).is_none()); + } + + #[test] + fn cobs_decode_full_254_block() { + // 254 non-zero bytes + let payload: Vec = (1u8..=254).collect(); + let mut encoded = vec![0xFF]; + encoded.extend_from_slice(&payload); + let decoded = cobs_decode(&encoded).unwrap(); + assert_eq!(decoded, payload); + } + + // ── CobsFramer integration tests ───────────────────────────────── + + #[test] + fn cobs_framer_single_packet() { + let mut f = CobsFramer::default(); + let frames = f.feed(&[0x06, b'h', b'e', b'l', b'l', b'o', 0x00]); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"hello"); + } + + #[test] + fn cobs_framer_two_packets() { + let mut f = CobsFramer::default(); + let mut data = vec![0x04, b'f', b'o', b'o', 0x00]; + data.extend_from_slice(&[0x04, b'b', b'a', b'r', 0x00]); + let frames = f.feed(&data); + assert_eq!(frames.len(), 2); + assert_eq!(frames[0], b"foo"); + assert_eq!(frames[1], b"bar"); + } + + #[test] + fn cobs_framer_split_across_chunks() { + let mut f = CobsFramer::default(); + let frames = f.feed(&[0x06, b'h', b'e']); + assert!(frames.is_empty()); + + let frames = f.feed(&[b'l', b'l', b'o', 0x00]); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"hello"); + } + + #[test] + fn cobs_framer_consecutive_zeros() { + let mut f = CobsFramer::default(); + let frames = f.feed(&[0x00, 0x00, 0x03, b'a', b'b', 0x00]); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"ab"); + } + + #[test] + fn cobs_framer_corrupt_packet_discarded() { + let mut f = CobsFramer::default(); + let frames = f.feed(&[0x06, 0x01, 0x02, 0x00]); + assert!(frames.is_empty()); + } + + #[test] + fn cobs_framer_flush_partial() { + let mut f = CobsFramer::default(); + f.feed(&[0x03, b'h', b'e']); + let flushed = f.flush().unwrap(); + assert_eq!(flushed, b"he"); + } + + #[test] + fn cobs_framer_flush_empty() { + let mut f = CobsFramer::default(); + assert_eq!(f.flush(), None); + } + + #[test] + fn cobs_framer_reset() { + let mut f = CobsFramer::default(); + f.feed(&[0x05, b'h', b'e']); + assert!(f.pending_len() > 0); + f.reset(); + assert_eq!(f.pending_len(), 0); + } + + #[test] + fn cobs_framer_max_frame() { + let mut f = CobsFramer::new(3); + f.feed(&[0x05, b'a', b'b', b'c', b'd', b'e']); + assert_eq!(f.pending_len(), 3); + } + + #[test] + fn cobs_framer_empty_payload_packet() { + let mut f = CobsFramer::default(); + let frames = f.feed(&[0x01, 0x00]); + assert_eq!(frames.len(), 1); + assert!(frames[0].is_empty()); + } + + #[test] + fn cobs_framer_pending_len() { + let mut f = CobsFramer::default(); + f.feed(&[0x05, b'h', b'e']); + assert_eq!(f.pending_len(), 3); + } +} diff --git a/crates/xserial-core/src/frame/fixed.rs b/crates/xserial-core/src/frame/fixed.rs new file mode 100644 index 0000000..e44ea94 --- /dev/null +++ b/crates/xserial-core/src/frame/fixed.rs @@ -0,0 +1,151 @@ +use super::Framer; + +/// Fixed-length framer — every N bytes forms one frame. +#[derive(Debug, Clone)] +pub struct FixedLengthFramer { + buf: Vec, + frame_len: usize, +} + +impl FixedLengthFramer { + pub fn new(frame_len: usize) -> Self { + assert!(frame_len > 0, "frame_len must be > 0"); + Self { + buf: Vec::new(), + frame_len, + } + } + + pub fn frame_len(&self) -> usize { + self.frame_len + } +} + +impl Framer for FixedLengthFramer { + fn feed(&mut self, data: &[u8]) -> Vec> { + let mut frames = Vec::new(); + self.buf.extend_from_slice(data); + + while self.buf.len() >= self.frame_len { + let frame: Vec = self.buf.drain(..self.frame_len).collect(); + frames.push(frame); + } + + frames + } + + fn flush(&mut self) -> Option> { + if self.buf.is_empty() { + None + } else { + Some(std::mem::take(&mut self.buf)) + } + } + + fn reset(&mut self) { + self.buf.clear(); + } + + fn pending_len(&self) -> usize { + self.buf.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn fixed_exact_one_frame() { + let mut f = FixedLengthFramer::new(5); + let frames = f.feed(b"hello"); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"hello"); + } + + #[test] + fn fixed_multiple_frames() { + let mut f = FixedLengthFramer::new(3); + let frames = f.feed(b"abcdefghi"); + assert_eq!(frames.len(), 3); + assert_eq!(frames[0], b"abc"); + assert_eq!(frames[1], b"def"); + assert_eq!(frames[2], b"ghi"); + } + + #[test] + fn fixed_partial_buffered() { + let mut f = FixedLengthFramer::new(5); + let frames = f.feed(b"abc"); + assert!(frames.is_empty()); + assert_eq!(f.pending_len(), 3); + + let frames = f.feed(b"de"); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"abcde"); + } + + #[test] + fn fixed_more_than_one_frame_in_chunk() { + let mut f = FixedLengthFramer::new(4); + let frames = f.feed(b"abcdefghij"); // 10 bytes → 2 full + 2 pending + assert_eq!(frames.len(), 2); + assert_eq!(frames[0], b"abcd"); + assert_eq!(frames[1], b"efgh"); + assert_eq!(f.pending_len(), 2); + } + + #[test] + fn fixed_flush_partial() { + let mut f = FixedLengthFramer::new(5); + f.feed(b"xy"); + let flushed = f.flush(); + assert_eq!(flushed, Some(b"xy".to_vec())); + } + + #[test] + fn fixed_flush_empty() { + let mut f = FixedLengthFramer::new(3); + f.feed(b"abc"); + assert_eq!(f.flush(), None); + } + + #[test] + fn fixed_reset() { + let mut f = FixedLengthFramer::new(4); + f.feed(b"ab"); + assert_eq!(f.pending_len(), 2); + f.reset(); + assert_eq!(f.pending_len(), 0); + } + + #[test] + fn fixed_empty_feed() { + let mut f = FixedLengthFramer::new(5); + let frames = f.feed(b""); + assert!(frames.is_empty()); + } + + #[test] + fn fixed_exact_multiple_drains() { + let mut f = FixedLengthFramer::new(2); + let frames = f.feed(b"abcd"); + assert_eq!(frames.len(), 2); + assert!(f.pending_len() == 0); + let more = f.feed(b"ef"); + assert_eq!(more.len(), 1); + assert_eq!(more[0], b"ef"); + } + + #[test] + #[should_panic(expected = "frame_len must be > 0")] + fn fixed_zero_frame_len_panics() { + FixedLengthFramer::new(0); + } + + #[test] + fn fixed_frame_len_accessor() { + let f = FixedLengthFramer::new(128); + assert_eq!(f.frame_len(), 128); + } +} diff --git a/crates/xserial-core/src/frame/length.rs b/crates/xserial-core/src/frame/length.rs new file mode 100644 index 0000000..c017dc5 --- /dev/null +++ b/crates/xserial-core/src/frame/length.rs @@ -0,0 +1,370 @@ +use super::{Endian, Framer}; + +/// Configuration for the length-prefixed framer. +#[derive(Debug, Clone)] +pub struct LengthConfig { + /// Size of the length field in bytes: 1, 2, or 4. + pub len_bytes: usize, + pub endian: Endian, + /// When true, the length value includes the length field itself. + pub length_includes_self: bool, + /// Maximum payload size (0 = unlimited). + pub max_payload: usize, +} + +impl Default for LengthConfig { + fn default() -> Self { + Self { + len_bytes: 2, + endian: Endian::Big, + length_includes_self: false, + max_payload: 1024 * 1024, + } + } +} + +#[derive(Debug, Clone)] +enum State { + ReadingLength, + ReadingPayload { expected: usize }, +} + +#[derive(Debug, Clone)] +pub struct LengthPrefixedFramer { + buf: Vec, + config: LengthConfig, + state: State, +} + +impl LengthPrefixedFramer { + pub fn new(config: LengthConfig) -> Self { + assert!( + matches!(config.len_bytes, 1 | 2 | 4), + "len_bytes must be 1, 2, or 4" + ); + Self { + buf: Vec::new(), + config, + state: State::ReadingLength, + } + } + + pub fn config(&self) -> &LengthConfig { + &self.config + } + + fn parse_length(&self, bytes: &[u8]) -> usize { + let raw = match self.config.len_bytes { + 1 => bytes[0] as usize, + 2 => { + let b = [bytes[0], bytes[1]]; + match self.config.endian { + Endian::Big => u16::from_be_bytes(b) as usize, + Endian::Little => u16::from_le_bytes(b) as usize, + } + } + 4 => { + let b = [bytes[0], bytes[1], bytes[2], bytes[3]]; + match self.config.endian { + Endian::Big => u32::from_be_bytes(b) as usize, + Endian::Little => u32::from_le_bytes(b) as usize, + } + } + _ => unreachable!(), + }; + + if self.config.length_includes_self { + raw.saturating_sub(self.config.len_bytes) + } else { + raw + } + } +} + +impl Framer for LengthPrefixedFramer { + fn feed(&mut self, data: &[u8]) -> Vec> { + let mut frames = Vec::new(); + self.buf.extend_from_slice(data); + + loop { + match self.state { + State::ReadingLength => { + if self.buf.len() < self.config.len_bytes { + break; + } + let payload_len = self.parse_length(&self.buf[..self.config.len_bytes]); + + // Discard length, re-sync on corrupt frame + if self.config.max_payload > 0 && payload_len > self.config.max_payload { + self.buf.drain(..self.config.len_bytes); + continue; + } + + self.buf.drain(..self.config.len_bytes); + + if payload_len == 0 { + frames.push(Vec::new()); + } else { + self.state = State::ReadingPayload { + expected: payload_len, + }; + } + } + State::ReadingPayload { expected } => { + if self.buf.len() < expected { + break; + } + let frame: Vec = self.buf.drain(..expected).collect(); + frames.push(frame); + self.state = State::ReadingLength; + } + } + } + + frames + } + + fn flush(&mut self) -> Option> { + let remainder = std::mem::take(&mut self.buf); + self.state = State::ReadingLength; + if remainder.is_empty() { + None + } else { + Some(remainder) + } + } + + fn reset(&mut self) { + self.buf.clear(); + self.state = State::ReadingLength; + } + + fn pending_len(&self) -> usize { + self.buf.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn be2(len: u16) -> Vec { + len.to_be_bytes().to_vec() + } + + fn le2(len: u16) -> Vec { + len.to_le_bytes().to_vec() + } + + // ── 2-byte BE, length excludes self ─────────────────────────────── + + #[test] + fn length_single_frame() { + let mut f = LengthPrefixedFramer::new(LengthConfig::default()); + let mut data = be2(5); + data.extend_from_slice(b"hello"); + let frames = f.feed(&data); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"hello"); + } + + #[test] + fn length_two_frames_in_one_chunk() { + let mut f = LengthPrefixedFramer::new(LengthConfig::default()); + let mut data = Vec::new(); + data.extend(&be2(3)); + data.extend_from_slice(b"foo"); + data.extend(&be2(3)); + data.extend_from_slice(b"bar"); + let frames = f.feed(&data); + assert_eq!(frames.len(), 2); + assert_eq!(frames[0], b"foo"); + assert_eq!(frames[1], b"bar"); + } + + #[test] + fn length_split_header() { + let mut f = LengthPrefixedFramer::new(LengthConfig::default()); + // Feed first half of 2-byte length header + assert!(f.feed(&be2(5)[..1]).is_empty()); + assert_eq!(f.pending_len(), 1); + let mut rest = be2(5)[1..].to_vec(); + rest.extend_from_slice(b"hello"); + let frames = f.feed(&rest); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"hello"); + } + + #[test] + fn length_split_payload() { + let mut f = LengthPrefixedFramer::new(LengthConfig::default()); + let frames = f.feed(&be2(5)); + assert!(frames.is_empty()); + assert_eq!(f.pending_len(), 0); // length consumed, waiting for payload + + let frames = f.feed(b"hel"); + assert!(frames.is_empty()); + + let frames = f.feed(b"lo"); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"hello"); + } + + #[test] + fn length_zero_payload() { + let mut f = LengthPrefixedFramer::new(LengthConfig::default()); + let frames = f.feed(&be2(0)); + assert_eq!(frames.len(), 1); + assert!(frames[0].is_empty()); + } + + #[test] + fn length_chain_zero_then_data() { + let mut f = LengthPrefixedFramer::new(LengthConfig::default()); + let mut data = Vec::new(); + data.extend(&be2(0)); + data.extend(&be2(3)); + data.extend_from_slice(b"foo"); + let frames = f.feed(&data); + assert_eq!(frames.len(), 2); + assert!(frames[0].is_empty()); + assert_eq!(frames[1], b"foo"); + } + + // ── 2-byte LE ──────────────────────────────────────────────────── + + #[test] + fn length_little_endian() { + let cfg = LengthConfig { + endian: Endian::Little, + ..LengthConfig::default() + }; + let mut f = LengthPrefixedFramer::new(cfg); + let mut data = le2(5); + data.extend_from_slice(b"hello"); + let frames = f.feed(&data); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"hello"); + } + + // ── 1-byte length ──────────────────────────────────────────────── + + #[test] + fn length_one_byte() { + let cfg = LengthConfig { + len_bytes: 1, + ..LengthConfig::default() + }; + let mut f = LengthPrefixedFramer::new(cfg); + let data = [5, b'h', b'e', b'l', b'l', b'o']; + let frames = f.feed(&data); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"hello"); + } + + // ── 4-byte length ──────────────────────────────────────────────── + + #[test] + fn length_four_byte() { + let cfg = LengthConfig { + len_bytes: 4, + ..LengthConfig::default() + }; + let mut f = LengthPrefixedFramer::new(cfg); + let mut data = 5u32.to_be_bytes().to_vec(); + data.extend_from_slice(b"hello"); + let frames = f.feed(&data); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"hello"); + } + + // ── length_includes_self ───────────────────────────────────────── + + #[test] + fn length_includes_self_enabled() { + let cfg = LengthConfig { + length_includes_self: true, + ..LengthConfig::default() + }; + let mut f = LengthPrefixedFramer::new(cfg); + let mut data = be2(5); + data.extend_from_slice(b"foo"); + let frames = f.feed(&data); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"foo"); + } + + #[test] + fn length_includes_self_zero_payload() { + let cfg = LengthConfig { + length_includes_self: true, + ..LengthConfig::default() + }; + let mut f = LengthPrefixedFramer::new(cfg); + let frames = f.feed(&be2(2)); + assert_eq!(frames.len(), 1); + assert!(frames[0].is_empty()); + } + + // ── max_payload safety ─────────────────────────────────────────── + + #[test] + fn length_max_payload_exceeded_skips() { + let cfg = LengthConfig { + max_payload: 10, + ..LengthConfig::default() + }; + let mut f = LengthPrefixedFramer::new(cfg); + // Corrupt frame: claims 200 bytes, actual payload is 0xFF bytes + let mut data = be2(200); + data.extend_from_slice(&vec![0xFFu8; 200]); + // Followed by valid frame + data.extend(&be2(3)); + data.extend_from_slice(b"foo"); + let frames = f.feed(&data); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"foo"); + } + + // ── flush / reset ──────────────────────────────────────────────── + + #[test] + fn length_flush_partial() { + let mut f = LengthPrefixedFramer::new(LengthConfig::default()); + f.feed(&be2(10)); + f.feed(b"abc"); + let flushed = f.flush(); + assert_eq!(flushed, Some(b"abc".to_vec())); + } + + #[test] + fn length_flush_empty() { + let mut f = LengthPrefixedFramer::new(LengthConfig::default()); + assert_eq!(f.flush(), None); + } + + #[test] + fn length_reset_mid_frame() { + let mut f = LengthPrefixedFramer::new(LengthConfig::default()); + f.feed(&be2(50)); + f.feed(b"partial"); + assert!(f.pending_len() > 0); + f.reset(); + assert_eq!(f.pending_len(), 0); + // Should work normally after reset + let mut data = be2(3); + data.extend_from_slice(b"foo"); + let frames = f.feed(&data); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"foo"); + } + + #[test] + fn length_pending_len_reflects_buffer() { + let mut f = LengthPrefixedFramer::new(LengthConfig::default()); + f.feed(&be2(5)); + assert_eq!(f.pending_len(), 0); + f.feed(b"he"); + assert_eq!(f.pending_len(), 2); + } +} diff --git a/crates/xserial-core/src/frame/line.rs b/crates/xserial-core/src/frame/line.rs new file mode 100644 index 0000000..37d114d --- /dev/null +++ b/crates/xserial-core/src/frame/line.rs @@ -0,0 +1,218 @@ +use super::Framer; + +/// Line-delimited framer. +/// +/// Splits incoming bytes on `\n` (LF). Optional `\r` (CR) stripping is +/// controlled via [`LineConfig::strip_cr`]. +#[derive(Debug, Clone)] +pub struct LineConfig { + pub strip_cr: bool, + pub max_line_len: usize, +} + +impl Default for LineConfig { + fn default() -> Self { + Self { + strip_cr: true, + max_line_len: 1024 * 1024, + } + } +} + +#[derive(Debug, Clone)] +pub struct LineFramer { + buf: Vec, + config: LineConfig, +} + +impl LineFramer { + pub fn new(config: LineConfig) -> Self { + Self { + buf: Vec::new(), + config, + } + } + + pub fn config(&self) -> &LineConfig { + &self.config + } +} + +impl Framer for LineFramer { + fn feed(&mut self, data: &[u8]) -> Vec> { + let mut frames = Vec::new(); + self.buf.extend_from_slice(data); + + while let Some(pos) = self.buf.iter().position(|&b| b == b'\n') { + let mut line = self.buf.drain(..=pos).collect::>(); + line.pop(); + if self.config.strip_cr && line.last() == Some(&b'\r') { + line.pop(); + } + if line.len() <= self.config.max_line_len { + frames.push(line); + } + } + + frames + } + + fn flush(&mut self) -> Option> { + if self.buf.is_empty() { + None + } else { + Some(std::mem::take(&mut self.buf)) + } + } + + fn reset(&mut self) { + self.buf.clear(); + } + + fn pending_len(&self) -> usize { + self.buf.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn line_single_complete() { + let mut f = LineFramer::new(LineConfig::default()); + let frames = f.feed(b"hello\n"); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"hello"); + } + + #[test] + fn line_multiple_in_one_chunk() { + let mut f = LineFramer::new(LineConfig::default()); + let frames = f.feed(b"aaa\nbbb\nccc\n"); + assert_eq!(frames.len(), 3); + assert_eq!(frames[0], b"aaa"); + assert_eq!(frames[1], b"bbb"); + assert_eq!(frames[2], b"ccc"); + } + + #[test] + fn line_split_across_chunks() { + let mut f = LineFramer::new(LineConfig::default()); + let frames = f.feed(b"hel"); + assert!(frames.is_empty()); + let frames = f.feed(b"lo\n"); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"hello"); + } + + #[test] + fn line_crlf_stripping() { + let mut f = LineFramer::new(LineConfig::default()); + let frames = f.feed(b"hello\r\nworld\r\n"); + assert_eq!(frames.len(), 2); + assert_eq!(frames[0], b"hello"); + assert_eq!(frames[1], b"world"); + } + + #[test] + fn line_crlf_no_strip() { + let cfg = LineConfig { + strip_cr: false, + ..LineConfig::default() + }; + let mut f = LineFramer::new(cfg); + let frames = f.feed(b"hello\r\n"); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"hello\r"); + } + + #[test] + fn line_empty_lines() { + let mut f = LineFramer::new(LineConfig::default()); + let frames = f.feed(b"\n\n\n"); + assert_eq!(frames.len(), 3); + assert!(frames[0].is_empty()); + assert!(frames[1].is_empty()); + assert!(frames[2].is_empty()); + } + + #[test] + fn line_no_newline_buffered() { + let mut f = LineFramer::new(LineConfig::default()); + let frames = f.feed(b"incomplete"); + assert!(frames.is_empty()); + assert_eq!(f.pending_len(), 10); + } + + #[test] + fn line_flush_partial() { + let mut f = LineFramer::new(LineConfig::default()); + f.feed(b"partial data"); + let flushed = f.flush(); + assert_eq!(flushed, Some(b"partial data".to_vec())); + assert_eq!(f.pending_len(), 0); + } + + #[test] + fn line_flush_empty() { + let mut f = LineFramer::new(LineConfig::default()); + assert_eq!(f.flush(), None); + } + + #[test] + fn line_reset() { + let mut f = LineFramer::new(LineConfig::default()); + f.feed(b"some data"); + assert!(f.pending_len() > 0); + f.reset(); + assert_eq!(f.pending_len(), 0); + } + + #[test] + fn line_max_len_filtered() { + let cfg = LineConfig { + max_line_len: 5, + ..LineConfig::default() + }; + let mut f = LineFramer::new(cfg); + let frames = f.feed(b"short\nvery long line\nok\n"); + assert_eq!(frames.len(), 2); + assert_eq!(frames[0], b"short"); + assert_eq!(frames[1], b"ok"); + } + + #[test] + fn line_feed_then_flush_chain() { + let mut f = LineFramer::new(LineConfig::default()); + let frames = f.feed(b"complete\npartial"); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"complete"); + + let flushed = f.flush(); + assert_eq!(flushed, Some(b"partial".to_vec())); + + let frames = f.feed(b"new\n"); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"new"); + } + + #[test] + fn line_binary_data_with_embedded_newlines() { + let mut f = LineFramer::new(LineConfig::default()); + let data = [0x00, 0x01, b'\n', 0xFF, 0xFE, b'\n', 0x7F]; + let frames = f.feed(&data); + assert_eq!(frames.len(), 2); + assert_eq!(frames[0], &[0x00, 0x01]); + assert_eq!(frames[1], &[0xFF, 0xFE]); + } + + #[test] + fn line_lf_only_with_strip_cr() { + let mut f = LineFramer::new(LineConfig::default()); + let frames = f.feed(b"line1\nline2\n"); + assert_eq!(frames.len(), 2); + assert_eq!(frames[0], b"line1"); + assert_eq!(frames[1], b"line2"); + } +} diff --git a/crates/xserial-core/src/frame/mod.rs b/crates/xserial-core/src/frame/mod.rs new file mode 100644 index 0000000..43c9ac8 --- /dev/null +++ b/crates/xserial-core/src/frame/mod.rs @@ -0,0 +1,35 @@ +pub mod line; +pub mod fixed; +pub mod length; +pub mod cobs; + +pub use crate::protocol::Endian; + +/// Stateful byte-stream framer. +/// +/// A `Framer` accumulates raw bytes from a transport layer and yields +/// complete frames once a boundary is detected. Each call to [`feed`] +/// may return zero, one, or many frames depending on how much data has +/// arrived. +/// +/// [`feed`]: Framer::feed +pub trait Framer { + /// Feed a chunk of newly arrived bytes. + /// + /// Any complete frames that can be extracted are returned. The + /// framer keeps incomplete trailing data in its internal buffer so + /// it can be completed on the next call. + fn feed(&mut self, data: &[u8]) -> Vec>; + + /// Drain any remaining buffered data as a final frame. + /// + /// This is typically called when the transport disconnects or the + /// user explicitly wants to flush a partial frame. + fn flush(&mut self) -> Option>; + + /// Discard all buffered state and start fresh. + fn reset(&mut self); + + /// Number of bytes currently buffered in the incomplete frame. + fn pending_len(&self) -> usize; +} diff --git a/crates/xserial-core/src/lib.rs b/crates/xserial-core/src/lib.rs new file mode 100644 index 0000000..24a68e3 --- /dev/null +++ b/crates/xserial-core/src/lib.rs @@ -0,0 +1,7 @@ +pub mod error; +pub mod transport; +pub mod protocol; +pub mod frame; +pub mod pipeline; + +pub use error::{Error, Result}; diff --git a/crates/xserial-core/src/pipeline.rs b/crates/xserial-core/src/pipeline.rs new file mode 100644 index 0000000..370974c --- /dev/null +++ b/crates/xserial-core/src/pipeline.rs @@ -0,0 +1,430 @@ +use crate::frame::Framer; +use crate::protocol::{DecodedData, ProtocolDecoder}; + +/// A self-contained framing + decoding pipeline. +/// +/// Each pipeline has its own `Framer` (with independent internal buffer) +/// and its own `ProtocolDecoder`. Feed the same byte chunk to multiple +/// pipelines and each will extract and decode only the frames it understands. +pub struct Pipeline { + name: String, + framer: Box, + decoder: Box, +} + +impl Pipeline { + pub fn new( + name: impl Into, + framer: Box, + decoder: Box, + ) -> Self { + Self { + name: name.into(), + framer, + decoder, + } + } + + pub fn name(&self) -> &str { + &self.name + } + + fn feed_inner(&mut self, data: &[u8]) -> Vec { + self.framer + .feed(data) + .into_iter() + .filter_map(|frame| self.decoder.decode(&frame)) + .collect() + } + + fn flush_inner(&mut self) -> Vec { + self.framer + .flush() + .into_iter() + .filter_map(|frame| self.decoder.decode(&frame)) + .collect() + } + + fn reset_inner(&mut self) { + self.framer.reset(); + } +} + +/// One decoded result from a pipeline, tagged with the pipeline name. +#[derive(Debug, Clone)] +pub struct PipelineResult { + pub pipeline_name: String, + pub data: DecodedData, +} + +/// Manages multiple [`Pipeline`]s, feeding the same byte stream to all of them. +/// +/// ``` +/// use xserial_core::pipeline::{MultiPipeline, Pipeline}; +/// use xserial_core::frame::line::{LineFramer, LineConfig}; +/// use xserial_core::frame::fixed::FixedLengthFramer; +/// use xserial_core::protocol::text::{TextDecoder, TextEncoding}; +/// use xserial_core::protocol::hex::{HexDecoder, HexConfig}; +/// +/// let mut mp = MultiPipeline::new(); +/// mp.add( +/// Pipeline::new("text", +/// Box::new(LineFramer::new(LineConfig::default())), +/// Box::new(TextDecoder::new(TextEncoding::Utf8)), +/// ) +/// ); +/// mp.add( +/// Pipeline::new("hex", +/// Box::new(FixedLengthFramer::new(8)), +/// Box::new(HexDecoder::new(HexConfig::default())), +/// ) +/// ); +/// +/// let results = mp.feed(b"hello\n"); +/// // "text" pipeline produced one line, "hex" pipeline is still buffering +/// ``` +pub struct MultiPipeline { + pipelines: Vec, +} + +impl MultiPipeline { + pub fn new() -> Self { + Self { + pipelines: Vec::new(), + } + } + + pub fn add(&mut self, pipeline: Pipeline) { + self.pipelines.push(pipeline); + } + + pub fn pipeline_count(&self) -> usize { + self.pipelines.len() + } + + /// Feed a chunk of bytes to every pipeline. + /// + /// Returns all decoded results from all pipelines, in the order the + /// pipelines were added. Pipelines that produced no complete frames + /// (or whose decoder returned `None`) are simply absent from the output. + pub fn feed(&mut self, data: &[u8]) -> Vec { + let mut results = Vec::new(); + for p in &mut self.pipelines { + let name = p.name.clone(); + for decoded in p.feed_inner(data) { + results.push(PipelineResult { + pipeline_name: name.clone(), + data: decoded, + }); + } + } + results + } + + /// Flush every pipeline. + /// + /// Call after the transport disconnects to drain any incomplete + /// frames still buffered inside the framers. + pub fn flush(&mut self) -> Vec { + let mut results = Vec::new(); + for p in &mut self.pipelines { + let name = p.name.clone(); + for decoded in p.flush_inner() { + results.push(PipelineResult { + pipeline_name: name.clone(), + data: decoded, + }); + } + } + results + } + + /// Reset every pipeline to a clean state (discards all buffered data). + pub fn reset(&mut self) { + for p in &mut self.pipelines { + p.reset_inner(); + } + } + + /// Return `(name, pending_bytes)` for each pipeline that has buffered data. + pub fn pending_bytes(&self) -> Vec<(&str, usize)> { + self.pipelines + .iter() + .map(|p| (p.name.as_str(), p.framer.pending_len())) + .filter(|(_, len)| *len > 0) + .collect() + } +} + +impl Default for MultiPipeline { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::frame::fixed::FixedLengthFramer; + use crate::frame::length::{LengthConfig, LengthPrefixedFramer}; + use crate::frame::line::{LineConfig, LineFramer}; + use crate::protocol::hex::{HexConfig, HexDecoder}; + use crate::protocol::plot::{PlotConfig, PlotDecoder, PlotFormat, SampleType}; + use crate::protocol::text::{TextDecoder, TextEncoding}; + use crate::protocol::Endian; + + #[test] + fn multi_text_and_hex_same_stream() { + let mut mp = MultiPipeline::new(); + mp.add(Pipeline::new( + "text", + Box::new(LineFramer::new(LineConfig::default())), + Box::new(TextDecoder::new(TextEncoding::Utf8)), + )); + mp.add(Pipeline::new( + "hex", + Box::new(LineFramer::new(LineConfig::default())), + Box::new(HexDecoder::new(HexConfig::default())), + )); + + // Both pipelines use LineFramer → both see "hello\n" and "42\n" + let results = mp.feed(b"hello\n42\n"); + + assert_eq!(results.len(), 4); + assert_eq!(results[0].pipeline_name, "text"); + assert!(matches!(&results[0].data, DecodedData::Text(s) if s == "hello")); + assert_eq!(results[1].pipeline_name, "text"); + assert!(matches!(&results[1].data, DecodedData::Text(s) if s == "42")); + assert_eq!(results[2].pipeline_name, "hex"); + assert!(matches!(&results[2].data, DecodedData::Hex(s) if s == "68 65 6c 6c 6f")); + assert_eq!(results[3].pipeline_name, "hex"); + assert!(matches!(&results[3].data, DecodedData::Hex(s) if s == "34 32")); + } + + #[test] + fn multi_text_success_hex_silent_on_binary_trash() { + let mut mp = MultiPipeline::new(); + mp.add(Pipeline::new( + "text", + Box::new(LineFramer::new(LineConfig::default())), + Box::new(TextDecoder::new(TextEncoding::Utf8)), + )); + mp.add(Pipeline::new( + "hex", + Box::new(LineFramer::new(LineConfig::default())), + Box::new(HexDecoder::new(HexConfig::default())), + )); + + let results = mp.feed(b"valid\n\xff\xfe\xfd\n"); + + // Text pipeline: "valid" ok, binary line fails UTF-8 → only 1 result + // Hex pipeline: both lines decode successfully → 2 results + assert_eq!(results.len(), 3); + assert_eq!(results[0].pipeline_name, "text"); + assert!(matches!(&results[0].data, DecodedData::Text(s) if s == "valid")); + assert_eq!(results[1].pipeline_name, "hex"); + assert!(matches!(&results[1].data, DecodedData::Hex(_))); + assert_eq!(results[2].pipeline_name, "hex"); + assert!(matches!(&results[2].data, DecodedData::Hex(_))); + } + + #[test] + fn multi_different_framers_per_pipeline() { + let mut mp = MultiPipeline::new(); + // Text uses LineFramer, hex uses FixedLengthFramer(4) + mp.add(Pipeline::new( + "text", + Box::new(LineFramer::new(LineConfig::default())), + Box::new(TextDecoder::new(TextEncoding::Utf8)), + )); + mp.add(Pipeline::new( + "hex", + Box::new(FixedLengthFramer::new(4)), + Box::new(HexDecoder::new(HexConfig::default())), + )); + + let results = mp.feed(b"abc\n1234"); + + // text: sees "abc\n" → produces "abc" + // hex: sees 8 bytes → produces 2 fixed frames: "abc\n" and "1234" + assert_eq!(results.len(), 3); + assert_eq!(results[0].pipeline_name, "text"); + assert!(matches!(&results[0].data, DecodedData::Text(s) if s == "abc")); + + let hex_results: Vec<_> = results + .iter() + .filter(|r| r.pipeline_name == "hex") + .collect(); + assert_eq!(hex_results.len(), 2); + } + + #[test] + fn multi_flush_drains_all() { + let mut mp = MultiPipeline::new(); + mp.add(Pipeline::new( + "text", + Box::new(LineFramer::new(LineConfig::default())), + Box::new(TextDecoder::new(TextEncoding::Utf8)), + )); + mp.add(Pipeline::new( + "hex", + Box::new(FixedLengthFramer::new(4)), + Box::new(HexDecoder::new(HexConfig::default())), + )); + + // "partial" = 7 bytes. Line: no \n → buffers. Fixed(4): 1 frame "part", 3 left. + mp.feed(b"partial"); + + let flushed = mp.flush(); + // Line flush → "partial" as text + // Fixed flush → "ial" as hex ("69 61 6c") + assert_eq!(flushed.len(), 2); + assert_eq!(flushed[0].pipeline_name, "text"); + assert!(matches!(&flushed[0].data, DecodedData::Text(s) if s == "partial")); + assert_eq!(flushed[1].pipeline_name, "hex"); + assert!(matches!(&flushed[1].data, DecodedData::Hex(s) if s == "69 61 6c")); + } + + #[test] + fn multi_reset_clears_all_state() { + let mut mp = MultiPipeline::new(); + mp.add(Pipeline::new( + "text", + Box::new(LineFramer::new(LineConfig::default())), + Box::new(TextDecoder::new(TextEncoding::Utf8)), + )); + mp.add(Pipeline::new( + "hex", + Box::new(FixedLengthFramer::new(4)), + Box::new(HexDecoder::new(HexConfig::default())), + )); + + mp.feed(b"unused data here that never completes"); + assert_eq!(mp.pending_bytes().len(), 2); + + mp.reset(); + assert!(mp.pending_bytes().is_empty()); + + // After reset, pipelines work normally + let results = mp.feed(b"ok\nabcd"); + assert!(!results.is_empty()); + } + + #[test] + fn multi_pending_bytes_reporting() { + let mut mp = MultiPipeline::new(); + mp.add(Pipeline::new( + "t", + Box::new(LineFramer::new(LineConfig::default())), + Box::new(TextDecoder::new(TextEncoding::Utf8)), + )); + mp.add(Pipeline::new( + "h", + Box::new(FixedLengthFramer::new(100)), + Box::new(HexDecoder::new(HexConfig::default())), + )); + + mp.feed(b"hello world"); + let pending: Vec<_> = mp.pending_bytes(); + assert_eq!(pending.len(), 2); + // Both have the same 11 bytes buffered + assert_eq!(pending[0], ("t", 11)); + assert_eq!(pending[1], ("h", 11)); + } + + #[test] + fn multi_empty_feed_no_results() { + let mut mp = MultiPipeline::new(); + mp.add(Pipeline::new( + "text", + Box::new(LineFramer::new(LineConfig::default())), + Box::new(TextDecoder::new(TextEncoding::Utf8)), + )); + + let results = mp.feed(b""); + assert!(results.is_empty()); + } + + #[test] + fn multi_no_pipelines() { + let mut mp = MultiPipeline::new(); + let results = mp.feed(b"data"); + assert!(results.is_empty()); + assert_eq!(mp.pipeline_count(), 0); + } + + #[test] + fn multi_pipeline_count() { + let mut mp = MultiPipeline::new(); + assert_eq!(mp.pipeline_count(), 0); + mp.add(Pipeline::new( + "a", + Box::new(LineFramer::new(LineConfig::default())), + Box::new(TextDecoder::new(TextEncoding::Utf8)), + )); + assert_eq!(mp.pipeline_count(), 1); + } + + #[test] + fn multi_text_and_plot_mixed_stream() { + let mut mp = MultiPipeline::new(); + mp.add(Pipeline::new( + "text", + Box::new(LineFramer::new(LineConfig::default())), + Box::new(TextDecoder::new(TextEncoding::Utf8)), + )); + mp.add(Pipeline::new( + "plot", + Box::new(FixedLengthFramer::new(8)), // 2 × f32 LE per frame + Box::new(PlotDecoder::new(PlotConfig { + sample_type: SampleType::F32, + endian: Endian::Little, + channels: 1, + format: PlotFormat::Interleaved, + })), + )); + + // 10 bytes of text + 8 bytes of f32 samples = 18 total. + // FixedLengthFramer(8) extracts 2 frames. The first frame is ASCII + // bytes which happen to decode as valid (but meaningless) f32 values + // — PlotDecoder can't reject them. This is by design: garbage-in, + // garbage-out; the UI layer decides what to display. + let mut data = b"status ok\n".to_vec(); + data.extend_from_slice(&1.0f32.to_le_bytes()); + data.extend_from_slice(&2.0f32.to_le_bytes()); + let results = mp.feed(&data); + + // text: 1 line, plot: 2 fixed frames (first = garbage f32, second = real) + assert_eq!(results.len(), 3); + assert_eq!(results[0].pipeline_name, "text"); + assert!(matches!(&results[0].data, DecodedData::Text(s) if s == "status ok")); + } + + #[test] + fn multi_length_prefixed_and_line_stacked() { + let mut mp = MultiPipeline::new(); + mp.add(Pipeline::new( + "line", + Box::new(LineFramer::new(LineConfig::default())), + Box::new(TextDecoder::new(TextEncoding::Utf8)), + )); + // Second pipeline uses a different framer on the same bytes. + // The length framer sees "he" (from "hello\n") as a bogus length header + // and silently produces no frames — that's expected. + mp.add(Pipeline::new( + "len", + Box::new(LengthPrefixedFramer::new(LengthConfig::default())), + Box::new(HexDecoder::new(HexConfig::default())), + )); + + let mut data = b"hello\n".to_vec(); + data.extend_from_slice(&3u16.to_be_bytes()); + data.extend_from_slice(b"abc"); + + let results = mp.feed(&data); + // Only the line pipeline produces output; length pipeline silently + // ignores bytes that don't form valid length-prefixed frames. + assert_eq!(results.len(), 1); + assert!(matches!(&results[0].data, DecodedData::Text(s) if s == "hello")); + } +} diff --git a/crates/xserial-core/src/protocol/hex.rs b/crates/xserial-core/src/protocol/hex.rs new file mode 100644 index 0000000..ace7e7d --- /dev/null +++ b/crates/xserial-core/src/protocol/hex.rs @@ -0,0 +1,229 @@ +use super::{DecodedData, ProtocolDecoder}; +use crate::protocol::Endian; + +#[derive(Debug, Clone)] +pub struct HexConfig { + pub uppercase: bool, + pub separator: String, + pub bytes_per_group: usize, + pub endian: Endian, +} + +impl Default for HexConfig { + fn default() -> Self { + Self { + uppercase: false, + separator: String::from(" "), + bytes_per_group: 1, + endian: Endian::Big, + } + } +} + +#[derive(Debug, Clone)] +pub struct HexDecoder { + config: HexConfig, +} + +impl HexDecoder { + pub fn new(config: HexConfig) -> Self { + Self { config } + } + + pub fn config(&self) -> &HexConfig { + &self.config + } +} + +impl ProtocolDecoder for HexDecoder { + fn name(&self) -> &str { + "Hex" + } + + fn decode(&self, frame: &[u8]) -> Option { + if frame.is_empty() { + return Some(DecodedData::Hex(String::new())); + } + + let group_size = self.config.bytes_per_group.max(1); + let mut result = String::new(); + let chunks: Vec<&[u8]> = frame.chunks(group_size).collect(); + let total = chunks.len(); + + for (i, chunk) in chunks.iter().enumerate() { + if i > 0 { + result.push_str(&self.config.separator); + } + + let is_last = i == total - 1; + let mut display: Vec = chunk.to_vec(); + + if !is_last || chunk.len() == group_size { + // Complete group — apply endian reversal + if self.config.endian == Endian::Little { + display.reverse(); + } + } + // Incomplete trailing group — display bytes in original order + + for b in &display { + let hex_byte = if self.config.uppercase { + format!("{:02X}", b) + } else { + format!("{:02x}", b) + }; + result.push_str(&hex_byte); + } + } + + Some(DecodedData::Hex(result)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn hex_default_single_byte_lower() { + let d = HexDecoder::new(HexConfig::default()); + let result = d.decode(&[0x0a, 0x1b, 0xff]); + assert!(matches!(result, Some(DecodedData::Hex(ref s)) if s == "0a 1b ff")); + } + + #[test] + fn hex_uppercase() { + let cfg = HexConfig { + uppercase: true, + ..HexConfig::default() + }; + let d = HexDecoder::new(cfg); + let result = d.decode(&[0x0a, 0x1b, 0xff]); + assert!(matches!(result, Some(DecodedData::Hex(ref s)) if s == "0A 1B FF")); + } + + #[test] + fn hex_no_separator() { + let cfg = HexConfig { + separator: String::new(), + ..HexConfig::default() + }; + let d = HexDecoder::new(cfg); + let result = d.decode(&[0xde, 0xad, 0xbe, 0xef]); + assert!(matches!(result, Some(DecodedData::Hex(ref s)) if s == "deadbeef")); + } + + #[test] + fn hex_custom_separator() { + let cfg = HexConfig { + separator: String::from(":"), + ..HexConfig::default() + }; + let d = HexDecoder::new(cfg); + let result = d.decode(&[0x01, 0x02, 0x03]); + assert!(matches!(result, Some(DecodedData::Hex(ref s)) if s == "01:02:03")); + } + + #[test] + fn hex_group_u16_big_endian() { + let cfg = HexConfig { + bytes_per_group: 2, + endian: Endian::Big, + ..HexConfig::default() + }; + let d = HexDecoder::new(cfg); + let result = d.decode(&[0x12, 0x34, 0x56, 0x78]); + assert!(matches!(result, Some(DecodedData::Hex(ref s)) if s == "1234 5678")); + } + + #[test] + fn hex_group_u16_little_endian() { + let cfg = HexConfig { + bytes_per_group: 2, + endian: Endian::Little, + ..HexConfig::default() + }; + let d = HexDecoder::new(cfg); + let result = d.decode(&[0x12, 0x34, 0x56, 0x78]); + assert!(matches!(result, Some(DecodedData::Hex(ref s)) if s == "3412 7856")); + } + + #[test] + fn hex_group_u32_little_endian() { + let cfg = HexConfig { + bytes_per_group: 4, + endian: Endian::Little, + ..HexConfig::default() + }; + let d = HexDecoder::new(cfg); + let result = d.decode(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08]); + assert!(matches!(result, Some(DecodedData::Hex(ref s)) if s == "04030201 08070605")); + } + + #[test] + fn hex_incomplete_trailing_group_no_reversal() { + let cfg = HexConfig { + bytes_per_group: 2, + endian: Endian::Little, + ..HexConfig::default() + }; + let d = HexDecoder::new(cfg); + // 5 bytes: two complete groups + 1 trailing byte + let result = d.decode(&[0x12, 0x34, 0x56, 0x78, 0x9a]); + assert!(matches!(result, Some(DecodedData::Hex(ref s)) if s == "3412 7856 9a")); + } + + #[test] + fn hex_empty_input() { + let d = HexDecoder::new(HexConfig::default()); + let result = d.decode(&[]); + assert!(matches!(result, Some(DecodedData::Hex(ref s)) if s.is_empty())); + } + + #[test] + fn hex_single_byte() { + let d = HexDecoder::new(HexConfig::default()); + let result = d.decode(&[0x42]); + assert!(matches!(result, Some(DecodedData::Hex(ref s)) if s == "42")); + } + + #[test] + fn hex_zero_bytes() { + let d = HexDecoder::new(HexConfig::default()); + let result = d.decode(&[0x00, 0x00, 0x00]); + assert!(matches!(result, Some(DecodedData::Hex(ref s)) if s == "00 00 00")); + } + + #[test] + fn hex_group_size_one_never_reverses() { + let cfg = HexConfig { + bytes_per_group: 1, + endian: Endian::Little, + ..HexConfig::default() + }; + let d = HexDecoder::new(cfg); + let result = d.decode(&[0x01, 0x02, 0x03]); + assert!(matches!(result, Some(DecodedData::Hex(ref s)) if s == "01 02 03")); + } + + #[test] + fn hex_name() { + let d = HexDecoder::new(HexConfig::default()); + assert_eq!(d.name(), "Hex"); + } + + #[test] + fn hex_config_accessor() { + let cfg = HexConfig { + bytes_per_group: 4, + endian: Endian::Little, + uppercase: true, + separator: String::from("-"), + }; + let d = HexDecoder::new(cfg.clone()); + assert_eq!(d.config().bytes_per_group, 4); + assert_eq!(d.config().endian, Endian::Little); + assert!(d.config().uppercase); + assert_eq!(d.config().separator, "-"); + } +} diff --git a/crates/xserial-core/src/protocol/mod.rs b/crates/xserial-core/src/protocol/mod.rs new file mode 100644 index 0000000..0c39dfc --- /dev/null +++ b/crates/xserial-core/src/protocol/mod.rs @@ -0,0 +1,41 @@ +pub mod text; +pub mod hex; +pub mod plot; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Endian { + Little, + Big, +} + +/// Decoded result from a protocol decoder. +#[derive(Debug, Clone)] +pub enum DecodedData { + Text(String), + Hex(String), + Plot(plot::PlotFrame), + Binary(Vec), +} + +impl DecodedData { + pub fn summary(&self) -> String { + match self { + DecodedData::Text(s) => s.clone(), + DecodedData::Hex(s) => s.clone(), + DecodedData::Plot(frame) => { + format!( + "Plot: {} channels × {} samples ({})", + frame.channels.len(), + frame.sample_count(), + frame.sample_type.name(), + ) + } + DecodedData::Binary(v) => format!("Binary: {} bytes", v.len()), + } + } +} + +pub trait ProtocolDecoder: Send + Sync { + fn name(&self) -> &str; + fn decode(&self, frame: &[u8]) -> Option; +} diff --git a/crates/xserial-core/src/protocol/plot.rs b/crates/xserial-core/src/protocol/plot.rs new file mode 100644 index 0000000..d9af2a7 --- /dev/null +++ b/crates/xserial-core/src/protocol/plot.rs @@ -0,0 +1,508 @@ +use super::{DecodedData, ProtocolDecoder}; +use crate::protocol::Endian; + +/// Numeric type of samples in a plot frame. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SampleType { + I8, + U8, + I16, + U16, + I32, + U32, + I64, + U64, + F32, + F64, +} + +impl SampleType { + pub fn name(&self) -> &str { + match self { + SampleType::I8 => "i8", + SampleType::U8 => "u8", + SampleType::I16 => "i16", + SampleType::U16 => "u16", + SampleType::I32 => "i32", + SampleType::U32 => "u32", + SampleType::I64 => "i64", + SampleType::U64 => "u64", + SampleType::F32 => "f32", + SampleType::F64 => "f64", + } + } + + pub fn byte_size(&self) -> usize { + match self { + SampleType::I8 | SampleType::U8 => 1, + SampleType::I16 | SampleType::U16 => 2, + SampleType::I32 | SampleType::U32 | SampleType::F32 => 4, + SampleType::I64 | SampleType::U64 | SampleType::F64 => 8, + } + } +} + +/// Channel layout for multi-channel plot data. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PlotFormat { + /// Interleaved: ch0_s0, ch1_s0, ch0_s1, ch1_s1, ... + Interleaved, + /// Block: ch0_s0, ch0_s1, ..., ch1_s0, ch1_s1, ... + Block, + /// XY pairs: x0, y0, x1, y1, ... (equivalent to 2 interleaved channels). + XY, +} + +/// Configuration for the plot protocol decoder. +#[derive(Debug, Clone)] +pub struct PlotConfig { + pub sample_type: SampleType, + pub endian: Endian, + pub channels: usize, + pub format: PlotFormat, +} + +impl Default for PlotConfig { + fn default() -> Self { + Self { + sample_type: SampleType::F32, + endian: Endian::Little, + channels: 1, + format: PlotFormat::Interleaved, + } + } +} + +/// A decoded plot frame containing numeric channel data. +#[derive(Debug, Clone)] +pub struct PlotFrame { + pub channels: Vec>, + pub raw: Vec, + pub sample_type: SampleType, +} + +impl PlotFrame { + pub fn sample_count(&self) -> usize { + self.channels.first().map_or(0, |c| c.len()) + } +} + +/// Plot protocol decoder. +/// +/// Interprets binary frames as numeric samples and organizes them into +/// channels according to the configured layout. +#[derive(Debug, Clone)] +pub struct PlotDecoder { + config: PlotConfig, +} + +impl PlotDecoder { + pub fn new(config: PlotConfig) -> Self { + Self { config } + } + + pub fn config(&self) -> &PlotConfig { + &self.config + } + + fn read_one_sample(&self, data: &[u8]) -> Option { + let size = self.config.sample_type.byte_size(); + if data.len() < size { + return None; + } + let bytes = &data[..size]; + let raw = match self.config.sample_type { + SampleType::I8 => bytes[0] as i8 as f64, + SampleType::U8 => bytes[0] as f64, + SampleType::I16 => { + let b = [bytes[0], bytes[1]]; + let val = match self.config.endian { + Endian::Big => i16::from_be_bytes(b), + Endian::Little => i16::from_le_bytes(b), + }; + val as f64 + } + SampleType::U16 => { + let b = [bytes[0], bytes[1]]; + let val = match self.config.endian { + Endian::Big => u16::from_be_bytes(b), + Endian::Little => u16::from_le_bytes(b), + }; + val as f64 + } + SampleType::I32 => { + let b = [bytes[0], bytes[1], bytes[2], bytes[3]]; + let val = match self.config.endian { + Endian::Big => i32::from_be_bytes(b), + Endian::Little => i32::from_le_bytes(b), + }; + val as f64 + } + SampleType::U32 => { + let b = [bytes[0], bytes[1], bytes[2], bytes[3]]; + let val = match self.config.endian { + Endian::Big => u32::from_be_bytes(b), + Endian::Little => u32::from_le_bytes(b), + }; + val as f64 + } + SampleType::I64 => { + let b = [ + bytes[0], bytes[1], bytes[2], bytes[3], + bytes[4], bytes[5], bytes[6], bytes[7], + ]; + let val = match self.config.endian { + Endian::Big => i64::from_be_bytes(b), + Endian::Little => i64::from_le_bytes(b), + }; + val as f64 + } + SampleType::U64 => { + let b = [ + bytes[0], bytes[1], bytes[2], bytes[3], + bytes[4], bytes[5], bytes[6], bytes[7], + ]; + let val = match self.config.endian { + Endian::Big => u64::from_be_bytes(b), + Endian::Little => u64::from_le_bytes(b), + }; + val as f64 + } + SampleType::F32 => { + let b = [bytes[0], bytes[1], bytes[2], bytes[3]]; + let val = match self.config.endian { + Endian::Big => f32::from_be_bytes(b), + Endian::Little => f32::from_le_bytes(b), + }; + val as f64 + } + SampleType::F64 => { + let b = [ + bytes[0], bytes[1], bytes[2], bytes[3], + bytes[4], bytes[5], bytes[6], bytes[7], + ]; + match self.config.endian { + Endian::Big => f64::from_be_bytes(b), + Endian::Little => f64::from_le_bytes(b), + } + } + }; + Some(raw) + } +} + +impl ProtocolDecoder for PlotDecoder { + fn name(&self) -> &str { + "Plot" + } + + fn decode(&self, frame: &[u8]) -> Option { + let sample_size = self.config.sample_type.byte_size(); + let num_channels = match self.config.format { + PlotFormat::XY => 2, + _ => self.config.channels.max(1), + }; + + let total_samples = frame.len() / sample_size; + if total_samples == 0 { + return None; + } + + let samples_per_channel = total_samples / num_channels; + if samples_per_channel == 0 { + return None; + } + + // Read all samples + let mut flat: Vec = Vec::with_capacity(total_samples); + let mut offset = 0; + while offset + sample_size <= frame.len() { + let val = self.read_one_sample(&frame[offset..])?; + flat.push(val); + offset += sample_size; + } + + // Distribute into channels + let mut channels: Vec> = vec![Vec::with_capacity(samples_per_channel); num_channels]; + match self.config.format { + PlotFormat::Interleaved | PlotFormat::XY => { + for (i, val) in flat.into_iter().enumerate() { + let ch = i % num_channels; + if channels[ch].len() < samples_per_channel { + channels[ch].push(val); + } + } + } + PlotFormat::Block => { + for (ch, channel) in channels.iter_mut().enumerate() { + let start = ch * samples_per_channel; + let end = start + samples_per_channel; + if end <= flat.len() { + channel.extend_from_slice(&flat[start..end]); + } + } + } + } + + Some(DecodedData::Plot(PlotFrame { + channels, + raw: frame.to_vec(), + sample_type: self.config.sample_type, + })) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn sample_type_names() { + assert_eq!(SampleType::I8.name(), "i8"); + assert_eq!(SampleType::U16.name(), "u16"); + assert_eq!(SampleType::F32.name(), "f32"); + assert_eq!(SampleType::F64.name(), "f64"); + } + + #[test] + fn sample_type_sizes() { + assert_eq!(SampleType::I8.byte_size(), 1); + assert_eq!(SampleType::U8.byte_size(), 1); + assert_eq!(SampleType::I16.byte_size(), 2); + assert_eq!(SampleType::U16.byte_size(), 2); + assert_eq!(SampleType::I32.byte_size(), 4); + assert_eq!(SampleType::F32.byte_size(), 4); + assert_eq!(SampleType::I64.byte_size(), 8); + assert_eq!(SampleType::F64.byte_size(), 8); + } + + #[test] + fn plot_single_channel_u8() { + let cfg = PlotConfig { + sample_type: SampleType::U8, + channels: 1, + ..PlotConfig::default() + }; + let d = PlotDecoder::new(cfg); + let data = vec![10u8, 20, 30, 40]; + let result = d.decode(&data).unwrap(); + match result { + DecodedData::Plot(frame) => { + assert_eq!(frame.channels.len(), 1); + assert_eq!(frame.channels[0], vec![10.0, 20.0, 30.0, 40.0]); + assert_eq!(frame.sample_count(), 4); + } + other => panic!("expected Plot, got {:?}", other), + } + } + + #[test] + fn plot_single_channel_i16_le() { + let cfg = PlotConfig { + sample_type: SampleType::I16, + endian: Endian::Little, + channels: 1, + ..PlotConfig::default() + }; + let d = PlotDecoder::new(cfg); + let data = vec![0x00, 0x80, 0xff, 0x7f]; // -32768, 32767 in LE + let result = d.decode(&data).unwrap(); + match result { + DecodedData::Plot(frame) => { + assert_eq!(frame.channels.len(), 1); + assert_eq!(frame.channels[0], vec![-32768.0, 32767.0]); + } + other => panic!("expected Plot, got {:?}", other), + } + } + + #[test] + fn plot_single_channel_f32_le() { + let cfg = PlotConfig { + sample_type: SampleType::F32, + endian: Endian::Little, + channels: 1, + ..PlotConfig::default() + }; + let d = PlotDecoder::new(cfg); + // 1.0, -2.5 in f32 LE + let mut data = Vec::new(); + data.extend_from_slice(&1.0f32.to_le_bytes()); + data.extend_from_slice(&(-2.5f32).to_le_bytes()); + let result = d.decode(&data).unwrap(); + match result { + DecodedData::Plot(frame) => { + assert_eq!(frame.channels.len(), 1); + assert_eq!(frame.channels[0].len(), 2); + assert!((frame.channels[0][0] - 1.0).abs() < 1e-6); + assert!((frame.channels[0][1] - (-2.5)).abs() < 1e-6); + } + other => panic!("expected Plot, got {:?}", other), + } + } + + #[test] + fn plot_two_channels_interleaved_u16_le() { + let cfg = PlotConfig { + sample_type: SampleType::U16, + endian: Endian::Little, + channels: 2, + format: PlotFormat::Interleaved, + }; + let d = PlotDecoder::new(cfg); + // ch0: 100, 300 ch1: 200, 400 + let data = vec![ + 100u16.to_le_bytes(), 200u16.to_le_bytes(), + 300u16.to_le_bytes(), 400u16.to_le_bytes(), + ].into_iter().flatten().collect::>(); + let result = d.decode(&data).unwrap(); + match result { + DecodedData::Plot(frame) => { + assert_eq!(frame.channels.len(), 2); + assert_eq!(frame.channels[0], vec![100.0, 300.0]); + assert_eq!(frame.channels[1], vec![200.0, 400.0]); + } + other => panic!("expected Plot, got {:?}", other), + } + } + + #[test] + fn plot_two_channels_block_u16_be() { + let cfg = PlotConfig { + sample_type: SampleType::U16, + endian: Endian::Big, + channels: 2, + format: PlotFormat::Block, + }; + let d = PlotDecoder::new(cfg); + // ch0: 10, 20 ch1: 30, 40 + let data = vec![ + 10u16.to_be_bytes(), 20u16.to_be_bytes(), + 30u16.to_be_bytes(), 40u16.to_be_bytes(), + ].into_iter().flatten().collect::>(); + let result = d.decode(&data).unwrap(); + match result { + DecodedData::Plot(frame) => { + assert_eq!(frame.channels.len(), 2); + assert_eq!(frame.channels[0], vec![10.0, 20.0]); + assert_eq!(frame.channels[1], vec![30.0, 40.0]); + } + other => panic!("expected Plot, got {:?}", other), + } + } + + #[test] + fn plot_xy_format() { + let cfg = PlotConfig { + sample_type: SampleType::U16, + endian: Endian::Little, + channels: 0, // ignored for XY + format: PlotFormat::XY, + }; + let d = PlotDecoder::new(cfg); + // (x,y) pairs: (10, 100), (20, 200) + let data = vec![ + 10u16.to_le_bytes(), 100u16.to_le_bytes(), + 20u16.to_le_bytes(), 200u16.to_le_bytes(), + ].into_iter().flatten().collect::>(); + let result = d.decode(&data).unwrap(); + match result { + DecodedData::Plot(frame) => { + assert_eq!(frame.channels.len(), 2); + assert_eq!(frame.channels[0], vec![10.0, 20.0]); // X + assert_eq!(frame.channels[1], vec![100.0, 200.0]); // Y + } + other => panic!("expected Plot, got {:?}", other), + } + } + + #[test] + fn plot_empty_frame_returns_none() { + let d = PlotDecoder::new(PlotConfig::default()); + assert!(d.decode(&[]).is_none()); + } + + #[test] + fn plot_too_few_bytes_returns_none() { + let cfg = PlotConfig { + sample_type: SampleType::F64, + channels: 1, + ..PlotConfig::default() + }; + let d = PlotDecoder::new(cfg); + assert!(d.decode(&[0x00, 0x01, 0x02]).is_none()); + } + + #[test] + fn plot_insufficient_for_channels_returns_none() { + let cfg = PlotConfig { + sample_type: SampleType::U8, + endian: Endian::Little, + channels: 3, + format: PlotFormat::Interleaved, + }; + let d = PlotDecoder::new(cfg); + // 2 bytes for 3 channels → not enough for 1 full sample per channel + assert!(d.decode(&[1, 2]).is_none()); + } + + #[test] + fn plot_trailing_partial_sample_ignored() { + let cfg = PlotConfig { + sample_type: SampleType::U32, + channels: 1, + ..PlotConfig::default() + }; + let d = PlotDecoder::new(cfg); + // 7 bytes: 1 full u32 (4 bytes) + 3 trailing bytes + let data = vec![1u32.to_le_bytes().to_vec(), vec![0xff; 3]].concat(); + let result = d.decode(&data).unwrap(); + match result { + DecodedData::Plot(frame) => { + assert_eq!(frame.channels.len(), 1); + assert_eq!(frame.channels[0], vec![1.0]); + } + other => panic!("expected Plot, got {:?}", other), + } + } + + #[test] + fn plot_name() { + let d = PlotDecoder::new(PlotConfig::default()); + assert_eq!(d.name(), "Plot"); + } + + #[test] + fn plot_frame_sample_count_empty() { + let frame = PlotFrame { + channels: vec![], + raw: vec![], + sample_type: SampleType::U8, + }; + assert_eq!(frame.sample_count(), 0); + } + + #[test] + fn plot_raw_preserved() { + let cfg = PlotConfig { + sample_type: SampleType::U8, + channels: 1, + ..PlotConfig::default() + }; + let d = PlotDecoder::new(cfg); + let data = vec![1, 2, 3]; + let result = d.decode(&data).unwrap(); + match result { + DecodedData::Plot(frame) => { + assert_eq!(frame.raw, data); + } + other => panic!("expected Plot, got {:?}", other), + } + } + + #[test] + fn plot_decoder_is_send_sync() { + fn assert_send_sync() {} + assert_send_sync::(); + } +} diff --git a/crates/xserial-core/src/protocol/text.rs b/crates/xserial-core/src/protocol/text.rs new file mode 100644 index 0000000..319704b --- /dev/null +++ b/crates/xserial-core/src/protocol/text.rs @@ -0,0 +1,145 @@ +use super::{DecodedData, ProtocolDecoder}; + +/// Supported text encodings for the text protocol decoder. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TextEncoding { + /// Standard UTF-8. Returns `None` from `decode()` on invalid byte sequences. + Utf8, + /// ISO-8859-1 / Latin-1. Every byte maps 1:1 to a Unicode code point. + /// This encoding never fails. + Latin1, + /// 7-bit ASCII. Returns `None` from `decode()` if any byte has the high bit set. + Ascii, +} + +/// Text protocol decoder. +/// +/// Converts raw byte frames to UTF-8 strings using the configured encoding. +#[derive(Debug, Clone)] +pub struct TextDecoder { + encoding: TextEncoding, +} + +impl TextDecoder { + pub fn new(encoding: TextEncoding) -> Self { + Self { encoding } + } + + pub fn encoding(&self) -> TextEncoding { + self.encoding + } +} + +impl ProtocolDecoder for TextDecoder { + fn name(&self) -> &str { + "Text" + } + + fn decode(&self, frame: &[u8]) -> Option { + let text = match self.encoding { + TextEncoding::Utf8 => String::from_utf8(frame.to_vec()).ok()?, + TextEncoding::Latin1 => frame.iter().map(|&b| b as char).collect(), + TextEncoding::Ascii => { + if frame.iter().any(|&b| b >= 128) { + return None; + } + frame.iter().map(|&b| b as char).collect() + } + }; + Some(DecodedData::Text(text)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn text_utf8_valid() { + let d = TextDecoder::new(TextEncoding::Utf8); + let result = d.decode(b"hello"); + assert!(matches!(result, Some(DecodedData::Text(ref s)) if s == "hello")); + } + + #[test] + fn text_utf8_chinese() { + let d = TextDecoder::new(TextEncoding::Utf8); + let result = d.decode("你好世界".as_bytes()); + assert!(matches!(result, Some(DecodedData::Text(ref s)) if s == "你好世界")); + } + + #[test] + fn text_utf8_invalid_returns_none() { + let d = TextDecoder::new(TextEncoding::Utf8); + let invalid = vec![0xff, 0xfe, 0xfd]; + assert!(d.decode(&invalid).is_none()); + } + + #[test] + fn text_utf8_empty() { + let d = TextDecoder::new(TextEncoding::Utf8); + assert!(matches!(d.decode(b""), Some(DecodedData::Text(ref s)) if s.is_empty())); + } + + #[test] + fn text_latin1_all_bytes() { + let d = TextDecoder::new(TextEncoding::Latin1); + let data: Vec = (0..=255).collect(); + let result = d.decode(&data).unwrap(); + if let DecodedData::Text(s) = result { + assert_eq!(s.chars().count(), 256); + // Verify character code points match original bytes + for (i, ch) in s.chars().enumerate() { + assert_eq!(ch as u32, i as u32); + } + } else { + panic!("expected Text"); + } + } + + #[test] + fn text_latin1_empty() { + let d = TextDecoder::new(TextEncoding::Latin1); + assert!(matches!(d.decode(b""), Some(DecodedData::Text(ref s)) if s.is_empty())); + } + + #[test] + fn text_ascii_valid() { + let d = TextDecoder::new(TextEncoding::Ascii); + let result = d.decode(b"Hello World 123!"); + assert!(matches!(result, Some(DecodedData::Text(ref s)) if s == "Hello World 123!")); + } + + #[test] + fn text_ascii_high_bit_returns_none() { + let d = TextDecoder::new(TextEncoding::Ascii); + assert!(d.decode(&[0x80]).is_none()); + assert!(d.decode(&[b'A', 0xff, b'B']).is_none()); + } + + #[test] + fn text_ascii_boundary() { + let d = TextDecoder::new(TextEncoding::Ascii); + assert!(d.decode(&[0x7f]).is_some()); + assert!(d.decode(&[0x80]).is_none()); + } + + #[test] + fn text_name() { + assert_eq!(TextDecoder::new(TextEncoding::Utf8).name(), "Text"); + assert_eq!(TextDecoder::new(TextEncoding::Latin1).name(), "Text"); + assert_eq!(TextDecoder::new(TextEncoding::Ascii).name(), "Text"); + } + + #[test] + fn text_encoding_accessor() { + let d = TextDecoder::new(TextEncoding::Latin1); + assert_eq!(d.encoding(), TextEncoding::Latin1); + } + + #[test] + fn text_decoder_is_send_sync() { + fn assert_send_sync() {} + assert_send_sync::(); + } +} diff --git a/crates/xserial-core/src/transport/mod.rs b/crates/xserial-core/src/transport/mod.rs new file mode 100644 index 0000000..3b91b40 --- /dev/null +++ b/crates/xserial-core/src/transport/mod.rs @@ -0,0 +1,767 @@ +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use tokio::io::{AsyncRead, AsyncWrite}; + +use crate::transport::serial::SerialTransport; +use crate::transport::tcp::TcpTransport; +use crate::transport::udp::UdpTransport; +use crate::error::Result; + +pub mod serial; +pub mod tcp; +pub mod udp; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum TransportType { + Serial, + Tcp, + Udp +} + +impl std::fmt::Display for TransportType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TransportType::Serial => write!(f, "Serial"), + TransportType::Tcp => write!(f, "Tcp"), + TransportType::Udp => write!(f, "Udp") + } + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum TransportConfig { + Serial { + port: String, + baud_rate: u32 + }, + Tcp { + addr: String + }, + Udp { + bind_addr: String, + remote_addr: Option + } +} + +#[async_trait] +pub trait Transport: AsyncRead + AsyncWrite + Send + Sync + Unpin { + fn name(&self) -> &str; + fn transport_type(&self) -> TransportType; + fn is_connected(&self) -> bool; + async fn connect(&mut self) -> Result<()>; + async fn disconnect(&mut self) -> Result<()>; +} + +#[derive(Debug)] +pub enum Connection { + Serial(SerialTransport), + Tcp(TcpTransport), + Udp(UdpTransport), +} + +impl Connection { + pub fn new(config: TransportConfig) -> Self { + match config { + TransportConfig::Serial { port, baud_rate } => { + Connection::Serial(SerialTransport::new(port, baud_rate)) + } + TransportConfig::Tcp { addr } => Connection::Tcp(TcpTransport::new(addr)), + TransportConfig::Udp { + bind_addr, + remote_addr, + } => Connection::Udp(UdpTransport::new(bind_addr, remote_addr)), + } + } + + pub fn transport_type(&self) -> TransportType { + match self { + Connection::Serial(_) => TransportType::Serial, + Connection::Tcp(_) => TransportType::Tcp, + Connection::Udp(_) => TransportType::Udp, + } + } + + pub fn name(&self) -> &str { + match self { + Connection::Serial(t) => t.name(), + Connection::Tcp(t) => t.name(), + Connection::Udp(t) => t.name(), + } + } + + pub fn is_connected(&self) -> bool { + match self { + Connection::Serial(t) => t.is_connected(), + Connection::Tcp(t) => t.is_connected(), + Connection::Udp(t) => t.is_connected(), + } + } + + pub async fn connect(&mut self) -> Result<()> { + match self { + Connection::Serial(t) => t.connect().await, + Connection::Tcp(t) => t.connect().await, + Connection::Udp(t) => t.connect().await, + } + } + + pub async fn disconnect(&mut self) -> Result<()> { + match self { + Connection::Serial(t) => t.disconnect().await, + Connection::Tcp(t) => t.disconnect().await, + Connection::Udp(t) => t.disconnect().await, + } + } +} + +impl AsyncRead for Connection { + fn poll_read( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut tokio::io::ReadBuf<'_>, + ) -> std::task::Poll> { + match self.get_mut() { + Connection::Serial(t) => std::pin::Pin::new(t).poll_read(cx, buf), + Connection::Tcp(t) => std::pin::Pin::new(t).poll_read(cx, buf), + Connection::Udp(t) => std::pin::Pin::new(t).poll_read(cx, buf), + } + } +} + +impl AsyncWrite for Connection { + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + match self.get_mut() { + Connection::Serial(t) => std::pin::Pin::new(t).poll_write(cx, buf), + Connection::Tcp(t) => std::pin::Pin::new(t).poll_write(cx, buf), + Connection::Udp(t) => std::pin::Pin::new(t).poll_write(cx, buf), + } + } + + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match self.get_mut() { + Connection::Serial(t) => std::pin::Pin::new(t).poll_flush(cx), + Connection::Tcp(t) => std::pin::Pin::new(t).poll_flush(cx), + Connection::Udp(t) => std::pin::Pin::new(t).poll_flush(cx), + } + } + + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match self.get_mut() { + Connection::Serial(t) => std::pin::Pin::new(t).poll_shutdown(cx), + Connection::Tcp(t) => std::pin::Pin::new(t).poll_shutdown(cx), + Connection::Udp(t) => std::pin::Pin::new(t).poll_shutdown(cx), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_transport_type_display() { + assert_eq!(TransportType::Serial.to_string(), "Serial"); + assert_eq!(TransportType::Tcp.to_string(), "Tcp"); + assert_eq!(TransportType::Udp.to_string(), "Udp"); + } + + #[test] + fn test_transport_type_debug() { + let _ = format!("{:?}", TransportType::Serial); + let _ = format!("{:?}", TransportType::Tcp); + let _ = format!("{:?}", TransportType::Udp); + } + + #[test] + fn test_transport_type_eq() { + assert_eq!(TransportType::Serial, TransportType::Serial); + assert_eq!(TransportType::Tcp, TransportType::Tcp); + assert_eq!(TransportType::Udp, TransportType::Udp); + assert_ne!(TransportType::Serial, TransportType::Tcp); + assert_ne!(TransportType::Serial, TransportType::Udp); + assert_ne!(TransportType::Tcp, TransportType::Udp); + } + + #[test] + fn test_transport_type_copy_clone() { + let original = TransportType::Serial; + let cloned = original; + assert_eq!(original, cloned); + + let original = TransportType::Tcp; + let cloned = original.clone(); + assert_eq!(original, cloned); + + let original = TransportType::Udp; + let cloned = original.clone(); + assert_eq!(original, cloned); + } + + #[test] + fn test_transport_type_serialize() { + assert_eq!( + serde_json::to_string(&TransportType::Serial).unwrap(), + "\"Serial\"" + ); + assert_eq!( + serde_json::to_string(&TransportType::Tcp).unwrap(), + "\"Tcp\"" + ); + assert_eq!( + serde_json::to_string(&TransportType::Udp).unwrap(), + "\"Udp\"" + ); + } + + #[test] + fn test_transport_type_deserialize() { + let t: TransportType = serde_json::from_str("\"Serial\"").unwrap(); + assert_eq!(t, TransportType::Serial); + + let t: TransportType = serde_json::from_str("\"Tcp\"").unwrap(); + assert_eq!(t, TransportType::Tcp); + + let t: TransportType = serde_json::from_str("\"Udp\"").unwrap(); + assert_eq!(t, TransportType::Udp); + } + + #[test] + fn test_transport_type_roundtrip() { + for original in [TransportType::Serial, TransportType::Tcp, TransportType::Udp] { + let json = serde_json::to_string(&original).unwrap(); + let roundtripped: TransportType = serde_json::from_str(&json).unwrap(); + assert_eq!(original, roundtripped); + } + } + + #[test] + fn test_transport_config_debug() { + let cfg = TransportConfig::Serial { + port: "COM1".into(), + baud_rate: 115200, + }; + let _ = format!("{:?}", cfg); + + let cfg = TransportConfig::Tcp { + addr: "127.0.0.1:8080".into(), + }; + let _ = format!("{:?}", cfg); + + let cfg = TransportConfig::Udp { + bind_addr: "0.0.0.0:9000".into(), + remote_addr: Some("192.168.1.1:9001".into()), + }; + let _ = format!("{:?}", cfg); + } + + #[test] + fn test_transport_config_clone() { + let original = TransportConfig::Serial { + port: "COM1".into(), + baud_rate: 115200, + }; + let cloned = original.clone(); + assert_eq!(format!("{:?}", original), format!("{:?}", cloned)); + + let original = TransportConfig::Tcp { + addr: "127.0.0.1:8080".into(), + }; + let cloned = original.clone(); + assert_eq!(format!("{:?}", original), format!("{:?}", cloned)); + + let original = TransportConfig::Udp { + bind_addr: "0.0.0.0:9000".into(), + remote_addr: Some("192.168.1.1:9001".into()), + }; + let cloned = original.clone(); + assert_eq!(format!("{:?}", original), format!("{:?}", cloned)); + } + + #[test] + fn test_transport_config_serialize_serial() { + let cfg = TransportConfig::Serial { + port: "COM1".into(), + baud_rate: 115200, + }; + let json = serde_json::to_string(&cfg).unwrap(); + assert!(json.contains("COM1")); + assert!(json.contains("115200")); + } + + #[test] + fn test_transport_config_deserialize_serial() { + let json = r#"{"Serial":{"port":"COM1","baud_rate":9600}}"#; + let cfg: TransportConfig = serde_json::from_str(json).unwrap(); + match cfg { + TransportConfig::Serial { port, baud_rate } => { + assert_eq!(port, "COM1"); + assert_eq!(baud_rate, 9600); + } + _ => panic!("expected Serial variant"), + } + } + + #[test] + fn test_transport_config_serialize_tcp() { + let cfg = TransportConfig::Tcp { + addr: "127.0.0.1:8080".into(), + }; + let json = serde_json::to_string(&cfg).unwrap(); + assert!(json.contains("127.0.0.1:8080")); + } + + #[test] + fn test_transport_config_deserialize_tcp() { + let json = r#"{"Tcp":{"addr":"127.0.0.1:8080"}}"#; + let cfg: TransportConfig = serde_json::from_str(json).unwrap(); + match cfg { + TransportConfig::Tcp { addr } => { + assert_eq!(addr, "127.0.0.1:8080"); + } + _ => panic!("expected Tcp variant"), + } + } + + #[test] + fn test_transport_config_serialize_udp_with_remote() { + let cfg = TransportConfig::Udp { + bind_addr: "0.0.0.0:9000".into(), + remote_addr: Some("192.168.1.1:9001".into()), + }; + let json = serde_json::to_string(&cfg).unwrap(); + assert!(json.contains("0.0.0.0:9000")); + assert!(json.contains("192.168.1.1:9001")); + } + + #[test] + fn test_transport_config_serialize_udp_without_remote() { + let cfg = TransportConfig::Udp { + bind_addr: "0.0.0.0:9000".into(), + remote_addr: None, + }; + let json = serde_json::to_string(&cfg).unwrap(); + assert!(json.contains("0.0.0.0:9000")); + assert!(json.contains("null")); + } + + #[test] + fn test_transport_config_deserialize_udp() { + let json = r#"{"Udp":{"bind_addr":"0.0.0.0:9000","remote_addr":"192.168.1.1:9001"}}"#; + let cfg: TransportConfig = serde_json::from_str(json).unwrap(); + match cfg { + TransportConfig::Udp { + bind_addr, + remote_addr, + } => { + assert_eq!(bind_addr, "0.0.0.0:9000"); + assert_eq!(remote_addr, Some("192.168.1.1:9001".into())); + } + _ => panic!("expected Udp variant"), + } + + let json = r#"{"Udp":{"bind_addr":"0.0.0.0:9000","remote_addr":null}}"#; + let cfg: TransportConfig = serde_json::from_str(json).unwrap(); + match cfg { + TransportConfig::Udp { + bind_addr, + remote_addr, + } => { + assert_eq!(bind_addr, "0.0.0.0:9000"); + assert_eq!(remote_addr, None); + } + _ => panic!("expected Udp variant"), + } + } + + #[test] + fn test_transport_config_roundtrip() { + let configs = vec![ + TransportConfig::Serial { + port: "COM3".into(), + baud_rate: 57600, + }, + TransportConfig::Tcp { + addr: "10.0.0.1:9999".into(), + }, + TransportConfig::Udp { + bind_addr: "0.0.0.0:7000".into(), + remote_addr: Some("10.0.0.2:7001".into()), + }, + TransportConfig::Udp { + bind_addr: "127.0.0.1:8000".into(), + remote_addr: None, + }, + ]; + + for original in &configs { + let json = serde_json::to_string(original).unwrap(); + let roundtripped: TransportConfig = serde_json::from_str(&json).unwrap(); + assert_eq!( + serde_json::to_string(&roundtripped).unwrap(), + json, + "roundtrip serialized forms must match" + ); + } + } + + // ── Connection tests ────────────────────────────────────────────── + + #[test] + fn test_connection_new_serial() { + let conn = Connection::new(TransportConfig::Serial { + port: "COM1".into(), + baud_rate: 115200, + }); + assert_eq!(conn.transport_type(), TransportType::Serial); + assert_eq!(conn.name(), "COM1"); + assert!(!conn.is_connected()); + } + + #[test] + fn test_connection_new_tcp() { + let conn = Connection::new(TransportConfig::Tcp { + addr: "192.168.1.1:8080".into(), + }); + assert_eq!(conn.transport_type(), TransportType::Tcp); + assert_eq!(conn.name(), "192.168.1.1:8080"); + assert!(!conn.is_connected()); + } + + #[test] + fn test_connection_new_udp_with_remote() { + let conn = Connection::new(TransportConfig::Udp { + bind_addr: "0.0.0.0:9000".into(), + remote_addr: Some("192.168.1.1:9001".into()), + }); + assert_eq!(conn.transport_type(), TransportType::Udp); + assert_eq!(conn.name(), "0.0.0.0:9000"); + assert!(!conn.is_connected()); + } + + #[test] + fn test_connection_new_udp_without_remote() { + let conn = Connection::new(TransportConfig::Udp { + bind_addr: "127.0.0.1:0".into(), + remote_addr: None, + }); + assert_eq!(conn.transport_type(), TransportType::Udp); + assert_eq!(conn.name(), "127.0.0.1:0"); + assert!(!conn.is_connected()); + } + + #[test] + fn test_connection_is_connected_default() { + let serial = Connection::new(TransportConfig::Serial { + port: "COM1".into(), + baud_rate: 9600, + }); + let tcp = Connection::new(TransportConfig::Tcp { + addr: "127.0.0.1:8080".into(), + }); + let udp = Connection::new(TransportConfig::Udp { + bind_addr: "0.0.0.0:0".into(), + remote_addr: None, + }); + assert!(!serial.is_connected()); + assert!(!tcp.is_connected()); + assert!(!udp.is_connected()); + } + + #[test] + fn test_connection_debug() { + let serial = Connection::new(TransportConfig::Serial { + port: "COM1".into(), + baud_rate: 115200, + }); + let tcp = Connection::new(TransportConfig::Tcp { + addr: "127.0.0.1:8080".into(), + }); + let udp = Connection::new(TransportConfig::Udp { + bind_addr: "0.0.0.0:0".into(), + remote_addr: None, + }); + let _ = format!("{:?}", serial); + let _ = format!("{:?}", tcp); + let _ = format!("{:?}", udp); + } + + fn noop_waker() -> std::task::Waker { + use std::task::{RawWaker, RawWakerVTable}; + unsafe fn clone(_: *const ()) -> RawWaker { + RawWaker::new(std::ptr::null(), &VTABLE) + } + unsafe fn wake(_: *const ()) {} + unsafe fn wake_by_ref(_: *const ()) {} + unsafe fn drop(_: *const ()) {} + static VTABLE: RawWakerVTable = + RawWakerVTable::new(clone, wake, wake_by_ref, drop); + unsafe { std::task::Waker::from_raw(RawWaker::new(std::ptr::null(), &VTABLE)) } + } + + fn serial_conn() -> Connection { + Connection::new(TransportConfig::Serial { + port: "COM1".into(), + baud_rate: 115200, + }) + } + + fn tcp_conn() -> Connection { + Connection::new(TransportConfig::Tcp { + addr: "127.0.0.1:8080".into(), + }) + } + + fn udp_conn() -> Connection { + Connection::new(TransportConfig::Udp { + bind_addr: "127.0.0.1:0".into(), + remote_addr: None, + }) + } + + #[test] + fn test_connection_poll_read_not_connected() { + use tokio::io::ReadBuf; + use std::pin::Pin; + use std::task::{Context, Poll}; + + for mut conn in [serial_conn(), tcp_conn(), udp_conn()] { + let pinned = Pin::new(&mut conn); + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + let mut buf_data = [0u8; 16]; + let mut buf = ReadBuf::new(&mut buf_data); + match pinned.poll_read(&mut cx, &mut buf) { + Poll::Ready(Err(e)) => { + assert_eq!(e.kind(), std::io::ErrorKind::NotConnected); + } + other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other), + } + } + } + + #[test] + fn test_connection_poll_write_not_connected() { + use std::pin::Pin; + use std::task::{Context, Poll}; + + for mut conn in [serial_conn(), tcp_conn(), udp_conn()] { + let pinned = Pin::new(&mut conn); + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + match pinned.poll_write(&mut cx, b"hello") { + Poll::Ready(Err(e)) => { + assert_eq!(e.kind(), std::io::ErrorKind::NotConnected); + } + other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other), + } + } + } + + #[test] + fn test_connection_poll_flush_not_connected() { + use std::pin::Pin; + use std::task::{Context, Poll}; + + for mut conn in [serial_conn(), tcp_conn(), udp_conn()] { + let pinned = Pin::new(&mut conn); + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + match pinned.poll_flush(&mut cx) { + Poll::Ready(Ok(())) => {} + other => panic!("expected Poll::Ready(Ok(())), got {:?}", other), + } + } + } + + #[test] + fn test_connection_poll_shutdown_not_connected() { + use std::pin::Pin; + use std::task::{Context, Poll}; + + for mut conn in [serial_conn(), tcp_conn(), udp_conn()] { + let pinned = Pin::new(&mut conn); + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + match pinned.poll_shutdown(&mut cx) { + Poll::Ready(Ok(())) => {} + other => panic!("expected Poll::Ready(Ok(())), got {:?}", other), + } + } + } + + #[tokio::test] + async fn test_connection_tcp_connect_and_disconnect() { + use tokio::net::TcpListener; + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let mut conn = Connection::new(TransportConfig::Tcp { addr }); + assert!(!conn.is_connected()); + + conn.connect().await.unwrap(); + assert!(conn.is_connected()); + + conn.disconnect().await.unwrap(); + assert!(!conn.is_connected()); + } + + #[tokio::test] + async fn test_connection_udp_connect_and_disconnect_without_remote() { + let mut conn = Connection::new(TransportConfig::Udp { + bind_addr: "127.0.0.1:0".into(), + remote_addr: None, + }); + assert!(!conn.is_connected()); + + conn.connect().await.unwrap(); + assert!(conn.is_connected()); + + conn.disconnect().await.unwrap(); + assert!(!conn.is_connected()); + } + + #[tokio::test] + async fn test_connection_udp_connect_and_disconnect_with_remote() { + use tokio::net::UdpSocket; + let remote = UdpSocket::bind("127.0.0.1:0").await.unwrap(); + let remote_addr = remote.local_addr().unwrap().to_string(); + + let mut conn = Connection::new(TransportConfig::Udp { + bind_addr: "127.0.0.1:0".into(), + remote_addr: Some(remote_addr), + }); + assert!(!conn.is_connected()); + + conn.connect().await.unwrap(); + assert!(conn.is_connected()); + + conn.disconnect().await.unwrap(); + assert!(!conn.is_connected()); + } + + #[tokio::test] + async fn test_connection_tcp_double_connect_is_noop() { + use tokio::net::TcpListener; + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let mut conn = Connection::new(TransportConfig::Tcp { addr }); + conn.connect().await.unwrap(); + assert!(conn.is_connected()); + + let result = conn.connect().await; + assert!(result.is_ok(), "double connect should be a no-op"); + assert!(conn.is_connected()); + } + + #[tokio::test] + async fn test_connection_tcp_disconnect_without_connect() { + let mut conn = Connection::new(TransportConfig::Tcp { + addr: "127.0.0.1:8080".into(), + }); + let result = conn.disconnect().await; + assert!(result.is_ok(), "disconnect without connect should be safe"); + assert!(!conn.is_connected()); + } + + #[tokio::test] + async fn test_connection_tcp_connect_to_unreachable() { + let mut conn = Connection::new(TransportConfig::Tcp { + addr: "127.0.0.1:1".into(), + }); + let result = conn.connect().await; + assert!(result.is_err()); + assert!(!conn.is_connected()); + } + + #[tokio::test] + async fn test_connection_udp_connect_bind_failure() { + let mut conn = Connection::new(TransportConfig::Udp { + bind_addr: "invalid_addr".into(), + remote_addr: None, + }); + let result = conn.connect().await; + assert!(result.is_err()); + assert!(!conn.is_connected()); + } + + #[tokio::test] + async fn test_connection_udp_double_connect_is_noop() { + let mut conn = Connection::new(TransportConfig::Udp { + bind_addr: "127.0.0.1:0".into(), + remote_addr: None, + }); + conn.connect().await.unwrap(); + assert!(conn.is_connected()); + + let result = conn.connect().await; + assert!(result.is_ok()); + assert!(conn.is_connected()); + } + + #[tokio::test] + async fn test_connection_tcp_async_read_write() { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tokio::net::TcpListener; + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let server = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + AsyncWriteExt::write_all(&mut stream, b"hello").await.unwrap(); + let mut buf = [0u8; 5]; + AsyncReadExt::read_exact(&mut stream, &mut buf).await.unwrap(); + assert_eq!(&buf, b"world"); + }); + + let mut conn = Connection::new(TransportConfig::Tcp { addr }); + conn.connect().await.unwrap(); + + let mut buf = [0u8; 5]; + AsyncReadExt::read_exact(&mut conn, &mut buf).await.unwrap(); + assert_eq!(&buf, b"hello"); + + AsyncWriteExt::write_all(&mut conn, b"world").await.unwrap(); + + conn.disconnect().await.unwrap(); + server.await.unwrap(); + } + + #[tokio::test] + async fn test_connection_udp_async_read_write_with_remote() { + use tokio::io::{AsyncReadExt, AsyncWriteExt}; + use tokio::net::UdpSocket; + + let remote = UdpSocket::bind("127.0.0.1:0").await.unwrap(); + let remote_addr = remote.local_addr().unwrap().to_string(); + + let mut conn = Connection::new(TransportConfig::Udp { + bind_addr: "127.0.0.1:0".into(), + remote_addr: Some(remote_addr), + }); + conn.connect().await.unwrap(); + + AsyncWriteExt::write_all(&mut conn, b"hello").await.unwrap(); + + let mut buf = [0u8; 1024]; + let (n, from) = remote.recv_from(&mut buf).await.unwrap(); + assert_eq!(&buf[..n], b"hello"); + + remote.send_to(b"world", from).await.unwrap(); + + let mut read_buf = [0u8; 5]; + AsyncReadExt::read_exact(&mut conn, &mut read_buf).await.unwrap(); + assert_eq!(&read_buf, b"world"); + } +} \ No newline at end of file diff --git a/crates/xserial-core/src/transport/serial.rs b/crates/xserial-core/src/transport/serial.rs new file mode 100644 index 0000000..0bebe9f --- /dev/null +++ b/crates/xserial-core/src/transport/serial.rs @@ -0,0 +1,286 @@ +use async_trait::async_trait; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio_serial::{SerialPortBuilderExt, SerialStream}; +use tracing::{debug, info, warn}; + +use super::{Transport, TransportType}; +use crate::error::Result; + +#[derive(Debug)] +pub struct SerialTransport { + port: Option, + port_name: String, + baud_rate: u32, +} + +impl SerialTransport { + pub fn new(port_name: String, baud_rate: u32) -> Self { + Self { + port: None, + port_name, + baud_rate, + } + } + + pub fn list_ports() -> Vec { + match serialport::available_ports() { + Ok(ports) => ports, + Err(e) => { + warn!("Failed to enumerate serial ports: {}", e); + vec![] + } + } + } + + pub fn port_name(&self) -> &str { + &self.port_name + } + + pub fn baud_rate(&self) -> u32 { + self.baud_rate + } +} + +impl AsyncRead for SerialTransport { + fn poll_read( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> std::task::Poll> { + match &mut self.get_mut().port { + Some(port) => std::pin::Pin::new(port).poll_read(cx, buf), + None => std::task::Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::NotConnected, + "Serial port not open", + ))), + } + } +} + +impl AsyncWrite for SerialTransport { + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + match &mut self.get_mut().port { + Some(port) => std::pin::Pin::new(port).poll_write(cx, buf), + None => std::task::Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::NotConnected, + "Serial port not open", + ))), + } + } + + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match &mut self.get_mut().port { + Some(port) => std::pin::Pin::new(port).poll_flush(cx), + None => std::task::Poll::Ready(Ok(())), + } + } + + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match &mut self.get_mut().port { + Some(port) => std::pin::Pin::new(port).poll_shutdown(cx), + None => std::task::Poll::Ready(Ok(())), + } + } +} + +#[async_trait] +impl Transport for SerialTransport { + fn name(&self) -> &str { + &self.port_name + } + + fn transport_type(&self) -> TransportType { + TransportType::Serial + } + + fn is_connected(&self) -> bool { + self.port.is_some() + } + + async fn connect(&mut self) -> Result<()> { + if self.is_connected() { + return Ok(()) + } + + info!("Opening serial port {} at {} baud", self.port_name, self.baud_rate); + + let port = tokio_serial::new(&self.port_name, self.baud_rate) + .open_native_async() + .map_err(|e| crate::error::Error::ConnectionFailed(format!( + "Failed to open {}: {}", + self.port_name, e + )))?; + + debug!("Serial port {} opened successfully", self.port_name); + self.port = Some(port); + Ok(()) + } + + async fn disconnect(&mut self) -> Result<()> { + if let Some(port) = self.port.take() { + debug!("Closing serial port {}", self.port_name); + drop(port); + debug!("Serial port {} closed", self.port_name); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::ErrorKind; + use std::pin::Pin; + use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; + use tokio::io::ReadBuf; + + fn noop_waker() -> Waker { + unsafe fn raw_clone(_: *const ()) -> RawWaker { + RawWaker::new(std::ptr::null(), &RAW_VTABLE) + } + unsafe fn raw_wake(_: *const ()) {} + unsafe fn raw_wake_by_ref(_: *const ()) {} + unsafe fn raw_drop(_: *const ()) {} + + static RAW_VTABLE: RawWakerVTable = RawWakerVTable::new( + raw_clone, + raw_wake, + raw_wake_by_ref, + raw_drop, + ); + + unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &RAW_VTABLE)) } + } + + // ── Constructor and accessor tests ── + + #[test] + fn test_new() { + let transport = SerialTransport::new("COM1".into(), 115200); + assert_eq!(transport.port_name(), "COM1"); + assert_eq!(transport.baud_rate(), 115200); + } + + #[test] + fn test_new_different_params() { + let transport = SerialTransport::new("COM3".into(), 9600); + assert_eq!(transport.port_name(), "COM3"); + assert_eq!(transport.baud_rate(), 9600); + } + + #[test] + fn test_name() { + let transport = SerialTransport::new("COM1".into(), 115200); + assert_eq!(transport.name(), "COM1"); + } + + #[test] + fn test_transport_type() { + let transport = SerialTransport::new("COM1".into(), 115200); + assert_eq!(transport.transport_type(), TransportType::Serial); + } + + #[test] + fn test_is_connected_false_by_default() { + let transport = SerialTransport::new("COM1".into(), 115200); + assert!(!transport.is_connected()); + } + + // ── list_ports tests ── + + #[test] + fn test_list_ports_does_not_panic() { + let _ = SerialTransport::list_ports(); + } + + #[test] + fn test_list_ports_returns_vec() { + let ports: Vec = SerialTransport::list_ports(); + let _ = ports; + } + + // ── AsyncRead tests (not connected) ── + + #[tokio::test] + async fn test_poll_read_not_connected() { + let mut transport = SerialTransport::new("COM1".into(), 115200); + let pinned = Pin::new(&mut transport); + let mut buf_data = [0u8; 16]; + let mut buf = ReadBuf::new(&mut buf_data); + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + match pinned.poll_read(&mut cx, &mut buf) { + Poll::Ready(Err(e)) => assert_eq!(e.kind(), ErrorKind::NotConnected), + other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other), + } + } + + // ── AsyncWrite tests (not connected) ── + + #[tokio::test] + async fn test_poll_write_not_connected() { + let mut transport = SerialTransport::new("COM1".into(), 115200); + let pinned = Pin::new(&mut transport); + let data = b"hello"; + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + match pinned.poll_write(&mut cx, data) { + Poll::Ready(Err(e)) => assert_eq!(e.kind(), ErrorKind::NotConnected), + other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other), + } + } + + #[tokio::test] + async fn test_poll_flush_not_connected() { + let mut transport = SerialTransport::new("COM1".into(), 115200); + let pinned = Pin::new(&mut transport); + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + match pinned.poll_flush(&mut cx) { + Poll::Ready(Ok(())) => {} + other => panic!("expected Poll::Ready(Ok(())), got {:?}", other), + } + } + + #[tokio::test] + async fn test_poll_shutdown_not_connected() { + let mut transport = SerialTransport::new("COM1".into(), 115200); + let pinned = Pin::new(&mut transport); + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + match pinned.poll_shutdown(&mut cx) { + Poll::Ready(Ok(())) => {} + other => panic!("expected Poll::Ready(Ok(())), got {:?}", other), + } + } + + // ── Transport trait method tests ── + + #[test] + fn test_transport_name() { + let transport = SerialTransport::new("COM1".into(), 115200); + assert_eq!(transport.name(), "COM1"); + } + + #[test] + fn test_transport_transport_type() { + let transport = SerialTransport::new("COM1".into(), 115200); + assert_eq!(transport.transport_type(), TransportType::Serial); + } + + #[test] + fn test_transport_is_connected() { + let transport = SerialTransport::new("COM1".into(), 115200); + assert!(!transport.is_connected()); + } +} diff --git a/crates/xserial-core/src/transport/tcp.rs b/crates/xserial-core/src/transport/tcp.rs new file mode 100644 index 0000000..933be3b --- /dev/null +++ b/crates/xserial-core/src/transport/tcp.rs @@ -0,0 +1,325 @@ + +use async_trait::async_trait; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio::net::TcpStream; +use tracing::{debug, info}; + +use super::{Transport, TransportType}; +use crate::error::Result; + +#[derive(Debug)] +pub struct TcpTransport { + stream: Option, + addr: String +} + +impl TcpTransport { + pub fn new(addr: String) -> Self { + Self { + stream: None, + addr + } + } + + pub fn addr(&self) -> &str { + &self.addr + } +} + +impl AsyncRead for TcpTransport { + fn poll_read( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> std::task::Poll> { + match &mut self.get_mut().stream { + Some(stream) => std::pin::Pin::new(stream).poll_read(cx, buf), + None => std::task::Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::NotConnected, + "TCP not connected", + ))), + } + } +} + +impl AsyncWrite for TcpTransport { + fn poll_write( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + buf: &[u8], + ) -> std::task::Poll> { + match &mut self.get_mut().stream { + Some(stream) => std::pin::Pin::new(stream).poll_write(cx, buf), + None => std::task::Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::NotConnected, + "TCP not connected", + ))), + } + } + + fn poll_flush( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match &mut self.get_mut().stream { + Some(stream) => std::pin::Pin::new(stream).poll_flush(cx), + None => std::task::Poll::Ready(Ok(())), + } + } + + fn poll_shutdown( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll> { + match &mut self.get_mut().stream { + Some(stream) => std::pin::Pin::new(stream).poll_shutdown(cx), + None => std::task::Poll::Ready(Ok(())), + } + } +} + +#[async_trait] +impl Transport for TcpTransport { + fn name(&self) -> &str { + &self.addr + } + + fn transport_type(&self) -> TransportType { + TransportType::Tcp + } + + fn is_connected(&self) -> bool { + self.stream.is_some() + } + + async fn connect(&mut self) -> Result<()> { + if self.is_connected() { + return Ok(()); + } + + info!("Connecting to TCP {}", self.addr); + + let stream = TcpStream::connect(&self.addr).await.map_err(|e| { + crate::error::Error::ConnectionFailed(format!( + "Failed to connect to {}: {}", + self.addr, e + )) + })?; + + debug!("TCP connection to {} established", self.addr); + self.stream = Some(stream); + Ok(()) + } + + async fn disconnect(&mut self) -> Result<()> { + if let Some(stream) = self.stream.take() { + debug!("Closing TCP connection {}", self.addr); + drop(stream); + info!("TCP connection {} closed", self.addr); + } + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::error::Error; + use std::pin::Pin; + use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker}; + use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadBuf}; + use tokio::net::TcpListener; + + // ── Helper: noop waker for poll tests ───────────────────────────── + + fn noop_waker() -> Waker { + unsafe fn noop_raw_clone(data: *const ()) -> RawWaker { + RawWaker::new(data, &NOOP_VTABLE) + } + unsafe fn noop_raw_wake(_data: *const ()) {} + unsafe fn noop_raw_wake_by_ref(_data: *const ()) {} + unsafe fn noop_raw_drop(_data: *const ()) {} + + static NOOP_VTABLE: RawWakerVTable = RawWakerVTable::new( + noop_raw_clone, + noop_raw_wake, + noop_raw_wake_by_ref, + noop_raw_drop, + ); + + unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &NOOP_VTABLE)) } + } + + // ── Sync accessor tests ─────────────────────────────────────────── + + #[test] + fn test_new() { + let transport = TcpTransport::new("127.0.0.1:8080".into()); + assert_eq!(transport.addr(), "127.0.0.1:8080"); + } + + #[test] + fn test_name() { + let transport = TcpTransport::new("127.0.0.1:8080".into()); + assert_eq!(transport.name(), transport.addr()); + assert_eq!(transport.name(), "127.0.0.1:8080"); + } + + #[test] + fn test_transport_type() { + let transport = TcpTransport::new("127.0.0.1:8080".into()); + assert_eq!(transport.transport_type(), TransportType::Tcp); + } + + #[test] + fn test_is_connected_false_by_default() { + let transport = TcpTransport::new("127.0.0.1:8080".into()); + assert!(!transport.is_connected()); + } + + // ── Poll error-state tests (no real connection) ─────────────────── + + #[test] + fn test_poll_read_not_connected() { + let mut transport = TcpTransport::new("127.0.0.1:8080".into()); + let pinned = Pin::new(&mut transport); + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + let mut buf_data = [0u8; 16]; + let mut buf = ReadBuf::new(&mut buf_data); + + match pinned.poll_read(&mut cx, &mut buf) { + Poll::Ready(Err(e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotConnected), + other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other), + } + } + + #[test] + fn test_poll_write_not_connected() { + let mut transport = TcpTransport::new("127.0.0.1:8080".into()); + let pinned = Pin::new(&mut transport); + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + + match pinned.poll_write(&mut cx, b"hello") { + Poll::Ready(Err(e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotConnected), + other => panic!("expected Poll::Ready(Err(NotConnected)), got {:?}", other), + } + } + + #[test] + fn test_poll_flush_not_connected() { + let mut transport = TcpTransport::new("127.0.0.1:8080".into()); + let pinned = Pin::new(&mut transport); + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + + match pinned.poll_flush(&mut cx) { + Poll::Ready(Ok(())) => {} // expected + other => panic!("expected Poll::Ready(Ok(())), got {:?}", other), + } + } + + #[test] + fn test_poll_shutdown_not_connected() { + let mut transport = TcpTransport::new("127.0.0.1:8080".into()); + let pinned = Pin::new(&mut transport); + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + + match pinned.poll_shutdown(&mut cx) { + Poll::Ready(Ok(())) => {} // expected + other => panic!("expected Poll::Ready(Ok(())), got {:?}", other), + } + } + + // ── Async connect/disconnect tests ──────────────────────────────── + + #[tokio::test] + async fn test_connect_and_disconnect() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let mut transport = TcpTransport::new(addr); + assert!(!transport.is_connected()); + + transport.connect().await.unwrap(); + assert!(transport.is_connected()); + + transport.disconnect().await.unwrap(); + assert!(!transport.is_connected()); + } + + #[tokio::test] + async fn test_double_connect_is_noop() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let mut transport = TcpTransport::new(addr); + transport.connect().await.unwrap(); + assert!(transport.is_connected()); + + // Second connect should be a safe no-op + let result = transport.connect().await; + assert!(result.is_ok(), "double connect should return Ok"); + assert!(transport.is_connected()); + } + + #[tokio::test] + async fn test_connect_to_unreachable_addr() { + let mut transport = TcpTransport::new("127.0.0.1:1".into()); + let result = transport.connect().await; + assert!(result.is_err()); + match result.unwrap_err() { + Error::ConnectionFailed(_) => {} // expected + other => panic!("expected ConnectionFailed, got {:?}", other), + } + assert!(!transport.is_connected()); + } + + #[tokio::test] + async fn test_disconnect_without_connect() { + let mut transport = TcpTransport::new("127.0.0.1:8080".into()); + let result = transport.disconnect().await; + assert!(result.is_ok(), "disconnect without connect should be a safe no-op"); + assert!(!transport.is_connected()); + } + + // ── Async read/write with real connection ───────────────────────── + + #[tokio::test] + async fn test_async_read_write() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + // Server: accept one connection, write "hello", read 5 bytes back, verify "world" + let server_handle = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + let written = tokio::io::AsyncWriteExt::write_all(&mut stream, b"hello").await; + assert!(written.is_ok(), "server write_all hello failed"); + let mut buf = [0u8; 5]; + let read = tokio::io::AsyncReadExt::read_exact(&mut stream, &mut buf).await; + assert!(read.is_ok(), "server read_exact failed"); + assert_eq!(&buf, b"world"); + }); + + // Client: connect, read "hello", write "world", disconnect + let mut transport = TcpTransport::new(addr); + transport.connect().await.unwrap(); + + let mut read_buf = [0u8; 5]; + AsyncReadExt::read_exact(&mut transport, &mut read_buf) + .await + .unwrap(); + assert_eq!(&read_buf, b"hello"); + + AsyncWriteExt::write_all(&mut transport, b"world") + .await + .unwrap(); + + transport.disconnect().await.unwrap(); + + // Ensure server task completed without panic + server_handle.await.unwrap(); + } +} \ No newline at end of file diff --git a/crates/xserial-core/src/transport/udp.rs b/crates/xserial-core/src/transport/udp.rs new file mode 100644 index 0000000..971c625 --- /dev/null +++ b/crates/xserial-core/src/transport/udp.rs @@ -0,0 +1,426 @@ +use std::pin::Pin; +use std::task::{Context, Poll}; + +use async_trait::async_trait; +use tokio::io::{AsyncRead, AsyncWrite, ReadBuf}; +use tokio::net::UdpSocket; +use tracing::{debug, info}; + +use super::{Transport, TransportType}; +use crate::error::Result; + +#[derive(Debug)] +pub struct UdpTransport { + socket: Option, + bind_addr: String, + remote_addr: Option, + read_buf: Vec, + read_pos: usize, + + temp_recv_buf: Vec, +} + +impl UdpTransport { + pub fn new(bind_addr: String, remote_addr: Option) -> Self { + Self { + socket: None, + bind_addr, + remote_addr, + read_buf: Vec::new(), + read_pos: 0, + temp_recv_buf: vec![0u8; 65536], // 初始化一次,重复使用 + } + } + + pub fn bind_addr(&self) -> &str { + &self.bind_addr + } +} + +impl AsyncRead for UdpTransport { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let this = self.get_mut(); + + // 优先处理遗留的内部缓冲数据 + if this.read_pos < this.read_buf.len() { + let remaining = &this.read_buf[this.read_pos..]; + let to_copy = std::cmp::min(remaining.len(), buf.remaining()); + + buf.put_slice(&remaining[..to_copy]); + this.read_pos += to_copy; + + // 如果缓冲区数据已被全部读完,清空它以便后续复用,避免无止境增长 + if this.read_pos == this.read_buf.len() { + this.read_buf.clear(); + this.read_pos = 0; + } + return Poll::Ready(Ok(())); + } + + // 如果内部缓冲为空,尝试从 Socket 读取新的 UDP 包 + let socket = match this.socket.as_ref() { + Some(s) => s, + None => { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::NotConnected, + "UDP socket is not connected", + ))); + } + }; + + // 使用复用的 temp_recv_buf,包裹为 tokio 需要的 ReadBuf + let mut temp_buf = ReadBuf::new(&mut this.temp_recv_buf); + + // 如果配置了 remote_addr,说明 socket 已经 connect 过,可以用 poll_recv + // 否则只能用 poll_recv_from,丢弃掉远端地址信息 + let poll_result = if this.remote_addr.is_some() { + socket.poll_recv(cx, &mut temp_buf) + } else { + // poll_recv_from 会返回 (usize, SocketAddr),我们将其映射回统一的 () 类型 + socket.poll_recv_from(cx, &mut temp_buf).map(|res| res.map(|_| ())) + }; + + match poll_result { + Poll::Ready(Ok(())) => { + let filled = temp_buf.filled(); + let to_copy = std::cmp::min(filled.len(), buf.remaining()); + + buf.put_slice(&filled[..to_copy]); + + // 核心逻辑:如果本次读取到的 UDP 包大于外面提供的 buf 的剩余空间 + // 将没装下的剩余部分放进内部的 read_buf 留作下次 poll_read 使用 + if to_copy < filled.len() { + this.read_buf.clear(); // 确保安全清空 + this.read_buf.extend_from_slice(&filled[to_copy..]); + this.read_pos = 0; + } + + Poll::Ready(Ok(())) + } + Poll::Ready(Err(e)) => Poll::Ready(Err(e)), + Poll::Pending => Poll::Pending, + } + } +} + +impl AsyncWrite for UdpTransport { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + let this = self.get_mut(); + + let socket = match this.socket.as_ref() { + Some(s) => s, + None => { + return Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::NotConnected, + "UDP socket is not connected", + ))); + } + }; + + // AsyncWrite 是不带目标地址的流式写入接口。 + // 因此必须绑定了远端地址 (remote_addr) 才能确切知道把 UDP 包发送给谁。 + if this.remote_addr.is_some() { + socket.poll_send(cx, buf) + } else { + Poll::Ready(Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "Remote address must be set to use AsyncWrite for UDP", + ))) + } + } + + fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + // UDP 是无连接的数据报协议,没有缓冲区需要手动 flush + Poll::Ready(Ok(())) + } + + fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { + // UDP 没有 TCP 的四次挥手过程,直接 Ready + Poll::Ready(Ok(())) + } +} + +#[async_trait] +impl Transport for UdpTransport { + fn name(&self) -> &str { + &self.bind_addr + } + + fn transport_type(&self) -> TransportType { + TransportType::Udp + } + + fn is_connected(&self) -> bool { + self.socket.is_some() + } + + async fn connect(&mut self) -> Result<()> { + if self.is_connected() { + return Ok(()); + } + + info!("Binding UDP socket to {}", self.bind_addr); + + let socket = UdpSocket::bind(&self.bind_addr).await.map_err(|e| { + crate::error::Error::ConnectionFailed(format!( + "Failed to bind UDP {}: {}", + self.bind_addr, e + )) + })?; + + if let Some(ref remote) = self.remote_addr { + socket.connect(remote).await.map_err(|e| { + crate::error::Error::ConnectionFailed(format!( + "Failed to connect UDP to {}: {}", + remote, e + )) + })?; + debug!("UDP connected to {}", remote); + } + + debug!("UDP socket bound to {}", self.bind_addr); + self.socket = Some(socket); + Ok(()) + } + + async fn disconnect(&mut self) -> Result<()> { + if let Some(socket) = self.socket.take() { + debug!("Closing UDP socket {}", self.bind_addr); + drop(socket); + info!("UDP socket {} closed", self.bind_addr); + } + self.read_buf.clear(); + self.read_pos = 0; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::error::Error; + use std::sync::LazyLock; + use std::task::{RawWaker, RawWakerVTable, Waker}; + use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadBuf}; + use tokio::net::UdpSocket; + + static NOOP_VTABLE: RawWakerVTable = RawWakerVTable::new( + |_| RawWaker::new(std::ptr::null(), &NOOP_VTABLE), + |_| {}, + |_| {}, + |_| {}, + ); + + fn noop_waker() -> Waker { + unsafe { Waker::from_raw(RawWaker::new(std::ptr::null(), &NOOP_VTABLE)) } + } + + static NOOP_WAKER: LazyLock = LazyLock::new(noop_waker); + + fn noop_context() -> Context<'static> { + Context::from_waker(&NOOP_WAKER) + } + + #[test] + fn test_new_with_remote() { + let t = UdpTransport::new("127.0.0.1:9000".into(), Some("127.0.0.1:9001".into())); + assert_eq!(t.bind_addr(), "127.0.0.1:9000"); + } + + #[test] + fn test_new_without_remote() { + let t = UdpTransport::new("0.0.0.0:0".into(), None); + assert_eq!(t.bind_addr(), "0.0.0.0:0"); + } + + #[test] + fn test_name() { + let t = UdpTransport::new("192.168.1.1:8888".into(), None); + assert_eq!(t.name(), "192.168.1.1:8888"); + } + + #[test] + fn test_transport_type() { + let t = UdpTransport::new("127.0.0.1:0".into(), None); + assert_eq!(t.transport_type(), TransportType::Udp); + } + + #[test] + fn test_is_connected_false_by_default() { + let t = UdpTransport::new("127.0.0.1:0".into(), None); + assert!(!t.is_connected()); + } + + #[test] + fn test_poll_read_not_connected() { + let mut t = UdpTransport::new("127.0.0.1:0".into(), None); + let mut buf_data = [0u8; 64]; + let mut buf = ReadBuf::new(&mut buf_data); + let mut cx = noop_context(); + let result = Pin::new(&mut t).poll_read(&mut cx, &mut buf); + match result { + Poll::Ready(Err(ref e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotConnected), + other => panic!( + "expected Poll::Ready(Err(NotConnected)), got {other:?}" + ), + } + } + + #[test] + fn test_poll_write_not_connected() { + let mut t = UdpTransport::new("127.0.0.1:0".into(), None); + let mut cx = noop_context(); + let data = b"test"; + let result = Pin::new(&mut t).poll_write(&mut cx, data); + match result { + Poll::Ready(Err(ref e)) => assert_eq!(e.kind(), std::io::ErrorKind::NotConnected), + other => panic!( + "expected Poll::Ready(Err(NotConnected)), got {other:?}" + ), + } + } + + #[test] + fn test_poll_flush_not_connected() { + let mut t = UdpTransport::new("127.0.0.1:0".into(), None); + let mut cx = noop_context(); + let result = Pin::new(&mut t).poll_flush(&mut cx); + assert!(matches!(result, Poll::Ready(Ok(())))); + } + + #[test] + fn test_poll_shutdown_not_connected() { + let mut t = UdpTransport::new("127.0.0.1:0".into(), None); + let mut cx = noop_context(); + let result = Pin::new(&mut t).poll_shutdown(&mut cx); + assert!(matches!(result, Poll::Ready(Ok(())))); + } + + #[tokio::test] + async fn test_connect_and_disconnect_without_remote() { + let mut t = UdpTransport::new("127.0.0.1:0".into(), None); + assert!(!t.is_connected()); + + t.connect().await.expect("connect should succeed"); + assert!(t.is_connected()); + + t.disconnect().await.expect("disconnect should succeed"); + assert!(!t.is_connected()); + } + + #[tokio::test] + async fn test_connect_and_disconnect_with_remote() { + let remote = UdpSocket::bind("127.0.0.1:0").await.expect("remote bind"); + let remote_addr = remote.local_addr().unwrap().to_string(); + + let mut t = UdpTransport::new("127.0.0.1:0".into(), Some(remote_addr)); + t.connect().await.expect("connect with remote should succeed"); + assert!(t.is_connected()); + + t.disconnect().await.expect("disconnect should succeed"); + assert!(!t.is_connected()); + } + + #[tokio::test] + async fn test_double_connect_is_noop() { + let mut t = UdpTransport::new("127.0.0.1:0".into(), None); + t.connect().await.expect("first connect"); + assert!(t.is_connected()); + + t.connect().await.expect("second connect (idempotent)"); + assert!(t.is_connected()); + } + + #[tokio::test] + async fn test_connect_bind_failure() { + let mut t = UdpTransport::new("invalid_addr".into(), None); + let result = t.connect().await; + match result { + Err(Error::ConnectionFailed(_)) => {} + other => panic!("expected ConnectionFailed error, got {other:?}"), + } + } + + #[tokio::test] + async fn test_async_read_write_with_remote() { + let remote = UdpSocket::bind("127.0.0.1:0").await.expect("remote bind"); + let remote_addr = remote.local_addr().unwrap().to_string(); + + let mut t = UdpTransport::new("127.0.0.1:0".into(), Some(remote_addr)); + t.connect().await.expect("connect"); + + AsyncWriteExt::write_all(&mut t, b"hello") + .await + .expect("write_all hello"); + + let mut buf = [0u8; 1024]; + let (n, transport_addr) = remote.recv_from(&mut buf).await.expect("remote recv_from"); + assert_eq!(&buf[..n], b"hello"); + + remote + .send_to(b"world", transport_addr) + .await + .expect("remote send_to"); + + let mut read_buf = [0u8; 5]; + AsyncReadExt::read_exact(&mut t, &mut read_buf) + .await + .expect("read_exact world"); + assert_eq!(&read_buf, b"world"); + } + + #[tokio::test] + async fn test_write_fails_without_remote() { + let mut t = UdpTransport::new("127.0.0.1:0".into(), None); + t.connect().await.expect("connect (binds but no remote)"); + + let result = AsyncWriteExt::write_all(&mut t, b"test").await; + assert!(result.is_err()); + let err = result.unwrap_err(); + assert_eq!(err.kind(), std::io::ErrorKind::InvalidInput); + } + + #[tokio::test] + async fn test_oversized_datagram_buffering() { + let remote = UdpSocket::bind("127.0.0.1:0").await.expect("remote bind"); + let remote_addr = remote.local_addr().unwrap().to_string(); + + let mut t = UdpTransport::new("127.0.0.1:0".into(), Some(remote_addr)); + t.connect().await.expect("connect"); + + AsyncWriteExt::write_all(&mut t, b"x") + .await + .expect("write ping"); + let mut ping_buf = [0u8; 1]; + let (_, transport_addr) = remote + .recv_from(&mut ping_buf) + .await + .expect("remote recv ping"); + + let large_data: Vec = vec![b'A'; 100]; + remote + .send_to(&large_data, transport_addr) + .await + .expect("send large datagram"); + + let mut small_buf = [0u8; 10]; + AsyncReadExt::read_exact(&mut t, &mut small_buf) + .await + .expect("read 10 bytes"); + assert_eq!(&small_buf, b"AAAAAAAAAA"); + + let mut rest_buf = [0u8; 90]; + AsyncReadExt::read_exact(&mut t, &mut rest_buf) + .await + .expect("read 90 bytes"); + assert_eq!(&rest_buf[..], &vec![b'A'; 90][..]); + } +} diff --git a/crates/xserial-core/tests/pipeline.rs b/crates/xserial-core/tests/pipeline.rs new file mode 100644 index 0000000..bac110b --- /dev/null +++ b/crates/xserial-core/tests/pipeline.rs @@ -0,0 +1,775 @@ +use std::time::Duration; + +use tokio::io::{AsyncReadExt, AsyncWriteExt}; +use tokio::net::{TcpListener, UdpSocket}; +use tokio::time::timeout; + +use xserial_core::frame::{ + cobs::CobsFramer, + fixed::FixedLengthFramer, + length::{LengthConfig, LengthPrefixedFramer}, + line::{LineConfig, LineFramer}, + Endian, Framer, +}; +use xserial_core::protocol::{ + hex::{HexConfig, HexDecoder}, + plot::{PlotConfig, PlotDecoder, PlotFormat, SampleType}, + text::{TextDecoder, TextEncoding}, + DecodedData, ProtocolDecoder, +}; +use xserial_core::transport::{Connection, TransportConfig, TransportType}; + +const TEST_TIMEOUT: Duration = Duration::from_secs(5); + +async fn read_to_framer( + conn: &mut Connection, + framer: &mut dyn Framer, +) -> Vec> { + let mut buf = [0u8; 4096]; + let mut all_frames = Vec::new(); + + loop { + match timeout(TEST_TIMEOUT, AsyncReadExt::read(conn, &mut buf)).await { + Ok(Ok(0)) => break, + Ok(Ok(n)) => { + all_frames.extend(framer.feed(&buf[..n])); + } + Ok(Err(_)) => break, + Err(_) => break, + } + } + + if let Some(rest) = framer.flush() { + all_frames.push(rest); + } + all_frames +} + +// ══════════════════════════════════════════════════════════════════════ +// TCP + LineFramer + TextDecoder +// ══════════════════════════════════════════════════════════════════════ + +#[tokio::test] +async fn tcp_line_text_utf8() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let server = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + stream.write_all(b"hello\nworld\n").await.unwrap(); + }); + + let mut conn = Connection::new(TransportConfig::Tcp { addr }); + conn.connect().await.unwrap(); + + let mut framer = LineFramer::new(LineConfig::default()); + let frames = read_to_framer(&mut conn, &mut framer).await; + conn.disconnect().await.unwrap(); + + let decoder = TextDecoder::new(TextEncoding::Utf8); + let results: Vec = frames + .iter() + .filter_map(|f| decoder.decode(f)) + .filter_map(|d| match d { + DecodedData::Text(s) => Some(s), + _ => None, + }) + .collect(); + + assert_eq!(results, vec!["hello", "world"]); + server.await.unwrap(); +} + +#[tokio::test] +async fn tcp_line_text_crlf_stripping() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let server = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + stream.write_all(b"line1\r\nline2\r\n").await.unwrap(); + }); + + let mut conn = Connection::new(TransportConfig::Tcp { addr }); + conn.connect().await.unwrap(); + + let mut framer = LineFramer::new(LineConfig::default()); + let frames = read_to_framer(&mut conn, &mut framer).await; + conn.disconnect().await.unwrap(); + + let decoder = TextDecoder::new(TextEncoding::Utf8); + let results: Vec = frames + .iter() + .filter_map(|f| decoder.decode(f)) + .filter_map(|d| match d { + DecodedData::Text(s) => Some(s), + _ => None, + }) + .collect(); + + assert_eq!(results, vec!["line1", "line2"]); + server.await.unwrap(); +} + +#[tokio::test] +async fn tcp_line_text_chinese_utf8() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let server = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + stream.write_all("你好\n世界\n".as_bytes()).await.unwrap(); + }); + + let mut conn = Connection::new(TransportConfig::Tcp { addr }); + conn.connect().await.unwrap(); + + let mut framer = LineFramer::new(LineConfig::default()); + let frames = read_to_framer(&mut conn, &mut framer).await; + conn.disconnect().await.unwrap(); + + let decoder = TextDecoder::new(TextEncoding::Utf8); + let results: Vec = frames + .iter() + .filter_map(|f| decoder.decode(f)) + .filter_map(|d| match d { + DecodedData::Text(s) => Some(s), + _ => None, + }) + .collect(); + + assert_eq!(results, vec!["你好", "世界"]); + server.await.unwrap(); +} + +// ══════════════════════════════════════════════════════════════════════ +// TCP + LengthPrefixedFramer + HexDecoder +// ══════════════════════════════════════════════════════════════════════ + +#[tokio::test] +async fn tcp_length_hex() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let server = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + // Frame 1: 3 bytes "foo" + stream.write_all(&3u16.to_be_bytes()).await.unwrap(); + stream.write_all(b"foo").await.unwrap(); + // Frame 2: 3 bytes "bar" + stream.write_all(&3u16.to_be_bytes()).await.unwrap(); + stream.write_all(b"bar").await.unwrap(); + }); + + let mut conn = Connection::new(TransportConfig::Tcp { addr }); + conn.connect().await.unwrap(); + + let mut framer = LengthPrefixedFramer::new(LengthConfig::default()); + let frames = read_to_framer(&mut conn, &mut framer).await; + conn.disconnect().await.unwrap(); + + let decoder = HexDecoder::new(HexConfig::default()); + let results: Vec = frames + .iter() + .filter_map(|f| decoder.decode(f)) + .filter_map(|d| match d { + DecodedData::Hex(s) => Some(s), + _ => None, + }) + .collect(); + + assert_eq!(results, vec!["66 6f 6f", "62 61 72"]); // "foo", "bar" in hex + server.await.unwrap(); +} + +#[tokio::test] +async fn tcp_length_hex_little_endian() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let server = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + stream.write_all(&3u16.to_le_bytes()).await.unwrap(); + stream.write_all(b"xyz").await.unwrap(); + }); + + let mut conn = Connection::new(TransportConfig::Tcp { addr }); + conn.connect().await.unwrap(); + + let mut framer = LengthPrefixedFramer::new(LengthConfig { + endian: Endian::Little, + ..LengthConfig::default() + }); + let frames = read_to_framer(&mut conn, &mut framer).await; + conn.disconnect().await.unwrap(); + + assert_eq!(frames.len(), 1); + let decoder = HexDecoder::new(HexConfig::default()); + let result = decoder.decode(&frames[0]).unwrap(); + assert!(matches!(result, DecodedData::Hex(ref s) if s == "78 79 7a")); + server.await.unwrap(); +} + +// ══════════════════════════════════════════════════════════════════════ +// TCP + FixedLengthFramer + PlotDecoder +// ══════════════════════════════════════════════════════════════════════ + +#[tokio::test] +async fn tcp_fixed_plot_f32() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let server = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + // 3 samples × 4 bytes each = 12 bytes per frame + let samples: [f32; 3] = [1.0, -2.5, 3.14]; + let mut data = Vec::new(); + for s in &samples { + data.extend_from_slice(&s.to_le_bytes()); + } + stream.write_all(&data).await.unwrap(); + }); + + let mut conn = Connection::new(TransportConfig::Tcp { addr }); + conn.connect().await.unwrap(); + + let mut framer = FixedLengthFramer::new(12); // 3 × f32 + let frames = read_to_framer(&mut conn, &mut framer).await; + conn.disconnect().await.unwrap(); + + let decoder = PlotDecoder::new(PlotConfig { + sample_type: SampleType::F32, + endian: Endian::Little, + channels: 1, + format: PlotFormat::Interleaved, + }); + + assert_eq!(frames.len(), 1); + let result = decoder.decode(&frames[0]).unwrap(); + match result { + DecodedData::Plot(frame) => { + assert_eq!(frame.channels.len(), 1); + assert_eq!(frame.channels[0].len(), 3); + assert!((frame.channels[0][0] - 1.0).abs() < 1e-5); + assert!((frame.channels[0][1] - (-2.5)).abs() < 1e-5); + assert!((frame.channels[0][2] - 3.14).abs() < 1e-5); + } + other => panic!("expected Plot, got {:?}", other), + } + server.await.unwrap(); +} + +#[tokio::test] +async fn tcp_fixed_plot_two_channel_u16() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let server = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + // 2 channels × 2 samples each × 2 bytes = 8 bytes + let samples: [u16; 4] = [100, 200, 300, 400]; + let mut data = Vec::new(); + for s in &samples { + data.extend_from_slice(&s.to_le_bytes()); + } + stream.write_all(&data).await.unwrap(); + }); + + let mut conn = Connection::new(TransportConfig::Tcp { addr }); + conn.connect().await.unwrap(); + + let mut framer = FixedLengthFramer::new(8); + let frames = read_to_framer(&mut conn, &mut framer).await; + conn.disconnect().await.unwrap(); + + let decoder = PlotDecoder::new(PlotConfig { + sample_type: SampleType::U16, + endian: Endian::Little, + channels: 2, + format: PlotFormat::Interleaved, + }); + + assert_eq!(frames.len(), 1); + let result = decoder.decode(&frames[0]).unwrap(); + match result { + DecodedData::Plot(frame) => { + assert_eq!(frame.channels.len(), 2); + assert_eq!(frame.channels[0], vec![100.0, 300.0]); + assert_eq!(frame.channels[1], vec![200.0, 400.0]); + } + other => panic!("expected Plot, got {:?}", other), + } + server.await.unwrap(); +} + +// ══════════════════════════════════════════════════════════════════════ +// UDP + CobsFramer + TextDecoder +// ══════════════════════════════════════════════════════════════════════ + +fn cobs_encode(data: &[u8]) -> Vec { + if data.is_empty() { + return vec![0x01, 0x00]; + } + let mut out = Vec::new(); + let mut block_start = 0; + for (i, &byte) in data.iter().enumerate() { + if byte == 0x00 || i - block_start == 254 { + let len = i - block_start + 1; + out.push(len as u8); + out.extend_from_slice(&data[block_start..i]); + block_start = i + if byte == 0x00 { 1 } else { 0 }; + } + } + if block_start < data.len() { + let len = data.len() - block_start + 1; + out.push(len as u8); + out.extend_from_slice(&data[block_start..]); + } + out.push(0x00); + out +} + +#[tokio::test] +async fn udp_cobs_text() { + let remote = UdpSocket::bind("127.0.0.1:0").await.unwrap(); + let remote_addr = remote.local_addr().unwrap().to_string(); + + let mut conn = Connection::new(TransportConfig::Udp { + bind_addr: "127.0.0.1:0".into(), + remote_addr: Some(remote_addr.clone()), + }); + conn.connect().await.unwrap(); + + // Send a probe so the remote learns our actual port + AsyncWriteExt::write_all(&mut conn, b"x").await.unwrap(); + let mut probe_buf = [0u8; 1]; + let (_, conn_actual_addr) = remote.recv_from(&mut probe_buf).await.unwrap(); + + // Send COBS-encoded frames to the connection's actual port + let packet1 = cobs_encode(b"hello"); + let packet2 = cobs_encode(b"world"); + let mut combined = packet1; + combined.extend_from_slice(&packet2); + remote.send_to(&combined, conn_actual_addr).await.unwrap(); + + let mut framer = CobsFramer::default(); + let frames = read_to_framer(&mut conn, &mut framer).await; + conn.disconnect().await.unwrap(); + + let decoder = TextDecoder::new(TextEncoding::Utf8); + let results: Vec = frames + .iter() + .filter_map(|f| decoder.decode(f)) + .filter_map(|d| match d { + DecodedData::Text(s) => Some(s), + _ => None, + }) + .collect(); + + assert_eq!(results, vec!["hello", "world"]); +} + +// ══════════════════════════════════════════════════════════════════════ +// Connection lifecycle: connect → use → disconnect → reconnect → use +// ══════════════════════════════════════════════════════════════════════ + +#[tokio::test] +async fn connection_reconnect_tcp() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + // First connection + let server1 = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + stream.write_all(b"first\n").await.unwrap(); + }); + + let mut conn = Connection::new(TransportConfig::Tcp { + addr: addr.clone(), + }); + conn.connect().await.unwrap(); + + let mut framer = LineFramer::new(LineConfig::default()); + let frames = read_to_framer(&mut conn, &mut framer).await; + conn.disconnect().await.unwrap(); + server1.await.unwrap(); + + let decoder = TextDecoder::new(TextEncoding::Utf8); + let results: Vec = frames + .iter() + .filter_map(|f| decoder.decode(f)) + .filter_map(|d| match d { + DecodedData::Text(s) => Some(s), + _ => None, + }) + .collect(); + assert_eq!(results, vec!["first"]); + + // Framer state should not carry over (it was fully consumed) + framer.reset(); + + // Second connection + let listener2 = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr2 = listener2.local_addr().unwrap().to_string(); + + let server2 = tokio::spawn(async move { + let (mut stream, _) = listener2.accept().await.unwrap(); + stream.write_all(b"second\n").await.unwrap(); + }); + + let mut conn2 = Connection::new(TransportConfig::Tcp { addr: addr2 }); + conn2.connect().await.unwrap(); + + let frames = read_to_framer(&mut conn2, &mut framer).await; + conn2.disconnect().await.unwrap(); + server2.await.unwrap(); + + let results: Vec = frames + .iter() + .filter_map(|f| decoder.decode(f)) + .filter_map(|d| match d { + DecodedData::Text(s) => Some(s), + _ => None, + }) + .collect(); + assert_eq!(results, vec!["second"]); +} + +// ══════════════════════════════════════════════════════════════════════ +// Framer reset mid-stream +// ══════════════════════════════════════════════════════════════════════ + +#[tokio::test] +async fn framer_reset_mid_stream() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let server = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + stream.write_all(b"garbage__data\nvalid\n").await.unwrap(); + }); + + let mut conn = Connection::new(TransportConfig::Tcp { addr }); + conn.connect().await.unwrap(); + + let mut framer = LineFramer::new(LineConfig::default()); + + // Read exactly "garbage_" (8 bytes) — no newline yet + let mut buf = [0u8; 8]; + let n = timeout(TEST_TIMEOUT, AsyncReadExt::read(&mut conn, &mut buf)) + .await + .unwrap() + .unwrap(); + let frames = framer.feed(&buf[..n]); + assert!(frames.is_empty()); + assert!(framer.pending_len() > 0); + + // Reset — discard the incomplete "garbage_" + framer.reset(); + assert_eq!(framer.pending_len(), 0); + + // Read remaining data + let frames = read_to_framer(&mut conn, &mut framer).await; + conn.disconnect().await.unwrap(); + server.await.unwrap(); + + let decoder = TextDecoder::new(TextEncoding::Utf8); + let results: Vec = frames + .iter() + .filter_map(|f| decoder.decode(f)) + .filter_map(|d| match d { + DecodedData::Text(s) => Some(s), + _ => None, + }) + .collect(); + + assert_eq!(results, vec!["_data", "valid"]); +} + +// ══════════════════════════════════════════════════════════════════════ +// Multi-frame burst + edge cases +// ══════════════════════════════════════════════════════════════════════ + +#[tokio::test] +async fn tcp_many_frames_burst() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let server = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + // 50 lines in one write + let mut data = String::new(); + for i in 0..50 { + data.push_str(&format!("line_{}\n", i)); + } + stream.write_all(data.as_bytes()).await.unwrap(); + }); + + let mut conn = Connection::new(TransportConfig::Tcp { addr }); + conn.connect().await.unwrap(); + + let mut framer = LineFramer::new(LineConfig::default()); + let frames = read_to_framer(&mut conn, &mut framer).await; + conn.disconnect().await.unwrap(); + server.await.unwrap(); + + assert_eq!(frames.len(), 50); + + let decoder = TextDecoder::new(TextEncoding::Utf8); + let results: Vec = frames + .iter() + .filter_map(|f| decoder.decode(f)) + .filter_map(|d| match d { + DecodedData::Text(s) => Some(s), + _ => None, + }) + .collect(); + + assert_eq!(results.len(), 50); + for (i, line) in results.iter().enumerate() { + assert_eq!(line, &format!("line_{}", i)); + } +} + +#[tokio::test] +async fn tcp_trailing_data_flushed_on_disconnect() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let server = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + stream.write_all(b"no_newline_at_end").await.unwrap(); + // Close without sending \n + }); + + let mut conn = Connection::new(TransportConfig::Tcp { addr }); + conn.connect().await.unwrap(); + + let mut framer = LineFramer::new(LineConfig::default()); + let frames = read_to_framer(&mut conn, &mut framer).await; + conn.disconnect().await.unwrap(); + server.await.unwrap(); + + // flush() in read_to_framer should capture the trailing data + assert_eq!(frames.len(), 1); + assert_eq!(frames[0], b"no_newline_at_end"); +} + +#[tokio::test] +async fn empty_data_produces_no_frames() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let server = tokio::spawn(async move { + let (stream, _) = listener.accept().await.unwrap(); + drop(stream); + }); + + let mut conn = Connection::new(TransportConfig::Tcp { addr }); + conn.connect().await.unwrap(); + + let mut framer = LineFramer::new(LineConfig::default()); + let frames = read_to_framer(&mut conn, &mut framer).await; + conn.disconnect().await.unwrap(); + server.await.unwrap(); + + assert!(frames.is_empty()); +} + +#[tokio::test] +async fn protocol_switching_on_same_connection() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let server = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + // Send text line then length-prefixed binary + stream.write_all(b"text_mode\n").await.unwrap(); + stream.write_all(&3u16.to_be_bytes()).await.unwrap(); + stream.write_all(b"bin").await.unwrap(); + }); + + let mut conn = Connection::new(TransportConfig::Tcp { addr }); + conn.connect().await.unwrap(); + + // Phase 1: text protocol + let mut line_framer = LineFramer::new(LineConfig::default()); + let buf = read_chunk(&mut conn).await; + let frames = line_framer.feed(&buf); + + let text_decoder = TextDecoder::new(TextEncoding::Utf8); + let results: Vec<_> = frames + .iter() + .filter_map(|f| text_decoder.decode(f)) + .collect(); + assert_eq!(results.len(), 1); + assert!(matches!(&results[0], DecodedData::Text(s) if s == "text_mode")); + + // Phase 2: feed leftovers + remaining data to length-prefixed framer + let mut len_framer = LengthPrefixedFramer::new(LengthConfig::default()); + let mut frames = if let Some(rest) = line_framer.flush() { + len_framer.feed(&rest) + } else { + Vec::new() + }; + let rest_buf = read_remaining(&mut conn).await; + frames.extend(len_framer.feed(&rest_buf)); + + let hex_decoder = HexDecoder::new(HexConfig::default()); + let results: Vec<_> = frames + .iter() + .filter_map(|f| hex_decoder.decode(f)) + .collect(); + assert_eq!(results.len(), 1); + assert!(matches!(&results[0], DecodedData::Hex(s) if s == "62 69 6e")); + server.await.unwrap(); +} + +// ══════════════════════════════════════════════════════════════════════ +// TransportType dispatch via Connection enum +// ══════════════════════════════════════════════════════════════════════ + +#[tokio::test] +async fn connection_transport_type_dispatch() { + let serial = Connection::new(TransportConfig::Serial { + port: "COM1".into(), + baud_rate: 115200, + }); + let tcp = Connection::new(TransportConfig::Tcp { + addr: "127.0.0.1:8080".into(), + }); + let udp = Connection::new(TransportConfig::Udp { + bind_addr: "0.0.0.0:0".into(), + remote_addr: None, + }); + + assert_eq!(serial.transport_type(), TransportType::Serial); + assert_eq!(tcp.transport_type(), TransportType::Tcp); + assert_eq!(udp.transport_type(), TransportType::Udp); +} + +// ══════════════════════════════════════════════════════════════════════ +// Multiple framers, one connection +// ══════════════════════════════════════════════════════════════════════ + +#[tokio::test] +async fn multiple_framers_one_connection() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let server = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + stream.write_all(b"a\nbb\nccc\n").await.unwrap(); + }); + + let mut conn = Connection::new(TransportConfig::Tcp { addr }); + conn.connect().await.unwrap(); + + let mut buf = [0u8; 256]; + let n = AsyncReadExt::read(&mut conn, &mut buf).await.unwrap(); + conn.disconnect().await.unwrap(); + server.await.unwrap(); + + let data = &buf[..n]; + + // Framer 1: LineFramer + let mut line = LineFramer::new(LineConfig::default()); + let line_frames = line.feed(data); + assert_eq!(line_frames.len(), 3); + + // Framer 2: FixedLengthFramer (same bytes, different interpretation) + let mut fixed = FixedLengthFramer::new(3); + let fixed_frames = fixed.feed(data); + assert_eq!(fixed_frames.len(), 3); + assert_eq!(fixed_frames[0], b"a\nb"); + assert_eq!(fixed_frames[1], b"b\nc"); + assert_eq!(fixed_frames[2], b"cc\n"); +} + +// ══════════════════════════════════════════════════════════════════════ +// Robustness: partial writes, slow consumer +// ══════════════════════════════════════════════════════════════════════ + +#[tokio::test] +async fn tcp_partial_writes_line_framing() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let server = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + // Simulate fragmented writes + stream.write_all(b"hel").await.unwrap(); + tokio::time::sleep(Duration::from_millis(10)).await; + stream.write_all(b"lo\nwor").await.unwrap(); + tokio::time::sleep(Duration::from_millis(10)).await; + stream.write_all(b"ld\n").await.unwrap(); + }); + + let mut conn = Connection::new(TransportConfig::Tcp { addr }); + conn.connect().await.unwrap(); + + let mut framer = LineFramer::new(LineConfig::default()); + let frames = read_to_framer(&mut conn, &mut framer).await; + conn.disconnect().await.unwrap(); + server.await.unwrap(); + + let decoder = TextDecoder::new(TextEncoding::Utf8); + let results: Vec = frames + .iter() + .filter_map(|f| decoder.decode(f)) + .filter_map(|d| match d { + DecodedData::Text(s) => Some(s), + _ => None, + }) + .collect(); + + assert_eq!(results, vec!["hello", "world"]); +} + +#[tokio::test] +async fn decode_summary_on_full_pipeline() { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap().to_string(); + + let server = tokio::spawn(async move { + let (mut stream, _) = listener.accept().await.unwrap(); + stream.write_all(b"hello\n").await.unwrap(); + }); + + let mut conn = Connection::new(TransportConfig::Tcp { addr }); + conn.connect().await.unwrap(); + + let mut framer = LineFramer::new(LineConfig::default()); + let frames = read_to_framer(&mut conn, &mut framer).await; + conn.disconnect().await.unwrap(); + server.await.unwrap(); + + let decoder = TextDecoder::new(TextEncoding::Utf8); + let decoded = decoder.decode(&frames[0]).unwrap(); + assert_eq!(decoded.summary(), "hello"); +} + +// ══════════════════════════════════════════════════════════════════════ +// Helpers +// ══════════════════════════════════════════════════════════════════════ + +async fn read_chunk(conn: &mut Connection) -> Vec { + let mut buf = [0u8; 4096]; + match timeout(TEST_TIMEOUT, AsyncReadExt::read(conn, &mut buf)).await { + Ok(Ok(n)) => buf[..n].to_vec(), + _ => Vec::new(), + } +} + +async fn read_remaining(conn: &mut Connection) -> Vec { + let mut all = Vec::new(); + let mut buf = [0u8; 4096]; + loop { + match timeout(TEST_TIMEOUT, AsyncReadExt::read(conn, &mut buf)).await { + Ok(Ok(0)) => break, + Ok(Ok(n)) => all.extend_from_slice(&buf[..n]), + _ => break, + } + } + all +} diff --git a/crates/xserial-gui/Cargo.toml b/crates/xserial-gui/Cargo.toml new file mode 100644 index 0000000..6f5f272 --- /dev/null +++ b/crates/xserial-gui/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "xserial-gui" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "xserial-gui" +path = "src/main.rs" + +[dependencies] +xserial-core = { path = "../xserial-core" } +xserial-client = { path = "../xserial-client" } +tokio = { workspace = true } +egui = { workspace = true } +eframe = { workspace = true } +egui_plot = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } diff --git a/crates/xserial-gui/src/main.rs b/crates/xserial-gui/src/main.rs new file mode 100644 index 0000000..63a82f5 --- /dev/null +++ b/crates/xserial-gui/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + println!("xserial-gui"); +} diff --git a/crates/xserial-tui/Cargo.toml b/crates/xserial-tui/Cargo.toml new file mode 100644 index 0000000..288dd75 --- /dev/null +++ b/crates/xserial-tui/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "xserial-tui" +version = "0.1.0" +edition = "2024" + +[[bin]] +name = "xserial-tui" +path = "src/main.rs" + +[dependencies] +xserial-core = { path = "../xserial-core" } +xserial-client = { path = "../xserial-client" } +tokio = { workspace = true } +ratatui = { workspace = true } +crossterm = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } +tracing-appender = { workspace = true } +clap = { workspace = true } +hex = { workspace = true } +image = { workspace = true } diff --git a/crates/xserial-tui/src/main.rs b/crates/xserial-tui/src/main.rs new file mode 100644 index 0000000..8a55e80 --- /dev/null +++ b/crates/xserial-tui/src/main.rs @@ -0,0 +1,3 @@ +fn main() { + +} \ No newline at end of file diff --git a/tools/gen_plot.py b/tools/gen_plot.py new file mode 100755 index 0000000..6553978 --- /dev/null +++ b/tools/gen_plot.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python3 +""" +SerialPlot 协议测试数据生成器 + +协议格式: + [Sync: AA 55] [Size: 2B LE] [Payload: N×2B i16 LE] [Checksum: 1B LSB sum of payload] + +用法: + python3 gen_plot.py /dev/pts/X --channels 2 --wave sine + python3 gen_plot.py /dev/pts/X --channels 2 --wave sine --freq 0.3 # 慢波,易观察 + python3 gen_plot.py /dev/pts/X --channels 3 --wave saw --freq 0.2 +""" +import struct +import sys +import time +import math +import argparse + + +def checksum(data: bytes) -> int: + return sum(data) & 0xFF + + +def make_frame(channel_data: list[list[int]]) -> bytes: + num_channels = len(channel_data) + num_samples = len(channel_data[0]) + payload = bytearray() + for i in range(num_samples): + for ch in range(num_channels): + payload.extend(struct.pack('> 8) & 0xFF) + frame.extend(payload) + frame.append(checksum(payload)) + return bytes(frame) + + +def generate_wave( + num_channels: int, + num_samples: int, + wave_type: str, + freq: float, + amplitude: int, +) -> list[list[int]]: + channels = [] + for ch in range(num_channels): + phase_offset = ch * (2 * math.pi / num_channels) + samples = [] + for i in range(num_samples): + t = i / num_samples + if wave_type == 'sine': + angle = 2 * math.pi * t * freq + phase_offset + value = math.sin(angle) + elif wave_type == 'saw': + value = 2 * ((t * freq) % 1.0) - 1.0 + elif wave_type == 'square': + value = 1.0 if ((t * freq) % 2.0) < 1.0 else -1.0 + else: + value = 0.0 + samples.append(int(value * amplitude)) + channels.append(samples) + return channels + + +def main(): + parser = argparse.ArgumentParser(description='SerialPlot test data generator') + parser.add_argument('port', help='Serial port or file (e.g. /dev/pts/3)') + parser.add_argument('--channels', type=int, default=2, help='Number of channels') + parser.add_argument('--samples', type=int, default=200, help='Samples per frame') + parser.add_argument('--wave', choices=['sine', 'saw', 'square'], default='sine') + parser.add_argument('--freq', type=float, default=0.5, + help='Wave frequency (cycles per frame; 0.5 = half a sine wave, easy to observe)') + parser.add_argument('--amplitude', type=int, default=8000, + help='Amplitude (i16 range, max 32767)') + parser.add_argument('--interval', type=float, default=0.05, + help='Interval between frames (seconds)') + parser.add_argument('--frames', type=int, default=0, + help='Number of frames (0=infinite)') + args = parser.parse_args() + + count = 0 + try: + with open(args.port, 'wb') as f: + print(f"Generating {args.channels}-channel {args.wave} wave → {args.port}") + print(f" {args.samples} samples/frame, freq={args.freq} cycles/frame") + print(f" amplitude={args.amplitude}, interval={args.interval}s") + print(f" Protocol: sync=AA55, i16 LE, 1B size+checksum") + print() + + while args.frames == 0 or count < args.frames: + count += 1 + data = generate_wave(args.channels, args.samples, args.wave, args.freq, args.amplitude) + frame = make_frame(data) + f.write(frame) + f.flush() + + vals = [data[ch][0] for ch in range(args.channels)] + vals_str = ", ".join(f"ch{ch}={v:+6d}" for ch, v in enumerate(vals)) + print(f" Frame #{count}: {vals_str} ... ({len(frame)} bytes)", end='\r') + + time.sleep(args.interval) + except KeyboardInterrupt: + print(f"\nDone. {count} frames sent.") + except Exception as e: + print(f"Error: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == '__main__': + main()