Xuanwo commented on code in PR #7074:
URL: https://github.com/apache/opendal/pull/7074#discussion_r2641908926


##########
core/services/webdav/src/core.rs:
##########
@@ -368,6 +429,218 @@ impl WebdavCore {
     }
 }
 
+/// Build a PROPPATCH request body to set user-defined metadata.
+///
+/// The request uses the specified namespace to store metadata as dead 
properties
+/// on the WebDAV server.
+///
+/// # Arguments
+/// * `user_metadata` - Key-value pairs to store as properties
+/// * `namespace_prefix` - XML namespace prefix (e.g., "opendal")
+/// * `namespace_uri` - XML namespace URI (e.g., 
"https://opendal.apache.org/ns";)
+///
+/// # Example output
+/// ```xml
+/// <?xml version="1.0" encoding="utf-8"?>
+/// <D:propertyupdate xmlns:D="DAV:" 
xmlns:opendal="https://opendal.apache.org/ns";>
+///   <D:set>
+///     <D:prop>
+///       <opendal:key1>value1</opendal:key1>
+///       <opendal:key2>value2</opendal:key2>
+///     </D:prop>
+///   </D:set>
+/// </D:propertyupdate>
+/// ```
+pub fn build_proppatch_request(
+    user_metadata: &HashMap<String, String>,
+    namespace_prefix: &str,
+    namespace_uri: &str,
+) -> String {
+    let mut props = String::new();
+    for (key, value) in user_metadata {
+        // Escape XML special characters in key and value
+        let escaped_key = escape_xml(key);
+        let escaped_value = escape_xml(value);
+        props.push_str(&format!(
+            
"<{namespace_prefix}:{escaped_key}>{escaped_value}</{namespace_prefix}:{escaped_key}>"
+        ));
+    }
+
+    format!(
+        r#"<?xml version="1.0" encoding="utf-8"?><D:propertyupdate 
xmlns:D="DAV:" 
xmlns:{namespace_prefix}="{namespace_uri}"><D:set><D:prop>{props}</D:prop></D:set></D:propertyupdate>"#
+    )
+}
+
+/// Escape XML special characters.
+fn escape_xml(s: &str) -> String {
+    s.replace('&', "&amp;")
+        .replace('<', "&lt;")
+        .replace('>', "&gt;")
+        .replace('"', "&quot;")
+        .replace('\'', "&apos;")
+}
+
+/// Parse user metadata from the raw XML response.
+///
+/// This function extracts properties in the specified namespace
+/// from the PROPFIND response and returns them as a HashMap.
+///
+/// Note: WebDAV servers like Nextcloud/ownCloud may use dynamic namespace 
prefixes
+/// (e.g., x1:, x2:) instead of the prefix we send. The namespace URI is the
+/// reliable identifier, not the prefix.
+///
+/// # Arguments
+/// * `xml` - The raw XML response string
+/// * `namespace_uri` - The namespace URI to look for
+pub fn parse_user_metadata_from_xml(xml: &str, namespace_uri: &str) -> 
HashMap<String, String> {
+    let mut user_metadata = HashMap::new();
+
+    // Find all namespace prefixes mapped to our namespace URI
+    let prefixes = find_namespace_prefixes(xml, namespace_uri);
+
+    // For each prefix, extract properties
+    for prefix in prefixes {
+        extract_properties_with_prefix(xml, &prefix, &mut user_metadata);
+    }
+
+    user_metadata
+}
+
+/// Find all XML namespace prefixes that map to the given namespace URI.
+///
+/// Looks for patterns like: xmlns:PREFIX="URI" or xmlns:PREFIX='URI'
+fn find_namespace_prefixes(xml: &str, namespace_uri: &str) -> Vec<String> {
+    let mut prefixes = Vec::new();
+
+    // Pattern 1: xmlns:PREFIX="URI"
+    let pattern1 = format!("=\"{namespace_uri}\"");
+    // Pattern 2: xmlns:PREFIX='URI'
+    let pattern2 = format!("='{namespace_uri}'");
+
+    for pattern in [&pattern1, &pattern2] {
+        let mut search_start = 0;
+        while let Some(pos) = xml[search_start..].find(pattern) {
+            let abs_pos = search_start + pos;
+            // Look backwards to find "xmlns:"
+            if let Some(xmlns_start) = xml[..abs_pos].rfind("xmlns:") {
+                let prefix_start = xmlns_start + 6; // length of "xmlns:"
+                let prefix = &xml[prefix_start..abs_pos];
+                // Validate prefix (should be alphanumeric/underscore) and not 
already added
+                if !prefix.is_empty()
+                    && prefix
+                        .chars()
+                        .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
+                    && !prefixes.contains(&prefix.to_string())
+                {
+                    prefixes.push(prefix.to_string());
+                }
+            }
+            search_start = abs_pos + pattern.len();
+        }
+    }
+
+    prefixes
+}
+
+/// Extract properties with a specific namespace prefix from the XML.
+fn extract_properties_with_prefix(
+    xml: &str,
+    prefix: &str,
+    user_metadata: &mut HashMap<String, String>,
+) {
+    let open_prefix = format!("<{prefix}:");
+    let mut search_start = 0;
+
+    while let Some(start) = xml[search_start..].find(&open_prefix) {
+        let abs_start = search_start + start + open_prefix.len();
+
+        // Find the key name (up to '>' or ' ' for attributes)
+        if let Some(tag_end) = xml[abs_start..].find('>') {
+            let tag_content = &xml[abs_start..abs_start + tag_end];
+            // Key is before any space (attributes) or the whole thing if no 
space
+            let key = 
tag_content.split_whitespace().next().unwrap_or(tag_content);
+
+            // Check for self-closing tag
+            if tag_content.ends_with('/') {
+                // Self-closing tag like <prefix:key/>, value is empty
+                let key = key.trim_end_matches('/');
+                user_metadata.insert(key.to_string(), String::new());
+                search_start = abs_start + tag_end + 1;
+                continue;
+            }
+
+            // Find the closing tag
+            let close_tag = format!("</{prefix}:{key}>");
+            let value_start = abs_start + tag_end + 1;
+
+            if let Some(value_end) = xml[value_start..].find(&close_tag) {
+                let value = &xml[value_start..value_start + value_end];
+                // Unescape XML entities
+                let unescaped_value = unescape_xml(value);
+                user_metadata.insert(key.to_string(), unescaped_value);
+                search_start = value_start + value_end + close_tag.len();
+            } else {
+                search_start = value_start;
+            }
+        } else {
+            break;
+        }
+    }
+}
+
+/// Unescape XML entities.
+fn unescape_xml(s: &str) -> String {

Review Comment:
   The same.



##########
core/services/webdav/src/core.rs:
##########
@@ -368,6 +429,218 @@ impl WebdavCore {
     }
 }
 
+/// Build a PROPPATCH request body to set user-defined metadata.
+///
+/// The request uses the specified namespace to store metadata as dead 
properties
+/// on the WebDAV server.
+///
+/// # Arguments
+/// * `user_metadata` - Key-value pairs to store as properties
+/// * `namespace_prefix` - XML namespace prefix (e.g., "opendal")
+/// * `namespace_uri` - XML namespace URI (e.g., 
"https://opendal.apache.org/ns";)
+///
+/// # Example output
+/// ```xml
+/// <?xml version="1.0" encoding="utf-8"?>
+/// <D:propertyupdate xmlns:D="DAV:" 
xmlns:opendal="https://opendal.apache.org/ns";>
+///   <D:set>
+///     <D:prop>
+///       <opendal:key1>value1</opendal:key1>
+///       <opendal:key2>value2</opendal:key2>
+///     </D:prop>
+///   </D:set>
+/// </D:propertyupdate>
+/// ```
+pub fn build_proppatch_request(
+    user_metadata: &HashMap<String, String>,
+    namespace_prefix: &str,
+    namespace_uri: &str,
+) -> String {
+    let mut props = String::new();
+    for (key, value) in user_metadata {
+        // Escape XML special characters in key and value
+        let escaped_key = escape_xml(key);
+        let escaped_value = escape_xml(value);
+        props.push_str(&format!(
+            
"<{namespace_prefix}:{escaped_key}>{escaped_value}</{namespace_prefix}:{escaped_key}>"
+        ));
+    }
+
+    format!(
+        r#"<?xml version="1.0" encoding="utf-8"?><D:propertyupdate 
xmlns:D="DAV:" 
xmlns:{namespace_prefix}="{namespace_uri}"><D:set><D:prop>{props}</D:prop></D:set></D:propertyupdate>"#
+    )
+}
+
+/// Escape XML special characters.
+fn escape_xml(s: &str) -> String {
+    s.replace('&', "&amp;")
+        .replace('<', "&lt;")
+        .replace('>', "&gt;")
+        .replace('"', "&quot;")
+        .replace('\'', "&apos;")
+}
+
+/// Parse user metadata from the raw XML response.
+///
+/// This function extracts properties in the specified namespace
+/// from the PROPFIND response and returns them as a HashMap.
+///
+/// Note: WebDAV servers like Nextcloud/ownCloud may use dynamic namespace 
prefixes
+/// (e.g., x1:, x2:) instead of the prefix we send. The namespace URI is the
+/// reliable identifier, not the prefix.
+///
+/// # Arguments
+/// * `xml` - The raw XML response string
+/// * `namespace_uri` - The namespace URI to look for
+pub fn parse_user_metadata_from_xml(xml: &str, namespace_uri: &str) -> 
HashMap<String, String> {
+    let mut user_metadata = HashMap::new();
+
+    // Find all namespace prefixes mapped to our namespace URI
+    let prefixes = find_namespace_prefixes(xml, namespace_uri);
+
+    // For each prefix, extract properties
+    for prefix in prefixes {
+        extract_properties_with_prefix(xml, &prefix, &mut user_metadata);
+    }
+
+    user_metadata
+}
+
+/// Find all XML namespace prefixes that map to the given namespace URI.
+///
+/// Looks for patterns like: xmlns:PREFIX="URI" or xmlns:PREFIX='URI'
+fn find_namespace_prefixes(xml: &str, namespace_uri: &str) -> Vec<String> {
+    let mut prefixes = Vec::new();
+
+    // Pattern 1: xmlns:PREFIX="URI"
+    let pattern1 = format!("=\"{namespace_uri}\"");
+    // Pattern 2: xmlns:PREFIX='URI'
+    let pattern2 = format!("='{namespace_uri}'");
+
+    for pattern in [&pattern1, &pattern2] {
+        let mut search_start = 0;
+        while let Some(pos) = xml[search_start..].find(pattern) {
+            let abs_pos = search_start + pos;
+            // Look backwards to find "xmlns:"
+            if let Some(xmlns_start) = xml[..abs_pos].rfind("xmlns:") {
+                let prefix_start = xmlns_start + 6; // length of "xmlns:"
+                let prefix = &xml[prefix_start..abs_pos];
+                // Validate prefix (should be alphanumeric/underscore) and not 
already added
+                if !prefix.is_empty()
+                    && prefix
+                        .chars()
+                        .all(|c| c.is_alphanumeric() || c == '_' || c == '-')
+                    && !prefixes.contains(&prefix.to_string())
+                {
+                    prefixes.push(prefix.to_string());
+                }
+            }
+            search_start = abs_pos + pattern.len();
+        }
+    }
+
+    prefixes
+}
+
+/// Extract properties with a specific namespace prefix from the XML.
+fn extract_properties_with_prefix(

Review Comment:
   Seems we are parsing xml here. Can we use `quick-xml` instead?



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to