From 8caed3c8b4571ee0c5925e225fb82cbcee5ebafb Mon Sep 17 00:00:00 2001 From: Starpoles Date: Wed, 28 Jan 2026 23:57:04 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8D=87=E7=BA=A7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 56 +++++++++ Cargo.toml | 2 + build.rs | 3 +- src/config.rs | 324 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 29 +---- src/utils.rs | 29 +++++ 6 files changed, 416 insertions(+), 27 deletions(-) create mode 100644 src/config.rs create mode 100644 src/utils.rs diff --git a/Cargo.lock b/Cargo.lock index 592e645..639d740 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,7 +18,9 @@ dependencies = [ "libc", "log", "reqwest", + "serde", "tokio", + "toml", "tracing-appender", ] @@ -1258,6 +1260,15 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + [[package]] name = "sharded-slab" version = "0.1.7" @@ -1491,6 +1502,45 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml" +version = "0.9.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "0.7.5+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + [[package]] name = "tower" version = "0.5.3" @@ -1899,6 +1949,12 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" + [[package]] name = "wit-bindgen" version = "0.51.0" diff --git a/Cargo.toml b/Cargo.toml index 99e5c67..3d0ad6d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,8 @@ colored = "3.1.1" env_logger = "0.11.8" encoding_rs = "0.8.35" tracing-appender = "0.2.4" +serde = { version = "1.0.228", features = ["derive"] } +toml = "0.9.11" [build-dependencies] cxx-build = "1.0.192" diff --git a/build.rs b/build.rs index c4103ba..663b037 100644 --- a/build.rs +++ b/build.rs @@ -1,6 +1,7 @@ fn main() { - cxx_build::bridge("src/lib.rs") + cxx_build::bridges(["src/lib.rs", "src/config.rs"]) .std("c++20") .compile("dnf_utils"); println!("cargo:rerun-if-changed=src/lib.rs"); + println!("cargo:rerun-if-changed=src/config.rs"); } diff --git a/src/config.rs b/src/config.rs new file mode 100644 index 0000000..ae30fe6 --- /dev/null +++ b/src/config.rs @@ -0,0 +1,324 @@ +use crate::utils::cxx_string_to_string; +use cxx::CxxString; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; +use std::path::Path; +use std::{fs, io}; +use toml::Value; + +#[cxx::bridge] +mod ffi { + extern "Rust" { + type Config; + + /// 载入配置文件 + fn load_or_create(path: &CxxString) -> Result>; + /// 保存配置文件 + fn save_config(cfg: &Box, path: &CxxString) -> Result<()>; + + /// 做不做每日任务 + fn is_daily_missions(cfg: &Box) -> bool; + fn set_daily_missions(cfg: &mut Box, value: bool); + + /// 做不做群岛 + fn is_archipelago(cfg: &Box) -> bool; + fn set_archipelago(cfg: &mut Box, value: bool); + + /// 做不做深渊 + fn is_spiral_abyss(cfg: &Box) -> bool; + fn set_spiral_abyss(cfg: &mut Box, value: bool); + + /// 做不做妖气 + fn is_demonic_energy(cfg: &Box) -> bool; + fn set_demonic_energy(cfg: &mut Box, value: bool); + + /// 森林 + fn is_forest(cfg: &Box) -> bool; + fn set_forest(cfg: &mut Box, value: bool); + + /// 分解 + fn is_decompose(cfg: &Box) -> bool; + fn set_decompose(cfg: &mut Box, value: bool); + + /// 从配置文件中读取一个配置项 + fn get_config_value( + path: &CxxString, + path_name: &CxxString, + key: &CxxString, + default_value: &CxxString, + ) -> String; + + /// 设置配置文件中的一个配置项 + fn set_config_value( + path: &CxxString, + path_name: &CxxString, + key: &CxxString, + value: &CxxString, + ) -> bool; + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(default)] +pub struct Config { + /// 做不做每日任务 + is_daily_missions: bool, + /// 做不做群岛 + is_archipelago: bool, + /// 做不做深渊 + is_spiral_abyss: bool, + /// 做不做妖气 + is_demonic_energy: bool, + /// 森林 + is_forest: bool, + /// 分解 + is_decompose: bool, +} + +impl Default for Config { + fn default() -> Self { + Self { + is_daily_missions: false, + is_archipelago: false, + is_spiral_abyss: false, + is_demonic_energy: false, + is_forest: false, + is_decompose: false, + } + } +} + +impl Config { + /// 生成带“文档注释”的 toml 文本(toml/serde 不会自动输出 Rust 的 /// 注释,所以这里手动拼) + pub fn to_toml_with_comments(&self) -> String { + // 你也可以加一个总注释、版本号等 + format!( + r#"# 自动生成的配置文件 +# 实现可能修改配置存储结构,请不要使用程序化工具修改此配置文件,如果非要这么做,请链接utils.lib后,使用其提供的接口。 + +# 做不做每日任务 +is_daily_missions = {is_daily_missions} + +# 做不做群岛 +is_archipelago = {is_archipelago} + +# 做不做深渊 +is_spiral_abyss = {is_spiral_abyss} + +# 做不做妖气 +is_demonic_energy = {is_demonic_energy} + +# 森林 +is_forest = {is_forest} + +# 分解 +is_decompose = {is_decompose} +"#, + is_daily_missions = self.is_daily_missions, + is_archipelago = self.is_archipelago, + is_spiral_abyss = self.is_spiral_abyss, + is_demonic_energy = self.is_demonic_energy, + is_forest = self.is_forest, + is_decompose = self.is_decompose, + ) + } + + pub fn save_with_comments(&self, path: impl AsRef) -> io::Result<()> { + let path = path.as_ref(); + if let Some(parent) = path.parent() { + fs::create_dir_all(parent)?; + } + fs::write(path, self.to_toml_with_comments()) + } + + /// 读取配置:不存在则创建(写入默认值+注释),存在则从文件构造实例。 + /// + /// - toml 缺字段:会自动用 Default 补齐(因为 #[serde(default)]) + /// - 可选:如果你想“读取后把缺的字段补齐回写”,这里也做了回写 + pub fn load_or_create(path: impl AsRef) -> Result> { + let path = path.as_ref(); + + if !path.exists() { + let cfg = Self::default(); + cfg.save_with_comments(path)?; + return Ok(cfg); + } + + let text = fs::read_to_string(path)?; + let cfg: Self = toml::from_str(&text)?; + + // 可选:自动回写补全(比如用户旧版配置少字段时,补齐并带注释写回去) + // 如果你不想覆盖用户文件,把下面两行删掉即可。 + cfg.save_with_comments(path)?; + + Ok(cfg) + } +} + +pub(crate) fn load_or_create(path: &CxxString) -> Result, Box> { + let path = cxx_string_to_string(path); + let config = Config::load_or_create(&path)?; + Ok(Box::new(config)) +} + +pub(crate) fn save_config( + this: &Box, + path: &CxxString, +) -> Result<(), Box> { + let path = cxx_string_to_string(path); + this.save_with_comments(path)?; + Ok(()) +} + +pub(crate) fn is_daily_missions(this: &Box) -> bool { + this.is_daily_missions +} + +pub(crate) fn set_daily_missions(this: &mut Box, flag: bool) { + this.is_daily_missions = flag; +} + +pub(crate) fn is_archipelago(this: &Box) -> bool { + this.is_archipelago +} + +pub(crate) fn set_archipelago(this: &mut Box, flag: bool) { + this.is_archipelago = flag; +} + +pub(crate) fn is_spiral_abyss(this: &Box) -> bool { + this.is_spiral_abyss +} + +pub(crate) fn set_spiral_abyss(this: &mut Box, flag: bool) { + this.is_spiral_abyss = flag; +} + +pub(crate) fn is_demonic_energy(this: &Box) -> bool { + this.is_demonic_energy +} + +pub(crate) fn set_demonic_energy(this: &mut Box, flag: bool) { + this.is_demonic_energy = flag; +} + +pub(crate) fn is_forest(this: &Box) -> bool { + this.is_forest +} + +pub(crate) fn set_forest(this: &mut Box, flag: bool) { + this.is_forest = flag; +} + +pub(crate) fn is_decompose(this: &Box) -> bool { + this.is_decompose +} + +pub(crate) fn set_decompose(this: &mut Box, flag: bool) { + this.is_decompose = flag; +} + +pub(crate) fn set_config_value( + path: &CxxString, + path_name: &CxxString, + key: &CxxString, + value: &CxxString, +) -> bool { + let path_str = cxx_string_to_string(path); + let path_name_str = cxx_string_to_string(path_name); + let key_str = cxx_string_to_string(key); + let value_str = cxx_string_to_string(value); + + // 创建父目录(如果不存在) + let path = Path::new(&path_str); + if let Some(parent) = path.parent() { + if let Err(_) = fs::create_dir_all(parent) { + return false; + } + } + + // 读取现有内容或创建新的 TOML 值 + let mut value: Value = if path.exists() { + match fs::read_to_string(&path_str) { + Ok(content) => { + toml::from_str(&content).unwrap_or_else(|_| Value::Table(toml::map::Map::new())) + } + Err(_) => Value::Table(toml::map::Map::new()), + } + } else { + Value::Table(toml::map::Map::new()) + }; + + // 确保根值是一个表 + let root_table = match value.as_table_mut() { + Some(t) => t, + None => return false, + }; + + // 获取或创建 path_name 对应的表 + let path_table = root_table + .entry(&path_name_str) + .or_insert(Value::Table(toml::map::Map::new())) + .as_table_mut(); + + let path_table = match path_table { + Some(t) => t, + None => return false, + }; + + // 设置键值对 + path_table.insert(key_str, Value::String(value_str)); + + // 序列化并写入文件 + let toml_string = match toml::to_string_pretty(&value) { + Ok(s) => s, + Err(_) => return false, + }; + + match fs::write(&path_str, toml_string) { + Ok(_) => true, + Err(_) => false, + } +} + +/// 从配置文件中读取一个配置项 +pub(crate) fn get_config_value( + path: &CxxString, + path_name: &CxxString, + key: &CxxString, + default_value: &CxxString, +) -> String { + let path_str = cxx_string_to_string(path); + let path_name_str = cxx_string_to_string(path_name); + let key_str = cxx_string_to_string(key); + let default_str = cxx_string_to_string(default_value); + + // 读取文件内容 + let content = match fs::read_to_string(&path_str) { + Ok(c) => c, + Err(_) => return default_str, + }; + + // 解析 TOML + let value: Value = match toml::from_str(&content) { + Ok(v) => v, + Err(_) => return default_str, + }; + + // 导航到 path_name 下的 table + let table = match value.get(&path_name_str) { + Some(Value::Table(t)) => t, + _ => return default_str, + }; + + // 获取 key 对应的值 + let result = match table.get(&key_str) { + Some(v) => match v.as_str() { + Some(s) => s.to_string(), + None => v.to_string(), + }, + None => return default_str, + }; + + result +} diff --git a/src/lib.rs b/src/lib.rs index 13f56e0..6ca79e0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,9 +1,12 @@ use ::log::{debug, error, info, trace, warn}; use encoding_rs::Encoding; use tokio::runtime::Runtime; +use crate::utils::cxx_string_to_string; mod log; mod network; +mod config; +pub mod utils; #[cxx::bridge] mod ffi { @@ -76,30 +79,4 @@ fn http_get(url: &cxx::CxxString) -> Result> rt.block_on(network::http_get(&url)) } -fn cxx_string_to_string(s: &cxx::CxxString) -> String { - match s.to_str() { - Ok(s) => return s.to_string(), - Err(_) => {} - }; - // 不是UTF-8,尝试转换 - let candidates = [ - "gb18030", // 覆盖 GBK/GB2312 的常见场景 - "windows-1252", // 西欧常见 - "shift_jis", // 日文常见 - "big5", // 繁体中文常见 - ]; - - let bytes = s.as_bytes(); - - for label in candidates { - let enc = Encoding::for_label(label.as_bytes()).unwrap(); - let (cow, _actual_used, had_errors) = enc.decode(bytes); - if !had_errors { - return cow.into_owned(); - } - } - - /// 编码全部没有命中的话,则丢弃无法解析的部分 - s.to_string_lossy().into_owned() -} diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..8e1e3d6 --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,29 @@ +use encoding_rs::Encoding; + +pub fn cxx_string_to_string(s: &cxx::CxxString) -> String { + match s.to_str() { + Ok(s) => return s.to_string(), + Err(_) => {} + }; + + // 不是UTF-8,尝试转换 + let candidates = [ + "gb18030", // 覆盖 GBK/GB2312 的常见场景 + "windows-1252", // 西欧常见 + "shift_jis", // 日文常见 + "big5", // 繁体中文常见 + ]; + + let bytes = s.as_bytes(); + + for label in candidates { + let enc = Encoding::for_label(label.as_bytes()).unwrap(); + let (cow, _actual_used, had_errors) = enc.decode(bytes); + if !had_errors { + return cow.into_owned(); + } + } + + // 编码全部没有命中的话,则丢弃无法解析的部分 + s.to_string_lossy().into_owned() +} \ No newline at end of file