code_moniker_core/core/uri/
parse.rs1use 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}