Skip to content

Commit b8cf113

Browse files
committed
refactor: deserialize config DSL with Serde
Replace pest with quick-xml and a Serde wire model while preserving XML v3 metadata, projection and redaction. Check the restricted XML syntax and resource limits before recursive deserialization, retain source diagnostics, and add compatibility regressions. Validated Rust tests, formatting, native and WASM Clippy, release build, browser regressions, format round trips, real TUIC parsing and loopback transport. Assisted-by: Codex:gpt-6
1 parent 0651b33 commit b8cf113

10 files changed

Lines changed: 384 additions & 171 deletions

File tree

‎Cargo.lock‎

Lines changed: 11 additions & 50 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,14 @@ BROWSER_CHANNEL=chromium node tests/config-generator/browser.mjs
8989

9090
## DSL 与维护约定
9191

92-
[Config DSL v3](docs/tools/config-dsl.md) 使用独立的 `config-generator/schema/tuic.xml` 静态描述输入、默认值、枚举、条件、列表、映射及敏感字段,由 pest 解析,不使用 Rust 宏或闭包编写配置描述。Leptos 读取同一份元数据生成表单;通用投影与脱敏在 `dsl.rs`,跨字段 TUIC 校验在 `validation.rs`。新增普通字段时编辑 XML、说明及相关测试。
92+
[Config DSL v3](docs/tools/config-dsl.md) 使用独立的 `config-generator/schema/tuic.xml` 静态描述输入、默认值、枚举、条件、列表、映射及敏感字段,由 quick-xml + Serde 反序列化,不使用 Rust 宏或闭包编写配置描述。Leptos 读取同一份元数据生成表单;通用投影与脱敏在 `dsl.rs`,跨字段 TUIC 校验在 `validation.rs`。新增普通字段时编辑 XML、说明及相关测试。
9393

9494
| 路径 | 内容 |
9595
| --- | --- |
9696
| `Cargo.toml` / `Cargo.lock` | 生成器 Rust workspace 与锁定依赖 |
9797
| `config-generator/` | 可独立构建的 Rust + Leptos 单页应用 |
9898
| `config-generator/schema/tuic.xml` | 静态 XML 配置描述,字段、初值、条件与输出的来源 |
99-
| `config-generator/src/dsl/xml.pest` / `dsl/parser.rs` | pest 语法与描述检查 |
99+
| `config-generator/src/dsl/xml.rs` / `dsl/wire.rs` / `dsl/parser.rs` | XML 子集检查、Serde 数据模型与语义校验 |
100100
| `config-generator/src/dsl.rs` | 数据投影、类型检查与脱敏 |
101101
| `config-generator/src/schema.rs` | 嵌入 XML、缓存解析结果与表单状态绑定 |
102102
| `config-generator/src/model.rs` | 配置生成入口及三种格式序列化 |

‎config-generator/Cargo.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ serde = { version = "1", features = ["derive"] }
99
serde_json = { version = "1", features = ["preserve_order"] }
1010
toml = "0.9"
1111
uuid = "1"
12-
pest = "2.8"
13-
pest_derive = "2.8"
12+
# v3 preserves attribute whitespace; newer XML normalization changes that contract.
13+
quick-xml = { version = "0.38.4", features = ["serialize"] }
1414

1515
[target.'cfg(target_arch = "wasm32")'.dependencies]
1616
leptos = { version = "0.8.20", features = ["csr"] }

‎config-generator/src/dsl.rs‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1-
//! Static XML configuration descriptions, parsed by pest. No callbacks or scripts.
1+
//! Static XML configuration descriptions, deserialized with Serde. No callbacks or scripts.
22
mod parser;
3+
mod wire;
4+
mod xml;
35

46
use std::{
57
collections::{BTreeMap, BTreeSet},

‎config-generator/src/dsl/parser.rs‎

Lines changed: 2 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,8 @@
1-
use pest::{Parser, error::LineColLocation, iterators::Pair};
2-
31
use super::*;
42

5-
#[derive(pest_derive::Parser)]
6-
#[grammar = "dsl/xml.pest"]
7-
struct XmlParser;
8-
93
pub(super) fn parse(source: &str) -> Result<Document, DslError> {
10-
if source.len() > 1_048_576 {
11-
return Err(DslError("描述文件超过 1 MiB".into()));
12-
}
13-
if !source.chars().all(xml_char) {
14-
return Err(DslError("描述包含无效 XML 字符".into()));
15-
}
16-
let mut pairs = XmlParser::parse(Rule::document, source).map_err(|e| {
17-
let (line, column) = match e.line_col {
18-
LineColLocation::Pos(p) | LineColLocation::Span(p, _) => p,
19-
};
20-
DslError(format!("{line}:{column} XML 语法错误"))
21-
})?;
22-
let document = pairs.next().ok_or_else(|| DslError("缺少文档".into()))?;
23-
let root_pair = document
24-
.into_inner()
25-
.find(|p| p.as_rule() == Rule::element)
26-
.ok_or_else(|| DslError("缺少根元素".into()))?;
27-
let root = element(root_pair, 0)?;
4+
let positions = xml::inspect(source)?;
5+
let root = wire::deserialize(source, positions)?;
286
if root.tag != "config-dsl" {
297
return Err(root.error("根元素必须为 config-dsl"));
308
}
@@ -124,85 +102,6 @@ pub(super) fn parse(source: &str) -> Result<Document, DslError> {
124102
}
125103
Ok(doc)
126104
}
127-
fn element(pair: Pair<'_, Rule>, depth: usize) -> Result<Element, DslError> {
128-
let (line, column) = pair.as_span().start_pos().line_col();
129-
if depth > 64 {
130-
return Err(DslError(format!("{line}:{column} 元素嵌套超过 64 层")));
131-
}
132-
let pair = if pair.as_rule() == Rule::element {
133-
pair.into_inner().next().ok_or_else(|| DslError("缺少元素".into()))?
134-
} else {
135-
pair
136-
};
137-
let mut result = Element {
138-
tag: String::new(),
139-
attrs: BTreeMap::new(),
140-
children: Vec::new(),
141-
line,
142-
column,
143-
};
144-
for part in pair.into_inner() {
145-
match part.as_rule() {
146-
Rule::name => {
147-
if result.tag.is_empty() {
148-
result.tag = part.as_str().into();
149-
} else if result.tag != part.as_str() {
150-
return Err(result.error("开始与结束标签不匹配"));
151-
}
152-
}
153-
Rule::attribute => {
154-
let mut parts = part.into_inner();
155-
let name = parts.next().ok_or_else(|| result.error("缺少属性名"))?;
156-
let quoted = parts.next().ok_or_else(|| result.error("缺少属性值"))?;
157-
let text = quoted.as_str();
158-
let value = decode(&result, &text[1..text.len() - 1])?;
159-
if result.attrs.insert(name.as_str().into(), value).is_some() {
160-
return Err(result.error("重复属性"));
161-
}
162-
}
163-
Rule::element => result.children.push(element(part, depth + 1)?),
164-
_ => {}
165-
}
166-
}
167-
Ok(result)
168-
}
169-
fn xml_char(c: char) -> bool {
170-
matches!(c as u32, 0x9 | 0xa | 0xd | 0x20..=0xd7ff | 0xe000..=0xfffd | 0x10000..=0x10ffff)
171-
}
172-
fn decode(node: &Element, text: &str) -> Result<String, DslError> {
173-
let mut result = String::new();
174-
let mut rest = text;
175-
while let Some(i) = rest.find('&') {
176-
result.push_str(&rest[..i]);
177-
rest = &rest[i + 1..];
178-
let end = rest.find(';').ok_or_else(|| node.error("实体缺少分号"))?;
179-
let entity = &rest[..end];
180-
let ch = match entity {
181-
"amp" => Some('&'),
182-
"lt" => Some('<'),
183-
"gt" => Some('>'),
184-
"quot" => Some('"'),
185-
"apos" => Some('\''),
186-
_ => {
187-
let number = if let Some(hex) = entity.strip_prefix("#x") {
188-
u32::from_str_radix(hex, 16).ok()
189-
} else {
190-
entity.strip_prefix('#').and_then(|n| n.parse::<u32>().ok())
191-
};
192-
number.and_then(char::from_u32)
193-
}
194-
}
195-
.filter(|c| xml_char(*c))
196-
.ok_or_else(|| node.error("不支持的实体或字符引用"))?;
197-
result.push(ch);
198-
rest = &rest[end + 1..];
199-
}
200-
result.push_str(rest);
201-
if !result.chars().all(xml_char) {
202-
return Err(node.error("无效 XML 字符"));
203-
}
204-
Ok(result)
205-
}
206105
fn attrs(node: &Element, allowed: &[&str]) -> Result<(), DslError> {
207106
if node.attrs.keys().any(|k| !allowed.contains(&k.as_str())) {
208107
return Err(node.error("含有未知属性"));

‎config-generator/src/dsl/wire.rs‎

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
//! Serde wire model. Attributes remain lexical strings until semantic validation:
2+
//! this preserves v3's strict booleans, numeric rules, and empty values.
3+
use std::{collections::BTreeMap, fmt};
4+
5+
use serde::{
6+
Deserialize, Deserializer,
7+
de::{self, MapAccess, Visitor},
8+
};
9+
10+
use super::{DslError, Element};
11+
12+
#[derive(Deserialize)]
13+
#[serde(rename_all = "kebab-case")]
14+
enum Node {
15+
ConfigDsl(Body),
16+
Inputs(Body),
17+
Conditions(Body),
18+
Values(Body),
19+
Outputs(Body),
20+
Field(Body),
21+
Option(Body),
22+
Collection(Body),
23+
Condition(Body),
24+
Value(Body),
25+
All(Body),
26+
Any(Body),
27+
Not(Body),
28+
Use(Body),
29+
Eq(Body),
30+
Truthy(Body),
31+
Ip(Body),
32+
Source(Body),
33+
Coalesce(Body),
34+
Endpoint(Body),
35+
Select(Body),
36+
Object(Body),
37+
List(Body),
38+
Record(Body),
39+
String(Body),
40+
Boolean(Body),
41+
Integer(Body),
42+
Enum(Body),
43+
}
44+
45+
// A streaming Serde visitor keeps recursive stack use independent of the number
46+
// of possible attributes. parser.rs validates permitted attributes per construct.
47+
struct Body {
48+
attrs: BTreeMap<String, String>,
49+
children: Vec<Node>,
50+
}
51+
52+
impl<'de> Deserialize<'de> for Body {
53+
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
54+
struct BodyVisitor;
55+
impl<'de> Visitor<'de> for BodyVisitor {
56+
type Value = Body;
57+
58+
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
59+
formatter.write_str("XML attributes and ordered child elements")
60+
}
61+
62+
fn visit_map<A: MapAccess<'de>>(self, mut map: A) -> Result<Body, A::Error> {
63+
let mut attrs = BTreeMap::new();
64+
let mut children = None;
65+
while let Some(key) = map.next_key::<String>()? {
66+
if let Some(name) = key.strip_prefix('@') {
67+
if attrs.insert(name.to_owned(), map.next_value()?).is_some() {
68+
return Err(de::Error::custom("duplicate XML attribute"));
69+
}
70+
} else if key == "$value" && children.is_none() {
71+
children = Some(map.next_value()?);
72+
} else {
73+
return Err(de::Error::custom("unexpected XML content"));
74+
}
75+
}
76+
Ok(Body {
77+
attrs,
78+
children: children.unwrap_or_default(),
79+
})
80+
}
81+
}
82+
// $value asks quick-xml to deserialize heterogeneous children as one sequence.
83+
deserializer.deserialize_struct("Body", &["$value"], BodyVisitor)
84+
}
85+
}
86+
87+
impl Node {
88+
fn into_element(self, positions: &mut impl Iterator<Item = (usize, usize)>) -> Result<Element, DslError> {
89+
let (tag, body) = match self {
90+
Self::ConfigDsl(body) => ("config-dsl", body),
91+
Self::Inputs(body) => ("inputs", body),
92+
Self::Conditions(body) => ("conditions", body),
93+
Self::Values(body) => ("values", body),
94+
Self::Outputs(body) => ("outputs", body),
95+
Self::Field(body) => ("field", body),
96+
Self::Option(body) => ("option", body),
97+
Self::Collection(body) => ("collection", body),
98+
Self::Condition(body) => ("condition", body),
99+
Self::Value(body) => ("value", body),
100+
Self::All(body) => ("all", body),
101+
Self::Any(body) => ("any", body),
102+
Self::Not(body) => ("not", body),
103+
Self::Use(body) => ("use", body),
104+
Self::Eq(body) => ("eq", body),
105+
Self::Truthy(body) => ("truthy", body),
106+
Self::Ip(body) => ("ip", body),
107+
Self::Source(body) => ("source", body),
108+
Self::Coalesce(body) => ("coalesce", body),
109+
Self::Endpoint(body) => ("endpoint", body),
110+
Self::Select(body) => ("select", body),
111+
Self::Object(body) => ("object", body),
112+
Self::List(body) => ("list", body),
113+
Self::Record(body) => ("record", body),
114+
Self::String(body) => ("string", body),
115+
Self::Boolean(body) => ("boolean", body),
116+
Self::Integer(body) => ("integer", body),
117+
Self::Enum(body) => ("enum", body),
118+
};
119+
let (line, column) = positions.next().ok_or_else(|| DslError("缺少 XML 元素位置".into()))?;
120+
let attrs = body.attrs;
121+
let children = body
122+
.children
123+
.into_iter()
124+
.map(|node| node.into_element(positions))
125+
.collect::<Result<_, _>>()?;
126+
Ok(Element {
127+
tag: tag.into(),
128+
attrs,
129+
children,
130+
line,
131+
column,
132+
})
133+
}
134+
}
135+
136+
pub(super) fn deserialize(source: &str, positions: Vec<(usize, usize)>) -> Result<Element, DslError> {
137+
let mut deserializer = quick_xml::de::Deserializer::from_str(source);
138+
let node = Node::deserialize(&mut deserializer).map_err(|_| {
139+
// Serde errors may contain attribute values. Only expose a source position.
140+
let offset = deserializer.get_ref().get_ref().buffer_position() as usize;
141+
let prefix = &source[..offset.min(source.len())];
142+
let line = prefix.bytes().filter(|b| *b == b'\n').count() + 1;
143+
let column = prefix.rsplit('\n').next().map_or(1, |s| s.chars().count() + 1);
144+
DslError(format!("{line}:{column} XML 描述反序列化失败(未知标签、属性或无效结构)"))
145+
})?;
146+
node.into_element(&mut positions.into_iter())
147+
}

0 commit comments

Comments
 (0)