This commit is contained in:
2026-01-28 23:57:04 +08:00
parent 1bb2ca0f40
commit 8caed3c8b4
6 changed files with 416 additions and 27 deletions

56
Cargo.lock generated
View File

@@ -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"

View File

@@ -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"

View File

@@ -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");
}

324
src/config.rs Normal file
View File

@@ -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<Box<Config>>;
/// 保存配置文件
fn save_config(cfg: &Box<Config>, path: &CxxString) -> Result<()>;
/// 做不做每日任务
fn is_daily_missions(cfg: &Box<Config>) -> bool;
fn set_daily_missions(cfg: &mut Box<Config>, value: bool);
/// 做不做群岛
fn is_archipelago(cfg: &Box<Config>) -> bool;
fn set_archipelago(cfg: &mut Box<Config>, value: bool);
/// 做不做深渊
fn is_spiral_abyss(cfg: &Box<Config>) -> bool;
fn set_spiral_abyss(cfg: &mut Box<Config>, value: bool);
/// 做不做妖气
fn is_demonic_energy(cfg: &Box<Config>) -> bool;
fn set_demonic_energy(cfg: &mut Box<Config>, value: bool);
/// 森林
fn is_forest(cfg: &Box<Config>) -> bool;
fn set_forest(cfg: &mut Box<Config>, value: bool);
/// 分解
fn is_decompose(cfg: &Box<Config>) -> bool;
fn set_decompose(cfg: &mut Box<Config>, 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<Path>) -> 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<Path>) -> Result<Self, Box<dyn std::error::Error>> {
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<Config>, Box<dyn std::error::Error>> {
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<Config>,
path: &CxxString,
) -> Result<(), Box<dyn std::error::Error>> {
let path = cxx_string_to_string(path);
this.save_with_comments(path)?;
Ok(())
}
pub(crate) fn is_daily_missions(this: &Box<Config>) -> bool {
this.is_daily_missions
}
pub(crate) fn set_daily_missions(this: &mut Box<Config>, flag: bool) {
this.is_daily_missions = flag;
}
pub(crate) fn is_archipelago(this: &Box<Config>) -> bool {
this.is_archipelago
}
pub(crate) fn set_archipelago(this: &mut Box<Config>, flag: bool) {
this.is_archipelago = flag;
}
pub(crate) fn is_spiral_abyss(this: &Box<Config>) -> bool {
this.is_spiral_abyss
}
pub(crate) fn set_spiral_abyss(this: &mut Box<Config>, flag: bool) {
this.is_spiral_abyss = flag;
}
pub(crate) fn is_demonic_energy(this: &Box<Config>) -> bool {
this.is_demonic_energy
}
pub(crate) fn set_demonic_energy(this: &mut Box<Config>, flag: bool) {
this.is_demonic_energy = flag;
}
pub(crate) fn is_forest(this: &Box<Config>) -> bool {
this.is_forest
}
pub(crate) fn set_forest(this: &mut Box<Config>, flag: bool) {
this.is_forest = flag;
}
pub(crate) fn is_decompose(this: &Box<Config>) -> bool {
this.is_decompose
}
pub(crate) fn set_decompose(this: &mut Box<Config>, 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
}

View File

@@ -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<String, Box<dyn std::error::Error>>
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()
}

29
src/utils.rs Normal file
View File

@@ -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()
}