sp_core/
address_uri.rs

1// This file is part of Substrate.
2
3// Copyright (C) Parity Technologies (UK) Ltd.
4// SPDX-License-Identifier: Apache-2.0
5
6// Licensed under the Apache License, Version 2.0 (the "License");
7// you may not use this file except in compliance with the License.
8// You may obtain a copy of the License at
9//
10// 	http://www.apache.org/licenses/LICENSE-2.0
11//
12// Unless required by applicable law or agreed to in writing, software
13// distributed under the License is distributed on an "AS IS" BASIS,
14// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15// See the License for the specific language governing permissions and
16// limitations under the License.
17
18//! Little util for parsing an address URI. Replaces regular expressions.
19
20#[cfg(not(feature = "std"))]
21use alloc::{
22	string::{String, ToString},
23	vec::Vec,
24};
25
26/// A container for results of parsing the address uri string.
27///
28/// Intended to be equivalent of:
29/// `Regex::new(r"^(?P<phrase>[a-zA-Z0-9 ]+)?(?P<path>(//?[^/]+)*)(///(?P<password>.*))?$")`
30/// which also handles soft and hard derivation paths:
31/// `Regex::new(r"/(/?[^/]+)")`
32///
33/// Example:
34/// ```
35/// 	use sp_core::crypto::AddressUri;
36/// 	let manual_result = AddressUri::parse("hello world/s//h///pass");
37/// 	assert_eq!(
38/// 		manual_result.unwrap(),
39/// 		AddressUri { phrase: Some("hello world"), paths: vec!["s", "/h"], pass: Some("pass") }
40/// 	);
41/// ```
42#[derive(Debug, PartialEq)]
43pub struct AddressUri<'a> {
44	/// Phrase, hexadecimal string, or ss58-compatible string.
45	pub phrase: Option<&'a str>,
46	/// Key derivation paths, ordered as in input string,
47	pub paths: Vec<&'a str>,
48	/// Password.
49	pub pass: Option<&'a str>,
50}
51
52/// Errors that are possible during parsing the address URI.
53#[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	/// Creates an instance of `Error::InvalidCharacterInPhrase` using given parameters.
69	pub fn in_phrase(input: &str, pos: usize) -> Self {
70		Self::InvalidCharacterInPhrase(InvalidCharacterInfo::new(input, pos))
71	}
72	/// Creates an instance of `Error::InvalidCharacterInPass` using given parameters.
73	pub fn in_pass(input: &str, pos: usize) -> Self {
74		Self::InvalidCharacterInPass(InvalidCharacterInfo::new(input, pos))
75	}
76	/// Creates an instance of `Error::MissingCharacterInHardPath` using given parameters.
77	pub fn in_hard_path(input: &str, pos: usize) -> Self {
78		Self::MissingCharacterInHardPath(InvalidCharacterInfo::new(input, pos))
79	}
80	/// Creates an instance of `Error::MissingCharacterInSoftPath` using given parameters.
81	pub fn in_soft_path(input: &str, pos: usize) -> Self {
82		Self::MissingCharacterInSoftPath(InvalidCharacterInfo::new(input, pos))
83	}
84}
85
86/// Complementary error information.
87///
88/// Structure contains complementary information about parsing address URI string.
89/// String contains a copy of an original URI string, 0-based integer indicates position of invalid
90/// character.
91#[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
107/// Escapes the control characters in given string, and recomputes the position if some characters
108/// were actually escaped.
109fn 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	/// Parses the given string.
157	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				// hard path shall contain leading '/', so take it from unstripped input.
175				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}