Skip to main content

code_moniker_core/core/uri/
parse.rs

1use super::{UriConfig, UriError};
2use crate::core::moniker::{self, Moniker, MonikerBuilder};
3
4pub fn from_uri(uri: &str, config: &UriConfig<'_>) -> Result<Moniker, UriError> {
5	let rest = uri
6		.strip_prefix(config.scheme)
7		.ok_or_else(|| UriError::MissingScheme(config.scheme.to_string()))?;
8	let bytes = rest.as_bytes();
9
10	let mut i = 0;
11	let (project, project_end) = read_name(bytes, i, |c| c == b'/')?;
12	if project.is_empty() {
13		return Err(UriError::MissingProject);
14	}
15	std::str::from_utf8(&project).map_err(|_| UriError::NonUtf8Project)?;
16	i = project_end;
17
18	let mut builder = MonikerBuilder::new();
19	builder.project(&project);
20
21	while i < bytes.len() {
22		debug_assert_eq!(bytes[i], b'/');
23		i += 1;
24		let seg_start = i;
25		let (kind, kind_end) = read_kind(bytes, i, seg_start)?;
26		i = kind_end;
27		if i >= bytes.len() || bytes[i] != b':' {
28			return Err(UriError::MissingKindSeparator(seg_start));
29		}
30		i += 1;
31		let (name, name_end) = read_name(bytes, i, |c| c == b'/')?;
32		if name.is_empty() && kind.is_empty() {
33			return Err(UriError::EmptySegment(seg_start));
34		}
35		std::str::from_utf8(&name).map_err(|_| UriError::NonUtf8Segment)?;
36		builder.segment(&kind, &name);
37		i = name_end;
38	}
39
40	builder.try_build().map_err(uri_error_from_encoding)
41}
42
43fn uri_error_from_encoding(error: moniker::EncodingError) -> UriError {
44	match error {
45		moniker::EncodingError::EmptyProject => UriError::MissingProject,
46		moniker::EncodingError::NonUtf8Project => UriError::NonUtf8Project,
47		moniker::EncodingError::NonUtf8SegmentKind | moniker::EncodingError::NonUtf8SegmentName => {
48			UriError::NonUtf8Segment
49		}
50		moniker::EncodingError::InvalidSegmentKind => UriError::InvalidKind(String::new()),
51		moniker::EncodingError::ProjectTooLong(len) => UriError::ComponentTooLong("project", len),
52		moniker::EncodingError::SegmentKindTooLong(len) => {
53			UriError::ComponentTooLong("segment kind", len)
54		}
55		moniker::EncodingError::SegmentNameTooLong(len) => {
56			UriError::ComponentTooLong("segment name", len)
57		}
58		moniker::EncodingError::Truncated
59		| moniker::EncodingError::UnknownVersion(_)
60		| moniker::EncodingError::ProjectOverflow
61		| moniker::EncodingError::SegmentOverflow => {
62			unreachable!("URI parser builds encoded monikers from validated components")
63		}
64	}
65}
66
67fn read_kind(bytes: &[u8], start: usize, seg_start: usize) -> Result<(Vec<u8>, usize), UriError> {
68	let mut i = start;
69	while i < bytes.len() && bytes[i] != b':' && bytes[i] != b'/' {
70		i += 1;
71	}
72	let kind = &bytes[start..i];
73	if kind.is_empty() {
74		return Err(UriError::EmptySegment(seg_start));
75	}
76	if !is_plain_identifier(kind) {
77		return Err(UriError::InvalidKind(
78			String::from_utf8_lossy(kind).into_owned(),
79		));
80	}
81	Ok((kind.to_vec(), i))
82}
83
84fn is_plain_identifier(bytes: &[u8]) -> bool {
85	if bytes.is_empty() {
86		return false;
87	}
88	let first = bytes[0];
89	if !(first.is_ascii_alphabetic()) {
90		return false;
91	}
92	bytes[1..]
93		.iter()
94		.all(|b| b.is_ascii_alphanumeric() || *b == b'_')
95}
96
97fn read_name(
98	bytes: &[u8],
99	start: usize,
100	is_terminator: impl Fn(u8) -> bool,
101) -> Result<(Vec<u8>, usize), UriError> {
102	if start < bytes.len() && bytes[start] == b'`' {
103		let mut i = start + 1;
104		let mut out = Vec::new();
105		loop {
106			if i >= bytes.len() {
107				return Err(UriError::UnterminatedBacktick(start));
108			}
109			if bytes[i] == b'`' {
110				if i + 1 < bytes.len() && bytes[i + 1] == b'`' {
111					out.push(b'`');
112					i += 2;
113				} else {
114					i += 1;
115					break;
116				}
117			} else {
118				out.push(bytes[i]);
119				i += 1;
120			}
121		}
122		if i < bytes.len() && !is_terminator(bytes[i]) {
123			return Err(UriError::TrailingAfterBacktick(start));
124		}
125		Ok((out, i))
126	} else {
127		let mut i = start;
128		while i < bytes.len() && !is_terminator(bytes[i]) {
129			i += 1;
130		}
131		Ok((bytes[start..i].to_vec(), i))
132	}
133}
134
135#[cfg(test)]
136mod tests {
137	use super::super::test_helpers::*;
138	use super::*;
139
140	#[test]
141	fn from_uri_project_only() {
142		let m = from_uri("esac+moniker://my-app", &default_config()).unwrap();
143		assert_eq!(m.as_view().project(), b"my-app");
144		assert_eq!(m.as_view().segment_count(), 0);
145	}
146
147	#[test]
148	fn from_uri_path_chain() {
149		let m = from_uri(
150			"esac+moniker://my-app/path:main/path:com/path:acme/class:Foo",
151			&default_config(),
152		)
153		.unwrap();
154		let v = m.as_view();
155		let segs: Vec<_> = v.segments().collect();
156		assert_eq!(segs.len(), 4);
157		assert_eq!(segs[0].kind, b"path");
158		assert_eq!(segs[0].name, b"main");
159		assert_eq!(segs[3].kind, b"class");
160		assert_eq!(segs[3].name, b"Foo");
161	}
162
163	#[test]
164	fn from_uri_method_with_arity_in_name() {
165		let m = from_uri(
166			"esac+moniker://app/class:Foo/method:bar(2)",
167			&default_config(),
168		)
169		.unwrap();
170		let segs: Vec<_> = m.as_view().segments().collect();
171		assert_eq!(segs[1].kind, b"method");
172		assert_eq!(segs[1].name, b"bar(2)");
173	}
174
175	#[test]
176	fn from_uri_typed_signature_in_name() {
177		let m = from_uri(
178			"esac+moniker://app/class:UserService/method:findById(String)",
179			&default_config(),
180		)
181		.unwrap();
182		let segs: Vec<_> = m.as_view().segments().collect();
183		assert_eq!(segs[1].name, b"findById(String)");
184	}
185
186	#[test]
187	fn from_uri_backtick_name() {
188		let m = from_uri("esac+moniker://app/path:`util/test.ts`", &default_config()).unwrap();
189		let segs: Vec<_> = m.as_view().segments().collect();
190		assert_eq!(segs[0].name, b"util/test.ts");
191	}
192
193	#[test]
194	fn from_uri_doubled_backtick() {
195		let m = from_uri("esac+moniker://app/class:`weird``name`", &default_config()).unwrap();
196		assert_eq!(m.as_view().segments().next().unwrap().name, b"weird`name");
197	}
198
199	#[test]
200	fn from_uri_rejects_missing_scheme() {
201		let err = from_uri("esac://app", &default_config()).unwrap_err();
202		match err {
203			UriError::MissingScheme(expected) => assert_eq!(expected, "esac+moniker://"),
204			other => panic!("unexpected error: {other:?}"),
205		}
206	}
207
208	#[test]
209	fn from_uri_rejects_missing_project() {
210		assert_eq!(
211			from_uri("esac+moniker:///path:foo", &default_config()).unwrap_err(),
212			UriError::MissingProject
213		);
214	}
215
216	#[test]
217	fn from_uri_rejects_missing_kind_separator() {
218		let err = from_uri("esac+moniker://app/just_a_name", &default_config()).unwrap_err();
219		assert!(matches!(err, UriError::MissingKindSeparator(_)));
220	}
221
222	#[test]
223	fn from_uri_rejects_invalid_kind_starting_with_digit() {
224		let err = from_uri("esac+moniker://app/9bad:name", &default_config()).unwrap_err();
225		assert!(matches!(err, UriError::InvalidKind(_)));
226	}
227
228	#[test]
229	fn from_uri_rejects_unterminated_backtick() {
230		let r = from_uri("esac+moniker://app/path:`unterminated", &default_config());
231		assert!(matches!(r.unwrap_err(), UriError::UnterminatedBacktick(_)));
232	}
233
234	#[test]
235	fn from_uri_rejects_trailing_data_after_backtick() {
236		let r = from_uri("esac+moniker://`x`A", &default_config());
237		assert!(matches!(r.unwrap_err(), UriError::TrailingAfterBacktick(_)));
238	}
239
240	#[test]
241	fn from_uri_rejects_oversized_project_without_panic() {
242		let project = "a".repeat(u16::MAX as usize + 1);
243		let uri = format!("esac+moniker://{project}");
244		assert_eq!(
245			from_uri(&uri, &default_config()).unwrap_err(),
246			UriError::ComponentTooLong("project", project.len())
247		);
248	}
249
250	#[test]
251	fn from_uri_rejects_oversized_segment_name_without_panic() {
252		let name = "a".repeat(u16::MAX as usize + 1);
253		let uri = format!("esac+moniker://app/path:{name}");
254		assert_eq!(
255			from_uri(&uri, &default_config()).unwrap_err(),
256			UriError::ComponentTooLong("segment name", name.len())
257		);
258	}
259
260	use proptest::prelude::*;
261
262	proptest! {
263		#![proptest_config(ProptestConfig {
264			cases: 256,
265			..ProptestConfig::default()
266		})]
267
268		#[test]
269		fn from_uri_never_panics(input in ".{0,512}") {
270			let _ = from_uri(&input, &default_config());
271		}
272
273		#[test]
274		fn from_uri_with_scheme_never_panics(suffix in ".{0,512}") {
275			let s = format!("esac+moniker://{suffix}");
276			let _ = from_uri(&s, &default_config());
277		}
278
279		#[test]
280		fn from_uri_lossy_bytes_never_panics(bytes in proptest::collection::vec(any::<u8>(), 0..512)) {
281			let s = String::from_utf8_lossy(&bytes);
282			let _ = from_uri(&s, &default_config());
283		}
284	}
285}