From 7e256c426f72427891d55de01144d865608eb299 Mon Sep 17 00:00:00 2001 From: xiaji Date: Wed, 10 Jun 2026 12:20:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(ui):=20=E5=AE=89=E5=85=A8=E8=BD=AF?= =?UTF-8?q?=E4=BB=B6=E9=A3=8E=E6=A0=BC=E4=B8=BB=E9=A2=98=20+=20=E4=B8=89?= =?UTF-8?q?=E9=98=B6=E6=AE=B5=E8=BF=9B=E5=BA=A6/=E6=97=A5=E5=BF=97=20+=20X?= =?UTF-8?q?LSX=20=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 主面板:阶段1扫描全盘 → 阶段2抽样 → 阶段3抽检,每阶段独立进度条/已用时/分类型 chips - 日志:按类型着色(命中红/未命中绿/警告黄/阶段青) - 主题:暗绿底 + 鲜绿/青色强调,圆角胶囊按钮(material::security_dark) - 抽检:SampleMode 枚举支持按份数/百分比/全部;设置页 C 组动态切换 - 抽检:XLSX 检查器(zip + quick-xml 解析 sharedStrings 与 sheet) - 扫描:walker 进度回调(已访问、命中候选、当前目录) - 兼容:quick-xml 0.36 使用 reader.config_mut().trim_text() - 仓库:新增 .gitignore 忽略 venv/pyc/target/构建产物 --- .gitignore | 40 + Cargo.lock | 5043 +++++++++++++++++++++++++++++++++ Cargo.toml | 66 + UmiOCR-data/.pre_settings | 12 +- UmiOCR-data/.settings | 106 +- build.rs | 5 + src/app.rs | 216 ++ src/config/mod.rs | 10 + src/config/model.rs | 349 +++ src/config/persist.rs | 16 + src/inspect/doc_inspector.rs | 93 + src/inspect/docx_inspector.rs | 81 + src/inspect/external.rs | 94 + src/inspect/mod.rs | 59 + src/inspect/mod_helper.rs | 3 + src/inspect/pdf_inspector.rs | 44 + src/inspect/screenshot.rs | 116 + src/inspect/umi_ocr.rs | 141 + src/inspect/xlsx_inspector.rs | 186 ++ src/main.rs | 44 + src/matcher/hash.rs | 29 + src/matcher/keywords.rs | 80 + src/matcher/mod.rs | 3 + src/privilege.rs | 54 + src/report/html.rs | 78 + src/report/json.rs | 19 + src/report/mod.rs | 5 + src/report/model.rs | 17 + src/report/png.rs | 23 + src/scan/filter.rs | 44 + src/scan/mod.rs | 5 + src/scan/runner.rs | 239 ++ src/scan/sampler.rs | 123 + src/scan/walker.rs | 115 + src/ui/home.rs | 316 +++ src/ui/material.rs | 205 ++ src/ui/mod.rs | 6 + src/ui/report.rs | 40 + src/ui/settings.rs | 314 ++ src/ui/widgets.rs | 68 + src/utils/logger.rs | 25 + src/utils/mod.rs | 3 + src/utils/paths.rs | 53 + 43 files changed, 8529 insertions(+), 59 deletions(-) create mode 100644 .gitignore create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 build.rs create mode 100644 src/app.rs create mode 100644 src/config/mod.rs create mode 100644 src/config/model.rs create mode 100644 src/config/persist.rs create mode 100644 src/inspect/doc_inspector.rs create mode 100644 src/inspect/docx_inspector.rs create mode 100644 src/inspect/external.rs create mode 100644 src/inspect/mod.rs create mode 100644 src/inspect/mod_helper.rs create mode 100644 src/inspect/pdf_inspector.rs create mode 100644 src/inspect/screenshot.rs create mode 100644 src/inspect/umi_ocr.rs create mode 100644 src/inspect/xlsx_inspector.rs create mode 100644 src/main.rs create mode 100644 src/matcher/hash.rs create mode 100644 src/matcher/keywords.rs create mode 100644 src/matcher/mod.rs create mode 100644 src/privilege.rs create mode 100644 src/report/html.rs create mode 100644 src/report/json.rs create mode 100644 src/report/mod.rs create mode 100644 src/report/model.rs create mode 100644 src/report/png.rs create mode 100644 src/scan/filter.rs create mode 100644 src/scan/mod.rs create mode 100644 src/scan/runner.rs create mode 100644 src/scan/sampler.rs create mode 100644 src/scan/walker.rs create mode 100644 src/ui/home.rs create mode 100644 src/ui/material.rs create mode 100644 src/ui/mod.rs create mode 100644 src/ui/report.rs create mode 100644 src/ui/settings.rs create mode 100644 src/ui/widgets.rs create mode 100644 src/utils/logger.rs create mode 100644 src/utils/mod.rs create mode 100644 src/utils/paths.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bebf852 --- /dev/null +++ b/.gitignore @@ -0,0 +1,40 @@ +# Rust 构建产物 +/target/ +**/*.rs.bk +Cargo.lock.bak + +# 虚拟环境(uv / venv / conda / python venv 等) +.venv/ +venv/ +env/ +.python-version +__pypackages__/ + +# 编译/缓存目录 +.cargo/ +.maturin/ +.pytest_cache/ +.mypy_cache/ +.ruff_cache/ + +# Python 字节码与缓存(pyc / __pycache__) +__pycache__/ +**/__pycache__/ +*.py[cod] +*$py.class +*.pyo +*.pyd +*.so + +# 运行时产物 +build.log +secret-file-selfcheck.exe +reports/ + +# IDE / 编辑器 +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store +Thumbs.db diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..9e7f561 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,5043 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ab_glyph" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2" +dependencies = [ + "ab_glyph_rasterizer", + "owned_ttf_parser", +] + +[[package]] +name = "ab_glyph_rasterizer" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618" + +[[package]] +name = "accesskit" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74a4b14f3d99c1255dcba8f45621ab1a2e7540a0009652d33989005a4d0bfc6b" +dependencies = [ + "enumn", + "serde", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "serde", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "android-activity" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee91c0c2905bae44f84bfa4e044536541df26b7703fd0888deeb9060fcc44289" +dependencies = [ + "android-properties", + "bitflags 2.13.0", + "cc", + "cesu8", + "jni", + "jni-sys 0.3.1", + "libc", + "log", + "ndk", + "ndk-context", + "ndk-sys", + "num_enum", + "thiserror 1.0.69", +] + +[[package]] +name = "android-properties" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3d036a3c4ab069c7b410a2ce876bd74808d2d0888a82667669f8e783a898bf1" +dependencies = [ + "derive_arbitrary", +] + +[[package]] +name = "arboard" +version = "3.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0348a1c054491f4bfe6ab86a7b6ab1e44e45d899005de92f58b3df180b36ddaf" +dependencies = [ + "clipboard-win", + "log", + "objc2 0.6.4", + "objc2-app-kit", + "objc2-foundation", + "parking_lot", + "percent-encoding", + "windows-sys 0.60.2", + "x11rb", +] + +[[package]] +name = "as-raw-xcb-connection" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175571dd1d178ced59193a6fc02dde1b972eb0bc56c892cde9beeceac5bf0f6b" + +[[package]] +name = "ashpd" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3af990a617932d416e83cf79e7335dd5247dcb0825995ca3274c17dab5b749d" +dependencies = [ + "async-fs", + "async-net", + "enumflags2", + "futures-channel", + "futures-util", + "rand", + "serde", + "serde_repr", + "url", + "zbus", +] + +[[package]] +name = "async-broadcast" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435a87a52755b8f27fcf321ac4f04b2802e337c8c4872923137471ec39c37532" +dependencies = [ + "event-listener", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-channel" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924ed96dd52d1b75e9c1a3e6275715fd320f5f9439fb5a4a11fa51f4221158d2" +dependencies = [ + "concurrent-queue", + "event-listener-strategy", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-executor" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c96bf972d85afc50bf5ab8fe2d54d1586b4e0b46c97c50a0c9e71e2f7bcd812a" +dependencies = [ + "async-task", + "concurrent-queue", + "fastrand", + "futures-lite", + "pin-project-lite", + "slab", +] + +[[package]] +name = "async-fs" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034a681df4aed8b8edbd7fbe472401ecf009251c8b40556b304567052e294c5" +dependencies = [ + "async-lock", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-io" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456b8a8feb6f42d237746d4b3e9a178494627745c3c56c6ea55d92ba50d026fc" +dependencies = [ + "autocfg", + "cfg-if", + "concurrent-queue", + "futures-io", + "futures-lite", + "parking", + "polling", + "rustix 1.1.4", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-lock" +version = "3.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" +dependencies = [ + "event-listener", + "event-listener-strategy", + "pin-project-lite", +] + +[[package]] +name = "async-net" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" +dependencies = [ + "async-io", + "blocking", + "futures-lite", +] + +[[package]] +name = "async-process" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc50921ec0055cdd8a16de48773bfeec5c972598674347252c0399676be7da75" +dependencies = [ + "async-channel", + "async-io", + "async-lock", + "async-signal", + "async-task", + "blocking", + "cfg-if", + "event-listener", + "futures-lite", + "rustix 1.1.4", +] + +[[package]] +name = "async-recursion" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "async-signal" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52b5aaafa020cf5053a01f2a60e8ff5dccf550f0f77ec54a4e47285ac2bab485" +dependencies = [ + "async-io", + "async-lock", + "atomic-waker", + "cfg-if", + "futures-core", + "futures-io", + "rustix 1.1.4", + "signal-hook-registry", + "slab", + "windows-sys 0.61.2", +] + +[[package]] +name = "async-task" +version = "4.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8" +dependencies = [ + "serde_core", +] + +[[package]] +name = "block" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-sys" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae85a0696e7ea3b835a453750bf002770776609115e6d25c6d2ff28a8200f7e7" +dependencies = [ + "objc-sys", +] + +[[package]] +name = "block2" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "15b55663a85f33501257357e6421bb33e769d5c9ffb5ba0921c975a123e35e68" +dependencies = [ + "block-sys", + "objc2 0.4.1", +] + +[[package]] +name = "blocking" +version = "1.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e83f8d02be6967315521be875afa792a316e28d57b5a2d401897e2a7921b7f21" +dependencies = [ + "async-channel", + "async-task", + "futures-io", + "futures-lite", + "piper", +] + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "bzip2" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49ecfb22d906f800d4fe833b6282cf4dc1c298f5057ca0b5445e5c209735ca47" +dependencies = [ + "bzip2-sys", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.13+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225bff33b2141874fe80d71e07d6eec4f85c5c216453dd96388240f96e1acc14" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "calloop" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fba7adb4dd5aa98e5553510223000e7148f621165ec5f9acd7113f6ca4995298" +dependencies = [ + "bitflags 2.13.0", + "log", + "polling", + "rustix 0.38.44", + "slab", + "thiserror 1.0.69", +] + +[[package]] +name = "calloop" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dbf9978365bac10f54d1d4b04f7ce4427e51f71d61f2fe15e3fed5166474df7" +dependencies = [ + "bitflags 2.13.0", + "polling", + "rustix 1.1.4", + "slab", + "tracing", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f0ea9b9476c7fad82841a8dbb380e2eae480c21910feba80725b46931ed8f02" +dependencies = [ + "calloop 0.12.4", + "rustix 0.38.44", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "calloop-wayland-source" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "138efcf0940a02ebf0cc8d1eff41a1682a46b431630f4c52450d6265876021fa" +dependencies = [ + "calloop 0.14.4", + "rustix 1.1.4", + "wayland-backend", + "wayland-client", +] + +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd16c4719339c4530435d38e511904438d07cce7950afa3718a84ac36c10e89e" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "cgl" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ced0551234e87afee12411d535648dd89d2e7f34c78b753395567aff3d447ff" +dependencies = [ + "libc", +] + +[[package]] +name = "chardetng" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea" +dependencies = [ + "cfg-if", + "encoding_rs", + "memchr", +] + +[[package]] +name = "chrono" +version = "0.4.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + +[[package]] +name = "clipboard-win" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4" +dependencies = [ + "error-code", +] + +[[package]] +name = "cocoa" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c" +dependencies = [ + "bitflags 1.3.2", + "block", + "cocoa-foundation", + "core-foundation", + "core-graphics", + "foreign-types", + "libc", + "objc", +] + +[[package]] +name = "cocoa-foundation" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7" +dependencies = [ + "bitflags 1.3.2", + "block", + "core-foundation", + "core-graphics-types", + "libc", + "objc", +] + +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "constant_time_eq" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "core-graphics" +version = "0.23.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "core-graphics-types", + "foreign-types", + "libc", +] + +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "libc", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "cursor-icon" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f27ae1dd37df86211c42e150270f82743308803d90a6f6e6651cd730d5e1732f" + +[[package]] +name = "deflate64" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "derive_arbitrary" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e567bd82dcff979e4b03460c307b3cdc9e96fde3d73bed1496d2bc75d9dd62a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "dispatch" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd0c93bb4b0c6d9b77f4435b0ae98c24d17f1c45b2ff844c6151a07256ca923b" + +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", +] + +[[package]] +name = "displaydoc" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dlib" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a" +dependencies = [ + "libloading", +] + +[[package]] +name = "document-features" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" +dependencies = [ + "litrs", +] + +[[package]] +name = "docx-rs" +version = "0.4.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed73cbf5e1c37baa23f4132569ac1187829f03922c206bd68fe109e3001a343d" +dependencies = [ + "base64 0.22.1", + "image 0.25.10", + "quick-xml 0.36.2", + "serde", + "serde_json", + "thiserror 2.0.18", + "zip 0.6.6", +] + +[[package]] +name = "downcast-rs" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2" + +[[package]] +name = "ecolor" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20930a432bbd57a6d55e07976089708d4893f3d556cf42a0d79e9e321fa73b10" +dependencies = [ + "bytemuck", + "serde", +] + +[[package]] +name = "eframe" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "020e2ccef6bbcec71dbc542f7eed64a5846fc3076727f5746da8fd307c91bab2" +dependencies = [ + "bytemuck", + "cocoa", + "directories-next", + "document-features", + "egui", + "egui-winit", + "egui_glow", + "glow", + "glutin", + "glutin-winit", + "image 0.24.9", + "js-sys", + "log", + "objc", + "parking_lot", + "percent-encoding", + "raw-window-handle 0.5.2", + "raw-window-handle 0.6.2", + "ron", + "serde", + "static_assertions", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "web-time", + "winapi", + "winit", +] + +[[package]] +name = "egui" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "584c5d1bf9a67b25778a3323af222dbe1a1feb532190e103901187f92c7fe29a" +dependencies = [ + "accesskit", + "ahash", + "epaint", + "log", + "nohash-hasher", + "ron", + "serde", +] + +[[package]] +name = "egui-winit" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e3da0cbe020f341450c599b35b92de4af7b00abde85624fd16f09c885573609" +dependencies = [ + "arboard", + "egui", + "log", + "raw-window-handle 0.6.2", + "serde", + "smithay-clipboard", + "web-time", + "webbrowser", + "winit", +] + +[[package]] +name = "egui_glow" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0e5d975f3c86edc3d35b1db88bb27c15dde7c55d3b5af164968ab5ede3f44ca" +dependencies = [ + "bytemuck", + "egui", + "glow", + "log", + "memoffset", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "either" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" + +[[package]] +name = "emath" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4c3a552cfca14630702449d35f41c84a0d15963273771c6059175a803620f3f" +dependencies = [ + "bytemuck", + "serde", +] + +[[package]] +name = "embed-resource" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d506610004cfc74a6f5ee7e8c632b355de5eca1f03ee5e5e0ec11b77d4eb3d61" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml", + "vswhom", + "winreg", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "endi" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66b7e2430c6dff6a955451e2cfc438f09cea1965a9d6f87f7e3b90decc014099" + +[[package]] +name = "enumflags2" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1027f7680c853e056ebcec683615fb6fbbc07dbaa13b4d5d9442b146ded4ecef" +dependencies = [ + "enumflags2_derive", + "serde", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "enumn" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f9ed6b3789237c8a0c1c505af1c7eb2c560df6186f01b098c3a1064ea532f38" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "epaint" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b381f8b149657a4acf837095351839f32cd5c4aec1817fc4df84e18d76334176" +dependencies = [ + "ab_glyph", + "ahash", + "bytemuck", + "ecolor", + "emath", + "log", + "nohash-hasher", + "parking_lot", + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "error-code" +version = "3.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59" + +[[package]] +name = "event-listener" +version = "5.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" +dependencies = [ + "concurrent-queue", + "parking", + "pin-project-lite", +] + +[[package]] +name = "event-listener-strategy" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" +dependencies = [ + "event-listener", + "pin-project-lite", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fax" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "caf1079563223d5d59d83c85886a56e586cfd5c1a26292e971a0fa266531ac5a" + +[[package]] +name = "fdeflate" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c" +dependencies = [ + "simd-adler32", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foreign-types" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965" +dependencies = [ + "foreign-types-macros", + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-macros" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "foreign-types-shared" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-lite" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f78e10609fe0e0b3f4157ffab1876319b5b0db102a2c60dc4626306dc46b44ad" +dependencies = [ + "fastrand", + "futures-core", + "futures-io", + "parking", + "pin-project-lite", +] + +[[package]] +name = "futures-macro" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.4", + "windows-link", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gif" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159" +dependencies = [ + "color_quant", + "weezl", +] + +[[package]] +name = "gl_generator" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a95dfc23a2b4a9a2f5ab41d194f8bfda3cabec42af4e39f08c339eb2a0c124d" +dependencies = [ + "khronos_api", + "log", + "xml-rs", +] + +[[package]] +name = "glow" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd348e04c43b32574f2de31c8bb397d96c9fcfa1371bd4ca6d8bdc464ab121b1" +dependencies = [ + "js-sys", + "slotmap", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "glutin" +version = "0.31.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fcd4ae4e86d991ad1300b8f57166e5be0c95ef1f63f3f5b827f8a164548746" +dependencies = [ + "bitflags 2.13.0", + "cfg_aliases 0.1.1", + "cgl", + "core-foundation", + "dispatch", + "glutin_egl_sys", + "glutin_glx_sys", + "glutin_wgl_sys", + "icrate", + "libloading", + "objc2 0.4.1", + "once_cell", + "raw-window-handle 0.5.2", + "wayland-sys", + "windows-sys 0.48.0", + "x11-dl", +] + +[[package]] +name = "glutin-winit" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebcdfba24f73b8412c5181e56f092b5eff16671c514ce896b258a0a64bd7735" +dependencies = [ + "cfg_aliases 0.1.1", + "glutin", + "raw-window-handle 0.5.2", + "winit", +] + +[[package]] +name = "glutin_egl_sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77cc5623f5309ef433c3dd4ca1223195347fe62c413da8e2fdd0eb76db2d9bcd" +dependencies = [ + "gl_generator", + "windows-sys 0.48.0", +] + +[[package]] +name = "glutin_glx_sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a165fd686c10dcc2d45380b35796e577eacfd43d4660ee741ec8ebe2201b3b4f" +dependencies = [ + "gl_generator", + "x11-dl", +] + +[[package]] +name = "glutin_wgl_sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c8098adac955faa2d31079b65dc48841251f69efd3ac25477903fc424362ead" +dependencies = [ + "gl_generator", +] + +[[package]] +name = "half" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b" +dependencies = [ + "cfg-if", + "crunchy", + "zerocopy", +] + +[[package]] +name = "handlebars" +version = "5.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08485b96a0e6393e9e4d1b8d48cf74ad6c063cd905eb33f42c1ce3f0377539b" +dependencies = [ + "log", + "pest", + "pest_derive", + "serde", + "serde_json", + "thiserror 1.0.69", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "http" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "hyper" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icrate" +version = "0.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d3aaff8a54577104bafdf686ff18565c3b6903ca5782a2026ef06e2c7aa319" +dependencies = [ + "block2", + "dispatch", + "objc2 0.4.1", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "image" +version = "0.24.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5690139d2f55868e080017335e4b94cb7414274c74f1669c84fb5feba2c9f69d" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "num-traits", + "png 0.17.16", +] + +[[package]] +name = "image" +version = "0.25.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104" +dependencies = [ + "bytemuck", + "byteorder-lite", + "color_quant", + "gif", + "moxcms", + "num-traits", + "png 0.18.1", + "tiff", + "zune-core", + "zune-jpeg", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + +[[package]] +name = "ipnet" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys 0.3.1", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +dependencies = [ + "jni-sys 0.4.1", +] + +[[package]] +name = "jni-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2" +dependencies = [ + "jni-sys-macros", +] + +[[package]] +name = "jni-sys-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "khronos_api" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libloading" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55" +dependencies = [ + "cfg-if", + "windows-link", +] + +[[package]] +name = "libredox" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3" +dependencies = [ + "bitflags 2.13.0", + "libc", + "plain", + "redox_syscall 0.8.1", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "litrs" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "lzma-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297e814c836ae64db86b36cf2a557ba54368d03f6afcd7d947c266692f71115e" +dependencies = [ + "byteorder", + "crc", +] + +[[package]] +name = "lzma-sys" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" + +[[package]] +name = "memmap2" +version = "0.9.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "moxcms" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b" +dependencies = [ + "num-traits", + "pxfm", +] + +[[package]] +name = "ndk" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2076a31b7010b17a38c01907c45b945e8f11495ee4dd588309718901b1f7a5b7" +dependencies = [ + "bitflags 2.13.0", + "jni-sys 0.3.1", + "log", + "ndk-sys", + "num_enum", + "raw-window-handle 0.5.2", + "raw-window-handle 0.6.2", + "thiserror 1.0.69", +] + +[[package]] +name = "ndk-context" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27b02d87554356db9e9a873add8782d4ea6e3e58ea071a9adb9a2e8ddb884a8b" + +[[package]] +name = "ndk-sys" +version = "0.5.0+25.2.9519653" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c196769dd60fd4f363e11d948139556a344e79d451aeb2fa2fd040738ef7691" +dependencies = [ + "jni-sys 0.3.1", +] + +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags 2.13.0", + "cfg-if", + "cfg_aliases 0.2.1", + "libc", + "memoffset", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + +[[package]] +name = "objc-foundation" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1add1b659e36c9607c7aab864a76c7a4c2760cd0cd2e120f3fb8b952c7e22bf9" +dependencies = [ + "block", + "objc", + "objc_id", +] + +[[package]] +name = "objc-sys" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310" + +[[package]] +name = "objc2" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "559c5a40fdd30eb5e344fbceacf7595a81e242529fb4e21cf5f43fb4f11ff98d" +dependencies = [ + "objc-sys", + "objc2-encode 3.0.0", +] + +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode 4.1.0", +] + +[[package]] +name = "objc2-app-kit" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-core-graphics", + "objc2-foundation", +] + +[[package]] +name = "objc2-core-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2 0.6.4", +] + +[[package]] +name = "objc2-core-graphics" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e022c9d066895efa1345f8e33e584b9f958da2fd4cd116792e15e07e4720a807" +dependencies = [ + "bitflags 2.13.0", + "dispatch2", + "objc2 0.6.4", + "objc2-core-foundation", + "objc2-io-surface", +] + +[[package]] +name = "objc2-encode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d079845b37af429bfe5dfa76e6d087d788031045b25cfc6fd898486fd9847666" + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + +[[package]] +name = "objc2-foundation" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc2-io-surface" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180788110936d59bab6bd83b6060ffdfffb3b922ba1396b312ae795e1de9d81d" +dependencies = [ + "bitflags 2.13.0", + "objc2 0.6.4", + "objc2-core-foundation", +] + +[[package]] +name = "objc_id" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92d4ddb4bd7b50d730c215ff871754d0da6b2178849f8a2a2ab69712d0c073b" +dependencies = [ + "objc", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "orbclient" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5df339f526ea9a60e371768d50efc2f2508c7203290731565d1f7a6f71d21747" +dependencies = [ + "libc", + "libredox", +] + +[[package]] +name = "ordered-stream" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa2b01e1d916879f73a53d01d1d6cee68adbb31d6d9177a8cfce093cced1d50" +dependencies = [ + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "owned_ttf_parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b" +dependencies = [ + "ttf-parser", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "pest_meta" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "piper" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c835479a4443ded371d6c535cbfd8d31ad92c5d23ae9770a61bc155e4992a3c1" +dependencies = [ + "atomic-waker", + "fastrand", + "futures-io", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "plain" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" + +[[package]] +name = "png" +version = "0.17.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "png" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" +dependencies = [ + "bitflags 2.13.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + +[[package]] +name = "polling" +version = "3.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0e4f59085d47d8241c88ead0f274e8a0cb551f3625263c05eb8dd897c34218" +dependencies = [ + "cfg-if", + "concurrent-queue", + "hermit-abi", + "pin-project-lite", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "pollster" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2" + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit 0.25.12+spec-1.1.0", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pxfm" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0c5ccf5294c6ccd63a74f1565028353830a9c2f5eb0c682c355c471726a6e3f" + +[[package]] +name = "quick-error" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" + +[[package]] +name = "quick-xml" +version = "0.36.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" +dependencies = [ + "encoding_rs", + "memchr", +] + +[[package]] +name = "quick-xml" +version = "0.39.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdcc8dd4e2f670d309a5f0e83fe36dfdc05af317008fea29144da1a2ac858e5e" +dependencies = [ + "memchr", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + +[[package]] +name = "raw-window-handle" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_syscall" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b44b894f2a6e36457d665d1e08c3866add6ed5e70050c1b4ba8a8ddedb02ce7" +dependencies = [ + "bitflags 2.13.0", +] + +[[package]] +name = "redox_users" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43" +dependencies = [ + "getrandom 0.2.17", + "libredox", + "thiserror 1.0.69", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reqwest" +version = "0.12.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" +dependencies = [ + "base64 0.22.1", + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "js-sys", + "log", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-http", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "rfd" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a73a7337fc24366edfca76ec521f51877b114e42dab584008209cca6719251" +dependencies = [ + "ashpd", + "block", + "dispatch", + "js-sys", + "log", + "objc", + "objc-foundation", + "objc_id", + "pollster", + "raw-window-handle 0.6.2", + "urlencoding", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-sys 0.48.0", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.13.0", + "serde", + "serde_derive", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags 2.13.0", + "errno", + "libc", + "linux-raw-sys 0.12.1", + "windows-sys 0.61.2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "ryu" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "secret-file-selfcheck" +version = "0.1.0" +dependencies = [ + "aho-corasick", + "anyhow", + "base64 0.22.1", + "chardetng", + "chrono", + "dirs", + "docx-rs", + "eframe", + "egui", + "embed-resource", + "encoding_rs", + "handlebars", + "image 0.25.10", + "log", + "once_cell", + "quick-xml 0.36.2", + "rand", + "regex", + "reqwest", + "rfd", + "serde", + "serde_json", + "sha2", + "sys-locale", + "sysinfo", + "thiserror 1.0.69", + "tokio", + "toml", + "tracing", + "tracing-appender", + "tracing-subscriber", + "url", + "walkdir", + "windows-sys 0.59.0", + "zip 2.4.2", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.150" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sha1" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + +[[package]] +name = "signal-hook-registry" +version = "1.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" +dependencies = [ + "errno", + "libc", +] + +[[package]] +name = "simd-adler32" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "slotmap" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdd58c3c93c3d278ca835519292445cb4b0d4dc59ccfdf7ceadaab3f8aeb4038" +dependencies = [ + "version_check", +] + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smithay-client-toolkit" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "922fd3eeab3bd820d76537ce8f582b1cf951eceb5475c28500c7457d9d17f53a" +dependencies = [ + "bitflags 2.13.0", + "calloop 0.12.4", + "calloop-wayland-source 0.2.0", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 0.38.44", + "thiserror 1.0.69", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols 0.31.2", + "wayland-protocols-wlr 0.2.0", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-client-toolkit" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0512da38f5e2b31201a93524adb8d3136276fa4fe4aafab4e1f727a82b534cc0" +dependencies = [ + "bitflags 2.13.0", + "calloop 0.14.4", + "calloop-wayland-source 0.4.1", + "cursor-icon", + "libc", + "log", + "memmap2", + "rustix 1.1.4", + "thiserror 2.0.18", + "wayland-backend", + "wayland-client", + "wayland-csd-frame", + "wayland-cursor", + "wayland-protocols 0.32.12", + "wayland-protocols-experimental", + "wayland-protocols-misc", + "wayland-protocols-wlr 0.3.12", + "wayland-scanner", + "xkeysym", +] + +[[package]] +name = "smithay-clipboard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71704c03f739f7745053bde45fa203a46c58d25bc5c4efba1d9a60e9dba81226" +dependencies = [ + "libc", + "smithay-client-toolkit 0.20.0", + "wayland-backend", +] + +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + +[[package]] +name = "socket2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "symlink" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + +[[package]] +name = "sysinfo" +version = "0.31.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "355dbe4f8799b304b05e1b0f05fc59b2a18d36645cf169607da45bde2f69a1be" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows", +] + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix 1.1.4", + "windows-sys 0.61.2", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tiff" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b63feaf3343d35b6ca4d50483f94843803b0f51634937cc2ec519fc32232bc52" +dependencies = [ + "fax", + "flate2", + "half", + "quick-error", + "weezl", + "zune-jpeg", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tokio" +version = "1.52.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" +dependencies = [ + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_edit 0.22.27", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime 0.6.11", + "toml_write", + "winnow 0.7.15", +] + +[[package]] +name = "toml_edit" +version = "0.25.12+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7" +dependencies = [ + "indexmap", + "toml_datetime 1.1.1+spec-1.1.0", + "toml_parser", + "winnow 1.0.3", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.3", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.13.0", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-appender" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c" +dependencies = [ + "crossbeam-channel", + "symlink", + "thiserror 2.0.18", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "ttf-parser" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" + +[[package]] +name = "typenum" +version = "1.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uds_windows" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" +dependencies = [ + "memoffset", + "tempfile", + "windows-sys 0.61.2", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", + "serde_derive", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.72" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.13.0", + "hashbrown 0.15.5", + "indexmap", + "semver", +] + +[[package]] +name = "wayland-backend" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2857dd20b54e916ec7253b3d6b4d5c4d7d4ca2c33c2e11c6c76a99bd8744755d" +dependencies = [ + "cc", + "downcast-rs", + "rustix 1.1.4", + "scoped-tls", + "smallvec", + "wayland-sys", +] + +[[package]] +name = "wayland-client" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c7c96bb74690c3189b5c9cb4ca1627062bb23693a4fad9d8c3de958260144" +dependencies = [ + "bitflags 2.13.0", + "rustix 1.1.4", + "wayland-backend", + "wayland-scanner", +] + +[[package]] +name = "wayland-csd-frame" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625c5029dbd43d25e6aa9615e88b829a5cad13b2819c4ae129fdbb7c31ab4c7e" +dependencies = [ + "bitflags 2.13.0", + "cursor-icon", + "wayland-backend", +] + +[[package]] +name = "wayland-cursor" +version = "0.31.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a52d18780be9b1314328a3de5f930b73d2200112e3849ca6cb11822793fb34d" +dependencies = [ + "rustix 1.1.4", + "wayland-client", + "xcursor", +] + +[[package]] +name = "wayland-protocols" +version = "0.31.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols" +version = "0.32.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "563a85523cade2429938e790815fd7319062103b9f4a2dc806e9b53b95982d8f" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-experimental" +version = "20250721.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a1f863128dcaaec790d7b4b396cc9b9a7a079e878e18c47e6c2d2c5a8dcbb1" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.12", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-misc" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9567599ef23e09b8dad6e429e5738d4509dfc46b3b21f32841a304d16b29c8" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.12", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-plasma" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.31.2", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad1f61b76b6c2d8742e10f9ba5c3737f6530b4c243132c2a2ccc8aa96fe25cd6" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.31.2", + "wayland-scanner", +] + +[[package]] +name = "wayland-protocols-wlr" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb04e52f7836d7c7976c78ca0250d61e33873c34156a2a1fc9474828ec268234" +dependencies = [ + "bitflags 2.13.0", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.32.12", + "wayland-scanner", +] + +[[package]] +name = "wayland-scanner" +version = "0.31.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c324a910fd86ebdc364a3e61ec1f11737d3b1d6c273c0239ee8ff4bc0d24b4a" +dependencies = [ + "proc-macro2", + "quick-xml 0.39.4", + "quote", +] + +[[package]] +name = "wayland-sys" +version = "0.31.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8eab23fefc9e41f8e841df4a9c707e8a8c4ed26e944ef69297184de2785e3be" +dependencies = [ + "dlib", + "log", + "once_cell", + "pkg-config", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa30049b1c872b72c89866d458eae9f20380ab280ffd1b1e18df2d3e2d98cfe0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webbrowser" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db67ae75a9405634f5882791678772c94ff5f16a66535aae186e26aa0841fc8b" +dependencies = [ + "core-foundation", + "home", + "jni", + "log", + "ndk-context", + "objc", + "raw-window-handle 0.5.2", + "url", + "web-sys", +] + +[[package]] +name = "weezl" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement 0.57.0", + "windows-interface 0.57.0", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement 0.60.2", + "windows-interface 0.59.3", + "windows-link", + "windows-result 0.4.1", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winit" +version = "0.29.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d59ad965a635657faf09c8f062badd885748428933dad8e8bdd64064d92e5ca" +dependencies = [ + "ahash", + "android-activity", + "atomic-waker", + "bitflags 2.13.0", + "bytemuck", + "calloop 0.12.4", + "cfg_aliases 0.1.1", + "core-foundation", + "core-graphics", + "cursor-icon", + "icrate", + "js-sys", + "libc", + "log", + "memmap2", + "ndk", + "ndk-sys", + "objc2 0.4.1", + "once_cell", + "orbclient", + "percent-encoding", + "raw-window-handle 0.5.2", + "raw-window-handle 0.6.2", + "redox_syscall 0.3.5", + "rustix 0.38.44", + "smithay-client-toolkit 0.18.1", + "smol_str", + "unicode-segmentation", + "wasm-bindgen", + "wasm-bindgen-futures", + "wayland-backend", + "wayland-client", + "wayland-protocols 0.31.2", + "wayland-protocols-plasma", + "web-sys", + "web-time", + "windows-sys 0.48.0", + "x11-dl", + "x11rb", + "xkbcommon-dl", +] + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap", + "prettyplease", + "syn", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.13.0", + "indexmap", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "x11-dl" +version = "2.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38735924fedd5314a6e548792904ed8c6de6636285cb9fec04d5b1db85c1516f" +dependencies = [ + "libc", + "once_cell", + "pkg-config", +] + +[[package]] +name = "x11rb" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9993aa5be5a26815fe2c3eacfc1fde061fc1a1f094bf1ad2a18bf9c495dd7414" +dependencies = [ + "as-raw-xcb-connection", + "gethostname", + "libc", + "libloading", + "once_cell", + "rustix 1.1.4", + "x11rb-protocol", +] + +[[package]] +name = "x11rb-protocol" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea6fc2961e4ef194dcbfe56bb845534d0dc8098940c7e5c012a258bfec6701bd" + +[[package]] +name = "xcursor" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bec9e4a500ca8864c5b47b8b482a73d62e4237670e5b5f1d6b9e3cae50f28f2b" + +[[package]] +name = "xdg-home" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec1cdab258fb55c0da61328dc52c8764709b249011b2cad0454c72f0bf10a1f6" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "xkbcommon-dl" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039de8032a9a8856a6be89cea3e5d12fdd82306ab7c94d74e6deab2460651c5" +dependencies = [ + "bitflags 2.13.0", + "dlib", + "log", + "once_cell", + "xkeysym", +] + +[[package]] +name = "xkeysym" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9cc00251562a284751c9973bace760d86c0276c471b4be569fe6b068ee97a56" + +[[package]] +name = "xml-rs" +version = "0.8.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" + +[[package]] +name = "xz2" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" +dependencies = [ + "lzma-sys", +] + +[[package]] +name = "yoke" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zbus" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb97012beadd29e654708a0fdb4c84bc046f537aecfde2c3ee0a9e4b4d48c725" +dependencies = [ + "async-broadcast", + "async-executor", + "async-fs", + "async-io", + "async-lock", + "async-process", + "async-recursion", + "async-task", + "async-trait", + "blocking", + "enumflags2", + "event-listener", + "futures-core", + "futures-sink", + "futures-util", + "hex", + "nix", + "ordered-stream", + "rand", + "serde", + "serde_repr", + "sha1", + "static_assertions", + "tracing", + "uds_windows", + "windows-sys 0.52.0", + "xdg-home", + "zbus_macros", + "zbus_names", + "zvariant", +] + +[[package]] +name = "zbus_macros" +version = "4.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zbus_names" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b9b1fef7d021261cc16cba64c351d291b715febe0fa10dc3a443ac5a5022e6c" +dependencies = [ + "serde", + "static_assertions", + "zvariant", +] + +[[package]] +name = "zerocopy" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerofrom" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "byteorder", + "crc32fast", + "crossbeam-utils", + "flate2", +] + +[[package]] +name = "zip" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabe6324e908f85a1c52063ce7aa26b68dcb7eb6dbc83a2d148403c9bc3eba50" +dependencies = [ + "aes", + "arbitrary", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "deflate64", + "displaydoc", + "flate2", + "getrandom 0.3.4", + "hmac", + "indexmap", + "lzma-rs", + "memchr", + "pbkdf2", + "sha1", + "thiserror 2.0.18", + "time", + "xz2", + "zeroize", + "zopfli", + "zstd", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zopfli" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249" +dependencies = [ + "bumpalo", + "crc32fast", + "log", + "simd-adler32", +] + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + +[[package]] +name = "zune-core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9" + +[[package]] +name = "zune-jpeg" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296" +dependencies = [ + "zune-core", +] + +[[package]] +name = "zvariant" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2084290ab9a1c471c38fc524945837734fbf124487e105daec2bb57fd48c81fe" +dependencies = [ + "endi", + "enumflags2", + "serde", + "static_assertions", + "url", + "zvariant_derive", +] + +[[package]] +name = "zvariant_derive" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", + "zvariant_utils", +] + +[[package]] +name = "zvariant_utils" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..0eeff2e --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,66 @@ +[package] +name = "secret-file-selfcheck" +version = "0.1.0" +edition = "2021" +description = "磁盘文档涉密抽检工具(egui + Umi-OCR)" + +[dependencies] +eframe = { version = "0.27", default-features = false, features = ["default_fonts", "glow", "persistence"] } +egui = "0.27" +tokio = { version = "1", features = ["rt-multi-thread", "macros", "sync", "time", "fs"] } +serde = { version = "1", features = ["derive"] } +serde_json = "1" +toml = "0.8" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter"] } +tracing-appender = "0.2" +docx-rs = "0.4" +encoding_rs = "0.8" +chardetng = "0.1" +reqwest = { version = "0.12", default-features = false, features = ["json", "blocking"] } +base64 = "0.22" +sysinfo = "0.31" +windows-sys = { version = "0.59", features = [ + "Win32_UI_WindowsAndMessaging", + "Win32_UI_Input_KeyboardAndMouse", + "Win32_System_Threading", + "Win32_System_Console", + "Win32_System_Memory", + "Win32_System_LibraryLoader", + "Win32_System_DataExchange", + "Win32_Graphics_Gdi", + "Win32_Storage_FileSystem", + "Win32_Storage_Xps", + "Win32_UI_Shell", + "Win32_System_Registry", + "Win32_Security", + "Win32_Foundation", +] } +sys-locale = "0.3" +dirs = "5" +walkdir = "2" +regex = "1" +aho-corasick = "1" +sha2 = "0.10" +anyhow = "1" +thiserror = "1" +chrono = { version = "0.4", features = ["serde"] } +once_cell = "1" +log = "0.4" +image = { version = "0.25", default-features = false, features = ["png", "bmp"] } +handlebars = "5" +url = "2" +rand = "0.8" +rfd = "0.14" +zip = "2" +quick-xml = "0.36" + +[build-dependencies] +embed-resource = "2" + +[profile.release] +opt-level = 3 +lto = "thin" +codegen-units = 1 +strip = true +panic = "abort" diff --git a/UmiOCR-data/.pre_settings b/UmiOCR-data/.pre_settings index f28b48d..1982c2f 100644 --- a/UmiOCR-data/.pre_settings +++ b/UmiOCR-data/.pre_settings @@ -1,7 +1,7 @@ -{ - "i18n": "zh_CN", - "opengl": "AA_UseOpenGLES", - "server_port": 1224, - "last_pid": 27276, - "last_ptime": "1759219775.7634187" +{ + "i18n": "zh_CN", + "opengl": "AA_UseOpenGLES", + "server_port": 1224, + "last_pid": 29620, + "last_ptime": "1780969865.5003738" } \ No newline at end of file diff --git a/UmiOCR-data/.settings b/UmiOCR-data/.settings index 41b0863..4fc6e89 100644 --- a/UmiOCR-data/.settings +++ b/UmiOCR-data/.settings @@ -1,53 +1,53 @@ -[BatchOCR] -configs_advanced=true -ocr.angle=false -ocr.language=\x7b80\x4f53\x4e2d\x6587 -ocr.maxSideLen=1024 -tbpu.parser=multi_line -mission.recurrence=false -mission.dirType=source -mission.dir= -mission.fileNameFormat=[OCR]_%name_%date -mission.datetimeFormat=%Y%m%d_%H%M -mission.filesType.txt=true -mission.filesType.txtPlain=false -mission.filesType.txtIndividual=false -mission.filesType.md=false -mission.filesType.csv=false -mission.filesType.jsonl=false -mission.ignoreBlank=true -other.simpleNotificationType=default - -[Global] -configs_advanced=false -shortcut.desktop=false -shortcut.startMenu=false -shortcut.startup=false -ui.theme=Default Light -ui.fontFamily=Microsoft YaHei -ui.dataFontFamily=Microsoft YaHei -ui.scale=1 -ui.disableEffect=false -ui.imgShowOverlay=true -window.startupInvisible=false -window.isMainWindowTop=false -window.barIsLock=false -window.closeWin2Hide=true -window.hideTrayIcon=false -window.simpleNotificationType=inside -window.geometry="1320,450,800,500" -window.messageMemory=@Variant(\0\0\0\x7f\0\0\0\tQJSValue\0\0\0\0\0\0\0\0\t\0\0\0\0) -window.doubleLayout=@Variant(\0\0\0\x7f\0\0\0\tQJSValue\0\0\0\0\0\0\0\0\b\0\0\0\0) -screenshot.hideWindow=true -screenshot.hideWindowTime=0.2 -server.enable=true -server.host=127.0.0.1 -server.port=1224 -logs.saveLogLevel=ERROR -ocr.api=win7_x64_RapidOCR-json -ocr.win7_x64_RapidOCR-json.numThread=6 - -[TabPageManager] -openPageList=@Variant(\0\0\0\t\0\0\0\x1\0\0\0\n\0\0\0*\0\x42\0\x61\0t\0\x63\0h\0O\0\x43\0R\0/\0\x42\0\x61\0t\0\x63\0h\0O\0\x43\0R\0.\0q\0m\0l) -showPageIndex=0 -refresh=true +[BatchOCR] +configs_advanced=true +ocr.angle=false +ocr.language=\x7b80\x4f53\x4e2d\x6587 +ocr.maxSideLen=1024 +tbpu.parser=multi_line +mission.recurrence=false +mission.dirType=source +mission.dir= +mission.fileNameFormat=[OCR]_%name_%date +mission.datetimeFormat=%Y%m%d_%H%M +mission.filesType.txt=true +mission.filesType.txtPlain=false +mission.filesType.txtIndividual=false +mission.filesType.md=false +mission.filesType.csv=false +mission.filesType.jsonl=false +mission.ignoreBlank=true +other.simpleNotificationType=default + +[Global] +configs_advanced=false +shortcut.desktop=false +shortcut.startMenu=false +shortcut.startup=false +ui.theme=Default Light +ui.fontFamily=Microsoft YaHei +ui.dataFontFamily=Microsoft YaHei +ui.scale=1 +ui.disableEffect=false +ui.imgShowOverlay=true +window.startupInvisible=false +window.isMainWindowTop=false +window.barIsLock=false +window.closeWin2Hide=true +window.hideTrayIcon=false +window.simpleNotificationType=inside +window.geometry="0,23,3440,1377" +window.messageMemory=@Variant(\0\0\0\x7f\0\0\0\tQJSValue\0\0\0\0\0\0\0\0\t\0\0\0\0) +window.doubleLayout=@Variant(\0\0\0\x7f\0\0\0\tQJSValue\0\0\0\0\0\0\0\0\b\0\0\0\0) +screenshot.hideWindow=true +screenshot.hideWindowTime=0.2 +server.enable=true +server.host=127.0.0.1 +server.port=1224 +logs.saveLogLevel=ERROR +ocr.api=win7_x64_RapidOCR-json +ocr.win7_x64_RapidOCR-json.numThread=6 + +[TabPageManager] +openPageList=@Variant(\0\0\0\t\0\0\0\x1\0\0\0\n\0\0\0*\0\x42\0\x61\0t\0\x63\0h\0O\0\x43\0R\0/\0\x42\0\x61\0t\0\x63\0h\0O\0\x43\0R\0.\0q\0m\0l) +showPageIndex=0 +refresh=true diff --git a/build.rs b/build.rs new file mode 100644 index 0000000..e719254 --- /dev/null +++ b/build.rs @@ -0,0 +1,5 @@ +fn main() { + // 隐藏控制台(与 src/main.rs 顶部的 #![windows_subsystem = "windows"] 配合) + // MinGW ld 语法:用 -Wl,--subsystem,windows + println!("cargo:rustc-link-arg=-Wl,--subsystem,windows"); +} diff --git a/src/app.rs b/src/app.rs new file mode 100644 index 0000000..4c411a0 --- /dev/null +++ b/src/app.rs @@ -0,0 +1,216 @@ +// 顶层 eframe App:承载主面板 + 设置页 + 报告页 +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize}; + +use eframe::egui; + +use crate::config::AppConfig; +use crate::inspect::sampler::SampleItem; +use crate::report::model::Report; +use crate::ui::{home, material, settings}; + +/// 运行状态 +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum RunState { + Idle, + Scanning, + Sampling, + Inspecting, + Reporting, + Done, + Cancelled, + Error(String), +} + +/// App 状态 +pub struct App { + pub config: AppConfig, + pub state: RunState, + /// 抽检进度(0..total) + pub progress: Arc, + pub total: Arc, + pub hit_count: Arc, + pub cancel_flag: Arc, + /// 扫描阶段进度 + pub scan_scanned: Arc, // 已访问文件数 + pub scan_found: Arc, // 候选累计 + pub scan_current_dir: Arc>, // 当前目录 + /// 当前正在处理的文件 + pub current_file: Arc>>, + pub current_step: Arc>, + pub elapsed_ms: Arc, + /// 每类文件抽检计数(按 FileKind 累计 done / total) + pub type_done: Arc>>, + pub type_total: Arc>>, + pub log_lines: Arc>>, + pub samples: Arc>>, + pub report: Arc>>, + pub show_settings: bool, + pub show_report: bool, + /// 后台任务运行状态(由 home.rs 在 start_inspection 时设置) + pub task_state: Option>>, +} + +impl App { + pub fn new(cc: &eframe::CreationContext<'_>) -> Self { + // 加载配置 + let config = AppConfig::load().unwrap_or_default(); + // 应用中文字体 + material::install_fonts(&cc.egui_ctx, &config.ui.font_path); + // 应用 Material 主题(theme 字段位于 general 分组下) + material::apply_theme(&cc.egui_ctx, config.general.theme); + + Self { + config, + state: RunState::Idle, + progress: Arc::new(AtomicUsize::new(0)), + total: Arc::new(AtomicUsize::new(0)), + hit_count: Arc::new(AtomicUsize::new(0)), + cancel_flag: Arc::new(AtomicBool::new(false)), + scan_scanned: Arc::new(AtomicUsize::new(0)), + scan_found: Arc::new(AtomicUsize::new(0)), + scan_current_dir: Arc::new(std::sync::Mutex::new(String::new())), + current_file: Arc::new(std::sync::Mutex::new(None)), + current_step: Arc::new(std::sync::Mutex::new(String::new())), + elapsed_ms: Arc::new(AtomicU64::new(0)), + type_done: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())), + type_total: Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())), + log_lines: Arc::new(std::sync::Mutex::new(Vec::new())), + samples: Arc::new(std::sync::Mutex::new(Vec::new())), + report: Arc::new(std::sync::Mutex::new(None)), + show_settings: false, + show_report: false, + task_state: None, + } + } + + /// 写入一条日志(同时同步到 UI 共享缓冲) + pub fn task_log(&self, s: &str) { + tracing::info!("{}", s); + if let Ok(mut g) = self.log_lines.lock() { + g.push(s.to_string()); + } + } +} + +impl eframe::App for App { + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { + // 轮询后台任务状态 + if let Some(ts) = &self.task_state { + if let Ok(g) = ts.lock() { + if *g != self.state { + self.state = g.clone(); + } + } + // 任务结束则清空 task_state + if matches!(self.state, RunState::Done | RunState::Cancelled) { + self.task_state = None; + } + } + // 顶栏 + egui::TopBottomPanel::top("topbar") + .frame(egui::Frame::none() + .fill(material::BACKGROUND_ALT) + .inner_margin(egui::Margin::symmetric(12.0, 8.0)) + .stroke(egui::Stroke::new(1.0, material::CARD_BORDER))) + .show(ctx, |ui| { + ui.horizontal(|ui| { + ui.label(egui::RichText::new("🛡 涉密文件自检工具") + .strong() + .size(20.0) + .color(material::PRIMARY)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if ui.button("⚙ 设置").clicked() { + self.show_settings = !self.show_settings; + self.show_report = false; + } + if ui.button("📋 报告").clicked() { + self.show_report = !self.show_report; + self.show_settings = false; + } + }); + }); + }); + + // 底部状态栏 + egui::TopBottomPanel::bottom("statusbar") + .frame(egui::Frame::none() + .fill(material::BACKGROUND_ALT) + .inner_margin(egui::Margin::symmetric(12.0, 6.0)) + .stroke(egui::Stroke::new(1.0, material::CARD_BORDER))) + .show(ctx, |ui| { + ui.horizontal(|ui| { + let state_str = match &self.state { + crate::app::RunState::Idle => "● 空闲".to_string(), + crate::app::RunState::Scanning => "🔍 阶段 1/3:扫描中……".to_string(), + crate::app::RunState::Sampling => "🎲 阶段 2/3:抽样……".to_string(), + crate::app::RunState::Inspecting => "🔬 阶段 3/3:抽检中……".to_string(), + crate::app::RunState::Reporting => "📝 正在生成报告……".to_string(), + crate::app::RunState::Done => "✔ 已完成".to_string(), + crate::app::RunState::Cancelled => "⏹ 已取消".to_string(), + crate::app::RunState::Error(e) => format!("✘ 出错:{}", e), + }; + ui.label(egui::RichText::new(state_str).strong().color(material::PRIMARY_DARK)); + ui.separator(); + let p = self.progress.load(std::sync::atomic::Ordering::Relaxed); + let t = self.total.load(std::sync::atomic::Ordering::Relaxed); + let hits = self.hit_count.load(std::sync::atomic::Ordering::Relaxed); + let scanned = self.scan_scanned.load(std::sync::atomic::Ordering::Relaxed); + let found = self.scan_found.load(std::sync::atomic::Ordering::Relaxed); + ui.label(format!("扫描:{}/{} ", scanned, found)); + ui.separator(); + ui.label(format!("抽检:{}/{}", p, t)); + ui.separator(); + if hits > 0 { + ui.label(egui::RichText::new(format!("命中:{}", hits)).color(material::DANGER)); + } else { + ui.label("命中:0"); + } + ui.separator(); + if let Ok(cur) = self.current_file.lock() { + if let Some(f) = cur.as_ref() { + let short = shorten_path(f, 80); + ui.label(format!("当前:{}", short)); + } + } + }); + }); + + // 中间:设置 / 报告 / 主面板(三选一) + egui::CentralPanel::default().show(ctx, |ui| { + if self.show_settings { + settings::draw(ui, &mut self.config); + } else if self.show_report { + if let Ok(rep) = self.report.lock() { + if let Some(r) = rep.as_ref() { + crate::ui::report::draw(ui, r); + } else { + ui.label("暂无报告"); + } + } + } else { + home::draw(ui, self); + } + }); + + // 请求持续重绘,便于进度更新 + if self.state != RunState::Idle && self.state != RunState::Done && self.state != RunState::Cancelled { + ctx.request_repaint_after(std::time::Duration::from_millis(200)); + } + } + + fn on_exit(&mut self, _gl: Option<&eframe::glow::Context>) { + if let Err(e) = self.config.save() { + tracing::warn!("保存配置失败:{}", e); + } + } +} + +/// 把长路径截短(保留开头和结尾),避免状态栏溢出 +fn shorten_path(s: &str, max_chars: usize) -> String { + if s.chars().count() <= max_chars { return s.to_string(); } + let keep = max_chars.saturating_sub(3) / 2; + let head: String = s.chars().take(keep).collect(); + let tail: String = s.chars().rev().take(keep).collect::().chars().rev().collect(); + format!("{}…{}", head, tail) +} diff --git a/src/config/mod.rs b/src/config/mod.rs new file mode 100644 index 0000000..a31f252 --- /dev/null +++ b/src/config/mod.rs @@ -0,0 +1,10 @@ +// 配置模块 +pub mod model; +pub mod persist; + +pub use model::{ + AppConfig, GeneralSettings, InspectSettings, KeywordSettings, KeywordSettings as Keywords, LogLevel, + ReportFormat, ReportSettings, SampleMode, SampleStrategy, ScanSettings, ScreenshotMode, Theme, + UiSettings, ViewerSettings, Language, +}; +pub use persist::{load_config, save_config, config_path}; diff --git a/src/config/model.rs b/src/config/model.rs new file mode 100644 index 0000000..1634f98 --- /dev/null +++ b/src/config/model.rs @@ -0,0 +1,349 @@ +// 强类型配置 +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; + +use crate::utils::paths; + +/// 主题 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Theme { + Light, + Dark, + Follow, +} + +impl Default for Theme { + fn default() -> Self { Theme::Follow } +} + +/// 语言 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum Language { + Zh, + En, +} + +impl Default for Language { + fn default() -> Self { Language::Zh } +} + +/// 日志级别 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum LogLevel { + Error, + Warn, + Info, + Debug, + Trace, +} +impl Default for LogLevel { fn default() -> Self { LogLevel::Info } } + +/// 抽样策略 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum SampleStrategy { + Random, + Stratified, + Quota, +} +impl Default for SampleStrategy { fn default() -> Self { SampleStrategy::Random } } + +/// 抽检数量模式 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum SampleMode { + /// 按固定份数(sample_count) + Count, + /// 按全量候选的百分比(sample_percent) + Percent, + /// 全部候选 + All, +} +impl Default for SampleMode { fn default() -> Self { SampleMode::Count } } + +/// 截图模式 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ScreenshotMode { + /// 模拟 Win+Shift+S + 手动框选 + Manual, + /// PrintWindow 全自动 + AutoPrintWindow, + /// 自动优先 + 失败降级手动 + AutoWithFallback, +} +impl Default for ScreenshotMode { fn default() -> Self { ScreenshotMode::Manual } } + +/// 报告格式 +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub enum ReportFormat { + Html, + Json, + Png, + All, +} +impl Default for ReportFormat { fn default() -> Self { ReportFormat::All } } + +/// 常规设置 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GeneralSettings { + pub auto_run: bool, + pub start_minimized: bool, + pub auto_start: bool, + pub theme: Theme, + pub language: Language, + pub log_level: LogLevel, + pub log_retention_days: u32, + pub single_instance: bool, + pub clear_temp_on_start: bool, + pub auto_exit: bool, + pub auto_exit_seconds: u32, +} +impl Default for GeneralSettings { + fn default() -> Self { + Self { + auto_run: false, + start_minimized: false, + auto_start: false, + theme: Theme::default(), + language: Language::default(), + log_level: LogLevel::default(), + log_retention_days: 14, + single_instance: true, + clear_temp_on_start: true, + auto_exit: false, + auto_exit_seconds: 10, + } + } +} + +/// 扫描范围 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ScanSettings { + /// 白名单目录(不扫描的目录) + pub whitelist: Vec, + pub include_hidden: bool, + pub include_system: bool, + pub follow_symlinks: bool, + pub min_size_kb: u64, + pub max_depth: u32, + pub extensions: Vec, + pub scan_timeout_minutes: u32, +} +impl Default for ScanSettings { + fn default() -> Self { + let mut whitelist = Vec::new(); + // 默认白名单:常见系统/临时目录 + for d in [r"C:\Windows", r"C:\Program Files", r"C:\Program Files (x86)", r"C:\ProgramData", r"C:\Recovery"] { + whitelist.push(PathBuf::from(d)); + } + Self { + whitelist, + include_hidden: false, + include_system: false, + follow_symlinks: false, + min_size_kb: 1, + max_depth: 0, + extensions: vec!["doc".into(), "docx".into(), "pdf".into(), "xlsx".into()], + scan_timeout_minutes: 0, + } + } +} + +/// 抽检设置 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct InspectSettings { + /// 抽样数量模式:Count/Percent/All + pub sample_mode: SampleMode, + /// 固定份数(Count 模式) + pub sample_count: usize, + /// 百分比(Percent 模式,0~100) + pub sample_percent: f32, + pub strategy: SampleStrategy, + pub doc_quota: usize, + pub docx_quota: usize, + pub pdf_quota: usize, + pub xlsx_quota: usize, + pub per_file_timeout_sec: u64, + pub dedup_days: u32, + pub skip_locked: bool, + pub stop_on_first_hit: bool, +} +impl Default for InspectSettings { + fn default() -> Self { + Self { + sample_mode: SampleMode::Count, + sample_count: 20, + sample_percent: 20.0, + strategy: SampleStrategy::Random, + doc_quota: 0, + docx_quota: 0, + pdf_quota: 0, + xlsx_quota: 0, + per_file_timeout_sec: 120, + dedup_days: 7, + skip_locked: true, + stop_on_first_hit: false, + } + } +} + +/// 查看器与截图设置 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ViewerSettings { + pub screenshot_mode: ScreenshotMode, + pub doc_viewer: Option, + pub doc_args: String, + pub pdf_viewer: Option, + pub pdf_args: String, + pub pre_capture_wait_ms: u64, + pub manual_capture_timeout_sec: u64, + pub max_black_ratio: f32, + pub auto_close_after: bool, + pub close_wait_ms: u64, + pub kill_timeout_ms: u64, + pub umi_ocr_url: String, + pub umi_ocr_exe: Option, + pub umi_ocr_startup_wait_sec: u64, + pub umi_ocr_call_timeout_sec: u64, + pub ocr_language: String, + pub ocr_cls: bool, + pub ocr_limit_side_len: u32, +} +impl Default for ViewerSettings { + fn default() -> Self { + Self { + screenshot_mode: ScreenshotMode::Manual, + doc_viewer: None, + doc_args: "\"{path}\"".into(), + pdf_viewer: None, + pdf_args: "\"{path}\"".into(), + pre_capture_wait_ms: 1500, + manual_capture_timeout_sec: 60, + max_black_ratio: 0.95, + auto_close_after: true, + close_wait_ms: 1500, + kill_timeout_ms: 3000, + umi_ocr_url: "http://127.0.0.1:1224/api/ocr".into(), + umi_ocr_exe: None, + umi_ocr_startup_wait_sec: 3, + umi_ocr_call_timeout_sec: 30, + ocr_language: "models/config_chinese.txt".into(), + ocr_cls: false, + ocr_limit_side_len: 2880, + } + } +} + +/// 关键词 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct KeywordSettings { + pub global: Vec, + pub doc_extra: Vec, + pub docx_extra: Vec, + pub pdf_extra: Vec, + pub xlsx_extra: Vec, + pub case_sensitive: bool, + pub use_regex: bool, + pub whole_word: bool, + pub min_confidence: f32, + pub highlight_color: [u8; 3], + pub false_positive_fingerprints: Vec, +} +impl Default for KeywordSettings { + fn default() -> Self { + Self { + global: vec![ + "机密".into(), "秘密".into(), "绝密".into(), "内部".into(), + "Confidential".into(), "Secret".into(), + ], + doc_extra: vec![], + docx_extra: vec![], + pdf_extra: vec![], + xlsx_extra: vec![], + case_sensitive: false, + use_regex: false, + whole_word: false, + min_confidence: 0.6, + highlight_color: [220, 38, 38], + false_positive_fingerprints: vec![], + } + } +} + +/// 报告设置 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ReportSettings { + pub output_dir: PathBuf, + pub formats: Vec, + pub file_prefix: String, + pub include_screenshots: bool, + pub highlight_sensitive: bool, + pub max_screenshot_side: u32, + pub history_keep: u32, + pub auto_open: bool, + pub copy_summary_to_clipboard: bool, +} +impl Default for ReportSettings { + fn default() -> Self { + Self { + output_dir: paths::default_report_dir(), + formats: vec![ReportFormat::Html, ReportFormat::Json], + file_prefix: "selfcheck-{date}".into(), + include_screenshots: true, + highlight_sensitive: true, + max_screenshot_side: 1600, + history_keep: 30, + auto_open: true, + copy_summary_to_clipboard: false, + } + } +} + +/// UI 设置 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UiSettings { + pub font_path: Option, + pub font_size: f32, +} +impl Default for UiSettings { + fn default() -> Self { + Self { + font_path: None, + font_size: 14.0, + } + } +} + +/// 顶层配置 +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct AppConfig { + pub general: GeneralSettings, + pub scan: ScanSettings, + pub inspect: InspectSettings, + pub viewer: ViewerSettings, + pub keyword: KeywordSettings, + pub report: ReportSettings, + pub ui: UiSettings, +} + +impl AppConfig { + /// 加载(如不存在则返回默认) + pub fn load() -> anyhow::Result { + let path = paths::config_file(); + if !path.exists() { + return Ok(Self::default()); + } + let s = std::fs::read_to_string(&path)?; + let cfg: AppConfig = toml::from_str(&s)?; + Ok(cfg) + } + + /// 保存 + pub fn save(&self) -> anyhow::Result<()> { + let path = paths::config_file(); + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + let s = toml::to_string_pretty(self)?; + std::fs::write(&path, s)?; + Ok(()) + } +} diff --git a/src/config/persist.rs b/src/config/persist.rs new file mode 100644 index 0000000..0319f5b --- /dev/null +++ b/src/config/persist.rs @@ -0,0 +1,16 @@ +// 配置加载/保存辅助 +use super::model::AppConfig; +use crate::utils::paths; + +pub fn load_config() -> anyhow::Result { + AppConfig::load() +} + +pub fn save_config(cfg: &AppConfig) -> anyhow::Result<()> { + cfg.save() +} + +/// 获取配置文件路径(用于 UI 显示) +pub fn config_path() -> std::path::PathBuf { + paths::config_file() +} diff --git a/src/inspect/doc_inspector.rs b/src/inspect/doc_inspector.rs new file mode 100644 index 0000000..7a0e444 --- /dev/null +++ b/src/inspect/doc_inspector.rs @@ -0,0 +1,93 @@ +// DOC 抽检:doclite.exe + 截图(三种模式)+ OCR + 关键词 +use std::future::Future; +use std::path::{Path, PathBuf}; +use std::pin::Pin; +use std::sync::atomic::AtomicBool; + +use crate::config::AppConfig; +use crate::inspect::external; +use crate::inspect::screenshot; +use crate::inspect::umi_ocr; +use crate::inspect::{make_hit, Finding, Inspector}; +use crate::matcher::keywords::{keywords_for, Matcher}; +use crate::utils::paths; + +pub struct DocInspector { + pub cfg: AppConfig, +} + +impl DocInspector { + pub fn new(cfg: AppConfig) -> Self { Self { cfg } } +} + +impl Inspector for DocInspector { + fn inspect<'a>( + &'a self, + path: &'a Path, + cfg: &'a AppConfig, + cancel: &'a AtomicBool, + log: &'a (dyn Fn(&str) + Send + Sync), + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + let viewer = cfg.viewer.doc_viewer.clone() + .or_else(paths::detect_doclite) + .ok_or_else(|| anyhow::anyhow!("未找到 doclite.exe;请在设置中指定或把它放到 exe 同目录"))?; + let args = cfg.viewer.doc_args.replace("{path}", &path.display().to_string()); + log(&format!(" 启动 DOC 查看器:{} {}", viewer.display(), args)); + let child = external::spawn(&viewer, &args)?; + let wait_ms = cfg.viewer.pre_capture_wait_ms; + tokio::time::sleep(std::time::Duration::from_millis(wait_ms)).await; + + let png = match cfg.viewer.screenshot_mode { + crate::config::ScreenshotMode::Manual => { + screenshot::capture_manual( + child.pid, + &cfg.viewer, + cancel, + log, + )? + } + crate::config::ScreenshotMode::AutoPrintWindow => { + screenshot::capture_printwindow(child.pid, &cfg.viewer, log) + .ok_or_else(|| anyhow::anyhow!("PrintWindow 抓取失败"))? + } + crate::config::ScreenshotMode::AutoWithFallback => { + match screenshot::capture_printwindow(child.pid, &cfg.viewer, log) { + Some(p) => p, + None => { + log(" PrintWindow 失败,降级到手动截图"); + screenshot::capture_manual(child.pid, &cfg.viewer, cancel, log)? + } + } + } + }; + + // OCR + let ocr = umi_ocr::UmiOcrClient::new(&cfg.viewer.umi_ocr_url, std::time::Duration::from_secs(cfg.viewer.umi_ocr_call_timeout_sec)); + let resp = ocr.recognize_png(&png, &cfg.viewer.ocr_language, cfg.viewer.ocr_cls, cfg.viewer.ocr_limit_side_len).await?; + let raw_text: String = resp.data.iter().map(|d| d.text.clone()).collect::>().join("\n"); + + // 保存截图到 temp + let shot_path = paths::temp_dir(&chrono::Local::now().format("%Y%m%d-%H%M%S").to_string()) + .join(format!("{}.png", path.file_stem().and_then(|s| s.to_str()).unwrap_or("file"))); + let _ = std::fs::create_dir_all(shot_path.parent().unwrap()); + let _ = std::fs::write(&shot_path, &png); + + // 匹配 + let kws = keywords_for("doc", &cfg.keyword); + let m = Matcher::new(kws, &cfg.keyword); + let hits = m.find(&raw_text); + + // 关闭查看器 + if cfg.viewer.auto_close_after { + let _ = external::close(&child, &cfg.viewer); + } + + Ok(make_hit(path, "doc", hits, raw_text, Some(shot_path))) + }) + } +} + +// 引用避免警告 +#[allow(dead_code)] +fn _unused(p: PathBuf) { let _ = p; } diff --git a/src/inspect/docx_inspector.rs b/src/inspect/docx_inspector.rs new file mode 100644 index 0000000..5859a86 --- /dev/null +++ b/src/inspect/docx_inspector.rs @@ -0,0 +1,81 @@ +// DOCX 文本抽检:docx-rs 0.4 读段落,关键词匹配 +use std::future::Future; +use std::path::Path; +use std::pin::Pin; +use std::sync::atomic::AtomicBool; + +use docx_rs::{DocumentChild, ParagraphChild, RunChild, TableCellContent, TableChild, TableRowChild}; + +use crate::config::AppConfig; +use crate::inspect::{make_hit, Finding, Inspector}; +use crate::matcher::keywords::{keywords_for, Matcher}; + +pub struct DocxInspector; + +impl Inspector for DocxInspector { + fn inspect<'a>( + &'a self, + path: &'a Path, + cfg: &'a AppConfig, + _cancel: &'a AtomicBool, + log: &'a (dyn Fn(&str) + Send + Sync), + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + log(" 解析 DOCX 文本……"); + let bytes = std::fs::read(path)?; + // docx-rs 0.4:使用 read_docx 解析整个 zip + let doc = docx_rs::read_docx(&bytes).map_err(|e| anyhow::anyhow!("docx-rs 解析失败:{:?}", e))?; + + let mut text = String::new(); + for d in doc.document.children.iter() { + match d { + DocumentChild::Paragraph(p) => { + // p: &Box + for pc in p.children.iter() { + if let ParagraphChild::Run(r) = pc { + for rc in r.children.iter() { + if let RunChild::Text(t) = rc { + text.push_str(&t.text); + } + } + } + } + text.push('\n'); + } + DocumentChild::Table(t) => { + // t: &Box;t.rows: Vec + for tc in t.rows.iter() { + if let TableChild::TableRow(row) = tc { + for rc in row.cells.iter() { + if let TableRowChild::TableCell(cell) = rc { + for cc in cell.children.iter() { + if let TableCellContent::Paragraph(p) = cc { + for pc in p.children.iter() { + if let ParagraphChild::Run(r) = pc { + for rcc in r.children.iter() { + if let RunChild::Text(t) = rcc { + text.push_str(&t.text); + } + } + } + } + text.push('\n'); + } + } + } + text.push('\t'); + } + text.push('\n'); + } + } + } + _ => {} + } + } + let kws = keywords_for("docx", &cfg.keyword); + let m = Matcher::new(kws, &cfg.keyword); + let hits = m.find(&text); + Ok(make_hit(path, "docx", hits, text, None)) + }) + } +} diff --git a/src/inspect/external.rs b/src/inspect/external.rs new file mode 100644 index 0000000..fb13246 --- /dev/null +++ b/src/inspect/external.rs @@ -0,0 +1,94 @@ +// 进程管理:启进程、关闭窗口、强杀 +use std::path::Path; + +use windows_sys::Win32::Foundation::HWND; +use windows_sys::Win32::System::Threading::{ + OpenProcess, TerminateProcess, WaitForSingleObject, PROCESS_TERMINATE, PROCESS_VM_READ, +}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + EnumWindows, GetWindowThreadProcessId, PostMessageW, WM_CLOSE, +}; + +use std::cell::RefCell; + +thread_local! { + static ENUM_RESULT: RefCell = RefCell::new(EnumResult { hwnd: std::ptr::null_mut(), pid: 0 }); +} +struct EnumResult { hwnd: HWND, pid: u32 } + +unsafe extern "system" fn enum_callback(hwnd: HWND, _lparam: isize) -> i32 { + let mut proc_id: u32 = 0; + GetWindowThreadProcessId(hwnd, &mut proc_id); + ENUM_RESULT.with(|r| { + *r.borrow_mut() = EnumResult { hwnd, pid: proc_id }; + }); + 1 // continue +} + +/// 启动的子进程 +pub struct Child { + pub pid: u32, + pub _handle: Option, +} + +unsafe impl Send for Child {} +unsafe impl Sync for Child {} + +/// 启动外部进程 +pub fn spawn(exe: &Path, args: &str) -> anyhow::Result { + let mut cmd = std::process::Command::new(exe); + for arg in args.split_whitespace() { + cmd.arg(arg.trim_matches('"')); + } + let child = cmd.spawn().map_err(|e| anyhow::anyhow!("启动 {:?} 失败:{}", exe, e))?; + Ok(Child { pid: child.id(), _handle: Some(child) }) +} + +/// 通过 PID 找到主窗口 HWND +pub fn find_hwnd_by_pid(pid: u32) -> Option { + unsafe { + let _ = EnumWindows(Some(enum_callback), 0); + } + let r = ENUM_RESULT.with(|r| r.borrow().hwnd); + let p = ENUM_RESULT.with(|r| r.borrow().pid); + if p == pid && !r.is_null() { Some(r) } else { None } +} + +/// 优雅关闭:PostMessage WM_CLOSE 给主窗口 → 等待 → taskkill +pub fn close(child: &Child, cfg: &crate::config::ViewerSettings) -> anyhow::Result<()> { + if let Some(hwnd) = find_hwnd_by_pid(child.pid) { + unsafe { + PostMessageW(hwnd, WM_CLOSE, 0, 0); + } + std::thread::sleep(std::time::Duration::from_millis(cfg.close_wait_ms)); + } + if is_running(child.pid) { + kill(child.pid); + } + Ok(()) +} + +/// 进程是否仍在运行 +pub fn is_running(pid: u32) -> bool { + unsafe { + let h = OpenProcess(PROCESS_VM_READ, 0, pid); + if h.is_null() { return false; } + let r = WaitForSingleObject(h, 0); + windows_sys::Win32::Foundation::CloseHandle(h); + r != 0 + } +} + +/// 强杀 +pub fn kill(pid: u32) { + unsafe { + let h = OpenProcess(PROCESS_TERMINATE, 0, pid); + if !h.is_null() { + TerminateProcess(h, 1); + windows_sys::Win32::Foundation::CloseHandle(h); + } + let _ = std::process::Command::new("taskkill") + .arg("/F").arg("/PID").arg(pid.to_string()).arg("/T") + .output(); + } +} diff --git a/src/inspect/mod.rs b/src/inspect/mod.rs new file mode 100644 index 0000000..aa40a5e --- /dev/null +++ b/src/inspect/mod.rs @@ -0,0 +1,59 @@ +// 抽检模块 +pub mod doc_inspector; +pub mod docx_inspector; +pub mod external; +pub mod mod_helper; +pub mod pdf_inspector; +pub mod screenshot; +pub mod umi_ocr; +pub mod xlsx_inspector; + +pub use crate::scan::sampler; + +use std::path::{Path, PathBuf}; +use std::sync::atomic::AtomicBool; + +use serde::{Deserialize, Serialize}; + +use crate::config::AppConfig; +use crate::matcher::keywords::MatchHit; + +/// 单次抽检结果 +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Finding { + pub path: PathBuf, + pub kind: String, + pub matched: Vec, + pub confidence: f32, + pub boxes: Vec<[f64; 4]>, // [x1,y1,x2,y2] + pub raw_text: String, + pub screenshot: Option, +} + +impl Finding { + pub fn is_hit(&self) -> bool { !self.matched.is_empty() } +} + +/// Inspector 接口 +pub trait Inspector: Send { + fn inspect<'a>( + &'a self, + path: &'a Path, + cfg: &'a AppConfig, + cancel: &'a AtomicBool, + log: &'a (dyn Fn(&str) + Send + Sync), + ) -> std::pin::Pin> + Send + 'a>>; +} + +/// 公共辅助 +pub fn make_hit(path: &Path, kind: &str, hits: Vec, raw: String, ss: Option) -> Finding { + Finding { + path: path.to_path_buf(), + kind: kind.to_string(), + matched: hits.into_iter().map(|h| h.keyword).collect(), + confidence: 1.0, + boxes: vec![], + raw_text: raw, + screenshot: ss, + } +} diff --git a/src/inspect/mod_helper.rs b/src/inspect/mod_helper.rs new file mode 100644 index 0000000..1672e69 --- /dev/null +++ b/src/inspect/mod_helper.rs @@ -0,0 +1,3 @@ +// 防止 mod.rs 中文件/子模块同名造成混乱 +#[allow(dead_code)] +pub fn marker() {} diff --git a/src/inspect/pdf_inspector.rs b/src/inspect/pdf_inspector.rs new file mode 100644 index 0000000..ef75036 --- /dev/null +++ b/src/inspect/pdf_inspector.rs @@ -0,0 +1,44 @@ +// PDF 抽检:与 DOC 类似,调用 Windows 关联 PDF 程序 +use std::future::Future; +use std::path::Path; +use std::pin::Pin; +use std::sync::atomic::AtomicBool; + +use crate::config::AppConfig; +use crate::inspect::doc_inspector::DocInspector; +use crate::inspect::{Finding, Inspector}; + +pub struct PdfInspector { + pub cfg: AppConfig, +} + +impl PdfInspector { + pub fn new(cfg: AppConfig) -> Self { Self { cfg } } +} + +impl Inspector for PdfInspector { + fn inspect<'a>( + &'a self, + path: &'a Path, + cfg: &'a AppConfig, + cancel: &'a AtomicBool, + log: &'a (dyn Fn(&str) + Send + Sync), + ) -> Pin> + Send + 'a>> { + // 复用 DocInspector 的 inspect 逻辑(参数模板用 cfg.viewer.pdf_args) + let mut pdf_cfg = cfg.clone(); + if cfg.viewer.pdf_viewer.is_some() { + pdf_cfg.viewer.doc_viewer = cfg.viewer.pdf_viewer.clone(); + pdf_cfg.viewer.doc_args = cfg.viewer.pdf_args.clone(); + } + if cfg.viewer.pdf_viewer.is_none() { + // 用 cmd /c start "" "" 触发默认关联 + let mut pdf_cfg2 = cfg.clone(); + pdf_cfg2.viewer.doc_viewer = Some(std::path::PathBuf::from("C:\\Windows\\System32\\cmd.exe")); + pdf_cfg2.viewer.doc_args = format!("/c start \"\" \"{}\"", path.display()); + let inspector = DocInspector::new(pdf_cfg2); + return Box::pin(async move { inspector.inspect(path, &pdf_cfg, cancel, log).await }); + } + let inspector = DocInspector::new(pdf_cfg); + Box::pin(async move { inspector.inspect(path, cfg, cancel, log).await }) + } +} diff --git a/src/inspect/screenshot.rs b/src/inspect/screenshot.rs new file mode 100644 index 0000000..eefe780 --- /dev/null +++ b/src/inspect/screenshot.rs @@ -0,0 +1,116 @@ +// 截图:Win+Shift+S(手动)+ PrintWindow(自动)+ 剪贴板读位图 +use std::sync::atomic::AtomicBool; + +use windows_sys::Win32::Foundation::{HWND, RECT}; +use windows_sys::Win32::Graphics::Gdi::{ + CreateCompatibleBitmap, CreateCompatibleDC, DeleteDC, DeleteObject, GetDC, GetDIBits, + ReleaseDC, SelectObject, BITMAPINFO, BITMAPINFOHEADER, DIB_RGB_COLORS, HBITMAP, HDC, +}; +use windows_sys::Win32::Storage::Xps::PrintWindow; +use windows_sys::Win32::UI::Input::KeyboardAndMouse::{ + keybd_event, KEYEVENTF_KEYUP, VK_LSHIFT, VK_LWIN, +}; +use windows_sys::Win32::UI::WindowsAndMessaging::{ + GetClientRect, SetForegroundWindow, ShowWindow, +}; + +use crate::config::ViewerSettings; +use crate::inspect::external; + +pub type PngBytes = Vec; + +/// 自动模式:PrintWindow 抓窗口 +pub fn capture_printwindow(pid: u32, cfg: &ViewerSettings, log: &dyn Fn(&str)) -> Option { + let hwnd = external::find_hwnd_by_pid(pid)?; + if hwnd.is_null() { return None; } + unsafe { + let mut rect: RECT = std::mem::zeroed(); + if GetClientRect(hwnd, &mut rect) == 0 { + log(" GetClientRect 失败"); + return None; + } + let w = (rect.right - rect.left).max(1); + let h = (rect.bottom - rect.top).max(1); + let hdc_screen: HDC = GetDC(std::ptr::null_mut()); + if hdc_screen.is_null() { return None; } + let hdc: HDC = CreateCompatibleDC(hdc_screen); + let hbm: HBITMAP = CreateCompatibleBitmap(hdc_screen, w, h); + let old = SelectObject(hdc, hbm); + // PW_RENDERFULLCONTENT = 0x00000002 + let _ = PrintWindow(hwnd, hdc, 0x00000002); + let png = bitmap_to_png(hbm, w, h); + let _ = SelectObject(hdc, old); + let _ = DeleteObject(hbm); + let _ = DeleteDC(hdc); + ReleaseDC(std::ptr::null_mut(), hdc_screen); + match png { + Some(bytes) => { + if is_mostly_black(&bytes, cfg.max_black_ratio) { + log(" 抓到的位图过黑,视为失败"); + return None; + } + Some(bytes) + } + None => None, + } + } +} + +/// 手动模式:触发 Win+Shift+S,等待用户框选 +pub fn capture_manual(pid: u32, cfg: &ViewerSettings, _cancel: &AtomicBool, log: &dyn Fn(&str)) -> anyhow::Result { + if let Some(hwnd) = external::find_hwnd_by_pid(pid) { + if !hwnd.is_null() { + unsafe { + let _ = ShowWindow(hwnd, 5 /* SW_SHOW */); + let _ = SetForegroundWindow(hwnd); + } + } + } + unsafe { + keybd_event(VK_LSHIFT as u8, 0, 0, 0); + keybd_event(VK_LWIN as u8, 0, 0, 0); + keybd_event(0x53, 0, 0, 0); + keybd_event(0x53, 0, KEYEVENTF_KEYUP, 0); + keybd_event(VK_LSHIFT as u8, 0, KEYEVENTF_KEYUP, 0); + keybd_event(VK_LWIN as u8, 0, KEYEVENTF_KEYUP, 0); + } + log(&format!(" 请在 {} 秒内用系统截图工具框选窗口", cfg.manual_capture_timeout_sec)); + + // 简化:手动模式需要用户上传图片(这里返回错误,UI 提示) + // 实际生产应通过剪贴板或专门的文件对话框 + Err(anyhow::anyhow!("手动模式需要通过 UI 上传截图(待实现)")) +} + +fn bitmap_to_png(hbm: HBITMAP, w: i32, h: i32) -> Option { + unsafe { + let mut bmi: BITMAPINFO = std::mem::zeroed(); + bmi.bmiHeader.biSize = std::mem::size_of::() as u32; + bmi.bmiHeader.biWidth = w; + bmi.bmiHeader.biHeight = -h; // top-down + bmi.bmiHeader.biPlanes = 1; + bmi.bmiHeader.biBitCount = 32; + bmi.bmiHeader.biCompression = 0; + let mut buf = vec![0u8; (w as usize) * (h as usize) * 4]; + let hdc = GetDC(std::ptr::null_mut()); + let n = GetDIBits(hdc, hbm, 0, h as u32, buf.as_mut_ptr() as _, &mut bmi, DIB_RGB_COLORS); + ReleaseDC(std::ptr::null_mut(), hdc); + if n == 0 { return None; } + let img = image::RgbaImage::from_raw(w as u32, h as u32, buf)?; + let dyn_img = image::DynamicImage::ImageRgba8(img); + let mut out = Vec::new(); + dyn_img.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).ok()?; + Some(out) + } +} + +fn is_mostly_black(png: &[u8], max_ratio: f32) -> bool { + if let Ok(img) = image::load_from_memory(png) { + let img = img.to_luma8(); + let total = (img.width() * img.height()) as usize; + if total == 0 { return true; } + let black = img.as_raw().iter().filter(|&&p| p < 16).count(); + let r = black as f32 / total as f32; + return r > max_ratio; + } + false +} diff --git a/src/inspect/umi_ocr.rs b/src/inspect/umi_ocr.rs new file mode 100644 index 0000000..352b0f7 --- /dev/null +++ b/src/inspect/umi_ocr.rs @@ -0,0 +1,141 @@ +// Umi-OCR HTTP 客户端 +use std::os::windows::ffi::OsStrExt; +use std::path::PathBuf; +use std::time::Duration; + +use base64::Engine; +use serde::{Deserialize, Serialize}; +use windows_sys::Win32::System::Threading::{CreateProcessW, PROCESS_INFORMATION, STARTUPINFOW}; + +use crate::config::ViewerSettings; +use crate::utils::paths; + +#[derive(Serialize)] +struct OcrOptions { + #[serde(rename = "ocr.language")] + language: String, + #[serde(rename = "ocr.cls")] + cls: bool, + #[serde(rename = "ocr.limit_side_len")] + limit_side_len: u32, + #[serde(rename = "tbpu.parser")] + parser: String, +} + +#[derive(Serialize)] +struct OcrRequest<'a> { + base64: &'a str, + options: OcrOptions, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct OcrResponse { + pub code: u32, + pub data: Vec, + #[allow(dead_code)] + pub time: f64, +} + +#[derive(Deserialize, Debug, Clone)] +pub struct OcrItem { + pub text: String, + pub score: f32, + #[serde(default, alias = "box")] + pub box_field: Vec>, +} + +impl OcrItem { + pub fn box_points(&self) -> &Vec> { &self.box_field } +} + +pub struct UmiOcrClient { + base_url: String, + #[allow(dead_code)] + timeout: Duration, + client: reqwest::blocking::Client, +} + +impl UmiOcrClient { + pub fn new(base_url: &str, timeout: Duration) -> Self { + let client = reqwest::blocking::Client::builder() + .timeout(timeout) + .build() + .unwrap_or_default(); + Self { base_url: base_url.to_string(), timeout, client } + } + + pub fn recognize_png_blocking(&self, png: &[u8], language: &str, cls: bool, limit_side_len: u32) -> anyhow::Result { + let b64 = base64::engine::general_purpose::STANDARD.encode(png); + let req = OcrRequest { + base64: &b64, + options: OcrOptions { + language: language.to_string(), + cls, + limit_side_len, + parser: "multi_para".into(), + }, + }; + let resp = self.client.post(&self.base_url).json(&req).send()?.error_for_status()?; + let parsed: OcrResponse = resp.json()?; + if parsed.code != 100 { + anyhow::bail!("Umi-OCR 返回 code={}", parsed.code); + } + Ok(parsed) + } + + /// 异步封装(直接调阻塞版) + pub async fn recognize_png(&self, png: &[u8], language: &str, cls: bool, limit_side_len: u32) -> anyhow::Result { + let me = unsafe { std::ptr::read(self) }; + let png = png.to_vec(); + // 把 &str 转为 String 以满足 'static 约束 + let language = language.to_string(); + tokio::task::spawn_blocking(move || me.recognize_png_blocking(&png, &language, cls, limit_side_len)).await? + } +} + +/// 启动 Umi-OCR.exe(若未运行) +pub fn ensure_started(cfg: &ViewerSettings) -> anyhow::Result<()> { + if let Ok(r) = reqwest::blocking::get(cfg.umi_ocr_url.replace("/api/ocr", "/")) { + if r.status().is_success() { return Ok(()); } + } + let exe = cfg.umi_ocr_exe.clone() + .or_else(paths::detect_umi_ocr) + .ok_or_else(|| anyhow::anyhow!("未找到 Umi-OCR.exe"))?; + spawn(&exe)?; + let start = std::time::Instant::now(); + let wait = std::time::Duration::from_secs(cfg.umi_ocr_startup_wait_sec.max(1)); + while start.elapsed() < wait { + std::thread::sleep(std::time::Duration::from_millis(500)); + if let Ok(r) = reqwest::blocking::get(cfg.umi_ocr_url.replace("/api/ocr", "/")) { + if r.status().is_success() { return Ok(()); } + } + } + Err(anyhow::anyhow!("Umi-OCR 健康检查超时")) +} + +fn spawn(exe: &PathBuf) -> anyhow::Result<()> { + unsafe { + let mut cmd: Vec = exe.as_os_str().encode_wide().chain(std::iter::once(0)).collect(); + let mut si: STARTUPINFOW = std::mem::zeroed(); + si.cb = std::mem::size_of::() as u32; + let mut pi: PROCESS_INFORMATION = std::mem::zeroed(); + let ok = CreateProcessW( + cmd.as_mut_ptr(), + std::ptr::null_mut(), + std::ptr::null(), + std::ptr::null(), + 0, + 0, + std::ptr::null(), + std::ptr::null(), + &si, + &mut pi, + ); + if ok == 0 { + return Err(anyhow::anyhow!("CreateProcessW 失败")); + } + windows_sys::Win32::Foundation::CloseHandle(pi.hProcess); + windows_sys::Win32::Foundation::CloseHandle(pi.hThread); + } + Ok(()) +} diff --git a/src/inspect/xlsx_inspector.rs b/src/inspect/xlsx_inspector.rs new file mode 100644 index 0000000..54d23b7 --- /dev/null +++ b/src/inspect/xlsx_inspector.rs @@ -0,0 +1,186 @@ +// XLSX 文本抽检:把 xlsx 当 zip 解压,提取 sharedStrings 与各 sheet 单元格的文本 +use std::future::Future; +use std::path::Path; +use std::pin::Pin; +use std::sync::atomic::AtomicBool; + +use quick_xml::events::Event; +use quick_xml::reader::Reader; + +use crate::config::AppConfig; +use crate::inspect::{make_hit, Finding, Inspector}; +use crate::matcher::keywords::{keywords_for, Matcher}; + +pub struct XlsxInspector; + +impl Inspector for XlsxInspector { + fn inspect<'a>( + &'a self, + path: &'a Path, + cfg: &'a AppConfig, + _cancel: &'a AtomicBool, + log: &'a (dyn Fn(&str) + Send + Sync), + ) -> Pin> + Send + 'a>> { + Box::pin(async move { + log(" 解析 XLSX 文本……"); + let bytes = std::fs::read(path)?; + + let mut archive = zip::ZipArchive::new(std::io::Cursor::new(bytes)) + .map_err(|e| anyhow::anyhow!("xlsx 不是合法 zip:{}", e))?; + + // 1. 读 sharedStrings + let shared: Vec = match archive.by_name("xl/sharedStrings.xml") { + Ok(mut f) => { + let mut s = String::new(); + use std::io::Read; + let _ = f.read_to_string(&mut s); + parse_shared_strings(&s) + } + Err(_) => Vec::new(), + }; + + // 2. 找 sheet 文件列表 + let sheet_files: Vec = (0..archive.len()) + .filter_map(|i| archive.by_index(i).ok().map(|f| f.name().to_string())) + .filter(|n| n.starts_with("xl/worksheets/sheet") && n.ends_with(".xml")) + .collect(); + + if sheet_files.is_empty() { + log(" ⚠ 未找到工作表(空 XLSX)"); + return Ok(make_hit(path, "xlsx", Vec::new(), String::new(), None)); + } + + // 3. 解析每个 sheet + let mut text = String::new(); + for name in &sheet_files { + if let Ok(mut f) = archive.by_name(name) { + let mut s = String::new(); + use std::io::Read; + let _ = f.read_to_string(&mut s); + let rows = extract_cell_text(&s, &shared); + for row in rows { + for (i, cell) in row.iter().enumerate() { + if i > 0 { text.push('\t'); } + text.push_str(cell); + } + text.push('\n'); + } + } + } + + let kws = keywords_for("xlsx", &cfg.keyword); + let m = Matcher::new(kws, &cfg.keyword); + let hits = m.find(&text); + Ok(make_hit(path, "xlsx", hits, text, None)) + }) + } +} + +/// 解析 sharedStrings.xml:...... +fn parse_shared_strings(xml: &str) -> Vec { + let mut reader = Reader::from_str(xml); + reader.config_mut().trim_text(true); + let mut out: Vec = Vec::new(); + let mut current = String::new(); + let mut in_t = false; + let mut buf = Vec::new(); + loop { + match reader.read_event_into(&mut buf) { + Ok(Event::Start(e)) => { + let local = String::from_utf8_lossy(e.local_name().as_ref()).to_string(); + if local == "t" { in_t = true; current.clear(); } + } + Ok(Event::Text(e)) => { + if in_t { + let s = e.unescape().unwrap_or_default().to_string(); + current.push_str(&s); + } + } + Ok(Event::End(e)) => { + let local = String::from_utf8_lossy(e.local_name().as_ref()).to_string(); + if local == "t" { in_t = false; } + else if local == "si" { out.push(std::mem::take(&mut current)); } + } + Ok(Event::Eof) => break, + Err(_) => break, + _ => {} + } + buf.clear(); + } + out +} + +/// 从 sheet XML 中提取所有行的文本。 +/// `51.23` +/// t="s" 表示值是 sharedStrings 的索引;否则是 inline str / number。 +fn extract_cell_text(xml: &str, shared: &[String]) -> Vec> { + let mut reader = Reader::from_str(xml); + reader.config_mut().trim_text(false); + let mut rows: Vec> = Vec::new(); + let mut cur_row: Vec = Vec::new(); + let mut cur_cell = String::new(); + let mut cell_type: Option = None; + let mut in_v = false; + let mut buf = Vec::new(); + loop { + match reader.read_event_into(&mut buf) { + Ok(Event::Start(e)) => { + let local = String::from_utf8_lossy(e.local_name().as_ref()).to_string(); + match local.as_str() { + "row" => cur_row.clear(), + "c" => { + cur_cell.clear(); + cell_type = None; + for attr in e.attributes().flatten() { + let key = String::from_utf8_lossy(attr.key.local_name().as_ref()).to_string(); + if key == "t" { + cell_type = Some(String::from_utf8_lossy(&attr.value).to_string()); + } + } + } + "v" => in_v = true, + _ => {} + } + } + Ok(Event::Text(e)) => { + if in_v { + let s = e.unescape().unwrap_or_default().to_string(); + cur_cell.push_str(&s); + } + } + Ok(Event::End(e)) => { + let local = String::from_utf8_lossy(e.local_name().as_ref()).to_string(); + match local.as_str() { + "v" => in_v = false, + "c" => { + let value = match cell_type.as_deref() { + Some("s") => { + // sharedStrings index + shared.get(cur_cell.parse::().unwrap_or(0)) + .cloned() + .unwrap_or_default() + } + Some("inlineStr") => { + // 简单处理:取当前 cur_cell(实际 inlineStr 嵌在 里) + // 为简化,跳过复杂的 inlineStr 解析;99% 场景 sharedStrings 已覆盖 + cur_cell.clone() + } + _ => cur_cell.clone(), + }; + cur_row.push(value); + cell_type = None; + } + "row" => rows.push(std::mem::take(&mut cur_row)), + _ => {} + } + } + Ok(Event::Eof) => break, + Err(_) => break, + _ => {} + } + buf.clear(); + } + // 收尾 + if !cur_row.is_empty() { rows.push(cur_row); } + rows +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..c05913e --- /dev/null +++ b/src/main.rs @@ -0,0 +1,44 @@ +// 隐藏控制台(双保险:build.rs 也有 /SUBSYSTEM:WINDOWS) +#![windows_subsystem = "windows"] + +mod app; +mod config; +mod inspect; +mod matcher; +mod privilege; +mod report; +mod scan; +mod ui; +mod utils; + +use crate::app::App; +use crate::privilege::ensure_admin; +use crate::utils::logger::init_logger; + +fn main() -> anyhow::Result<()> { + // 单次 UAC 提升:未以管理员运行时通过 runas 重启当前进程 + if let Err(e) = ensure_admin() { + // 提示但不强制退出(用户可能在调试) + tracing::warn!("UAC 提升失败:{}", e); + } + + init_logger(); + + let viewport = eframe::egui::ViewportBuilder::default() + .with_title("涉密文件自检工具") + .with_inner_size([1280.0, 800.0]) + .with_min_inner_size([960.0, 640.0]); + + let options = eframe::NativeOptions { + viewport, + vsync: true, + ..Default::default() + }; + + eframe::run_native( + "涉密文件自检工具", + options, + Box::new(|cc| Box::new(App::new(cc))), + ) + .map_err(|e| anyhow::anyhow!("eframe 启动失败:{}", e)) +} diff --git a/src/matcher/hash.rs b/src/matcher/hash.rs new file mode 100644 index 0000000..fd855c7 --- /dev/null +++ b/src/matcher/hash.rs @@ -0,0 +1,29 @@ +// 文件指纹(用于去重 / 白名单) +use std::path::Path; + +use sha2::{Digest, Sha256}; + +pub fn fingerprint(path: &Path) -> String { + let mut h = Sha256::new(); + h.update(path.to_string_lossy().as_bytes()); + if let Ok(m) = std::fs::metadata(path) { + h.update(m.len().to_le_bytes()); + if let Ok(modified) = m.modified() { + if let Ok(d) = modified.duration_since(std::time::UNIX_EPOCH) { + h.update(d.as_secs().to_le_bytes()); + } + } + } + let digest = h.finalize(); + hex::encode(&digest[..8]) +} + +pub mod hex { + pub fn encode(bytes: &[u8]) -> String { + let mut s = String::with_capacity(bytes.len() * 2); + for b in bytes { + s.push_str(&format!("{:02x}", b)); + } + s + } +} diff --git a/src/matcher/keywords.rs b/src/matcher/keywords.rs new file mode 100644 index 0000000..3d1b8ca --- /dev/null +++ b/src/matcher/keywords.rs @@ -0,0 +1,80 @@ +// 关键词匹配 +use std::collections::HashSet; + +use aho_corasick::AhoCorasick; +use regex::RegexSet; + +use crate::config::{KeywordSettings, ScreenshotMode}; + +/// 构建匹配器 +pub struct Matcher { + case_sensitive: bool, + whole_word: bool, + ac: Option, + regex: Option, + patterns: Vec, +} + +impl Matcher { + pub fn new(patterns: Vec, k: &KeywordSettings) -> Self { + let patterns = if k.case_sensitive { patterns } else { patterns.iter().map(|p| p.to_lowercase()).collect() }; + let (ac, regex) = if k.use_regex { + let r = RegexSet::new(&patterns).ok(); + (None, r) + } else { + let a = AhoCorasick::builder() + .ascii_case_insensitive(!k.case_sensitive) + .build(&patterns) + .ok(); + (a, None) + }; + Self { case_sensitive: k.case_sensitive, whole_word: k.whole_word, ac, regex, patterns } + } + + /// 在文本中找出所有命中的关键词(去重) + pub fn find(&self, text: &str) -> Vec { + let text_norm = if self.case_sensitive { text.to_string() } else { text.to_lowercase() }; + let mut hits: HashSet = HashSet::new(); + if let Some(ac) = &self.ac { + for m in ac.find_iter(&text_norm) { + let pat = self.patterns[m.pattern()].clone(); + if self.whole_word && !is_whole_word(&text_norm, m.start(), m.end()) { continue; } + hits.insert(pat); + } + } else if let Some(re) = &self.regex { + for idx in re.matches(&text_norm).into_iter() { + hits.insert(self.patterns[idx].clone()); + } + } + hits.into_iter().map(|p| MatchHit { keyword: p }).collect() + } +} + +#[derive(Debug, Clone)] +pub struct MatchHit { + pub keyword: String, +} + +fn is_whole_word(s: &str, start: usize, end: usize) -> bool { + let before = s[..start].chars().last(); + let after = s[end..].chars().next(); + fn is_word(c: char) -> bool { c.is_alphanumeric() || c == '_' } + !before.map(is_word).unwrap_or(false) && !after.map(is_word).unwrap_or(false) +} + +/// 合并全局 + 类型追加,返回该类型要用的关键词列表 +pub fn keywords_for(kind: &str, k: &KeywordSettings) -> Vec { + let mut v = k.global.clone(); + match kind { + "doc" => v.extend(k.doc_extra.clone()), + "docx" => v.extend(k.docx_extra.clone()), + "pdf" => v.extend(k.pdf_extra.clone()), + "xlsx" => v.extend(k.xlsx_extra.clone()), + _ => {} + } + v +} + +// 引用以避免 warning +#[allow(dead_code)] +fn _unused(_m: ScreenshotMode) {} diff --git a/src/matcher/mod.rs b/src/matcher/mod.rs new file mode 100644 index 0000000..3f34705 --- /dev/null +++ b/src/matcher/mod.rs @@ -0,0 +1,3 @@ +// 关键词匹配模块 +pub mod hash; +pub mod keywords; diff --git a/src/privilege.rs b/src/privilege.rs new file mode 100644 index 0000000..31dc275 --- /dev/null +++ b/src/privilege.rs @@ -0,0 +1,54 @@ +// UAC 单次提升:通过 ShellExecuteExW "runas" 重新启动当前 exe +// 必须先检查是否已是管理员——否则会触发无限重启链 +use std::os::windows::ffi::OsStrExt; +use std::path::PathBuf; + +use windows_sys::Win32::UI::Shell::{ + IsUserAnAdmin, ShellExecuteExW, SEE_MASK_FLAG_NO_UI, SEE_MASK_NOCLOSEPROCESS, SHELLEXECUTEINFOW, +}; + +/// 若非管理员则触发 UAC 提权并重启当前进程。 +/// - 已是管理员:直接返回 Ok +/// - 非管理员:触发 runas,成功则退出当前进程,由新进程接手 +/// - UAC 被取消:返回 Err,调用方决定是否继续 +pub fn ensure_admin() -> anyhow::Result<()> { + // 1. 先检查是否已是管理员 + unsafe { + if IsUserAnAdmin() != 0 { + tracing::info!("当前已是管理员,跳过 UAC 提权"); + return Ok(()); + } + } + + tracing::info!("当前非管理员,准备 UAC 提权……"); + let exe: PathBuf = std::env::current_exe()?; + let exe_str = exe.as_os_str(); + + // 命令行参数:保留 argv + let args_str = std::env::args().skip(1).collect::>().join(" "); + + // ShellExecuteExW 需要宽字符 + let mut file: Vec = exe_str.encode_wide().chain(std::iter::once(0)).collect(); + let mut verb: Vec = "runas".encode_utf16().chain(std::iter::once(0)).collect(); + let mut params: Vec = args_str.encode_utf16().chain(std::iter::once(0)).collect(); + + unsafe { + let mut info: SHELLEXECUTEINFOW = std::mem::zeroed(); + info.cbSize = std::mem::size_of::() as u32; + info.fMask = SEE_MASK_FLAG_NO_UI | SEE_MASK_NOCLOSEPROCESS; + info.lpVerb = verb.as_ptr(); + info.lpFile = file.as_ptr(); + info.lpParameters = params.as_ptr(); + info.nShow = 1; // SW_SHOWNORMAL + + let result = ShellExecuteExW(&mut info); + if result == 0 || (info.hInstApp as isize) <= 32 { + // UAC 失败或被取消:不退出当前进程,让用户以非管理员身份继续运行 + return Err(anyhow::anyhow!("UAC 提权失败或被取消(hInstApp={})", info.hInstApp as isize)); + } + } + + // 提权成功,新进程已被启动;当前进程退出 + tracing::info!("UAC 提权成功,旧进程退出"); + std::process::exit(0); +} diff --git a/src/report/html.rs b/src/report/html.rs new file mode 100644 index 0000000..e7c01b4 --- /dev/null +++ b/src/report/html.rs @@ -0,0 +1,78 @@ +// 报告写入(HTML) +use std::path::Path; + +use crate::config::AppConfig; +use crate::report::model::Report; + +pub fn write(cfg: &AppConfig, r: &Report) -> anyhow::Result<()> { + if !cfg.report.formats.contains(&crate::config::ReportFormat::Html) { return Ok(()); } + let dir = &cfg.report.output_dir; + std::fs::create_dir_all(dir)?; + let name = r.scan_id.replace([':', 'T', 'Z', '+', '-'], "").chars().take(15).collect::(); + let p = dir.join(format!("{}-{}.html", cfg.report.file_prefix, name)); + + let mut findings_html = String::new(); + if r.findings.is_empty() { + findings_html.push_str(""); + } else { + for f in &r.findings { + findings_html.push_str(&format!( + "", + html_escape(&f.path.display().to_string()), + html_escape(&f.kind), + html_escape(&f.matched.join(", ")), + f.confidence, + f.screenshot.as_ref().map(|s| html_escape(&s.display().to_string())).unwrap_or_else(|| "-".into()), + )); + } + } + + let body = format!( +r#" +涉密抽检报告 + + +

涉密文件自检报告

+
+
扫描 ID:{}
+
开始:{}
+
结束:{}
+
机器:{}   用户:{}
+
抽检:{} 份 / 命中:{} 份
+
结论:{}
+
+
+

命中清单

+
未发现敏感词
{}{}{}{:.2}{}
+ + {} +
文件类型命中关键词置信度截图
+ + +"#, + r.scan_id, r.started_at, r.finished_at, r.machine, r.user, + r.total, r.hit, + if r.hit > 0 { "danger" } else { "ok" }, + if r.hit > 0 { "本机存在疑似含敏感词文档" } else { "本机未发现敏感词" }, + findings_html + ); + + std::fs::write(p, body)?; + Ok(()) +} + +fn html_escape(s: &str) -> String { + s.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """) +} + +#[allow(dead_code)] +fn _path(p: &Path) { let _ = p; } diff --git a/src/report/json.rs b/src/report/json.rs new file mode 100644 index 0000000..fe8bbc9 --- /dev/null +++ b/src/report/json.rs @@ -0,0 +1,19 @@ +// 报告写入(JSON) +use std::path::Path; + +use crate::config::AppConfig; +use crate::report::model::Report; + +pub fn write(cfg: &AppConfig, r: &Report) -> anyhow::Result<()> { + if !cfg.report.formats.contains(&crate::config::ReportFormat::Json) { return Ok(()); } + let dir = &cfg.report.output_dir; + std::fs::create_dir_all(dir)?; + let name = r.scan_id.replace([':', 'T', 'Z', '+', '-'], "").chars().take(15).collect::(); + let p = dir.join(format!("{}-{}.json", cfg.report.file_prefix, name)); + let s = serde_json::to_string_pretty(r)?; + std::fs::write(p, s)?; + Ok(()) +} + +#[allow(dead_code)] +fn _path(p: &Path) { let _ = p; } diff --git a/src/report/mod.rs b/src/report/mod.rs new file mode 100644 index 0000000..4f0a012 --- /dev/null +++ b/src/report/mod.rs @@ -0,0 +1,5 @@ +// 报告模块 +pub mod html; +pub mod json; +pub mod model; +pub mod png; diff --git a/src/report/model.rs b/src/report/model.rs new file mode 100644 index 0000000..b751400 --- /dev/null +++ b/src/report/model.rs @@ -0,0 +1,17 @@ +// 报告数据模型:Finding 直接复用 inspect::Finding,避免两套重复类型 +pub use crate::inspect::Finding; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Report { + pub scan_id: String, + pub started_at: String, + pub finished_at: String, + pub machine: String, + pub user: String, + pub total: usize, + pub hit: usize, + pub findings: Vec, + pub samples: Vec, +} diff --git a/src/report/png.rs b/src/report/png.rs new file mode 100644 index 0000000..1bb0d91 --- /dev/null +++ b/src/report/png.rs @@ -0,0 +1,23 @@ +// 报告写入(PNG 组图):把所有截图拼成一张大图(简化版:每张截图保存为独立 PNG) +use std::path::Path; + +use crate::config::AppConfig; +use crate::report::model::Report; + +pub fn write(cfg: &AppConfig, r: &Report) -> anyhow::Result<()> { + if !cfg.report.formats.contains(&crate::config::ReportFormat::Png) { return Ok(()); } + if !cfg.report.include_screenshots { return Ok(()); } + let dir = &cfg.report.output_dir.join("screenshots"); + std::fs::create_dir_all(&dir)?; + // 把每张截图拷贝过去(它们本来就在 temp) + for (i, f) in r.findings.iter().enumerate() { + if let Some(src) = &f.screenshot { + let dst = dir.join(format!("{}-{}{}", i + 1, f.path.file_name().and_then(|s| s.to_str()).unwrap_or("file"), ".png")); + let _ = std::fs::copy(src, dst); + } + } + Ok(()) +} + +#[allow(dead_code)] +fn _path(p: &Path) { let _ = p; } diff --git a/src/scan/filter.rs b/src/scan/filter.rs new file mode 100644 index 0000000..b0c7b6b --- /dev/null +++ b/src/scan/filter.rs @@ -0,0 +1,44 @@ +// 过滤:白名单、隐藏、系统、扩展名、大小 +use std::path::Path; + +pub fn is_whitelisted(p: &Path, whitelist: &[std::path::PathBuf]) -> bool { + for w in whitelist { + if p.starts_with(w) { return true; } + } + false +} + +#[cfg(windows)] +pub fn is_hidden(p: &Path) -> bool { + use std::os::windows::fs::MetadataExt; + if let Ok(md) = p.metadata() { + const FILE_ATTRIBUTE_HIDDEN: u32 = 0x2; + return md.file_attributes() & FILE_ATTRIBUTE_HIDDEN != 0; + } + false +} +#[cfg(not(windows))] +pub fn is_hidden(p: &Path) -> bool { + p.file_name().and_then(|s| s.to_str()).map(|s| s.starts_with('.')).unwrap_or(false) +} + +#[cfg(windows)] +pub fn is_system(p: &Path) -> bool { + use std::os::windows::fs::MetadataExt; + if let Ok(md) = p.metadata() { + const FILE_ATTRIBUTE_SYSTEM: u32 = 0x4; + return md.file_attributes() & FILE_ATTRIBUTE_SYSTEM != 0; + } + false +} +#[cfg(not(windows))] +pub fn is_system(_p: &Path) -> bool { false } + +pub fn extension_allowed(p: &Path, allowlist: &[String]) -> bool { + if allowlist.is_empty() { return true; } + let ext = match p.extension().and_then(|s| s.to_str()) { + Some(e) => e.to_ascii_lowercase(), + None => return false, + }; + allowlist.iter().any(|a| a.eq_ignore_ascii_case(&ext)) +} diff --git a/src/scan/mod.rs b/src/scan/mod.rs new file mode 100644 index 0000000..36f692d --- /dev/null +++ b/src/scan/mod.rs @@ -0,0 +1,5 @@ +// 扫描模块 +pub mod filter; +pub mod runner; +pub mod sampler; +pub mod walker; diff --git a/src/scan/runner.rs b/src/scan/runner.rs new file mode 100644 index 0000000..58a1cd8 --- /dev/null +++ b/src/scan/runner.rs @@ -0,0 +1,239 @@ +// 抽检主流程:扫描所有候选 → 按配置抽样 → 串行 inspect → 报告 +use std::collections::HashMap; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Instant; + +use crate::app::RunState; +use crate::config::AppConfig; +use crate::inspect::{self, Finding, Inspector}; +use crate::report::model::Report; +use crate::scan::sampler::{self, FileKind, SampleItem}; +use crate::scan::walker; + +/// 调度入口 +#[allow(clippy::too_many_arguments)] +pub async fn run( + cfg: AppConfig, + progress: Arc, + total: Arc, + hit_count: Arc, + cancel: Arc, + scan_scanned: Arc, + scan_found: Arc, + scan_current_dir: Arc>, + current_file: Arc>>, + current_step: Arc>, + start_instant: Instant, + elapsed_ms: Arc, + type_done: Arc>>, + type_total: Arc>>, + log_lines: Arc>>, + samples_slot: Arc>>, + report_slot: Arc>>, + state_slot: Arc>, +) -> anyhow::Result<()> { + let push_log = { + let log_lines = Arc::clone(&log_lines); + move |s: String| { + tracing::info!("{}", s); + if let Ok(mut g) = log_lines.lock() { + g.push(s); + } + } + }; + let set_state = { + let state_slot = Arc::clone(&state_slot); + move |s: RunState| { + if let Ok(mut g) = state_slot.lock() { *g = s; } + } + }; + let set_current = { + let cur = Arc::clone(¤t_file); + move |p: Option| { if let Ok(mut g) = cur.lock() { *g = p; } } + }; + let set_step = { + let step = Arc::clone(¤t_step); + move |s: String| { if let Ok(mut g) = step.lock() { *g = s; } } + }; + + // 启动 Umi-OCR(若需要) + if let Err(e) = inspect::umi_ocr::ensure_started(&cfg.viewer) { + push_log(format!("⚠ Umi-OCR 启动失败(可继续但 OCR 会失败):{}", e)); + } + + // —— 阶段 1:扫描全盘所有候选文件 —— + set_state(RunState::Scanning); + set_step("🔍 阶段 1/3:正在扫描全盘候选文件……".into()); + push_log("═══ 阶段 1:扫描全盘所有候选文件 ═══".into()); + let scan_started = Instant::now(); + + // 进度回调:walker 每个目录+每个文件都会调 + let scan_scanned_cb = Arc::clone(&scan_scanned); + let scan_found_cb = Arc::clone(&scan_found); + let scan_dir_cb = Arc::clone(&scan_current_dir); + let mut on_progress = |_scanned: usize, found: usize, dir: &std::path::Path| { + scan_scanned_cb.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + scan_found_cb.store(found, std::sync::atomic::Ordering::Relaxed); + if let Ok(mut g) = scan_dir_cb.lock() { *g = dir.display().to_string(); } + }; + let candidates = walker::walk(&cfg.scan, &cancel, |s| push_log(s.to_string()), &mut on_progress); + let scan_ms = scan_started.elapsed().as_millis(); + let candidates_count = candidates.len(); + push_log(format!( + "✔ 扫描完成:访问 {} 个文件,命中候选 {} 份(用时 {} ms)", + scan_scanned.load(Ordering::Relaxed), + candidates_count, + scan_ms + )); + + if candidates_count == 0 { + push_log("⚠ 没有可抽检的文件,请检查扫描范围/白名单".into()); + set_state(RunState::Done); + return Ok(()); + } + + // —— 阶段 2:按配置抽样 —— + set_state(RunState::Sampling); + let mode_str = match cfg.inspect.sample_mode { + crate::config::SampleMode::Count => format!("取 {} 份", cfg.inspect.sample_count), + crate::config::SampleMode::Percent => format!("取 {}%", cfg.inspect.sample_percent), + crate::config::SampleMode::All => "取全部".to_string(), + }; + set_step(format!("🎲 阶段 2/3:正在抽样({})……", mode_str)); + push_log("═══ 阶段 2:抽样决策 ═══".into()); + push_log(format!(" 抽样模式:{}", mode_str)); + push_log(format!(" 候选总数:{} 份", candidates_count)); + let samples = sampler::sample(&candidates, &cfg.inspect); + + // 按类型统计抽样结果 + let mut by_kind_total: HashMap = HashMap::new(); + for it in &samples { + *by_kind_total.entry(it.kind).or_insert(0) += 1; + } + push_log(format!(" 最终抽检:{} 份", samples.len())); + for k in FileKind::all() { + if let Some(n) = by_kind_total.get(&k) { + if *n > 0 { + push_log(format!(" • {} :{} 份", k.as_str(), n)); + } + } + } + if let Ok(mut g) = type_total.lock() { *g = by_kind_total; } + if let Ok(mut g) = type_done.lock() { + g.clear(); + for k in FileKind::all() { g.entry(k).or_insert(0); } + } + if let Ok(mut g) = samples_slot.lock() { *g = samples.clone(); } + total.store(samples.len(), Ordering::Relaxed); + progress.store(0, Ordering::Relaxed); + hit_count.store(0, Ordering::Relaxed); + + // —— 阶段 3:抽检 —— + set_state(RunState::Inspecting); + let mut findings: Vec = Vec::new(); + let started_at = chrono::Local::now().to_rfc3339(); + for (i, item) in samples.iter().enumerate() { + if cancel.load(Ordering::Relaxed) { + push_log("⏹ 已取消".into()); + set_state(RunState::Cancelled); + break; + } + let p = item.path.display().to_string(); + set_current(Some(p.clone())); + let kind_str = item.kind.as_str(); + set_step(format!("🔬 阶段 3/3:正在抽检 {}/{}({})", i + 1, samples.len(), kind_str.to_uppercase())); + let size = std::fs::metadata(&item.path).map(|m| m.len()).unwrap_or(0); + push_log(format!("\n→ [{}/{}] ({}) {} ({} bytes)", i + 1, samples.len(), kind_str.to_uppercase(), p, size)); + let file_start = Instant::now(); + + // 选择 Inspector + let inspector: Box = match item.kind { + FileKind::Docx => Box::new(inspect::docx_inspector::DocxInspector), + FileKind::Doc => Box::new(inspect::doc_inspector::DocInspector::new(cfg.clone())), + FileKind::Pdf => Box::new(inspect::pdf_inspector::PdfInspector::new(cfg.clone())), + FileKind::Xlsx => Box::new(inspect::xlsx_inspector::XlsxInspector), + }; + + let log_closure = |s: &str| push_log(s.to_string()); + let fut = inspector.inspect(&item.path, &cfg, &cancel, &log_closure); + let res = match tokio::time::timeout(std::time::Duration::from_secs(cfg.inspect.per_file_timeout_sec), fut).await { + Ok(r) => r, + Err(_) => Err(anyhow::anyhow!("单文件超时(>{}s)", cfg.inspect.per_file_timeout_sec)), + }; + let file_ms = file_start.elapsed().as_millis(); + match res { + Ok(f) => { + if f.is_hit() { + let kws = f.matched.join("、"); + push_log(format!("✘ 命中:{}(用时 {} ms)", kws, file_ms)); + findings.push(f); + hit_count.fetch_add(1, Ordering::Relaxed); + if cfg.inspect.stop_on_first_hit { break; } + } else { + push_log(format!("✔ 未命中(用时 {} ms)", file_ms)); + } + } + Err(e) => push_log(format!("✘ 错误:{}(用时 {} ms)", e, file_ms)), + } + + // 更新每类完成计数 + if let Ok(mut g) = type_done.lock() { + *g.entry(item.kind).or_insert(0) += 1; + } + + progress.store(i + 1, Ordering::Relaxed); + elapsed_ms.store(start_instant.elapsed().as_millis() as u64, Ordering::Relaxed); + } + set_current(None); + set_step(String::new()); + + set_state(RunState::Reporting); + set_step("📝 正在生成报告……".into()); + let finished_at = chrono::Local::now().to_rfc3339(); + let machine = hostname(); + let user = std::env::var("USERNAME").unwrap_or_default(); + let report = Report { + scan_id: started_at.clone(), + started_at, + finished_at, + machine, + user, + total: samples.len(), + hit: findings.len(), + findings, + samples: samples.iter().map(|s| s.path.display().to_string()).collect(), + }; + + if let Err(e) = crate::report::html::write(&cfg, &report) { + push_log(format!("✘ 写 HTML 报告失败:{}", e)); + } else { + push_log("✔ 已写 HTML 报告".into()); + } + if let Err(e) = crate::report::json::write(&cfg, &report) { + push_log(format!("✘ 写 JSON 报告失败:{}", e)); + } else { + push_log("✔ 已写 JSON 报告".into()); + } + if let Err(e) = crate::report::png::write(&cfg, &report) { + push_log(format!("✘ 写 PNG 组图失败:{}", e)); + } else { + push_log("✔ 已写 PNG 组图".into()); + } + push_log(format!("📁 报告目录:{}", cfg.report.output_dir.display())); + if let Ok(mut g) = report_slot.lock() { *g = Some(report); } + + set_step(String::new()); + set_state(RunState::Done); + elapsed_ms.store(start_instant.elapsed().as_millis() as u64, Ordering::Relaxed); + Ok(()) +} + +fn hostname() -> String { + sysinfo::System::host_name().unwrap_or_else(|| "unknown".into()) +} + +// 供 Result/Err 引用避免 warning +#[allow(dead_code)] +fn _pathbuf_marker(_p: PathBuf) {} diff --git a/src/scan/sampler.rs b/src/scan/sampler.rs new file mode 100644 index 0000000..7157ab9 --- /dev/null +++ b/src/scan/sampler.rs @@ -0,0 +1,123 @@ +// 抽检:随机 / 分层 / 类型配额 +use std::path::PathBuf; + +use rand::seq::SliceRandom; +use rand::SeedableRng; + +use crate::config::{InspectSettings, SampleMode, SampleStrategy}; + +#[derive(Debug, Clone)] +pub struct SampleItem { + pub path: PathBuf, + pub kind: FileKind, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum FileKind { + Doc, + Docx, + Pdf, + Xlsx, +} + +impl FileKind { + pub fn from_path(p: &std::path::Path) -> Option { + match p.extension()?.to_str()?.to_ascii_lowercase().as_str() { + "doc" => Some(FileKind::Doc), + "docx" => Some(FileKind::Docx), + "pdf" => Some(FileKind::Pdf), + "xlsx" => Some(FileKind::Xlsx), + _ => None, + } + } + pub fn as_str(&self) -> &'static str { + match self { + FileKind::Doc => "doc", + FileKind::Docx => "docx", + FileKind::Pdf => "pdf", + FileKind::Xlsx => "xlsx", + } + } + pub fn all() -> [FileKind; 4] { + [FileKind::Doc, FileKind::Docx, FileKind::Pdf, FileKind::Xlsx] + } +} + +/// 根据 sample_mode + 全量候选数,计算最终抽样数 +fn compute_target(candidates: usize, s: &InspectSettings) -> usize { + if candidates == 0 { return 0; } + match s.sample_mode { + SampleMode::Count => s.sample_count.max(1).min(candidates), + SampleMode::Percent => { + let p = s.sample_percent.clamp(0.1, 100.0) as f64; + let n = ((candidates as f64) * p / 100.0).round() as usize; + n.max(1).min(candidates) + } + SampleMode::All => candidates, + } +} + +/// 统计各类型候选数(用于显示和配额上限) +pub fn count_by_kind(items: &[SampleItem]) -> std::collections::HashMap { + let mut m = std::collections::HashMap::new(); + for it in items { + *m.entry(it.kind).or_insert(0) += 1; + } + m +} + +/// 从候选路径中按策略抽样 +pub fn sample(candidates: &[PathBuf], s: &InspectSettings) -> Vec { + let items: Vec = candidates.iter() + .filter_map(|p| FileKind::from_path(p).map(|k| SampleItem { path: p.clone(), kind: k })) + .collect(); + if items.is_empty() { return items; } + + let mut rng = rand::rngs::StdRng::from_entropy(); + let target = compute_target(items.len(), s); + + match s.strategy { + SampleStrategy::Random => { + let mut pool = items.clone(); + pool.shuffle(&mut rng); + pool.into_iter().take(target).collect() + } + SampleStrategy::Stratified => { + // 按所在目录(父目录的前两段)分层 + use std::collections::BTreeMap; + let mut groups: BTreeMap> = BTreeMap::new(); + for it in items.iter() { + let key = it.path.ancestors().nth(2).map(|p| p.display().to_string()).unwrap_or_default(); + groups.entry(key).or_default().push(it.clone()); + } + let per_group = std::cmp::max(1, target / std::cmp::max(1, groups.len())); + let mut out: Vec = Vec::new(); + for (_, mut g) in groups { + g.shuffle(&mut rng); + out.extend(g.into_iter().take(per_group)); + } + out.shuffle(&mut rng); + out.into_iter().take(target).collect() + } + SampleStrategy::Quota => { + // 类型配额模式:每个类型按配额取 + let mut by_kind: std::collections::HashMap> = std::collections::HashMap::new(); + for it in items { by_kind.entry(it.kind).or_default().push(it); } + for v in by_kind.values_mut() { v.shuffle(&mut rng); } + let mut out: Vec = Vec::new(); + if s.doc_quota > 0 { if let Some(v) = by_kind.get_mut(&FileKind::Doc) { out.extend(v.iter().take(s.doc_quota).cloned()); v.drain(..s.doc_quota.min(v.len())); } } + if s.docx_quota > 0 { if let Some(v) = by_kind.get_mut(&FileKind::Docx) { out.extend(v.iter().take(s.docx_quota).cloned()); v.drain(..s.docx_quota.min(v.len())); } } + if s.pdf_quota > 0 { if let Some(v) = by_kind.get_mut(&FileKind::Pdf) { out.extend(v.iter().take(s.pdf_quota).cloned()); v.drain(..s.pdf_quota.min(v.len())); } } + if s.xlsx_quota > 0 { if let Some(v) = by_kind.get_mut(&FileKind::Xlsx) { out.extend(v.iter().take(s.xlsx_quota).cloned()); v.drain(..s.xlsx_quota.min(v.len())); } } + // 配额剩余用 Random 补齐到 target + if out.len() < target { + let mut rest: Vec = by_kind.into_values().flatten().collect(); + rest.shuffle(&mut rng); + let need = target - out.len(); + out.extend(rest.into_iter().take(need)); + } + out.shuffle(&mut rng); + out + } + } +} diff --git a/src/scan/walker.rs b/src/scan/walker.rs new file mode 100644 index 0000000..961b3dc --- /dev/null +++ b/src/scan/walker.rs @@ -0,0 +1,115 @@ +// 目录遍历:walkdir + 白名单剔除 + 扩展名/大小/隐藏/系统过滤 +use std::path::{Path, PathBuf}; +use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; + +use walkdir::WalkDir; + +use crate::config::ScanSettings; +use crate::scan::filter; + +/// 遍历过程中每次回调: +/// - scanned_so_far: 已扫描的文件总数(仅候选类型) +/// - found_so_far: 当前累计已收纳的候选数 +/// - current_dir: 当前正在扫描的目录路径 +pub type ProgressFn<'a> = &'a mut dyn FnMut(usize, usize, &Path); + +/// 遍历全盘,输出满足条件的文件路径 +pub fn walk( + s: &ScanSettings, + cancel: &AtomicBool, + log: impl Fn(&str), + progress: ProgressFn, +) -> Vec { + let mut out = Vec::new(); + let scanned = AtomicUsize::new(0); // 已访问的文件数(仅作信息) + let found = AtomicUsize::new(0); // 已收纳的候选数 + let last_dir_logged = std::sync::Mutex::new(String::new()); + let dirs_scanned = AtomicUsize::new(0); + + // 全盘根:从所有可用盘符的根开始 + let roots: Vec = if cfg!(windows) { + list_drive_letters() + } else { + vec![PathBuf::from("/")] + }; + log(&format!("🔍 扫描根:{:?}", roots)); + log(&format!(" 扩展名白名单:{:?}", s.extensions)); + log(&format!(" 白名单(不扫):{} 个目录", s.whitelist.len())); + + for root in roots { + if cancel.load(Ordering::Relaxed) { break; } + let walker = WalkDir::new(&root) + .follow_links(s.follow_symlinks) + .max_depth(if s.max_depth == 0 { usize::MAX } else { s.max_depth as usize }); + for entry in walker.into_iter().filter_map(Result::ok) { + if cancel.load(Ordering::Relaxed) { break; } + let p = entry.path(); + // 跳过目录 + if !p.is_file() { + if p.is_dir() { + // 每 30 个目录打印一次(避免刷屏) + let n = dirs_scanned.fetch_add(1, Ordering::Relaxed) + 1; + if n % 30 == 1 { + log(&format!(" 📁 进入目录:{}", p.display())); + if let Ok(mut last) = last_dir_logged.lock() { + *last = p.display().to_string(); + } + progress( + scanned.load(Ordering::Relaxed), + found.load(Ordering::Relaxed), + p, + ); + } + } + continue; + } + scanned.fetch_add(1, Ordering::Relaxed); + + if filter::is_whitelisted(p, &s.whitelist) { continue; } + if !s.include_hidden && filter::is_hidden(p) { continue; } + if !s.include_system && filter::is_system(p) { continue; } + if !filter::extension_allowed(p, &s.extensions) { continue; } + if s.min_size_kb > 0 { + if let Ok(m) = std::fs::metadata(p) { + if m.len() < s.min_size_kb * 1024 { continue; } + } + } + found.fetch_add(1, Ordering::Relaxed); + out.push(p.to_path_buf()); + } + } + + log(&format!( + "✔ 扫描完成:访问 {} 个文件,命中候选 {} 份", + scanned.load(Ordering::Relaxed), + out.len() + )); + out +} + +#[cfg(windows)] +fn list_drive_letters() -> Vec { + use windows_sys::Win32::Storage::FileSystem::GetLogicalDrives; + let mask = unsafe { GetLogicalDrives() }; + let mut out = Vec::new(); + for i in 0..26u32 { + if (mask >> i) & 1 == 1 { + let letter = (b'A' + i as u8) as char; + out.push(PathBuf::from(format!("{}:\\", letter))); + } + } + out +} + +#[cfg(not(windows))] +fn list_drive_letters() -> Vec { + vec![PathBuf::from("/")] +} + +/// 检测某路径是否在白名单中(不区分大小写,前缀匹配) +pub fn is_in_whitelist(p: &Path, whitelist: &[PathBuf]) -> bool { + for w in whitelist { + if p.starts_with(w) { return true; } + } + false +} diff --git a/src/ui/home.rs b/src/ui/home.rs new file mode 100644 index 0000000..5615922 --- /dev/null +++ b/src/ui/home.rs @@ -0,0 +1,316 @@ +// 主面板:开始检测 + 扫描/抽样/抽检三阶段进度 + 实时日志 +use std::collections::HashMap; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::time::Instant; + +use eframe::egui; + +use crate::app::{App, RunState}; +use crate::scan::sampler::FileKind; +use crate::ui::material; + +/// 主面板 +pub fn draw(ui: &mut egui::Ui, app: &mut App) { + // 1. 主控制 + material::group(ui, "主控制", |ui| { + ui.horizontal(|ui| { + let can_start = matches!(app.state, RunState::Idle | RunState::Done | RunState::Cancelled | RunState::Error(_)); + let can_cancel = matches!(app.state, RunState::Scanning | RunState::Sampling | RunState::Inspecting | RunState::Reporting); + + if ui.add_enabled(can_start, material::primary_button("▶ 开始检测")).clicked() { + start_inspection(app); + } + if ui.add_enabled(can_cancel, material::danger_button("⏹ 取消")).clicked() { + app.cancel_flag.store(true, Ordering::Relaxed); + app.state = RunState::Cancelled; + } + if ui.button("📂 打开报告目录").clicked() { + let _ = std::fs::create_dir_all(&app.config.report.output_dir); + let _ = open_in_explorer(&app.config.report.output_dir); + } + }); + }); + + ui.add_space(6.0); + + // 2. 状态卡片 + material::group(ui, "当前状态", |ui| { + let state_str = match &app.state { + RunState::Idle => "● 空闲".to_string(), + RunState::Scanning => "🔍 阶段 1/3:扫描全盘候选文件……".to_string(), + RunState::Sampling => "🎲 阶段 2/3:抽样决策……".to_string(), + RunState::Inspecting => "🔬 阶段 3/3:抽检文件……".to_string(), + RunState::Reporting => "📝 正在生成报告……".to_string(), + RunState::Done => "✔ 已完成".to_string(), + RunState::Cancelled => "⏹ 已取消".to_string(), + RunState::Error(e) => format!("✘ 出错:{}", e), + }; + ui.horizontal(|ui| { + ui.label(egui::RichText::new(&state_str).strong().size(16.0).color(material::PRIMARY_DARK)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + let hits = app.hit_count.load(Ordering::Relaxed); + let badge = egui::RichText::new(format!("✘ 命中 {} 项", hits)) + .strong().size(15.0) + .color(if hits > 0 { material::DANGER } else { material::ON_SURFACE_DIM }); + ui.label(badge); + }); + }); + if let Ok(step) = app.current_step.lock() { + if !step.is_empty() { + ui.label(egui::RichText::new(&*step).color(material::PRIMARY_DARK)); + } + } + }); + + ui.add_space(6.0); + + // 3. 阶段 1:扫描进度 + material::group(ui, "阶段 1/3:扫描全盘候选文件", |ui| { + let scanned = app.scan_scanned.load(Ordering::Relaxed); + let found = app.scan_found.load(Ordering::Relaxed); + ui.horizontal(|ui| { + ui.label(egui::RichText::new(format!("已访问 {} 个文件", scanned)).strong().size(15.0)); + ui.separator(); + ui.label(egui::RichText::new(format!("命中候选 {} 份", found)).strong().size(15.0).color(material::LIME)); + }); + // 不确定进度:扫描中时用动画效果 + let is_scanning = matches!(app.state, RunState::Scanning); + let frac = if is_scanning { 0.0 } else { 1.0 }; + let pb = egui::ProgressBar::new(frac) + .fill(material::PRIMARY) + .desired_width(ui.available_width()) + .desired_height(18.0) + .animate(is_scanning); + ui.add(pb); + + // 当前目录 + if let Ok(d) = app.scan_current_dir.lock() { + if !d.is_empty() { + ui.add_space(2.0); + ui.horizontal(|ui| { + ui.label("📁 正在扫描:"); + ui.label(egui::RichText::new(shorten_path(&d, 90)).monospace().color(material::ON_SURFACE)); + }); + } + } + }); + + ui.add_space(6.0); + + // 4. 阶段 2/3:抽样结果 + 抽检进度 + material::group(ui, "阶段 2/3:抽样结果 / 阶段 3/3:抽检", |ui| { + let p = app.progress.load(Ordering::Relaxed); + let t = app.total.load(Ordering::Relaxed); + let frac = if t == 0 { 0.0 } else { p as f32 / t as f32 }; + ui.horizontal(|ui| { + ui.label(egui::RichText::new(format!("已抽检 {}/{}", p, t)).strong().size(16.0)); + let elapsed_ms = app.elapsed_ms.load(Ordering::Relaxed); + if elapsed_ms > 0 { + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + ui.label(egui::RichText::new(format!("已用时 {}", format_duration(elapsed_ms))).color(material::ON_SURFACE_DIM)); + }); + } + }); + let is_inspecting = matches!(app.state, RunState::Inspecting | RunState::Sampling); + let pb = egui::ProgressBar::new(frac) + .show_percentage() + .fill(material::LIME) + .desired_width(ui.available_width()) + .desired_height(20.0) + .animate(is_inspecting); + ui.add(pb); + + // 类型分项进度(chips) + ui.add_space(4.0); + ui.horizontal_wrapped(|ui| { + let done_map: HashMap = app.type_done.lock().map(|g| g.clone()).unwrap_or_default(); + let total_map: HashMap = app.type_total.lock().map(|g| g.clone()).unwrap_or_default(); + for k in FileKind::all() { + let done = *done_map.get(&k).unwrap_or(&0); + let total = *total_map.get(&k).unwrap_or(&0); + if total == 0 && done == 0 { continue; } + let color = kind_color(k); + let label = format!("{} {}/{}", kind_label(k), done, total); + let chip = egui::RichText::new(label).strong().size(13.0).color(material::BACKGROUND); + let frame = egui::Frame::none() + .fill(color) + .rounding(egui::Rounding::same(10.0)) + .inner_margin(egui::Margin::symmetric(8.0, 3.0)); + frame.show(ui, |ui| { ui.label(chip); }); + } + }); + + // 当前正在处理 + if let Ok(cur) = app.current_file.lock() { + if let Some(f) = cur.as_ref() { + ui.add_space(4.0); + ui.horizontal(|ui| { + ui.label("📄 正在处理:"); + ui.label(egui::RichText::new(shorten_path(f, 100)).monospace().color(material::PRIMARY_DARK)); + }); + } + } + }); + + ui.add_space(6.0); + + // 5. 实时日志 + material::group(ui, "实时日志", |ui| { + egui::ScrollArea::vertical() + .stick_to_bottom(true) + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible) + .auto_shrink([false, false]) + .max_height(380.0) + .show(ui, |ui| { + if let Ok(lines) = app.log_lines.lock() { + for line in lines.iter() { + let (color, _) = classify_log(line); + ui.label(egui::RichText::new(line).monospace().color(color)); + } + } + }); + }); +} + +fn kind_label(k: FileKind) -> &'static str { + match k { + FileKind::Doc => "📄 DOC", + FileKind::Docx => "📝 DOCX", + FileKind::Pdf => "📕 PDF", + FileKind::Xlsx => "📊 XLSX", + } +} +fn kind_color(k: FileKind) -> egui::Color32 { + match k { + FileKind::Doc => egui::Color32::from_rgb(0, 150, 200), + FileKind::Docx => material::PRIMARY, + FileKind::Pdf => egui::Color32::from_rgb(255, 100, 100), + FileKind::Xlsx => material::LIME, + } +} + +fn shorten_path(s: &str, max_chars: usize) -> String { + if s.chars().count() <= max_chars { return s.to_string(); } + let keep = max_chars.saturating_sub(3) / 2; + let head: String = s.chars().take(keep).collect(); + let tail: String = s.chars().rev().take(keep).collect::().chars().rev().collect(); + format!("{}…{}", head, tail) +} + +fn format_duration(ms: u64) -> String { + let s = ms / 1000; + if s < 60 { format!("{} 秒", s) } + else if s < 3600 { format!("{} 分 {} 秒", s / 60, s % 60) } + else { format!("{} 时 {} 分", s / 3600, (s % 3600) / 60) } +} + +fn classify_log(line: &str) -> (egui::Color32, &'static str) { + if line.starts_with("═══") { + (material::LIME, "•") + } else if line.contains("命中") { + (material::DANGER, "✘") + } else if line.contains("未命中") || line.contains(" ✔") { + (material::SUCCESS, "✔") + } else if line.starts_with("⚠") || line.contains("警告") { + (material::WARNING, "⚠") + } else if line.starts_with("✘") || line.contains("错误") || line.contains("失败") { + (material::DANGER, "✘") + } else if line.starts_with("→") { + (material::PRIMARY_DARK, "→") + } else if line.starts_with("🔍") || line.starts_with("🎲") || line.starts_with("📁") || line.starts_with("📝") || line.starts_with("🔬") { + (material::PRIMARY_DARK, "•") + } else if line.starts_with("✔") { + (material::SUCCESS, "✔") + } else { + (material::ON_SURFACE, "•") + } +} + +/// 启动后台抽检任务 +fn start_inspection(app: &mut App) { + app.state = RunState::Scanning; + app.progress.store(0, Ordering::Relaxed); + app.total.store(0, Ordering::Relaxed); + app.hit_count.store(0, Ordering::Relaxed); + app.cancel_flag.store(false, Ordering::Relaxed); + app.elapsed_ms.store(0, Ordering::Relaxed); + app.scan_scanned.store(0, Ordering::Relaxed); + app.scan_found.store(0, Ordering::Relaxed); + if let Ok(mut s) = app.scan_current_dir.lock() { s.clear(); } + if let Ok(mut s) = app.current_step.lock() { s.clear(); } + if let Ok(mut cur) = app.current_file.lock() { cur.take(); } + if let Ok(mut lines) = app.log_lines.lock() { lines.clear(); } + if let Ok(mut s) = app.samples.lock() { s.clear(); } + if let Ok(mut d) = app.type_done.lock() { d.clear(); } + if let Ok(mut t) = app.type_total.lock() { t.clear(); } + + let progress = Arc::clone(&app.progress); + let total = Arc::clone(&app.total); + let hit_count = Arc::clone(&app.hit_count); + let cancel = Arc::clone(&app.cancel_flag); + let scan_scanned = Arc::clone(&app.scan_scanned); + let scan_found = Arc::clone(&app.scan_found); + let scan_dir = Arc::clone(&app.scan_current_dir); + let cur_file = Arc::clone(&app.current_file); + let cur_step = Arc::clone(&app.current_step); + let elapsed_ms = Arc::clone(&app.elapsed_ms); + let type_done = Arc::clone(&app.type_done); + let type_total = Arc::clone(&app.type_total); + let log_lines = Arc::clone(&app.log_lines); + let samples = Arc::clone(&app.samples); + let report_slot = Arc::clone(&app.report); + let state_slot = Arc::new(std::sync::Mutex::new(RunState::Scanning)); + let state_slot_for_task = Arc::clone(&state_slot); + let start_instant = Instant::now(); + let cfg = app.config.clone(); + + app.task_state = Some(state_slot.clone()); + app.task_log("开始抽检……"); + + std::thread::spawn(move || { + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("tokio runtime"); + let outcome = rt.block_on(async move { + crate::scan::runner::run( + cfg, + progress, + total, + hit_count, + cancel, + scan_scanned, + scan_found, + scan_dir, + cur_file, + cur_step, + start_instant, + elapsed_ms, + type_done, + type_total, + log_lines, + samples, + report_slot, + state_slot_for_task, + ) + .await + }); + tracing::info!("抽检任务结束:{:?}", outcome); + }); +} + +fn open_in_explorer(path: &std::path::Path) -> std::io::Result<()> { + #[cfg(windows)] + { + std::process::Command::new("explorer") + .arg(path) + .spawn() + .map(|_| ()) + } + #[cfg(not(windows))] + { + let _ = path; + Ok(()) + } +} diff --git a/src/ui/material.rs b/src/ui/material.rs new file mode 100644 index 0000000..229f168 --- /dev/null +++ b/src/ui/material.rs @@ -0,0 +1,205 @@ +// Material/Security 风格:颜色 token + 主题 + 字体安装 +use eframe::egui; +use eframe::egui::{FontData, FontDefinitions, FontFamily}; + +use crate::config::Theme; + +// —— 主色:鲜亮绿(深色底上的主色,类似安全软件主色) —— +pub const PRIMARY: egui::Color32 = egui::Color32::from_rgb(0, 230, 118); // #00E676 鲜绿 +pub const PRIMARY_DARK: egui::Color32 = egui::Color32::from_rgb(29, 233, 182); // #1DE9B6 薄荷 +pub const SECONDARY: egui::Color32 = egui::Color32::from_rgb(0, 176, 255); // #00B0FF 青蓝 +pub const SUCCESS: egui::Color32 = egui::Color32::from_rgb(0, 200, 83); // #00C853 +pub const WARNING: egui::Color32 = egui::Color32::from_rgb(255, 171, 0); // #FFAB00 +pub const DANGER: egui::Color32 = egui::Color32::from_rgb(255, 82, 82); // #FF5252 +pub const LIME: egui::Color32 = egui::Color32::from_rgb(118, 255, 3); // #76FF03 +pub const TEAL: egui::Color32 = egui::Color32::from_rgb(0, 191, 165); // #00BFA5 +// —— 暗色背景 —— +pub const SURFACE: egui::Color32 = egui::Color32::from_rgb(8, 40, 22); // #082816 卡片底 +pub const BACKGROUND: egui::Color32 = egui::Color32::from_rgb(0, 26, 13); // #001A0D 主底 +pub const BACKGROUND_ALT: egui::Color32 = egui::Color32::from_rgb(0, 46, 26); // #002E1A 渐变次底 +pub const ON_SURFACE: egui::Color32 = egui::Color32::from_rgb(185, 246, 202); // #B9F6CA 浅绿文字 +pub const ON_SURFACE_DIM: egui::Color32 = egui::Color32::from_rgb(120, 180, 140); // 暗一档 +pub const CARD_BORDER: egui::Color32 = egui::Color32::from_rgb(0, 191, 165); // #00BFA5 卡片边 + +/// 安装中文字体(如有指定路径或同目录字体文件) +pub fn install_fonts(ctx: &egui::Context, font_path: &Option) { + let mut fonts = FontDefinitions::default(); + let mut added = false; + + // 字体候选路径(顺序:用户指定 → exe 同目录 → 系统字体目录) + let mut candidates: Vec<(std::path::PathBuf, usize)> = Vec::new(); + if let Some(p) = font_path.as_ref() { + candidates.push((p.clone(), 0)); + } + let exe = crate::utils::paths::exe_dir(); + for name in [ + "SourceHanSansSC-Regular.otf", + "NotoSansCJKsc-Regular.otf", + "msyh.ttc", + "simhei.ttf", + "simsun.ttc", + ] { + candidates.push((exe.join(name), 0)); + } + // 系统字体目录(C:\Windows\Fonts) + let sys_fonts = std::path::PathBuf::from(r"C:\Windows\Fonts"); + for (name, idx) in [ + ("msyh.ttc", 0usize), // 微软雅黑 + ("msyhbd.ttc", 0), // 微软雅黑 Bold + ("simhei.ttf", 0), // 黑体 + ("simsun.ttc", 0), // 宋体 + ("simkai.ttf", 0), // 楷体 + ] { + candidates.push((sys_fonts.join(name), idx)); + } + + for (p, idx) in &candidates { + match std::fs::read(p) { + Ok(data) => { + tracing::info!("加载中文字体:{} (index={}, {} bytes)", p.display(), idx, data.len()); + let fd = if *idx == 0 { + FontData::from_owned(data) + } else { + FontData { + font: std::borrow::Cow::Owned(data), + index: *idx as u32, + tweak: Default::default(), + } + }; + fonts.font_data.insert("cn".into(), fd); + added = true; + break; + } + Err(_) => continue, + } + } + + if added { + if let Some(p) = fonts.families.get_mut(&FontFamily::Proportional) { + p.insert(0, "cn".into()); + } + if let Some(p) = fonts.families.get_mut(&FontFamily::Monospace) { + p.insert(0, "cn".into()); + } + } else { + tracing::warn!("未找到中文字体文件,UI 中文可能显示为方块;可把字体文件放到 exe 同目录或在设置中指定"); + } + + ctx.set_fonts(fonts); +} + +/// 应用主题 +pub fn apply_theme(ctx: &egui::Context, theme: Theme) { + let visuals = match theme { + Theme::Light => material_light(), + Theme::Dark => security_dark(), + Theme::Follow => security_dark(), // 默认走深色安全风格 + }; + ctx.set_visuals(visuals); +} + +/// Material 风格浅色主题(蓝主色 + 中性灰白底) +fn material_light() -> egui::Visuals { + let mut v = egui::Visuals::light(); + v.window_fill = egui::Color32::from_rgb(255, 255, 255); + v.panel_fill = egui::Color32::from_rgb(250, 250, 250); + v.extreme_bg_color = egui::Color32::from_rgb(240, 244, 250); + v.faint_bg_color = egui::Color32::from_rgb(245, 247, 250); + v.widgets.noninteractive.bg_fill = egui::Color32::from_rgb(245, 247, 250); + v.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, egui::Color32::from_rgb(220, 224, 230)); + v.widgets.inactive.bg_fill = egui::Color32::from_rgb(250, 250, 250); + v.widgets.inactive.bg_stroke = egui::Stroke::new(1.0, egui::Color32::from_rgb(220, 224, 230)); + v.widgets.hovered.bg_fill = egui::Color32::from_rgb(235, 243, 255); + v.widgets.hovered.bg_stroke = egui::Stroke::new(1.0, PRIMARY); + v.widgets.active.bg_fill = egui::Color32::from_rgb(205, 230, 255); + v.widgets.active.bg_stroke = egui::Stroke::new(1.5, PRIMARY_DARK); + v.widgets.noninteractive.fg_stroke = egui::Stroke::new(1.0, egui::Color32::from_rgb(60, 60, 70)); + v.widgets.inactive.fg_stroke = egui::Stroke::new(1.0, egui::Color32::from_rgb(50, 50, 60)); + v.selection.bg_fill = egui::Color32::from_rgb(180, 215, 255); + v.selection.stroke = egui::Stroke::new(1.0, PRIMARY); + v.hyperlink_color = PRIMARY; + v.widgets.noninteractive.rounding = egui::Rounding::same(6.0); + v.widgets.inactive.rounding = egui::Rounding::same(6.0); + v.widgets.hovered.rounding = egui::Rounding::same(6.0); + v.widgets.active.rounding = egui::Rounding::same(6.0); + v.window_rounding = egui::Rounding::same(8.0); + v +} + +/// 安全软件风格深色主题:暗绿底 + 鲜绿/青色强调 +fn security_dark() -> egui::Visuals { + let mut v = egui::Visuals::dark(); + // 整体色调:暗绿 + v.window_fill = BACKGROUND; + v.panel_fill = BACKGROUND_ALT; + v.extreme_bg_color = egui::Color32::from_rgb(0, 18, 8); + v.faint_bg_color = SURFACE; + + // 控件底色(卡片用 SURFACE,控件用略亮) + v.widgets.noninteractive.bg_fill = SURFACE; + v.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, CARD_BORDER); + v.widgets.inactive.bg_fill = egui::Color32::from_rgb(12, 50, 28); + v.widgets.inactive.bg_stroke = egui::Stroke::new(1.0, egui::Color32::from_rgb(0, 150, 130)); + v.widgets.hovered.bg_fill = egui::Color32::from_rgb(20, 90, 50); + v.widgets.hovered.bg_stroke = egui::Stroke::new(1.0, PRIMARY); + v.widgets.active.bg_fill = egui::Color32::from_rgb(0, 120, 70); + v.widgets.active.bg_stroke = egui::Stroke::new(1.5, LIME); + + // 文字色 + v.widgets.noninteractive.fg_stroke = egui::Stroke::new(1.0, ON_SURFACE); + v.widgets.inactive.fg_stroke = egui::Stroke::new(1.0, ON_SURFACE); + v.widgets.hovered.fg_stroke = egui::Stroke::new(1.0, egui::Color32::WHITE); + v.widgets.active.fg_stroke = egui::Stroke::new(1.0, egui::Color32::WHITE); + + // 文本默认色 + v.override_text_color = Some(ON_SURFACE); + + // 选中色 + v.selection.bg_fill = egui::Color32::from_rgb(0, 150, 80); + v.selection.stroke = egui::Stroke::new(1.0, LIME); + + // 链接 + v.hyperlink_color = PRIMARY_DARK; + + // 圆角 + v.widgets.noninteractive.rounding = egui::Rounding::same(8.0); + v.widgets.inactive.rounding = egui::Rounding::same(8.0); + v.widgets.hovered.rounding = egui::Rounding::same(8.0); + v.widgets.active.rounding = egui::Rounding::same(8.0); + + v.window_rounding = egui::Rounding::same(10.0); + + v +} + +/// 创建一个安全软件风格主按钮(绿渐变 + 圆角胶囊) +pub fn primary_button(text: &str) -> egui::Button<'_> { + egui::Button::new(egui::RichText::new(text).color(BACKGROUND).strong().size(15.0)) + .fill(PRIMARY) + .stroke(egui::Stroke::new(1.0, LIME)) + .rounding(egui::Rounding::same(18.0)) + .min_size(egui::vec2(120.0, 36.0)) +} + +/// 创建一个危险按钮(红色) +pub fn danger_button(text: &str) -> egui::Button<'_> { + egui::Button::new(egui::RichText::new(text).color(egui::Color32::WHITE).strong()) + .fill(DANGER) + .rounding(egui::Rounding::same(18.0)) + .min_size(egui::vec2(100.0, 36.0)) +} + +/// 一个安全软件风格分组面板:暗底 + 青色边 +pub fn group(ui: &mut egui::Ui, title: &str, add_contents: impl FnOnce(&mut egui::Ui) -> R) -> R { + egui::Frame::group(ui.style()) + .rounding(egui::Rounding::same(10.0)) + .inner_margin(egui::Margin::same(14.0)) + .fill(SURFACE) + .stroke(egui::Stroke::new(1.0, CARD_BORDER)) + .show(ui, |ui| { + ui.label(egui::RichText::new(title).strong().size(15.0).color(PRIMARY_DARK)); + ui.add_space(6.0); + add_contents(ui) + }) + .inner +} diff --git a/src/ui/mod.rs b/src/ui/mod.rs new file mode 100644 index 0000000..132b215 --- /dev/null +++ b/src/ui/mod.rs @@ -0,0 +1,6 @@ +// UI 模块 +pub mod home; +pub mod material; +pub mod report; +pub mod settings; +pub mod widgets; diff --git a/src/ui/report.rs b/src/ui/report.rs new file mode 100644 index 0000000..8f76c6c --- /dev/null +++ b/src/ui/report.rs @@ -0,0 +1,40 @@ +// 报告查看页 +use eframe::egui; + +use crate::report::model::Report; +use crate::ui::material; + +pub fn draw(ui: &mut egui::Ui, r: &Report) { + material::group(ui, "本次抽检", |ui| { + ui.label(format!("检查时间:{} ~ {}", r.started_at, r.finished_at)); + ui.label(format!("抽检:{},命中:{}", r.total, r.hit)); + ui.label(format!("机器:{},用户:{}", r.machine, r.user)); + }); + + ui.add_space(8.0); + + material::group(ui, "命中清单", |ui| { + egui::ScrollArea::vertical().max_height(400.0).show(ui, |ui| { + egui::Grid::new("findings").striped(true).show(ui, |ui| { + ui.strong("文件"); + ui.strong("类型"); + ui.strong("命中关键词"); + ui.strong("置信度"); + ui.strong("截图"); + ui.end_row(); + for f in &r.findings { + ui.label(f.path.display().to_string()); + ui.label(&f.kind); + ui.label(f.matched.join(", ")); + ui.label(format!("{:.2}", f.confidence)); + if let Some(s) = &f.screenshot { + ui.label(s.display().to_string()); + } else { + ui.label("-"); + } + ui.end_row(); + } + }); + }); + }); +} diff --git a/src/ui/settings.rs b/src/ui/settings.rs new file mode 100644 index 0000000..a44763b --- /dev/null +++ b/src/ui/settings.rs @@ -0,0 +1,314 @@ +// 设置页:A-F 六分组 +use eframe::egui; + +use crate::config::{AppConfig, KeywordSettings, LogLevel, ReportFormat, SampleMode, SampleStrategy, ScreenshotMode, Theme}; +use crate::ui::{material, widgets}; + +/// 绘制设置页 +pub fn draw(ui: &mut egui::Ui, cfg: &mut AppConfig) { + egui::ScrollArea::vertical() + .scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysVisible) + .auto_shrink([false, false]) + .show(ui, |ui| { + group_a_general(ui, &mut cfg.general); + ui.add_space(8.0); + group_b_scan(ui, &mut cfg.scan); + ui.add_space(8.0); + group_c_inspect(ui, &mut cfg.inspect); + ui.add_space(8.0); + group_d_viewer(ui, &mut cfg.viewer); + ui.add_space(8.0); + group_e_keyword(ui, &mut cfg.keyword); + ui.add_space(8.0); + group_f_report(ui, &mut cfg.report); + ui.add_space(8.0); + material::group(ui, "操作", |ui| { + ui.horizontal(|ui| { + if ui.button("💾 立即保存").clicked() { + if let Err(e) = cfg.save() { + tracing::warn!("保存失败:{}", e); + } + } + if ui.button("🔄 恢复默认").clicked() { + *cfg = AppConfig::default(); + } + if ui.button("📂 打开配置文件").clicked() { + let path = crate::config::config_path(); + let _ = std::fs::create_dir_all(path.parent().unwrap()); + let _ = open_in_explorer(&path); + } + }); + }); + ui.add_space(20.0); // 底部留白,避免被裁切 + }); +} + +fn group_a_general(ui: &mut egui::Ui, g: &mut crate::config::GeneralSettings) { + material::group(ui, "A. 常规", |ui| { + ui.checkbox(&mut g.auto_run, "启动时自动开始检测"); + ui.checkbox(&mut g.start_minimized, "启动时最小化到托盘"); + ui.checkbox(&mut g.auto_start, "开机自启"); + ui.horizontal(|ui| { + ui.label("主题:"); + egui::ComboBox::from_id_source("theme") + .selected_text(match g.theme { Theme::Light => "Light", Theme::Dark => "Dark", Theme::Follow => "Follow System" }) + .show_ui(ui, |ui| { + ui.selectable_value(&mut g.theme, Theme::Light, "Light"); + ui.selectable_value(&mut g.theme, Theme::Dark, "Dark"); + ui.selectable_value(&mut g.theme, Theme::Follow, "Follow System"); + }); + }); + ui.horizontal(|ui| { + ui.label("日志级别:"); + egui::ComboBox::from_id_source("log_level") + .selected_text(format!("{:?}", g.log_level)) + .show_ui(ui, |ui| { + for l in [LogLevel::Error, LogLevel::Warn, LogLevel::Info, LogLevel::Debug, LogLevel::Trace] { + ui.selectable_value(&mut g.log_level, l, format!("{:?}", l)); + } + }); + }); + ui.horizontal(|ui| { + ui.label("日志保留天数:"); + ui.add(egui::DragValue::new(&mut g.log_retention_days).clamp_range(1..=365)); + }); + ui.checkbox(&mut g.single_instance, "单实例锁"); + ui.checkbox(&mut g.clear_temp_on_start, "启动时清空临时目录"); + ui.checkbox(&mut g.auto_exit, "检查完自动退出"); + if g.auto_exit { + ui.horizontal(|ui| { + ui.label("倒计时(秒):"); + ui.add(egui::DragValue::new(&mut g.auto_exit_seconds).clamp_range(1..=600)); + }); + } + }); +} + +fn group_b_scan(ui: &mut egui::Ui, s: &mut crate::config::ScanSettings) { + material::group(ui, "B. 扫描范围(默认全盘 + 白名单 = 不扫)", |ui| { + ui.label("白名单目录(不扫描):"); + // 先把 s.whitelist 转 Vec 给 string_list_editor + let mut wl_strs: Vec = s.whitelist.iter().map(|p| p.display().to_string()).collect(); + widgets::string_list_editor(ui, &mut wl_strs, r"例如 C:\Windows"); + // 写回 + s.whitelist = wl_strs.iter().map(|x| std::path::PathBuf::from(x)).collect(); + ui.horizontal(|ui| { + if ui.button("➕ 系统目录").clicked() { + s.whitelist.push(std::path::PathBuf::from(r"C:\Windows")); + } + if ui.button("➕ Program Files").clicked() { + s.whitelist.push(std::path::PathBuf::from(r"C:\Program Files")); + } + if ui.button("➕ ProgramData").clicked() { + s.whitelist.push(std::path::PathBuf::from(r"C:\ProgramData")); + } + if ui.button("➕ 回收站").clicked() { + s.whitelist.push(std::path::PathBuf::from(r"C:\$Recycle.Bin")); + } + }); + // 把编辑框的字符串结果写回 s.whitelist + // (简化:用 text 列表同步一次 —— 实际可用 table state;这里临时方案) + // 提示:上面的 string_list_editor 改的是临时 Vec,需要保留后再写回。 + // 简单起见:使用一个固定 placeholder + ui.checkbox(&mut s.include_hidden, "包含隐藏文件"); + ui.checkbox(&mut s.include_system, "包含系统文件"); + ui.checkbox(&mut s.follow_symlinks, "跟随符号链接"); + ui.horizontal(|ui| { + ui.label("文件最小大小 KB:"); + ui.add(egui::DragValue::new(&mut s.min_size_kb).clamp_range(0..=1024 * 1024)); + }); + ui.horizontal(|ui| { + ui.label("最大遍历深度(0=无限):"); + ui.add(egui::DragValue::new(&mut s.max_depth).clamp_range(0..=1000)); + }); + ui.label("扩展名白名单(不含点,逗号分隔):"); + let mut ext_str = s.extensions.join(","); + if ui.text_edit_singleline(&mut ext_str).changed() { + s.extensions = ext_str.split(',').map(|x| x.trim().to_string()).filter(|x| !x.is_empty()).collect(); + } + ui.horizontal(|ui| { + ui.label("单次扫描超时(分钟,0=无限):"); + ui.add(egui::DragValue::new(&mut s.scan_timeout_minutes).clamp_range(0..=24 * 60)); + }); + }); +} + +fn group_c_inspect(ui: &mut egui::Ui, i: &mut crate::config::InspectSettings) { + material::group(ui, "C. 抽检(先扫全盘,再按下方配置抽样检查)", |ui| { + // 抽样数量模式 + ui.horizontal(|ui| { + ui.label("抽样模式:"); + egui::ComboBox::from_id_source("sample_mode") + .selected_text(match i.sample_mode { + SampleMode::Count => "📦 按份数", + SampleMode::Percent => "📊 按百分比", + SampleMode::All => "🌐 全部", + }) + .show_ui(ui, |ui| { + ui.selectable_value(&mut i.sample_mode, SampleMode::Count, "📦 按份数"); + ui.selectable_value(&mut i.sample_mode, SampleMode::Percent, "📊 按百分比"); + ui.selectable_value(&mut i.sample_mode, SampleMode::All, "🌐 全部"); + }); + }); + ui.horizontal(|ui| { + match i.sample_mode { + SampleMode::Count => { + ui.label("抽检份数:"); + ui.add(egui::DragValue::new(&mut i.sample_count).clamp_range(1..=100_000)); + } + SampleMode::Percent => { + ui.label("百分比:"); + ui.add(egui::DragValue::new(&mut i.sample_percent).clamp_range(0.1..=100.0).speed(0.5)); + ui.label("%"); + } + SampleMode::All => { ui.label("(将抽检全部候选文件)"); } + } + }); + ui.horizontal(|ui| { + ui.label("抽样策略:"); + egui::ComboBox::from_id_source("strategy") + .selected_text(match i.strategy { SampleStrategy::Random => "完全随机", SampleStrategy::Stratified => "分层", SampleStrategy::Quota => "类型配额" }) + .show_ui(ui, |ui| { + ui.selectable_value(&mut i.strategy, SampleStrategy::Random, "完全随机"); + ui.selectable_value(&mut i.strategy, SampleStrategy::Stratified, "分层"); + ui.selectable_value(&mut i.strategy, SampleStrategy::Quota, "类型配额"); + }); + }); + ui.horizontal(|ui| { + ui.label("DOC 配额:"); + ui.add(egui::DragValue::new(&mut i.doc_quota).clamp_range(0..=100_000)); + ui.label("DOCX 配额:"); + ui.add(egui::DragValue::new(&mut i.docx_quota).clamp_range(0..=100_000)); + ui.label("PDF 配额:"); + ui.add(egui::DragValue::new(&mut i.pdf_quota).clamp_range(0..=100_000)); + ui.label("XLSX 配额:"); + ui.add(egui::DragValue::new(&mut i.xlsx_quota).clamp_range(0..=100_000)); + }); + ui.horizontal(|ui| { + ui.label("单文件总超时(秒):"); + ui.add(egui::DragValue::new(&mut i.per_file_timeout_sec).clamp_range(10..=3600)); + }); + ui.horizontal(|ui| { + ui.label("同文件 N 天内不重复:"); + ui.add(egui::DragValue::new(&mut i.dedup_days).clamp_range(0..=365)); + }); + ui.checkbox(&mut i.skip_locked, "跳过锁定/无权限文件"); + ui.checkbox(&mut i.stop_on_first_hit, "命中后停止后续抽检"); + }); +} + +fn group_d_viewer(ui: &mut egui::Ui, v: &mut crate::config::ViewerSettings) { + material::group(ui, "D. 查看器与截图", |ui| { + ui.horizontal(|ui| { + ui.label("截图模式:"); + egui::ComboBox::from_id_source("screenshot_mode") + .selected_text(match v.screenshot_mode { + ScreenshotMode::Manual => "Manual(Win+Shift+S + 手动框选)", + ScreenshotMode::AutoPrintWindow => "Auto(PrintWindow 全自动)", + ScreenshotMode::AutoWithFallback => "Auto + 手动兜底", + }) + .show_ui(ui, |ui| { + ui.selectable_value(&mut v.screenshot_mode, ScreenshotMode::Manual, "Manual(Win+Shift+S + 手动框选)"); + ui.selectable_value(&mut v.screenshot_mode, ScreenshotMode::AutoPrintWindow, "Auto(PrintWindow 全自动)"); + ui.selectable_value(&mut v.screenshot_mode, ScreenshotMode::AutoWithFallback, "Auto + 手动兜底"); + }); + }); + widgets::file_picker(ui, ".doc 查看器(默认 exe 同目录 doclite.exe)", &mut v.doc_viewer, &["exe"]); + ui.horizontal(|ui| { ui.label("启动参数模板:"); ui.text_edit_singleline(&mut v.doc_args); }); + widgets::file_picker(ui, ".pdf 查看器(默认 Windows 关联)", &mut v.pdf_viewer, &["exe"]); + ui.horizontal(|ui| { ui.label("启动参数模板:"); ui.text_edit_singleline(&mut v.pdf_args); }); + ui.horizontal(|ui| { ui.label("截图前等待时间 ms:"); ui.add(egui::DragValue::new(&mut v.pre_capture_wait_ms).clamp_range(0..=30_000)); }); + ui.horizontal(|ui| { ui.label("手动截图超时秒:"); ui.add(egui::DragValue::new(&mut v.manual_capture_timeout_sec).clamp_range(5..=600)); }); + ui.horizontal(|ui| { ui.label("自动模式空位图判定阈值 (max_black_ratio):"); ui.add(egui::DragValue::new(&mut v.max_black_ratio).clamp_range(0.0..=1.0)); }); + ui.checkbox(&mut v.auto_close_after, "截图后自动关闭查看器"); + ui.horizontal(|ui| { ui.label("关闭等待 ms:"); ui.add(egui::DragValue::new(&mut v.close_wait_ms).clamp_range(0..=30_000)); }); + ui.horizontal(|ui| { ui.label("强杀超时 ms:"); ui.add(egui::DragValue::new(&mut v.kill_timeout_ms).clamp_range(0..=30_000)); }); + ui.add_space(6.0); + ui.label(egui::RichText::new("Umi-OCR").strong()); + ui.horizontal(|ui| { ui.label("HTTP 地址:"); ui.text_edit_singleline(&mut v.umi_ocr_url); }); + widgets::file_picker(ui, "Umi-OCR.exe 路径(默认 exe 同目录)", &mut v.umi_ocr_exe, &["exe"]); + ui.horizontal(|ui| { ui.label("启动后等待秒:"); ui.add(egui::DragValue::new(&mut v.umi_ocr_startup_wait_sec).clamp_range(0..=60)); }); + ui.horizontal(|ui| { ui.label("调用超时秒:"); ui.add(egui::DragValue::new(&mut v.umi_ocr_call_timeout_sec).clamp_range(5..=600)); }); + ui.horizontal(|ui| { ui.label("OCR 语言:"); ui.text_edit_singleline(&mut v.ocr_language); }); + ui.checkbox(&mut v.ocr_cls, "启用文本方向校正"); + ui.horizontal(|ui| { ui.label("限制边长 px:"); ui.add(egui::DragValue::new(&mut v.ocr_limit_side_len).clamp_range(0..=99999)); }); + }); +} + +fn group_e_keyword(ui: &mut egui::Ui, k: &mut KeywordSettings) { + material::group(ui, "E. 关键词", |ui| { + ui.label("全局关键词(每行一个):"); + widgets::string_list_editor(ui, &mut k.global, "新增关键词"); + ui.horizontal(|ui| { + if ui.button("📥 导入 txt").clicked() { + if let Some(p) = rfd::FileDialog::new().add_filter("text", &["txt"]).pick_file() { + if let Ok(s) = std::fs::read_to_string(&p) { + k.global = s.lines().map(|l| l.trim().to_string()).filter(|l| !l.is_empty()).collect(); + } + } + } + if ui.button("📤 导出 txt").clicked() { + if let Some(p) = rfd::FileDialog::new().add_filter("text", &["txt"]).save_file() { + let _ = std::fs::write(p, k.global.join("\n")); + } + } + }); + ui.checkbox(&mut k.case_sensitive, "区分大小写"); + ui.checkbox(&mut k.use_regex, "使用正则"); + ui.checkbox(&mut k.whole_word, "整词匹配"); + ui.label("DOC 追加关键词:"); + widgets::string_list_editor(ui, &mut k.doc_extra, "新增"); + ui.label("DOCX 追加关键词:"); + widgets::string_list_editor(ui, &mut k.docx_extra, "新增"); + ui.label("PDF 追加关键词:"); + widgets::string_list_editor(ui, &mut k.pdf_extra, "新增"); + ui.label("XLSX 追加关键词:"); + widgets::string_list_editor(ui, &mut k.xlsx_extra, "新增"); + ui.horizontal(|ui| { ui.label("最低置信度:"); ui.add(egui::DragValue::new(&mut k.min_confidence).clamp_range(0.0..=1.0)); }); + ui.label("误报白名单指纹(每行一个,可清除):"); + widgets::string_list_editor(ui, &mut k.false_positive_fingerprints, "新增"); + }); +} + +fn group_f_report(ui: &mut egui::Ui, r: &mut crate::config::ReportSettings) { + material::group(ui, "F. 报告", |ui| { + widgets::folder_picker(ui, "输出目录:", &mut Some(r.output_dir.clone())); + ui.label("输出格式:"); + ui.horizontal(|ui| { + // 用三个 bool 缓冲切换 Vec 的成员 + let mut has_html = r.formats.contains(&ReportFormat::Html); + let mut has_json = r.formats.contains(&ReportFormat::Json); + let mut has_png = r.formats.contains(&ReportFormat::Png); + if ui.checkbox(&mut has_html, "HTML").changed() { + toggle_format(&mut r.formats, ReportFormat::Html, has_html); + } + if ui.checkbox(&mut has_json, "JSON").changed() { + toggle_format(&mut r.formats, ReportFormat::Json, has_json); + } + if ui.checkbox(&mut has_png, "PNG 组图").changed() { + toggle_format(&mut r.formats, ReportFormat::Png, has_png); + } + }); + ui.horizontal(|ui| { ui.label("文件名前缀:"); ui.text_edit_singleline(&mut r.file_prefix); }); + ui.checkbox(&mut r.include_screenshots, "包含截图"); + ui.checkbox(&mut r.highlight_sensitive, "敏感词高亮"); + ui.horizontal(|ui| { ui.label("截图最大边长 px:"); ui.add(egui::DragValue::new(&mut r.max_screenshot_side).clamp_range(320..=8192)); }); + ui.horizontal(|ui| { ui.label("历史保留条数:"); ui.add(egui::DragValue::new(&mut r.history_keep).clamp_range(1..=1000)); }); + ui.checkbox(&mut r.auto_open, "检查完自动打开报告"); + ui.checkbox(&mut r.copy_summary_to_clipboard, "复制摘要到剪贴板"); + }); +} + +fn open_in_explorer(path: &std::path::Path) -> std::io::Result<()> { + #[cfg(windows)] + { std::process::Command::new("explorer").arg(path).spawn().map(|_| ()) } + #[cfg(not(windows))] + { let _ = path; Ok(()) } +} + +/// 切换 formats 列表中的某一项 +fn toggle_format(list: &mut Vec, fmt: ReportFormat, on: bool) { + let has = list.contains(&fmt); + if on && !has { list.push(fmt); } + if !on && has { list.retain(|f| *f != fmt); } +} diff --git a/src/ui/widgets.rs b/src/ui/widgets.rs new file mode 100644 index 0000000..a5e2bee --- /dev/null +++ b/src/ui/widgets.rs @@ -0,0 +1,68 @@ +// 通用 widget:路径选择器、列表编辑器等 +use std::path::PathBuf; + +use eframe::egui; + +/// 文件夹选择按钮:点击后调用 native picker;选完后把路径写入 `target` +pub fn folder_picker(ui: &mut egui::Ui, label: &str, target: &mut Option) { + ui.horizontal(|ui| { + ui.label(label); + let display = target.as_ref().map(|p| p.display().to_string()).unwrap_or_else(|| "<未选择>".to_string()); + ui.label(egui::RichText::new(&display).monospace()); + if ui.button("📁").clicked() { + if let Some(p) = rfd::FileDialog::new().pick_folder() { + *target = Some(p); + } + } + if ui.button("清除").clicked() { + *target = None; + } + }); +} + +/// 文件选择按钮 +pub fn file_picker(ui: &mut egui::Ui, label: &str, target: &mut Option, extensions: &[&str]) { + ui.horizontal(|ui| { + ui.label(label); + let display = target.as_ref().map(|p| p.display().to_string()).unwrap_or_else(|| "<未选择>".to_string()); + ui.label(egui::RichText::new(&display).monospace()); + if ui.button("📄").clicked() { + let mut d = rfd::FileDialog::new(); + for ext in extensions { + // 把 &&str 解引用为 &str,rfd 才会接受 + d = d.add_filter(*ext, &[*ext]); + } + if let Some(p) = d.pick_file() { + *target = Some(p); + } + } + if ui.button("清除").clicked() { + *target = None; + } + }); +} + +/// 列表编辑器:每一项是 String,下方一个添加按钮、一个删除按钮 +pub fn string_list_editor(ui: &mut egui::Ui, items: &mut Vec, hint: &str) { + let mut to_remove: Option = None; + for (i, item) in items.iter_mut().enumerate() { + ui.horizontal(|ui| { + ui.label(format!("{}.", i + 1)); + ui.text_edit_singleline(item); + if ui.button("✕").clicked() { + to_remove = Some(i); + } + }); + } + if let Some(i) = to_remove { + items.remove(i); + } + ui.horizontal(|ui| { + if ui.button("➕ 添加").clicked() { + items.push(hint.to_string()); + } + if ui.button("🗑 清空").clicked() { + items.clear(); + } + }); +} diff --git a/src/utils/logger.rs b/src/utils/logger.rs new file mode 100644 index 0000000..3ecd92b --- /dev/null +++ b/src/utils/logger.rs @@ -0,0 +1,25 @@ +// 日志初始化:tracing + 文件输出 +use std::fs; +use tracing_subscriber::{fmt, prelude::*, EnvFilter}; + +pub fn init_logger() { + let log_dir = crate::utils::paths::log_dir(); + let _ = fs::create_dir_all(&log_dir); + + let file_appender = tracing_appender::rolling::daily(&log_dir, "selfcheck.log"); + let (file_writer, _guard) = tracing_appender::non_blocking(file_appender); + + // 保留 guard 防止文件写入线程退出 + Box::leak(Box::new(_guard)); + + let env_filter = EnvFilter::try_from_default_env() + .unwrap_or_else(|_| EnvFilter::new("info,secret_file_selfcheck=debug")); + + tracing_subscriber::registry() + .with(env_filter) + .with(fmt::layer().with_writer(file_writer).with_ansi(false)) + .with(fmt::layer().with_writer(std::io::stdout)) + .init(); + + tracing::info!("日志系统初始化完成;日志目录:{}", log_dir.display()); +} diff --git a/src/utils/mod.rs b/src/utils/mod.rs new file mode 100644 index 0000000..49fce55 --- /dev/null +++ b/src/utils/mod.rs @@ -0,0 +1,3 @@ +// 工具模块入口 +pub mod logger; +pub mod paths; diff --git a/src/utils/paths.rs b/src/utils/paths.rs new file mode 100644 index 0000000..732d2ad --- /dev/null +++ b/src/utils/paths.rs @@ -0,0 +1,53 @@ +// 路径工具:APPDATA、exe 同目录、临时目录 +use std::path::PathBuf; + +/// 应用配置目录:%APPDATA%\secret-file-selfcheck\ +pub fn app_config_dir() -> PathBuf { + if let Some(base) = dirs::config_dir() { + base.join("secret-file-selfcheck") + } else { + PathBuf::from(".") + } +} + +/// 配置文件:%APPDATA%\secret-file-selfcheck\config.toml +pub fn config_file() -> PathBuf { + app_config_dir().join("config.toml") +} + +/// 日志目录:%APPDATA%\secret-file-selfcheck\logs\ +pub fn log_dir() -> PathBuf { + app_config_dir().join("logs") +} + +/// exe 所在目录(用于探测同目录的 doclite.exe / Umi-OCR.exe / 字体) +pub fn exe_dir() -> PathBuf { + std::env::current_exe() + .ok() + .and_then(|p| p.parent().map(|p| p.to_path_buf())) + .unwrap_or_else(|| PathBuf::from(".")) +} + +/// 临时目录:%TEMP%\secret-scan-XXXX\ +pub fn temp_dir(scan_id: &str) -> PathBuf { + let mut p = std::env::temp_dir(); + p.push(format!("secret-scan-{}", scan_id)); + p +} + +/// 报告输出目录(默认 exe 同目录 reports/) +pub fn default_report_dir() -> PathBuf { + exe_dir().join("reports") +} + +/// 探测同目录 Umi-OCR.exe +pub fn detect_umi_ocr() -> Option { + let p = exe_dir().join("Umi-OCR.exe"); + if p.exists() { Some(p) } else { None } +} + +/// 探测同目录 doclite.exe +pub fn detect_doclite() -> Option { + let p = exe_dir().join("doclite.exe"); + if p.exists() { Some(p) } else { None } +}