aster_forge_webdav/
xml.rs

1//! `WebDAV` XML grammar and representation boundary.
2//!
3//! The concrete XML implementation is intentionally private to this module. Products consume
4//! WebDAV-specific request models and [`DavXmlElement`] instead of depending on an XML crate.
5
6use std::collections::BTreeMap;
7use std::io::{Read, Write};
8
9use aster_forge_xml::{
10    BorrowedDocument, ElementRef, Error as ForgeXmlError, NodeRef, OwnedDocument, ParseOptions,
11    XmlSafetyError, XmlSafetyPolicy, XmlStreamWriter, XmlWriteAttribute, is_valid_xml_local_name,
12    is_valid_xml_namespace_name,
13};
14
15use crate::deltav::{DavExpandPropertySelection, DavParsedReport};
16
17const DAV_NAMESPACE: &str = "DAV:";
18
19/// XML failure returned by the `WebDAV` grammar boundary.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
21pub enum DavXmlError {
22    /// The document declares a DTD or entity.
23    #[error("XML external entity declarations are not allowed")]
24    ExternalEntity,
25    /// The document exceeds the configured nesting depth.
26    #[error("XML nesting depth exceeds the configured limit")]
27    TooDeep,
28    /// The request XML exceeds an input or decoded-text size limit.
29    #[error("XML input exceeds the configured size limit")]
30    TooLarge,
31    /// The document is malformed or is not a single-root document.
32    #[error("malformed XML input")]
33    Malformed,
34    /// The document is well-formed XML but violates the method grammar.
35    #[error("invalid WebDAV XML grammar")]
36    InvalidGrammar,
37}
38
39impl From<XmlSafetyError> for DavXmlError {
40    fn from(error: XmlSafetyError) -> Self {
41        match error {
42            XmlSafetyError::ExternalEntity => Self::ExternalEntity,
43            XmlSafetyError::TooDeep => Self::TooDeep,
44            XmlSafetyError::InputTooLarge | XmlSafetyError::TextTooLarge => Self::TooLarge,
45            XmlSafetyError::InvalidPolicy
46            | XmlSafetyError::OutputTooLarge
47            | XmlSafetyError::TooManyElements
48            | XmlSafetyError::TooManyAttributes
49            | XmlSafetyError::TooManyEvents
50            | XmlSafetyError::InvalidEncoding
51            | XmlSafetyError::Malformed => Self::Malformed,
52        }
53    }
54}
55
56/// XML content owned by the `WebDAV` boundary.
57#[derive(Debug, Clone, PartialEq, Eq)]
58pub enum DavXmlNode {
59    /// Child element.
60    Element(DavXmlElement),
61    /// Escaped character data.
62    Text(String),
63    /// CDATA content.
64    CData(String),
65    /// Comment content.
66    Comment(String),
67    /// Processing instruction.
68    ProcessingInstruction(String, Option<String>),
69}
70
71/// Owned DAV element used for persisted subtrees and response composition.
72///
73/// Known request grammars traverse the source-backed `aster_forge_xml` arena directly and only
74/// materialize the owner or property subtrees that must cross the backend boundary.
75#[derive(Debug, Clone, PartialEq, Eq)]
76pub struct DavXmlElement {
77    /// Local element name.
78    pub name: String,
79    /// Lexical prefix, when present.
80    pub prefix: Option<String>,
81    /// Resolved namespace URI, when present.
82    pub namespace: Option<String>,
83    /// In-scope namespace declarations keyed by prefix; an empty key is the default namespace.
84    pub namespaces: BTreeMap<String, String>,
85    /// Element attributes in their lexical form.
86    pub attributes: BTreeMap<String, String>,
87    /// Ordered child content.
88    pub children: Vec<DavXmlNode>,
89}
90
91impl DavXmlElement {
92    /// Creates an element from a lexical `QName` such as `D:href`.
93    #[must_use]
94    pub fn new(name: &str) -> Self {
95        let (prefix, local_name) = name
96            .split_once(':')
97            .map_or((None, name), |(prefix, local)| {
98                (Some(prefix.to_owned()), local)
99            });
100        Self {
101            name: local_name.to_owned(),
102            prefix,
103            namespace: None,
104            namespaces: BTreeMap::new(),
105            attributes: BTreeMap::new(),
106            children: Vec::new(),
107        }
108    }
109
110    /// Creates a `DAV:` element using the conventional `D` prefix.
111    #[must_use]
112    pub fn dav(local_name: &str) -> Self {
113        let mut element = Self::new(&format!("D:{local_name}"));
114        element.namespace = Some(DAV_NAMESPACE.to_owned());
115        element
116    }
117
118    /// Parses one bounded XML element.
119    ///
120    /// # Errors
121    ///
122    /// Returns [`DavXmlError`] when the bounded XML element is unsafe, malformed, or invalid.
123    pub fn parse(bytes: &[u8]) -> Result<Self, DavXmlError> {
124        parse_element(bytes)
125    }
126
127    /// Parses one bounded XML element from a reader.
128    ///
129    /// # Errors
130    ///
131    /// Returns [`DavXmlError`] when reader input is unsafe, malformed, or invalid XML.
132    pub fn parse_reader(reader: impl Read) -> Result<Self, DavXmlError> {
133        let options = webdav_parse_options();
134        let document = OwnedDocument::from_reader_with_options(reader, &options)
135            .map_err(|error| map_forge_xml_error(&error))?;
136        Ok(element_from_forge(document.root()))
137    }
138
139    /// Serializes the element as UTF-8 XML bytes.
140    ///
141    /// # Errors
142    ///
143    /// Returns [`DavXmlError`] when the element cannot be serialized as valid XML.
144    pub fn to_bytes(&self) -> Result<Vec<u8>, DavXmlError> {
145        let mut writer =
146            XmlStreamWriter::new(Vec::new()).map_err(|error| map_forge_xml_error(&error))?;
147        write_element(&mut writer, self, &BTreeMap::new())
148            .map_err(|error| map_forge_xml_error(&error))?;
149        writer.finish().map_err(|error| map_forge_xml_error(&error))
150    }
151
152    /// Iterates over direct child elements while ignoring text, comments, and CDATA.
153    pub fn child_elements(&self) -> impl Iterator<Item = &Self> {
154        self.children.iter().filter_map(|child| match child {
155            DavXmlNode::Element(element) => Some(element),
156            DavXmlNode::Text(_)
157            | DavXmlNode::CData(_)
158            | DavXmlNode::Comment(_)
159            | DavXmlNode::ProcessingInstruction(_, _) => None,
160        })
161    }
162
163    /// Returns concatenated direct text and CDATA content.
164    #[must_use]
165    pub fn text(&self) -> Option<String> {
166        let text = self
167            .children
168            .iter()
169            .filter_map(|child| match child {
170                DavXmlNode::Text(text) | DavXmlNode::CData(text) => Some(text.as_str()),
171                _ => None,
172            })
173            .collect::<String>();
174        (!text.is_empty()).then_some(text)
175    }
176}
177
178/// Property name selected by PROPFIND.
179#[derive(Debug, Clone, PartialEq, Eq)]
180pub struct DavRequestedProperty {
181    /// Local property name.
182    pub name: String,
183    /// Resolved namespace URI.
184    pub namespace: Option<String>,
185    /// Client-supplied lexical prefix.
186    pub prefix: Option<String>,
187}
188
189/// Parsed PROPFIND request selector.
190#[derive(Debug, Clone, PartialEq, Eq)]
191pub enum DavPropfindRequest {
192    /// All live/dead properties plus optional explicit properties.
193    AllProp {
194        /// Additional requested properties.
195        include: Vec<DavRequestedProperty>,
196    },
197    /// Property names without values.
198    PropName,
199    /// Explicit property selection.
200    Prop(Vec<DavRequestedProperty>),
201}
202
203/// One ordered PROPPATCH operation.
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub struct DavPropertyPatchRequest {
206    /// Whether the operation sets rather than removes the property.
207    pub set: bool,
208    /// Property value/name.
209    pub property: DavPropertyPatchValue,
210}
211
212/// Validated property element carried by PROPPATCH.
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct DavPropertyPatchValue {
215    /// Local property name.
216    pub name: String,
217    /// Resolved namespace URI.
218    pub namespace: Option<String>,
219    /// Lexical prefix.
220    pub prefix: Option<String>,
221    /// Standalone validated element, including inherited `xml:lang` when needed.
222    pub element: DavXmlElement,
223}
224
225/// Parsed LOCK creation body.
226#[derive(Debug, Clone, PartialEq, Eq)]
227pub struct DavLockRequestBody {
228    /// Whether the requested lock scope is shared.
229    pub shared: bool,
230    /// Optional owner element, preserved for discovery and persistence.
231    pub owner: Option<DavXmlElement>,
232}
233
234/// Parses a PROPFIND body. An absent body selects `allprop`.
235///
236/// # Errors
237///
238/// Returns [`DavXmlError`] when the body is unsafe or violates PROPFIND grammar.
239pub fn parse_propfind_request(body: &[u8]) -> Result<DavPropfindRequest, DavXmlError> {
240    if body.is_empty() {
241        return Ok(DavPropfindRequest::AllProp {
242            include: Vec::new(),
243        });
244    }
245    let document = parse_document(body)?;
246    let root = document.root();
247    if !is_dav_element(root, "propfind") {
248        return Err(DavXmlError::InvalidGrammar);
249    }
250    require_element_content(root)?;
251
252    let mut kind = None;
253    let mut include = Vec::new();
254    let mut include_seen = false;
255    for child in root.child_elements() {
256        if is_dav_element(child, "propname") {
257            if kind.is_some() {
258                return Err(DavXmlError::InvalidGrammar);
259            }
260            require_element_content(child)?;
261            kind = Some(DavPropfindRequest::PropName);
262        } else if is_dav_element(child, "allprop") {
263            if kind.is_some() {
264                return Err(DavXmlError::InvalidGrammar);
265            }
266            require_element_content(child)?;
267            kind = Some(DavPropfindRequest::AllProp {
268                include: Vec::new(),
269            });
270        } else if is_dav_element(child, "include") {
271            if include_seen {
272                return Err(DavXmlError::InvalidGrammar);
273            }
274            include_seen = true;
275            require_property_names(child)?;
276            include.extend(child.child_elements().map(requested_property));
277        } else if is_dav_element(child, "prop") {
278            if kind.is_some() {
279                return Err(DavXmlError::InvalidGrammar);
280            }
281            require_property_names(child)?;
282            kind = Some(DavPropfindRequest::Prop(
283                child.child_elements().map(requested_property).collect(),
284            ));
285        }
286    }
287
288    match kind {
289        Some(DavPropfindRequest::AllProp { .. }) => Ok(DavPropfindRequest::AllProp { include }),
290        Some(kind) if !include_seen => Ok(kind),
291        _ => Err(DavXmlError::InvalidGrammar),
292    }
293}
294
295/// Parses an ordered PROPPATCH request.
296///
297/// # Errors
298///
299/// Returns [`DavXmlError`] when the body is unsafe or violates PROPPATCH grammar.
300pub fn parse_proppatch_request(body: &[u8]) -> Result<Vec<DavPropertyPatchRequest>, DavXmlError> {
301    let document = parse_document(body)?;
302    let root = document.root();
303    if !is_dav_element(root, "propertyupdate") {
304        return Err(DavXmlError::InvalidGrammar);
305    }
306    require_element_content(root)?;
307    let root_lang = xml_lang_value(root).map(str::to_owned);
308    let mut patches = Vec::new();
309    for action in root.child_elements() {
310        let set = if is_dav_element(action, "set") {
311            true
312        } else if is_dav_element(action, "remove") {
313            false
314        } else {
315            // RFC 4918 section 17: unknown extension elements are ignored with their subtree.
316            continue;
317        };
318        require_element_content(action)?;
319        let action_lang = xml_lang_value(action).or(root_lang.as_deref());
320        let prop_container =
321            unique_dav_child(action, "prop")?.ok_or(DavXmlError::InvalidGrammar)?;
322        require_element_content(prop_container)?;
323        let container_lang = xml_lang_value(prop_container).or(action_lang);
324        for property in prop_container.child_elements() {
325            if !set {
326                require_property_name(property)?;
327            }
328            let mut element = element_from_forge(property);
329            let inherited_lang = xml_lang_value(property).or(container_lang);
330            if let Some(lang) = inherited_lang.filter(|lang| !lang.is_empty()) {
331                element
332                    .attributes
333                    .entry("xml:lang".to_owned())
334                    .or_insert_with(|| lang.to_owned());
335            }
336            patches.push(DavPropertyPatchRequest {
337                set,
338                property: DavPropertyPatchValue {
339                    name: element.name.clone(),
340                    namespace: element.namespace.clone(),
341                    prefix: element.prefix.clone(),
342                    element,
343                },
344            });
345        }
346    }
347    if patches.is_empty() {
348        return Err(DavXmlError::InvalidGrammar);
349    }
350    Ok(patches)
351}
352
353/// Parses a LOCK creation body.
354///
355/// # Errors
356///
357/// Returns [`DavXmlError`] when the body is unsafe or violates LOCK creation grammar.
358pub fn parse_lock_request(body: &[u8]) -> Result<DavLockRequestBody, DavXmlError> {
359    let document = parse_document(body)?;
360    let root = document.root();
361    if !is_dav_element(root, "lockinfo") {
362        return Err(DavXmlError::InvalidGrammar);
363    }
364    require_element_content(root)?;
365    let mut shared = None;
366    let mut write_lock = false;
367    let mut owner = None;
368    for child in root.child_elements() {
369        if is_dav_element(child, "lockscope") {
370            if shared.is_some() {
371                return Err(DavXmlError::InvalidGrammar);
372            }
373            require_element_content(child)?;
374            let exclusive_scope = unique_dav_child(child, "exclusive")?;
375            let shared_scope = unique_dav_child(child, "shared")?;
376            let (selected_scope, is_shared) = match (exclusive_scope, shared_scope) {
377                (Some(scope), None) => (scope, false),
378                (None, Some(scope)) => (scope, true),
379                (Some(_), Some(_)) | (None, None) => return Err(DavXmlError::InvalidGrammar),
380            };
381            require_element_content(selected_scope)?;
382            shared = Some(is_shared);
383        } else if is_dav_element(child, "locktype") {
384            if write_lock {
385                return Err(DavXmlError::InvalidGrammar);
386            }
387            require_element_content(child)?;
388            let write = unique_dav_child(child, "write")?.ok_or(DavXmlError::InvalidGrammar)?;
389            require_element_content(write)?;
390            write_lock = true;
391        } else if is_dav_element(child, "owner") {
392            if owner.is_some() {
393                return Err(DavXmlError::InvalidGrammar);
394            }
395            owner = Some(element_from_forge(child));
396        }
397    }
398    match (shared, write_lock) {
399        (Some(shared), true) => Ok(DavLockRequestBody { shared, owner }),
400        _ => Err(DavXmlError::InvalidGrammar),
401    }
402}
403
404pub(crate) fn parse_report_request(
405    body: &[u8],
406    maximum_input_bytes: usize,
407    maximum_xml_depth: usize,
408    maximum_selection_depth: usize,
409    maximum_selection_properties: usize,
410) -> Result<DavParsedReport, DavXmlError> {
411    let options = webdav_parse_options()
412        .max_size(maximum_input_bytes)
413        .max_depth(maximum_xml_depth);
414    let document = parse_document_with_options(body, &options)?;
415    let root = document.root();
416    if is_dav_element(root, "version-tree") {
417        return Ok(DavParsedReport::VersionTree(parse_version_tree_prop(root)?));
418    }
419    if is_dav_element(root, "expand-property") {
420        return Ok(DavParsedReport::ExpandProperty(parse_expand_property(
421            root,
422            maximum_selection_depth,
423            maximum_selection_properties,
424        )?));
425    }
426    Ok(DavParsedReport::Other(requested_property(root)))
427}
428
429/// Validates an optional RFC 3253 VERSION-CONTROL request body.
430pub(crate) fn parse_version_control_request(body: &[u8]) -> Result<(), DavXmlError> {
431    if body.is_empty() {
432        return Ok(());
433    }
434    let document = parse_document(body)?;
435    if !is_dav_element(document.root(), "version-control") {
436        return Err(DavXmlError::InvalidGrammar);
437    }
438    // RFC 3253 section 3.5 declares DAV:version-control as ANY. The complete document has
439    // already passed the shared WebDAV safety policy, so extensions and mixed content are kept.
440    Ok(())
441}
442
443fn parse_element(bytes: &[u8]) -> Result<DavXmlElement, DavXmlError> {
444    let document = parse_document(bytes)?;
445    Ok(element_from_forge(document.root()))
446}
447
448fn parse_document(bytes: &[u8]) -> Result<BorrowedDocument<'_>, DavXmlError> {
449    // The Forge parser applies the WebDAV safety policy while building its source-backed arena.
450    // A separate validator pass here would scan every request twice.
451    BorrowedDocument::parse_with_options(bytes, &webdav_parse_options())
452        .map_err(|error| map_forge_xml_error(&error))
453}
454
455fn parse_document_with_options<'a>(
456    bytes: &'a [u8],
457    options: &ParseOptions,
458) -> Result<BorrowedDocument<'a>, DavXmlError> {
459    BorrowedDocument::parse_with_options(bytes, options)
460        .map_err(|error| map_forge_xml_error(&error))
461}
462
463fn is_dav_element<S: AsRef<[u8]>>(element: ElementRef<'_, S>, local_name: &str) -> bool {
464    element.name() == local_name && element.namespace() == Some(DAV_NAMESPACE)
465}
466
467fn parse_version_tree_prop<S: AsRef<[u8]>>(
468    root: ElementRef<'_, S>,
469) -> Result<Option<Vec<DavRequestedProperty>>, DavXmlError> {
470    require_element_content(root)?;
471    if let Some(prop) = unique_dav_child(root, "prop")? {
472        require_property_names(prop)?;
473        return Ok(Some(
474            prop.child_elements().map(requested_property).collect(),
475        ));
476    }
477    Ok(None)
478}
479
480fn parse_expand_property<S: AsRef<[u8]>>(
481    root: ElementRef<'_, S>,
482    maximum_depth: usize,
483    maximum_properties: usize,
484) -> Result<Vec<DavExpandPropertySelection>, DavXmlError> {
485    require_element_content(root)?;
486    if root
487        .child_elements()
488        .any(|child| !is_dav_element(child, "property"))
489    {
490        return Err(DavXmlError::InvalidGrammar);
491    }
492    let mut count = 0usize;
493    root.child_elements()
494        .filter(|child| is_dav_element(*child, "property"))
495        .map(|property| {
496            parse_expand_property_selection(
497                property,
498                1,
499                maximum_depth,
500                maximum_properties,
501                &mut count,
502            )
503        })
504        .collect()
505}
506
507fn parse_expand_property_selection<S: AsRef<[u8]>>(
508    property: ElementRef<'_, S>,
509    depth: usize,
510    maximum_depth: usize,
511    maximum_properties: usize,
512    count: &mut usize,
513) -> Result<DavExpandPropertySelection, DavXmlError> {
514    if depth > maximum_depth {
515        return Err(DavXmlError::TooDeep);
516    }
517    *count = count.checked_add(1).ok_or(DavXmlError::TooLarge)?;
518    if *count > maximum_properties {
519        return Err(DavXmlError::TooLarge);
520    }
521    require_element_content(property)?;
522    if property
523        .child_elements()
524        .any(|child| !is_dav_element(child, "property"))
525    {
526        return Err(DavXmlError::InvalidGrammar);
527    }
528    let name = property
529        .attribute("name")
530        .filter(|name| is_valid_xml_local_name(name))
531        .ok_or(DavXmlError::InvalidGrammar)?;
532    let namespace = property.attribute("namespace").unwrap_or(DAV_NAMESPACE);
533    if namespace.is_empty() || !is_valid_xml_namespace_name(namespace) {
534        return Err(DavXmlError::InvalidGrammar);
535    }
536    let nested = property
537        .child_elements()
538        .filter(|child| is_dav_element(*child, "property"))
539        .map(|child| {
540            parse_expand_property_selection(
541                child,
542                depth + 1,
543                maximum_depth,
544                maximum_properties,
545                count,
546            )
547        })
548        .collect::<Result<Vec<_>, _>>()?;
549    Ok(DavExpandPropertySelection {
550        property: DavRequestedProperty {
551            name: name.to_owned(),
552            namespace: Some(namespace.to_owned()),
553            prefix: None,
554        },
555        nested,
556    })
557}
558
559fn unique_dav_child<'document, S: AsRef<[u8]>>(
560    parent: ElementRef<'document, S>,
561    local_name: &str,
562) -> Result<Option<ElementRef<'document, S>>, DavXmlError> {
563    let mut selected = None;
564    for child in parent.child_elements() {
565        if is_dav_element(child, local_name) {
566            if selected.is_some() {
567                return Err(DavXmlError::InvalidGrammar);
568            }
569            selected = Some(child);
570        }
571    }
572    Ok(selected)
573}
574
575fn require_element_content<S: AsRef<[u8]>>(element: ElementRef<'_, S>) -> Result<(), DavXmlError> {
576    if element
577        .children()
578        .any(|child| matches!(child, NodeRef::Text(_) | NodeRef::CData(_)))
579    {
580        Err(DavXmlError::InvalidGrammar)
581    } else {
582        Ok(())
583    }
584}
585
586fn require_property_names<S: AsRef<[u8]>>(container: ElementRef<'_, S>) -> Result<(), DavXmlError> {
587    require_element_content(container)?;
588    for property in container.child_elements() {
589        require_property_name(property)?;
590    }
591    Ok(())
592}
593
594fn require_property_name<S: AsRef<[u8]>>(property: ElementRef<'_, S>) -> Result<(), DavXmlError> {
595    // In a property-name context every child element is unrecognized and RFC 4918 section 17
596    // removes its complete subtree from semantic processing. Direct character data would still
597    // be a property value, which PROPFIND/REPORT selectors and PROPPATCH remove do not permit.
598    require_element_content(property)
599}
600
601fn requested_property<S: AsRef<[u8]>>(element: ElementRef<'_, S>) -> DavRequestedProperty {
602    DavRequestedProperty {
603        name: element.name().to_owned(),
604        namespace: element.namespace().map(str::to_owned),
605        prefix: element.prefix().map(str::to_owned),
606    }
607}
608
609fn xml_lang_value<S: AsRef<[u8]>>(element: ElementRef<'_, S>) -> Option<&str> {
610    element.attribute("xml:lang")
611}
612
613fn webdav_parse_options() -> ParseOptions {
614    // Preserve the established WebDAV XML boundary: formatting whitespace is ignored and retained
615    // text is trimmed before WebDAV grammar evaluation or dead-property persistence.
616    ParseOptions::new()
617        .safety_policy(XmlSafetyPolicy::untrusted())
618        .trim_whitespace(true)
619}
620
621fn map_forge_xml_error(error: &ForgeXmlError) -> DavXmlError {
622    match error {
623        ForgeXmlError::Safety(error) => (*error).into(),
624        ForgeXmlError::InvalidXml(_) | ForgeXmlError::InvalidData(_) | ForgeXmlError::Io(_) => {
625            DavXmlError::Malformed
626        }
627    }
628}
629
630fn element_from_forge<S: AsRef<[u8]>>(element: ElementRef<'_, S>) -> DavXmlElement {
631    let mut namespaces = BTreeMap::new();
632    match (element.prefix(), element.namespace()) {
633        (Some(prefix), Some(namespace)) if prefix != "xml" => {
634            namespaces.insert(prefix.to_owned(), namespace.to_owned());
635        }
636        (None, Some(namespace)) => {
637            namespaces.insert(String::new(), namespace.to_owned());
638        }
639        // This owned subtree may later be embedded under a default namespace. Declaring the
640        // empty namespace keeps an originally unqualified element unqualified.
641        (None, None) => {
642            namespaces.insert(String::new(), String::new());
643        }
644        _ => {}
645    }
646
647    let mut attributes = BTreeMap::new();
648    for attribute in element.attributes() {
649        if let (Some(prefix), Some(namespace)) = (attribute.prefix(), attribute.namespace())
650            && prefix != "xml"
651        {
652            namespaces
653                .entry(prefix.to_owned())
654                .or_insert_with(|| namespace.to_owned());
655        }
656        attributes.insert(
657            attribute.qualified_name().to_owned(),
658            attribute.value().to_owned(),
659        );
660    }
661
662    DavXmlElement {
663        name: element.name().to_owned(),
664        prefix: element.prefix().map(str::to_owned),
665        namespace: element.namespace().map(str::to_owned),
666        namespaces,
667        attributes,
668        children: element
669            .children()
670            .map(|child| match child {
671                NodeRef::Element(element) => DavXmlNode::Element(element_from_forge(element)),
672                NodeRef::Text(text) => DavXmlNode::Text(text.to_owned()),
673                NodeRef::CData(text) => DavXmlNode::CData(text.to_owned()),
674                NodeRef::Comment(text) => DavXmlNode::Comment(text.to_owned()),
675                NodeRef::ProcessingInstruction(instruction) => DavXmlNode::ProcessingInstruction(
676                    instruction.target.to_owned(),
677                    instruction.content.map(str::to_owned),
678                ),
679            })
680            .collect(),
681    }
682}
683
684pub(crate) fn write_element<W: Write>(
685    writer: &mut XmlStreamWriter<W>,
686    element: &DavXmlElement,
687    inherited_namespaces: &BTreeMap<String, String>,
688) -> Result<(), ForgeXmlError> {
689    let qualified_name = element.prefix.as_ref().map_or_else(
690        || element.name.clone(),
691        |prefix| format!("{prefix}:{}", element.name),
692    );
693    let mut namespaces = inherited_namespaces.clone();
694    let mut attributes = BTreeMap::new();
695    for (prefix, namespace) in &element.namespaces {
696        if namespaces.get(prefix) != Some(namespace) {
697            let name = if prefix.is_empty() {
698                "xmlns".to_owned()
699            } else {
700                format!("xmlns:{prefix}")
701            };
702            attributes.insert(name, namespace.clone());
703        }
704        namespaces.insert(prefix.clone(), namespace.clone());
705    }
706    attributes.extend(element.attributes.clone());
707    for (name, namespace) in &attributes {
708        if let Some(prefix) = namespace_declaration_prefix(name) {
709            namespaces.insert(prefix.to_owned(), namespace.clone());
710        }
711    }
712
713    if let Some(namespace) = &element.namespace {
714        let prefix = element.prefix.as_deref().unwrap_or("");
715        if namespaces.get(prefix).map(String::as_str) != Some(namespace) {
716            let binding_name = if prefix.is_empty() {
717                "xmlns".to_owned()
718            } else {
719                format!("xmlns:{prefix}")
720            };
721            match attributes.get(&binding_name) {
722                Some(binding) if binding != namespace => {
723                    return Err(ForgeXmlError::InvalidData(
724                        "conflicting XML namespace binding".to_owned(),
725                    ));
726                }
727                Some(_) => {}
728                None => {
729                    attributes.insert(binding_name, namespace.clone());
730                }
731            }
732            namespaces.insert(prefix.to_owned(), namespace.clone());
733        }
734    } else if element.prefix.is_none()
735        && namespaces
736            .get("")
737            .is_some_and(|namespace| !namespace.is_empty())
738    {
739        match attributes.get("xmlns") {
740            Some(namespace) if !namespace.is_empty() => {
741                return Err(ForgeXmlError::InvalidData(
742                    "conflicting XML default namespace binding".to_owned(),
743                ));
744            }
745            Some(_) => {}
746            None => {
747                attributes.insert("xmlns".to_owned(), String::new());
748            }
749        }
750        namespaces.insert(String::new(), String::new());
751    }
752
753    let write_attributes = attributes
754        .iter()
755        .map(|(name, value)| XmlWriteAttribute::new(name, value));
756    if element.children.is_empty() {
757        writer.empty_element(&qualified_name, write_attributes)?;
758        return Ok(());
759    }
760    writer.start_element(&qualified_name, write_attributes)?;
761    for child in &element.children {
762        match child {
763            DavXmlNode::Element(element) => write_element(writer, element, &namespaces)?,
764            DavXmlNode::Text(text) => writer.text(text)?,
765            DavXmlNode::CData(text) => writer.cdata(text)?,
766            DavXmlNode::Comment(text) => writer.comment(text)?,
767            DavXmlNode::ProcessingInstruction(target, content) => {
768                writer.processing_instruction(target, content.as_deref())?;
769            }
770        }
771    }
772    writer.end_element()
773}
774
775fn namespace_declaration_prefix(name: &str) -> Option<&str> {
776    if name == "xmlns" {
777        Some("")
778    } else {
779        name.strip_prefix("xmlns:")
780    }
781}