Skip to main content

this_me/
me_uri.rs

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        // The matched space and handle were normalized above; this path is unreachable.
349        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}