ferroui_base/utilities/
uri_extensions.rs1use crate::platform::ASSET_SCHEME;
2use crate::utilities::{FormatError, Uri};
3
4pub struct UriExtensions;
6
7impl UriExtensions {
8 pub fn is_absolute_resm(uri: &Uri) -> bool {
10 uri.is_absolute_uri() && Self::is_resm(uri)
11 }
12
13 pub fn is_resm(uri: &Uri) -> bool {
16 uri.scheme() == "resm"
17 }
18
19 pub fn is_asset(uri: &Uri) -> bool {
22 uri.scheme() == ASSET_SCHEME
23 }
24
25 pub fn is_font_collection(uri: &Uri) -> bool {
28 uri.scheme() == crate::media::FontManager::FONT_COLLECTION_SCHEME
29 }
30
31 pub fn ensure_absolute(uri: &Uri, base_uri: Option<&Uri>) -> Result<Uri, FormatError> {
35 if uri.is_absolute_uri() {
36 return Ok(uri.clone());
37 }
38 let Some(base_uri) = base_uri else {
39 return Err(FormatError::from_string(format!("Relative uri {uri} without base url")));
40 };
41 if !base_uri.is_absolute_uri() {
42 return Err(FormatError::from_string(format!("Base uri {base_uri} is relative")));
43 }
44 Ok(Uri::combine(base_uri, uri))
45 }
46
47 pub fn authority(uri: &Uri) -> &str {
50 let text = uri.absolute_uri();
51 let rest = &text[uri.scheme().len() + 1..];
52 match rest.strip_prefix("//") {
53 Some(stripped) => &stripped[..stripped.find(['/', '?', '#']).unwrap_or(stripped.len())],
54 None => "",
55 }
56 }
57
58 pub fn get_unescape_absolute_path(uri: &Uri) -> String {
60 Self::unescape_data_string(uri.absolute_path())
61 }
62
63 pub fn get_unescape_absolute_uri(uri: &Uri) -> String {
65 Self::unescape_data_string(uri.absolute_uri())
66 }
67
68 pub fn get_assembly_name_from_query(uri: &Uri) -> String {
71 const ASSEMBLY: &[u8] = b"assembly";
72
73 let query = Self::unescape_data_string(uri.query());
74 let query = query.as_bytes();
75
76 let mut current_index = 1;
78 while current_index < query.len() {
79 let mut is_find = false;
80 let mut i = 0;
81 while i < ASSEMBLY.len() {
82 if current_index < query.len() && query[current_index] == ASSEMBLY[i] {
83 is_find = i == ASSEMBLY.len() - 1;
84 } else {
85 break;
86 }
87 current_index += 1;
88 i += 1;
89 }
90
91 current_index += 1;
93
94 let begin_index = current_index;
95 while current_index < query.len() && query[current_index] != b'&' {
96 current_index += 1;
97 }
98
99 if is_find {
100 let end_index = current_index.min(query.len());
103 let begin_index = begin_index.min(end_index);
104 return String::from_utf8_lossy(&query[begin_index..end_index]).into_owned();
105 }
106
107 current_index += 1;
108 }
109
110 String::new()
111 }
112
113 pub fn unescape_data_string(s: &str) -> String {
117 if !s.contains('%') {
118 return s.to_owned();
119 }
120
121 fn hex(b: u8) -> Option<u8> {
122 (b as char).to_digit(16).map(|d| d as u8)
123 }
124
125 let bytes = s.as_bytes();
126 let mut result = String::with_capacity(s.len());
127 let mut literal_start = 0;
128 let mut i = 0;
129 while i < bytes.len() {
130 if bytes[i] != b'%' {
131 i += 1;
132 continue;
133 }
134
135 let run_start = i;
137 let mut decoded = Vec::new();
138 while i + 2 < bytes.len() && bytes[i] == b'%' {
139 match (hex(bytes[i + 1]), hex(bytes[i + 2])) {
140 (Some(high), Some(low)) => {
141 decoded.push(high * 16 + low);
142 i += 3;
143 }
144 _ => break,
145 }
146 }
147 if decoded.is_empty() {
148 i += 1;
150 continue;
151 }
152
153 result.push_str(&s[literal_start..run_start]);
154 let mut offset = 0;
155 while offset < decoded.len() {
156 match std::str::from_utf8(&decoded[offset..]) {
157 Ok(valid) => {
158 result.push_str(valid);
159 offset = decoded.len();
160 }
161 Err(error) => {
162 let valid = error.valid_up_to();
163 result.push_str(std::str::from_utf8(&decoded[offset..offset + valid]).unwrap_or_default());
164 let invalid = error.error_len().unwrap_or(decoded.len() - offset - valid);
165 let source_start = run_start + (offset + valid) * 3;
166 result.push_str(&s[source_start..source_start + invalid * 3]);
167 offset += valid + invalid;
168 }
169 }
170 }
171 literal_start = i;
172 }
173 result.push_str(&s[literal_start..]);
174 result
175 }
176}
177
178#[cfg(test)]
179mod tests {
180 use super::*;
181 use crate::utilities::UriKind;
182
183 fn absolute(s: &str) -> Uri {
184 Uri::absolute(s).unwrap()
185 }
186
187 #[test]
188 fn assembly_name_from_query_parsed() {
189 const KEY: &str = "assembly";
190 const VALUE: &str = "Ferro.Themes.Simple";
191
192 let uri = absolute(&format!("resm:Ferro.Themes.Simple.Accents.BaseLight.xaml?{KEY}={VALUE}"));
193 let name = UriExtensions::get_assembly_name_from_query(&uri);
194
195 assert_eq!(VALUE, name);
196 }
197
198 #[test]
199 fn assembly_name_from_empty_query_not_parsed() {
200 let uri = absolute("resm:Ferro.Themes.Simple.Accents.BaseLight.xaml");
201 let name = UriExtensions::get_assembly_name_from_query(&uri);
202
203 assert_eq!("", name);
204 }
205
206 #[test]
209 fn schemes() {
210 let asset = absolute(&format!("{ASSET_SCHEME}://Lib/Assets/a.png"));
211 assert!(UriExtensions::is_asset(&asset));
212 assert!(!UriExtensions::is_resm(&asset));
213 assert!(!UriExtensions::is_absolute_resm(&asset));
214 let resm = absolute("resm:Lib.Assets.a.png?assembly=Lib");
215 assert!(UriExtensions::is_absolute_resm(&resm));
216 assert!(!UriExtensions::is_asset(&resm));
217 let relative = Uri::new("Assets/a.png", UriKind::Relative).unwrap();
218 assert!(!UriExtensions::is_absolute_resm(&relative));
219 }
220
221 #[test]
222 fn authority_and_paths() {
223 let asset = absolute(&format!("{ASSET_SCHEME}://Lib.Name/Assets/a%20b.png"));
224 assert_eq!("lib.name", UriExtensions::authority(&asset));
225 assert_eq!("/Assets/a b.png", UriExtensions::get_unescape_absolute_path(&asset));
226 assert_eq!(
227 format!("{ASSET_SCHEME}://lib.name/Assets/a b.png"),
228 UriExtensions::get_unescape_absolute_uri(&asset)
229 );
230 assert_eq!("", UriExtensions::authority(&absolute("resm:Lib.a.png")));
231 }
232
233 #[test]
234 fn ensure_absolute() {
235 let base = absolute(&format!("{ASSET_SCHEME}://lib/Views/Main.xaml"));
236 let relative = Uri::new("../Assets/a.png", UriKind::Relative).unwrap();
237 let result = UriExtensions::ensure_absolute(&relative, Some(&base)).unwrap();
238 assert_eq!(format!("{ASSET_SCHEME}://lib/Assets/a.png"), result.absolute_uri());
239 assert!(UriExtensions::ensure_absolute(&relative, None).is_err());
240 assert!(UriExtensions::ensure_absolute(&relative, Some(&relative)).is_err());
241 assert_eq!(base, UriExtensions::ensure_absolute(&base, None).unwrap());
242 }
243
244 #[test]
245 fn assembly_name_from_query() {
246 for (uri, expected) in [
247 ("resm:a.png?assembly=Lib", "Lib"),
248 ("resm:a.png?x=1&assembly=Lib.Name", "Lib.Name"),
249 ("resm:a.png?assembly=Lib&x=1", "Lib"),
250 ("resm:a.png?assembly=My%20Lib", "My Lib"),
251 ("resm:a.png?x=1", ""),
252 ("resm:a.png", ""),
253 ("resm:a.png?as", ""),
254 ("resm:a.png?assembly", ""),
255 ] {
256 assert_eq!(expected, UriExtensions::get_assembly_name_from_query(&absolute(uri)), "{uri}");
257 }
258 }
259
260 #[test]
261 fn unescape() {
262 assert_eq!("a b", UriExtensions::unescape_data_string("a%20b"));
263 assert_eq!("plain", UriExtensions::unescape_data_string("plain"));
264 assert_eq!("ż/ó", UriExtensions::unescape_data_string("%C5%BC/%C3%B3"));
265 assert_eq!("100%", UriExtensions::unescape_data_string("100%"));
266 assert_eq!("%zz%2", UriExtensions::unescape_data_string("%zz%2"));
267 assert_eq!("a%FFb", UriExtensions::unescape_data_string("a%FFb"));
268 assert_eq!("%C5 ", UriExtensions::unescape_data_string("%C5%20"));
269 }
270}