libdav/
common.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// Copyright 2023-2024 Hugo Osvaldo Barrera
//
// SPDX-License-Identifier: EUPL-1.2

//! Common bits shared between caldav and carddav clients.

use crate::{
    dav::{check_status, FoundCollection, WebDavClient, WebDavError},
    names,
    xmlutils::get_unquoted_href,
    CheckSupportError, PropertyName,
};

use http::{Method, Request, Response};
use hyper::{body::Incoming, Uri};
use log::debug;
use tower::Service;

/// Parse the response from PROPFIND requests searching for collections.
pub(crate) fn parse_find_multiple_collections(
    body: impl AsRef<[u8]>,
    only: &PropertyName<'_, '_>,
) -> Result<Vec<FoundCollection>, WebDavError> {
    let body = std::str::from_utf8(body.as_ref())?;
    let doc = roxmltree::Document::parse(body)?;
    let root = doc.root_element();

    let responses = root
        .descendants()
        .filter(|node| node.tag_name() == names::RESPONSE);

    let mut items = Vec::new();
    for response in responses {
        if !response
            .descendants()
            .find(|node| node.tag_name() == names::RESOURCETYPE)
            .map_or(false, |node| {
                node.descendants().any(|node| node.tag_name() == *only)
            })
        {
            continue;
        }

        let href = get_unquoted_href(&response)?.to_string();
        let etag = response
            .descendants()
            .find(|node| node.tag_name() == names::GETETAG)
            .and_then(|node| node.text().map(str::to_string));
        let supports_sync = response
            .descendants()
            .find(|node| node.tag_name() == names::SUPPORTED_REPORT_SET)
            .map_or(false, |node| {
                node.descendants()
                    .any(|node| node.tag_name() == names::SYNC_COLLECTION)
            });

        items.push(FoundCollection {
            href,
            etag,
            supports_sync,
        });
    }

    Ok(items)
}

pub(crate) async fn check_support<C>(
    client: &WebDavClient<C>,
    uri: &Uri,
    expectation: &str,
) -> Result<(), CheckSupportError>
where
    C: Service<http::Request<String>, Response = Response<Incoming>> + Sync + Send,
    <C as Service<http::Request<String>>>::Error: std::error::Error + Send + Sync,
{
    let request = Request::builder()
        .method(Method::OPTIONS)
        .uri(uri)
        .body(String::new())?;

    let (head, _body) = client.request(request).await?;
    check_status(head.status)?;

    let header = head
        .headers
        .get("DAV")
        .ok_or(CheckSupportError::MissingHeader)?
        .to_str()?;

    debug!("DAV header: '{}'", header);
    if header.split(',').any(|part| part.trim() == expectation) {
        Ok(())
    } else {
        Err(CheckSupportError::NotAdvertised)
    }
}

/// Error type for [`crate::caldav::service_for_url`] and [`crate::carddav::service_for_url`].
#[derive(thiserror::Error, Debug)]
pub enum ServiceForUrlError {
    /// The provided URL is missing a scheme.
    #[error("missing scheme in URL")]
    MissingScheme,
    /// The provided URL has an unknown scheme.
    #[error("unknown scheme in URL")]
    UnknownScheme,
}