1use zerodds_ast::PrimitiveKind;
27use zerodds_types::dynamic::descriptor::TypeKind;
28use zerodds_types::dynamic::{DynamicData, DynamicType};
29use zerodds_xml_wire::emitter::XmlEmitter;
30use zerodds_xml_wire::parser::{Event, XmlParser};
31
32#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum DynXmlError {
35 Malformed(String),
38 BadLiteral {
40 at: String,
42 text: String,
44 },
45 Unsupported(&'static str),
52 Model(String),
55 TooDeep(String),
58}
59
60const MAX_DEPTH: usize = 64;
64
65impl std::fmt::Display for DynXmlError {
66 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
67 match self {
68 Self::Malformed(m) => write!(f, "malformed XML body: {m}"),
69 Self::BadLiteral { at, text } => write!(f, "bad literal at `{at}`: `{text}`"),
70 Self::Unsupported(k) => write!(f, "XML conversion not implemented for {k}"),
71 Self::Model(m) => write!(f, "type model: {m}"),
72 Self::TooDeep(at) => write!(f, "`{at}`: nesting exceeds {MAX_DEPTH} levels"),
73 }
74 }
75}
76
77impl std::error::Error for DynXmlError {}
78
79#[derive(Debug, Clone, Default)]
82struct XmlNode {
83 name: String,
84 children: Vec<XmlNode>,
85 text: String,
86}
87
88fn parse_root(xml: &str) -> Result<XmlNode, DynXmlError> {
95 let mut stack: Vec<XmlNode> = Vec::new();
96 let mut root: Option<XmlNode> = None;
97 for event in XmlParser::new(xml) {
98 let event = event.map_err(|e| DynXmlError::Malformed(e.to_string()))?;
99 match event {
100 Event::Declaration(_) | Event::CData(_) => {}
101 Event::StartElement { name, .. } => {
102 if stack.len() > MAX_DEPTH {
104 return Err(DynXmlError::TooDeep(name));
105 }
106 stack.push(XmlNode {
107 name,
108 children: Vec::new(),
109 text: String::new(),
110 });
111 }
112 Event::Text(text) => {
113 if let Some(top) = stack.last_mut() {
114 top.text.push_str(text.trim());
115 }
116 }
117 Event::EndElement(name) => {
118 let node = stack.pop().ok_or_else(|| {
119 DynXmlError::Malformed(format!("unmatched close tag `{name}`"))
120 })?;
121 if node.name != name {
122 return Err(DynXmlError::Malformed(format!(
123 "close tag `{name}` does not match open tag `{}`",
124 node.name
125 )));
126 }
127 match stack.last_mut() {
128 Some(parent) => parent.children.push(node),
129 None => root = Some(node),
130 }
131 }
132 }
133 }
134 root.ok_or_else(|| DynXmlError::Malformed("no root element".into()))
135}
136
137pub fn encode_value(
143 emitter: &mut XmlEmitter,
144 tag: &str,
145 value: &DynamicData,
146 ty: &DynamicType,
147) -> Result<(), DynXmlError> {
148 encode_at(emitter, tag, value, ty, 0)
149}
150
151fn encode_at(
154 emitter: &mut XmlEmitter,
155 tag: &str,
156 value: &DynamicData,
157 ty: &DynamicType,
158 depth: usize,
159) -> Result<(), DynXmlError> {
160 if depth > MAX_DEPTH {
161 return Err(DynXmlError::TooDeep(tag.to_string()));
162 }
163 let kind = ty.kind();
164 match kind {
165 TypeKind::Alias => {
166 let target = ty
167 .resolve_alias()
168 .map_err(|e| DynXmlError::Model(e.to_string()))?;
169 return encode_at(emitter, tag, value, &target, depth + 1);
170 }
171 TypeKind::Structure => {
172 emitter
173 .start_element(tag, &[])
174 .map_err(|e| DynXmlError::Malformed(e.to_string()))?;
175 for member in ty.members() {
176 let member_ty = member
177 .dynamic_type()
178 .map_err(|e| DynXmlError::Model(e.to_string()))?;
179 let member_value = value.member(member.name()).ok_or_else(|| {
180 DynXmlError::Malformed(format!("missing member `{}`", member.name()))
181 })?;
182 encode_at(emitter, member.name(), member_value, &member_ty, depth + 1)?;
183 }
184 emitter
185 .end_element()
186 .map_err(|e| DynXmlError::Malformed(e.to_string()))?;
187 }
188 TypeKind::Union => {
189 let disc_ty = ty
190 .discriminator_type()
191 .ok_or_else(|| DynXmlError::Model("union with no discriminator".into()))?;
192 let (disc_entry, active) = value
193 .members()
194 .and_then(|m| m.first())
195 .zip(value.members().and_then(|m| m.get(1)))
196 .ok_or_else(|| DynXmlError::Malformed("union value has no case".into()))?;
197 emitter
198 .start_element(tag, &[])
199 .map_err(|e| DynXmlError::Malformed(e.to_string()))?;
200 encode_at(emitter, "discriminator", &disc_entry.1, &disc_ty, depth + 1)?;
201 let case_name = &active.0;
202 let case_member = ty.member_by_name(case_name).ok_or_else(|| {
203 DynXmlError::Malformed(format!("union has no case `{case_name}`"))
204 })?;
205 let case_ty = case_member
206 .dynamic_type()
207 .map_err(|e| DynXmlError::Model(e.to_string()))?;
208 encode_at(emitter, case_name, &active.1, &case_ty, depth + 1)?;
209 emitter
210 .end_element()
211 .map_err(|e| DynXmlError::Malformed(e.to_string()))?;
212 }
213 TypeKind::Sequence | TypeKind::Array => {
214 let element_ty = ty
215 .element_type()
216 .ok_or_else(|| DynXmlError::Model("collection with no element type".into()))?;
217 emitter
218 .start_element(tag, &[])
219 .map_err(|e| DynXmlError::Malformed(e.to_string()))?;
220 for item in value.items().unwrap_or(&[]) {
221 encode_at(emitter, "item", item, &element_ty, depth + 1)?;
222 }
223 emitter
224 .end_element()
225 .map_err(|e| DynXmlError::Malformed(e.to_string()))?;
226 }
227 TypeKind::Map => return Err(DynXmlError::Unsupported("map")),
228 TypeKind::Float128 => return Err(DynXmlError::Unsupported("long double")),
229 TypeKind::Bitmask | TypeKind::Bitset => {
230 emitter
231 .start_element(tag, &[])
232 .map_err(|e| DynXmlError::Malformed(e.to_string()))?;
233 for item in value.items().into_iter().flatten() {
234 if let DynamicData::Text(name) = item {
235 emitter
236 .start_element("item", &[])
237 .map_err(|e| DynXmlError::Malformed(e.to_string()))?;
238 emitter.text(name);
239 emitter
240 .end_element()
241 .map_err(|e| DynXmlError::Malformed(e.to_string()))?;
242 }
243 }
244 for (name, bit_value) in value.members().into_iter().flatten() {
245 if matches!(bit_value, DynamicData::Boolean(true)) {
246 emitter
247 .start_element("item", &[])
248 .map_err(|e| DynXmlError::Malformed(e.to_string()))?;
249 emitter.text(name);
250 emitter
251 .end_element()
252 .map_err(|e| DynXmlError::Malformed(e.to_string()))?;
253 }
254 }
255 emitter
256 .end_element()
257 .map_err(|e| DynXmlError::Malformed(e.to_string()))?;
258 }
259 _ => {
260 let text = primitive_to_text(value)?;
261 emitter
262 .start_element(tag, &[])
263 .map_err(|e| DynXmlError::Malformed(e.to_string()))?;
264 emitter.text(&text);
265 emitter
266 .end_element()
267 .map_err(|e| DynXmlError::Malformed(e.to_string()))?;
268 }
269 }
270 Ok(())
271}
272
273pub fn to_xml_document(
279 root_tag: &str,
280 value: &DynamicData,
281 ty: &DynamicType,
282) -> Result<String, DynXmlError> {
283 let mut emitter = XmlEmitter::new();
284 emitter.declaration();
285 encode_value(&mut emitter, root_tag, value, ty)?;
286 Ok(emitter.finish())
287}
288
289pub fn from_xml_document(xml: &str, ty: &DynamicType) -> Result<DynamicData, DynXmlError> {
296 let root = parse_root(xml)?;
297 decode_value(&root, ty, &root.name, 0)
298}
299
300fn decode_value(
304 node: &XmlNode,
305 ty: &DynamicType,
306 path: &str,
307 depth: usize,
308) -> Result<DynamicData, DynXmlError> {
309 if depth > MAX_DEPTH {
310 return Err(DynXmlError::TooDeep(path.to_string()));
311 }
312 let kind = ty.kind();
313 match kind {
314 TypeKind::Alias => {
315 let target = ty
316 .resolve_alias()
317 .map_err(|e| DynXmlError::Model(e.to_string()))?;
318 decode_value(node, &target, path, depth + 1)
319 }
320 TypeKind::Structure => {
321 let mut entries = Vec::new();
322 for member in ty.members() {
323 let member_ty = member
324 .dynamic_type()
325 .map_err(|e| DynXmlError::Model(e.to_string()))?;
326 let child = node
327 .children
328 .iter()
329 .find(|c| c.name == member.name())
330 .ok_or_else(|| {
331 DynXmlError::Malformed(format!(
332 "{path}: missing member `{}`",
333 member.name()
334 ))
335 })?;
336 let value = decode_value(
337 child,
338 &member_ty,
339 &format!("{path}.{}", member.name()),
340 depth + 1,
341 )?;
342 entries.push((member.name().to_string(), value));
343 }
344 Ok(DynamicData::Aggregate(entries))
345 }
346 TypeKind::Union => {
347 let disc_ty = ty
348 .discriminator_type()
349 .ok_or_else(|| DynXmlError::Model("union with no discriminator".into()))?;
350 let disc_node = node
351 .children
352 .iter()
353 .find(|c| c.name == "discriminator")
354 .ok_or_else(|| DynXmlError::Malformed(format!("{path}: missing discriminator")))?;
355 let disc_value = decode_value(
356 disc_node,
357 &disc_ty,
358 &format!("{path}.discriminator"),
359 depth + 1,
360 )?;
361 let case_node = node
362 .children
363 .iter()
364 .find(|c| c.name != "discriminator")
365 .ok_or_else(|| DynXmlError::Malformed(format!("{path}: missing active case")))?;
366 let case_member = ty.member_by_name(&case_node.name).ok_or_else(|| {
367 DynXmlError::Malformed(format!("{path}: unknown case `{}`", case_node.name))
368 })?;
369 let case_ty = case_member
370 .dynamic_type()
371 .map_err(|e| DynXmlError::Model(e.to_string()))?;
372 let case_value = decode_value(
373 case_node,
374 &case_ty,
375 &format!("{path}.{}", case_node.name),
376 depth + 1,
377 )?;
378 Ok(DynamicData::Aggregate(vec![
379 ("discriminator".to_string(), disc_value),
380 (case_node.name.clone(), case_value),
381 ]))
382 }
383 TypeKind::Sequence | TypeKind::Array => {
384 let element_ty = ty
385 .element_type()
386 .ok_or_else(|| DynXmlError::Model("collection with no element type".into()))?;
387 let mut items = Vec::with_capacity(node.children.len());
388 for (i, child) in node.children.iter().enumerate() {
389 items.push(decode_value(
390 child,
391 &element_ty,
392 &format!("{path}[{i}]"),
393 depth + 1,
394 )?);
395 }
396 Ok(DynamicData::Collection(items))
397 }
398 TypeKind::Map => Err(DynXmlError::Unsupported("map")),
399 TypeKind::Float128 => Err(DynXmlError::Unsupported("long double")),
400 TypeKind::Bitmask => {
401 let bits = node
402 .children
403 .iter()
404 .map(|c| DynamicData::Text(c.text.clone()))
405 .collect();
406 Ok(DynamicData::Bits(bits))
407 }
408 _ => text_to_primitive(kind, &node.text, path),
409 }
410}
411
412fn primitive_to_text(value: &DynamicData) -> Result<String, DynXmlError> {
415 Ok(match value {
416 DynamicData::Boolean(b) => b.to_string(),
417 DynamicData::Integer { value, .. } => value.to_string(),
418 DynamicData::Float { value, .. } => value.to_string(),
419 DynamicData::Character { value, .. } => value.clone(),
420 DynamicData::Text(s) => s.clone(),
421 DynamicData::Enumerator(name) => name.clone(),
422 DynamicData::Fixed(s) => s.clone(),
423 DynamicData::LongDouble(_) => return Err(DynXmlError::Unsupported("long double")),
424 _ => {
425 return Err(DynXmlError::Malformed(
426 "value is not a primitive/enum".into(),
427 ));
428 }
429 })
430}
431
432const fn primitive_kind_of(kind: TypeKind) -> Option<PrimitiveKind> {
435 Some(match kind {
436 TypeKind::Boolean => PrimitiveKind::Boolean,
437 TypeKind::Byte => PrimitiveKind::Octet,
438 TypeKind::Int8 => PrimitiveKind::Int8,
439 TypeKind::UInt8 => PrimitiveKind::UInt8,
440 TypeKind::Int16 => PrimitiveKind::Int16,
441 TypeKind::UInt16 => PrimitiveKind::UInt16,
442 TypeKind::Int32 => PrimitiveKind::Int32,
443 TypeKind::UInt32 => PrimitiveKind::UInt32,
444 TypeKind::Int64 => PrimitiveKind::Int64,
445 TypeKind::UInt64 => PrimitiveKind::UInt64,
446 TypeKind::Float32 => PrimitiveKind::Float32,
447 TypeKind::Float64 => PrimitiveKind::Float64,
448 TypeKind::Char8 => PrimitiveKind::Char,
449 TypeKind::Char16 => PrimitiveKind::WChar,
450 _ => return None,
451 })
452}
453
454fn text_to_primitive(kind: TypeKind, text: &str, path: &str) -> Result<DynamicData, DynXmlError> {
455 let bad = || DynXmlError::BadLiteral {
456 at: path.to_string(),
457 text: text.to_string(),
458 };
459 match kind {
460 TypeKind::Boolean => Ok(DynamicData::Boolean(match text {
461 "true" | "1" => true,
462 "false" | "0" => false,
463 _ => return Err(bad()),
464 })),
465 TypeKind::String8 | TypeKind::String16 => Ok(DynamicData::Text(text.to_string())),
466 TypeKind::Enumeration => Ok(DynamicData::Enumerator(text.to_string())),
467 TypeKind::Char8 | TypeKind::Char16 => {
468 let ch = text.chars().next().ok_or_else(bad)?;
469 let pk = primitive_kind_of(kind).ok_or_else(bad)?;
470 Ok(DynamicData::character(pk, ch))
471 }
472 TypeKind::Float32 | TypeKind::Float64 => {
473 let v: f64 = text.trim().parse().map_err(|_| bad())?;
474 let pk = primitive_kind_of(kind).ok_or_else(bad)?;
475 Ok(DynamicData::float(pk, v))
476 }
477 _ => {
478 let pk = primitive_kind_of(kind).ok_or(DynXmlError::Unsupported("this TypeKind"))?;
479 let v: i128 = text.trim().parse().map_err(|_| bad())?;
480 Ok(DynamicData::integer(pk, v))
481 }
482 }
483}
484
485#[cfg(test)]
486#[allow(clippy::unwrap_used, clippy::expect_used)]
487mod tests {
488 use super::*;
489 use crate::types_registry::TypeRegistry;
490
491 fn resolved(idl: &str, type_name: &str) -> crate::types_registry::ResolvedType {
492 static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
493 let n = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
494 let dir = std::env::temp_dir().join(format!(
495 "zd-dynxml-{}-{n}-{}",
496 std::process::id(),
497 type_name.replace("::", "_")
498 ));
499 std::fs::create_dir_all(&dir).unwrap();
500 let path = dir.join("t.idl");
501 std::fs::write(&path, idl).unwrap();
502 let reg = TypeRegistry::new();
503 let r = reg.load_idl_file(&path, type_name).expect("resolved");
504 let _ = std::fs::remove_dir_all(&dir);
505 r
506 }
507
508 #[test]
509 fn struct_roundtrip_through_xml() {
510 let r = resolved(
511 "module M { @final struct P { @key long id; string name; boolean active; }; };",
512 "M::P",
513 );
514 let value = DynamicData::Aggregate(vec![
515 (
516 "id".to_string(),
517 DynamicData::integer(PrimitiveKind::Int32, 7),
518 ),
519 ("name".to_string(), DynamicData::text("alice".to_string())),
520 ("active".to_string(), DynamicData::Boolean(true)),
521 ]);
522 let xml = to_xml_document("data", &value, &r.dynamic_type).expect("encode");
523 assert!(xml.contains("<id>7</id>"));
524 assert!(xml.contains("<name>alice</name>"));
525 assert!(xml.contains("<active>true</active>"));
526 let back = from_xml_document(&xml, &r.dynamic_type).expect("decode");
527 assert_eq!(
528 back.member("id"),
529 Some(&DynamicData::integer(PrimitiveKind::Int32, 7))
530 );
531 assert_eq!(
532 back.member("name"),
533 Some(&DynamicData::text("alice".to_string()))
534 );
535 assert_eq!(back.member("active"), Some(&DynamicData::Boolean(true)));
536 }
537
538 #[test]
539 fn sequence_of_structs_roundtrip() {
540 let r = resolved(
541 "module M { @final struct Item { long v; }; @final struct Bag { sequence<Item> items; }; };",
542 "M::Bag",
543 );
544 let value = DynamicData::Aggregate(vec![(
545 "items".to_string(),
546 DynamicData::Collection(vec![
547 DynamicData::Aggregate(vec![(
548 "v".to_string(),
549 DynamicData::integer(PrimitiveKind::Int32, 1),
550 )]),
551 DynamicData::Aggregate(vec![(
552 "v".to_string(),
553 DynamicData::integer(PrimitiveKind::Int32, 2),
554 )]),
555 ]),
556 )]);
557 let xml = to_xml_document("data", &value, &r.dynamic_type).expect("encode");
558 assert!(xml.contains("<item>"));
559 let back = from_xml_document(&xml, &r.dynamic_type).expect("decode");
560 let items = back.member("items").unwrap().items().unwrap();
561 assert_eq!(items.len(), 2);
562 }
563
564 #[test]
565 fn a_body_nested_past_the_bound_is_refused_before_it_is_walked() {
566 let r = resolved("module M { @final struct P { long x; }; };", "M::P");
567 let levels = MAX_DEPTH + 2;
568 let xml = format!(
569 "<data>{}{}</data>",
570 "<x>".repeat(levels),
571 "</x>".repeat(levels)
572 );
573 let err = from_xml_document(&xml, &r.dynamic_type).unwrap_err();
574 assert!(matches!(err, DynXmlError::TooDeep(_)), "{err:?}");
575 }
576
577 #[test]
578 fn missing_member_is_reported() {
579 let r = resolved("module M { @final struct P { long x; long y; }; };", "M::P");
580 let value = DynamicData::Aggregate(vec![(
581 "x".to_string(),
582 DynamicData::integer(PrimitiveKind::Int32, 1),
583 )]);
584 let err = to_xml_document("data", &value, &r.dynamic_type).unwrap_err();
585 assert!(matches!(err, DynXmlError::Malformed(_)));
586 }
587
588 #[test]
589 fn bad_literal_is_reported_not_silently_defaulted() {
590 let r = resolved("module M { @final struct P { long x; }; };", "M::P");
591 let xml = "<data><x>not-a-number</x></data>";
592 let err = from_xml_document(xml, &r.dynamic_type).unwrap_err();
593 assert!(matches!(err, DynXmlError::BadLiteral { .. }));
594 }
595
596 #[test]
597 fn enum_roundtrip() {
598 let r = resolved(
599 "module M { enum Color { RED, GREEN, BLUE }; @final struct P { Color c; }; };",
600 "M::P",
601 );
602 let value = DynamicData::Aggregate(vec![(
603 "c".to_string(),
604 DynamicData::Enumerator("GREEN".to_string()),
605 )]);
606 let xml = to_xml_document("data", &value, &r.dynamic_type).expect("encode");
607 assert!(xml.contains("<c>GREEN</c>"));
608 let back = from_xml_document(&xml, &r.dynamic_type).expect("decode");
609 assert_eq!(
610 back.member("c"),
611 Some(&DynamicData::Enumerator("GREEN".to_string()))
612 );
613 }
614}