1#![warn(clippy::all, clippy::pedantic, clippy::nursery)]
2#![allow(clippy::missing_errors_doc)]
3pub mod error;
4pub mod extensions;
5pub mod header;
6pub mod namespace;
7pub mod privileges;
8pub mod resource;
9pub mod resources;
10pub mod xml;
11use std::borrow::Cow;
12
13pub use error::Error;
14use http::Uri;
15use itertools::Itertools;
16use percent_encoding::{AsciiSet, CONTROLS, percent_decode_str};
17
18pub trait Principal: std::fmt::Debug + Clone + Send + Sync + 'static {
22 fn get_id(&self) -> &str;
23}
24
25const RFC_3986: &AsciiSet = &CONTROLS
35 .add(b':')
36 .add(b'/')
37 .add(b'?')
38 .add(b'#')
39 .add(b'[')
40 .add(b']')
41 .add(b'@')
42 .add(b'!')
43 .add(b'$')
44 .add(b'&')
45 .add(b'\\')
46 .add(b'(')
47 .add(b')')
48 .add(b'*')
49 .add(b'+')
50 .add(b',')
51 .add(b';')
52 .add(b'=')
53 .add(b' ');
55
56#[inline]
57#[must_use]
58pub fn rfc_3986_percent_encode(input: &'_ str) -> percent_encoding::PercentEncode<'_> {
59 percent_encoding::percent_encode(input.as_bytes(), RFC_3986)
60}
61
62#[cfg(test)]
63mod tests {
64 use crate::rfc_3986_percent_encode;
65
66 #[rstest::rstest]
68 #[case("hello.ics", "hello.ics")]
69 #[case("hello@example.com", "hello%40example.com")]
70 #[case("slash/slash", "slash%2Fslash")]
71 fn test_percent_encoding(#[case] input: &str, #[case] expected_result: &str) {
72 assert_eq!(&rfc_3986_percent_encode(input).to_string(), expected_result);
73 }
74}
75
76pub fn resolve_child_uri<'a>(
77 collection_uri: &'_ Uri,
78 child_uri: &'a Uri,
79) -> Option<Vec<Cow<'a, str>>> {
80 if let Some(child_scheme) = child_uri.scheme() {
82 let collection_scheme = collection_uri.scheme()?;
83 if child_scheme != collection_scheme {
84 return None;
85 }
86 }
87
88 if let Some(child_authority) = child_uri.authority() {
90 let collection_authority = collection_uri.authority()?;
91 if child_authority != collection_authority {
92 return None;
93 }
94 }
95
96 let collection_path_segments = collection_uri
98 .path()
99 .split('/')
100 .filter(|seg| !seg.is_empty());
101 let mut child_path_segments = child_uri.path().split('/').filter(|seg| !seg.is_empty());
102
103 for collection_segment in collection_path_segments {
104 let Some(child_segment) = child_path_segments.next() else {
105 return None;
107 };
108 if percent_decode_str(collection_segment).collect_vec()
110 != percent_decode_str(child_segment).collect_vec()
111 {
112 return None;
113 }
114 }
115
116 child_path_segments
117 .map(|segment| percent_decode_str(segment).decode_utf8().ok())
118 .collect()
119}
120
121#[cfg(test)]
122mod uri_tests {
123 use crate::resolve_child_uri;
124 use http::Uri;
125 use std::borrow::Cow;
126
127 #[rstest::rstest]
128 #[case("https://rustical.example.com", "/hello", Some(vec!["hello".into()]))]
129 #[case("https://rustical.example.com/", "/hello", Some(vec!["hello".into()]))]
130 #[case("https://rustical.example.com/", "hello", None)]
132 #[case(
134 "https://rustical.example.com/caldav/principal/user%40example%2Ecom/cal/",
135 "https://cal.hello.dev/caldav/principal/user%40example%2Ecom/cal/hello.ics",
136 None
137 )]
138 #[case(
140 "/caldav/principal/user%40example%2Ecom/cal/",
141 "/caldav/principal/user%40example%2Ecom/cal/hello.ics",
142 Some(vec!["hello.ics".into()])
143 )]
144 #[case(
146 "/caldav/principal/user%40example%2Ecom/cal/",
147 "/caldav/principal/user@example.com/cal/unescaped.ics",
148 Some(vec!["unescaped.ics".into()])
149 )]
150 #[case(
152 "/caldav/principal/user%40example%2Ecom/cal/",
153 "/caldav/principal/user%40example.com/cal/shouldwork.ics",
154 Some(vec!["shouldwork.ics".into()])
155 )]
156 #[case(
158 "/caldav/principal/user%40example%2Ecom/cal/",
159 "/caldav/principal/user%40example%2Ecom/nocal/hello.ics",
160 None
161 )]
162 #[case(
164 "/caldav/principal/user%40example.com/cal/",
165 "/caldav/principal/user%40example%2Ecom/cal/hello.ics",
166 Some(vec!["hello.ics".into()])
167 )]
168 #[case(
170 "/caldav/principal/user%40example.com/cal/",
171 "/caldav/principal/user@example.com/cal/unescaped.ics",
172 Some(vec!["unescaped.ics".into()])
173 )]
174 #[case(
176 "/caldav/principal/user%40example.com/cal/",
177 "/caldav/principal/user%40example.com/cal/shouldwork.ics",
178 Some(vec!["shouldwork.ics".into()])
179 )]
180 #[case(
182 "/caldav/principal/user%40example.com/cal/",
183 "/caldav/principal/user%40example%2Ecom/nocal/hello.ics",
184 None
185 )]
186 #[case("https://example.com", "/hello.ics", Some(vec!["hello.ics".into()]))]
188 #[case("https://example.com", "/hello%2Eics", Some(vec!["hello.ics".into()]))]
190 fn test_resolve_child_uri(
191 #[case] collection: &'static str,
192 #[case] child: &'static str,
193 #[case] expected_out: Option<Vec<Cow<'static, str>>>,
194 ) {
195 let collection_uri = Uri::from_static(collection);
196 let child_uri = Uri::from_static(child);
197
198 assert_eq!(resolve_child_uri(&collection_uri, &child_uri), expected_out);
199 }
200}