1#[cfg(not(feature = "std"))]
21use alloc::{
22 string::{String, ToString},
23 vec::Vec,
24};
25
26#[derive(Debug, PartialEq)]
43pub struct AddressUri<'a> {
44 pub phrase: Option<&'a str>,
46 pub paths: Vec<&'a str>,
48 pub pass: Option<&'a str>,
50}
51
52#[allow(missing_docs)]
54#[cfg_attr(feature = "std", derive(thiserror::Error))]
55#[derive(Debug, PartialEq, Eq, Clone)]
56pub enum Error {
57 #[cfg_attr(feature = "std", error("Invalid character in phrase:\n{0}"))]
58 InvalidCharacterInPhrase(InvalidCharacterInfo),
59 #[cfg_attr(feature = "std", error("Invalid character in password:\n{0}"))]
60 InvalidCharacterInPass(InvalidCharacterInfo),
61 #[cfg_attr(feature = "std", error("Missing character in hard path:\n{0}"))]
62 MissingCharacterInHardPath(InvalidCharacterInfo),
63 #[cfg_attr(feature = "std", error("Missing character in soft path:\n{0}"))]
64 MissingCharacterInSoftPath(InvalidCharacterInfo),
65}
66
67impl Error {
68 pub fn in_phrase(input: &str, pos: usize) -> Self {
70 Self::InvalidCharacterInPhrase(InvalidCharacterInfo::new(input, pos))
71 }
72 pub fn in_pass(input: &str, pos: usize) -> Self {
74 Self::InvalidCharacterInPass(InvalidCharacterInfo::new(input, pos))
75 }
76 pub fn in_hard_path(input: &str, pos: usize) -> Self {
78 Self::MissingCharacterInHardPath(InvalidCharacterInfo::new(input, pos))
79 }
80 pub fn in_soft_path(input: &str, pos: usize) -> Self {
82 Self::MissingCharacterInSoftPath(InvalidCharacterInfo::new(input, pos))
83 }
84}
85
86#[derive(Debug, PartialEq, Eq, Clone)]
92pub struct InvalidCharacterInfo(String, usize);
93
94impl InvalidCharacterInfo {
95 fn new(info: &str, pos: usize) -> Self {
96 Self(info.to_string(), pos)
97 }
98}
99
100impl core::fmt::Display for InvalidCharacterInfo {
101 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
102 let (s, pos) = escape_string(&self.0, self.1);
103 write!(f, "{s}\n{i}^", i = core::iter::repeat(" ").take(pos).collect::<String>())
104 }
105}
106
107fn escape_string(input: &str, pos: usize) -> (String, usize) {
110 let mut out = String::with_capacity(2 * input.len());
111 let mut out_pos = 0;
112 input
113 .chars()
114 .enumerate()
115 .map(|(i, c)| {
116 let esc = |c| (i, Some('\\'), c, 2);
117 match c {
118 '\t' => esc('t'),
119 '\n' => esc('n'),
120 '\r' => esc('r'),
121 '\x07' => esc('a'),
122 '\x08' => esc('b'),
123 '\x0b' => esc('v'),
124 '\x0c' => esc('f'),
125 _ => (i, None, c, 1),
126 }
127 })
128 .for_each(|(i, maybe_escape, c, increment)| {
129 maybe_escape.map(|e| out.push(e));
130 out.push(c);
131 if i < pos {
132 out_pos += increment;
133 }
134 });
135 (out, out_pos)
136}
137
138fn extract_prefix<'a>(input: &mut &'a str, is_allowed: &dyn Fn(char) -> bool) -> Option<&'a str> {
139 let output = input.trim_start_matches(is_allowed);
140 let prefix_len = input.len() - output.len();
141 let prefix = if prefix_len > 0 { Some(&input[..prefix_len]) } else { None };
142 *input = output;
143 prefix
144}
145
146fn strip_prefix(input: &mut &str, prefix: &str) -> bool {
147 if let Some(stripped_input) = input.strip_prefix(prefix) {
148 *input = stripped_input;
149 true
150 } else {
151 false
152 }
153}
154
155impl<'a> AddressUri<'a> {
156 pub fn parse(mut input: &'a str) -> Result<Self, Error> {
158 let initial_input = input;
159 let initial_input_len = input.len();
160 let phrase = extract_prefix(&mut input, &|ch: char| {
161 ch.is_ascii_digit() || ch.is_ascii_alphabetic() || ch == ' '
162 });
163
164 let mut pass = None;
165 let mut paths = Vec::new();
166 while !input.is_empty() {
167 let unstripped_input = input;
168 if strip_prefix(&mut input, "///") {
169 pass = Some(extract_prefix(&mut input, &|ch: char| ch != '\n').unwrap_or(""));
170 } else if strip_prefix(&mut input, "//") {
171 let path = extract_prefix(&mut input, &|ch: char| ch != '/')
172 .ok_or(Error::in_hard_path(initial_input, initial_input_len - input.len()))?;
173 assert!(path.len() > 0);
174 paths.push(&unstripped_input[1..path.len() + 2]);
176 } else if strip_prefix(&mut input, "/") {
177 paths.push(
178 extract_prefix(&mut input, &|ch: char| ch != '/').ok_or(
179 Error::in_soft_path(initial_input, initial_input_len - input.len()),
180 )?,
181 );
182 } else {
183 return Err(if pass.is_some() {
184 Error::in_pass(initial_input, initial_input_len - input.len())
185 } else {
186 Error::in_phrase(initial_input, initial_input_len - input.len())
187 })
188 }
189 }
190
191 Ok(Self { phrase, paths, pass })
192 }
193}
194
195#[cfg(test)]
196mod tests {
197 use super::*;
198 use regex::Regex;
199 use std::sync::LazyLock;
200
201 static SECRET_PHRASE_REGEX: LazyLock<Regex> = LazyLock::new(|| {
202 Regex::new(r"^(?P<phrase>[a-zA-Z0-9 ]+)?(?P<path>(//?[^/]+)*)(///(?P<password>.*))?$")
203 .expect("constructed from known-good static value; qed")
204 });
205
206 fn check_with_regex(input: &str) {
207 let regex_result = SECRET_PHRASE_REGEX.captures(input);
208 let manual_result = AddressUri::parse(input);
209 assert_eq!(regex_result.is_some(), manual_result.is_ok());
210 if let (Some(regex_result), Ok(manual_result)) = (regex_result, manual_result) {
211 assert_eq!(
212 regex_result.name("phrase").map(|phrase| phrase.as_str()),
213 manual_result.phrase
214 );
215
216 let manual_paths = manual_result
217 .paths
218 .iter()
219 .map(|s| "/".to_string() + s)
220 .collect::<Vec<_>>()
221 .join("");
222
223 assert_eq!(regex_result.name("path").unwrap().as_str().to_string(), manual_paths);
224 assert_eq!(
225 regex_result.name("password").map(|phrase| phrase.as_str()),
226 manual_result.pass
227 );
228 }
229 }
230
231 fn check(input: &str, result: Result<AddressUri, Error>) {
232 let manual_result = AddressUri::parse(input);
233 assert_eq!(manual_result, result);
234 check_with_regex(input);
235 }
236
237 #[test]
238 fn test00() {
239 check("///", Ok(AddressUri { phrase: None, pass: Some(""), paths: vec![] }));
240 }
241
242 #[test]
243 fn test01() {
244 check("////////", Ok(AddressUri { phrase: None, pass: Some("/////"), paths: vec![] }))
245 }
246
247 #[test]
248 fn test02() {
249 check(
250 "sdasd///asda",
251 Ok(AddressUri { phrase: Some("sdasd"), pass: Some("asda"), paths: vec![] }),
252 );
253 }
254
255 #[test]
256 fn test03() {
257 check(
258 "sdasd//asda",
259 Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec!["/asda"] }),
260 );
261 }
262
263 #[test]
264 fn test04() {
265 check("sdasd//a", Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec!["/a"] }));
266 }
267
268 #[test]
269 fn test05() {
270 let input = "sdasd//";
271 check(input, Err(Error::in_hard_path(input, 7)));
272 }
273
274 #[test]
275 fn test06() {
276 check(
277 "sdasd/xx//asda",
278 Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec!["xx", "/asda"] }),
279 );
280 }
281
282 #[test]
283 fn test07() {
284 check(
285 "sdasd/xx//a/b//c///pass",
286 Ok(AddressUri {
287 phrase: Some("sdasd"),
288 pass: Some("pass"),
289 paths: vec!["xx", "/a", "b", "/c"],
290 }),
291 );
292 }
293
294 #[test]
295 fn test08() {
296 check(
297 "sdasd/xx//a",
298 Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec!["xx", "/a"] }),
299 );
300 }
301
302 #[test]
303 fn test09() {
304 let input = "sdasd/xx//";
305 check(input, Err(Error::in_hard_path(input, 10)));
306 }
307
308 #[test]
309 fn test10() {
310 check(
311 "sdasd/asda",
312 Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec!["asda"] }),
313 );
314 }
315
316 #[test]
317 fn test11() {
318 check(
319 "sdasd/asda//x",
320 Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec!["asda", "/x"] }),
321 );
322 }
323
324 #[test]
325 fn test12() {
326 check("sdasd/a", Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec!["a"] }));
327 }
328
329 #[test]
330 fn test13() {
331 let input = "sdasd/";
332 check(input, Err(Error::in_soft_path(input, 6)));
333 }
334
335 #[test]
336 fn test14() {
337 check("sdasd", Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec![] }));
338 }
339
340 #[test]
341 fn test15() {
342 let input = "sdasd.";
343 check(input, Err(Error::in_phrase(input, 5)));
344 }
345
346 #[test]
347 fn test16() {
348 let input = "sd.asd/asd.a";
349 check(input, Err(Error::in_phrase(input, 2)));
350 }
351
352 #[test]
353 fn test17() {
354 let input = "sd.asd//asd.a";
355 check(input, Err(Error::in_phrase(input, 2)));
356 }
357
358 #[test]
359 fn test18() {
360 check(
361 "sdasd/asd.a",
362 Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec!["asd.a"] }),
363 );
364 }
365
366 #[test]
367 fn test19() {
368 check(
369 "sdasd//asd.a",
370 Ok(AddressUri { phrase: Some("sdasd"), pass: None, paths: vec!["/asd.a"] }),
371 );
372 }
373
374 #[test]
375 fn test20() {
376 let input = "///\n";
377 check(input, Err(Error::in_pass(input, 3)));
378 }
379
380 #[test]
381 fn test21() {
382 let input = "///a\n";
383 check(input, Err(Error::in_pass(input, 4)));
384 }
385
386 #[test]
387 fn test22() {
388 let input = "sd asd///asd.a\n";
389 check(input, Err(Error::in_pass(input, 14)));
390 }
391
392 #[test]
393 fn test_invalid_char_info_1() {
394 let expected = "01234\n^";
395 let f = format!("{}", InvalidCharacterInfo::new("01234", 0));
396 assert_eq!(expected, f);
397 }
398
399 #[test]
400 fn test_invalid_char_info_2() {
401 let expected = "01\n ^";
402 let f = format!("{}", InvalidCharacterInfo::new("01", 1));
403 assert_eq!(expected, f);
404 }
405
406 #[test]
407 fn test_invalid_char_info_3() {
408 let expected = "01234\n ^";
409 let f = format!("{}", InvalidCharacterInfo::new("01234", 2));
410 assert_eq!(expected, f);
411 }
412
413 #[test]
414 fn test_invalid_char_info_4() {
415 let expected = "012\\n456\n ^";
416 let f = format!("{}", InvalidCharacterInfo::new("012\n456", 3));
417 assert_eq!(expected, f);
418 }
419
420 #[test]
421 fn test_invalid_char_info_5() {
422 let expected = "012\\n456\n ^";
423 let f = format!("{}", InvalidCharacterInfo::new("012\n456", 5));
424 assert_eq!(expected, f);
425 }
426
427 #[test]
428 fn test_invalid_char_info_6() {
429 let expected = "012\\f456\\t89\n ^";
430 let f = format!("{}", InvalidCharacterInfo::new("012\x0c456\t89", 9));
431 assert_eq!(expected, f);
432 }
433}