Skip to main content

zerodds_webdds_bridge/
dynxml.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! DDS-WEB §8.1/§8.2 XML data representation, converted through the
5//! type's own [`DynamicType`] descriptor — never an opaque byte guess.
6//!
7//! §8.1 makes `application/dds-web+xml` the Mandatory format;
8//! nothing else is Required, so only XML is implemented here (see the
9//! crate README's "Open points" for the optional-format gap).
10//!
11//! The wire tags this module writes/reads are the sample's own member
12//! names, recursively — the same shape `zerodds_xml::sample`'s module
13//! doc documents for DDS-XML 1.0's "Data Samples" building block
14//! (`<member-name>value</member-name>`, `<item>` for sequence/array
15//! elements, `<discriminator>`+the active case's tag for a union). That
16//! crate's `SampleValue` is a different type model (`TypeLibrary`/
17//! `TypeDef`, not `zerodds_types::dynamic`), so this module talks
18//! directly to [`DynamicData`]/[`DynamicType`] instead of bridging the
19//! two — see the crate README for why.
20
21// `DynamicData::{integer,float,character}` take the AST's own
22// `PrimitiveKind` (`zerodds_ast::PrimitiveKind`) — a distinctly-named
23// enum from `zerodds_types::type_identifier::PrimitiveKind` (the
24// TypeIdentifier-wire one), spelled `Octet`/`Char`/`WChar` instead of
25// `Byte`/`Char8`/`Char16`.
26use 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/// Conversion error.
33#[derive(Debug, Clone, PartialEq, Eq)]
34pub enum DynXmlError {
35    /// The XML text did not parse, or did not have the expected shape
36    /// (a member/element the type descriptor asked for was missing).
37    Malformed(String),
38    /// A primitive's text content did not parse as its kind.
39    BadLiteral {
40        /// The member/element path (dotted).
41        at: String,
42        /// The raw text that failed to parse.
43        text: String,
44    },
45    /// A `TypeKind` this converter does not (yet) handle — currently
46    /// `long double` (Float128; blocked pending Rust `f128`, same
47    /// project-wide boundary as `idl-4.2`'s long-double bucket) and
48    /// `map` (DDS-WEB's own XML samples in the wild are struct/
49    /// sequence/enum/union; a map member is rare enough that adding
50    /// support is deferred rather than guessed at).
51    Unsupported(&'static str),
52    /// The `DynamicType` descriptor itself refused to answer (a stale
53    /// handle, or an inconsistent model).
54    Model(String),
55    /// The document or the value nests deeper than `MAX_DEPTH` — the
56    /// element named, or the path of the value.
57    TooDeep(String),
58}
59
60/// Nesting bound of the XML body and of the value walked through it. A
61/// body arrives over HTTP; its element tree is dropped recursively and a
62/// recursive type (a struct with a sequence of itself) follows it down.
63const 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/// A parsed element tree — the minimal DOM this module needs, built
80/// from `zerodds_xml_wire::parser`'s event stream.
81#[derive(Debug, Clone, Default)]
82struct XmlNode {
83    name: String,
84    children: Vec<XmlNode>,
85    text: String,
86}
87
88/// Parses `xml` and returns its single root element (skipping a leading
89/// `<?xml ...?>` declaration).
90///
91/// # Errors
92/// [`DynXmlError::Malformed`] on invalid XML or a document with no root
93/// element.
94fn 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                // The root plus one element per value level.
103                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
137/// Encodes `value` (of type `ty`) as the XML element `tag`, appended to
138/// `emitter`.
139///
140/// # Errors
141/// See [`DynXmlError`].
142pub 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
151/// zerodds-lint: recursion-depth 64 (one level per nested member, case,
152/// element or alias; refused past `MAX_DEPTH`)
153fn 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
273/// Top-level: `value` as a complete XML document (`<?xml?>` + the
274/// element named `root_tag`).
275///
276/// # Errors
277/// See [`DynXmlError`].
278pub 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
289/// Parses `xml` as a value of type `ty` (the root element's tag is not
290/// checked against a name — the caller already knows what it asked
291/// for).
292///
293/// # Errors
294/// See [`DynXmlError`].
295pub 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
300/// zerodds-lint: recursion-depth 64 (one level per nested member, case,
301/// element or alias; refused past `MAX_DEPTH`, and `parse_root` refuses a
302/// deeper document before)
303fn 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
412/// Text content of the wire tags the wire codec understands for a
413/// primitive `DynamicData`.
414fn 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
432/// The [`PrimitiveKind`] a [`TypeKind`] maps to, for every kind that is
433/// a primitive (not string/enum/collection/aggregate).
434const 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}