1pub use crate::format_version::ReleaseTriplet as IrFormatVersion;
8use crate::ir::v4::serde_v4::with_fingerprint_semantics;
9use crate::ir::v4::{TypeEncoding, with_type_encoding};
10use crate::naming::{Name, PackageName, Path};
11use serde::Serialize;
12use sha2::{Digest, Sha256};
13use std::fmt;
14
15mod index;
16mod legacy_v3;
17pub use index::{IndexedNodeKind, NodeCatalog, NodeIndex, NodeResolutionError, ResolvedNode};
18pub use legacy_v3::{LegacyNodeIdError, convert_v3_node_id};
19
20#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
22pub enum NodeUriError {
23 #[error("an unpinned indexed node address requires a guard")]
25 MissingGuard,
26 #[error("invalid node URI: {0}")]
28 Invalid(String),
29 #[error("cannot fingerprint selected semantic child: {0}")]
31 InvalidFingerprint(String),
32}
33
34#[derive(Debug, Clone, PartialEq, Eq, Hash)]
36pub enum ArtifactSelector {
37 Package(PackageName),
39 Workspace(String),
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45pub enum NodeOwner {
46 OwnPackage,
47 Dependency(PackageName),
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Hash)]
52pub enum NodeRoot {
53 Distribution,
54 Package,
55 Dependency(PackageName),
56 EntryPoint(String),
57 Module {
58 owner: NodeOwner,
59 module: Path,
60 },
61 Type {
62 owner: NodeOwner,
63 module: Path,
64 name: Name,
65 },
66 Value {
67 owner: NodeOwner,
68 module: Path,
69 name: Name,
70 },
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Hash)]
75pub enum NodeStep {
76 TypeExpression,
77 Body,
78 ValueInputType(Name),
79 ValueInputAnnotation(Name),
80 ValueOutputType,
81 DerivedBaseType,
82 PartialTypeExpression,
83 CustomConstructor(Name),
84 ConstructorArgument(usize),
85 RecordField(Name),
86 ExtensibleRecordField(Name),
87 TypeFunctionParameter,
88 TypeFunctionResult,
89 ReferenceArgument(usize),
90 ApplyFunction,
91 ApplyArgument,
92 FieldSubject,
93 DestructurePattern,
94 DestructureValue,
95 DestructureBody,
96 IfCondition,
97 IfThen,
98 IfElse,
99 LambdaPattern,
100 LambdaBody,
101 LetDefinition(Name),
102 LetBody,
103 ListElement(usize),
104 PatternMatchSubject,
105 TupleElement(usize),
106 PatternMatchCasePattern(usize),
107 PatternMatchCaseBody(usize),
108 UpdateSubject,
109 UpdateField(Name),
110 AsPatternChild,
111 PatternTupleElement(usize),
112 PatternConstructorArgument(usize),
113 HeadTailHead,
114 HeadTailTail,
115 ExternalFallback,
116 IncompletePartialBody,
117 HoleExpectedType,
118 InferredType,
120 AnnotationEntry(usize),
122 AnnotationArgument(usize),
124}
125
126impl NodeStep {
127 pub fn is_positional(&self) -> bool {
129 matches!(
130 self,
131 Self::ConstructorArgument(_)
132 | Self::AnnotationEntry(_)
133 | Self::AnnotationArgument(_)
134 | Self::ReferenceArgument(_)
135 | Self::ListElement(_)
136 | Self::TupleElement(_)
137 | Self::PatternMatchCasePattern(_)
138 | Self::PatternMatchCaseBody(_)
139 | Self::PatternTupleElement(_)
140 | Self::PatternConstructorArgument(_)
141 )
142 }
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Hash)]
147pub enum ArtifactRevision {
148 Current,
149 Pinned(Sha256Digest),
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Hash)]
154pub struct Sha256Digest(String);
155
156impl Sha256Digest {
157 pub fn from_bytes(bytes: &[u8]) -> Self {
159 Self(
160 Sha256::digest(bytes)
161 .iter()
162 .map(|byte| format!("{byte:02x}"))
163 .collect(),
164 )
165 }
166
167 pub fn parse(value: &str) -> Result<Self, NodeUriError> {
168 let Some(hex) = value.strip_prefix("sha256:") else {
169 return Err(NodeUriError::Invalid("digest must use sha256".into()));
170 };
171 if hex.len() != 64
172 || !hex
173 .bytes()
174 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
175 {
176 return Err(NodeUriError::Invalid(
177 "digest must have 64 lowercase hex digits".into(),
178 ));
179 }
180 Ok(Self(hex.to_owned()))
181 }
182}
183
184impl fmt::Display for Sha256Digest {
185 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
186 write!(f, "sha256:{}", self.0)
187 }
188}
189
190#[derive(Clone, Copy)]
197enum FingerprintMode {
198 Legacy,
199 V4_1,
200}
201
202#[derive(Clone)]
203pub struct NodeFingerprintBuilder {
204 hash: Sha256,
205 mode: FingerprintMode,
206}
207
208impl Default for NodeFingerprintBuilder {
209 fn default() -> Self {
210 Self::new()
211 }
212}
213
214impl NodeFingerprintBuilder {
215 pub fn new() -> Self {
216 let mut hash = Sha256::new();
217 hash.update(b"morphir-node-fingerprint-draft.1\0");
218 Self {
219 hash,
220 mode: FingerprintMode::Legacy,
221 }
222 }
223
224 pub fn for_v4_1() -> Self {
227 Self {
228 mode: FingerprintMode::V4_1,
229 ..Self::new()
230 }
231 }
232
233 pub fn push<T: Serialize>(&mut self, step: &NodeStep, child: &T) -> Result<(), NodeUriError> {
235 let (role, index) = match step {
236 NodeStep::ConstructorArgument(index) => ("constructor/argument", *index),
237 NodeStep::AnnotationEntry(index) => ("annotation/entry", *index),
238 NodeStep::AnnotationArgument(index) => ("annotation/argument", *index),
239 NodeStep::ReferenceArgument(index) => ("reference/argument", *index),
240 NodeStep::ListElement(index) => ("list/element", *index),
241 NodeStep::TupleElement(index) => ("tuple/element", *index),
242 NodeStep::PatternMatchCasePattern(index) => ("pattern-match/case/pattern", *index),
243 NodeStep::PatternMatchCaseBody(index) => ("pattern-match/case/body", *index),
244 NodeStep::PatternTupleElement(index) => ("tuple-pattern/element", *index),
245 NodeStep::PatternConstructorArgument(index) => ("constructor-pattern/argument", *index),
246 _ => {
247 return Err(NodeUriError::InvalidFingerprint(
248 "step is not positional".into(),
249 ));
250 }
251 };
252 let mut value = match self.mode {
253 FingerprintMode::Legacy => semantic_json(child),
254 FingerprintMode::V4_1 => with_fingerprint_semantics(|| semantic_json(child)),
255 }
256 .map_err(|error| NodeUriError::InvalidFingerprint(error.to_string()))?;
257 if matches!(self.mode, FingerprintMode::Legacy) {
258 strip_nonsemantic_attributes(&mut value);
259 }
260 let mut canonical = Vec::new();
261 write_canonical_json(&value, &mut canonical)
262 .map_err(|error| NodeUriError::InvalidFingerprint(error.to_string()))?;
263 self.hash.update((role.len() as u32).to_be_bytes());
264 self.hash.update(role.as_bytes());
265 self.hash.update((index as u64).to_be_bytes());
266 self.hash.update((canonical.len() as u64).to_be_bytes());
267 self.hash.update(&canonical);
268 Ok(())
269 }
270
271 pub fn finish(self) -> Sha256Digest {
273 Sha256Digest(
274 self.hash
275 .finalize()
276 .iter()
277 .map(|byte| format!("{byte:02x}"))
278 .collect(),
279 )
280 }
281}
282
283pub(crate) fn semantic_json<T: Serialize>(
286 node: &T,
287) -> Result<serde_json::Value, serde_json::Error> {
288 with_type_encoding(TypeEncoding::Expanded, || serde_json::to_value(node))
289}
290
291fn strip_nonsemantic_attributes(value: &mut serde_json::Value) {
295 match value {
296 serde_json::Value::Array(items) => {
297 for item in items {
298 strip_nonsemantic_attributes(item);
299 }
300 }
301 serde_json::Value::Object(members) => {
302 if members.contains_key("DocumentLiteral") {
305 return;
306 }
307 for member in members.values_mut() {
308 strip_nonsemantic_attributes(member);
309 }
310 if let Some(serde_json::Value::Object(attributes)) = members.get_mut("attributes") {
311 attributes.remove("source");
312 attributes.remove("extensions");
313 if attributes.is_empty() {
314 members.remove("attributes");
315 }
316 }
317 }
318 _ => {}
319 }
320}
321
322fn write_canonical_json(
323 value: &serde_json::Value,
324 output: &mut Vec<u8>,
325) -> Result<(), serde_json::Error> {
326 use serde_json::Value;
327 match value {
328 Value::Null => output.extend_from_slice(b"null"),
329 Value::Bool(flag) => output.extend_from_slice(if *flag { b"true" } else { b"false" }),
330 Value::Number(number) => output.extend_from_slice(number.to_string().as_bytes()),
331 Value::String(text) => output.extend_from_slice(serde_json::to_string(text)?.as_bytes()),
332 Value::Array(items) => {
333 output.push(b'[');
334 for (index, item) in items.iter().enumerate() {
335 if index > 0 {
336 output.push(b',');
337 }
338 write_canonical_json(item, output)?;
339 }
340 output.push(b']');
341 }
342 Value::Object(members) => {
343 output.push(b'{');
344 let mut keys = members.keys().collect::<Vec<_>>();
345 keys.sort_unstable_by(|left, right| left.as_bytes().cmp(right.as_bytes()));
346 for (index, key) in keys.into_iter().enumerate() {
347 if index > 0 {
348 output.push(b',');
349 }
350 output.extend_from_slice(serde_json::to_string(key)?.as_bytes());
351 output.push(b':');
352 write_canonical_json(&members[key], output)?;
353 }
354 output.push(b'}');
355 }
356 }
357 Ok(())
358}
359
360#[derive(Debug, Clone, PartialEq, Eq)]
362pub struct NodeUri {
363 artifact: ArtifactSelector,
364 format: IrFormatVersion,
365 root: NodeRoot,
366 steps: Vec<NodeStep>,
367 revision: ArtifactRevision,
368 guard: Option<Sha256Digest>,
369}
370
371impl NodeUri {
372 pub fn new(
375 artifact: ArtifactSelector,
376 format: IrFormatVersion,
377 root: NodeRoot,
378 steps: Vec<NodeStep>,
379 revision: ArtifactRevision,
380 guard: Option<Sha256Digest>,
381 ) -> Result<Self, NodeUriError> {
382 let candidate = Self {
383 artifact,
384 format,
385 root,
386 steps,
387 revision,
388 guard,
389 };
390 let checked = Self::parse(&candidate.to_string())?;
391 if checked != candidate {
392 return Err(invalid("address is not canonical"));
393 }
394 Ok(candidate)
395 }
396
397 pub fn artifact(&self) -> &ArtifactSelector {
398 &self.artifact
399 }
400 pub fn format(&self) -> IrFormatVersion {
401 self.format
402 }
403 pub fn root(&self) -> &NodeRoot {
404 &self.root
405 }
406 pub fn steps(&self) -> &[NodeStep] {
407 &self.steps
408 }
409 pub fn revision(&self) -> &ArtifactRevision {
410 &self.revision
411 }
412 pub fn guard(&self) -> Option<&Sha256Digest> {
413 self.guard.as_ref()
414 }
415
416 pub fn parse(uri: &str) -> Result<Self, NodeUriError> {
418 let body = uri
419 .strip_prefix("morphir://ir/")
420 .ok_or_else(|| invalid("wrong URI authority"))?;
421 let (before_fragment, fragment) = body
422 .split_once('#')
423 .ok_or_else(|| invalid("missing node fragment"))?;
424 if fragment.contains('#') {
425 return Err(invalid("duplicate fragment"));
426 }
427 let (selector, query) = before_fragment
428 .split_once('?')
429 .ok_or_else(|| invalid("missing format query"))?;
430 let artifact = if let Some(raw) = selector.strip_prefix("pkg/") {
431 if raw.is_empty() {
432 return Err(invalid("empty package selector"));
433 }
434 ArtifactSelector::Package(parse_package(raw)?)
435 } else if let Some(alias) = selector.strip_prefix("workspace/") {
436 if !valid_workspace_alias(alias) {
437 return Err(invalid("invalid workspace alias"));
438 }
439 ArtifactSelector::Workspace(alias.to_owned())
440 } else {
441 return Err(invalid("unknown artifact selector"));
442 };
443
444 let mut format = None;
445 let mut revision = ArtifactRevision::Current;
446 let mut guard = None;
447 for parameter in query.split('&') {
448 let (key, value) = parameter
449 .split_once('=')
450 .ok_or_else(|| invalid("malformed query parameter"))?;
451 match key {
452 "format" if format.is_none() => format = Some(parse_format(value)?),
453 "rev" if matches!(revision, ArtifactRevision::Current) => {
454 revision = ArtifactRevision::Pinned(Sha256Digest::parse(value)?)
455 }
456 "guard" if guard.is_none() => guard = Some(Sha256Digest::parse(value)?),
457 _ => return Err(invalid("duplicate or unknown query parameter")),
458 }
459 }
460 let format = format.ok_or_else(|| invalid("missing format"))?;
461 let parts = fragment
462 .strip_prefix('/')
463 .ok_or_else(|| invalid("node fragment must start with /"))?
464 .split('/')
465 .collect::<Vec<_>>();
466 let (root, consumed) = parse_root(&parts)?;
467 let steps = parse_steps(&parts[consumed..])?;
468 if matches!(revision, ArtifactRevision::Current)
469 && guard.is_none()
470 && steps.iter().any(NodeStep::is_positional)
471 {
472 return Err(NodeUriError::MissingGuard);
473 }
474 let address = Self {
475 artifact,
476 format,
477 root,
478 steps,
479 revision,
480 guard,
481 };
482 if address.to_string() != uri {
483 return Err(invalid("noncanonical URI spelling"));
484 }
485 Ok(address)
486 }
487}
488
489impl fmt::Display for NodeUri {
490 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
491 write!(f, "morphir://ir/")?;
492 match &self.artifact {
493 ArtifactSelector::Package(package) => {
494 write!(f, "pkg/{}", package.to_canonical_string())?
495 }
496 ArtifactSelector::Workspace(alias) => write!(f, "workspace/{alias}")?,
497 }
498 write!(f, "?format={}", self.format)?;
499 if let ArtifactRevision::Pinned(digest) = &self.revision {
500 write!(f, "&rev={digest}")?;
501 }
502 if let Some(digest) = &self.guard {
503 write!(f, "&guard={digest}")?;
504 }
505 write!(f, "#")?;
506 let mut root = Vec::new();
507 match &self.root {
508 NodeRoot::Distribution => root.push("distribution".to_owned()),
509 NodeRoot::Package => root.push("package".to_owned()),
510 NodeRoot::Dependency(package) => {
511 root.extend([
512 "dependency".to_owned(),
513 encode_component(&package.to_canonical_string()),
514 ]);
515 }
516 NodeRoot::EntryPoint(name) => {
517 root.extend(["entry-point".to_owned(), encode_component(name)])
518 }
519 NodeRoot::Module { owner, module } => {
520 push_owner(&mut root, owner);
521 root.extend([
522 "module".to_owned(),
523 encode_component(&module.to_canonical_string()),
524 ]);
525 }
526 NodeRoot::Type {
527 owner,
528 module,
529 name,
530 }
531 | NodeRoot::Value {
532 owner,
533 module,
534 name,
535 } => {
536 push_owner(&mut root, owner);
537 root.extend([
538 "module".to_owned(),
539 encode_component(&module.to_canonical_string()),
540 if matches!(self.root, NodeRoot::Type { .. }) {
541 "type"
542 } else {
543 "value"
544 }
545 .to_owned(),
546 encode_component(&name.to_canonical_string()),
547 ]);
548 }
549 }
550 for step in &self.steps {
551 match step {
552 NodeStep::TypeExpression => root.push("type-exp".to_owned()),
553 NodeStep::Body => root.push("body".to_owned()),
554 NodeStep::ValueInputType(name) => root.extend([
555 "input".to_owned(),
556 encode_component(&name.to_canonical_string()),
557 ]),
558 NodeStep::ValueInputAnnotation(name) => root.extend([
559 "input-annotation".to_owned(),
560 encode_component(&name.to_canonical_string()),
561 ]),
562 NodeStep::ValueOutputType => root.push("output".to_owned()),
563 NodeStep::DerivedBaseType => {
564 root.extend(["derived".to_owned(), "base-type".to_owned()])
565 }
566 NodeStep::PartialTypeExpression => {
567 root.extend(["incomplete".to_owned(), "partial-type".to_owned()])
568 }
569 NodeStep::CustomConstructor(name) => root.extend([
570 "constructor".to_owned(),
571 encode_component(&name.to_canonical_string()),
572 ]),
573 NodeStep::ConstructorArgument(index) => {
574 root.extend(["argument".to_owned(), index.to_string()])
575 }
576 NodeStep::RecordField(name) => root.extend([
577 "record".to_owned(),
578 "field".to_owned(),
579 encode_component(&name.to_canonical_string()),
580 ]),
581 NodeStep::ExtensibleRecordField(name) => root.extend([
582 "extensible-record".to_owned(),
583 "field".to_owned(),
584 encode_component(&name.to_canonical_string()),
585 ]),
586 NodeStep::TypeFunctionParameter => {
587 root.extend(["function".to_owned(), "parameter".to_owned()])
588 }
589 NodeStep::TypeFunctionResult => {
590 root.extend(["function".to_owned(), "result".to_owned()])
591 }
592 NodeStep::ReferenceArgument(index) => root.extend([
593 "reference".to_owned(),
594 "argument".to_owned(),
595 index.to_string(),
596 ]),
597 NodeStep::ApplyFunction => root.extend(["apply".to_owned(), "function".to_owned()]),
598 NodeStep::ApplyArgument => root.extend(["apply".to_owned(), "argument".to_owned()]),
599 NodeStep::FieldSubject => root.extend(["field".to_owned(), "subject".to_owned()]),
600 NodeStep::DestructurePattern => {
601 root.extend(["destructure".to_owned(), "pattern".to_owned()])
602 }
603 NodeStep::DestructureValue => {
604 root.extend(["destructure".to_owned(), "value".to_owned()])
605 }
606 NodeStep::DestructureBody => {
607 root.extend(["destructure".to_owned(), "body".to_owned()])
608 }
609 NodeStep::IfCondition => root.extend(["if".to_owned(), "condition".to_owned()]),
610 NodeStep::IfThen => root.extend(["if".to_owned(), "then".to_owned()]),
611 NodeStep::IfElse => root.extend(["if".to_owned(), "else".to_owned()]),
612 NodeStep::LambdaPattern => root.extend(["lambda".to_owned(), "pattern".to_owned()]),
613 NodeStep::LambdaBody => root.extend(["lambda".to_owned(), "body".to_owned()]),
614 NodeStep::LetDefinition(name) => root.extend([
615 "let".to_owned(),
616 "definition".to_owned(),
617 encode_component(&name.to_canonical_string()),
618 ]),
619 NodeStep::LetBody => root.extend(["let".to_owned(), "body".to_owned()]),
620 NodeStep::ListElement(index) => {
621 root.extend(["list".to_owned(), "element".to_owned(), index.to_string()])
622 }
623 NodeStep::PatternMatchSubject => {
624 root.extend(["pattern-match".to_owned(), "subject".to_owned()])
625 }
626 NodeStep::TupleElement(index) => {
627 root.extend(["tuple".to_owned(), "element".to_owned(), index.to_string()])
628 }
629 NodeStep::PatternMatchCasePattern(index) => root.extend([
630 "pattern-match".to_owned(),
631 "case".to_owned(),
632 index.to_string(),
633 "pattern".to_owned(),
634 ]),
635 NodeStep::PatternMatchCaseBody(index) => root.extend([
636 "pattern-match".to_owned(),
637 "case".to_owned(),
638 index.to_string(),
639 "body".to_owned(),
640 ]),
641 NodeStep::UpdateSubject => root.extend(["update".to_owned(), "subject".to_owned()]),
642 NodeStep::UpdateField(name) => root.extend([
643 "update".to_owned(),
644 "field".to_owned(),
645 encode_component(&name.to_canonical_string()),
646 ]),
647 NodeStep::AsPatternChild => {
648 root.extend(["as-pattern".to_owned(), "pattern".to_owned()])
649 }
650 NodeStep::PatternTupleElement(index) => root.extend([
651 "tuple-pattern".to_owned(),
652 "element".to_owned(),
653 index.to_string(),
654 ]),
655 NodeStep::PatternConstructorArgument(index) => root.extend([
656 "constructor-pattern".to_owned(),
657 "argument".to_owned(),
658 index.to_string(),
659 ]),
660 NodeStep::HeadTailHead => root.extend(["head-tail".to_owned(), "head".to_owned()]),
661 NodeStep::HeadTailTail => root.extend(["head-tail".to_owned(), "tail".to_owned()]),
662 NodeStep::ExternalFallback => {
663 root.extend(["external".to_owned(), "fallback".to_owned()])
664 }
665 NodeStep::IncompletePartialBody => {
666 root.extend(["incomplete".to_owned(), "partial-body".to_owned()])
667 }
668 NodeStep::HoleExpectedType => {
669 root.extend(["hole".to_owned(), "expected-type".to_owned()])
670 }
671 NodeStep::InferredType => root.push("inferred-type".to_owned()),
672 NodeStep::AnnotationEntry(index) => root.extend([
673 "annotation".to_owned(),
674 "entry".to_owned(),
675 index.to_string(),
676 ]),
677 NodeStep::AnnotationArgument(index) => {
678 root.extend(["argument".to_owned(), index.to_string()])
679 }
680 }
681 }
682 write!(f, "/{}", root.join("/"))
683 }
684}
685
686fn invalid(message: &str) -> NodeUriError {
687 NodeUriError::Invalid(message.to_owned())
688}
689
690fn parse_format(value: &str) -> Result<IrFormatVersion, NodeUriError> {
691 let parts = value.split('.').collect::<Vec<_>>();
692 if parts.len() != 3
693 || parts
694 .iter()
695 .any(|part| part.is_empty() || (part.len() > 1 && part.starts_with('0')))
696 {
697 return Err(invalid(
698 "format must have three canonical numeric components",
699 ));
700 }
701 let parse = |part: &str| {
702 part.parse::<u32>()
703 .map_err(|_| invalid("invalid format component"))
704 };
705 Ok(IrFormatVersion::new(
706 parse(parts[0])?,
707 parse(parts[1])?,
708 parse(parts[2])?,
709 ))
710}
711
712fn parse_package(value: &str) -> Result<PackageName, NodeUriError> {
713 let package =
714 PackageName::from_canonical_string(value).map_err(|_| invalid("invalid package name"))?;
715 if package.is_empty() || package.to_canonical_string() != value {
716 return Err(invalid("noncanonical package name"));
717 }
718 Ok(package)
719}
720
721fn parse_path(value: &str) -> Result<Path, NodeUriError> {
722 let decoded = decode_component(value)?;
723 let path = Path::from_canonical_string(&decoded).map_err(|_| invalid("invalid module path"))?;
724 if path.is_empty() || path.to_canonical_string() != decoded {
725 return Err(invalid("noncanonical module path"));
726 }
727 Ok(path)
728}
729
730fn parse_name(value: &str) -> Result<Name, NodeUriError> {
731 let decoded = decode_component(value)?;
732 let name = Name::from_canonical_string(&decoded).map_err(|_| invalid("invalid name"))?;
733 if name.to_canonical_string() != decoded {
734 return Err(invalid("noncanonical name"));
735 }
736 Ok(name)
737}
738
739fn parse_root(parts: &[&str]) -> Result<(NodeRoot, usize), NodeUriError> {
740 match parts {
741 ["distribution", ..] => Ok((NodeRoot::Distribution, 1)),
742 ["package", ..] => Ok((NodeRoot::Package, 1)),
743 ["entry-point", raw, ..] if !raw.is_empty() => {
744 Ok((NodeRoot::EntryPoint(decode_component(raw)?), 2))
745 }
746 _ => {
747 let (owner, rest, prefix) = match parts {
748 ["dependency", raw, rest @ ..] => (
749 NodeOwner::Dependency(parse_package(&decode_component(raw)?)?),
750 rest,
751 2,
752 ),
753 other => (NodeOwner::OwnPackage, other, 0),
754 };
755 if rest.is_empty()
756 && prefix == 2
757 && let NodeOwner::Dependency(package) = owner
758 {
759 return Ok((NodeRoot::Dependency(package), 2));
760 }
761 match rest {
762 ["module", module, "type", name, ..] => Ok((
763 NodeRoot::Type {
764 owner,
765 module: parse_path(module)?,
766 name: parse_name(name)?,
767 },
768 prefix + 4,
769 )),
770 ["module", module, "value", name, ..] => Ok((
771 NodeRoot::Value {
772 owner,
773 module: parse_path(module)?,
774 name: parse_name(name)?,
775 },
776 prefix + 4,
777 )),
778 ["module", module, ..] => Ok((
779 NodeRoot::Module {
780 owner,
781 module: parse_path(module)?,
782 },
783 prefix + 2,
784 )),
785 _ => Err(invalid("invalid node root")),
786 }
787 }
788 }
789}
790
791fn parse_steps(parts: &[&str]) -> Result<Vec<NodeStep>, NodeUriError> {
792 let mut steps = Vec::new();
793 let mut cursor = parts;
794 while !cursor.is_empty() {
795 let (step, count) = match cursor {
796 ["type-exp", ..] => (NodeStep::TypeExpression, 1),
797 ["body", ..] => (NodeStep::Body, 1),
798 ["input", name, ..] => (NodeStep::ValueInputType(parse_name(name)?), 2),
799 ["input-annotation", name, ..] => {
800 (NodeStep::ValueInputAnnotation(parse_name(name)?), 2)
801 }
802 ["output", ..] => (NodeStep::ValueOutputType, 1),
803 ["derived", "base-type", ..] => (NodeStep::DerivedBaseType, 2),
804 ["incomplete", "partial-type", ..] => (NodeStep::PartialTypeExpression, 2),
805 ["constructor", name, ..] => (NodeStep::CustomConstructor(parse_name(name)?), 2),
806 ["argument", index, ..] => (
807 if matches!(steps.last(), Some(NodeStep::AnnotationEntry(_))) {
808 NodeStep::AnnotationArgument(parse_index(index)?)
809 } else {
810 NodeStep::ConstructorArgument(parse_index(index)?)
811 },
812 2,
813 ),
814 ["record", "field", name, ..] => (NodeStep::RecordField(parse_name(name)?), 3),
815 ["extensible-record", "field", name, ..] => {
816 (NodeStep::ExtensibleRecordField(parse_name(name)?), 3)
817 }
818 ["function", "parameter", ..] => (NodeStep::TypeFunctionParameter, 2),
819 ["function", "result", ..] => (NodeStep::TypeFunctionResult, 2),
820 ["reference", "argument", index, ..] => {
821 (NodeStep::ReferenceArgument(parse_index(index)?), 3)
822 }
823 ["apply", "function", ..] => (NodeStep::ApplyFunction, 2),
824 ["apply", "argument", ..] => (NodeStep::ApplyArgument, 2),
825 ["field", "subject", ..] => (NodeStep::FieldSubject, 2),
826 ["destructure", "pattern", ..] => (NodeStep::DestructurePattern, 2),
827 ["destructure", "value", ..] => (NodeStep::DestructureValue, 2),
828 ["destructure", "body", ..] => (NodeStep::DestructureBody, 2),
829 ["if", "condition", ..] => (NodeStep::IfCondition, 2),
830 ["if", "then", ..] => (NodeStep::IfThen, 2),
831 ["if", "else", ..] => (NodeStep::IfElse, 2),
832 ["lambda", "pattern", ..] => (NodeStep::LambdaPattern, 2),
833 ["lambda", "body", ..] => (NodeStep::LambdaBody, 2),
834 ["let", "definition", name, ..] => (NodeStep::LetDefinition(parse_name(name)?), 3),
835 ["let", "body", ..] => (NodeStep::LetBody, 2),
836 ["list", "element", index, ..] => (NodeStep::ListElement(parse_index(index)?), 3),
837 ["pattern-match", "subject", ..] => (NodeStep::PatternMatchSubject, 2),
838 ["tuple", "element", index, ..] => (NodeStep::TupleElement(parse_index(index)?), 3),
839 ["pattern-match", "case", index, "pattern", ..] => {
840 (NodeStep::PatternMatchCasePattern(parse_index(index)?), 4)
841 }
842 ["pattern-match", "case", index, "body", ..] => {
843 (NodeStep::PatternMatchCaseBody(parse_index(index)?), 4)
844 }
845 ["update", "subject", ..] => (NodeStep::UpdateSubject, 2),
846 ["update", "field", name, ..] => (NodeStep::UpdateField(parse_name(name)?), 3),
847 ["as-pattern", "pattern", ..] => (NodeStep::AsPatternChild, 2),
848 ["tuple-pattern", "element", index, ..] => {
849 (NodeStep::PatternTupleElement(parse_index(index)?), 3)
850 }
851 ["constructor-pattern", "argument", index, ..] => {
852 (NodeStep::PatternConstructorArgument(parse_index(index)?), 3)
853 }
854 ["head-tail", "head", ..] => (NodeStep::HeadTailHead, 2),
855 ["head-tail", "tail", ..] => (NodeStep::HeadTailTail, 2),
856 ["external", "fallback", ..] => (NodeStep::ExternalFallback, 2),
857 ["incomplete", "partial-body", ..] => (NodeStep::IncompletePartialBody, 2),
858 ["hole", "expected-type", ..] => (NodeStep::HoleExpectedType, 2),
859 ["inferred-type", ..] => (NodeStep::InferredType, 1),
860 ["annotation", "entry", index, ..] => {
861 (NodeStep::AnnotationEntry(parse_index(index)?), 3)
862 }
863 _ => return Err(invalid("unknown semantic child role")),
864 };
865 steps.push(step);
866 cursor = &cursor[count..];
867 }
868 Ok(steps)
869}
870
871fn parse_index(value: &str) -> Result<usize, NodeUriError> {
872 if value.is_empty()
873 || (value.len() > 1 && value.starts_with('0'))
874 || !value.bytes().all(|byte| byte.is_ascii_digit())
875 {
876 return Err(invalid("invalid positional index"));
877 }
878 value
879 .parse()
880 .map_err(|_| invalid("positional index overflow"))
881}
882
883fn valid_workspace_alias(value: &str) -> bool {
884 !value.is_empty()
885 && value.split('-').all(|part| {
886 !part.is_empty()
887 && part
888 .bytes()
889 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit())
890 })
891 && value.as_bytes()[0].is_ascii_lowercase()
892}
893
894fn push_owner(parts: &mut Vec<String>, owner: &NodeOwner) {
895 if let NodeOwner::Dependency(package) = owner {
896 parts.extend([
897 "dependency".to_owned(),
898 encode_component(&package.to_canonical_string()),
899 ]);
900 }
901}
902
903fn encode_component(value: &str) -> String {
904 let mut result = String::new();
905 for byte in value.bytes() {
906 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
907 result.push(byte as char);
908 } else {
909 result.push_str(&format!("%{byte:02X}"));
910 }
911 }
912 result
913}
914
915fn decode_component(value: &str) -> Result<String, NodeUriError> {
916 let mut bytes = Vec::new();
917 let mut input = value.bytes();
918 while let Some(byte) = input.next() {
919 if byte == b'%' {
920 let high = input
921 .next()
922 .ok_or_else(|| invalid("truncated percent escape"))?;
923 let low = input
924 .next()
925 .ok_or_else(|| invalid("truncated percent escape"))?;
926 let hex = |digit: u8| match digit {
927 b'0'..=b'9' => Some(digit - b'0'),
928 b'A'..=b'F' => Some(digit - b'A' + 10),
929 _ => None,
930 };
931 bytes.push(
932 hex(high)
933 .zip(hex(low))
934 .map(|(high, low)| high * 16 + low)
935 .ok_or_else(|| invalid("invalid percent escape"))?,
936 );
937 } else {
938 if !byte.is_ascii() {
939 return Err(invalid("raw non-ASCII URI component"));
940 }
941 bytes.push(byte);
942 }
943 }
944 String::from_utf8(bytes).map_err(|_| invalid("invalid UTF-8 escape"))
945}