1use std::fmt;
2
3#[derive(Debug, Clone, PartialEq, Eq)]
4pub enum MeCanonicalSelectorKind {
5 Fanout,
6 Current,
7 Surface,
8 Claim,
9}
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub struct MeCanonicalSelector {
13 pub kind: MeCanonicalSelectorKind,
14 pub raw: String,
15 pub value: String,
16 pub shorthand: bool,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct MeCanonicalUri {
21 pub scheme: String,
22 pub raw: String,
23 pub href: String,
24 pub namespace: String,
25 pub handle: String,
26 pub space: String,
27 pub selector: Option<MeCanonicalSelector>,
28 pub path: Option<String>,
29 pub segments: Vec<String>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct MeHumanIdentity {
34 pub raw: String,
35 pub alias: String,
36 pub handle: String,
37 pub space: String,
38 pub namespace: String,
39 pub uri: String,
40}
41
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum MeDnsProjectionFailureReason {
44 InvalidHost,
45 TransportOnlyHost,
46 UnknownSpace,
47 NotCanonicalNamespace,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub enum MeDnsProjection {
52 Space {
53 raw_host: String,
54 host: String,
55 matched_space: String,
56 prefix_labels: Vec<String>,
57 space: String,
58 },
59 Namespace {
60 raw_host: String,
61 host: String,
62 matched_space: String,
63 prefix_labels: Vec<String>,
64 handle: String,
65 space: String,
66 namespace: String,
67 uri: String,
68 },
69 Invalid {
70 raw_host: String,
71 host: String,
72 matched_space: Option<String>,
73 prefix_labels: Vec<String>,
74 reason: MeDnsProjectionFailureReason,
75 },
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub enum MeUriError {
80 Required(&'static str),
81 InvalidScheme(String),
82 MissingNamespace(String),
83 InvalidCanonicalNamespace(String),
84 InvalidCanonicalHandle(String),
85 InvalidCanonicalSpace(String),
86 InvalidSpaceLabel(String),
87 InvalidSelector(String),
88 InvalidClaimSelector(String),
89 InvalidPath(String),
90 UnknownCanonicalSpace(String),
91 InvalidHumanIdentity(String),
92 MalformedSelector(String),
93}
94
95impl fmt::Display for MeUriError {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 match self {
98 Self::Required(label) => write!(f, "{label} is required"),
99 Self::InvalidScheme(value) => {
100 write!(f, "invalid me URI \"{value}\": expected \"me://\" scheme")
101 }
102 Self::MissingNamespace(value) => {
103 write!(f, "invalid me URI \"{value}\": missing namespace")
104 }
105 Self::InvalidCanonicalNamespace(value) => write!(
106 f,
107 "invalid canonical namespace \"{value}\": expected handle.space with a dotted space"
108 ),
109 Self::InvalidCanonicalHandle(value) => write!(f, "invalid canonical handle: {value}"),
110 Self::InvalidCanonicalSpace(value) => write!(
111 f,
112 "invalid canonical space \"{value}\": expected at least two labels"
113 ),
114 Self::InvalidSpaceLabel(value) => write!(f, "invalid space label: {value}"),
115 Self::InvalidSelector(value) => write!(f, "invalid surface selector: {value}"),
116 Self::InvalidClaimSelector(value) => write!(f, "invalid claim selector token: {value}"),
117 Self::InvalidPath(value) => write!(f, "invalid canonical path \"{value}\""),
118 Self::UnknownCanonicalSpace(value) => write!(f, "unknown canonical space \"{value}\""),
119 Self::InvalidHumanIdentity(value) => {
120 write!(
121 f,
122 "invalid human identity \"{value}\": expected handle@space"
123 )
124 }
125 Self::MalformedSelector(value) => {
126 write!(f, "invalid me URI \"{value}\": malformed selector")
127 }
128 }
129 }
130}
131
132impl std::error::Error for MeUriError {}
133
134pub fn normalize_canonical_handle(raw_handle: &str) -> Result<String, MeUriError> {
135 let handle = required(raw_handle, "Handle")?.to_ascii_lowercase();
136 if !is_dns_label(&handle) {
137 return Err(MeUriError::InvalidCanonicalHandle(raw_handle.to_string()));
138 }
139 Ok(handle)
140}
141
142pub fn normalize_canonical_space(raw_space: &str) -> Result<String, MeUriError> {
143 let normalized = normalize_hostish_value(required(raw_space, "Space")?);
144 let labels = split_labels(&normalized);
145 if labels.len() < 2 {
146 return Err(MeUriError::InvalidCanonicalSpace(raw_space.to_string()));
147 }
148 for label in &labels {
149 if !is_dns_label(label) {
150 return Err(MeUriError::InvalidSpaceLabel(raw_space.to_string()));
151 }
152 }
153 Ok(labels.join("."))
154}
155
156pub fn format_canonical_me_uri(
157 handle: &str,
158 space: &str,
159 selector: Option<&str>,
160 path: Option<&str>,
161) -> Result<String, MeUriError> {
162 let handle = normalize_canonical_handle(handle)?;
163 let space = normalize_canonical_space(space)?;
164 let selector = serialize_selector(selector)?;
165 let path = match path {
166 Some(raw_path) if !raw_path.trim().is_empty() => {
167 format!("/{}", parse_canonical_path(raw_path)?.0)
168 }
169 _ => String::new(),
170 };
171 Ok(format!("me://{handle}.{space}{selector}{path}"))
172}
173
174pub fn parse_me_uri(raw_input: &str) -> Result<MeCanonicalUri, MeUriError> {
175 let raw = required(raw_input, "me:// URI")?.to_string();
176 if !raw.to_ascii_lowercase().starts_with("me://") {
177 return Err(MeUriError::InvalidScheme(raw_input.to_string()));
178 }
179
180 let without_scheme = &raw[5..];
181 if without_scheme.trim().is_empty() {
182 return Err(MeUriError::MissingNamespace(raw_input.to_string()));
183 }
184
185 let slash_index = without_scheme.find('/');
186 let head = slash_index
187 .map(|index| &without_scheme[..index])
188 .unwrap_or(without_scheme)
189 .trim();
190 let path_part = slash_index
191 .map(|index| &without_scheme[index + 1..])
192 .unwrap_or("");
193
194 let mut namespace_part = head;
195 let mut selector_part = None;
196 if let Some(open_index) = head.find('[') {
197 let close_index = head.rfind(']');
198 if close_index.is_none_or(|index| index < open_index || index != head.len() - 1) {
199 return Err(MeUriError::MalformedSelector(raw_input.to_string()));
200 }
201 let close_index = close_index.expect("checked above");
202 namespace_part = head[..open_index].trim();
203 selector_part = Some(head[open_index + 1..close_index].trim());
204 }
205
206 let namespace = parse_canonical_namespace(namespace_part)?;
207 let selector = parse_canonical_selector(selector_part)?;
208 let path = if slash_index.is_some() {
209 Some(parse_canonical_path(path_part)?)
210 } else {
211 None
212 };
213 let href = format_canonical_me_uri(
214 &namespace.handle,
215 &namespace.space,
216 selector.as_ref().map(|selector| selector.raw.as_str()),
217 path.as_ref().map(|path| path.0.as_str()),
218 )?;
219
220 Ok(MeCanonicalUri {
221 scheme: "me".to_string(),
222 raw,
223 href,
224 namespace: namespace.value,
225 handle: namespace.handle,
226 space: namespace.space,
227 selector,
228 path: path.as_ref().map(|path| path.0.clone()),
229 segments: path.map(|path| path.1).unwrap_or_default(),
230 })
231}
232
233pub fn try_parse_me_uri(raw_input: &str) -> Option<MeCanonicalUri> {
234 parse_me_uri(raw_input).ok()
235}
236
237pub fn parse_canonical_me_uri(
238 raw_input: &str,
239 known_spaces: &[&str],
240) -> Result<MeCanonicalUri, MeUriError> {
241 let parsed = parse_me_uri(raw_input)?;
242 let known_spaces = normalize_known_spaces(known_spaces)?;
243 if !known_spaces.is_empty() && !known_spaces.contains(&parsed.space) {
244 return Err(MeUriError::UnknownCanonicalSpace(parsed.space));
245 }
246 Ok(parsed)
247}
248
249pub fn canonicalize_human_identity(
250 raw_input: &str,
251 known_spaces: &[&str],
252) -> Result<MeHumanIdentity, MeUriError> {
253 let raw = required(raw_input, "Human identity")?.to_string();
254 let mut parts = raw.split('@');
255 let handle_part = parts.next();
256 let space_part = parts.next();
257 if handle_part.is_none() || space_part.is_none() || parts.next().is_some() {
258 return Err(MeUriError::InvalidHumanIdentity(raw_input.to_string()));
259 }
260
261 let handle = normalize_canonical_handle(handle_part.expect("checked above"))?;
262 let space = normalize_canonical_space(space_part.expect("checked above"))?;
263 let uri = format_canonical_me_uri(&handle, &space, None, None)?;
264 parse_canonical_me_uri(&uri, known_spaces)?;
265 let namespace = format!("{handle}.{space}");
266
267 Ok(MeHumanIdentity {
268 raw,
269 alias: format!("{handle}@{space}"),
270 handle,
271 space,
272 namespace,
273 uri,
274 })
275}
276
277pub fn canonicalize_legacy_at_operator(raw_input: &str, known_spaces: &[&str]) -> Option<String> {
278 let raw = raw_input.trim();
279 if !raw.contains('@') {
280 return None;
281 }
282 canonicalize_human_identity(raw, known_spaces)
283 .ok()
284 .map(|identity| identity.uri)
285}
286
287pub fn project_dns_host_to_namespace(raw_host: &str, known_spaces: &[&str]) -> MeDnsProjection {
288 let host = normalize_hostish_value(raw_host);
289 if host.is_empty() {
290 return MeDnsProjection::Invalid {
291 raw_host: raw_host.to_string(),
292 host,
293 matched_space: None,
294 prefix_labels: Vec::new(),
295 reason: MeDnsProjectionFailureReason::InvalidHost,
296 };
297 }
298
299 if host == "localhost" || host.ends_with(".local") {
300 return MeDnsProjection::Invalid {
301 raw_host: raw_host.to_string(),
302 host,
303 matched_space: None,
304 prefix_labels: Vec::new(),
305 reason: MeDnsProjectionFailureReason::TransportOnlyHost,
306 };
307 }
308
309 let known_spaces = normalize_known_spaces(known_spaces).unwrap_or_default();
310 let Some(matched_space) = find_longest_known_space_suffix(&host, &known_spaces) else {
311 return MeDnsProjection::Invalid {
312 raw_host: raw_host.to_string(),
313 host,
314 matched_space: None,
315 prefix_labels: Vec::new(),
316 reason: MeDnsProjectionFailureReason::UnknownSpace,
317 };
318 };
319
320 if host == matched_space {
321 return MeDnsProjection::Space {
322 raw_host: raw_host.to_string(),
323 host,
324 matched_space: matched_space.clone(),
325 prefix_labels: Vec::new(),
326 space: matched_space,
327 };
328 }
329
330 let prefix = host
331 .strip_suffix(&format!(".{matched_space}"))
332 .unwrap_or("")
333 .to_string();
334 let prefix_labels = split_labels(&prefix);
335 if prefix_labels.len() != 1 || !is_dns_label(&prefix_labels[0]) {
336 return MeDnsProjection::Invalid {
337 raw_host: raw_host.to_string(),
338 host,
339 matched_space: Some(matched_space),
340 prefix_labels,
341 reason: MeDnsProjectionFailureReason::NotCanonicalNamespace,
342 };
343 }
344
345 let handle = prefix_labels[0].clone();
346 let namespace = format!("{handle}.{matched_space}");
347 let uri = format_canonical_me_uri(&handle, &matched_space, None, None).unwrap_or_else(|_| {
348 format!("me://{namespace}")
350 });
351
352 MeDnsProjection::Namespace {
353 raw_host: raw_host.to_string(),
354 host,
355 matched_space: matched_space.clone(),
356 prefix_labels,
357 handle,
358 space: matched_space,
359 namespace,
360 uri,
361 }
362}
363
364#[derive(Debug, Clone, PartialEq, Eq)]
365struct MeCanonicalNamespace {
366 handle: String,
367 space: String,
368 value: String,
369}
370
371fn parse_canonical_namespace(raw_namespace: &str) -> Result<MeCanonicalNamespace, MeUriError> {
372 let namespace = required(raw_namespace, "Canonical namespace")?
373 .trim_start_matches("me://")
374 .trim()
375 .to_ascii_lowercase();
376 let labels = split_labels(&namespace);
377 if labels.len() < 3 {
378 return Err(MeUriError::InvalidCanonicalNamespace(
379 raw_namespace.to_string(),
380 ));
381 }
382 let handle = normalize_canonical_handle(&labels[0])?;
383 let space = normalize_canonical_space(&labels[1..].join("."))?;
384 let value = format!("{handle}.{space}");
385 Ok(MeCanonicalNamespace {
386 handle,
387 space,
388 value,
389 })
390}
391
392fn parse_canonical_selector(
393 raw_selector: Option<&str>,
394) -> Result<Option<MeCanonicalSelector>, MeUriError> {
395 let Some(raw_selector) = raw_selector else {
396 return Ok(None);
397 };
398 let value = raw_selector.trim();
399
400 if value.is_empty() {
401 return Ok(Some(MeCanonicalSelector {
402 kind: MeCanonicalSelectorKind::Fanout,
403 raw: value.to_string(),
404 value: value.to_string(),
405 shorthand: false,
406 }));
407 }
408
409 if value == "current" {
410 return Ok(Some(MeCanonicalSelector {
411 kind: MeCanonicalSelectorKind::Current,
412 raw: value.to_string(),
413 value: value.to_string(),
414 shorthand: false,
415 }));
416 }
417
418 if let Some(token) = value.strip_prefix("claim:") {
419 if !is_surface_token(token) {
420 return Err(MeUriError::InvalidClaimSelector(value.to_string()));
421 }
422 return Ok(Some(MeCanonicalSelector {
423 kind: MeCanonicalSelectorKind::Claim,
424 raw: value.to_string(),
425 value: format!("claim:{token}"),
426 shorthand: false,
427 }));
428 }
429
430 let shorthand = !value.starts_with("surface:");
431 let surface_name = if shorthand {
432 value
433 } else {
434 &value["surface:".len()..]
435 };
436 if !is_surface_token(surface_name) {
437 return Err(MeUriError::InvalidSelector(value.to_string()));
438 }
439
440 Ok(Some(MeCanonicalSelector {
441 kind: MeCanonicalSelectorKind::Surface,
442 raw: value.to_string(),
443 value: format!("surface:{surface_name}"),
444 shorthand,
445 }))
446}
447
448fn parse_canonical_path(raw_path: &str) -> Result<(String, Vec<String>), MeUriError> {
449 let trimmed = raw_path.trim();
450 if trimmed.is_empty() {
451 return Err(MeUriError::InvalidPath(raw_path.to_string()));
452 }
453 if trimmed.contains('/') {
454 return Err(MeUriError::InvalidPath(raw_path.to_string()));
455 }
456
457 let segments = trimmed
458 .split('.')
459 .map(str::trim)
460 .map(str::to_string)
461 .collect::<Vec<_>>();
462 if segments.iter().any(|segment| segment.is_empty()) {
463 return Err(MeUriError::InvalidPath(raw_path.to_string()));
464 }
465 if segments.iter().any(|segment| !is_path_segment(segment)) {
466 return Err(MeUriError::InvalidPath(raw_path.to_string()));
467 }
468
469 Ok((segments.join("."), segments))
470}
471
472fn serialize_selector(selector: Option<&str>) -> Result<String, MeUriError> {
473 let Some(selector) = selector else {
474 return Ok(String::new());
475 };
476 let Some(parsed) = parse_canonical_selector(Some(selector))? else {
477 return Ok(String::new());
478 };
479 Ok(format!("[{}]", parsed.value))
480}
481
482fn normalize_known_spaces(known_spaces: &[&str]) -> Result<Vec<String>, MeUriError> {
483 let mut unique = Vec::<String>::new();
484 for raw_space in known_spaces {
485 let space = normalize_canonical_space(raw_space)?;
486 if !unique.contains(&space) {
487 unique.push(space);
488 }
489 }
490 unique.sort_by(|left, right| {
491 right
492 .split('.')
493 .count()
494 .cmp(&left.split('.').count())
495 .then_with(|| right.len().cmp(&left.len()))
496 });
497 Ok(unique)
498}
499
500fn find_longest_known_space_suffix(host: &str, known_spaces: &[String]) -> Option<String> {
501 for known_space in known_spaces {
502 if host == known_space || host.ends_with(&format!(".{known_space}")) {
503 return Some(known_space.clone());
504 }
505 }
506 None
507}
508
509fn normalize_hostish_value(raw_value: &str) -> String {
510 let mut value = raw_value.trim().to_string();
511 if value.is_empty() {
512 return value;
513 }
514
515 if let Some(scheme_index) = value.find("://") {
516 value = value[scheme_index + 3..].to_string();
517 }
518 for delimiter in ['/', '?', '#'] {
519 if let Some(index) = value.find(delimiter) {
520 value.truncate(index);
521 }
522 }
523 if let Some(colon_index) = value.rfind(':') {
524 if value[colon_index + 1..]
525 .chars()
526 .all(|ch| ch.is_ascii_digit())
527 {
528 value.truncate(colon_index);
529 }
530 }
531
532 value.trim_end_matches('.').trim().to_ascii_lowercase()
533}
534
535fn required<'a>(value: &'a str, label: &'static str) -> Result<&'a str, MeUriError> {
536 let trimmed = value.trim();
537 if trimmed.is_empty() {
538 return Err(MeUriError::Required(label));
539 }
540 Ok(trimmed)
541}
542
543fn split_labels(value: &str) -> Vec<String> {
544 value
545 .split('.')
546 .map(str::trim)
547 .filter(|label| !label.is_empty())
548 .map(str::to_string)
549 .collect()
550}
551
552fn is_dns_label(value: &str) -> bool {
553 let len = value.len();
554 if len == 0 || len > 63 {
555 return false;
556 }
557 let bytes = value.as_bytes();
558 if bytes[0] == b'-' || bytes[len - 1] == b'-' {
559 return false;
560 }
561 value
562 .chars()
563 .all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '-')
564}
565
566fn is_surface_token(value: &str) -> bool {
567 !value.is_empty()
568 && value
569 .chars()
570 .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
571}
572
573fn is_path_segment(value: &str) -> bool {
574 is_surface_token(value)
575}