From b166f13d5943fc69cd4df73e6bb18790629539f9 Mon Sep 17 00:00:00 2001 From: ospab Date: Sun, 12 Jul 2026 00:36:38 +0300 Subject: [PATCH] chore: remove dnstt, netstack-smoltcp, and ostp-web and add to gitignore --- .gitignore | 2 + dnstt | 1 - netstack-smoltcp/.cargo-ok | 1 - netstack-smoltcp/.cargo_vcs_info.json | 6 - .../.github/actions/ndk-dev-rs/action.yml | 37 -- netstack-smoltcp/.github/workflows/main.yml | 80 --- .../.github/workflows/publish.yml | 15 - netstack-smoltcp/.gitignore | 9 - netstack-smoltcp/Cargo.toml | 136 ----- netstack-smoltcp/Cargo.toml.orig | 51 -- netstack-smoltcp/LICENSE-APACHE | 201 ------- netstack-smoltcp/LICENSE-MIT | 25 - netstack-smoltcp/README.md | 136 ----- .../examples/forward-offload-linux.rs | 239 -------- netstack-smoltcp/examples/forward.rs | 326 ---------- netstack-smoltcp/scripts/bench-offload.sh | 174 ------ netstack-smoltcp/scripts/route-linux.sh | 26 - netstack-smoltcp/scripts/route-macos.sh | 36 -- netstack-smoltcp/scripts/route-windows.ps1 | 30 - netstack-smoltcp/src/device.rs | 109 ---- netstack-smoltcp/src/filter.rs | 56 -- netstack-smoltcp/src/lib.rs | 22 - netstack-smoltcp/src/packet.rs | 53 -- netstack-smoltcp/src/runner.rs | 42 -- netstack-smoltcp/src/stack.rs | 279 --------- netstack-smoltcp/src/tcp.rs | 564 ------------------ netstack-smoltcp/src/udp.rs | 155 ----- netstack-smoltcp/tests/regression.rs | 75 --- ostp-web/dist/assets/index-CBAXD1xO.css | 2 - ostp-web/dist/assets/index-DnZZkzK0.js | 19 - ostp-web/dist/favicon.png | Bin 25460 -> 0 bytes ostp-web/dist/favicon.svg | 12 - ostp-web/dist/icons.svg | 24 - ostp-web/dist/index.html | 19 - ostp-web/dist/logo.svg | 12 - 35 files changed, 2 insertions(+), 2972 deletions(-) delete mode 160000 dnstt delete mode 100644 netstack-smoltcp/.cargo-ok delete mode 100644 netstack-smoltcp/.cargo_vcs_info.json delete mode 100644 netstack-smoltcp/.github/actions/ndk-dev-rs/action.yml delete mode 100644 netstack-smoltcp/.github/workflows/main.yml delete mode 100644 netstack-smoltcp/.github/workflows/publish.yml delete mode 100644 netstack-smoltcp/.gitignore delete mode 100644 netstack-smoltcp/Cargo.toml delete mode 100644 netstack-smoltcp/Cargo.toml.orig delete mode 100644 netstack-smoltcp/LICENSE-APACHE delete mode 100644 netstack-smoltcp/LICENSE-MIT delete mode 100644 netstack-smoltcp/README.md delete mode 100644 netstack-smoltcp/examples/forward-offload-linux.rs delete mode 100644 netstack-smoltcp/examples/forward.rs delete mode 100644 netstack-smoltcp/scripts/bench-offload.sh delete mode 100644 netstack-smoltcp/scripts/route-linux.sh delete mode 100644 netstack-smoltcp/scripts/route-macos.sh delete mode 100644 netstack-smoltcp/scripts/route-windows.ps1 delete mode 100644 netstack-smoltcp/src/device.rs delete mode 100644 netstack-smoltcp/src/filter.rs delete mode 100644 netstack-smoltcp/src/lib.rs delete mode 100644 netstack-smoltcp/src/packet.rs delete mode 100644 netstack-smoltcp/src/runner.rs delete mode 100644 netstack-smoltcp/src/stack.rs delete mode 100644 netstack-smoltcp/src/tcp.rs delete mode 100644 netstack-smoltcp/src/udp.rs delete mode 100644 netstack-smoltcp/tests/regression.rs delete mode 100644 ostp-web/dist/assets/index-CBAXD1xO.css delete mode 100644 ostp-web/dist/assets/index-DnZZkzK0.js delete mode 100644 ostp-web/dist/favicon.png delete mode 100644 ostp-web/dist/favicon.svg delete mode 100644 ostp-web/dist/icons.svg delete mode 100644 ostp-web/dist/index.html delete mode 100644 ostp-web/dist/logo.svg diff --git a/.gitignore b/.gitignore index f87a5e6..57a23ec 100644 --- a/.gitignore +++ b/.gitignore @@ -47,3 +47,5 @@ ostp-control/ .agents/ netstack-smoltcp/ +dnstt/ +ostp-web/ diff --git a/dnstt b/dnstt deleted file mode 160000 index 0c5c52a..0000000 --- a/dnstt +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 0c5c52a57d899c05428c116898941761a2ed83c2 diff --git a/netstack-smoltcp/.cargo-ok b/netstack-smoltcp/.cargo-ok deleted file mode 100644 index 5f8b795..0000000 --- a/netstack-smoltcp/.cargo-ok +++ /dev/null @@ -1 +0,0 @@ -{"v":1} \ No newline at end of file diff --git a/netstack-smoltcp/.cargo_vcs_info.json b/netstack-smoltcp/.cargo_vcs_info.json deleted file mode 100644 index 444d072..0000000 --- a/netstack-smoltcp/.cargo_vcs_info.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "git": { - "sha1": "702f6dfe124c5e4d343cfd3ca5a3efe0446cf6f0" - }, - "path_in_vcs": "" -} \ No newline at end of file diff --git a/netstack-smoltcp/.github/actions/ndk-dev-rs/action.yml b/netstack-smoltcp/.github/actions/ndk-dev-rs/action.yml deleted file mode 100644 index 93b1eea..0000000 --- a/netstack-smoltcp/.github/actions/ndk-dev-rs/action.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Setup Android NDK and Rust compiler ENV -description: Setup an Android_NDK_HOME environment by downloading and Rust compiler environment. -inputs: - rust-target: - description: Rust target to build - required: true - sdk-version: - description: Exact SDK version to use - default: "33" - ndk-version: - description: Exact NDK version to use - default: "25" - ndk-platform: - description: Which host platform to use - default: "linux" -runs: - using: "composite" - steps: - - name: Download Android NDK - run: curl --http1.1 -O https://dl.google.com/android/repository/android-ndk-r${{ inputs.ndk-version }}-${{ inputs.ndk-platform }}.zip - shell: bash - - name: Extract Android NDK - run: unzip -q android-ndk-r${{ inputs.ndk-version }}-${{ inputs.ndk-platform }}.zip - shell: bash - - name: Set Rust compiler ENV - run: | - ndk_home=${{ github.workspace }}/android-ndk-r${{ inputs.ndk-version }} - platform=$(ls ${ndk_home}/toolchains/llvm/prebuilt/ | head -1) - ndk_tool=${ndk_home}/toolchains/llvm/prebuilt/${platform}/bin - envvar_suffix=$(echo ${{ inputs.rust-target }} | sed "s/-/_/g") - upper_suffix=$(echo ${envvar_suffix} | tr '[:lower:]' '[:upper:]') - tool_prefix=${{ inputs.rust-target }}${{ inputs.sdk-version }} - echo "ANDROID_NDK_HOME=${ndk_home}" >> $GITHUB_ENV - echo "CC_${envvar_suffix}=${ndk_tool}/${tool_prefix}-clang" >> $GITHUB_ENV - echo "AR_${envvar_suffix}=${ndk_tool}/llvm-ar" >> $GITHUB_ENV - echo "CARGO_TARGET_${upper_suffix}_LINKER=${ndk_tool}/${tool_prefix}-clang" >> $GITHUB_ENV - shell: bash diff --git a/netstack-smoltcp/.github/workflows/main.yml b/netstack-smoltcp/.github/workflows/main.yml deleted file mode 100644 index 02a9d3c..0000000 --- a/netstack-smoltcp/.github/workflows/main.yml +++ /dev/null @@ -1,80 +0,0 @@ -name: CI - -on: - push: - branches: - - '**' - pull_request: - branches: - - '**' - -env: - CARGO_INCREMENTAL: 0 - CARGO_REGISTRIES_CRATES_IO_PROTOCOL: sparse - -jobs: - test: - name: Test - runs-on: ${{ matrix.os }} - strategy: - matrix: - include: - - build: linux-amd64 - os: ubuntu-latest - target: x86_64-unknown-linux-gnu - - build: android-arm64 - os: ubuntu-latest - target: aarch64-linux-android - no_run: --no-run - - build: android-amd64 - os: ubuntu-latest - target: x86_64-linux-android - no_run: --no-run - - build: macos-amd64 - os: macos-latest - target: x86_64-apple-darwin - - build: macos-arm64 - os: macos-14 - target: aarch64-apple-darwin - - build: ios-arm64 - os: macos-latest - target: aarch64-apple-ios - no_run: --no-run - - build: windows-amd64 - os: windows-latest - target: x86_64-pc-windows-msvc - - build: windows-arm64 - os: windows-latest - target: aarch64-pc-windows-msvc - no_run: --no-run - steps: - - uses: actions/checkout@v4 - - name: Install Rust (rustup) - run: | - set -euxo pipefail - rustup toolchain install stable --no-self-update --profile minimal --target ${{ matrix.target }} - rustup default stable - shell: bash - - uses: Swatinem/rust-cache@v2 - - name: Setup android environment - if: contains(matrix.build, 'android') - uses: ./.github/actions/ndk-dev-rs - with: - rust-target: ${{ matrix.target }} - - run: cargo test ${{ matrix.no_run }} --workspace --target ${{ matrix.target }} - - run: cargo test ${{ matrix.no_run }} --workspace --target ${{ matrix.target }} --release - - msrv_n_clippy: - name: MSRV & Clippy & Rustfmt - runs-on: ${{ matrix.os }} - strategy: - fail-fast: false - matrix: - os: [ubuntu-latest, macos-latest, windows-latest] - steps: - - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - run: cargo fmt -- --check - - run: cargo clippy --all-features -- -D warnings - - run: cargo check --lib -p netstack-smoltcp - - run: cargo check --lib -p netstack-smoltcp --all-features diff --git a/netstack-smoltcp/.github/workflows/publish.yml b/netstack-smoltcp/.github/workflows/publish.yml deleted file mode 100644 index 9097444..0000000 --- a/netstack-smoltcp/.github/workflows/publish.yml +++ /dev/null @@ -1,15 +0,0 @@ -on: - push: - tags: - - '*' - -jobs: - publish: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Publish to crates.io - run: | - cargo publish - env: - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} diff --git a/netstack-smoltcp/.gitignore b/netstack-smoltcp/.gitignore deleted file mode 100644 index 154e981..0000000 --- a/netstack-smoltcp/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -/target -/Cargo.lock - -.idea -.VSCodeCounter/ -.vscode -.DS_Store -*.iml -**/*.log diff --git a/netstack-smoltcp/Cargo.toml b/netstack-smoltcp/Cargo.toml deleted file mode 100644 index 587bb65..0000000 --- a/netstack-smoltcp/Cargo.toml +++ /dev/null @@ -1,136 +0,0 @@ -# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO -# -# When uploading crates to the registry Cargo will automatically -# "normalize" Cargo.toml files for maximal compatibility -# with all versions of Cargo and also rewrite `path` dependencies -# to registry (e.g., crates.io) dependencies. -# -# If you are reading this file be aware that the original Cargo.toml -# will likely look very different (and much more reasonable). -# See Cargo.toml.orig for the original contents. - -[package] -edition = "2021" -rust-version = "1.75.0" -name = "netstack-smoltcp" -version = "0.2.2" -authors = ["cavivie "] -build = false -autolib = false -autobins = false -autoexamples = false -autotests = false -autobenches = false -description = """ -A netstack for the special purpose of turning packets from/to a TUN interface -into TCP streams and UDP packets. It uses smoltcp-rs as the backend netstack. -""" -homepage = "https://github.com/cavivie/netstack-smoltcp" -documentation = "https://docs.rs/netstack-smoltcp" -readme = "README.md" -keywords = [ - "netstack", - "smoltcp", - "network", - "ip", - "tun", -] -categories = ["network-programming"] -license = "MIT OR Apache-2.0" -repository = "https://github.com/cavivie/netstack-smoltcp" - -[lib] -name = "netstack_smoltcp" -path = "src/lib.rs" - -[[example]] -name = "forward" -path = "examples/forward.rs" - -[[example]] -name = "forward-offload-linux" -path = "examples/forward-offload-linux.rs" - -[[test]] -name = "regression" -path = "tests/regression.rs" - -[dependencies.etherparse] -version = "0.16" - -[dependencies.futures] -version = "0.3" - -[dependencies.rand] -version = "0.8" - -[dependencies.smoltcp] -version = "0.12" -features = [ - "std", - "log", - "medium-ip", - "proto-ipv4", - "proto-ipv6", - "socket-icmp", - "socket-udp", - "socket-tcp", -] -default-features = false - -[dependencies.spin] -version = "0.9" - -[dependencies.tokio] -version = "1" -features = [ - "sync", - "time", - "rt", - "macros", -] - -[dependencies.tokio-util] -version = "0.7.10" - -[dependencies.tracing] -version = "0.1" -features = ["std"] -default-features = false - -[dev-dependencies.socket2] -version = "0.5.6" - -[dev-dependencies.socket2-ext] -version = "0.1" - -[dev-dependencies.structopt] -version = "0.3" - -[dev-dependencies.tokio] -version = "1" -features = [ - "rt", - "macros", - "rt-multi-thread", - "io-util", -] - -[dev-dependencies.tracing] -version = "0.1" -features = ["std"] -default-features = false - -[dev-dependencies.tracing-subscriber] -version = "0.3.18" - -[dev-dependencies.tun-rs] -version = "2" -features = [ - "async", - "async_framed", -] - -[dev-dependencies.tun2] -version = "3" -features = ["async"] diff --git a/netstack-smoltcp/Cargo.toml.orig b/netstack-smoltcp/Cargo.toml.orig deleted file mode 100644 index 627b82f..0000000 --- a/netstack-smoltcp/Cargo.toml.orig +++ /dev/null @@ -1,51 +0,0 @@ -[package] -name = "netstack-smoltcp" -version = "0.2.2" -edition = "2021" -authors = ["cavivie "] -license = "MIT OR Apache-2.0" -repository = "https://github.com/cavivie/netstack-smoltcp" -homepage = "https://github.com/cavivie/netstack-smoltcp" -documentation = "https://docs.rs/netstack-smoltcp" -keywords = ["netstack", "smoltcp", "network", "ip", "tun"] -categories = ["network-programming"] -description = """ -A netstack for the special purpose of turning packets from/to a TUN interface -into TCP streams and UDP packets. It uses smoltcp-rs as the backend netstack. -""" -rust-version = "1.75.0" - -[dependencies] -tracing = { version = "0.1", default-features = false, features = ["std"] } -tokio = { version = "1", features = ["sync", "time", "rt", "macros"] } -tokio-util = "0.7.10" -etherparse = "0.16" -futures = "0.3" -rand = "0.8" -spin = "0.9" -smoltcp = { version = "0.12", default-features = false, features = [ - "std", - "log", - "medium-ip", - "proto-ipv4", - "proto-ipv6", - "socket-icmp", - "socket-udp", - "socket-tcp", -] } - -[dev-dependencies] -tun2 = { version = "3", features = ["async"] } -# has better performance on linux than tun2 -tun-rs = { version = "2", features = ["async", "async_framed"] } -tokio = { version = "1", features = [ - "rt", - "macros", - "rt-multi-thread", - "io-util", -] } -tracing = { version = "0.1", default-features = false, features = ["std"] } -tracing-subscriber = "0.3.18" -structopt = "0.3" -socket2 = "0.5.6" -socket2-ext = { version = "0.1" } diff --git a/netstack-smoltcp/LICENSE-APACHE b/netstack-smoltcp/LICENSE-APACHE deleted file mode 100644 index f49a4e1..0000000 --- a/netstack-smoltcp/LICENSE-APACHE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. \ No newline at end of file diff --git a/netstack-smoltcp/LICENSE-MIT b/netstack-smoltcp/LICENSE-MIT deleted file mode 100644 index ede7ee9..0000000 --- a/netstack-smoltcp/LICENSE-MIT +++ /dev/null @@ -1,25 +0,0 @@ -Copyright (c) 2024 cavivie and netstack-smoltcp Contributors - -Permission is hereby granted, free of charge, to any -person obtaining a copy of this software and associated -documentation files (the "Software"), to deal in the -Software without restriction, including without -limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of -the Software, and to permit persons to whom the Software -is furnished to do so, subject to the following -conditions: - -The above copyright notice and this permission notice -shall be included in all copies or substantial portions -of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF -ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED -TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A -PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT -SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY -CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR -IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER -DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/netstack-smoltcp/README.md b/netstack-smoltcp/README.md deleted file mode 100644 index 178e1be..0000000 --- a/netstack-smoltcp/README.md +++ /dev/null @@ -1,136 +0,0 @@ -# Netstack Smoltcp - -A netstack for the special purpose of turning packets from/to a TUN interface into TCP streams and UDP packets. It uses smoltcp-rs as the backend netstack. - -[![Crates.io][crates-badge]][crates-url] -[![MIT licensed][mit-badge]][mit-url] -[![Apache licensed, Version 2.0][apache-badge]][apache-url] -[![Build Status][actions-badge]][actions-url] - -[crates-badge]: https://img.shields.io/crates/v/netstack-smoltcp.svg -[crates-url]: https://crates.io/crates/netstack-smoltcp -[mit-badge]: https://img.shields.io/badge/license-MIT-blue.svg -[mit-url]: https://github.com/automesh-network/netstack-smoltcp/blob/master/LICENSE-MIT -[apache-badge]: https://img.shields.io/badge/license-APACHE2.0-blue.svg -[apache-url]: https://github.com/automesh-network/netstack-smoltcp/blob/master/LICENSE-APACHE -[actions-badge]: https://github.com/automesh-network/netstack-smoltcp/workflows/CI/badge.svg -[actions-url]: https://github.com/automesh-network/netstack-smoltcp/actions?query=workflow%3ACI+branch%3Amain - -## Features - -- Supports Future Send and non-Send, mostly pepole use Send. -- Supports ICMP protocol drive by TCP runner to use ICMP ping. -- Supports filtering packets by source and destination IP addresses. -- Can read IP packets from netstack, write IP packets to netstack. -- Can receive TcpStream from TcpListener exposed from netstack. -- Can receive UDP datagram from UdpSocket exposed from netstack. -- Implements popular future streaming traits and asynchronous IO traits: - * TcpListener implements futures Stream/Sink trait - * TcpStream implements tokio AsyncRead/AsyncWrite trait - * UdpSocket(ReadHalf/WriteHalf) implements futures Stream/Sink trait. - -## Platforms - -This crate provides lightweight netstack support for Linux, iOS, macOS, Android and Windows. -Currently, it works on most targets, but mainly tested the popular platforms which includes: -- linux-amd64: x86_64-unknown-linux-gnu -- android-arm64: aarch64-linux-android -- android-amd64: x86_64-linux-android -- macos-amd64: x86_64-apple-darwin -- macos-arm64: aarch64-apple-darwin -- ios-arm64: aarch64-apple-ios -- windows-amd64: x86_64-pc-windows-msvc -- windows-arm64: aarch64-pc-windows-msvc - -## Example - -```rust -// let device = tun2::create_as_async(&cfg)?; -// let framed = device.into_framed(); - -let (stack, runner, udp_socket, tcp_listener) = netstack_smoltcp::StackBuilder::default() - .stack_buffer_size(512) - .tcp_buffer_size(4096) - .enable_udp(true) - .enable_tcp(true) - .enable_icmp(true) - .mtu(9000) // virtual device usually benefits from larger MTU - .build() - .unwrap(); -let mut udp_socket = udp_socket.unwrap(); // udp enabled -let mut tcp_listener = tcp_listener.unwrap(); // tcp/icmp enabled -if let Some(runner) = runner { - tokio::spawn(runner); -} - -let (mut stack_sink, mut stack_stream) = stack.split(); -let (mut tun_sink, mut tun_stream) = framed.split(); - -// Reads packet from stack and sends to TUN. -tokio::spawn(async move { - while let Some(pkt) = stack_stream.next().await { - if let Ok(pkt) = pkt { - tun_sink.send(pkt).await.unwrap(); - } - } -}); - -// Reads packet from TUN and sends to stack. -tokio::spawn(async move { - while let Some(pkt) = tun_stream.next().await { - if let Ok(pkt) = pkt { - stack_sink.send(pkt).await.unwrap(); - } - } -}); - -// Extracts TCP connections from stack and sends them to the dispatcher. -tokio::spawn(async move { - handle_inbound_stream(tcp_listener).await; -}); - -// Receive and send UDP packets between netstack and NAT manager. The NAT -// manager would maintain UDP sessions and send them to the dispatcher. -tokio::spawn(async move { - handle_inbound_datagram(udp_socket).await; -}); -``` - -## Performance - -Typically, `netstack-smoltcp` will be used with an tun device, so a careful choice of TUN crate matters. - -[tun-rs](https://github.com/tun-rs/tun-rs) have better performance on **Linux** than [rust-tun](https://github.com/meh/rust-tun/) due to GSO/GRO which allow you to process the packets in batches. - -`bash scripts/bench-offload.sh` could tell that `tun-rs` boosts the performance by 4x. Try it out on your Linux machine! - -The example for using `tun-rs` with `netstack-smoltcp` could be found at [forward-offload-linux.rs](examples/forward-offload-linux.rs) - -For further tuning, refer to `tun-rs`'s detailed [README](https://github.com/tun-rs/tun-rs/blob/main/README.md) - -## License - -This project is licensed under either of - - * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or - https://www.apache.org/licenses/LICENSE-2.0) - * MIT license ([LICENSE-MIT](LICENSE-MIT) or - https://opensource.org/licenses/MIT) - -at your option. - -### Contribution - -Unless you explicitly state otherwise, any contribution intentionally submitted -for inclusion in netstack-smoltcp by you, as defined in the Apache-2.0 license, -shall be dual licensed as above, without any additional terms or conditions. - -## Inspired By - -Special thanks to these amazing projects that inspired netstack-smoltcp (in no particular order): -- [shadowsocks-rust](https://github.com/shadowsocks/shadowsocks-rust/) -- [netstack-lwip](https://github.com/eycorsican/netstack-lwip/) -- [rust-tun-active](https://github.com/tun2proxy/rust-tun) -- [rust-tun](https://github.com/meh/rust-tun/) -- [tun-rs](https://github.com/tun-rs/tun-rs) -- [smoltcp](https://github.com/smoltcp-rs/smoltcp) \ No newline at end of file diff --git a/netstack-smoltcp/examples/forward-offload-linux.rs b/netstack-smoltcp/examples/forward-offload-linux.rs deleted file mode 100644 index 573ba53..0000000 --- a/netstack-smoltcp/examples/forward-offload-linux.rs +++ /dev/null @@ -1,239 +0,0 @@ -#[cfg(target_os = "linux")] -mod inner { - use futures::{SinkExt, StreamExt}; - use netstack_smoltcp::{StackBuilder, TcpListener, UdpSocket}; - use std::{net::SocketAddr, sync::Arc}; - use structopt::StructOpt; - use tokio::net::{TcpSocket, TcpStream}; - use tracing::{error, info, warn}; - use tun_rs::{DeviceBuilder, IDEAL_BATCH_SIZE, VIRTIO_NET_HDR_LEN}; - - // Patched forward example: tun2 → tun-rs with Linux GRO/GSO offload. - // For further reading, check out https://blog.cloudflare.com/virtual-networking-101-understanding-tap - // - // Key changes vs forward.rs: - // 1. Use tun-rs DeviceBuilder with .offload(true) on Linux (enables - // IFF_VNET_HDR + TUN_F_CSUM/TSO4/TSO6/USO4/USO6). - // 2. TX (stack → TUN): prepend 10-byte zero virtio_net_hdr (GSO_NONE) - // so the kernel accepts the write when IFF_VNET_HDR is set. - // 3. RX (TUN → stack): use recv_multiple() for batch GSO splitting; - // buffers sized to 1600 to fit smoltcp's 1504-byte MTU segments. - #[derive(Debug, StructOpt)] - #[structopt(name = "forward", about = "Simply forward tun tcp/udp traffic.")] - struct Opt { - /// Outbound interface to bind forwarded connections to. - #[structopt(short = "i", long = "interface")] - interface: String, - /// Name of the TUN device. - #[structopt(short = "n", long = "name", default_value = "utun8")] - name: String, - /// Tracing log level. - #[structopt(long = "log-level", default_value = "debug")] - log_level: tracing::Level, - /// Use current-thread Tokio runtime (default: multi-thread). - #[structopt(long = "current-thread")] - current_thread: bool, - /// Use spawn_local instead of spawn. - #[structopt(long = "local-task")] - local_task: bool, - } - - pub(super) fn main() { - let opt = Opt::from_args(); - let rt = if opt.current_thread { - tokio::runtime::Builder::new_current_thread() - } else { - tokio::runtime::Builder::new_multi_thread() - } - .enable_all() - .build() - .unwrap(); - rt.block_on(main_exec(opt)); - } - - async fn main_exec(opt: Opt) { - macro_rules! tokio_spawn { - ($fut:expr) => { - if opt.local_task { - tokio::task::spawn_local($fut) - } else { - tokio::task::spawn($fut) - } - }; - } - tracing::subscriber::set_global_default( - tracing_subscriber::FmtSubscriber::builder() - .with_max_level(opt.log_level) - .finish(), - ) - .unwrap(); - - // Build TUN device with GRO/GSO offload on Linux. - let builder = DeviceBuilder::new() - .name(opt.name) - .ipv4("10.10.10.2", 24, Some("10.10.10.1")) - .mtu(9000); - let builder = builder.offload(true); - let dev = Arc::new(builder.build_async().unwrap()); - - let (stack, runner, udp_socket, tcp_listener) = StackBuilder::default() - .enable_tcp(true) - .enable_udp(true) - .enable_icmp(true) - .build() - .unwrap(); - let udp_socket = udp_socket.unwrap(); - let tcp_listener = tcp_listener.unwrap(); - if let Some(runner) = runner { - tokio_spawn!(runner); - } - let (mut stack_sink, mut stack_stream) = stack.split(); - - let mut futs = vec![]; - - // stack → TUN - // With IFF_VNET_HDR every write must start with a virtio_net_hdr. - // We use all-zero (gso_type = GSO_NONE, flags = 0): plain packet, - // checksum already valid (smoltcp always computes checksums itself). - let dev1 = dev.clone(); - futs.push(tokio_spawn!(async move { - while let Some(pkt) = stack_stream.next().await { - if let Ok(pkt) = pkt { - let result = { - let mut buf = vec![0u8; VIRTIO_NET_HDR_LEN + pkt.len()]; - buf[VIRTIO_NET_HDR_LEN..].copy_from_slice(&pkt); - dev1.send(&buf).await - }; - if let Err(e) = result { - warn!("failed to send packet to TUN: {:?}", e); - } - } - } - })); - - // TUN → stack - // recv_multiple() does one read() syscall and returns N individual IP - // packets after splitting any incoming GRO super-packet. - // Buffer size 1600 > smoltcp MTU (1504) to avoid an out-of-bounds panic - // when the kernel segments at MSS=1464 with 40-byte IP+TCP headers. - futs.push(tokio_spawn!(async move { - let mut orig = vec![0u8; VIRTIO_NET_HDR_LEN + 65535]; - let mut bufs = vec![vec![0u8; 1600]; IDEAL_BATCH_SIZE]; - let mut sizes = vec![0usize; IDEAL_BATCH_SIZE]; - while let Ok(n) = dev.recv_multiple(&mut orig, &mut bufs, &mut sizes, 0).await { - for i in 0..n { - let pkt = &bufs[i][..sizes[i]]; - if let Err(e) = stack_sink.send(pkt.to_vec()).await { - warn!("failed to send packet to stack: {:?}", e); - } - } - } - })); - - futs.push(tokio_spawn!({ - let iface = opt.interface.clone(); - async move { - handle_inbound_stream(tcp_listener, iface).await; - } - })); - - futs.push(tokio_spawn!(async move { - handle_inbound_datagram(udp_socket, opt.interface).await; - })); - - futures::future::join_all(futs).await.iter().for_each(|r| { - if let Err(e) = r { - error!("{:?}", e); - } - }); - } - - async fn handle_inbound_stream(mut tcp_listener: TcpListener, interface: String) { - while let Some((mut stream, local, remote)) = tcp_listener.next().await { - let interface = interface.clone(); - tokio::spawn(async move { - info!("tcp: {:?} => {:?}", local, remote); - match new_tcp_stream(remote, &interface).await { - Ok(mut r) => { - if let Err(e) = tokio::io::copy_bidirectional(&mut stream, &mut r).await { - warn!( - "failed to copy tcp stream {:?}=>{:?}: {:?}", - local, remote, e - ); - } - } - Err(e) => warn!( - "failed to open tcp stream {:?}=>{:?}: {:?}", - local, remote, e - ), - } - }); - } - } - - async fn handle_inbound_datagram(udp_socket: UdpSocket, interface: String) { - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); - let (mut read_half, mut write_half) = udp_socket.split(); - tokio::spawn(async move { - while let Some((data, local, remote)) = rx.recv().await { - let _ = write_half.send((data, remote, local)).await; - } - }); - while let Some((data, local, remote)) = read_half.next().await { - let tx = tx.clone(); - let interface = interface.clone(); - tokio::spawn(async move { - match new_udp_packet(remote, &interface).await { - Ok(sock) => { - let _ = sock.send(&data).await; - loop { - let mut buf = vec![0; 1024]; - match sock.recv_from(&mut buf).await { - Ok((n, _)) => { - let _ = tx.send((buf[..n].to_vec(), local, remote)); - } - Err(e) => { - warn!("udp recv {:?}: {:?}", remote, e); - break; - } - } - } - } - Err(e) => warn!("failed to open udp socket {:?}: {:?}", remote, e), - } - }); - } - } - - async fn new_tcp_stream(addr: SocketAddr, iface: &str) -> std::io::Result { - use socket2_ext::{AddressBinding, BindDeviceOption}; - let s = socket2::Socket::new(socket2::Domain::IPV4, socket2::Type::STREAM, None)?; - s.bind_to_device(BindDeviceOption::v4(iface))?; - s.set_keepalive(true)?; - s.set_nodelay(true)?; - s.set_nonblocking(true)?; - Ok(TcpSocket::from_std_stream(s.into()).connect(addr).await?) - } - - async fn new_udp_packet( - addr: SocketAddr, - iface: &str, - ) -> std::io::Result { - use socket2_ext::{AddressBinding, BindDeviceOption}; - let s = socket2::Socket::new(socket2::Domain::IPV4, socket2::Type::DGRAM, None)?; - s.bind_to_device(BindDeviceOption::v4(iface))?; - s.set_nonblocking(true)?; - let sock = tokio::net::UdpSocket::from_std(s.into())?; - sock.connect(addr).await?; - Ok(sock) - } -} - -#[cfg(not(target_os = "linux"))] -mod inner { - pub(super) fn main() {} -} - -fn main() { - inner::main(); -} diff --git a/netstack-smoltcp/examples/forward.rs b/netstack-smoltcp/examples/forward.rs deleted file mode 100644 index d9a79d4..0000000 --- a/netstack-smoltcp/examples/forward.rs +++ /dev/null @@ -1,326 +0,0 @@ -use std::net::{IpAddr, SocketAddr}; - -use futures::{SinkExt, StreamExt}; -use netstack_smoltcp::{StackBuilder, TcpListener, UdpSocket}; -use structopt::StructOpt; -use tokio::net::{TcpSocket, TcpStream}; -use tracing::{error, info, warn}; - -// to run this example, you should set the policy routing **after the start of the main program** -// -// linux: -// with bind device: -// `curl 1.1.1.1 --interface utun8` -// with default route: -// `bash scripts/route-linux.sh add` -// `curl 1.1.1.1` -// with single route: -// `ip rule add to 1.1.1.1 table 200` -// `ip route add default dev utun8 table 200` -// `curl 1.1.1.1` -// -// macos: -// with default route: -// `bash scripts/route-macos.sh add` -// `curl 1.1.1.1` -// -// windows: -// with default route: -// tun2 set default route automatically, won't set agian -// # `powershell.exe scripts/route-windows.ps1 add` -// `curl 1.1.1.1` -// -// currently, the example only supports the TCP stream, and the UDP packet will be dropped. - -#[derive(Debug, StructOpt)] -#[structopt(name = "forward", about = "Simply forward tun tcp/udp traffic.")] -struct Opt { - /// Default binding interface, default by guessed. - /// Specify but doesn't exist, no device is bound. - #[structopt(short = "i", long = "interface")] - interface: String, - - /// name of the tun device, default to rtun8. - #[structopt(short = "n", long = "name", default_value = "utun8")] - name: String, - - /// Tracing subscriber log level. - #[structopt(long = "log-level", default_value = "debug")] - log_level: tracing::Level, - - /// Tokio current-thread runtime, default to multi-thread. - #[structopt(long = "current-thread")] - current_thread: bool, - - /// Tokio task spawn_local, default to spwan. - #[structopt(long = "local-task")] - local_task: bool, -} - -fn main() { - let opt = Opt::from_args(); - - let rt = if opt.current_thread { - tokio::runtime::Builder::new_current_thread() - } else { - tokio::runtime::Builder::new_multi_thread() - } - .enable_all() - .build() - .unwrap(); - - rt.block_on(main_exec(opt)); -} - -async fn main_exec(opt: Opt) { - macro_rules! tokio_spawn { - ($fut: expr) => { - if opt.local_task { - tokio::task::spawn_local($fut) - } else { - tokio::task::spawn($fut) - } - }; - } - - tracing::subscriber::set_global_default( - tracing_subscriber::FmtSubscriber::builder() - .with_max_level(opt.log_level) - .finish(), - ) - .unwrap(); - - let mut cfg = tun2::Configuration::default(); - cfg.layer(tun2::Layer::L3); - let fd = -1; - if fd >= 0 { - cfg.raw_fd(fd); - } else { - cfg.tun_name(&opt.name) - .address("10.10.10.2") - .destination("10.10.10.1") - .mtu(tun2::DEFAULT_MTU); - #[cfg(not(any(target_arch = "mips", target_arch = "mips64",)))] - { - cfg.netmask("255.255.255.0"); - } - cfg.up(); - } - - let device = tun2::create_as_async(&cfg).unwrap(); - let mut builder = StackBuilder::default() - .enable_tcp(true) - .enable_udp(true) - .enable_icmp(true) - .mtu(9000); - if let Some(device_broadcast) = get_device_broadcast(&device) { - builder = builder - // .add_ip_filter(Box::new(move |src, dst| *src != device_broadcast && *dst != device_broadcast)); - .add_ip_filter_fn(move |src, dst| *src != device_broadcast && *dst != device_broadcast); - } - - let (stack, runner, udp_socket, tcp_listener) = builder.build().unwrap(); - let udp_socket = udp_socket.unwrap(); // udp enabled - let tcp_listener = tcp_listener.unwrap(); // tcp enabled or icmp enabled - - if let Some(runner) = runner { - tokio_spawn!(runner); - } - - let framed = device.into_framed(); - let (mut tun_sink, mut tun_stream) = framed.split(); - let (mut stack_sink, mut stack_stream) = stack.split(); - - let mut futs = vec![]; - - // Reads packet from stack and sends to TUN. - futs.push(tokio_spawn!(async move { - while let Some(pkt) = stack_stream.next().await { - if let Ok(pkt) = pkt { - match tun_sink.send(pkt).await { - Ok(_) => {} - Err(e) => warn!("failed to send packet to TUN, err: {:?}", e), - } - } - } - })); - - // Reads packet from TUN and sends to stack. - futs.push(tokio_spawn!(async move { - while let Some(pkt) = tun_stream.next().await { - if let Ok(pkt) = pkt { - match stack_sink.send(pkt).await { - Ok(_) => {} - Err(e) => warn!("failed to send packet to stack, err: {:?}", e), - }; - } - } - })); - - // Extracts TCP connections from stack and sends them to the dispatcher. - futs.push(tokio_spawn!({ - let interface = opt.interface.clone(); - async move { - handle_inbound_stream(tcp_listener, interface).await; - } - })); - - // Receive and send UDP packets between netstack and NAT manager. The NAT - // manager would maintain UDP sessions and send them to the dispatcher. - futs.push(tokio_spawn!(async move { - handle_inbound_datagram(udp_socket, opt.interface).await; - })); - - futures::future::join_all(futs) - .await - .iter() - .for_each(|res| { - if let Err(e) = res { - error!("error: {:?}", e); - } - }); -} - -/// simply forward tcp stream -async fn handle_inbound_stream(mut tcp_listener: TcpListener, interface: String) { - while let Some((mut stream, local, remote)) = tcp_listener.next().await { - let interface = interface.clone(); - tokio::spawn(async move { - info!("new tcp connection: {:?} => {:?}", local, remote); - match new_tcp_stream(remote, &interface).await { - Ok(mut remote_stream) => { - // pipe between two tcp stream - match tokio::io::copy_bidirectional(&mut stream, &mut remote_stream).await { - Ok(_) => {} - Err(e) => warn!( - "failed to copy tcp stream {:?}=>{:?}, err: {:?}", - local, remote, e - ), - } - } - Err(e) => warn!( - "failed to new tcp stream {:?}=>{:?}, err: {:?}", - local, remote, e - ), - } - }); - } -} - -/// simply forward udp datagram -async fn handle_inbound_datagram(udp_socket: UdpSocket, interface: String) { - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); - let (mut read_half, mut write_half) = udp_socket.split(); - tokio::spawn(async move { - while let Some((data, local, remote)) = rx.recv().await { - let _ = write_half.send((data, remote, local)).await; - } - }); - - while let Some((data, local, remote)) = read_half.next().await { - let tx = tx.clone(); - let interface = interface.clone(); - tokio::spawn(async move { - info!("new udp datagram: {:?} => {:?}", local, remote); - match new_udp_packet(remote, &interface).await { - Ok(remote_socket) => { - // pipe between two udp sockets - let _ = remote_socket.send(&data).await; - loop { - let mut buf = vec![0; 1024]; - match remote_socket.recv_from(&mut buf).await { - Ok((len, _)) => { - let _ = tx.send((buf[..len].to_vec(), local, remote)); - } - Err(e) => { - warn!( - "failed to recv udp datagram {:?}<->{:?}: {:?}", - local, remote, e - ); - break; - } - } - } - } - Err(e) => warn!( - "failed to new udp socket {:?}=>{:?}, err: {:?}", - local, remote, e - ), - } - }); - } -} - -async fn new_tcp_stream<'a>(addr: SocketAddr, iface: &str) -> std::io::Result { - use socket2_ext::{AddressBinding, BindDeviceOption}; - let socket = socket2::Socket::new(socket2::Domain::IPV4, socket2::Type::STREAM, None)?; - socket.bind_to_device(BindDeviceOption::v4(iface))?; - socket.set_keepalive(true)?; - socket.set_nodelay(true)?; - socket.set_nonblocking(true)?; - - let stream = TcpSocket::from_std_stream(socket.into()) - .connect(addr) - .await?; - - Ok(stream) -} - -async fn new_udp_packet(addr: SocketAddr, iface: &str) -> std::io::Result { - use socket2_ext::{AddressBinding, BindDeviceOption}; - let socket = socket2::Socket::new(socket2::Domain::IPV4, socket2::Type::DGRAM, None)?; - socket.bind_to_device(BindDeviceOption::v4(iface))?; - socket.set_nonblocking(true)?; - - let socket = tokio::net::UdpSocket::from_std(socket.into()); - if let Ok(ref socket) = socket { - socket.connect(addr).await?; - } - socket -} - -fn get_device_broadcast(device: &tun2::AsyncDevice) -> Option { - use tun2::AbstractDevice; - - let mtu = device.mtu().unwrap_or(tun2::DEFAULT_MTU); - - let address = match device.address() { - Ok(a) => match a { - IpAddr::V4(v4) => v4, - IpAddr::V6(_) => return None, - }, - Err(_) => return None, - }; - - let netmask = match device.netmask() { - Ok(n) => match n { - IpAddr::V4(v4) => v4, - IpAddr::V6(_) => return None, - }, - Err(_) => return None, - }; - - match smoltcp::wire::Ipv4Cidr::from_netmask(address, netmask) { - Ok(address_net) => match address_net.broadcast() { - Some(broadcast) => { - info!( - "tun device network: {} (address: {}, netmask: {}, broadcast: {}, mtu: {})", - address_net, address, netmask, broadcast, mtu, - ); - - Some(broadcast) - } - None => { - error!("invalid tun address {}, netmask {}", address, netmask); - None - } - }, - Err(err) => { - error!( - "invalid tun address {}, netmask {}, error: {}", - address, netmask, err - ); - None - } - } -} diff --git a/netstack-smoltcp/scripts/bench-offload.sh b/netstack-smoltcp/scripts/bench-offload.sh deleted file mode 100644 index 09bd837..0000000 --- a/netstack-smoltcp/scripts/bench-offload.sh +++ /dev/null @@ -1,174 +0,0 @@ -#!/usr/bin/env bash -# bench-offload.sh -# -# Benchmarks netstack-smoltcp's forward examples with 2-stream iperf3. -# Compares: -# - examples/forward (tun2, no GRO/GSO offload) -# - examples/forward-offload-linux (tun-rs, Linux GRO/GSO offload via IFF_VNET_HDR) -# -# Setup: creates a veth pair + network namespace; iperf3 server runs inside -# the namespace, the forward proxy bridges traffic through a TUN device. -# -# Requirements: cargo, iperf3, ip (iproute2), root/CAP_NET_ADMIN -# -# Usage: -# sudo bash scripts/bench-offload.sh -# -# Run from the root of the netstack-smoltcp repository. - -set -euo pipefail - -# ── config ──────────────────────────────────────────────────────────────────── -REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -NS=bench -VETH_HOST=veth-host -VETH_NS=veth-bench -HOST_IP=172.19.0.1 -NS_IP=172.19.0.2 -PREFIX=24 -TUN_NAME=utun8 -TUN_IP=10.10.10.2 -IPERF_PORT=5201 -DURATION=15 -STREAMS=2 - -# ── helpers ─────────────────────────────────────────────────────────────────── -die() { echo "ERROR: $*" >&2; exit 1; } -require() { command -v "$1" &>/dev/null || die "'$1' not found"; } - -cleanup() { - pkill -f "forward-" 2>/dev/null || true - ip netns exec "$NS" pkill iperf3 2>/dev/null || true - ip route del "${NS_IP}/32" dev "$TUN_NAME" 2>/dev/null || true - ip tuntap del dev "$TUN_NAME" mode tun 2>/dev/null || true - ip link del "$VETH_HOST" 2>/dev/null || true - ip netns del "$NS" 2>/dev/null || true -} -trap cleanup EXIT - -# ── preflight ───────────────────────────────────────────────────────────────── -require cargo -require iperf3 -require ip -[[ $EUID -eq 0 ]] || die "run as root (needs CAP_NET_ADMIN for TUN + netns)" -[[ -f "$REPO_DIR/Cargo.toml" ]] || die "run from the netstack-smoltcp repo root" -grep -q 'name = "netstack-smoltcp"' "$REPO_DIR/Cargo.toml" \ - || die "Cargo.toml does not look like netstack-smoltcp" - -# ── network setup ───────────────────────────────────────────────────────────── -echo "[net] setting up namespace '$NS' and veth pair..." -cleanup 2>/dev/null || true -sleep 0.5 - -ip netns add "$NS" -ip link add "$VETH_HOST" type veth peer name "$VETH_NS" -ip link set "$VETH_NS" netns "$NS" -ip addr add "${HOST_IP}/${PREFIX}" dev "$VETH_HOST" -ip link set "$VETH_HOST" up -ip netns exec "$NS" ip addr add "${NS_IP}/${PREFIX}" dev "$VETH_NS" -ip netns exec "$NS" ip link set "$VETH_NS" up -ip netns exec "$NS" ip link set lo up -echo "[net] ${HOST_IP} <──veth──> ${NS_IP} (ns:${NS})" - -# ── build: forward (tun2, no offload) ──────────────────────────────────────── -echo "" -echo "[build] examples/forward (tun2, no GRO/GSO offload)..." -( - cd "$REPO_DIR" - cargo build --example forward --release --quiet - cp target/release/examples/forward /tmp/forward-tun2 -) -echo "[build] done → /tmp/forward-tun2" - -# ── build: forward-offload-linux (tun-rs, GRO/GSO offload) ─────────────────── -echo "" -echo "[build] examples/forward-offload-linux (tun-rs, GRO/GSO offload)..." -( - cd "$REPO_DIR" - cargo build --example forward-offload-linux --release --quiet - cp target/release/examples/forward-offload-linux /tmp/forward-tun-rs -) -echo "[build] done → /tmp/forward-tun-rs" - -# ── benchmark runner ────────────────────────────────────────────────────────── -run_bench() { - local label="$1" binary="$2" - - # clean any leftover state - pkill -f "forward-" 2>/dev/null || true - ip netns exec "$NS" pkill iperf3 2>/dev/null || true - ip route del "${NS_IP}/32" dev "$TUN_NAME" 2>/dev/null || true - ip tuntap del dev "$TUN_NAME" mode tun 2>/dev/null || true - sleep 0.8 - - # start iperf3 server inside namespace - ip netns exec "$NS" iperf3 -s -p "$IPERF_PORT" -D \ - --logfile /tmp/iperf3-bench-server.log - - # start proxy - "$binary" -i "$VETH_HOST" -n "$TUN_NAME" --log-level warn & - sleep 2 - - ip link show "$TUN_NAME" &>/dev/null \ - || { echo " [!] TUN not up, skipping"; return 1; } - - # route iperf3 traffic through TUN (more-specific /32 overrides /24 via veth) - ip route add "${NS_IP}/32" dev "$TUN_NAME" - - echo " running iperf3: ${STREAMS} streams × ${DURATION}s …" - local out - out=$(iperf3 -c "$NS_IP" -p "$IPERF_PORT" \ - -t "$DURATION" -P "$STREAMS" 2>&1) - - local sender receiver - sender=$(echo "$out" | grep "SUM.*sender" | awk '{print $6, $7}') - receiver=$(echo "$out" | grep "SUM.*receiver" | awk '{print $6, $7}') - - if [[ -z "$sender" ]]; then - echo " result: FAILED" - echo "$out" | tail -5 | sed 's/^/ /' - else - printf " sender: %s\n" "$sender" - printf " receiver: %s\n" "$receiver" - fi - - pkill -f "forward-" 2>/dev/null || true - ip netns exec "$NS" pkill iperf3 2>/dev/null || true - ip route del "${NS_IP}/32" dev "$TUN_NAME" 2>/dev/null || true - ip tuntap del dev "$TUN_NAME" mode tun 2>/dev/null || true - sleep 0.8 -} - -# ── direct baseline ─────────────────────────────────────────────────────────── -echo "" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo " BASELINE: direct veth (no TUN, no proxy)" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -ip netns exec "$NS" pkill iperf3 2>/dev/null || true; sleep 0.3 -ip netns exec "$NS" iperf3 -s -p "$IPERF_PORT" -D \ - --logfile /tmp/iperf3-bench-server.log; sleep 0.3 -echo " running iperf3: ${STREAMS} streams × ${DURATION}s …" -baseline_out=$(iperf3 -c "$NS_IP" -p "$IPERF_PORT" \ - -t "$DURATION" -P "$STREAMS" 2>&1) -echo "$baseline_out" | grep "SUM.*sender" | awk '{printf " sender: %s %s\n", $6, $7}' -echo "$baseline_out" | grep "SUM.*receiver" | awk '{printf " receiver: %s %s\n", $6, $7}' -ip netns exec "$NS" pkill iperf3 2>/dev/null || true; sleep 0.5 - -# ── tun2 ───────────────────────────────────────────────────────────────────── -echo "" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo " tun2 (main branch — no GRO/GSO offload)" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -run_bench "tun2" /tmp/forward-tun2 - -# ── tun-rs + offload ────────────────────────────────────────────────────────── -echo "" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo " tun-rs (patched — GRO/GSO offload via IFF_VNET_HDR)" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -run_bench "tun-rs+offload" /tmp/forward-tun-rs - -echo "" -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" -echo " done." -echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" \ No newline at end of file diff --git a/netstack-smoltcp/scripts/route-linux.sh b/netstack-smoltcp/scripts/route-linux.sh deleted file mode 100644 index ca0b691..0000000 --- a/netstack-smoltcp/scripts/route-linux.sh +++ /dev/null @@ -1,26 +0,0 @@ -#!/bin/bash -#__author__: cavivie - -DEFAULT_TUN_NAME="utun8" - -function do_route() { - local route_op="${1}" - local tun_name="${2:-$DEFAULT_TUN_NAME}" - ip route ${route_op} 0.0.0.0/1 dev ${tun_name} - ip route ${route_op} 128.0.0.0/1 dev ${tun_name} -} - -function usage(){ - echo "Usage: - route add add tun routes to system route table - route del delete routes from system route table - route help display all usages of the shell script" -} - -# START MAIN-OPTIONS -case $1 in - add) do_route add $2;; - del) do_route delete $2;; - *) usage ;; -esac -# END MAIN-OPTIONS diff --git a/netstack-smoltcp/scripts/route-macos.sh b/netstack-smoltcp/scripts/route-macos.sh deleted file mode 100644 index 786f47d..0000000 --- a/netstack-smoltcp/scripts/route-macos.sh +++ /dev/null @@ -1,36 +0,0 @@ -#!/bin/bash -#__author__: cavivie - -DEFAULT_TUN_ADDR="10.10.10.2/24" -DEFAULT_TUN_DEST="10.10.10.1" - -function do_route() { - local route_op="${1}" - local tun_addr="${2:-$DEFAULT_TUN_ADDR}" - local tun_dest="${3:-$DEFAULT_TUN_DEST}" - sudo route ${route_op} -net 1.0.0.0/8 ${tun_dest} - sudo route ${route_op} -net 2.0.0.0/7 ${tun_dest} - sudo route ${route_op} -net 4.0.0.0/6 ${tun_dest} - sudo route ${route_op} -net 8.0.0.0/5 ${tun_dest} - sudo route ${route_op} -net 16.0.0.0/4 ${tun_dest} - sudo route ${route_op} -net 32.0.0.0/3 ${tun_dest} - sudo route ${route_op} -net 64.0.0.0/2 ${tun_dest} - sudo route ${route_op} -net 128.0.0.0/1 ${tun_dest} - # tun2 do like this automatically - sudo route ${route_op} -net ${tun_addr} ${tun_dest} -} - -function usage(){ - echo "Usage: - route add add tun routes to system route table - route del delete routes from system route table - route help display all usages of the shell script" -} - -# START MAIN-OPTIONS -case $1 in - add) do_route add $2 $3;; - del) do_route delete $2 $3;; - *) usage ;; -esac -# END MAIN-OPTIONS diff --git a/netstack-smoltcp/scripts/route-windows.ps1 b/netstack-smoltcp/scripts/route-windows.ps1 deleted file mode 100644 index 818221c..0000000 --- a/netstack-smoltcp/scripts/route-windows.ps1 +++ /dev/null @@ -1,30 +0,0 @@ -#__author__: cavivie - -param( - [string]$Cmd = "help", - [string]$TunName = "utun8", - [string]$TunGateway = "10.10.10.1" -) - -$ErrorActionPreference = "Stop" - -# START MAIN-OPTIONS -switch ($Cmd) { - "add" { - # tun2 do like this automatically - New-NetRoute -DestinationPrefix "0.0.0.0/1" -InterfaceAlias $TunName -NextHop "$TunGateway" - New-NetRoute -DestinationPrefix "128.0.0.0/1" -InterfaceAlias $TunName -NextHop "$TunGateway" - } - "del" { - # tun2 do like this automatically - Get-NetRoute -DestinationPrefix "0.0.0.0/1" -InterfaceAlias $TunName | Remove-NetRoute - Get-NetRoute -DestinationPrefix "128.0.0.0/1" -InterfaceAlias $TunName | Remove-NetRoute - } - default { - Write-Host "Usage: - route add add tun routes to system route table - route del delete routes from system route table - route help display all usages of the shell script" - } -} -# END MAIN-OPTIONS diff --git a/netstack-smoltcp/src/device.rs b/netstack-smoltcp/src/device.rs deleted file mode 100644 index 9369910..0000000 --- a/netstack-smoltcp/src/device.rs +++ /dev/null @@ -1,109 +0,0 @@ -use std::sync::{ - atomic::{AtomicBool, Ordering}, - Arc, -}; - -use smoltcp::{ - phy::{Device, DeviceCapabilities, Medium, RxToken, TxToken}, - time::Instant, -}; -use tokio::sync::mpsc::{unbounded_channel, Permit, Sender, UnboundedReceiver, UnboundedSender}; - -use crate::packet::AnyIpPktFrame; - -pub(super) struct VirtualDevice { - in_buf_avail: Arc, - in_buf: UnboundedReceiver>, - out_buf: Sender, - mtu: usize, - cached_packet: Option>, -} - -impl VirtualDevice { - pub(super) fn new( - iface_egress_tx: Sender, - mtu: usize, - ) -> (Self, UnboundedSender>, Arc) { - let iface_ingress_tx_avail = Arc::new(AtomicBool::new(false)); - let (iface_ingress_tx, iface_ingress_rx) = unbounded_channel(); - ( - Self { - in_buf_avail: iface_ingress_tx_avail.clone(), - in_buf: iface_ingress_rx, - out_buf: iface_egress_tx, - mtu, - cached_packet: None, - }, - iface_ingress_tx, - iface_ingress_tx_avail, - ) - } -} - -impl Device for VirtualDevice { - type RxToken<'a> = VirtualRxToken; - type TxToken<'a> = VirtualTxToken<'a>; - - fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { - let buffer = if let Some(buf) = self.cached_packet.take() { - buf - } else { - let Ok(buf) = self.in_buf.try_recv() else { - self.in_buf_avail.store(false, Ordering::Release); - return None; - }; - buf - }; - - let Ok(permit) = self.out_buf.try_reserve() else { - self.cached_packet = Some(buffer); - self.in_buf_avail.store(false, Ordering::Release); - return None; - }; - - Some((Self::RxToken { buffer }, Self::TxToken { permit })) - } - - fn transmit(&mut self, _timestamp: Instant) -> Option> { - match self.out_buf.try_reserve() { - Ok(permit) => Some(Self::TxToken { permit }), - Err(_) => None, - } - } - - fn capabilities(&self) -> DeviceCapabilities { - let mut capabilities = DeviceCapabilities::default(); - capabilities.medium = Medium::Ip; - capabilities.max_transmission_unit = self.mtu; - capabilities - } -} - -pub(super) struct VirtualRxToken { - buffer: Vec, -} - -impl RxToken for VirtualRxToken { - fn consume(self, f: F) -> R - where - F: FnOnce(&[u8]) -> R, - { - f(&self.buffer[..]) - } -} - -pub(super) struct VirtualTxToken<'a> { - permit: Permit<'a, Vec>, -} - -impl<'a> TxToken for VirtualTxToken<'a> { - fn consume(self, len: usize, f: F) -> R - where - F: FnOnce(&mut [u8]) -> R, - { - let mut buffer = vec![0u8; len]; - let result = f(&mut buffer); - self.permit.send(buffer); - result - } -} diff --git a/netstack-smoltcp/src/filter.rs b/netstack-smoltcp/src/filter.rs deleted file mode 100644 index daced19..0000000 --- a/netstack-smoltcp/src/filter.rs +++ /dev/null @@ -1,56 +0,0 @@ -use std::net::IpAddr; - -pub type IpFilter<'a> = Box bool + Send + Sync + 'a>; - -pub struct IpFilters<'a> { - filters: Vec>, -} - -impl<'a> Default for IpFilters<'a> { - fn default() -> Self { - Self::new() - } -} - -impl<'a> IpFilters<'a> { - pub fn new() -> Self { - Self { - filters: Default::default(), - } - } - - pub fn with_non_broadcast() -> Self { - macro_rules! non_broadcast { - ($addr:ident) => { - match $addr { - IpAddr::V4(a) => !(a.is_broadcast() || a.is_multicast() || a.is_unspecified()), - IpAddr::V6(a) => !(a.is_multicast() || a.is_unspecified()), - } - }; - } - Self { - filters: vec![Box::new(|src, dst| { - non_broadcast!(src) && non_broadcast!(dst) - })], - } - } - - pub fn add(&mut self, filter: IpFilter<'a>) { - self.filters.push(filter); - } - - pub fn add_fn(&mut self, filter: F) - where - F: Fn(&IpAddr, &IpAddr) -> bool + Send + Sync + 'a, - { - self.filters.push(Box::new(filter)); - } - - pub fn add_all>>(&mut self, filters: I) { - self.filters.extend(filters); - } - - pub fn is_allowed(&self, src: &IpAddr, dst: &IpAddr) -> bool { - self.filters.iter().all(|filter| filter(src, dst)) - } -} diff --git a/netstack-smoltcp/src/lib.rs b/netstack-smoltcp/src/lib.rs deleted file mode 100644 index ab72413..0000000 --- a/netstack-smoltcp/src/lib.rs +++ /dev/null @@ -1,22 +0,0 @@ -mod device; - -mod runner; -pub use runner::Runner; - -mod packet; -pub use packet::AnyIpPktFrame; - -mod filter; -pub use filter::{IpFilter, IpFilters}; - -pub mod udp; -pub use udp::UdpSocket; - -pub mod tcp; -pub use tcp::{TcpListener, TcpStream}; - -pub mod stack; -pub use stack::{Stack, StackBuilder}; - -/// Re-export -pub use smoltcp; diff --git a/netstack-smoltcp/src/packet.rs b/netstack-smoltcp/src/packet.rs deleted file mode 100644 index a12521b..0000000 --- a/netstack-smoltcp/src/packet.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::net::IpAddr; - -use smoltcp::wire::{IpProtocol, IpVersion, Ipv4Packet, Ipv6Packet}; - -pub type AnyIpPktFrame = Vec; - -#[derive(Debug)] -pub(super) enum IpPacket> { - Ipv4(Ipv4Packet), - Ipv6(Ipv6Packet), -} - -impl + Copy> IpPacket { - pub fn new_checked(packet: T) -> smoltcp::wire::Result> { - let buffer = packet.as_ref(); - match IpVersion::of_packet(buffer)? { - IpVersion::Ipv4 => Ok(IpPacket::Ipv4(Ipv4Packet::new_checked(packet)?)), - IpVersion::Ipv6 => Ok(IpPacket::Ipv6(Ipv6Packet::new_checked(packet)?)), - } - } - - pub fn src_addr(&self) -> IpAddr { - match *self { - IpPacket::Ipv4(ref packet) => IpAddr::from(packet.src_addr()), - IpPacket::Ipv6(ref packet) => IpAddr::from(packet.src_addr()), - } - } - - pub fn dst_addr(&self) -> IpAddr { - match *self { - IpPacket::Ipv4(ref packet) => IpAddr::from(packet.dst_addr()), - IpPacket::Ipv6(ref packet) => IpAddr::from(packet.dst_addr()), - } - } - - pub fn protocol(&self) -> IpProtocol { - match *self { - IpPacket::Ipv4(ref packet) => packet.next_header(), - IpPacket::Ipv6(ref packet) => packet.next_header(), - } - } -} - -impl<'a, T: AsRef<[u8]> + ?Sized> IpPacket<&'a T> { - /// Return a pointer to the payload. - #[inline] - pub fn payload(&self) -> &'a [u8] { - match *self { - IpPacket::Ipv4(ref packet) => packet.payload(), - IpPacket::Ipv6(ref packet) => packet.payload(), - } - } -} diff --git a/netstack-smoltcp/src/runner.rs b/netstack-smoltcp/src/runner.rs deleted file mode 100644 index dedf7e8..0000000 --- a/netstack-smoltcp/src/runner.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::{ - future::{Future, IntoFuture}, - pin::Pin, - task::{Context, Poll}, -}; - -/// BoxFuture acts the same as the [BoxFuture in crate futures utils], -/// which is an owned dynamically typed Future for use in cases where you -/// can’t statically type your result or need to add some indirection. -/// But the difference of this structure is that it will conditionally -/// implement Send according to the properties of type T, which does not -/// require two sets of API interfaces in single-threaded and multi-threaded. -/// -/// [BoxFuture in crate futures utils]: https://docs.rs/futures-util/latest/futures_util/future/type.BoxFuture.html -pub struct BoxFuture<'a, T>(Pin + Send + 'a>>); - -impl<'a, T> BoxFuture<'a, T> { - pub fn new(f: F) -> BoxFuture<'a, T> - where - F: IntoFuture + Send + 'a, - F::IntoFuture: Send + 'a, - { - BoxFuture(Box::pin(f.into_future())) - } - - #[allow(unused)] - pub fn wrap(f: Pin + Send + 'a>>) -> BoxFuture<'a, T> { - BoxFuture(f) - } -} - - - -impl Future for BoxFuture<'_, T> { - type Output = T; - - fn poll(mut self: Pin<&mut Self>, context: &mut Context<'_>) -> Poll { - self.0.as_mut().poll(context) - } -} - -pub type Runner = BoxFuture<'static, std::io::Result<()>>; diff --git a/netstack-smoltcp/src/stack.rs b/netstack-smoltcp/src/stack.rs deleted file mode 100644 index c30470d..0000000 --- a/netstack-smoltcp/src/stack.rs +++ /dev/null @@ -1,279 +0,0 @@ -use std::{ - net::IpAddr, - pin::Pin, - task::{ready, Context, Poll}, -}; - -use futures::{Sink, Stream}; -use smoltcp::wire::IpProtocol; -use tokio::sync::mpsc::{channel, Receiver}; -use tokio_util::sync::PollSender; -use tracing::{debug, trace}; - -use crate::{ - filter::{IpFilter, IpFilters}, - packet::{AnyIpPktFrame, IpPacket}, - runner::Runner, - tcp::TcpListener, - udp::UdpSocket, -}; - -pub struct StackBuilder { - enable_udp: bool, - enable_tcp: bool, - enable_icmp: bool, - stack_buffer_size: usize, - udp_buffer_size: usize, - tcp_buffer_size: usize, - mtu: usize, - ip_filters: IpFilters<'static>, -} - -impl Default for StackBuilder { - fn default() -> Self { - Self { - enable_udp: false, - enable_tcp: false, - enable_icmp: false, - stack_buffer_size: 1024, - udp_buffer_size: 512, - tcp_buffer_size: 512, - mtu: 1504, // 1500 for Ethernet + 4 for VLAN - ip_filters: IpFilters::with_non_broadcast(), - } - } -} - -#[allow(unused)] -impl StackBuilder { - pub fn enable_udp(mut self, enable: bool) -> Self { - self.enable_udp = enable; - self - } - - pub fn enable_tcp(mut self, enable: bool) -> Self { - self.enable_tcp = enable; - self - } - - pub fn enable_icmp(mut self, enable: bool) -> Self { - self.enable_icmp = enable; - self - } - - pub fn stack_buffer_size(mut self, size: usize) -> Self { - self.stack_buffer_size = size; - self - } - - pub fn udp_buffer_size(mut self, size: usize) -> Self { - self.udp_buffer_size = size; - self - } - - pub fn tcp_buffer_size(mut self, size: usize) -> Self { - self.tcp_buffer_size = size; - self - } - - pub fn set_ip_filters(mut self, filters: IpFilters<'static>) -> Self { - self.ip_filters = filters; - self - } - - pub fn add_ip_filter(mut self, filter: IpFilter<'static>) -> Self { - self.ip_filters.add(filter); - self - } - - pub fn add_ip_filter_fn(mut self, filter: F) -> Self - where - F: Fn(&IpAddr, &IpAddr) -> bool + Send + Sync + 'static, - { - self.ip_filters.add_fn(filter); - self - } - - pub fn mtu(mut self, mtu: usize) -> Self { - self.mtu = mtu; - self - } - - #[allow(clippy::type_complexity)] - pub fn build( - self, - ) -> std::io::Result<( - Stack, - Option, - Option, - Option, - )> { - let (stack_tx, stack_rx) = channel(self.stack_buffer_size); - - let (udp_tx, udp_rx) = if self.enable_udp { - let (udp_tx, udp_rx) = channel(self.udp_buffer_size); - (Some(PollSender::new(udp_tx)), Some(udp_rx)) - } else { - (None, None) - }; - - let (tcp_tx, tcp_rx) = if self.enable_tcp { - let (tcp_tx, tcp_rx) = channel(self.tcp_buffer_size); - (Some(PollSender::new(tcp_tx)), Some(tcp_rx)) - } else { - (None, None) - }; - - // ICMP is handled by TCP's Interface. - // smoltcp's interface will always send replies to EchoRequest - if self.enable_icmp && !self.enable_tcp { - use std::io::{Error, ErrorKind::InvalidInput}; - return Err(Error::new(InvalidInput, "ICMP requires TCP")); - } - let icmp_tx = if self.enable_icmp { - tcp_tx.clone() - } else { - None - }; - - let udp_socket = udp_rx.map(|udp_rx| UdpSocket::new(udp_rx, stack_tx.clone())); - - let (tcp_runner, tcp_listener) = if let Some(tcp_rx) = tcp_rx { - let (tcp_runner, tcp_listener) = TcpListener::new(tcp_rx, stack_tx, self.mtu)?; - (Some(tcp_runner), Some(tcp_listener)) - } else { - (None, None) - }; - - let stack = Stack { - ip_filters: self.ip_filters, - stack_rx, - sink_buf: None, - udp_tx, - tcp_tx, - icmp_tx, - }; - - Ok((stack, tcp_runner, udp_socket, tcp_listener)) - } -} - -pub struct Stack { - ip_filters: IpFilters<'static>, - sink_buf: Option<(AnyIpPktFrame, IpProtocol)>, - udp_tx: Option>, - tcp_tx: Option>, - icmp_tx: Option>, - stack_rx: Receiver, -} - -impl Stack { - fn poll_send(&mut self, cx: &mut Context<'_>) -> Poll> { - let (item, proto) = match self.sink_buf.take() { - Some(val) => val, - None => return Poll::Ready(Ok(())), - }; - - let tx = match proto { - IpProtocol::Tcp => self.tcp_tx.as_mut(), - IpProtocol::Udp => self.udp_tx.as_mut(), - IpProtocol::Icmp | IpProtocol::Icmpv6 => self.icmp_tx.as_mut(), - _ => unreachable!(), - }; - - let Some(tx) = tx else { - return Poll::Ready(Ok(())); - }; - - match tx.poll_reserve(cx) { - Poll::Pending => { - self.sink_buf = Some((item, proto)); - Poll::Pending - } - Poll::Ready(Err(_)) => Poll::Ready(Err(channel_closed_err("channel is closed"))), - Poll::Ready(Ok(_)) => match tx.send_item(item) { - Ok(()) => Poll::Ready(Ok(())), - Err(_) => Poll::Ready(Err(channel_closed_err("channel is closed"))), - }, - } - } -} - -// Recv from stack. -impl Stream for Stack { - type Item = std::io::Result; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.stack_rx.poll_recv(cx) { - Poll::Ready(Some(pkt)) => Poll::Ready(Some(Ok(pkt))), - Poll::Ready(None) => Poll::Ready(None), - Poll::Pending => Poll::Pending, - } - } -} - -// Send to stack. -impl Sink for Stack { - type Error = std::io::Error; - - fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - // If a buffered item exists, try to flush it first. This also properly - // registers the waker via poll_reserve so we get woken when the channel - // has capacity. Without this, returning Pending here with _cx unused - // means the task never gets rescheduled. - if self.sink_buf.is_some() { - ready!(self.poll_send(cx))?; - } - Poll::Ready(Ok(())) - } - - fn start_send(mut self: Pin<&mut Self>, item: AnyIpPktFrame) -> Result<(), Self::Error> { - if item.is_empty() { - return Ok(()); - } - - use std::io::{Error, ErrorKind::InvalidInput}; - let packet = IpPacket::new_checked(item.as_slice()) - .map_err(|err| Error::new(InvalidInput, format!("invalid IP packet: {err}")))?; - - let src_ip = packet.src_addr(); - let dst_ip = packet.dst_addr(); - - let addr_allowed = self.ip_filters.is_allowed(&src_ip, &dst_ip); - if !addr_allowed { - trace!("IP packet {src_ip} -> {dst_ip} (allowed? {addr_allowed}) throwing away",); - return Ok(()); - } - - let protocol = packet.protocol(); - if matches!( - protocol, - IpProtocol::Tcp | IpProtocol::Udp | IpProtocol::Icmp | IpProtocol::Icmpv6 - ) { - self.sink_buf.replace((item, protocol)); - } else { - debug!("tun IP packet ignored (protocol: {:?})", protocol); - } - - Ok(()) - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - self.poll_send(cx) - } - - fn poll_close( - mut self: Pin<&mut Self>, - _cx: &mut Context<'_>, - ) -> Poll> { - self.stack_rx.close(); - Poll::Ready(Ok(())) - } -} - -fn channel_closed_err(err: E) -> std::io::Error -where - E: Into>, -{ - std::io::Error::new(std::io::ErrorKind::BrokenPipe, err) -} diff --git a/netstack-smoltcp/src/tcp.rs b/netstack-smoltcp/src/tcp.rs deleted file mode 100644 index 91358d2..0000000 --- a/netstack-smoltcp/src/tcp.rs +++ /dev/null @@ -1,564 +0,0 @@ -use std::{ - collections::HashMap, - net::SocketAddr, - pin::Pin, - sync::{ - atomic::{AtomicBool, Ordering}, - Arc, - }, - task::{Context, Poll, Waker}, -}; - -use futures::Stream; -use smoltcp::{ - iface::{Config as InterfaceConfig, Interface, SocketHandle, SocketSet}, - phy::Device, - socket::tcp::{Socket as TcpSocket, SocketBuffer as TcpSocketBuffer, State as TcpState}, - storage::RingBuffer, - time::{Duration, Instant}, - wire::{HardwareAddress, IpAddress, IpCidr, IpProtocol, Ipv4Address, Ipv6Address, TcpPacket}, -}; -use spin::Mutex as SpinMutex; -use tokio::{ - io::{AsyncRead, AsyncWrite, ReadBuf}, - sync::{ - mpsc::{channel, Receiver, Sender, UnboundedSender}, - Notify, - }, -}; -use tracing::{error, trace}; - -use crate::{ - device::VirtualDevice, - packet::{AnyIpPktFrame, IpPacket}, - Runner, -}; - -// Reduced buffer sizes to 16KB to prevent excessive memory overhead (was 0x3FFF * 20 = 327KB per buffer) -const DEFAULT_TCP_SEND_BUFFER_SIZE: u32 = 16384; -const DEFAULT_TCP_RECV_BUFFER_SIZE: u32 = 16384; - -#[derive(Debug, Clone, Copy, Eq, PartialEq)] -enum TcpSocketState { - Normal, - Close, - Closing, - Closed, -} - -struct TcpSocketControl { - send_buffer: RingBuffer<'static, u8>, - send_waker: Option, - recv_buffer: RingBuffer<'static, u8>, - recv_waker: Option, - recv_state: TcpSocketState, - send_state: TcpSocketState, -} - -struct TcpSocketCreation { - control: SharedControl, - socket: TcpSocket<'static>, -} - -type SharedNotify = Arc; -type SharedControl = Arc>; - -struct TcpListenerRunner; - -impl TcpListenerRunner { - fn create( - device: VirtualDevice, - iface: Interface, - iface_ingress_tx: UnboundedSender>, - iface_ingress_tx_avail: Arc, - tcp_rx: Receiver, - stream_tx: Sender, - sockets: HashMap, - ) -> Runner { - Runner::new(async move { - let notify = Arc::new(Notify::new()); - let (socket_tx, socket_rx) = channel::(1024); - let res = tokio::select! { - v = Self::handle_packet(notify.clone(), iface_ingress_tx, iface_ingress_tx_avail.clone(), tcp_rx, stream_tx, socket_tx) => v, - v = Self::handle_socket(notify, device, iface, iface_ingress_tx_avail, sockets, socket_rx) => v, - }; - res?; - trace!("VirtDevice::poll thread exited"); - Ok(()) - }) - } - - async fn handle_packet( - notify: SharedNotify, - iface_ingress_tx: UnboundedSender>, - iface_ingress_tx_avail: Arc, - mut tcp_rx: Receiver, - stream_tx: Sender, - socket_tx: Sender, - ) -> std::io::Result<()> { - while let Some(frame) = tcp_rx.recv().await { - let packet = match IpPacket::new_checked(frame.as_slice()) { - Ok(p) => p, - Err(err) => { - error!("invalid TCP IP packet: {:?}", err,); - continue; - } - }; - - // Specially handle icmp packet by TCP interface. - if matches!(packet.protocol(), IpProtocol::Icmp | IpProtocol::Icmpv6) { - iface_ingress_tx - .send(frame) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::BrokenPipe, e))?; - iface_ingress_tx_avail.store(true, Ordering::Release); - notify.notify_one(); - continue; - } - - let src_ip = packet.src_addr(); - let dst_ip = packet.dst_addr(); - let payload = packet.payload(); - - let packet = match TcpPacket::new_checked(payload) { - Ok(p) => p, - Err(err) => { - error!("invalid TCP err: {err}, src_ip: {src_ip}, dst_ip: {dst_ip}, payload: {payload:?}"); - continue; - } - }; - let src_port = packet.src_port(); - let dst_port = packet.dst_port(); - - let src_addr = SocketAddr::new(src_ip, src_port); - let dst_addr = SocketAddr::new(dst_ip, dst_port); - - // TCP first handshake packet, create a new Connection - if packet.syn() && !packet.ack() { - let mut socket = TcpSocket::new( - TcpSocketBuffer::new(vec![0u8; DEFAULT_TCP_RECV_BUFFER_SIZE as usize]), - TcpSocketBuffer::new(vec![0u8; DEFAULT_TCP_SEND_BUFFER_SIZE as usize]), - ); - socket.set_keep_alive(Some(Duration::from_secs(28))); - // FIXME: It should follow system's setting. 7200 is Linux's default. - socket.set_timeout(Some(Duration::from_secs(7200))); - // NO ACK delay - // socket.set_ack_delay(None); - - if let Err(err) = socket.listen(dst_addr) { - error!("listen error: {:?}", err); - continue; - } - - trace!("created TCP connection for {} <-> {}", src_addr, dst_addr); - - let control = Arc::new(SpinMutex::new(TcpSocketControl { - send_buffer: RingBuffer::new(vec![0u8; DEFAULT_TCP_SEND_BUFFER_SIZE as usize]), - send_waker: None, - recv_buffer: RingBuffer::new(vec![0u8; DEFAULT_TCP_RECV_BUFFER_SIZE as usize]), - recv_waker: None, - recv_state: TcpSocketState::Normal, - send_state: TcpSocketState::Normal, - })); - - if let Err(_) = stream_tx.try_send(TcpStream { - src_addr, - dst_addr, - notify: notify.clone(), - control: control.clone(), - }) { - error!("stream_tx full or dropped, dropping SYN from {}", src_addr); - continue; - } - - if let Err(_) = socket_tx.try_send(TcpSocketCreation { control, socket }) { - error!("socket_tx full or dropped, dropping SYN from {}", src_addr); - continue; - } - } - - // Pipeline tcp stream packet - iface_ingress_tx - .send(frame) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::BrokenPipe, e))?; - iface_ingress_tx_avail.store(true, Ordering::Release); - notify.notify_one(); - } - Ok(()) - } - - async fn handle_socket( - notify: SharedNotify, - mut device: VirtualDevice, - mut iface: Interface, - iface_ingress_tx_avail: Arc, - mut sockets: HashMap, - mut socket_rx: Receiver, - ) -> std::io::Result<()> { - let mut socket_set = SocketSet::new(vec![]); - loop { - while let Ok(TcpSocketCreation { control, socket }) = socket_rx.try_recv() { - let handle = socket_set.add(socket); - sockets.insert(handle, control); - } - - let before_poll = Instant::now(); - let updated_sockets = iface.poll(before_poll, &mut device, &mut socket_set); - if matches!( - updated_sockets, - smoltcp::iface::PollResult::SocketStateChanged - ) { - trace!("VirtDevice::poll costed {}", Instant::now() - before_poll); - } - - // Check all the sockets' status - let mut sockets_to_remove = Vec::new(); - - for (socket_handle, control) in sockets.iter() { - let socket_handle = *socket_handle; - let socket = socket_set.get_mut::(socket_handle); - let mut control = control.lock(); - - // Remove the socket only when it is in the closed state. - if socket.state() == TcpState::Closed { - sockets_to_remove.push(socket_handle); - - control.send_state = TcpSocketState::Closed; - control.recv_state = TcpSocketState::Closed; - - if let Some(waker) = control.send_waker.take() { - waker.wake(); - } - if let Some(waker) = control.recv_waker.take() { - waker.wake(); - } - - trace!("closed TCP connection"); - continue; - } - - // SHUT_WR — only close once the send_buffer has been fully - // drained into the smoltcp socket. Closing earlier transitions - // the socket to FIN_WAIT_1, making can_send() return false, so - // the send loop below never runs and the remaining data is lost. - if matches!(control.send_state, TcpSocketState::Close) - && control.send_buffer.is_empty() - { - trace!("closing TCP Write Half, {:?}", socket.state()); - - socket.close(); - control.send_state = TcpSocketState::Closing; - } - - // Check if readable - let mut wake_receiver = false; - while socket.can_recv() && !control.recv_buffer.is_full() { - let result = socket.recv(|buffer| { - let n = control.recv_buffer.enqueue_slice(buffer); - (n, ()) - }); - - match result { - Ok(..) => wake_receiver = true, - Err(err) => { - error!("socket recv error: {:?}, {:?}", err, socket.state()); - - // Don't know why. Abort the connection. - socket.abort(); - - if matches!(control.recv_state, TcpSocketState::Normal) { - control.recv_state = TcpSocketState::Closed; - } - wake_receiver = true; - - // The socket will be recycled in the next poll. - break; - } - } - } - - // If socket is not in ESTABLISH, FIN-WAIT-1, FIN-WAIT-2, - // the local client have closed our receiver. - let states = [ - TcpState::Listen, - TcpState::SynReceived, - TcpState::Established, - TcpState::FinWait1, - TcpState::FinWait2, - ]; - if matches!(control.recv_state, TcpSocketState::Normal) - && !socket.may_recv() - && !states.contains(&socket.state()) - { - trace!("closed TCP Read Half, {:?}", socket.state()); - - // Let TcpStream::poll_read returns EOF. - control.recv_state = TcpSocketState::Closed; - wake_receiver = true; - } - - if wake_receiver && control.recv_waker.is_some() { - if let Some(waker) = control.recv_waker.take() { - waker.wake(); - } - } - - // Check if writable - let mut wake_sender = false; - while socket.can_send() && !control.send_buffer.is_empty() { - let result = socket.send(|buffer| { - let n = control.send_buffer.dequeue_slice(buffer); - (n, ()) - }); - - match result { - Ok(..) => wake_sender = true, - Err(err) => { - error!("socket send error: {:?}, {:?}", err, socket.state()); - - // Don't know why. Abort the connection. - socket.abort(); - - if matches!(control.send_state, TcpSocketState::Normal) { - control.send_state = TcpSocketState::Closed; - } - wake_sender = true; - - // The socket will be recycled in the next poll. - break; - } - } - } - - if wake_sender && control.send_waker.is_some() { - if let Some(waker) = control.send_waker.take() { - waker.wake(); - } - } - } - - for socket_handle in sockets_to_remove { - sockets.remove(&socket_handle); - socket_set.remove(socket_handle); - } - - if !iface_ingress_tx_avail.load(Ordering::Acquire) { - let next_duration = iface - .poll_delay(before_poll, &socket_set) - .unwrap_or(Duration::from_millis(5)); - if next_duration != Duration::ZERO { - let _ = tokio::time::timeout( - tokio::time::Duration::from(next_duration), - notify.notified(), - ) - .await; - } - } - } - } -} - -pub struct TcpListener { - stream_rx: Receiver, -} - -impl TcpListener { - pub(super) fn new( - tcp_rx: Receiver, - stack_tx: Sender, - mtu: usize, - ) -> std::io::Result<(Runner, Self)> { - let (mut device, iface_ingress_tx, iface_ingress_tx_avail) = - VirtualDevice::new(stack_tx, mtu); - let iface = Self::create_interface(&mut device)?; - - let (stream_tx, stream_rx) = channel(1024); - - let runner = TcpListenerRunner::create( - device, - iface, - iface_ingress_tx, - iface_ingress_tx_avail, - tcp_rx, - stream_tx, - HashMap::new(), - ); - - Ok((runner, Self { stream_rx })) - } - - fn create_interface(device: &mut D) -> std::io::Result - where - D: Device + ?Sized, - { - let mut iface_config = InterfaceConfig::new(HardwareAddress::Ip); - iface_config.random_seed = rand::random(); - let mut iface = Interface::new(iface_config, device, Instant::now()); - iface.update_ip_addrs(|ip_addrs| { - ip_addrs - .push(IpCidr::new(IpAddress::v4(0, 0, 0, 1), 0)) - .expect("iface IPv4"); - ip_addrs - .push(IpCidr::new(IpAddress::v6(0, 0, 0, 0, 0, 0, 0, 1), 0)) - .expect("iface IPv6"); - }); - iface - .routes_mut() - .add_default_ipv4_route(Ipv4Address::new(0, 0, 0, 1)) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::AddrNotAvailable, e))?; - iface - .routes_mut() - .add_default_ipv6_route(Ipv6Address::new(0, 0, 0, 0, 0, 0, 0, 1)) - .map_err(|e| std::io::Error::new(std::io::ErrorKind::AddrNotAvailable, e))?; - iface.set_any_ip(true); - Ok(iface) - } -} - -impl Stream for TcpListener { - type Item = (TcpStream, SocketAddr, SocketAddr); - - fn poll_next( - mut self: std::pin::Pin<&mut Self>, - cx: &mut std::task::Context<'_>, - ) -> std::task::Poll> { - self.stream_rx.poll_recv(cx).map(|stream| { - stream.map(|stream| { - let local_addr = *stream.local_addr(); - let remote_addr: SocketAddr = *stream.remote_addr(); - (stream, local_addr, remote_addr) - }) - }) - } -} - -pub struct TcpStream { - src_addr: SocketAddr, - dst_addr: SocketAddr, - notify: SharedNotify, - control: SharedControl, -} - -impl Drop for TcpStream { - fn drop(&mut self) { - let mut control = self.control.lock(); - - if matches!(control.recv_state, TcpSocketState::Normal) { - control.recv_state = TcpSocketState::Close; - } - - if matches!(control.send_state, TcpSocketState::Normal) { - control.send_state = TcpSocketState::Close; - } - - self.notify.notify_one(); - } -} - -impl TcpStream { - pub fn local_addr(&self) -> &SocketAddr { - &self.src_addr - } - - pub fn remote_addr(&self) -> &SocketAddr { - &self.dst_addr - } -} - -impl AsyncRead for TcpStream { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - let mut control = self.control.lock(); - - // Read from buffer - if control.recv_buffer.is_empty() { - // If socket is already closed / half closed, just return EOF directly. - if matches!(control.recv_state, TcpSocketState::Closed) { - return Ok(()).into(); - } - - // Nothing could be read. Wait for notify. - if let Some(old_waker) = control.recv_waker.replace(cx.waker().clone()) { - if !old_waker.will_wake(cx.waker()) { - old_waker.wake(); - } - } - - return Poll::Pending; - } - - let recv_buf = buf.initialize_unfilled(); - let n = control.recv_buffer.dequeue_slice(recv_buf); - buf.advance(n); - - if n > 0 { - self.notify.notify_one(); - } - - Ok(()).into() - } -} - -impl AsyncWrite for TcpStream { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - let mut control = self.control.lock(); - - // If state == Close | Closing | Closed, the TCP stream WR half is closed. - if !matches!(control.send_state, TcpSocketState::Normal) { - return Err(std::io::ErrorKind::BrokenPipe.into()).into(); - } - - // Write to buffer - - if control.send_buffer.is_full() { - if let Some(old_waker) = control.send_waker.replace(cx.waker().clone()) { - if !old_waker.will_wake(cx.waker()) { - old_waker.wake(); - } - } - - return Poll::Pending; - } - - let n = control.send_buffer.enqueue_slice(buf); - - if n > 0 { - self.notify.notify_one(); - } - - Ok(n).into() - } - - fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll> { - Ok(()).into() - } - - fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - let mut control = self.control.lock(); - - if matches!(control.send_state, TcpSocketState::Closed | TcpSocketState::Closing) { - return Ok(()).into(); - } - - // SHUT_WR - if matches!(control.send_state, TcpSocketState::Normal) { - control.send_state = TcpSocketState::Close; - } - - if let Some(old_waker) = control.send_waker.replace(cx.waker().clone()) { - if !old_waker.will_wake(cx.waker()) { - old_waker.wake(); - } - } - - self.notify.notify_one(); - - Poll::Pending - } -} diff --git a/netstack-smoltcp/src/udp.rs b/netstack-smoltcp/src/udp.rs deleted file mode 100644 index ac896a2..0000000 --- a/netstack-smoltcp/src/udp.rs +++ /dev/null @@ -1,155 +0,0 @@ -use std::{ - net::SocketAddr, - pin::Pin, - task::{Context, Poll}, -}; - -use etherparse::PacketBuilder; -use futures::{ready, Sink, SinkExt, Stream}; -use smoltcp::wire::UdpPacket; -use tokio::sync::mpsc::{Receiver, Sender}; -use tokio_util::sync::PollSender; -use tracing::{error, trace}; - -use crate::packet::{AnyIpPktFrame, IpPacket}; - -pub type UdpMsg = ( - Vec, /* payload */ - SocketAddr, /* local */ - SocketAddr, /* remote */ -); - -pub struct UdpSocket { - udp_rx: Receiver, - stack_tx: PollSender, -} - -impl UdpSocket { - pub(super) fn new(udp_rx: Receiver, stack_tx: Sender) -> Self { - Self { - udp_rx, - stack_tx: PollSender::new(stack_tx), - } - } - - pub fn split(self) -> (ReadHalf, WriteHalf) { - ( - ReadHalf { - udp_rx: self.udp_rx, - }, - WriteHalf { - stack_tx: self.stack_tx, - }, - ) - } -} - -pub struct ReadHalf { - udp_rx: Receiver, -} - -pub struct WriteHalf { - stack_tx: PollSender, -} - -impl Stream for ReadHalf { - type Item = UdpMsg; - - fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context) -> Poll> { - loop { - match ready!(self.udp_rx.poll_recv(cx)) { - Some(frame) => { - let packet = match IpPacket::new_checked(frame.as_slice()) { - Ok(p) => p, - Err(err) => { - error!("invalid IP packet: {}", err); - continue; - } - }; - - let src_ip = packet.src_addr(); - let dst_ip = packet.dst_addr(); - let payload = packet.payload(); - - let packet = match UdpPacket::new_checked(payload) { - Ok(p) => p, - Err(err) => { - error!("invalid err: {err}, src_ip: {src_ip}, dst_ip: {dst_ip}, payload: {payload:?}"); - continue; - } - }; - let src_port = packet.src_port(); - let dst_port = packet.dst_port(); - - let src_addr = SocketAddr::new(src_ip, src_port); - let dst_addr = SocketAddr::new(dst_ip, dst_port); - - trace!("created UDP socket for {} <-> {}", src_addr, dst_addr); - - return Poll::Ready(Some((packet.payload().to_vec(), src_addr, dst_addr))); - } - None => return Poll::Ready(None), - } - } - } -} - -impl Sink for WriteHalf { - type Error = std::io::Error; - - fn poll_ready(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match ready!(self.stack_tx.poll_ready_unpin(cx)) { - Ok(()) => Poll::Ready(Ok(())), - Err(err) => Poll::Ready(Err(std::io::Error::other(err))), - } - } - - fn start_send(mut self: Pin<&mut Self>, item: UdpMsg) -> Result<(), Self::Error> { - use std::io::{Error, ErrorKind::InvalidData}; - let (data, src_addr, dst_addr) = item; - - if data.is_empty() { - return Ok(()); - } - - let builder = match (src_addr, dst_addr) { - (SocketAddr::V4(src), SocketAddr::V4(dst)) => { - PacketBuilder::ipv4(src.ip().octets(), dst.ip().octets(), 20) - .udp(src_addr.port(), dst_addr.port()) - } - (SocketAddr::V6(src), SocketAddr::V6(dst)) => { - PacketBuilder::ipv6(src.ip().octets(), dst.ip().octets(), 20) - .udp(src_addr.port(), dst_addr.port()) - } - _ => { - return Err(Error::new(InvalidData, "src or destination type unmatch")); - } - }; - - let mut ip_packet_writer = Vec::with_capacity(builder.size(data.len())); - builder - .write(&mut ip_packet_writer, &data) - .map_err(|err| Error::other(format!("PacketBuilder::write: {err}")))?; - - match self.stack_tx.start_send_unpin(ip_packet_writer) { - Ok(()) => Ok(()), - Err(err) => Err(Error::other(format!("send error: {err}"))), - } - } - - fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - use std::io::Error; - match ready!(self.stack_tx.poll_flush_unpin(cx)) { - Ok(()) => Poll::Ready(Ok(())), - Err(err) => Poll::Ready(Err(Error::other(format!("flush error: {err}")))), - } - } - - fn poll_close(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - use std::io::Error; - match ready!(self.stack_tx.poll_close_unpin(cx)) { - Ok(()) => Poll::Ready(Ok(())), - Err(err) => Poll::Ready(Err(Error::other(format!("close error: {err}")))), - } - } -} diff --git a/netstack-smoltcp/tests/regression.rs b/netstack-smoltcp/tests/regression.rs deleted file mode 100644 index ddd41f6..0000000 --- a/netstack-smoltcp/tests/regression.rs +++ /dev/null @@ -1,75 +0,0 @@ -//! Regression tests that reproduce the bugs found in the static analysis. - -use std::time::Duration; - -use etherparse::{IpNumber, Ipv4Header, UdpHeader}; -use futures::SinkExt; -use tokio::time::timeout; - -use netstack_smoltcp::StackBuilder; - -fn make_udp_ipv4( - src_ip: [u8; 4], - src_port: u16, - dst_ip: [u8; 4], - dst_port: u16, - payload: &[u8], -) -> Vec { - let udp_hdr = UdpHeader::with_ipv4_checksum( - src_port, - dst_port, - &Ipv4Header::new( - (UdpHeader::LEN + payload.len()) as u16, - 64, - IpNumber::UDP, - src_ip, - dst_ip, - ) - .unwrap(), - payload, - ) - .unwrap(); - - let ip_hdr = Ipv4Header::new( - (UdpHeader::LEN + payload.len()) as u16, - 64, - IpNumber::UDP, - src_ip, - dst_ip, - ) - .unwrap(); - - let mut buf = Vec::with_capacity(Ipv4Header::MIN_LEN + UdpHeader::LEN + payload.len()); - ip_hdr.write(&mut buf).unwrap(); - udp_hdr.write(&mut buf).unwrap(); - buf.extend_from_slice(payload); - buf -} - -/// before(include) a15e0b72bfc72cb032e67138070da01e325d66f8 -/// sink_buf is used in `Stack` to hold a slot for sending any pkt -/// -/// the original assumption is that the `poll_ready` -> `start_send` -> `poll_flush` -/// are called sequentially so the slot could be reused and will never get blocked. -/// -/// but once the user calls `send_all` on `Stack`, which will not immediate flush the pkt(call `poll_flush`), -/// then `sink_buf` is could be Some(pkt), then it will trigger `Poll::Pending` branch in `Stack::poll_ready`, -/// who did not register the waker correctly, so it will got hanged forever. -#[tokio::test(flavor = "current_thread")] -async fn bug1_poll_ready_waker_registered_via_send_all() { - let (mut stack, _runner, _udp_socket, _tcp) = StackBuilder::default() - .enable_udp(true) - .udp_buffer_size(64) - .stack_buffer_size(64) - .build() - .unwrap(); - - let pkt1 = make_udp_ipv4([1, 2, 3, 4], 1111, [5, 6, 7, 8], 9999, b"first"); - let pkt2 = make_udp_ipv4([1, 2, 3, 4], 1111, [5, 6, 7, 8], 9999, b"second"); - - let mut stream = futures::stream::iter([Ok(pkt1), Ok(pkt2)]); - - let result = timeout(Duration::from_secs(1), stack.send_all(&mut stream)).await; - // should be ok after the fix - assert!(result.is_ok()); -} diff --git a/ostp-web/dist/assets/index-CBAXD1xO.css b/ostp-web/dist/assets/index-CBAXD1xO.css deleted file mode 100644 index 16fd91a..0000000 --- a/ostp-web/dist/assets/index-CBAXD1xO.css +++ /dev/null @@ -1,2 +0,0 @@ -/*! tailwindcss v4.3.1 | MIT License | https://tailwindcss.com */ -@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-scale-x:1;--tw-scale-y:1;--tw-scale-z:1;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-pan-x:initial;--tw-pan-y:initial;--tw-pinch-zoom:initial;--tw-scroll-snap-strictness:proximity;--tw-space-y-reverse:0;--tw-space-x-reverse:0;--tw-divide-x-reverse:0;--tw-border-style:solid;--tw-divide-y-reverse:0;--tw-leading:initial;--tw-font-weight:initial;--tw-tracking:initial;--tw-ordinal:initial;--tw-slashed-zero:initial;--tw-numeric-figure:initial;--tw-numeric-spacing:initial;--tw-numeric-fraction:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-outline-style:solid;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial;--tw-duration:initial;--tw-ease:initial;--tw-contain-size:initial;--tw-contain-layout:initial;--tw-contain-paint:initial;--tw-contain-style:initial;--tw-text-shadow-color:initial;--tw-text-shadow-alpha:100%}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-500:oklch(63.7% .237 25.331);--color-yellow-500:oklch(79.5% .184 86.047);--color-gray-100:oklch(96.7% .003 264.542);--color-black:#000;--spacing:.25rem;--container-sm:24rem;--container-2xl:42rem;--container-4xl:56rem;--container-7xl:80rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-xl:1.25rem;--text-xl--line-height:calc(1.75 / 1.25);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-4xl:2.25rem;--text-4xl--line-height:calc(2.5 / 2.25);--text-5xl:3rem;--text-5xl--line-height:1;--text-6xl:3.75rem;--text-6xl--line-height:1;--text-7xl:4.5rem;--text-7xl--line-height:1;--text-8xl:6rem;--text-8xl--line-height:1;--text-9xl:8rem;--text-9xl--line-height:1;--font-weight-light:300;--font-weight-normal:400;--font-weight-medium:500;--font-weight-bold:700;--tracking-tight:-.025em;--tracking-wide:.025em;--tracking-wider:.05em;--tracking-widest:.1em;--leading-relaxed:1.625;--ease-in:cubic-bezier(.4, 0, 1, 1);--ease-out:cubic-bezier(0, 0, .2, 1);--ease-in-out:cubic-bezier(.4, 0, .2, 1);--blur-xl:24px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono);--color-primary:var(--color-primary);--color-bg:var(--color-bg);--color-text:var(--color-text);--color-surface:var(--color-surface);--color-muted:var(--color-muted)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}:root{--color-bg:#0a0a0a;--color-text:#f5f5f5;--color-primary:#e0e0e0;--color-surface:#1e1e1e99;--color-border:#ffffff1a;--color-muted:#888;--radius-main:16px;--shadow-main:0 8px 32px 0 #0006;--border-main:1px solid var(--color-border);--font-heading:"Playfair Display", serif;--font-body:"Inter", sans-serif;--glass-blur:16px;--bg-gradient:radial-gradient(circle at 15% 50%, #ffffff08, transparent 25%), radial-gradient(circle at 85% 30%, #ffffff05, transparent 25%)}body{background-color:var(--color-bg);color:var(--color-text);font-family:var(--font-body);min-height:100vh;transition:background-color .8s,color .8s;overflow-x:hidden}}@layer components;@layer utilities{.\@container-size\/sidebar{container:sidebar/size}.\@container,.\@container-\[inline-size\]{container-type:inline-size}.\@container-size{container-type:size}.pointer-events-auto{pointer-events:auto}.pointer-events-none{pointer-events:none}.collapse{visibility:collapse}.invisible{visibility:hidden}.visible{visibility:visible}.sr-only{clip-path:inset(50%);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}.not-sr-only{clip-path:none;white-space:normal;width:auto;height:auto;margin:0;padding:0;position:static;overflow:visible}.\!absolute{position:absolute!important}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.-inset-1{inset:calc(var(--spacing) * -1)}.inset-0{inset:0}.top-0{top:0}.top-1\/2{top:50%}.right-0{right:0}.right-6{right:calc(var(--spacing) * 6)}.right-full{right:100%}.bottom-6{bottom:calc(var(--spacing) * 6)}.left-0{left:0}.left-\[45\%\]{left:45%}.isolate{isolation:isolate}.isolation-auto{isolation:auto}.z-0{z-index:0}.z-10{z-index:10}.z-40{z-index:40}.z-50{z-index:50}.order-0,.order-none{order:0}.float-end{float:inline-end}.float-left{float:left}.float-none{float:none}.float-right{float:right}.float-start{float:inline-start}.clear-both{clear:both}.clear-end{clear:inline-end}.clear-left{clear:left}.clear-none{clear:none}.clear-right{clear:right}.clear-start{clear:inline-start}.\!container{width:100%!important}@media (width>=40rem){.\!container{max-width:40rem!important}}@media (width>=48rem){.\!container{max-width:48rem!important}}@media (width>=64rem){.\!container{max-width:64rem!important}}@media (width>=80rem){.\!container{max-width:80rem!important}}@media (width>=96rem){.\!container{max-width:96rem!important}}.container{width:100%}@media (width>=40rem){.container{max-width:40rem}}@media (width>=48rem){.container{max-width:48rem}}@media (width>=64rem){.container{max-width:64rem}}@media (width>=80rem){.container{max-width:80rem}}@media (width>=96rem){.container{max-width:96rem}}.container\!{width:100%!important}@media (width>=40rem){.container\!{max-width:40rem!important}}@media (width>=48rem){.container\!{max-width:48rem!important}}@media (width>=64rem){.container\!{max-width:64rem!important}}@media (width>=80rem){.container\!{max-width:80rem!important}}@media (width>=96rem){.container\!{max-width:96rem!important}}.m-2{margin:calc(var(--spacing) * 2)}.mx-2{margin-inline:calc(var(--spacing) * 2)}.mx-auto{margin-inline:auto}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-8{margin-top:calc(var(--spacing) * 8)}.mt-16{margin-top:calc(var(--spacing) * 16)}.mt-\[16px\]{margin-top:16px}.-mr-2{margin-right:calc(var(--spacing) * -2)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mr-4{margin-right:calc(var(--spacing) * 4)}.mb-1{margin-bottom:var(--spacing)}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.mb-8{margin-bottom:calc(var(--spacing) * 8)}.mb-12{margin-bottom:calc(var(--spacing) * 12)}.mb-20{margin-bottom:calc(var(--spacing) * 20)}.ml-1{margin-left:var(--spacing)}.ml-2{margin-left:calc(var(--spacing) * 2)}.box-border{box-sizing:border-box}.box-content{box-sizing:content-box}.block{display:block}.contents{display:contents}.flex{display:flex}.flow-root{display:flow-root}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.inline-flex{display:inline-flex}.inline-grid{display:inline-grid}.inline-table{display:inline-table}.list-item{display:list-item}.table{display:table}.table-caption{display:table-caption}.table-cell{display:table-cell}.table-column{display:table-column}.table-column-group{display:table-column-group}.table-footer-group{display:table-footer-group}.table-header-group{display:table-header-group}.table-row{display:table-row}.table-row-group{display:table-row-group}.field-sizing-content{field-sizing:content}.field-sizing-fixed{field-sizing:fixed}.size-auto{width:auto;height:auto}.h-12{height:calc(var(--spacing) * 12)}.h-\[600px\]{height:600px}.h-auto{height:auto}.h-full{height:100%}.h-lh{height:1lh}.h-px{height:1px}.h-screen{height:100vh}.max-h-lh{max-height:1lh}.max-h-none{max-height:none}.max-h-screen{max-height:100vh}.min-h-\[70vh\]{min-height:70vh}.min-h-\[75vh\]{min-height:75vh}.min-h-\[85vh\]{min-height:85vh}.min-h-auto{min-height:auto}.min-h-lh{min-height:1lh}.min-h-screen{min-height:100vh}.w-1\/2{width:50%}.w-12{width:calc(var(--spacing) * 12)}.w-24{width:calc(var(--spacing) * 24)}.w-\[80vw\]{width:80vw}.w-\[600px\]{width:600px}.w-auto{width:auto}.w-full{width:100%}.w-screen{width:100vw}.max-w-2xl{max-width:var(--container-2xl)}.max-w-4xl{max-width:var(--container-4xl)}.max-w-7xl{max-width:var(--container-7xl)}.max-w-\[800px\]{max-width:800px}.max-w-full{max-width:100%}.max-w-none{max-width:none}.max-w-screen{max-width:100vw}.max-w-sm{max-width:var(--container-sm)}.min-w-auto{min-width:auto}.min-w-screen{min-width:100vw}.flex-1{flex:1}.flex-auto{flex:auto}.flex-initial{flex:0 auto}.flex-none{flex:none}.flex-shrink,.shrink{flex-shrink:1}.flex-grow,.grow{flex-grow:1}.basis-auto{flex-basis:auto}.basis-full{flex-basis:100%}.table-auto{table-layout:auto}.table-fixed{table-layout:fixed}.caption-bottom{caption-side:bottom}.caption-top{caption-side:top}.border-collapse{border-collapse:collapse}.border-separate{border-collapse:separate}.-translate-full{--tw-translate-x:-100%;--tw-translate-y:-100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-full{--tw-translate-x:100%;--tw-translate-y:100%;translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.-translate-y-1\/2{--tw-translate-y:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.translate-3d{translate:var(--tw-translate-x) var(--tw-translate-y) var(--tw-translate-z)}.translate-none{translate:none}.scale-3d{scale:var(--tw-scale-x) var(--tw-scale-y) var(--tw-scale-z)}.scale-none{scale:none}.rotate-none{rotate:none}.transform,.transform-cpu{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.transform-gpu{transform:translateZ(0) var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.transform-none{transform:none}.cursor-pointer{cursor:pointer}.touch-pinch-zoom{--tw-pinch-zoom:pinch-zoom;touch-action:var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)}.resize{resize:both}.resize-none{resize:none}.resize-x{resize:horizontal}.resize-y{resize:vertical}.snap-none{scroll-snap-type:none}.snap-mandatory{--tw-scroll-snap-strictness:mandatory}.snap-proximity{--tw-scroll-snap-strictness:proximity}.snap-align-none{scroll-snap-align:none}.snap-center{scroll-snap-align:center}.snap-end{scroll-snap-align:end}.snap-start{scroll-snap-align:start}.snap-always{scroll-snap-stop:always}.snap-normal{scroll-snap-stop:normal}.scrollbar-auto{scrollbar-width:auto}.scrollbar-none{scrollbar-width:none}.scrollbar-thin{scrollbar-width:thin}.scrollbar-gutter-auto{scrollbar-gutter:auto}.scrollbar-gutter-both{scrollbar-gutter:stable both-edges}.scrollbar-gutter-stable{scrollbar-gutter:stable}.list-inside{list-style-position:inside}.list-outside{list-style-position:outside}.list-disc{list-style-type:disc}.appearance-auto{appearance:auto}.appearance-none{appearance:none}.columns-2{columns:2}.columns-3{columns:3}.columns-4{columns:4}.grid-flow-col{grid-auto-flow:column}.grid-flow-col-dense{grid-auto-flow:column dense}.grid-flow-dense{grid-auto-flow:dense}.grid-flow-row{grid-auto-flow:row}.grid-flow-row-dense{grid-auto-flow:dense}.grid-cols-1{grid-template-columns:repeat(1,minmax(0,1fr))}.flex-col{flex-direction:column}.flex-col-reverse{flex-direction:column-reverse}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-nowrap{flex-wrap:nowrap}.flex-wrap{flex-wrap:wrap}.flex-wrap-reverse{flex-wrap:wrap-reverse}.place-content-around{place-content:space-around}.place-content-baseline{place-content:baseline start}.place-content-between{place-content:space-between}.place-content-center{place-content:center}.place-content-center-safe{place-content:safe center}.place-content-end{place-content:end}.place-content-end-safe{place-content:safe end}.place-content-evenly{place-content:space-evenly}.place-content-start{place-content:start}.place-content-stretch{place-content:stretch}.place-items-baseline{place-items:baseline}.place-items-center{place-items:center}.place-items-center-safe{place-items:safe center}.place-items-end{place-items:end}.place-items-end-safe{place-items:safe end}.place-items-start{place-items:start}.place-items-stretch{place-items:stretch stretch}.content-around{align-content:space-around}.content-baseline{align-content:baseline}.content-between{align-content:space-between}.content-center{align-content:center}.content-center-safe{align-content:safe center}.content-end{align-content:flex-end}.content-end-safe{align-content:safe flex-end}.content-evenly{align-content:space-evenly}.content-normal{align-content:normal}.content-start{align-content:flex-start}.content-stretch{align-content:stretch}.items-baseline{align-items:baseline}.items-baseline-last{align-items:last baseline}.items-center{align-items:center}.items-center-safe{align-items:safe center}.items-end{align-items:flex-end}.items-end-safe{align-items:safe flex-end}.items-start{align-items:flex-start}.items-stretch{align-items:stretch}.justify-around{justify-content:space-around}.justify-baseline{justify-content:baseline}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-center-safe{justify-content:safe center}.justify-end{justify-content:flex-end}.justify-end-safe{justify-content:safe flex-end}.justify-evenly{justify-content:space-evenly}.justify-normal{justify-content:normal}.justify-start{justify-content:flex-start}.justify-stretch{justify-content:stretch}.justify-items-center{justify-items:center}.justify-items-center-safe{justify-items:safe center}.justify-items-end{justify-items:end}.justify-items-end-safe{justify-items:safe end}.justify-items-normal{justify-items:normal}.justify-items-start{justify-items:start}.justify-items-stretch{justify-items:stretch}.gap-1{gap:var(--spacing)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}.gap-5{gap:calc(var(--spacing) * 5)}.gap-6{gap:calc(var(--spacing) * 6)}.gap-8{gap:calc(var(--spacing) * 8)}.gap-12{gap:calc(var(--spacing) * 12)}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-6>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 6) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 6) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-reverse>:not(:last-child)){--tw-space-y-reverse:1}.gap-x-8{column-gap:calc(var(--spacing) * 8)}:where(.space-x-reverse>:not(:last-child)){--tw-space-x-reverse:1}.gap-y-16{row-gap:calc(var(--spacing) * 16)}:where(.divide-x>:not(:last-child)){--tw-divide-x-reverse:0;border-inline-style:var(--tw-border-style);border-inline-start-width:calc(1px * var(--tw-divide-x-reverse));border-inline-end-width:calc(1px * calc(1 - var(--tw-divide-x-reverse)))}:where(.divide-y>:not(:last-child)){--tw-divide-y-reverse:0;border-bottom-style:var(--tw-border-style);border-top-style:var(--tw-border-style);border-top-width:calc(1px * var(--tw-divide-y-reverse));border-bottom-width:calc(1px * calc(1 - var(--tw-divide-y-reverse)))}:where(.divide-y-reverse>:not(:last-child)){--tw-divide-y-reverse:1}.place-self-auto{place-self:auto}.place-self-center{place-self:center}.place-self-center-safe{place-self:safe center}.place-self-end{place-self:end}.place-self-end-safe{place-self:safe end}.place-self-start{place-self:start}.place-self-stretch{place-self:stretch stretch}.self-auto{align-self:auto}.self-baseline{align-self:baseline}.self-baseline-last{align-self:last baseline}.self-center{align-self:center}.self-center-safe{align-self:safe center}.self-end{align-self:flex-end}.self-end-safe{align-self:safe flex-end}.self-start{align-self:flex-start}.self-stretch{align-self:stretch}.justify-self-auto{justify-self:auto}.justify-self-center{justify-self:center}.justify-self-center-safe{justify-self:safe center}.justify-self-end{justify-self:flex-end}.justify-self-end-safe{justify-self:safe flex-end}.justify-self-start{justify-self:flex-start}.justify-self-stretch{justify-self:stretch}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-hidden{overflow:hidden}.scroll-auto{scroll-behavior:auto}.scroll-smooth{scroll-behavior:smooth}.\!rounded-none{border-radius:0!important}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-s{border-start-start-radius:.25rem;border-end-start-radius:.25rem}.rounded-ss{border-start-start-radius:.25rem}.rounded-e{border-start-end-radius:.25rem;border-end-end-radius:.25rem}.rounded-se{border-start-end-radius:.25rem}.rounded-ee{border-end-end-radius:.25rem}.rounded-es{border-end-start-radius:.25rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-l{border-top-left-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-tl{border-top-left-radius:.25rem}.rounded-r{border-top-right-radius:.25rem;border-bottom-right-radius:.25rem}.rounded-tr{border-top-right-radius:.25rem}.rounded-b{border-bottom-right-radius:.25rem;border-bottom-left-radius:.25rem}.rounded-br{border-bottom-right-radius:.25rem}.rounded-bl{border-bottom-left-radius:.25rem}.border{border-style:var(--tw-border-style);border-width:1px}.\!border-x-0{border-inline-style:var(--tw-border-style)!important;border-inline-width:0!important}.border-x{border-inline-style:var(--tw-border-style);border-inline-width:1px}.border-y{border-block-style:var(--tw-border-style);border-block-width:1px}.border-s{border-inline-start-style:var(--tw-border-style);border-inline-start-width:1px}.border-e{border-inline-end-style:var(--tw-border-style);border-inline-end-width:1px}.border-bs{border-block-start-style:var(--tw-border-style);border-block-start-width:1px}.border-be{border-block-end-style:var(--tw-border-style);border-block-end-width:1px}.\!border-t-0{border-top-style:var(--tw-border-style)!important;border-top-width:0!important}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-r{border-right-style:var(--tw-border-style);border-right-width:1px}.\!border-b-0{border-bottom-style:var(--tw-border-style)!important;border-bottom-width:0!important}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-dashed{--tw-border-style:dashed;border-style:dashed}.border-dotted{--tw-border-style:dotted;border-style:dotted}.border-double{--tw-border-style:double;border-style:double}.border-hidden{--tw-border-style:hidden;border-style:hidden}.border-none{--tw-border-style:none;border-style:none}.border-solid{--tw-border-style:solid;border-style:solid}.border-\[var\(--color-border\)\],.border-\[var\(--color-border\)\]\/50{border-color:var(--color-border)}@supports (color:color-mix(in lab, red, red)){.border-\[var\(--color-border\)\]\/50{border-color:color-mix(in oklab, var(--color-border) 50%, transparent)}}.bg-\(--my_variable\){background-color:var(--my_variable)}.bg-\(color\:--my-color\){background-color:var(--my-color)}.bg-\[\#0088cc\]{background-color:#08c}.bg-\[\#B91C1C\]{background-color:#b91c1c}.bg-\[color\:var\(--my-color\)\]{background-color:var(--my-color)}.bg-\[var\(--color-bg\)\]\/95{background-color:var(--color-bg)}@supports (color:color-mix(in lab, red, red)){.bg-\[var\(--color-bg\)\]\/95{background-color:color-mix(in oklab, var(--color-bg) 95%, transparent)}}.bg-\[var\(--color-border\)\]{background-color:var(--color-border)}.bg-\[var\(--color-primary\)\]{background-color:var(--color-primary)}.bg-\[var\(--color-surface\)\],.bg-\[var\(--color-surface\)\]\/30{background-color:var(--color-surface)}@supports (color:color-mix(in lab, red, red)){.bg-\[var\(--color-surface\)\]\/30{background-color:color-mix(in oklab, var(--color-surface) 30%, transparent)}}.bg-\[var\(--color-text\)\],.bg-\[var\(--color-text\)\]\/5{background-color:var(--color-text)}@supports (color:color-mix(in lab, red, red)){.bg-\[var\(--color-text\)\]\/5{background-color:color-mix(in oklab, var(--color-text) 5%, transparent)}}.bg-\[var\(--my_variable\)\]{background-color:var(--my_variable)}.bg-gray-100{background-color:var(--color-gray-100)}.bg-gray-100\/50{background-color:#f3f4f680}@supports (color:color-mix(in lab, red, red)){.bg-gray-100\/50{background-color:color-mix(in oklab, var(--color-gray-100) 50%, transparent)}}.bg-red-500{background-color:var(--color-red-500)}.bg-red-500\/50{background-color:#fb2c3680}@supports (color:color-mix(in lab, red, red)){.bg-red-500\/50{background-color:color-mix(in oklab, var(--color-red-500) 50%, transparent)}}.bg-red-500\/\[50\%\]{background-color:#fb2c3680}@supports (color:color-mix(in lab, red, red)){.bg-red-500\/\[50\%\]{background-color:color-mix(in oklab, var(--color-red-500) 50%, transparent)}}.bg-transparent{background-color:#0000}.-bg-conic,.bg-conic{--tw-gradient-position:in oklab;background-image:conic-gradient(var(--tw-gradient-stops))}.bg-radial{--tw-gradient-position:in oklab;background-image:radial-gradient(var(--tw-gradient-stops))}.bg-none{background-image:none}.via-none{--tw-gradient-via-stops:initial}.mask-none{-webkit-mask-image:none;mask-image:none}.mask-circle{--tw-mask-radial-shape:circle}.mask-ellipse{--tw-mask-radial-shape:ellipse}.mask-radial-closest-corner{--tw-mask-radial-size:closest-corner}.mask-radial-closest-side{--tw-mask-radial-size:closest-side}.mask-radial-farthest-corner{--tw-mask-radial-size:farthest-corner}.mask-radial-farthest-side{--tw-mask-radial-size:farthest-side}.mask-radial-at-bottom{--tw-mask-radial-position:bottom}.mask-radial-at-bottom-left{--tw-mask-radial-position:bottom left}.mask-radial-at-bottom-right{--tw-mask-radial-position:bottom right}.mask-radial-at-center{--tw-mask-radial-position:center}.mask-radial-at-left{--tw-mask-radial-position:left}.mask-radial-at-right{--tw-mask-radial-position:right}.mask-radial-at-top{--tw-mask-radial-position:top}.mask-radial-at-top-left{--tw-mask-radial-position:top left}.mask-radial-at-top-right{--tw-mask-radial-position:top right}.box-decoration-clone{-webkit-box-decoration-break:clone;box-decoration-break:clone}.box-decoration-slice{-webkit-box-decoration-break:slice;box-decoration-break:slice}.decoration-clone{-webkit-box-decoration-break:clone;box-decoration-break:clone}.decoration-slice{-webkit-box-decoration-break:slice;box-decoration-break:slice}.bg-auto{background-size:auto}.bg-contain{background-size:contain}.bg-cover{background-size:cover}.bg-fixed{background-attachment:fixed}.bg-local{background-attachment:local}.bg-scroll{background-attachment:scroll}.bg-clip-border{background-clip:border-box}.bg-clip-content{background-clip:content-box}.bg-clip-padding{background-clip:padding-box}.bg-clip-text{-webkit-background-clip:text;background-clip:text}.bg-bottom{background-position:bottom}.bg-bottom-left{background-position:0 100%}.bg-bottom-right{background-position:100% 100%}.bg-center{background-position:50%}.bg-left{background-position:0}.bg-left-bottom{background-position:0 100%}.bg-left-top{background-position:0 0}.bg-right{background-position:100%}.bg-right-bottom{background-position:100% 100%}.bg-right-top{background-position:100% 0}.bg-top{background-position:top}.bg-top-left{background-position:0 0}.bg-top-right{background-position:100% 0}.bg-no-repeat{background-repeat:no-repeat}.bg-repeat{background-repeat:repeat}.bg-repeat-round{background-repeat:round}.bg-repeat-space{background-repeat:space}.bg-repeat-x{background-repeat:repeat-x}.bg-repeat-y{background-repeat:repeat-y}.bg-origin-border{background-origin:border-box}.bg-origin-content{background-origin:content-box}.bg-origin-padding{background-origin:padding-box}.mask-add{-webkit-mask-composite:source-over;-webkit-mask-composite:source-over;-webkit-mask-composite:source-over;mask-composite:add}.mask-exclude{-webkit-mask-composite:xor;-webkit-mask-composite:xor;-webkit-mask-composite:xor;mask-composite:exclude}.mask-intersect{-webkit-mask-composite:source-in;-webkit-mask-composite:source-in;-webkit-mask-composite:source-in;mask-composite:intersect}.mask-subtract{-webkit-mask-composite:source-out;-webkit-mask-composite:source-out;-webkit-mask-composite:source-out;mask-composite:subtract}.mask-alpha{-webkit-mask-source-type:alpha;-webkit-mask-source-type:alpha;-webkit-mask-source-type:alpha;mask-mode:alpha}.mask-luminance{-webkit-mask-source-type:luminance;-webkit-mask-source-type:luminance;-webkit-mask-source-type:luminance;mask-mode:luminance}.mask-match{-webkit-mask-source-type:auto;-webkit-mask-source-type:auto;-webkit-mask-source-type:auto;mask-mode:match-source}.mask-type-alpha{mask-type:alpha}.mask-type-luminance{mask-type:luminance}.mask-auto{-webkit-mask-size:auto;mask-size:auto}.mask-contain{-webkit-mask-size:contain;mask-size:contain}.mask-cover{-webkit-mask-size:cover;mask-size:cover}.mask-clip-border{-webkit-mask-clip:border-box;mask-clip:border-box}.mask-clip-content{-webkit-mask-clip:content-box;mask-clip:content-box}.mask-clip-fill{-webkit-mask-clip:fill-box;mask-clip:fill-box}.mask-clip-padding{-webkit-mask-clip:padding-box;mask-clip:padding-box}.mask-clip-stroke{-webkit-mask-clip:stroke-box;mask-clip:stroke-box}.mask-clip-view{-webkit-mask-clip:view-box;mask-clip:view-box}.mask-no-clip{-webkit-mask-clip:no-clip;mask-clip:no-clip}.mask-bottom{-webkit-mask-position:bottom;mask-position:bottom}.mask-bottom-left{-webkit-mask-position:0 100%;mask-position:0 100%}.mask-bottom-right{-webkit-mask-position:100% 100%;mask-position:100% 100%}.mask-center{-webkit-mask-position:50%;mask-position:50%}.mask-left{-webkit-mask-position:0;mask-position:0}.mask-right{-webkit-mask-position:100%;mask-position:100%}.mask-top{-webkit-mask-position:top;mask-position:top}.mask-top-left{-webkit-mask-position:0 0;mask-position:0 0}.mask-top-right{-webkit-mask-position:100% 0;mask-position:100% 0}.mask-no-repeat{-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat}.mask-repeat{-webkit-mask-repeat:repeat;mask-repeat:repeat}.mask-repeat-round{-webkit-mask-repeat:round;mask-repeat:round}.mask-repeat-space{-webkit-mask-repeat:space;mask-repeat:space}.mask-repeat-x{-webkit-mask-repeat:repeat-x;mask-repeat:repeat-x}.mask-repeat-y{-webkit-mask-repeat:repeat-y;mask-repeat:repeat-y}.mask-origin-border{-webkit-mask-origin:border-box;mask-origin:border-box}.mask-origin-content{-webkit-mask-origin:content-box;mask-origin:content-box}.mask-origin-fill{-webkit-mask-origin:fill-box;mask-origin:fill-box}.mask-origin-padding{-webkit-mask-origin:padding-box;mask-origin:padding-box}.mask-origin-stroke{-webkit-mask-origin:stroke-box;mask-origin:stroke-box}.mask-origin-view{-webkit-mask-origin:view-box;mask-origin:view-box}.fill-none{fill:none}.stroke-none{stroke:none}.object-contain{object-fit:contain}.object-cover{object-fit:cover}.object-fill{object-fit:fill}.object-none{object-fit:none}.object-scale-down{object-fit:scale-down}.object-left-bottom{object-position:left bottom}.object-left-top{object-position:left top}.object-right-bottom{object-position:right bottom}.object-right-top{object-position:right top}.p-1{padding:var(--spacing)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-6{padding:calc(var(--spacing) * 6)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.px-8{padding-inline:calc(var(--spacing) * 8)}.py-1{padding-block:var(--spacing)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-4{padding-block:calc(var(--spacing) * 4)}.py-20{padding-block:calc(var(--spacing) * 20)}.py-32{padding-block:calc(var(--spacing) * 32)}.pt-24{padding-top:calc(var(--spacing) * 24)}.pb-4{padding-bottom:calc(var(--spacing) * 4)}.pb-12{padding-bottom:calc(var(--spacing) * 12)}.pl-2{padding-left:calc(var(--spacing) * 2)}.pl-6{padding-left:calc(var(--spacing) * 6)}.text-center{text-align:center}.text-end{text-align:end}.text-justify{text-align:justify}.text-left{text-align:left}.text-right{text-align:right}.text-start{text-align:start}.align-baseline{vertical-align:baseline}.align-bottom{vertical-align:bottom}.align-middle{vertical-align:middle}.align-sub{vertical-align:sub}.align-super{vertical-align:super}.align-text-bottom{vertical-align:text-bottom}.align-text-top{vertical-align:text-top}.align-top{vertical-align:top}.font-mono{font-family:var(--font-mono)}.font-sans{font-family:var(--font-sans)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-4xl{font-size:var(--text-4xl);line-height:var(--tw-leading,var(--text-4xl--line-height))}.text-5xl{font-size:var(--text-5xl);line-height:var(--tw-leading,var(--text-5xl--line-height))}.text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xl{font-size:var(--text-xl);line-height:var(--tw-leading,var(--text-xl--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.leading-\[0\.95\]{--tw-leading:.95;line-height:.95}.leading-relaxed{--tw-leading:var(--leading-relaxed);line-height:var(--leading-relaxed)}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-light{--tw-font-weight:var(--font-weight-light);font-weight:var(--font-weight-light)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.tracking-wide{--tw-tracking:var(--tracking-wide);letter-spacing:var(--tracking-wide)}.tracking-wider{--tw-tracking:var(--tracking-wider);letter-spacing:var(--tracking-wider)}.tracking-widest{--tw-tracking:var(--tracking-widest);letter-spacing:var(--tracking-widest)}.text-balance{text-wrap:balance}.text-nowrap{text-wrap:nowrap}.text-pretty{text-wrap:pretty}.text-wrap{text-wrap:wrap}.break-normal{overflow-wrap:normal;word-break:normal}.break-words{overflow-wrap:break-word}.wrap-anywhere{overflow-wrap:anywhere}.wrap-break-word{overflow-wrap:break-word}.wrap-normal{overflow-wrap:normal}.break-all{word-break:break-all}.break-keep{word-break:keep-all}.overflow-ellipsis{text-overflow:ellipsis}.text-clip{text-overflow:clip}.text-ellipsis{text-overflow:ellipsis}.hyphens-auto{-webkit-hyphens:auto;hyphens:auto}.hyphens-manual{-webkit-hyphens:manual;hyphens:manual}.hyphens-none{-webkit-hyphens:none;hyphens:none}.whitespace-break-spaces{white-space:break-spaces}.whitespace-normal{white-space:normal}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.whitespace-pre-line{white-space:pre-line}.whitespace-pre-wrap{white-space:pre-wrap}.\[color\:red\]{color:red}.\[color\:red\]\/50{color:oklab(62.7955% .224863 .125846/.5)}.\[color\:red\]\/50\!{color:oklab(62.7955% .224863 .125846/.5)!important}.text-\[var\(--color-bg\)\]{color:var(--color-bg)}.text-\[var\(--color-muted\)\]{color:var(--color-muted)}.text-\[var\(--color-primary\)\]{color:var(--color-primary)}.text-\[var\(--color-text\)\]{color:var(--color-text)}.text-black{color:var(--color-black)}.text-primary{color:var(--color-primary)}.text-red-500{color:var(--color-red-500)}.text-yellow-500\/70{color:#edb200b3}@supports (color:color-mix(in lab, red, red)){.text-yellow-500\/70{color:color-mix(in oklab, var(--color-yellow-500) 70%, transparent)}}.capitalize{text-transform:capitalize}.lowercase{text-transform:lowercase}.normal-case{text-transform:none}.uppercase{text-transform:uppercase}.italic{font-style:italic}.not-italic{font-style:normal}.font-stretch-condensed{font-stretch:75%}.font-stretch-expanded{font-stretch:125%}.font-stretch-extra-condensed{font-stretch:62.5%}.font-stretch-extra-expanded{font-stretch:150%}.font-stretch-normal{font-stretch:100%}.font-stretch-semi-condensed{font-stretch:87.5%}.font-stretch-semi-expanded{font-stretch:112.5%}.font-stretch-ultra-condensed{font-stretch:50%}.font-stretch-ultra-expanded{font-stretch:200%}.diagonal-fractions{--tw-numeric-fraction:diagonal-fractions;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.lining-nums{--tw-numeric-figure:lining-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.oldstyle-nums{--tw-numeric-figure:oldstyle-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.ordinal{--tw-ordinal:ordinal;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.proportional-nums{--tw-numeric-spacing:proportional-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.slashed-zero{--tw-slashed-zero:slashed-zero;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.stacked-fractions{--tw-numeric-fraction:stacked-fractions;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.tabular-nums{--tw-numeric-spacing:tabular-nums;font-variant-numeric:var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)}.normal-nums{font-variant-numeric:normal}.line-through{text-decoration-line:line-through}.no-underline{text-decoration-line:none}.overline{text-decoration-line:overline}.underline{text-decoration-line:underline}.decoration-dashed{text-decoration-style:dashed}.decoration-dotted{text-decoration-style:dotted}.decoration-double{text-decoration-style:double}.decoration-solid{text-decoration-style:solid}.decoration-wavy{text-decoration-style:wavy}.decoration-auto{text-decoration-thickness:auto}.decoration-from-font{text-decoration-thickness:from-font}.antialiased{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.subpixel-antialiased{-webkit-font-smoothing:auto;-moz-osx-font-smoothing:auto}.accent-auto{accent-color:auto}.scheme-dark{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark}.scheme-light{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light}.scheme-light-dark{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark}@media (prefers-color-scheme:dark){.scheme-light-dark{--lightningcss-light: ;--lightningcss-dark:initial}}.scheme-normal{color-scheme:normal}.scheme-only-dark{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark only}.scheme-only-light{--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light only}.opacity-0{opacity:0}.opacity-40{opacity:.4}.opacity-70{opacity:.7}.opacity-\[0\.03\]{opacity:.03}.mix-blend-plus-darker{mix-blend-mode:plus-darker}.mix-blend-plus-lighter{mix-blend-mode:plus-lighter}.shadow{--tw-shadow:0 1px 3px 0 var(--tw-shadow-color,#0000001a), 0 1px 2px -1px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.inset-ring{--tw-inset-ring-shadow:inset 0 0 0 1px var(--tw-inset-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-initial{--tw-shadow-color:initial}.inset-shadow-initial{--tw-inset-shadow-color:initial}.outline-hidden{--tw-outline-style:none;outline-style:none}@media (forced-colors:active){.outline-hidden{outline-offset:2px;outline:2px solid #0000}}.outline{outline-style:var(--tw-outline-style);outline-width:1px}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.blur-\[80px\]{--tw-blur:blur(80px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow{--tw-drop-shadow-size:drop-shadow(0 1px 2px var(--tw-drop-shadow-color,#0000001a)) drop-shadow(0 1px 1px var(--tw-drop-shadow-color,#0000000f));--tw-drop-shadow:drop-shadow(0 1px 2px #0000001a) drop-shadow(0 1px 1px #0000000f);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.drop-shadow-none{--tw-drop-shadow: ;filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.grayscale{--tw-grayscale:grayscale(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.invert{--tw-invert:invert(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.sepia{--tw-sepia:sepia(100%);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.\!filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)!important}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-xl{--tw-backdrop-blur:blur(var(--blur-xl));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-grayscale{--tw-backdrop-grayscale:grayscale(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-invert{--tw-backdrop-invert:invert(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-sepia{--tw-backdrop-sepia:sepia(100%);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-filter{-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-all{transition-property:all;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-colors{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-opacity{transition-property:opacity;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-transform{transition-property:transform,translate,scale,rotate;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.transition-discrete{transition-behavior:allow-discrete}.transition-normal{transition-behavior:normal}.duration-300{--tw-duration:.3s;transition-duration:.3s}.duration-500{--tw-duration:.5s;transition-duration:.5s}.ease-in{--tw-ease:var(--ease-in);transition-timing-function:var(--ease-in)}.ease-in-out{--tw-ease:var(--ease-in-out);transition-timing-function:var(--ease-in-out)}.ease-out{--tw-ease:var(--ease-out);transition-timing-function:var(--ease-out)}.will-change-auto{will-change:auto}.will-change-contents{will-change:contents}.will-change-scroll{will-change:scroll-position}.will-change-transform{will-change:transform}.contain-inline-size{--tw-contain-size:inline-size;contain:var(--tw-contain-size,) var(--tw-contain-layout,) var(--tw-contain-paint,) var(--tw-contain-style,)}.contain-layout{--tw-contain-layout:layout;contain:var(--tw-contain-size,) var(--tw-contain-layout,) var(--tw-contain-paint,) var(--tw-contain-style,)}.contain-paint{--tw-contain-paint:paint;contain:var(--tw-contain-size,) var(--tw-contain-layout,) var(--tw-contain-paint,) var(--tw-contain-style,)}.contain-size{--tw-contain-size:size;contain:var(--tw-contain-size,) var(--tw-contain-layout,) var(--tw-contain-paint,) var(--tw-contain-style,)}.contain-style{--tw-contain-style:style;contain:var(--tw-contain-size,) var(--tw-contain-layout,) var(--tw-contain-paint,) var(--tw-contain-style,)}.contain-content{contain:content}.contain-none{contain:none}.contain-strict{contain:strict}.content-none{--tw-content:none;content:none}.forced-color-adjust-auto{forced-color-adjust:auto}.forced-color-adjust-none{forced-color-adjust:none}.outline-dashed{--tw-outline-style:dashed;outline-style:dashed}.outline-dotted{--tw-outline-style:dotted;outline-style:dotted}.outline-double{--tw-outline-style:double;outline-style:double}.outline-none{--tw-outline-style:none;outline-style:none}.outline-solid{--tw-outline-style:solid;outline-style:solid}.select-all{-webkit-user-select:all;user-select:all}.select-none{-webkit-user-select:none;user-select:none}.\[hash\:10\]{hash:10}.\[k\:K\]{k:K}.\[vite\:html\]{vite:html}.backface-hidden{backface-visibility:hidden}.backface-visible{backface-visibility:visible}.block-auto{block-size:auto}.block-lh{block-size:1lh}.block-screen{block-size:100vh}:where(.divide-x-reverse>:not(:last-child)){--tw-divide-x-reverse:1}.duration-initial{--tw-duration:initial}.inline-auto{inline-size:auto}.inline-screen{inline-size:100vw}.max-block-lh{max-block-size:1lh}.max-block-none{max-block-size:none}.max-block-screen{max-block-size:100vh}.max-inline-none{max-inline-size:none}.max-inline-screen{max-inline-size:100vw}.min-block-auto{min-block-size:auto}.min-block-lh{min-block-size:1lh}.min-block-screen{min-block-size:100vh}.min-inline-auto{min-inline-size:auto}.min-inline-screen{min-inline-size:100vw}.ring-inset{--tw-ring-inset:inset}.text-shadow-initial{--tw-text-shadow-color:initial}.transform-3d{transform-style:preserve-3d}.transform-border{transform-box:border-box}.transform-content{transform-box:content-box}.transform-fill{transform-box:fill-box}.transform-flat{transform-style:flat}.transform-stroke{transform-box:stroke-box}.transform-view{transform-box:view-box}@media (hover:hover){.group-hover\:translate-x-1:is(:where(.group):hover *){--tw-translate-x:var(--spacing);translate:var(--tw-translate-x) var(--tw-translate-y)}.group-hover\:text-\[var\(--color-text\)\]:is(:where(.group):hover *){color:var(--color-text)}.group-hover\:opacity-100:is(:where(.group):hover *){opacity:1}.hover\:border-transparent:hover{border-color:#0000}.hover\:bg-\[var\(--color-primary\)\]:hover{background-color:var(--color-primary)}.hover\:bg-\[var\(--color-surface\)\]:hover{background-color:var(--color-surface)}.hover\:bg-\[var\(--color-text\)\]:hover{background-color:var(--color-text)}.hover\:text-\[var\(--color-bg\)\]:hover{color:var(--color-bg)}.hover\:text-\[var\(--color-primary\)\]:hover{color:var(--color-primary)}.hover\:text-\[var\(--color-text\)\]:hover{color:var(--color-text)}}@media (width>=40rem){.sm\:w-auto{width:auto}.sm\:flex-row{flex-direction:row}}@media (width>=48rem){.md\:left-\[55\%\]{left:55%}.md\:mb-0{margin-bottom:0}.md\:flex{display:flex}.md\:hidden{display:none}.md\:h-\[800px\]{height:800px}.md\:w-\[800px\]{width:800px}.md\:grid-cols-2{grid-template-columns:repeat(2,minmax(0,1fr))}.md\:items-end{align-items:flex-end}.md\:text-right{text-align:right}.md\:text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.md\:text-6xl{font-size:var(--text-6xl);line-height:var(--tw-leading,var(--text-6xl--line-height))}.md\:text-7xl{font-size:var(--text-7xl);line-height:var(--tw-leading,var(--text-7xl--line-height))}.md\:text-8xl{font-size:var(--text-8xl);line-height:var(--tw-leading,var(--text-8xl--line-height))}}@media (width>=64rem){.lg\:grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.lg\:text-9xl{font-size:var(--text-9xl);line-height:var(--tw-leading,var(--text-9xl--line-height))}}@media (hover:hover){@media (prefers-color-scheme:dark){.hover\:dark\:\!bg-gray-100:hover{background-color:var(--color-gray-100)!important}.hover\:dark\:bg-gray-100:hover{background-color:var(--color-gray-100)}}}}.theme-transition *{transition:background-color .8s,border-color .8s,color .8s,box-shadow .8s,fill .8s,stroke .8s}.glass-panel{background:var(--color-surface);-webkit-backdrop-filter:blur(var(--glass-blur));border:var(--border-main);border-radius:var(--radius-main);box-shadow:var(--shadow-main)}.heading-font{font-family:var(--font-heading)}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-scale-x{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-y{syntax:"*";inherits:false;initial-value:1}@property --tw-scale-z{syntax:"*";inherits:false;initial-value:1}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-pan-x{syntax:"*";inherits:false}@property --tw-pan-y{syntax:"*";inherits:false}@property --tw-pinch-zoom{syntax:"*";inherits:false}@property --tw-scroll-snap-strictness{syntax:"*";inherits:false;initial-value:proximity}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-space-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-divide-x-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-divide-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-leading{syntax:"*";inherits:false}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-ordinal{syntax:"*";inherits:false}@property --tw-slashed-zero{syntax:"*";inherits:false}@property --tw-numeric-figure{syntax:"*";inherits:false}@property --tw-numeric-spacing{syntax:"*";inherits:false}@property --tw-numeric-fraction{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-outline-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}@property --tw-duration{syntax:"*";inherits:false}@property --tw-ease{syntax:"*";inherits:false}@property --tw-contain-size{syntax:"*";inherits:false}@property --tw-contain-layout{syntax:"*";inherits:false}@property --tw-contain-paint{syntax:"*";inherits:false}@property --tw-contain-style{syntax:"*";inherits:false}@property --tw-text-shadow-color{syntax:"*";inherits:false}@property --tw-text-shadow-alpha{syntax:"";inherits:false;initial-value:100%} diff --git a/ostp-web/dist/assets/index-DnZZkzK0.js b/ostp-web/dist/assets/index-DnZZkzK0.js deleted file mode 100644 index 023f54d..0000000 --- a/ostp-web/dist/assets/index-DnZZkzK0.js +++ /dev/null @@ -1,19 +0,0 @@ -var e=Object.create,t=Object.defineProperty,n=Object.getOwnPropertyDescriptor,r=Object.getOwnPropertyNames,i=Object.getPrototypeOf,a=Object.prototype.hasOwnProperty,o=(e,t)=>()=>(e&&(t=e(e=0)),t),s=(e,t)=>()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports),c=(e,n)=>{let r={};for(var i in e)t(r,i,{get:e[i],enumerable:!0});return n||t(r,Symbol.toStringTag,{value:`Module`}),r},l=(e,i,o,s)=>{if(i&&typeof i==`object`||typeof i==`function`)for(var c=r(i),l=0,u=c.length,d;li[e]).bind(null,d),enumerable:!(s=n(i,d))||s.enumerable});return e},u=(n,r,a)=>(a=n==null?{}:e(i(n)),l(r||!n||!n.__esModule?t(a,`default`,{value:n,enumerable:!0}):a,n)),d=e=>a.call(e,`module.exports`)?e[`module.exports`]:l(t({},`__esModule`,{value:!0}),e);(function(){let e=document.createElement(`link`).relList;if(e&&e.supports&&e.supports(`modulepreload`))return;for(let e of document.querySelectorAll(`link[rel="modulepreload"]`))n(e);new MutationObserver(e=>{for(let t of e)if(t.type===`childList`)for(let e of t.addedNodes)e.tagName===`LINK`&&e.rel===`modulepreload`&&n(e)}).observe(document,{childList:!0,subtree:!0});function t(e){let t={};return e.integrity&&(t.integrity=e.integrity),e.referrerPolicy&&(t.referrerPolicy=e.referrerPolicy),e.crossOrigin===`use-credentials`?t.credentials=`include`:e.crossOrigin===`anonymous`?t.credentials=`omit`:t.credentials=`same-origin`,t}function n(e){if(e.ep)return;e.ep=!0;let n=t(e);fetch(e.href,n)}})();var f=s((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.portal`),r=Symbol.for(`react.fragment`),i=Symbol.for(`react.strict_mode`),a=Symbol.for(`react.profiler`),o=Symbol.for(`react.consumer`),s=Symbol.for(`react.context`),c=Symbol.for(`react.forward_ref`),l=Symbol.for(`react.suspense`),u=Symbol.for(`react.memo`),d=Symbol.for(`react.lazy`),f=Symbol.for(`react.activity`),p=Symbol.iterator;function m(e){return typeof e!=`object`||!e?null:(e=p&&e[p]||e[`@@iterator`],typeof e==`function`?e:null)}var h={isMounted:function(){return!1},enqueueForceUpdate:function(){},enqueueReplaceState:function(){},enqueueSetState:function(){}},g=Object.assign,_={};function v(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}v.prototype.isReactComponent={},v.prototype.setState=function(e,t){if(typeof e!=`object`&&typeof e!=`function`&&e!=null)throw Error(`takes an object of state variables to update or a function which returns an object of state variables.`);this.updater.enqueueSetState(this,e,t,`setState`)},v.prototype.forceUpdate=function(e){this.updater.enqueueForceUpdate(this,e,`forceUpdate`)};function y(){}y.prototype=v.prototype;function b(e,t,n){this.props=e,this.context=t,this.refs=_,this.updater=n||h}var x=b.prototype=new y;x.constructor=b,g(x,v.prototype),x.isPureReactComponent=!0;var S=Array.isArray;function C(){}var w={H:null,A:null,T:null,S:null},ee=Object.prototype.hasOwnProperty;function te(e,n,r){var i=r.ref;return{$$typeof:t,type:e,key:n,ref:i===void 0?null:i,props:r}}function ne(e,t){return te(e.type,t,e.props)}function re(e){return typeof e==`object`&&!!e&&e.$$typeof===t}function ie(e){var t={"=":`=0`,":":`=2`};return`$`+e.replace(/[=:]/g,function(e){return t[e]})}var ae=/\/+/g;function oe(e,t){return typeof e==`object`&&e&&e.key!=null?ie(``+e.key):t.toString(36)}function T(e){switch(e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason;default:switch(typeof e.status==`string`?e.then(C,C):(e.status=`pending`,e.then(function(t){e.status===`pending`&&(e.status=`fulfilled`,e.value=t)},function(t){e.status===`pending`&&(e.status=`rejected`,e.reason=t)})),e.status){case`fulfilled`:return e.value;case`rejected`:throw e.reason}}throw e}function se(e,r,i,a,o){var s=typeof e;(s===`undefined`||s===`boolean`)&&(e=null);var c=!1;if(e===null)c=!0;else switch(s){case`bigint`:case`string`:case`number`:c=!0;break;case`object`:switch(e.$$typeof){case t:case n:c=!0;break;case d:return c=e._init,se(c(e._payload),r,i,a,o)}}if(c)return o=o(e),c=a===``?`.`+oe(e,0):a,S(o)?(i=``,c!=null&&(i=c.replace(ae,`$&/`)+`/`),se(o,r,i,``,function(e){return e})):o!=null&&(re(o)&&(o=ne(o,i+(o.key==null||e&&e.key===o.key?``:(``+o.key).replace(ae,`$&/`)+`/`)+c)),r.push(o)),1;c=0;var l=a===``?`.`:a+`:`;if(S(e))for(var u=0;u{t.exports=f()})),m=s((e=>{function t(e,t){var n=e.length;e.push(t);a:for(;0>>1,a=e[r];if(0>>1;ri(c,n))li(u,c)?(e[r]=u,e[l]=n,r=l):(e[r]=c,e[s]=n,r=s);else if(li(u,n))e[r]=u,e[l]=n,r=l;else break a}}return t}function i(e,t){var n=e.sortIndex-t.sortIndex;return n===0?e.id-t.id:n}if(e.unstable_now=void 0,typeof performance==`object`&&typeof performance.now==`function`){var a=performance;e.unstable_now=function(){return a.now()}}else{var o=Date,s=o.now();e.unstable_now=function(){return o.now()-s}}var c=[],l=[],u=1,d=null,f=3,p=!1,m=!1,h=!1,g=!1,_=typeof setTimeout==`function`?setTimeout:null,v=typeof clearTimeout==`function`?clearTimeout:null,y=typeof setImmediate<`u`?setImmediate:null;function b(e){for(var i=n(l);i!==null;){if(i.callback===null)r(l);else if(i.startTime<=e)r(l),i.sortIndex=i.expirationTime,t(c,i);else break;i=n(l)}}function x(e){if(h=!1,b(e),!m)if(n(c)!==null)m=!0,S||(S=!0,re());else{var t=n(l);t!==null&&oe(x,t.startTime-e)}}var S=!1,C=-1,w=5,ee=-1;function te(){return g?!0:!(e.unstable_now()-eet&&te());){var o=d.callback;if(typeof o==`function`){d.callback=null,f=d.priorityLevel;var s=o(d.expirationTime<=t);if(t=e.unstable_now(),typeof s==`function`){d.callback=s,b(t),i=!0;break b}d===n(c)&&r(c),b(t)}else r(c);d=n(c)}if(d!==null)i=!0;else{var u=n(l);u!==null&&oe(x,u.startTime-t),i=!1}}break a}finally{d=null,f=a,p=!1}i=void 0}}finally{i?re():S=!1}}}var re;if(typeof y==`function`)re=function(){y(ne)};else if(typeof MessageChannel<`u`){var ie=new MessageChannel,ae=ie.port2;ie.port1.onmessage=ne,re=function(){ae.postMessage(null)}}else re=function(){_(ne,0)};function oe(t,n){C=_(function(){t(e.unstable_now())},n)}e.unstable_IdlePriority=5,e.unstable_ImmediatePriority=1,e.unstable_LowPriority=4,e.unstable_NormalPriority=3,e.unstable_Profiling=null,e.unstable_UserBlockingPriority=2,e.unstable_cancelCallback=function(e){e.callback=null},e.unstable_forceFrameRate=function(e){0>e||125o?(r.sortIndex=a,t(l,r),n(c)===null&&r===n(l)&&(h?(v(C),C=-1):h=!0,oe(x,a-o))):(r.sortIndex=s,t(c,r),m||p||(m=!0,S||(S=!0,re()))),r},e.unstable_shouldYield=te,e.unstable_wrapCallback=function(e){var t=f;return function(){var n=f;f=t;try{return e.apply(this,arguments)}finally{f=n}}}})),h=s(((e,t)=>{t.exports=m()})),g=s((e=>{var t=p();function n(e){var t=`https://react.dev/errors/`+e;if(1{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=g()})),v=s((e=>{var t=h(),n=p(),r=_();function i(e){var t=`https://react.dev/errors/`+e;if(1fe||(e.current=de[fe],de[fe]=null,fe--)}function O(e,t){fe++,de[fe]=e.current,e.current=t}var he=pe(null),ge=pe(null),_e=pe(null),ve=pe(null);function ye(e,t){switch(O(_e,t),O(ge,e),O(he,null),t.nodeType){case 9:case 11:e=(e=t.documentElement)&&(e=e.namespaceURI)?Vd(e):0;break;default:if(e=t.tagName,t=t.namespaceURI)t=Vd(t),e=Hd(t,e);else switch(e){case`svg`:e=1;break;case`math`:e=2;break;default:e=0}}me(he),O(he,e)}function be(){me(he),me(ge),me(_e)}function xe(e){e.memoizedState!==null&&O(ve,e);var t=he.current,n=Hd(t,e.type);t!==n&&(O(ge,e),O(he,n))}function Se(e){ge.current===e&&(me(he),me(ge)),ve.current===e&&(me(ve),Qf._currentValue=ue)}var Ce,we;function Te(e){if(Ce===void 0)try{throw Error()}catch(e){var t=e.stack.trim().match(/\n( *(at )?)/);Ce=t&&t[1]||``,we=-1)`:-1i||c[r]!==l[i]){var u=` -`+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{Ee=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?Te(n):``}function Oe(e,t){switch(e.tag){case 26:case 27:case 5:return Te(e.type);case 16:return Te(`Lazy`);case 13:return e.child!==t&&t!==null?Te(`Suspense Fallback`):Te(`Suspense`);case 19:return Te(`SuspenseList`);case 0:case 15:return De(e.type,!1);case 11:return De(e.type.render,!1);case 1:return De(e.type,!0);case 31:return Te(`Activity`);default:return``}}function ke(e){try{var t=``,n=null;do t+=Oe(e,n),n=e,e=e.return;while(e);return t}catch(e){return` -Error generating stack: `+e.message+` -`+e.stack}}var Ae=Object.prototype.hasOwnProperty,je=t.unstable_scheduleCallback,Me=t.unstable_cancelCallback,Ne=t.unstable_shouldYield,Pe=t.unstable_requestPaint,Fe=t.unstable_now,Ie=t.unstable_getCurrentPriorityLevel,Le=t.unstable_ImmediatePriority,Re=t.unstable_UserBlockingPriority,ze=t.unstable_NormalPriority,Be=t.unstable_LowPriority,Ve=t.unstable_IdlePriority,He=t.log,Ue=t.unstable_setDisableYieldValue,We=null,Ge=null;function Ke(e){if(typeof He==`function`&&Ue(e),Ge&&typeof Ge.setStrictMode==`function`)try{Ge.setStrictMode(We,e)}catch{}}var qe=Math.clz32?Math.clz32:Xe,Je=Math.log,Ye=Math.LN2;function Xe(e){return e>>>=0,e===0?32:31-(Je(e)/Ye|0)|0}var Ze=256,Qe=262144,$e=4194304;function et(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&261888;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function tt(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=et(n))):i=et(o):i=et(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=et(n))):i=et(o)):i=et(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function nt(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function rt(e,t){switch(e){case 1:case 2:case 4:case 8:case 64:return t+250;case 16:case 32:case 128:case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:case 262144:case 524288:case 1048576:case 2097152:return t+5e3;case 4194304:case 8388608:case 16777216:case 33554432:return-1;case 67108864:case 134217728:case 268435456:case 536870912:case 1073741824:return-1;default:return-1}}function it(){var e=$e;return $e<<=1,!($e&62914560)&&($e=4194304),e}function at(e){for(var t=[],n=0;31>n;n++)t.push(e);return t}function ot(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function st(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0`u`||window.document===void 0||window.document.createElement===void 0),bn=!1;if(yn)try{var xn={};Object.defineProperty(xn,"passive",{get:function(){bn=!0}}),window.addEventListener(`test`,xn,xn),window.removeEventListener(`test`,xn,xn)}catch{bn=!1}var Sn=null,Cn=null,wn=null;function Tn(){if(wn)return wn;var e,t=Cn,n=t.length,r,i=`value`in Sn?Sn.value:Sn.textContent,a=i.length;for(e=0;e=rr),or=` `,sr=!1;function k(e,t){switch(e){case`keyup`:return tr.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function cr(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var lr=!1;function ur(e,t){switch(e){case`compositionend`:return cr(t);case`keypress`:return t.which===32?(sr=!0,or):null;case`textInput`:return e=t.data,e===or&&sr?null:e;default:return null}}function dr(e,t){if(lr)return e===`compositionend`||!nr&&k(e,t)?(e=Tn(),wn=Cn=Sn=null,lr=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=Nr(n)}}function Fr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Fr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Ir(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Kt(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Kt(e.document)}return t}function Lr(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var Rr=yn&&`documentMode`in document&&11>=document.documentMode,zr=null,Br=null,Vr=null,Hr=!1;function Ur(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Hr||zr==null||zr!==Kt(r)||(r=zr,`selectionStart`in r&&Lr(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),Vr&&Mr(Vr,r)||(Vr=r,r=Ed(Br,`onSelect`),0>=o,i-=o,Ni=1<<32-qe(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),a=o(_,a,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),N&&Fi(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(a,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(a,h),s=o(y,s,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(a,h),N&&Fi(a,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(a,v.value,l),v!==null&&(s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return N&&Fi(a,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,a,g,v.value,l),v!==null&&(e&&v.alternate!==null&&h.delete(v.key===null?g:v.key),s=o(v,s,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(a,e)}),N&&Fi(a,g),u}function b(e,r,o,c){if(typeof o==`object`&&o&&o.type===y&&o.key===null&&(o=o.props.children),typeof o==`object`&&o){switch(o.$$typeof){case g:a:{for(var l=o.key;r!==null;){if(r.key===l){if(l=o.type,l===y){if(r.tag===7){n(e,r.sibling),c=a(r,o.props.children),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===re&&ja(l)===r.type){n(e,r.sibling),c=a(r,o.props),Ra(c,o),c.return=e,e=c;break a}n(e,r);break}else t(e,r);r=r.sibling}o.type===y?(c=Si(o.props.children,e.mode,c,o.key),c.return=e,e=c):(c=xi(o.type,o.key,o.props,null,e.mode,c),Ra(c,o),c.return=e,e=c)}return s(e);case v:a:{for(l=o.key;r!==null;){if(r.key===l)if(r.tag===4&&r.stateNode.containerInfo===o.containerInfo&&r.stateNode.implementation===o.implementation){n(e,r.sibling),c=a(r,o.children||[]),c.return=e,e=c;break a}else{n(e,r);break}else t(e,r);r=r.sibling}c=A(o,e.mode,c),c.return=e,e=c}return s(e);case re:return o=ja(o),b(e,r,o,c)}if(le(o))return h(e,r,o,c);if(T(o)){if(l=T(o),typeof l!=`function`)throw Error(i(150));return o=l.call(o),_(e,r,o,c)}if(typeof o.then==`function`)return b(e,r,La(o),c);if(o.$$typeof===C)return b(e,r,oa(e,o),c);za(e,o)}return typeof o==`string`&&o!==``||typeof o==`number`||typeof o==`bigint`?(o=``+o,r!==null&&r.tag===6?(n(e,r.sibling),c=a(r,o),c.return=e,e=c):(n(e,r),c=Ci(o,e.mode,c),c.return=e,e=c),s(e)):n(e,r)}return function(e,t,n,r){try{Ia=0;var i=b(e,t,n,r);return Fa=null,i}catch(t){if(t===Ta||t===Da)throw t;var a=_i(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var Ba=L(!0),Va=L(!1),Ha=!1;function Ua(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function Wa(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function Ga(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function Ka(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,G&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=mi(e),pi(e,null,n),t}return ui(e,r,t,n),mi(e)}function qa(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,lt(e,n)}}function Ja(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var Ya=!1;function Xa(){if(Ya){var e=_a;if(e!==null)throw e}}function Za(e,t,n,r){Ya=!1;var i=e.updateQueue;Ha=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var p=s.lane&-536870913,m=p!==s.lane;if(m?(J&p)===p:(r&p)===p){p!==0&&p===ga&&(Ya=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var h=e,g=s;p=t;var _=n;switch(g.tag){case 1:if(h=g.payload,typeof h==`function`){d=h.call(_,d,p);break a}d=h;break a;case 3:h.flags=h.flags&-65537|128;case 0:if(h=g.payload,p=typeof h==`function`?h.call(_,d,p):h,p==null)break a;d=f({},d,p);break a;case 2:Ha=!0}}p=s.callback,p!==null&&(e.flags|=64,m&&(e.flags|=8192),m=i.callbacks,m===null?i.callbacks=[p]:m.push(p))}else m={lane:p,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=m,c=d):u=u.next=m,o|=p;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;m=s,s=m.next,m.next=null,i.lastBaseUpdate=m,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),Gl|=o,e.lanes=o,e.memoizedState=d}}function Qa(e,t){if(typeof e!=`function`)throw Error(i(191,e));e.call(t)}function $a(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=E.T,s={};E.T=s,Fs(e,!1,t,n);try{var c=i(),l=E.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?Ps(e,t,ba(c,r),pu(e)):Ps(e,t,r,pu(e))}catch(n){Ps(e,t,{then:function(){},status:`rejected`,reason:n},pu())}finally{D.p=a,o!==null&&s.types!==null&&(o.types=s.types),E.T=o}}function ws(){}function Ts(e,t,n,r){if(e.tag!==5)throw Error(i(476));var a=Es(e).queue;Cs(e,a,t,ue,n===null?ws:function(){return Ds(e),n(r)})}function Es(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:ue,baseState:ue,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Io,lastRenderedState:ue},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:Io,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function Ds(e){var t=Es(e);t.next===null&&(t=e.alternate.memoizedState),Ps(e,t.next.queue,{},pu())}function Os(){return aa(Qf)}function ks(){return H().memoizedState}function As(){return H().memoizedState}function js(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=pu();e=Ga(n);var r=Ka(t,e,n);r!==null&&(hu(r,t,n),qa(r,t,n)),t={cache:fa()},e.payload=t;return}t=t.return}}function Ms(e,t,n){var r=pu();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},Is(e)?Ls(t,n):(n=di(e,t,n,r),n!==null&&(hu(n,e,r),Rs(n,t,r)))}function Ns(e,t,n){Ps(e,t,n,pu())}function Ps(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(Is(e))Ls(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,jr(s,o))return ui(e,t,i,0),K===null&&li(),!1}catch{}if(n=di(e,t,i,r),n!==null)return hu(n,e,r),Rs(n,t,r),!0}return!1}function Fs(e,t,n,r){if(r={lane:2,revertLane:dd(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},Is(e)){if(t)throw Error(i(479))}else t=di(e,n,r,2),t!==null&&hu(t,e,2)}function Is(e){var t=e.alternate;return e===z||t!==null&&t===z}function Ls(e,t){go=ho=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function Rs(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,lt(e,n)}}var zs={readContext:aa,use:Po,useCallback:So,useContext:So,useEffect:So,useImperativeHandle:So,useLayoutEffect:So,useInsertionEffect:So,useMemo:So,useReducer:So,useRef:So,useState:So,useDebugValue:So,useDeferredValue:So,useTransition:So,useSyncExternalStore:So,useId:So,useHostTransitionStatus:So,useFormState:So,useActionState:So,useOptimistic:So,useMemoCache:So,useCacheRefresh:So};zs.useEffectEvent=So;var Bs={readContext:aa,use:Po,useCallback:function(e,t){return jo().memoizedState=[e,t===void 0?null:t],e},useContext:aa,useEffect:us,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),cs(4194308,4,gs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return cs(4194308,4,e,t)},useInsertionEffect:function(e,t){cs(4,2,e,t)},useMemo:function(e,t){var n=jo();t=t===void 0?null:t;var r=e();if(_o){Ke(!0);try{e()}finally{Ke(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=jo();if(n!==void 0){var i=n(t);if(_o){Ke(!0);try{n(t)}finally{Ke(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=Ms.bind(null,z,e),[r.memoizedState,e]},useRef:function(e){var t=jo();return e={current:e},t.memoizedState=e},useState:function(e){e=Ko(e);var t=e.queue,n=Ns.bind(null,z,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:vs,useDeferredValue:function(e,t){return xs(jo(),e,t)},useTransition:function(){var e=Ko(!1);return e=Cs.bind(null,z,e.queue,!0,!1),jo().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=z,a=jo();if(N){if(n===void 0)throw Error(i(407));n=n()}else{if(n=t(),K===null)throw Error(i(349));J&127||Vo(r,t,n)}a.memoizedState=n;var o={value:n,getSnapshot:t};return a.queue=o,us(Uo.bind(null,r,o,e),[e]),r.flags|=2048,os(9,{destroy:void 0},Ho.bind(null,r,o,n,t),null),n},useId:function(){var e=jo(),t=K.identifierPrefix;if(N){var n=Pi,r=Ni;n=(r&~(1<<32-qe(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=vo++,0<\/script>`,o=o.removeChild(o.firstChild);break;case`select`:o=typeof r.is==`string`?s.createElement(`select`,{is:r.is}):s.createElement(`select`),r.multiple?o.multiple=!0:r.size&&(o.size=r.size);break;default:o=typeof r.is==`string`?s.createElement(a,{is:r.is}):s.createElement(a)}}o[gt]=t,o[_t]=r;a:for(s=t.child;s!==null;){if(s.tag===5||s.tag===6)o.appendChild(s.stateNode);else if(s.tag!==4&&s.tag!==27&&s.child!==null){s.child.return=s,s=s.child;continue}if(s===t)break a;for(;s.sibling===null;){if(s.return===null||s.return===t)break a;s=s.return}s.sibling.return=s.return,s=s.sibling}t.stateNode=o;a:switch(Pd(o,a,r),a){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&Pc(t)}}return U(t),Fc(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&Pc(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(i(166));if(e=_e.current,qi(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,a=Bi,a!==null)switch(a.tag){case 27:case 5:r=a.memoizedProps}e[gt]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||Md(e.nodeValue,n)),e||Wi(t,!0)}else e=Bd(e).createTextNode(r),e[gt]=t,t.stateNode=e}return U(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=qi(t),n!==null){if(e===null){if(!r)throw Error(i(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(i(557));e[gt]=t}else Ji(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;U(t),e=!1}else n=Yi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(fo(t),t):(fo(t),null);if(t.flags&128)throw Error(i(558))}return U(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(a=qi(t),r!==null&&r.dehydrated!==null){if(e===null){if(!a)throw Error(i(318));if(a=t.memoizedState,a=a===null?null:a.dehydrated,!a)throw Error(i(317));a[gt]=t}else Ji(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;U(t),a=!1}else a=Yi(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=a),a=!0;if(!a)return t.flags&256?(fo(t),t):(fo(t),null)}return fo(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,a=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(a=r.alternate.memoizedState.cachePool.pool),o=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(o=r.memoizedState.cachePool.pool),o!==a&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),Lc(t,t.updateQueue),U(t),null);case 4:return be(),e===null&&Sd(t.stateNode.containerInfo),U(t),null;case 10:return ea(t.type),U(t),null;case 19:if(me(R),r=t.memoizedState,r===null)return U(t),null;if(a=(t.flags&128)!=0,o=r.rendering,o===null)if(a)Rc(r,!1);else{if(X!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(o=po(e),o!==null){for(t.flags|=128,Rc(r,!1),e=o.updateQueue,t.updateQueue=e,Lc(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)bi(n,e),n=n.sibling;return O(R,R.current&1|2),N&&Fi(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&Fe()>tu&&(t.flags|=128,a=!0,Rc(r,!1),t.lanes=4194304)}else{if(!a)if(e=po(o),e!==null){if(t.flags|=128,a=!0,e=e.updateQueue,t.updateQueue=e,Lc(t,e),Rc(r,!0),r.tail===null&&r.tailMode===`hidden`&&!o.alternate&&!N)return U(t),null}else 2*Fe()-r.renderingStartTime>tu&&n!==536870912&&(t.flags|=128,a=!0,Rc(r,!1),t.lanes=4194304);r.isBackwards?(o.sibling=t.child,t.child=o):(e=r.last,e===null?t.child=o:e.sibling=o,r.last=o)}return r.tail===null?(U(t),null):(e=r.tail,r.rendering=e,r.tail=e.sibling,r.renderingStartTime=Fe(),e.sibling=null,n=R.current,O(R,a?n&1|2:n&1),N&&Fi(t,r.treeForkCount),e);case 22:case 23:return fo(t),io(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(U(t),t.subtreeFlags&6&&(t.flags|=8192)):U(t),n=t.updateQueue,n!==null&&Lc(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&me(xa),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),ea(da),U(t),null;case 25:return null;case 30:return null}throw Error(i(156,t.tag))}function Bc(e,t){switch(Ri(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return ea(da),be(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return Se(t),null;case 31:if(t.memoizedState!==null){if(fo(t),t.alternate===null)throw Error(i(340));Ji()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(fo(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(i(340));Ji()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return me(R),null;case 4:return be(),null;case 10:return ea(t.type),null;case 22:case 23:return fo(t),io(),e!==null&&me(xa),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return ea(da),null;case 25:return null;default:return null}}function Vc(e,t){switch(Ri(t),t.tag){case 3:ea(da),be();break;case 26:case 27:case 5:Se(t);break;case 4:be();break;case 31:t.memoizedState!==null&&fo(t);break;case 13:fo(t);break;case 19:me(R);break;case 10:ea(t.type);break;case 22:case 23:fo(t),io(),e!==null&&me(xa);break;case 24:ea(da)}}function Hc(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Z(t,t.return,e)}}function Uc(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Z(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Z(t,t.return,e)}}function Wc(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{$a(t,n)}catch(t){Z(e,e.return,t)}}}function Gc(e,t,n){n.props=qs(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Z(e,t,n)}}function Kc(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Z(e,t,n)}}function qc(e,t){var n=e.ref,r=e.refCleanup;if(n!==null)if(typeof r==`function`)try{r()}catch(n){Z(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Z(e,t,n)}else n.current=null}function Jc(e){var t=e.type,n=e.memoizedProps,r=e.stateNode;try{a:switch(t){case`button`:case`input`:case`select`:case`textarea`:n.autoFocus&&r.focus();break a;case`img`:n.src?r.src=n.src:n.srcSet&&(r.srcset=n.srcSet)}}catch(t){Z(e,e.return,t)}}function Yc(e,t,n){try{var r=e.stateNode;Fd(r,e.type,n,t),r[_t]=t}catch(t){Z(e,e.return,t)}}function Xc(e){return e.tag===5||e.tag===3||e.tag===26||e.tag===27&&Zd(e.type)||e.tag===4}function Zc(e){a:for(;;){for(;e.sibling===null;){if(e.return===null||Xc(e.return))return null;e=e.return}for(e.sibling.return=e.return,e=e.sibling;e.tag!==5&&e.tag!==6&&e.tag!==18;){if(e.tag===27&&Zd(e.type)||e.flags&2||e.child===null||e.tag===4)continue a;e.child.return=e,e=e.child}if(!(e.flags&2))return e.stateNode}}function Qc(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?(n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n).insertBefore(e,t):(t=n.nodeType===9?n.body:n.nodeName===`HTML`?n.ownerDocument.body:n,t.appendChild(e),n=n._reactRootContainer,n!=null||t.onclick!==null||(t.onclick=un));else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode,t=null),e=e.child,e!==null))for(Qc(e,t,n),e=e.sibling;e!==null;)Qc(e,t,n),e=e.sibling}function $c(e,t,n){var r=e.tag;if(r===5||r===6)e=e.stateNode,t?n.insertBefore(e,t):n.appendChild(e);else if(r!==4&&(r===27&&Zd(e.type)&&(n=e.stateNode),e=e.child,e!==null))for($c(e,t,n),e=e.sibling;e!==null;)$c(e,t,n),e=e.sibling}function el(e){var t=e.stateNode,n=e.memoizedProps;try{for(var r=e.type,i=t.attributes;i.length;)t.removeAttributeNode(i[0]);Pd(t,r,n),t[gt]=e,t[_t]=n}catch(t){Z(e,e.return,t)}}var tl=!1,nl=!1,rl=!1,il=typeof WeakSet==`function`?WeakSet:Set,al=null;function ol(e,t){if(e=e.containerInfo,Rd=sp,e=Ir(e),Lr(e)){if(`selectionStart`in e)var n={start:e.selectionStart,end:e.selectionEnd};else a:{n=(n=e.ownerDocument)&&n.defaultView||window;var r=n.getSelection&&n.getSelection();if(r&&r.rangeCount!==0){n=r.anchorNode;var a=r.anchorOffset,o=r.focusNode;r=r.focusOffset;try{n.nodeType,o.nodeType}catch{n=null;break a}var s=0,c=-1,l=-1,u=0,d=0,f=e,p=null;b:for(;;){for(var m;f!==n||a!==0&&f.nodeType!==3||(c=s+a),f!==o||r!==0&&f.nodeType!==3||(l=s+r),f.nodeType===3&&(s+=f.nodeValue.length),(m=f.firstChild)!==null;)p=f,f=m;for(;;){if(f===e)break b;if(p===n&&++u===a&&(c=s),p===o&&++d===r&&(l=s),(m=f.nextSibling)!==null)break;f=p,p=f.parentNode}f=m}n=c===-1||l===-1?null:{start:c,end:l}}else n=null}n||={start:0,end:0}}else n=null;for(zd={focusedElem:e,selectionRange:n},sp=!1,al=t;al!==null;)if(t=al,e=t.child,t.subtreeFlags&1028&&e!==null)e.return=t,al=e;else for(;al!==null;){switch(t=al,o=t.alternate,e=t.flags,t.tag){case 0:if(e&4&&(e=t.updateQueue,e=e===null?null:e.events,e!==null))for(n=0;n title`))),Pd(o,r,n),o[gt]=e,kt(o),r=o;break a;case`link`:var s=Vf(`link`,`href`,a).get(r+(n.href||``));if(s){for(var c=0;cg&&(o=g,g=h,h=o);var _=Pr(s,h),v=Pr(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,E.T=null,n=lu,lu=null;var o=au,s=su;if(iu=0,ou=au=null,su=0,G&6)throw Error(i(331));var c=G;if(G|=4,Fl(o.current),Dl(o,o.current,s,n),G=c,id(0,!1),Ge&&typeof Ge.onPostCommitFiberRoot==`function`)try{Ge.onPostCommitFiberRoot(We,o)}catch{}return!0}finally{D.p=a,E.T=r,Vu(e,t)}}function Wu(e,t,n){t=j(n,t),t=$s(e.stateNode,t,2),e=Ka(e,t,2),e!==null&&(ot(e,2),rd(e))}function Z(e,t,n){if(e.tag===3)Wu(e,e,n);else for(;t!==null;){if(t.tag===3){Wu(t,e,n);break}else if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(ru===null||!ru.has(r))){e=j(n,e),n=ec(2),r=Ka(t,n,2),r!==null&&(tc(n,r,t,e),ot(r,2),rd(r));break}}t=t.return}}function Gu(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new zl;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(Ul=!0,i.add(n),e=Ku.bind(null,e,t,n),t.then(e,e))}function Ku(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,K===e&&(J&n)===n&&(X===4||X===3&&(J&62914560)===J&&300>Fe()-$l?!(G&2)&&Su(e,0):ql|=n,Yl===J&&(Yl=0)),rd(e)}function qu(e,t){t===0&&(t=it()),e=fi(e,t),e!==null&&(ot(e,t),rd(e))}function Ju(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),qu(e,n)}function Yu(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,a=e.memoizedState;a!==null&&(n=a.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(i(314))}r!==null&&r.delete(t),qu(e,n)}function Xu(e,t){return je(e,t)}var Zu=null,Qu=null,$u=!1,ed=!1,td=!1,nd=0;function rd(e){e!==Qu&&e.next===null&&(Qu===null?Zu=Qu=e:Qu=Qu.next=e),ed=!0,$u||($u=!0,ud())}function id(e,t){if(!td&&ed){td=!0;do for(var n=!1,r=Zu;r!==null;){if(!t)if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-qe(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,ld(r,a))}else a=J,a=tt(r,r===K?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||nt(r,a)||(n=!0,ld(r,a));r=r.next}while(n);td=!1}}function ad(){od()}function od(){ed=$u=!1;var e=0;nd!==0&&Gd()&&(e=nd);for(var t=Fe(),n=null,r=Zu;r!==null;){var i=r.next,a=sd(r,t);a===0?(r.next=null,n===null?Zu=i:n.next=i,i===null&&(Qu=n)):(n=r,(e!==0||a&3)&&(ed=!0)),r=i}iu!==0&&iu!==5||id(e,!1),nd!==0&&(nd=0)}function sd(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&Id(d)&&(c=c.responseEnd,o+=u*(c`u`?null:document;function xf(e,t,n){var r=bf;if(r&&typeof t==`string`&&t){var i=Jt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),hf.has(i)||(hf.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),Pd(t,`link`,e),kt(t),r.head.appendChild(t)))}}function Sf(e){_f.D(e),xf(`dns-prefetch`,e,null)}function Cf(e,t){_f.C(e,t),xf(`preconnect`,e,t)}function wf(e,t,n){_f.L(e,t,n);var r=bf;if(r&&e&&t){var i=`link[rel="preload"][as="`+Jt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Jt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Jt(n.imageSizes)+`"]`)):i+=`[href="`+Jt(e)+`"]`;var a=i;switch(t){case`style`:a=Af(e);break;case`script`:a=Pf(e)}mf.has(a)||(e=f({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),mf.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(jf(a))||t===`script`&&r.querySelector(Ff(a))||(t=r.createElement(`link`),Pd(t,`link`,e),kt(t),r.head.appendChild(t)))}}function Tf(e,t){_f.m(e,t);var n=bf;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Jt(r)+`"][href="`+Jt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Pf(e)}if(!mf.has(a)&&(e=f({rel:`modulepreload`,href:e},t),mf.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(Ff(a)))return}r=n.createElement(`link`),Pd(r,`link`,e),kt(r),n.head.appendChild(r)}}}function Ef(e,t,n){_f.S(e,t,n);var r=bf;if(r&&e){var i=Ot(r).hoistableStyles,a=Af(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(jf(a)))s.loading=5;else{e=f({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=mf.get(a))&&Rf(e,n);var c=o=r.createElement(`link`);kt(c),Pd(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Lf(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function Df(e,t){_f.X(e,t);var n=bf;if(n&&e){var r=Ot(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=f({src:e,async:!0},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),kt(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Of(e,t){_f.M(e,t);var n=bf;if(n&&e){var r=Ot(n).hoistableScripts,i=Pf(e),a=r.get(i);a||(a=n.querySelector(Ff(i)),a||(e=f({src:e,async:!0,type:`module`},t),(t=mf.get(i))&&zf(e,t),a=n.createElement(`script`),kt(a),Pd(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function kf(e,t,n,r){var a=(a=_e.current)?gf(a):null;if(!a)throw Error(i(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(t=Af(n.href),n=Ot(a).hoistableStyles,r=n.get(t),r||(r={type:`style`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Af(n.href);var o=Ot(a).hoistableStyles,s=o.get(e);if(s||(a=a.ownerDocument||a,s={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},o.set(e,s),(o=a.querySelector(jf(e)))&&!o._p&&(s.instance=o,s.state.loading=5),mf.has(e)||(n={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},mf.set(e,n),o||Nf(a,e,n,s.state))),t&&r===null)throw Error(i(528,``));return s}if(t&&r!==null)throw Error(i(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(t=Pf(n),n=Ot(a).hoistableScripts,r=n.get(t),r||(r={type:`script`,instance:null,count:0,state:null},n.set(t,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(i(444,e))}}function Af(e){return`href="`+Jt(e)+`"`}function jf(e){return`link[rel="stylesheet"][`+e+`]`}function Mf(e){return f({},e,{"data-precedence":e.precedence,precedence:null})}function Nf(e,t,n,r){e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)?r.loading=1:(t=e.createElement(`link`),r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2}),Pd(t,`link`,n),kt(t),e.head.appendChild(t))}function Pf(e){return`[src="`+Jt(e)+`"]`}function Ff(e){return`script[async]`+e}function If(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Jt(n.href)+`"]`);if(r)return t.instance=r,kt(r),r;var a=f({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),kt(r),Pd(r,`style`,a),Lf(r,n.precedence,e),t.instance=r;case`stylesheet`:a=Af(n.href);var o=e.querySelector(jf(a));if(o)return t.state.loading|=4,t.instance=o,kt(o),o;r=Mf(n),(a=mf.get(a))&&Rf(r,a),o=(e.ownerDocument||e).createElement(`link`),kt(o);var s=o;return s._p=new Promise(function(e,t){s.onload=e,s.onerror=t}),Pd(o,`link`,r),t.state.loading|=4,Lf(o,n.precedence,e),t.instance=o;case`script`:return o=Pf(n.src),(a=e.querySelector(Ff(o)))?(t.instance=a,kt(a),a):(r=n,(a=mf.get(o))&&(r=f({},n),zf(r,a)),e=e.ownerDocument||e,a=e.createElement(`script`),kt(a),Pd(a,`link`,r),e.head.appendChild(a),t.instance=a);case`void`:return null;default:throw Error(i(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Lf(r,n.precedence,e));return t.instance}function Lf(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function Uf(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Wf(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Gf(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Af(r.href),a=t.querySelector(jf(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=Jf.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,kt(a);return}a=t.ownerDocument||t,r=Mf(r),(i=mf.get(i))&&Rf(r,i),a=a.createElement(`link`),kt(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),Pd(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=Jf.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var Kf=0;function qf(e,t){return e.stylesheets&&e.count===0&&Xf(e,e.stylesheets),0Kf?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function Jf(){if(this.count--,this.count===0&&(this.imgCount===0||!this.waitingForImages)){if(this.stylesheets)Xf(this,this.stylesheets);else if(this.unsuspend){var e=this.unsuspend;this.unsuspend=null,e()}}}var Yf=null;function Xf(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,Yf=new Map,t.forEach(Zf,e),Yf=null,Jf.call(e))}function Zf(e,t){if(!(t.state.loading&4)){var n=Yf.get(e);if(n)var r=n.get(null);else{n=new Map,Yf.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(!(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__>`u`||typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE!=`function`))try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=v()})),b=u(p(),1),x=y(),S=`modulepreload`,C=function(e){return`/`+e},w={},ee=function(e,t,n){let r=Promise.resolve();if(t&&t.length>0){let e=document.getElementsByTagName(`link`),i=document.querySelector(`meta[property=csp-nonce]`),a=i?.nonce||i?.getAttribute(`nonce`);function o(e){return Promise.all(e.map(e=>Promise.resolve(e).then(e=>({status:`fulfilled`,value:e}),e=>({status:`rejected`,reason:e}))))}r=o(t.map(t=>{if(t=C(t,n),t in w)return;w[t]=!0;let r=t.endsWith(`.css`),i=r?`[rel="stylesheet"]`:``;if(n)for(let n=e.length-1;n>=0;n--){let i=e[n];if(i.href===t&&(!r||i.rel===`stylesheet`))return}else if(document.querySelector(`link[href="${t}"]${i}`))return;let o=document.createElement(`link`);if(o.rel=r?`stylesheet`:S,r||(o.as=`script`),o.crossOrigin=``,o.href=t,a&&o.setAttribute(`nonce`,a),document.head.appendChild(o),r)return new Promise((e,n)=>{o.addEventListener(`load`,e),o.addEventListener(`error`,()=>n(Error(`Unable to preload CSS for ${t}`)))})}))}function i(e){let t=new Event(`vite:preloadError`,{cancelable:!0});if(t.payload=e,window.dispatchEvent(t),!t.defaultPrevented)throw e}return r.then(t=>{for(let e of t||[])e.status===`rejected`&&i(e.reason);return e().catch(i)})},te=/^(?:[a-z][a-z0-9+.-]*:|[\\/]{2})/i,ne=/^[\\/]{2}/;function re(e,t){return t+e.replace(/\\/g,`/`)}var ie=`popstate`;function ae(e){return typeof e==`object`&&!!e&&`pathname`in e&&`search`in e&&`hash`in e&&`state`in e&&`key`in e}function oe(e={}){function t(e,t){let n=t.state?.masked,{pathname:r,search:i,hash:a}=n||e.location;return E(``,{pathname:r,search:i,hash:a},t.state&&t.state.usr||null,t.state&&t.state.key||`default`,n?{pathname:e.location.pathname,search:e.location.search,hash:e.location.hash}:void 0)}function n(e,t){return typeof t==`string`?t:D(t)}return de(t,n,null,e)}function T(e,t){if(e===!1||e==null)throw Error(t)}function se(e,t){if(!e){typeof console<`u`&&console.warn(t);try{throw Error(t)}catch{}}}function ce(){return Math.random().toString(36).substring(2,10)}function le(e,t){return{usr:e.state,key:e.key,idx:t,masked:e.mask?{pathname:e.pathname,search:e.search,hash:e.hash}:void 0}}function E(e,t,n=null,r,i){return{pathname:typeof e==`string`?e:e.pathname,search:``,hash:``,...typeof t==`string`?ue(t):t,state:n,key:t&&t.key||r||ce(),mask:i}}function D({pathname:e=`/`,search:t=``,hash:n=``}){return t&&t!==`?`&&(e+=t.charAt(0)===`?`?t:`?`+t),n&&n!==`#`&&(e+=n.charAt(0)===`#`?n:`#`+n),e}function ue(e){let t={};if(e){let n=e.indexOf(`#`);n>=0&&(t.hash=e.substring(n),e=e.substring(0,n));let r=e.indexOf(`?`);r>=0&&(t.search=e.substring(r),e=e.substring(0,r)),e&&(t.pathname=e)}return t}function de(e,t,n,r={}){let{window:i=document.defaultView,v5Compat:a=!1}=r,o=i.history,s=`POP`,c=null,l=u();l??(l=0,o.replaceState({...o.state,idx:l},``));function u(){return(o.state||{idx:null}).idx}function d(){s=`POP`;let e=u(),t=e==null?null:e-l;l=e,c&&c({action:s,location:h.location,delta:t})}function f(e,t){s=`PUSH`;let r=ae(e)?e:E(h.location,e,t);n&&n(r,e),l=u()+1;let d=le(r,l),f=h.createHref(r.mask||r);try{o.pushState(d,``,f)}catch(e){if(e instanceof DOMException&&e.name===`DataCloneError`)throw e;i.location.assign(f)}a&&c&&c({action:s,location:h.location,delta:1})}function p(e,t){s=`REPLACE`;let r=ae(e)?e:E(h.location,e,t);n&&n(r,e),l=u();let i=le(r,l),d=h.createHref(r.mask||r);o.replaceState(i,``,d),a&&c&&c({action:s,location:h.location,delta:0})}function m(e){return fe(i,e)}let h={get action(){return s},get location(){return e(i,o)},listen(e){if(c)throw Error(`A history only accepts one active listener`);return i.addEventListener(ie,d),c=e,()=>{i.removeEventListener(ie,d),c=null}},createHref(e){return t(i,e)},createURL:m,encodeLocation(e){let t=m(e);return{pathname:t.pathname,search:t.search,hash:t.hash}},push:f,replace:p,go(e){return o.go(e)}};return h}function fe(e,t,n=!1){let r=`http://localhost`;e&&(r=e.location.origin===`null`?e.location.href:e.location.origin),T(r,`No window.location.(origin|href) available to create URL`);let i=typeof t==`string`?t:D(t);return i=i.replace(/ $/,`%20`),!n&&ne.test(i)&&(i=r+i),new URL(i,r)}function pe(e,t,n=`/`){return me(e,t,n,!1)}function me(e,t,n,r,i){let a=Ne((typeof t==`string`?ue(t):t).pathname||`/`,n);if(a==null)return null;let o=i??he(e),s=null,c=Me(a);for(let e=0;s==null&&e{let c={relativePath:s===void 0?e.path||``:s,caseSensitive:e.caseSensitive===!0,childrenIndex:a,route:e};if(c.relativePath.startsWith(`/`)){if(!c.relativePath.startsWith(r)&&o)return;T(c.relativePath.startsWith(r),`Absolute route path "${c.relativePath}" nested under path "${r}" is not valid. An absolute child route path must start with the combined path of all its parent routes.`),c.relativePath=c.relativePath.slice(r.length)}let l=Ve([r,c.relativePath]),u=n.concat(c);e.children&&e.children.length>0&&(T(e.index!==!0,`Index routes must not have child routes. Please remove all child routes from route path "${l}".`),ge(e.children,t,u,l,o)),!(e.path==null&&!e.index)&&t.push({path:l,score:Ee(l,e.index),routesMeta:u.map((e,t)=>{let[n,r]=je(e.relativePath,e.caseSensitive,t===u.length-1);return{...e,matcher:n,compiledParams:r}})})};return e.forEach((e,t)=>{if(e.path===``||!e.path?.includes(`?`))a(e,t);else for(let n of _e(e.path))a(e,t,!0,n)}),t}function _e(e){let t=e.split(`/`);if(t.length===0)return[];let[n,...r]=t,i=n.endsWith(`?`),a=n.replace(/\?$/,``);if(r.length===0)return i?[a,``]:[a];let o=_e(r.join(`/`)),s=[];return s.push(...o.map(e=>e===``?a:[a,e].join(`/`))),i&&s.push(...o),s.map(t=>e.startsWith(`/`)&&t===``?`/`:t)}function ve(e){e.sort((e,t)=>e.score===t.score?De(e.routesMeta.map(e=>e.childrenIndex),t.routesMeta.map(e=>e.childrenIndex)):t.score-e.score)}var ye=/^:[\w-]+$/,be=3,xe=2,Se=1,Ce=10,we=-2,Te=e=>e===`*`;function Ee(e,t){let n=e.split(`/`),r=n.length;return n.some(Te)&&(r+=we),t&&(r+=xe),n.filter(e=>!Te(e)).reduce((e,t)=>e+(ye.test(t)?be:t===``?Se:Ce),r)}function De(e,t){return e.length===t.length&&e.slice(0,-1).every((e,n)=>e===t[n])?e[e.length-1]-t[t.length-1]:0}function Oe(e,t,n=!1){let{routesMeta:r}=e,i={},a=`/`,o=[];for(let e=0;e{if(t===`*`){let e=s[r]||``;o=a.slice(0,a.length-e.length).replace(/(.)\/+$/,`$1`)}let i=s[r];return n&&!i?e[t]=void 0:e[t]=(i||``).replace(/%2F/g,`/`),e},{}),pathname:a,pathnameBase:o,pattern:e}}function je(e,t=!1,n=!0){se(e===`*`||!e.endsWith(`*`)||e.endsWith(`/*`),`Route path "${e}" will be treated as if it were "${e.replace(/\*$/,`/*`)}" because the \`*\` character must always follow a \`/\` in the pattern. To get rid of this warning, please change the route path to "${e.replace(/\*$/,`/*`)}".`);let r=[],i=`^`+e.replace(/\/*\*?$/,``).replace(/^\/*/,`/`).replace(/[\\.*+^${}|()[\]]/g,`\\$&`).replace(/\/:([\w-]+)(\?)?/g,(e,t,n,i,a)=>{if(r.push({paramName:t,isOptional:n!=null}),n){let t=a.charAt(i+e.length);return t&&t!==`/`?`/([^\\/]*)`:`(?:/([^\\/]*))?`}return`/([^\\/]+)`}).replace(/\/([\w-]+)\?(\/|$)/g,`(/$1)?$2`);return e.endsWith(`*`)?(r.push({paramName:`*`}),i+=e===`*`||e===`/*`?`(.*)$`:`(?:\\/(.+)|\\/*)$`):n?i+=`\\/*$`:e!==``&&e!==`/`&&(i+=`(?:(?=\\/|$))`),[new RegExp(i,t?void 0:`i`),r]}function Me(e){try{return e.split(`/`).map(e=>decodeURIComponent(e).replace(/\//g,`%2F`)).join(`/`)}catch(t){return se(!1,`The URL path "${e}" could not be decoded because it is a malformed URL segment. This is probably due to a bad percent encoding (${t}).`),e}}function Ne(e,t){if(t===`/`)return e;if(!e.toLowerCase().startsWith(t.toLowerCase()))return null;let n=t.endsWith(`/`)?t.length-1:t.length,r=e.charAt(n);return r&&r!==`/`?null:e.slice(n)||`/`}function Pe(e,t=`/`){let{pathname:n,search:r=``,hash:i=``}=typeof e==`string`?ue(e):e,a;return n?(n=Be(n),a=n.startsWith(`/`)?Fe(n.substring(1),`/`):Fe(n,t)):a=t,{pathname:a,search:We(r),hash:Ge(i)}}function Fe(e,t){let n=He(t).split(`/`);return e.split(`/`).forEach(e=>{e===`..`?n.length>1&&n.pop():e!==`.`&&n.push(e)}),n.length>1?n.join(`/`):`/`}function Ie(e,t,n,r){return`Cannot include a '${e}' character in a manually specified \`to.${t}\` field [${JSON.stringify(r)}]. Please separate it out to the \`to.${n}\` field. Alternatively you may provide the full path as a string in and the router will parse it for you.`}function Le(e){return e.filter((e,t)=>t===0||e.route.path&&e.route.path.length>0)}function Re(e){let t=Le(e);return t.map((e,n)=>n===t.length-1?e.pathname:e.pathnameBase)}function ze(e,t,n,r=!1){let i;typeof e==`string`?i=ue(e):(i={...e},T(!i.pathname||!i.pathname.includes(`?`),Ie(`?`,`pathname`,`search`,i)),T(!i.pathname||!i.pathname.includes(`#`),Ie(`#`,`pathname`,`hash`,i)),T(!i.search||!i.search.includes(`#`),Ie(`#`,`search`,`hash`,i)));let a=e===``||i.pathname===``,o=a?`/`:i.pathname,s;if(o==null)s=n;else{let e=t.length-1;if(!r&&o.startsWith(`..`)){let t=o.split(`/`);for(;t[0]===`..`;)t.shift(),--e;i.pathname=t.join(`/`)}s=e>=0?t[e]:`/`}let c=Pe(i,s),l=o&&o!==`/`&&o.endsWith(`/`),u=(a||o===`.`)&&n.endsWith(`/`);return!c.pathname.endsWith(`/`)&&(l||u)&&(c.pathname+=`/`),c}var Be=e=>e.replace(/[\\/]{2,}/g,`/`),Ve=e=>Be(e.join(`/`)),He=e=>e.replace(/\/+$/,``),Ue=e=>He(e).replace(/^\/*/,`/`),We=e=>!e||e===`?`?``:e.startsWith(`?`)?e:`?`+e,Ge=e=>!e||e===`#`?``:e.startsWith(`#`)?e:`#`+e,Ke=class{constructor(e,t,n,r=!1){this.status=e,this.statusText=t||``,this.internal=r,n instanceof Error?(this.data=n.toString(),this.error=n):this.data=n}};function qe(e){return e!=null&&typeof e.status==`number`&&typeof e.statusText==`string`&&typeof e.internal==`boolean`&&`data`in e}function Je(e){return Ve(e.map(e=>e.route.path).filter(Boolean))||`/`}var Ye=typeof window<`u`&&window.document!==void 0&&window.document.createElement!==void 0;function Xe(e,t){let n=e;if(typeof n!=`string`||!te.test(n))return{absoluteURL:void 0,isExternal:!1,to:n};let r=n,i=!1;if(Ye)try{let e=new URL(window.location.href),r=ne.test(n)?new URL(re(n,e.protocol)):new URL(n),a=Ne(r.pathname,t);r.origin===e.origin&&a!=null?n=a+r.search+r.hash:i=!0}catch{se(!1,` contains an invalid URL which will probably break when clicked - please update to a valid URL path.`)}return{absoluteURL:r,isExternal:i,to:n}}Object.getOwnPropertyNames(Object.prototype).sort().join(`\0`);var Ze=[`POST`,`PUT`,`PATCH`,`DELETE`];new Set(Ze);var Qe=[`GET`,...Ze];new Set(Qe);var $e=[`about:`,`blob:`,`chrome:`,`chrome-untrusted:`,`content:`,`data:`,`devtools:`,`file:`,`filesystem:`,`javascript:`];function et(e){try{return $e.includes(new URL(e).protocol)}catch{return!1}}var tt=b.createContext(null);tt.displayName=`DataRouter`;var nt=b.createContext(null);nt.displayName=`DataRouterState`;var rt=b.createContext(!1);function it(){return b.useContext(rt)}var at=b.createContext({isTransitioning:!1});at.displayName=`ViewTransition`;var ot=b.createContext(new Map);ot.displayName=`Fetchers`;var st=b.createContext(null);st.displayName=`Await`;var ct=b.createContext(null);ct.displayName=`Navigation`;var lt=b.createContext(null);lt.displayName=`Location`;var ut=b.createContext({outlet:null,matches:[],isDataRoute:!1});ut.displayName=`Route`;var dt=b.createContext(null);dt.displayName=`RouteError`;var ft=`REACT_ROUTER_ERROR`,pt=`REDIRECT`,mt=`ROUTE_ERROR_RESPONSE`;function ht(e){if(e.startsWith(`${ft}:${pt}:{`))try{let t=JSON.parse(e.slice(28));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`&&typeof t.location==`string`&&typeof t.reloadDocument==`boolean`&&typeof t.replace==`boolean`)return t}catch{}}function gt(e){if(e.startsWith(`${ft}:${mt}:{`))try{let t=JSON.parse(e.slice(40));if(typeof t==`object`&&t&&typeof t.status==`number`&&typeof t.statusText==`string`)return new Ke(t.status,t.statusText,t.data)}catch{}}function _t(e,{relative:t}={}){T(vt(),`useHref() may be used only in the context of a component.`);let{basename:n,navigator:r}=b.useContext(ct),{hash:i,pathname:a,search:o}=Et(e,{relative:t}),s=a;return n!==`/`&&(s=a===`/`?n:Ve([n,a])),r.createHref({pathname:s,search:o,hash:i})}function vt(){return b.useContext(lt)!=null}function yt(){return T(vt(),`useLocation() may be used only in the context of a component.`),b.useContext(lt).location}var bt=`You should call navigate() in a React.useEffect(), not when your component is first rendered.`;function xt(e){b.useContext(ct).static||b.useLayoutEffect(e)}function St(){let{isDataRoute:e}=b.useContext(ut);return e?Gt():Ct()}function Ct(){T(vt(),`useNavigate() may be used only in the context of a component.`);let e=b.useContext(tt),{basename:t,navigator:n}=b.useContext(ct),{matches:r}=b.useContext(ut),{pathname:i}=yt(),a=JSON.stringify(Re(r)),o=b.useRef(!1);return xt(()=>{o.current=!0}),b.useCallback((r,s={})=>{if(se(o.current,bt),!o.current)return;if(typeof r==`number`){n.go(r);return}let c=ze(r,JSON.parse(a),i,s.relative===`path`);e==null&&t!==`/`&&(c.pathname=c.pathname===`/`?t:Ve([t,c.pathname])),(s.replace?n.replace:n.push)(c,s.state,s)},[t,n,a,i,e])}var wt=b.createContext(null);function Tt(e){let t=b.useContext(ut).outlet;return b.useMemo(()=>t&&b.createElement(wt.Provider,{value:e},t),[t,e])}function Et(e,{relative:t}={}){let{matches:n}=b.useContext(ut),{pathname:r}=yt(),i=JSON.stringify(Re(n));return b.useMemo(()=>ze(e,JSON.parse(i),r,t===`path`),[e,i,r,t])}function Dt(e,t){return Ot(e,t)}function Ot(e,t,n){T(vt(),`useRoutes() may be used only in the context of a component.`);let{navigator:r}=b.useContext(ct),{matches:i}=b.useContext(ut),a=i[i.length-1],o=a?a.params:{},s=a?a.pathname:`/`,c=a?a.pathnameBase:`/`,l=a&&a.route;{let e=l&&l.path||``;qt(s,!l||e.endsWith(`*`)||e.endsWith(`*?`),`You rendered descendant (or called \`useRoutes()\`) at "${s}" (under ) but the parent route path has no trailing "*". This means if you navigate deeper, the parent won't match anymore and therefore the child routes will never render. - -Please change the parent to .`)}let u=yt(),d;if(t){let e=typeof t==`string`?ue(t):t;T(c===`/`||e.pathname?.startsWith(c),`When overriding the location using \`\` or \`useRoutes(routes, location)\`, the location pathname must begin with the portion of the URL pathname that was matched by all parent routes. The current pathname base is "${c}" but pathname "${e.pathname}" was given in the \`location\` prop.`),d=e}else d=u;let f=d.pathname||`/`,p=f;if(c!==`/`){let e=c.replace(/^\//,``).split(`/`);p=`/`+f.replace(/^\//,``).split(`/`).slice(e.length).join(`/`)}let m=n&&n.state.matches.length?n.state.matches.map(e=>Object.assign(e,{route:n.manifest[e.route.id]||e.route})):pe(e,{pathname:p});se(l||m!=null,`No routes matched location "${d.pathname}${d.search}${d.hash}" `),se(m==null||m[m.length-1].route.element!==void 0||m[m.length-1].route.Component!==void 0||m[m.length-1].route.lazy!==void 0,`Matched leaf route at location "${d.pathname}${d.search}${d.hash}" does not have an element or Component. This means it will render an with a null value by default resulting in an "empty" page.`);let h=Ft(m&&m.map(e=>Object.assign({},e,{params:Object.assign({},o,e.params),pathname:Ve([c,r.encodeLocation?r.encodeLocation(e.pathname.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathname]),pathnameBase:e.pathnameBase===`/`?c:Ve([c,r.encodeLocation?r.encodeLocation(e.pathnameBase.replace(/%/g,`%25`).replace(/\?/g,`%3F`).replace(/#/g,`%23`)).pathname:e.pathnameBase])})),i,n);return t&&h?b.createElement(lt.Provider,{value:{location:{pathname:`/`,search:``,hash:``,state:null,key:`default`,mask:void 0,...d},navigationType:`POP`}},h):h}function kt(){let e=Wt(),t=qe(e)?`${e.status} ${e.statusText}`:e instanceof Error?e.message:JSON.stringify(e),n=e instanceof Error?e.stack:null,r=`rgba(200,200,200, 0.5)`,i={padding:`0.5rem`,backgroundColor:r},a={padding:`2px 4px`,backgroundColor:r},o=null;return console.error(`Error handled by React Router default ErrorBoundary:`,e),o=b.createElement(b.Fragment,null,b.createElement(`p`,null,`💿 Hey developer 👋`),b.createElement(`p`,null,`You can provide a way better UX than this when your app throws errors by providing your own `,b.createElement(`code`,{style:a},`ErrorBoundary`),` or`,` `,b.createElement(`code`,{style:a},`errorElement`),` prop on your route.`)),b.createElement(b.Fragment,null,b.createElement(`h2`,null,`Unexpected Application Error!`),b.createElement(`h3`,{style:{fontStyle:`italic`}},t),n?b.createElement(`pre`,{style:i},n):null,o)}var At=b.createElement(kt,null),jt=class extends b.Component{constructor(e){super(e),this.state={location:e.location,revalidation:e.revalidation,error:e.error}}static getDerivedStateFromError(e){return{error:e}}static getDerivedStateFromProps(e,t){return t.location!==e.location||t.revalidation!==`idle`&&e.revalidation===`idle`?{error:e.error,location:e.location,revalidation:e.revalidation}:{error:e.error===void 0?t.error:e.error,location:t.location,revalidation:e.revalidation||t.revalidation}}componentDidCatch(e,t){this.props.onError?this.props.onError(e,t):console.error(`React Router caught the following error during render`,e)}render(){let e=this.state.error;if(this.context&&typeof e==`object`&&e&&`digest`in e&&typeof e.digest==`string`){let t=gt(e.digest);t&&(e=t)}let t=e===void 0?this.props.children:b.createElement(ut.Provider,{value:this.props.routeContext},b.createElement(dt.Provider,{value:e,children:this.props.component}));return this.context?b.createElement(Nt,{error:e},t):t}};jt.contextType=rt;var Mt=new WeakMap;function Nt({children:e,error:t}){let{basename:n}=b.useContext(ct);if(typeof t==`object`&&t&&`digest`in t&&typeof t.digest==`string`){let e=ht(t.digest);if(e){let r=Mt.get(t);if(r)throw r;let i=Xe(e.location,n),a=i.absoluteURL||i.to;if(et(a))throw Error(`Invalid redirect location`);if(Ye&&!Mt.get(t))if(i.isExternal||e.reloadDocument)window.location.href=a;else{let n=Promise.resolve().then(()=>window.__reactRouterDataRouter.navigate(i.to,{replace:e.replace}));throw Mt.set(t,n),n}return b.createElement(`meta`,{httpEquiv:`refresh`,content:`0;url=${a}`})}}return e}function Pt({routeContext:e,match:t,children:n}){let r=b.useContext(tt);return r&&r.static&&r.staticContext&&(t.route.errorElement||t.route.ErrorBoundary)&&(r.staticContext._deepestRenderedBoundaryId=t.route.id),b.createElement(ut.Provider,{value:e},n)}function Ft(e,t=[],n){let r=n?.state;if(e==null){if(!r)return null;if(r.errors)e=r.matches;else if(t.length===0&&!r.initialized&&r.matches.length>0)e=r.matches;else return null}let i=e,a=r?.errors;if(a!=null){let e=i.findIndex(e=>e.route.id&&a?.[e.route.id]!==void 0);T(e>=0,`Could not find a matching route for errors on route IDs: ${Object.keys(a).join(`,`)}`),i=i.slice(0,Math.min(i.length,e+1))}let o=!1,s=-1;if(n&&r){o=r.renderFallback;for(let e=0;e=0?i.slice(0,s+1):[i[0]];break}}}}let c=n?.onError,l=r&&c?(e,t)=>{c(e,{location:r.location,params:r.matches?.[0]?.params??{},pattern:Je(r.matches),errorInfo:t})}:void 0;return i.reduceRight((e,n,c)=>{let u,d=!1,f=null,p=null;r&&(u=a&&n.route.id?a[n.route.id]:void 0,f=n.route.errorElement||At,o&&(s<0&&c===0?(qt(`route-fallback`,!1,"No `HydrateFallback` element provided to render during initial hydration"),d=!0,p=null):s===c&&(d=!0,p=n.route.hydrateFallbackElement||null)));let m=t.concat(i.slice(0,c+1)),h=()=>{let t;return t=u?f:d?p:n.route.Component?b.createElement(n.route.Component,null):n.route.element?n.route.element:e,b.createElement(Pt,{match:n,routeContext:{outlet:e,matches:m,isDataRoute:r!=null},children:t})};return r&&(n.route.ErrorBoundary||n.route.errorElement||c===0)?b.createElement(jt,{location:r.location,revalidation:r.revalidation,component:f,error:u,children:h(),routeContext:{outlet:null,matches:m,isDataRoute:!0},onError:l}):h()},null)}function It(e){return`${e} must be used within a data router. See https://reactrouter.com/en/main/routers/picking-a-router.`}function Lt(e){let t=b.useContext(tt);return T(t,It(e)),t}function Rt(e){let t=b.useContext(nt);return T(t,It(e)),t}function zt(e){let t=b.useContext(ut);return T(t,It(e)),t}function Bt(e){let t=zt(e),n=t.matches[t.matches.length-1];return T(n.route.id,`${e} can only be used on routes that contain a unique "id"`),n.route.id}function Vt(){return Bt(`useRouteId`)}function Ht(){let e=Rt(`useNavigation`);return b.useMemo(()=>{let{matches:t,historyAction:n,...r}=e.navigation;return r},[e.navigation])}function Ut(){let{matches:e,loaderData:t}=Rt(`useMatches`);return b.useMemo(()=>e.map(e=>O(e,t)),[e,t])}function Wt(){let e=b.useContext(dt),t=Rt(`useRouteError`),n=Bt(`useRouteError`);return e===void 0?t.errors?.[n]:e}function Gt(){let{router:e}=Lt(`useNavigate`),t=Bt(`useNavigate`),n=b.useRef(!1);return xt(()=>{n.current=!0}),b.useCallback(async(r,i={})=>{se(n.current,bt),n.current&&(typeof r==`number`?await e.navigate(r):await e.navigate(r,{fromRouteId:t,...i}))},[e,t])}var Kt={};function qt(e,t,n){!t&&!Kt[e]&&(Kt[e]=!0,se(!1,n))}b.memo(Jt);function Jt({routes:e,manifest:t,future:n,state:r,isStatic:i,onError:a}){return Ot(e,void 0,{manifest:t,state:r,isStatic:i,onError:a,future:n})}function Yt(e){return Tt(e.context)}function Xt(e){T(!1,`A is only ever to be used as the child of element, never rendered directly. Please wrap your in a .`)}function Zt({basename:e=`/`,children:t=null,location:n,navigationType:r=`POP`,navigator:i,static:a=!1,useTransitions:o}){T(!vt(),`You cannot render a inside another . You should never have more than one in your app.`);let s=e.replace(/^\/*/,`/`),c=b.useMemo(()=>({basename:s,navigator:i,static:a,useTransitions:o,future:{}}),[s,i,a,o]);typeof n==`string`&&(n=ue(n));let{pathname:l=`/`,search:u=``,hash:d=``,state:f=null,key:p=`default`,mask:m}=n,h=b.useMemo(()=>{let e=Ne(l,s);return e==null?null:{location:{pathname:e,search:u,hash:d,state:f,key:p,mask:m},navigationType:r}},[s,l,u,d,f,p,r,m]);return se(h!=null,` is not able to match the URL "${l}${u}${d}" because it does not start with the basename, so the won't render anything.`),h==null?null:b.createElement(ct.Provider,{value:c},b.createElement(lt.Provider,{children:t,value:h}))}function Qt({children:e,location:t}){return Dt($t(e),t)}b.Component;function $t(e,t=[]){let n=[];return b.Children.forEach(e,(e,r)=>{if(!b.isValidElement(e))return;let i=[...t,r];if(e.type===b.Fragment){n.push.apply(n,$t(e.props.children,i));return}T(e.type===Xt,`[${typeof e.type==`string`?e.type:e.type.name}] is not a component. All component children of must be a or `),T(!e.props.index||!e.props.children,`An index route cannot have child routes.`);let a={id:e.props.id||i.join(`-`),caseSensitive:e.props.caseSensitive,element:e.props.element,Component:e.props.Component,index:e.props.index,path:e.props.path,middleware:e.props.middleware,loader:e.props.loader,action:e.props.action,hydrateFallbackElement:e.props.hydrateFallbackElement,HydrateFallback:e.props.HydrateFallback,errorElement:e.props.errorElement,ErrorBoundary:e.props.ErrorBoundary,hasErrorBoundary:e.props.hasErrorBoundary===!0||e.props.ErrorBoundary!=null||e.props.errorElement!=null,shouldRevalidate:e.props.shouldRevalidate,handle:e.props.handle,lazy:e.props.lazy};e.props.children&&(a.children=$t(e.props.children,i)),n.push(a)}),n}var en=`get`,tn=`application/x-www-form-urlencoded`;function nn(e){return typeof HTMLElement<`u`&&e instanceof HTMLElement}function rn(e){return nn(e)&&e.tagName.toLowerCase()===`button`}function an(e){return nn(e)&&e.tagName.toLowerCase()===`form`}function on(e){return nn(e)&&e.tagName.toLowerCase()===`input`}function sn(e){return!!(e.metaKey||e.altKey||e.ctrlKey||e.shiftKey)}function cn(e,t){return e.button===0&&(!t||t===`_self`)&&!sn(e)}var ln=null;function un(){if(ln===null)try{new FormData(document.createElement(`form`),0),ln=!1}catch{ln=!0}return ln}var dn=new Set([`application/x-www-form-urlencoded`,`multipart/form-data`,`text/plain`]);function fn(e){return e!=null&&!dn.has(e)?(se(!1,`"${e}" is not a valid \`encType\` for \`
\`/\`\` and will default to "${tn}"`),null):e}function pn(e,t){let n,r,i,a,o;if(an(e)){let o=e.getAttribute(`action`);r=o?Ne(o,t):null,n=e.getAttribute(`method`)||en,i=fn(e.getAttribute(`enctype`))||tn,a=new FormData(e)}else if(rn(e)||on(e)&&(e.type===`submit`||e.type===`image`)){let o=e.form;if(o==null)throw Error(`Cannot submit a