diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..4e46bab --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,580 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + +[[package]] +name = "anyhow" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "2.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da" + +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "serde_core", +] + +[[package]] +name = "cc" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "509591b7bcd67f4ef775afad7662703b4935daaa6ec0e5605cfb1090b32a2b6d" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "crossbeam-channel" +version = "0.5.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d85363c37faeca707aef026efa9f3b34d077bce547e48f770770625c6013679e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17" + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "either" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d" + +[[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 = "find-msvc-tools" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d45db016d36b838f563236e9193d0ee6ce38f3f68b6c94e914b4929c96bbb890" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[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.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "lua-src" +version = "550.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c110c2fa33f34e0de05448e1f3eb2e0631e7a69e2d8ae1586cffc9fc9f9949" +dependencies = [ + "cc", +] + +[[package]] +name = "luajit-src" +version = "210.7.2+b925b3e" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "920cf654b23d217c550ceea57c32cd2a413ea27b6d47ed77b5ee0cf655adefa6" +dependencies = [ + "cc", + "which", +] + +[[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.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "mlua" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad72ffa037cf5970c9860674f32f703fda25d86cf217475fe7a79c5f9961bcaa" +dependencies = [ + "bstr", + "either", + "libc", + "mlua-sys", + "num-traits", + "parking_lot", + "rustc-hash", +] + +[[package]] +name = "mlua-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92136787b906d4e55cfe96cd6c62e010bb1a56889d0d6cf83eb016dbad07576b" +dependencies = [ + "cc", + "cfg-if", + "libc", + "lua-src", + "luajit-src", + "pkg-config", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[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 = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[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", + "smallvec", + "windows-link", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkg-config" +version = "0.3.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6b464fbc74e149a392436b17d523f769e057cb6877f6a5c4618bc6f11800548" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + +[[package]] +name = "rshim" +version = "0.1.0" +dependencies = [ + "anyhow", + "dunce", + "encoding_rs", + "mlua", + "tinyjson", + "tracing", + "tracing-appender", + "tracing-subscriber", + "windows-sys", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[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 = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[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.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec86235f5fcc2a73650310756d2ac5b138a5780bbbdfae3eeccec992c435ba4f" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc04cd3e1236dd4a98afca4569f2deb3f120e5422a4023be2cb683f8486292af" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109" + +[[package]] +name = "time-macros" +version = "0.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tinyjson" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab95735ea2c8fd51154d01e39cf13912a78071c2d89abc49a7ef102a7dd725a" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "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", + "time", + "tracing-subscriber", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[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 = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "which" +version = "8.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3ef584124b911bcc3875c2f1472e80f24361ceb789bd1c62b3e9a3df9ff43c" +dependencies = [ + "libc", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] diff --git a/Cargo.toml b/Cargo.toml index c184180..9e37233 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,3 +30,4 @@ tracing = "0.1.44" tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-appender = "0.2" encoding_rs = "0.8.35" +tinyjson="2.5.1" \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 75a46fc..abbaf9a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,17 @@ -mod config; +extern crate core; + +mod spec; mod layout; mod logger; mod runtime; mod shim; mod loader; +mod validators; +mod utils; +mod sys; -pub use config::ShimConfig; +pub use spec::ShimSpec; pub use layout::Layout; pub use runtime::LuaRuntime; pub use shim::Shim; +pub use sys::execute_elevated; diff --git a/src/loader.rs b/src/loader.rs new file mode 100644 index 0000000..2eab9de --- /dev/null +++ b/src/loader.rs @@ -0,0 +1,96 @@ +use crate::spec::ShimSpec; +use crate::validators::LuaValidator; +use crate::{Layout, LuaRuntime}; +use anyhow::{Context, Result, bail}; +use mlua::{FromLua, Lua, Table, Value}; +use std::fs; +use std::path::Path; +use tracing::{debug, trace, warn}; + +pub enum Source { + Lua(LuaRuntime), + Json, +} + +/// ShimSpec 加载器,统一对外暴露多源解析接口 +pub struct SpecLoader; + +impl SpecLoader { + pub fn resolve_spec(source: &Source, layout: &Layout, target_name: &str) -> Result { + match source { + Source::Lua(r) => Self::resolve_lua_spec(&r, layout, target_name), + Source::Json => Self::load_from_json(layout), + } + } + /// 从 Lua 脚本字符串加载 ShimSpec + fn resolve_lua_spec( + runtime: &LuaRuntime, + layout: &Layout, + target_name: &str, + ) -> Result { + // 策略 1: 尝试加载全局配置文件 mimic.lua + let global_config = layout.base_dir.join("rshim.lua"); + + // if !global_config.is_file() { + // bail!( + // "未找到主配置文件: [{}],请确保在安装根目录创建 rshim.lua", + // global_config.display() + // ); + // } + + if global_config.is_file() { + trace!(path = %global_config.display(), "发现全局配置文件,尝试解析"); + let root_table: Table = runtime.eval_script(&global_config)?; + + // 检查 mimic.lua 中是否存在以 target_name 命名的 Table 节点 + if root_table + .contains_key(target_name) + .context("检查全局配置失败")? + { + let target_val: ShimSpec = root_table + .get(target_name) + .with_context(|| format!("解析配置 [{}] 失败: ", target_name))?; + debug!( + target = %target_name, + source = %global_config.display(), + "成功从全局配置文件中匹配到目标工具" + ); + + return Ok(target_val); + } + // 穿透:若全局配置文件存在但未包含当前程序的 key,继续向下探查 + trace!(target = %target_name, "全局配置文件中未包含该目标,继续探查独立配置"); + } + + // 策略 2: 降级寻找独立文件 ({exe}.lua),优先顺序:tools/ > root/ + let target_filename = format!("{}.lua", target_name); + let candidates = [ + layout.tools_dir.join(&target_filename), + layout.base_dir.join(&target_filename), + ]; + + for config_path in &candidates { + if config_path.is_file() { + debug!(path = %config_path.display(), "找到独立配置文件,开始加载"); + // 直接泛型反序列化为 ShimConfig + return runtime.eval_script::(config_path); + } + trace!(path = %config_path.display(), "独立配置文件不存在,跳过"); + } + + // 策略 3: 所有查找失败,抛出错误 + warn!(target = %target_name, "未找到任何匹配的配置文件"); + bail!( + "未找到关于 '{}' 的配置。请检查 mimic.lua 或特定的 {}.lua 文件", + target_name, + target_name + ); + } + + /// 从 Lua 脚本文件加载 ShimSpec + fn load_from_json(layout: &Layout) -> Result { + todo!("实现json来源") + } + +} + diff --git a/src/main.rs b/src/main.rs index 38349a6..04acf7d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,7 +2,7 @@ use rshim::Shim; use std::os::windows::ffi::OsStrExt; use std::{env, ffi::CString, mem::size_of, path::Path, process::exit, ptr::null_mut}; use tracing_subscriber::{EnvFilter, fmt}; - +use rshim::execute_elevated; use std::ffi::{OsStr, OsString}; use windows_sys::Win32::UI::Shell::{SHELLEXECUTEINFOW, ShellExecuteExW}; @@ -107,129 +107,129 @@ fn main() { exit(status.code().unwrap_or(EXIT_PROG_TERMINATED)) } // 辅助函数:将任意 OsStr 转换为以 \0 结尾的 UTF-16 宽字符向量 (Vec) -fn to_wide_null(s: impl AsRef) -> Vec { - s.as_ref().encode_wide().chain(std::iter::once(0)).collect() -} - -fn execute_elevated( - program: &Path, - args: &[OsString], - env_vars: Option<&std::collections::HashMap>, -) -> i32 { - // 若提权启动,在此处将环境变量设置给当前进程(即将弹窗 UAC 的进程,随后会被子进程继承) - if let Some(env_map) = env_vars { - for (k, v) in env_map { - unsafe { - env::set_var(k, v); - } - } - } - // 2. 将参数列表按 Windows 命令行规则拼装为单个命令行字符串 - let mut arguments_os = OsString::new(); - for (i, arg) in args.iter().enumerate() { - if i > 0 { - arguments_os.push(" "); - } - let arg_str = arg.to_string_lossy(); - if arg_str.is_empty() { - arguments_os.push("\"\""); - } else if !arg_str.contains([' ', '\t', '"']) { - arguments_os.push(arg); - } else { - // 包含空格或引号时进行标准转义包裹 - arguments_os.push("\""); - for c in arg_str.chars() { - match c { - '\\' => arguments_os.push("\\\\"), - '"' => arguments_os.push("\\\""), - _ => arguments_os.push(c.to_string()), - } - } - arguments_os.push("\""); - } - } - // let runas = CString::new("runas").unwrap(); - // let program = CString::new(program.to_str().unwrap()).unwrap(); - // let mut arguments = String::new(); - // for arg in args.iter() { - // arguments.push(' '); - // if arg.len() == 0 { - // arguments.push_str("\"\""); - // } else if arg.find(&[' ', '\t', '"'][..]).is_none() { - // arguments.push_str(&arg); - // } else { - // arguments.push('"'); - // for c in arg.chars() { - // match c { - // '\\' => arguments.push_str("\\\\"), - // '"' => arguments.push_str("\\\""), - // c => arguments.push(c), - // } - // } - // arguments.push('"'); - // } - // } - // 3. 准备 Windows 宽字符参数 - let runas = to_wide_null("runas"); - let program_wide = to_wide_null(program.as_os_str()); - let arguments_wide = to_wide_null(&arguments_os); - - let mut info = SHELLEXECUTEINFOW { - cbSize: size_of::() as u32, - fMask: SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS, - hwnd: null_mut(), - lpVerb: runas.as_ptr(), - lpFile: program_wide.as_ptr(), - lpParameters: arguments_wide.as_ptr(), - lpDirectory: null_mut(), - nShow: SW_NORMAL as i32, - hInstApp: null_mut(), - lpIDList: null_mut(), - lpClass: null_mut(), - hkeyClass: null_mut(), - dwHotKey: 0, - Anonymous: unsafe { std::mem::zeroed() }, - hProcess: null_mut(), - }; - // let arguments = CString::new(&arguments[..]).unwrap(); - // let mut info = SHELLEXECUTEINFOA::default(); - // info.cbSize = size_of::() as u32; - // info.fMask = SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS; - // info.lpVerb = runas.as_ptr().cast::(); - // info.lpFile = program.as_ptr().cast::(); - // info.lpParameters = arguments.as_ptr().cast::(); - // info.nShow = SW_NORMAL; - let res = unsafe { - CoInitializeEx( - null_mut(), - (COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE) as u32, - ); - // ShellExecuteExA(&mut info as *mut _) - ShellExecuteExW(&mut info) - }; - if res == FALSE || info.hProcess == null_mut() { - return EXIT_FAILED_SPAWN_PROG; - } - // 5. 等待提权子进程执行结束并获取退出状态码 - let mut exit_code: u32 = 0; - unsafe { - WaitForSingleObject(info.hProcess, INFINITE); - let ok = GetExitCodeProcess(info.hProcess, &mut exit_code); - CloseHandle(info.hProcess); // 释放进程句柄,防止资源泄露 - - if ok == FALSE { - return EXIT_FAILED_WAIT_PROG; - } - } - - exit_code as i32 - - // let mut code: u32 = 0; - // unsafe { - // WaitForSingleObject(info.hProcess, INFINITE); - // if GetExitCodeProcess(info.hProcess, &mut code as *mut _) == FALSE { - // return EXIT_FAILED_WAIT_PROG; - // } - // } - // code as i32 -} +// fn to_wide_null(s: impl AsRef) -> Vec { +// s.as_ref().encode_wide().chain(std::iter::once(0)).collect() +// } +// +// fn execute_elevated( +// program: &Path, +// args: &[OsString], +// env_vars: Option<&std::collections::HashMap>, +// ) -> i32 { +// // 若提权启动,在此处将环境变量设置给当前进程(即将弹窗 UAC 的进程,随后会被子进程继承) +// if let Some(env_map) = env_vars { +// for (k, v) in env_map { +// unsafe { +// env::set_var(k, v); +// } +// } +// } +// // 2. 将参数列表按 Windows 命令行规则拼装为单个命令行字符串 +// let mut arguments_os = OsString::new(); +// for (i, arg) in args.iter().enumerate() { +// if i > 0 { +// arguments_os.push(" "); +// } +// let arg_str = arg.to_string_lossy(); +// if arg_str.is_empty() { +// arguments_os.push("\"\""); +// } else if !arg_str.contains([' ', '\t', '"']) { +// arguments_os.push(arg); +// } else { +// // 包含空格或引号时进行标准转义包裹 +// arguments_os.push("\""); +// for c in arg_str.chars() { +// match c { +// '\\' => arguments_os.push("\\\\"), +// '"' => arguments_os.push("\\\""), +// _ => arguments_os.push(c.to_string()), +// } +// } +// arguments_os.push("\""); +// } +// } +// // let runas = CString::new("runas").unwrap(); +// // let program = CString::new(program.to_str().unwrap()).unwrap(); +// // let mut arguments = String::new(); +// // for arg in args.iter() { +// // arguments.push(' '); +// // if arg.len() == 0 { +// // arguments.push_str("\"\""); +// // } else if arg.find(&[' ', '\t', '"'][..]).is_none() { +// // arguments.push_str(&arg); +// // } else { +// // arguments.push('"'); +// // for c in arg.chars() { +// // match c { +// // '\\' => arguments.push_str("\\\\"), +// // '"' => arguments.push_str("\\\""), +// // c => arguments.push(c), +// // } +// // } +// // arguments.push('"'); +// // } +// // } +// // 3. 准备 Windows 宽字符参数 +// let runas = to_wide_null("runas"); +// let program_wide = to_wide_null(program.as_os_str()); +// let arguments_wide = to_wide_null(&arguments_os); +// +// let mut info = SHELLEXECUTEINFOW { +// cbSize: size_of::() as u32, +// fMask: SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS, +// hwnd: null_mut(), +// lpVerb: runas.as_ptr(), +// lpFile: program_wide.as_ptr(), +// lpParameters: arguments_wide.as_ptr(), +// lpDirectory: null_mut(), +// nShow: SW_NORMAL as i32, +// hInstApp: null_mut(), +// lpIDList: null_mut(), +// lpClass: null_mut(), +// hkeyClass: null_mut(), +// dwHotKey: 0, +// Anonymous: unsafe { std::mem::zeroed() }, +// hProcess: null_mut(), +// }; +// // let arguments = CString::new(&arguments[..]).unwrap(); +// // let mut info = SHELLEXECUTEINFOA::default(); +// // info.cbSize = size_of::() as u32; +// // info.fMask = SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS; +// // info.lpVerb = runas.as_ptr().cast::(); +// // info.lpFile = program.as_ptr().cast::(); +// // info.lpParameters = arguments.as_ptr().cast::(); +// // info.nShow = SW_NORMAL; +// let res = unsafe { +// CoInitializeEx( +// null_mut(), +// (COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE) as u32, +// ); +// // ShellExecuteExA(&mut info as *mut _) +// ShellExecuteExW(&mut info) +// }; +// if res == FALSE || info.hProcess == null_mut() { +// return EXIT_FAILED_SPAWN_PROG; +// } +// // 5. 等待提权子进程执行结束并获取退出状态码 +// let mut exit_code: u32 = 0; +// unsafe { +// WaitForSingleObject(info.hProcess, INFINITE); +// let ok = GetExitCodeProcess(info.hProcess, &mut exit_code); +// CloseHandle(info.hProcess); // 释放进程句柄,防止资源泄露 +// +// if ok == FALSE { +// return EXIT_FAILED_WAIT_PROG; +// } +// } +// +// exit_code as i32 +// +// // let mut code: u32 = 0; +// // unsafe { +// // WaitForSingleObject(info.hProcess, INFINITE); +// // if GetExitCodeProcess(info.hProcess, &mut code as *mut _) == FALSE { +// // return EXIT_FAILED_WAIT_PROG; +// // } +// // } +// // code as i32 +// } diff --git a/src/runtime.rs b/src/runtime.rs index 398b6c9..5626c93 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -1,15 +1,12 @@ -use crate::{ShimConfig, Layout}; +use crate::{ShimSpec, Layout}; use anyhow::{Context, Result, anyhow, bail}; use mlua::{FromLua, Lua, StdLib, Table, Value}; use std::ffi::OsStr; use std::path::Path; use std::{env, fs}; +use crate::utils::normalize_path_for_lua; + /// 将 Path 转换为适合 Lua 使用的安全字符串路径 -fn normalize_path_for_lua(path: &Path) -> String { - // 自动将 Windows UNC 规范路径转回传统路径 - let simplified = dunce::simplified(path); - simplified.to_string_lossy().replace('\\', "/") -} pub struct LuaRuntime { lua: Lua, @@ -230,7 +227,7 @@ mod tests { // 复现用户场景:PATH = { base_dir .. "/tools/numa", get_env("PATH") } // 宿主 PATH 即使含双引号,也应拆分后正常拼接,而不是报错 let runtime = LuaRuntime::new(&test_layout()).unwrap(); - let cfg: ShimConfig = runtime + let cfg: ShimSpec = runtime .lua .load( r#" diff --git a/src/shim.rs b/src/shim.rs index 30961d1..3f9e440 100644 --- a/src/shim.rs +++ b/src/shim.rs @@ -1,12 +1,14 @@ -use crate::{LuaRuntime, ShimConfig, Layout}; -use anyhow::{Context, Result, bail}; -use mlua::{Table, Value}; +use crate::loader::Source; +use crate::loader::SpecLoader; +use crate::{Layout, LuaRuntime, ShimSpec}; +use anyhow::{Context, Result}; use std::env; -use tracing::{debug, trace, warn}; +use tracing::debug; + pub struct Shim; impl Shim { - pub fn load() -> Result { + pub fn load() -> Result { let current_exe = env::current_exe().context("获取代理程序路径失败: {}")?; debug!("当前目录 {}", current_exe.display()); @@ -30,72 +32,6 @@ impl Shim { ); let runtime = LuaRuntime::new(&layout)?; - - Self::resolve_config(&runtime, &layout, &target_name) - } - - fn resolve_config( - runtime: &LuaRuntime, - paths: &Layout, - target_name: &str, - ) -> Result { - // 策略 1: 尝试加载全局配置文件 mimic.lua - let global_config = paths.base_dir.join("rshim.lua"); - - // if !global_config.is_file() { - // bail!( - // "未找到主配置文件: [{}],请确保在安装根目录创建 rshim.lua", - // global_config.display() - // ); - // } - - - if global_config.is_file() { - trace!(path = %global_config.display(), "发现全局配置文件,尝试解析"); - let root_table: Table = runtime.eval_script(&global_config)?; - - // 检查 mimic.lua 中是否存在以 target_name 命名的 Table 节点 - if root_table - .contains_key(target_name) - .context("检查全局配置失败")? - { - let target_val: ShimConfig = root_table - .get(target_name) - .with_context(|| format!("解析配置 [{}] 失败: ", target_name))?; - debug!( - target = %target_name, - source = %global_config.display(), - "成功从全局配置文件中匹配到目标工具" - ); - - return Ok(target_val); - } - // 穿透:若全局配置文件存在但未包含当前程序的 key,继续向下探查 - trace!(target = %target_name, "全局配置文件中未包含该目标,继续探查独立配置"); - } - - // 策略 2: 降级寻找独立文件 ({exe}.lua),优先顺序:tools/ > root/ - let target_filename = format!("{}.lua", target_name); - let candidates = [ - paths.tools_dir.join(&target_filename), - paths.base_dir.join(&target_filename), - ]; - - for config_path in &candidates { - if config_path.is_file() { - debug!(path = %config_path.display(), "找到独立配置文件,开始加载"); - // 直接泛型反序列化为 ShimConfig - return runtime.eval_script::(config_path); - } - trace!(path = %config_path.display(), "独立配置文件不存在,跳过"); - } - - // 策略 3: 所有查找失败,抛出错误 - warn!(target = %target_name, "未找到任何匹配的配置文件"); - bail!( - "未找到关于 '{}' 的配置。请检查 mimic.lua 或特定的 {}.lua 文件", - target_name, - target_name - ); + SpecLoader::resolve_spec(&Source::Lua(runtime), &layout, &target_name) } } diff --git a/src/spec.rs b/src/spec.rs new file mode 100644 index 0000000..569eab9 --- /dev/null +++ b/src/spec.rs @@ -0,0 +1,127 @@ +use crate::validators::{JsonValidator, LuaValidator}; +use mlua::{FromLua, Lua, ObjectLike, Value}; +use std::collections::HashMap; +use std::ffi::OsString; +use std::path::PathBuf; +use std::process::Command; +use std::str::FromStr; +use tinyjson::JsonValue; +use anyhow::{anyhow, Context, Result}; +/// 构造一个带上下文的 FromLua 转换错误,便于定位配置问题 +fn conversion_error(message: impl Into) -> mlua::Error { + mlua::Error::FromLuaConversionError { + from: "Lua value", + to: "ShimConfig".into(), + message: Some(message.into()), + } +} + + +#[derive(Debug, Clone, Default)] +pub struct ShimSpec { + pub target: PathBuf, + pub args: Vec, + pub env: HashMap, +} + +impl ShimSpec { + /// 根据配置快速构建准备执行的 Command 对象 + pub fn to_command(&self, runtime_args: I) -> Command + where + I: IntoIterator, + S: AsRef, + { + let mut cmd = Command::new(&self.target); + + // 1. 先追加配置里固定的默认参数 (例如: node, --max-old-space-size=4096) + cmd.args(&self.args); + + // 2. 透传外部动态运行时参数 + cmd.args(runtime_args); + + for (key, val) in &self.env { + // 直接应用环境变量(Lua 端已经处理好字符串拼接或列表合并) + cmd.env(key, val); + } + + cmd + } +} + +/// 实现 FromLua Trait,由 mlua 自动处理 Table 转换 +impl FromLua for ShimSpec { + fn from_lua(value: Value, _lua: &Lua) -> mlua::Result { + // 脚本返回必须是一个 Table 变体 + let table = match value { + Value::Table(t) => t, + _ => { + return Err(conversion_error(format!( + "期望得到一个 Lua Table 配置对象,实际是 {}", + value.type_name() + ))); + } + }; + + // 必填字段: target(严格限定为字符串,避免数字被 mlua 宽松转为字符串后掩盖错误) + let target = match table.get::>("target")? { + None | Some(Value::Nil) => { + return Err(conversion_error("缺少必填字段 target(应为字符串路径)")); + } + Some(target_val) => LuaValidator::parse_target(&target_val)?, + }; + + // 可选字段: args(缺失或 nil 时默认空列表;保留空字符串参数,与提权路径的 "" 语义一致) + let args = match table.get::>("args")? { + None | Some(Value::Nil) => Vec::new(), + Some(args_val) => LuaValidator::parse_args(&args_val)?, + }; + + // 可选字段: env(只允许缺失/nil,其他类型由 Option 转换报错,不再静默忽略) + let env = match table.get::>("env")? { + None | Some(Value::Nil) => HashMap::new(), + Some(env_val) => LuaValidator::parse_env(&env_val)?, + + }; + println!("环境变量结果:{:?}", env); + Ok(ShimSpec { target, args, env }) + } +} + +impl TryFrom<&str> for ShimSpec { + type Error = anyhow::Error; + + fn try_from(json_str: &str) -> Result { + // 1. 解析 JSON 字符串为 JsonValue 树 + let root = JsonValue::from_str(json_str) + .map_err(|e| anyhow!("JSON 语法错误: {}", e))?; + + // 根节点必须是一个 JSON Object + let map: &HashMap = root + .get() + .ok_or_else(|| anyhow!("JSON 根节点必须是 Object 对象"))?; + + // 1. target (必填) + let target_val = map + .get("target") + .ok_or_else(|| anyhow!("缺少必填字段: target"))?; + let target = JsonValidator::parse_target(target_val)?; + + // 2. args (选填) + let args = match map.get("args") { + Some(args_val) => JsonValidator::parse_args(args_val)?, + None => Vec::new(), + }; + + // 3. env (选填) + let env = match map.get("env") { + Some(env_val) => JsonValidator::parse_env(env_val)?, + None => HashMap::new(), + }; + + Ok(ShimSpec { + target, + args, + env, + }) + } +} \ No newline at end of file diff --git a/src/sys.rs b/src/sys.rs new file mode 100644 index 0000000..d8da9c7 --- /dev/null +++ b/src/sys.rs @@ -0,0 +1,3 @@ +mod win; + +pub use win::{set_console_ctrl_handler, execute_elevated}; \ No newline at end of file diff --git a/src/sys/win.rs b/src/sys/win.rs new file mode 100644 index 0000000..14c5f73 --- /dev/null +++ b/src/sys/win.rs @@ -0,0 +1,157 @@ +use crate::Shim; +use std::os::windows::ffi::OsStrExt; +use std::{env, ffi::CString, mem::size_of, path::Path, process::exit, ptr::null_mut}; +use tracing_subscriber::{EnvFilter, fmt}; + +use std::ffi::{OsStr, OsString}; + +use windows_sys::Win32::UI::Shell::{SHELLEXECUTEINFOW, ShellExecuteExW}; + +use windows_sys::Win32::Foundation::CloseHandle; +use windows_sys::{ + Win32::{ + Foundation::{FALSE, TRUE}, + System::{ + Com::{COINIT_APARTMENTTHREADED, COINIT_DISABLE_OLE1DDE, CoInitializeEx}, + Console::{ + CTRL_BREAK_EVENT, CTRL_C_EVENT, CTRL_CLOSE_EVENT, CTRL_LOGOFF_EVENT, + CTRL_SHUTDOWN_EVENT, SetConsoleCtrlHandler, + }, + Threading::{GetExitCodeProcess, INFINITE, WaitForSingleObject}, + }, + UI::{ + Shell::{ + SEE_MASK_NOASYNC, SEE_MASK_NOCLOSEPROCESS, SHELLEXECUTEINFOA, ShellExecuteExA, + }, + WindowsAndMessaging::SW_NORMAL, + }, + }, + core::BOOL, +}; + +const EXIT_FAILED_LOAD_SHIM: i32 = 1; +const EXIT_FAILED_SPAWN_PROG: i32 = 2; +const EXIT_FAILED_WAIT_PROG: i32 = 3; +const EXIT_PROG_TERMINATED: i32 = 4; + +const ERROR_ELEVATION_REQUIRED: i32 = 740; + +unsafe extern "system" fn console_ctrl_handler(evt: u32) -> BOOL { + match evt { + CTRL_C_EVENT => TRUE, //eprintln!("ctrl_c handled!"), + CTRL_BREAK_EVENT => TRUE, //eprintln!("ctrl_break handled!"), + CTRL_CLOSE_EVENT => TRUE, //eprintln!("ctrl_close handled!"), + CTRL_LOGOFF_EVENT => TRUE, //eprintln!("ctrl_logoff handled!"), + CTRL_SHUTDOWN_EVENT => TRUE, //eprintln!("ctrl_shutdown handled!"), + other => { + eprintln!("未知的系统事件编号: {},未处理!", other); + FALSE + } + } +} + +pub fn set_console_ctrl_handler() { + let res: BOOL = unsafe { SetConsoleCtrlHandler(Some(console_ctrl_handler), TRUE) }; + if res == FALSE { + eprintln!("警告: 注册控制台中断事件处理器失败。"); + } +} + +fn to_wide_null(s: impl AsRef) -> Vec { + s.as_ref().encode_wide().chain(std::iter::once(0)).collect() +} + +pub fn execute_elevated( + program: &Path, + args: &[OsString], + env_vars: Option<&std::collections::HashMap>, +) -> i32 { + // 若提权启动,在此处将环境变量设置给当前进程(即将弹窗 UAC 的进程,随后会被子进程继承) + if let Some(env_map) = env_vars { + for (k, v) in env_map { + unsafe { + env::set_var(k, v); + } + } + } + // 2. 将参数列表按 Windows 命令行规则拼装为单个命令行字符串 + let mut arguments_os = OsString::new(); + for (i, arg) in args.iter().enumerate() { + if i > 0 { + arguments_os.push(" "); + } + let arg_str = arg.to_string_lossy(); + if arg_str.is_empty() { + arguments_os.push("\"\""); + } else if !arg_str.contains([' ', '\t', '"']) { + arguments_os.push(arg); + } else { + // 包含空格或引号时进行标准转义包裹 + arguments_os.push("\""); + for c in arg_str.chars() { + match c { + '\\' => arguments_os.push("\\\\"), + '"' => arguments_os.push("\\\""), + _ => arguments_os.push(c.to_string()), + } + } + arguments_os.push("\""); + } + } + + // 3. 准备 Windows 宽字符参数 + let runas = to_wide_null("runas"); + let program_wide = to_wide_null(program.as_os_str()); + let arguments_wide = to_wide_null(&arguments_os); + + let mut info = SHELLEXECUTEINFOW { + cbSize: size_of::() as u32, + fMask: SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS, + hwnd: null_mut(), + lpVerb: runas.as_ptr(), + lpFile: program_wide.as_ptr(), + lpParameters: arguments_wide.as_ptr(), + lpDirectory: null_mut(), + nShow: SW_NORMAL as i32, + hInstApp: null_mut(), + lpIDList: null_mut(), + lpClass: null_mut(), + hkeyClass: null_mut(), + dwHotKey: 0, + Anonymous: unsafe { std::mem::zeroed() }, + hProcess: null_mut(), + }; + // let arguments = CString::new(&arguments[..]).unwrap(); + // let mut info = SHELLEXECUTEINFOA::default(); + // info.cbSize = size_of::() as u32; + // info.fMask = SEE_MASK_NOASYNC | SEE_MASK_NOCLOSEPROCESS; + // info.lpVerb = runas.as_ptr().cast::(); + // info.lpFile = program.as_ptr().cast::(); + // info.lpParameters = arguments.as_ptr().cast::(); + // info.nShow = SW_NORMAL; + let res = unsafe { + CoInitializeEx( + null_mut(), + (COINIT_APARTMENTTHREADED | COINIT_DISABLE_OLE1DDE) as u32, + ); + // ShellExecuteExA(&mut info as *mut _) + ShellExecuteExW(&mut info) + }; + if res == FALSE || info.hProcess == null_mut() { + return EXIT_FAILED_SPAWN_PROG; + } + // 5. 等待提权子进程执行结束并获取退出状态码 + let mut exit_code: u32 = 0; + unsafe { + WaitForSingleObject(info.hProcess, INFINITE); + let ok = GetExitCodeProcess(info.hProcess, &mut exit_code); + CloseHandle(info.hProcess); // 释放进程句柄,防止资源泄露 + + if ok == FALSE { + return EXIT_FAILED_WAIT_PROG; + } + } + + exit_code as i32 + +} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..d1d306b --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,7 @@ +use std::path::Path; + +pub fn normalize_path_for_lua(path: &Path) -> String { + // 自动将 Windows UNC 规范路径转回传统路径 + let simplified = dunce::simplified(path); + simplified.to_string_lossy().replace('\\', "/") +} diff --git a/src/config.rs b/src/validators.rs similarity index 59% rename from src/config.rs rename to src/validators.rs index 27840af..202b7bb 100644 --- a/src/config.rs +++ b/src/validators.rs @@ -1,10 +1,12 @@ -use mlua::{FromLua, Lua, LuaString, ObjectLike, Table, Value}; +use mlua::{LuaString, Table, Value}; use std::collections::HashMap; use std::ffi::OsString; use std::fmt; -use std::fmt::Display; use std::path::PathBuf; -use std::process::Command; +use std::str::FromStr; +use tinyjson::JsonValue; +use anyhow::{anyhow, Context, Result}; + /// 构造一个带上下文的 FromLua 转换错误,便于定位配置问题 fn conversion_error(message: impl Into) -> mlua::Error { mlua::Error::FromLuaConversionError { @@ -42,11 +44,9 @@ fn lua_string_2_os_string(s: &LuaString) -> mlua::Result { } } -/// 按连续整数下标遍历表的序列部分;存在空洞或非序列键时返回错误,避免静默截断 - /// Lua 值校验器:针对不同上下文定义校验规则 #[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum ValueValidator { +pub enum LuaValidator { /// 目标程序路径:必须是字符串路径 Target, /// 命令行参数元素:仅支持一维基础标量,禁止嵌套 Table @@ -55,7 +55,7 @@ pub enum ValueValidator { Env, } -impl fmt::Display for ValueValidator { +impl fmt::Display for LuaValidator { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Target => write!(f, "目标路径 (target)"), @@ -64,66 +64,7 @@ impl fmt::Display for ValueValidator { } } } -impl ValueValidator { - /// 【纯粹校验入口】只进行逻辑与结构判定,零副作用、不产生内存分配 - pub fn validate(&self, value: &Value) -> mlua::Result<()> { - let mut n = 0; - if matches!(self, Self::Target) { - println!("跟踪taget{}", value.type_name()); - } - if matches!(self, Self::Args) { - println!("跟踪args2{}", value.type_name()); - } - if matches!(self, Self::Env) { - n = n + 1; - println!("跟踪env{} {}", n, value.type_name()); - } - match self { - Self::Target => match value { - Value::String(_) => Ok(()), - Value::Nil => Err(conversion_error("缺少必填字段 target(应为字符串路径)")), - other => Err(conversion_error(format!( - "{} 需为有效的路径且类型必须是字符串,实际类型是 {}", - self, - other.type_name() - ))), - }, - Self::Args => match value { - Value::Table(tbl) => self.validate_sequence_table(tbl), - other => Err(conversion_error(format!( - "{} 必须是数组列表,实际类型是 {}", - self, - other.type_name() - ))), - }, - Self::Env => match value { - Value::Nil - | Value::String(_) - | Value::Integer(_) - | Value::Number(_) - | Value::Boolean(_) => Ok(()), - Value::Table(tbl) => self.validate_sequence_table(tbl), - other => Err(conversion_error(format!( - "{} 不支持 {} 类型, 仅支持 string/number/boolean 或嵌套数组", - self, - other.type_name() - ))), - }, - } - } - // fn parse_sequence(&self,index: i64,item: &Value) -> mlua::Result<()> { - // match item { - // Value::String(_) | Value::Integer(_) | Value::Number(_) | Value::Boolean(_) => {} - // other => { - // return Err(conversion_error(format!( - // "第 {} 个元素类型无效: {}", - // index, - // other.type_name() - // ))); - // } - // } - // Ok(()) - // } +impl LuaValidator { fn parse_sequence(&self, index: i64, item: &Value) -> mlua::Result<()> { match item { Value::String(_) | Value::Integer(_) | Value::Number(_) | Value::Boolean(_) => Ok(()), @@ -182,34 +123,106 @@ impl ValueValidator { Ok(()) } + /// 底层 NUL 字符跨平台安全检查 + fn ensure_no_nul(context: &impl fmt::Display, os_str: &std::ffi::OsStr) -> mlua::Result<()> { + #[cfg(unix)] + { + use std::os::unix::ffi::OsStrExt; + if os_str.as_bytes().contains(&0) { + return Err(conversion_error(format!( + "{} 的值不能包含 NUL 字符", + context + ))); + } + } + #[cfg(windows)] + { + use std::os::windows::ffi::OsStrExt; + if os_str.encode_wide().any(|c| c == 0) { + return Err(conversion_error(format!( + "{} 的值不能包含 NUL 字符", + context + ))); + } + } + Ok(()) + } + + /// 【纯粹数据转换与展开】将已通过校验的 Value 递归解析为 OsString 动态数组 + fn collect_value_into( + context: &impl fmt::Display, + value: &Value, + out: &mut Vec, + ) -> mlua::Result<()> { + match value { + Value::Nil => {} + Value::String(s) => out.push(lua_string_2_os_string(s)?), + Value::Integer(i) => out.push(OsString::from(i.to_string())), + Value::Number(n) => { + tracing::warn!(%context, value = %n, "浮点数将按十进制格式转换为字符串"); + out.push(OsString::from(n.to_string())); + } + Value::Boolean(b) => { + tracing::warn!(%context, value = %b, "布尔值将转换为字符串"); + out.push(OsString::from(b.to_string())); + } + Value::Table(tbl) => { + let mut index = 1i64; + loop { + let item: Value = tbl.raw_get(index)?; + if matches!(item, Value::Nil) { + break; + } + Self::collect_value_into(context, &item, out)?; + index += 1; + } + } + _ => unreachable!("传入收集器的 Value 应已通过 validate 校验"), + } + Ok(()) + } +} +impl LuaValidator { /// 解析并校验 `target` pub fn parse_target(value: &Value) -> mlua::Result { - let validator = Self::Target; - validator.validate(&value)?; - let ctx = format!("{}", validator); - - if let Value::String(s) = value { - let os_str = lua_string_2_os_string(&s)?; - Self::ensure_no_nul(&ctx, &os_str)?; - Ok(PathBuf::from(os_str)) - } else { - unreachable!() + let ctx = Self::Target; + match value { + Value::String(s) => { + let os_str = lua_string_2_os_string(&s)?; + Self::ensure_no_nul(&ctx, &os_str)?; + Ok(PathBuf::from(os_str)) + } + Value::Nil => Err(conversion_error("缺少必填字段 target(应为字符串路径)")), + other => Err(conversion_error(format!( + "{} 需为有效的路径且类型必须是字符串,实际类型是 {}", + ctx, + other.type_name() + ))), } } + /// 解析并校验 `args` pub fn parse_args(value: &Value) -> mlua::Result> { println!("跟踪args1{}", value.type_name()); - let validator = Self::Args; - validator.validate(&value)?; - let ctx = format!("{}", validator); + let ctx = Self::Args; + match value { + Value::Table(tbl) => { + ctx.validate_sequence_table(tbl)?; - let mut raw_parts = Vec::new(); - collect_value_into(&ctx, value, &mut raw_parts)?; + let mut raw_parts = Vec::new(); + Self::collect_value_into(&ctx, value, &mut raw_parts)?; - for part in &raw_parts { - Self::ensure_no_nul(&ctx, &part)?; + for part in &raw_parts { + Self::ensure_no_nul(&ctx, &part)?; + } + Ok(raw_parts) + } + other => Err(conversion_error(format!( + "{} 必须是数组列表,实际类型是 {}", + ctx, + other.type_name() + )))?, } - Ok(raw_parts) } /// 校验环境变量名的合法性(Windows 约束:非空、不含 '='、不含 NUL) @@ -244,18 +257,39 @@ impl ValueValidator { Ok(name_str) } - /// 解析、校验并拼接单个环境变量键值对 (`key`, `value`) -> (`String`, `OsString`) - fn parse_env_val(name: &str, raw_val: &Value) -> mlua::Result { - Self::Env.validate(&raw_val)?; - - let ctx = format!("环境变量 [{}]", name); + /// 解析、校验并拼接单个环境变量键值对 (`key`, `value`) -> (`OsString`) + fn parse_env_val( + context: &LuaValidator, + name: &str, + raw_val: &Value, + ) -> mlua::Result { + // Self::Env.validate(&raw_val)?; + match raw_val { + Value::Nil + | Value::String(_) + | Value::Integer(_) + | Value::Number(_) + | Value::Boolean(_) => {} + Value::Table(tbl) => { + context.validate_sequence_table(tbl)?; + } + other => { + return Err(conversion_error(format!( + "{} [{}]不支持 {} 类型, 仅支持 string/number/boolean 或嵌套数组", + context, + name, + other.type_name() + ))); + } + } + let ctx = format!("{} [{}]", context, name); let mut parts = Vec::new(); - collect_value_into(&ctx, raw_val, &mut parts)?; + Self::collect_value_into(&ctx, raw_val, &mut parts)?; // 校验每个展开元素的 NUL 字符 for part in &parts { - Self::ensure_no_nul(name, part)?; + Self::ensure_no_nul(&name, part)?; } // 3. 使用系统路径分隔符拼接数组列表 @@ -264,194 +298,156 @@ impl ValueValidator { })?; Ok(joined_os_str) } - /// 解析并校验单个环境变量键值对 (`key`, `val`) -> (`String`, `OsString`) - fn parse_env_pair(key: &Value, val: &Value) -> mlua::Result<(String, OsString)> { - // 1. 解析并校验 Key,拿到安全的 String - let name = Self::parse_env_name(key)?; - // 2. 借用 &name 传递给 Value 解析器作为上下文 - let os_val = Self::parse_env_val(&name, val)?; - // 3. 所有权平滑转移,组装元组返回 - Ok((name, os_val)) - } + /// 解析整个 `env` Table,直接返回安全的环境变量 Map pub fn parse_env(value: &Value) -> mlua::Result> { + let ctx = Self::Env; + let Value::Table(tbl) = value else { // 这里作为防御性校验,外部虽然 match 过了,但内层仍保持强类型安全 return Err(conversion_error(format!( - "env 必须是键值表 (table),实际类型是 {}", + "{} 必须是键值表 (table),实际类型是 {}", + ctx, value.type_name() ))); }; let mut env_map = HashMap::new(); let mut n = 0i32; + for pair in tbl.pairs::() { n += 1; let (raw_key, raw_val) = pair?; println!("env {} {}", n, raw_val.type_name()); - let (key, val) = Self::parse_env_pair(&raw_key, &raw_val)?; + + // 1. 解析并校验 Key,拿到安全的 String + let key = Self::parse_env_name(&raw_key)?; + + // 2. 借用 &name 传递给 Value 解析器作为上下文 + let val = Self::parse_env_val(&ctx, &key, &raw_val)?; + env_map.insert(key, val); } Ok(env_map) } - // ================= 3. 底层 NUL 字符跨平台安全检查 ================= +} - fn ensure_no_nul(context_desc: &str, os_str: &std::ffi::OsStr) -> mlua::Result<()> { - #[cfg(unix)] - { - use std::os::unix::ffi::OsStrExt; - if os_str.as_bytes().contains(&0) { - return Err(conversion_error(format!( - "{} 的值不能包含 NUL 字符", - context_desc - ))); +/// 专用于 JSON (tinyjson) 的类型校验与字段提取器 +//当前JsonValidator还为完全可用,后期会对其lua配置特别需要处理动态函数生成的值如:get_env("PATH") +// 数据结构:{ +// "target": "C:/tools/git.exe", +// "args": [ +// "--no-pager", +// 2, +// true +// ], +// "env": { +// "PATH": ["C:/tools/git/bin", "C:/Windows"], +// "HOME": "C:/tools/home", +// "CONST": 3, +// "BOOL": true +// } +// } +pub struct JsonValidator; + +impl JsonValidator { + + /// 校验并解析 target (必填,非空字符串) + pub fn parse_target(val: &JsonValue) -> Result { + let s: &String = val + .get() + .ok_or_else(|| anyhow!("字段 'target' 必须是字符串"))?; + + if s.trim().is_empty() { + return Err(anyhow!("字段 'target' 不能为空字符串")); + } + Ok(PathBuf::from(s)) + } + + /// 校验并解析 args (选填,必须为字符串数组) + pub fn parse_args(val: &JsonValue) -> Result> { + let args_vec: &Vec = val + .get() + .ok_or_else(|| anyhow!("字段 'args' 必须是数组"))?; + + args_vec + .iter() + .map(|item| { + let s: &String = item + .get() + .ok_or_else(|| anyhow!("'args' 数组内的元素必须全为字符串"))?; + Ok(OsString::from(s)) + }) + .collect() + } + + /// 校验并解析 env (选填,必须为 KV 对象) + pub fn parse_env(val: &JsonValue) -> Result> { + let env_map: &HashMap = val + .get() + .ok_or_else(|| anyhow!("字段 'env' 必须是 Object 键值对"))?; + let mut result = HashMap::new(); + for (k, v) in env_map { + let mut segments = Vec::new(); + Self::collect_env_segments(v, &mut segments)?; + + if !segments.is_empty() { + // 根据操作系统自动拼接环境变量分隔符 (Windows 为 ';', Unix 为 ':') + #[cfg(windows)] + let sep = std::ffi::OsStr::new(";"); + #[cfg(not(windows))] + let sep = std::ffi::OsStr::new(":"); + + let joined = segments.join(sep); + result.insert(k.clone(), joined); } } - #[cfg(windows)] - { - use std::os::windows::ffi::OsStrExt; - if os_str.encode_wide().any(|c| c == 0) { - return Err(conversion_error(format!( - "{} 的值不能包含 NUL 字符", - context_desc - ))); + + Ok(result) + } + + // ========================================================================= + // 私有辅助方法 + // ========================================================================= + + /// 单个标量节点转换为 OsString (支持 String, Number, Boolean) + fn value_to_os_string(val: &JsonValue, ctx: &str) -> Result { + if let Some(s) = val.get::() { + Ok(OsString::from(s)) + } else if let Some(n) = val.get::() { + Ok(OsString::from(n.to_string())) + } else if let Some(b) = val.get::() { + Ok(OsString::from(b.to_string())) + } else { + Err(anyhow!("{} 内只支持字符串、数字或布尔类型", ctx)) + } + } + + /// 递归打平 env 节点中的数组/标量值 + fn collect_env_segments(val: &JsonValue, segments: &mut Vec) -> Result<()> { + if let Some(arr) = val.get::>() { + for item in arr { + Self::collect_env_segments(item, segments)?; } + } else { + let parsed_segment = Self::value_to_os_string(val, "'env' 节点的 Value")?; + segments.push(parsed_segment); } Ok(()) } } -/// 【纯粹数据转换与展开】将已通过校验的 Value 递归解析为 OsString 动态数组 -pub fn collect_value_into( - context: &impl fmt::Display, - value: &Value, - out: &mut Vec, -) -> mlua::Result<()> { - match value { - Value::Nil => {} - Value::String(s) => out.push(lua_string_2_os_string(s)?), - Value::Integer(i) => out.push(OsString::from(i.to_string())), - Value::Number(n) => { - tracing::warn!(%context, value = %n, "浮点数将按十进制格式转换为字符串"); - out.push(OsString::from(n.to_string())); - } - Value::Boolean(b) => { - tracing::warn!(%context, value = %b, "布尔值将转换为字符串"); - out.push(OsString::from(b.to_string())); - } - Value::Table(tbl) => { - let mut index = 1i64; - loop { - let item: Value = tbl.raw_get(index)?; - if matches!(item, Value::Nil) { - break; - } - collect_value_into(context, &item, out)?; - index += 1; - } - } - _ => unreachable!("传入收集器的 Value 应已通过 validate 校验"), - } - Ok(()) -} -/// 递归将任意 Lua Value 展开为扁平的字符串片段列表 -// fn collect_env_segments(value: Value, out: &mut Vec) -> mlua::Result<()>{OK(())} - -#[derive(Debug, Clone, Default)] -pub struct ShimConfig { - pub target: PathBuf, - pub args: Vec, - pub env: HashMap, -} - -impl ShimConfig { - /// 根据配置快速构建准备执行的 Command 对象 - pub fn to_command(&self, runtime_args: I) -> Command - where - I: IntoIterator, - S: AsRef, - { - let mut cmd = Command::new(&self.target); - - // 1. 先追加配置里固定的默认参数 (例如: node, --max-old-space-size=4096) - cmd.args(&self.args); - - // 2. 透传外部动态运行时参数 - cmd.args(runtime_args); - - for (key, val) in &self.env { - // 直接应用环境变量(Lua 端已经处理好字符串拼接或列表合并) - cmd.env(key, val); - } - - cmd - } -} - -/// 实现 FromLua Trait,由 mlua 自动处理 Table 转换 -impl FromLua for ShimConfig { - fn from_lua(value: Value, _lua: &Lua) -> mlua::Result { - // 脚本返回必须是一个 Table 变体 - let table = match value { - Value::Table(t) => t, - _ => { - return Err(conversion_error(format!( - "期望得到一个 Lua Table 配置对象,实际是 {}", - value.type_name() - ))); - } - }; - - // 必填字段: target(严格限定为字符串,避免数字被 mlua 宽松转为字符串后掩盖错误) - let target = match table.get::>("target")? { - None | Some(Value::Nil) => { - return Err(conversion_error("缺少必填字段 target(应为字符串路径)")); - } - Some(target_val) => ValueValidator::parse_target(&target_val)?, - // Value::String(s) => lua_string_2_os_string(&s)?, - // Value::Nil => { - // return Err(conversion_error("缺少必填字段 target(应为字符串路径)")); - // } - // other => { - // return Err(conversion_error(format!( - // "target 需为有效的路径且类型必须是字符串,实际类型是 {}", - // other.type_name() - // ))); - // } - }; - // let mut args = Vec::new(); - - // 可选字段: args(缺失或 nil 时默认空列表;保留空字符串参数,与提权路径的 "" 语义一致) - let args = match table.get::>("args")? { - None | Some(Value::Nil) => Vec::new(), - Some(args_val) => ValueValidator::parse_args(&args_val)?, - }; - - // 可选字段: env(只允许缺失/nil,其他类型由 Option
转换报错,不再静默忽略) - let env = match table.get::>("env")? { - None | Some(Value::Nil) => HashMap::new(), - Some(ref env_val @ Value::Table(_)) => ValueValidator::parse_env(&env_val)?, - Some(other) => { - return Err(conversion_error(format!( - "env 必须是键值表 (table),实际类型是 {}", - other.type_name() - ))); - } - }; - println!("环境变量结果:{:?}", env); - Ok(ShimConfig { target, args, env }) - } -} - #[cfg(test)] mod tests { use super::*; + use crate::ShimSpec; + use mlua::{FromLua, Lua}; - fn parse(src: &str) -> mlua::Result { + fn parse(src: &str) -> mlua::Result { let lua = Lua::new(); // 模拟 runtime 注入的 get_env:返回按平台分隔符拆分的段数组(空变量返回空表) let get_env = lua @@ -470,7 +466,7 @@ mod tests { lua.globals().set("get_env", get_env).unwrap(); let value = lua.load(src).eval::()?; - let t = ShimConfig::from_lua(value, &lua); + let t = ShimSpec::from_lua(value, &lua); println!("读取出的数据:{:?}", t.clone()?); t } @@ -481,7 +477,7 @@ mod tests { r#" return { target = "C:/tools/git.exe", - args = { "--no-pager"}, + args = { "--no-pager",2}, env = { PATH = { "C:/tools/git/bin", "C:/Windows", get_env("PATH")}, HOME = "C:/tools/home", @@ -494,7 +490,7 @@ mod tests { ) .unwrap(); assert_eq!(cfg.target, PathBuf::from("C:/tools/git.exe")); - assert_eq!(cfg.args, vec!["--no-pager"]); + assert_eq!(cfg.args, vec!["--no-pager", "2"]); assert_eq!(cfg.env.get("HOME").unwrap().to_str(), Some("C:/tools/home")); // PATH 前缀来自配置,随后附加 get_env("PATH") 拆出的宿主 PATH 段 let path = cfg.env.get("PATH").unwrap().to_str().unwrap();