Skip to main content

fastmcp_protocol/
uri_template.rs

1//! Bounded RFC 6570 Level 4 URI-template parsing and expansion.
2//!
3//! A URI template is deliberately distinct from [`crate::AbsoluteUri`]: a
4//! template describes a set of references and must be expanded before it can
5//! be used where an ordinary URI is required. This module owns the
6//! syntax-preserving representation, forward expansion, and the separate
7//! stricter reverse-routing compiler because many valid RFC 6570 templates
8//! are not invertible.
9
10use std::collections::{BTreeMap, BTreeSet};
11use std::fmt;
12use std::str::FromStr;
13
14/// Maximum UTF-8 bytes accepted in a single URI template source string.
15pub const MAX_URI_TEMPLATE_BYTES: usize = 16 * 1024;
16/// Maximum literal/expression parts retained in one parsed template.
17pub const MAX_URI_TEMPLATE_PARTS: usize = 256;
18/// Maximum expressions retained in one parsed template.
19pub const MAX_URI_TEMPLATE_EXPRESSIONS: usize = 128;
20/// Maximum variable specifications allowed in one expression.
21pub const MAX_URI_TEMPLATE_VARIABLES_PER_EXPRESSION: usize = 64;
22/// Maximum UTF-8 bytes in one RFC 6570 variable name.
23pub const MAX_URI_TEMPLATE_VARIABLE_NAME_BYTES: usize = 512;
24/// Maximum RFC 6570 prefix modifier, which is strictly less than 10000.
25pub const MAX_URI_TEMPLATE_PREFIX_LENGTH: usize = 9_999;
26/// Maximum individual UTF-8 value/key bytes admitted by the bounded expander.
27pub const MAX_URI_TEMPLATE_VALUE_BYTES: usize = 16 * 1024;
28/// Maximum composite members inspected by one expansion.
29pub const MAX_URI_TEMPLATE_COMPOSITE_ITEMS: usize = 1_024;
30/// Maximum UTF-8 output bytes emitted by one expansion.
31pub const MAX_URI_TEMPLATE_EXPANSION_OUTPUT_BYTES: usize = 64 * 1024;
32/// Maximum UTF-8 bytes accepted by a reverse match candidate.
33///
34/// A successful reverse match must re-expand under the ordinary output bound,
35/// so accepting a longer candidate could never produce a valid result.
36pub const MAX_URI_TEMPLATE_MATCH_INPUT_BYTES: usize = MAX_URI_TEMPLATE_EXPANSION_OUTPUT_BYTES;
37
38/// Values supplied to a [`UriTemplate`] expansion.
39pub type TemplateValues = BTreeMap<String, TemplateValue>;
40
41/// One defined RFC 6570 variable value.
42///
43/// An absent map entry denotes RFC 6570's undefined value. Empty list and map
44/// values are likewise treated as undefined, while an empty scalar is defined.
45/// Associative values retain their pair order so expansions reproduce the
46/// order supplied by an RFC 6570 data model rather than silently sorting keys.
47#[derive(Clone, Debug, Eq, PartialEq)]
48pub enum TemplateValue {
49    /// A single scalar string value.
50    Scalar(String),
51    /// An ordered composite list value.
52    List(Vec<Option<String>>),
53    /// A composite associative value.
54    Associative(Vec<(String, Option<String>)>),
55}
56
57impl TemplateValue {
58    /// Constructs a scalar value.
59    #[must_use]
60    pub fn scalar(value: impl Into<String>) -> Self {
61        Self::Scalar(value.into())
62    }
63
64    /// Constructs a list value.
65    #[must_use]
66    pub fn list(values: Vec<String>) -> Self {
67        Self::List(values.into_iter().map(Some).collect())
68    }
69
70    /// Constructs a list that can retain undefined members.
71    #[must_use]
72    pub fn list_with_undefined(values: Vec<Option<String>>) -> Self {
73        Self::List(values)
74    }
75
76    /// Constructs an associative value.
77    #[must_use]
78    pub fn associative(values: Vec<(String, String)>) -> Self {
79        Self::Associative(
80            values
81                .into_iter()
82                .map(|(key, value)| (key, Some(value)))
83                .collect(),
84        )
85    }
86
87    /// Constructs an associative value that can retain undefined members.
88    #[must_use]
89    pub fn associative_with_undefined(values: Vec<(String, Option<String>)>) -> Self {
90        Self::Associative(values)
91    }
92}
93
94impl From<String> for TemplateValue {
95    fn from(value: String) -> Self {
96        Self::Scalar(value)
97    }
98}
99
100impl From<&str> for TemplateValue {
101    fn from(value: &str) -> Self {
102        Self::Scalar(value.to_owned())
103    }
104}
105
106/// Hard-bounded limits used by [`UriTemplate::expand_with_limits`].
107///
108/// Callers can tighten the defaults for a particular boundary but cannot
109/// configure away the crate-level safety caps.
110#[derive(Clone, Copy, Debug, Eq, PartialEq)]
111pub struct UriTemplateExpansionLimits {
112    output_bytes: usize,
113    composite_items: usize,
114    value_bytes: usize,
115}
116
117impl UriTemplateExpansionLimits {
118    /// Creates a bounded expansion configuration.
119    pub fn new(
120        max_output_bytes: usize,
121        max_composite_items: usize,
122        max_value_bytes: usize,
123    ) -> Result<Self, UriTemplateError> {
124        if max_output_bytes == 0 || max_output_bytes > MAX_URI_TEMPLATE_EXPANSION_OUTPUT_BYTES {
125            return Err(UriTemplateError::InvalidExpansionLimit {
126                field: "max_output_bytes",
127                actual: max_output_bytes,
128                maximum: MAX_URI_TEMPLATE_EXPANSION_OUTPUT_BYTES,
129            });
130        }
131        if max_composite_items == 0 || max_composite_items > MAX_URI_TEMPLATE_COMPOSITE_ITEMS {
132            return Err(UriTemplateError::InvalidExpansionLimit {
133                field: "max_composite_items",
134                actual: max_composite_items,
135                maximum: MAX_URI_TEMPLATE_COMPOSITE_ITEMS,
136            });
137        }
138        if max_value_bytes == 0 || max_value_bytes > MAX_URI_TEMPLATE_VALUE_BYTES {
139            return Err(UriTemplateError::InvalidExpansionLimit {
140                field: "max_value_bytes",
141                actual: max_value_bytes,
142                maximum: MAX_URI_TEMPLATE_VALUE_BYTES,
143            });
144        }
145        Ok(Self {
146            output_bytes: max_output_bytes,
147            composite_items: max_composite_items,
148            value_bytes: max_value_bytes,
149        })
150    }
151
152    /// Returns the maximum output bytes permitted by this configuration.
153    #[must_use]
154    pub const fn max_output_bytes(self) -> usize {
155        self.output_bytes
156    }
157
158    /// Returns the maximum composite members inspected by this configuration.
159    #[must_use]
160    pub const fn max_composite_items(self) -> usize {
161        self.composite_items
162    }
163
164    /// Returns the maximum bytes permitted in one key or value.
165    #[must_use]
166    pub const fn max_value_bytes(self) -> usize {
167        self.value_bytes
168    }
169}
170
171impl Default for UriTemplateExpansionLimits {
172    fn default() -> Self {
173        Self {
174            output_bytes: MAX_URI_TEMPLATE_EXPANSION_OUTPUT_BYTES,
175            composite_items: MAX_URI_TEMPLATE_COMPOSITE_ITEMS,
176            value_bytes: MAX_URI_TEMPLATE_VALUE_BYTES,
177        }
178    }
179}
180
181/// An immutable, source-preserving RFC 6570 Level 4 template AST.
182#[derive(Clone, Debug, Eq, PartialEq)]
183pub struct UriTemplate {
184    source: String,
185    parts: Vec<UriTemplatePart>,
186}
187
188impl UriTemplate {
189    /// Parses a complete RFC 6570 Level 4 URI template.
190    pub fn parse(source: impl AsRef<str>) -> Result<Self, UriTemplateError> {
191        let source = source.as_ref();
192        if source.len() > MAX_URI_TEMPLATE_BYTES {
193            return Err(UriTemplateError::SourceTooLong {
194                actual: source.len(),
195                maximum: MAX_URI_TEMPLATE_BYTES,
196            });
197        }
198
199        let bytes = source.as_bytes();
200        let mut index = 0;
201        let mut literal_start = 0;
202        let mut expressions = 0;
203        let mut parts = Vec::new();
204
205        while index < bytes.len() {
206            match bytes[index] {
207                b'{' => {
208                    push_literal(&mut parts, &source[literal_start..index], literal_start)?;
209                    let expression_start = index + 1;
210                    let Some(relative_end) = bytes[expression_start..]
211                        .iter()
212                        .position(|byte| *byte == b'}')
213                    else {
214                        return Err(UriTemplateError::UnclosedExpression { offset: index });
215                    };
216                    let expression_end = expression_start + relative_end;
217                    if let Some(relative_nested) = bytes[expression_start..expression_end]
218                        .iter()
219                        .position(|byte| *byte == b'{')
220                    {
221                        return Err(UriTemplateError::NestedExpression {
222                            offset: expression_start + relative_nested,
223                        });
224                    }
225                    if expressions == MAX_URI_TEMPLATE_EXPRESSIONS {
226                        return Err(UriTemplateError::TooManyExpressions {
227                            maximum: MAX_URI_TEMPLATE_EXPRESSIONS,
228                        });
229                    }
230                    push_part(
231                        &mut parts,
232                        UriTemplatePart::Expression(parse_expression(
233                            &source[expression_start..expression_end],
234                            expression_start,
235                        )?),
236                    )?;
237                    expressions += 1;
238                    index = expression_end + 1;
239                    literal_start = index;
240                }
241                b'}' => return Err(UriTemplateError::UnexpectedCloseBrace { offset: index }),
242                _ => index += 1,
243            }
244        }
245        push_literal(&mut parts, &source[literal_start..], literal_start)?;
246
247        Ok(Self {
248            source: source.to_owned(),
249            parts,
250        })
251    }
252
253    /// Returns the exact source text used to construct this template.
254    #[must_use]
255    pub fn source(&self) -> &str {
256        &self.source
257    }
258
259    /// Returns the source-preserving AST parts in expansion order.
260    #[must_use]
261    pub fn parts(&self) -> &[UriTemplatePart] {
262        &self.parts
263    }
264
265    /// Expands the template using the default hard-bounded limits.
266    pub fn expand(&self, values: &TemplateValues) -> Result<String, UriTemplateError> {
267        self.expand_with_limits(values, UriTemplateExpansionLimits::default())
268    }
269
270    /// Expands the template using caller-tightened, hard-bounded limits.
271    pub fn expand_with_limits(
272        &self,
273        values: &TemplateValues,
274        limits: UriTemplateExpansionLimits,
275    ) -> Result<String, UriTemplateError> {
276        let mut output = String::new();
277        let mut state = ExpansionState::default();
278        for part in &self.parts {
279            match part {
280                UriTemplatePart::Literal(literal) => {
281                    append_encoded_literal(&mut output, literal, limits)?;
282                }
283                UriTemplatePart::Expression(expression) => {
284                    expand_expression(&mut output, expression, values, limits, &mut state)?;
285                }
286            }
287        }
288        Ok(output)
289    }
290
291    /// Compiles this template for deterministic, byte-exact reverse matching.
292    ///
293    /// RFC 6570 permits expansions that do not have a unique inverse. This
294    /// operation deliberately admits only the scalar dispatch subset whose
295    /// captures can be reconstructed without normalization or guessing.
296    pub fn compile_reversible(&self) -> Result<ReversibleResourceTemplate, UriTemplateError> {
297        ReversibleResourceTemplate::from_template(self)
298    }
299}
300
301impl AsRef<str> for UriTemplate {
302    fn as_ref(&self) -> &str {
303        self.source()
304    }
305}
306
307impl fmt::Display for UriTemplate {
308    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
309        formatter.write_str(self.source())
310    }
311}
312
313impl FromStr for UriTemplate {
314    type Err = UriTemplateError;
315
316    fn from_str(source: &str) -> Result<Self, Self::Err> {
317        Self::parse(source)
318    }
319}
320
321/// A compiled, deterministic RFC 6570 template suitable for local dispatch.
322///
323/// This is intentionally stricter than [`UriTemplate`]. It accepts scalar
324/// captures only, requires unambiguous expression boundaries, and decodes
325/// captured percent triplets once. Named (`;`, `?`, and `&`) expressions may
326/// contain multiple scalar variables because their wire names make omission
327/// and ordering deterministic. List and associative inputs are rejected
328/// before expansion because their compact and exploded forms are not generally
329/// invertible. An explode modifier on a scalar is admitted: RFC 6570 gives it
330/// the same wire form as the unmodified scalar.
331#[derive(Clone, Debug, Eq, PartialEq)]
332pub struct ReversibleResourceTemplate {
333    template: UriTemplate,
334    parts: Vec<ReversibleTemplatePart>,
335}
336
337impl ReversibleResourceTemplate {
338    /// Compiles an owned template for deterministic reverse matching.
339    pub fn compile(template: UriTemplate) -> Result<Self, UriTemplateError> {
340        Self::from_template(&template)
341    }
342
343    /// Compiles a borrowed template for deterministic reverse matching.
344    pub fn from_template(template: &UriTemplate) -> Result<Self, UriTemplateError> {
345        let mut parts = Vec::with_capacity(template.parts.len());
346        let mut variable_names = BTreeSet::new();
347
348        for (index, part) in template.parts.iter().enumerate() {
349            match part {
350                UriTemplatePart::Literal(literal) => {
351                    let mut encoded = String::new();
352                    append_encoded_literal(
353                        &mut encoded,
354                        literal,
355                        UriTemplateExpansionLimits::default(),
356                    )?;
357                    parts.push(ReversibleTemplatePart::Literal(encoded));
358                }
359                UriTemplatePart::Expression(expression) => {
360                    if expression.variables().is_empty()
361                        || (expression.variables().len() != 1
362                            && !expression.operator().properties().named)
363                    {
364                        return Err(UriTemplateError::NonReversibleTemplate {
365                            reason: UriTemplateMatchRejection::MultipleVariables,
366                        });
367                    }
368
369                    let mut variables = Vec::with_capacity(expression.variables().len());
370                    for variable in expression.variables() {
371                        if matches!(variable.modifier(), Some(UriTemplateModifier::Prefix(_))) {
372                            return Err(UriTemplateError::NonReversibleTemplate {
373                                reason: UriTemplateMatchRejection::LossyPrefix {
374                                    variable: variable.name().to_owned(),
375                                },
376                            });
377                        }
378                        if !variable_names.insert(variable.name().to_owned()) {
379                            return Err(UriTemplateError::NonReversibleTemplate {
380                                reason: UriTemplateMatchRejection::DuplicateVariable {
381                                    variable: variable.name().to_owned(),
382                                },
383                            });
384                        }
385                        let mut encoded_name = String::new();
386                        append_variable_name(
387                            &mut encoded_name,
388                            variable.name(),
389                            UriTemplateExpansionLimits::default(),
390                        )?;
391                        variables.push(ReversibleTemplateVariable {
392                            name: variable.name().to_owned(),
393                            encoded_name,
394                        });
395                    }
396
397                    let next_boundary = match template.parts.get(index + 1) {
398                        Some(UriTemplatePart::Literal(literal)) => {
399                            let mut encoded = String::new();
400                            append_encoded_literal(
401                                &mut encoded,
402                                literal,
403                                UriTemplateExpansionLimits::default(),
404                            )?;
405                            Some(ReversibleBoundary::Literal(encoded))
406                        }
407                        Some(UriTemplatePart::Expression(next)) => {
408                            Some(reversible_adjacent_expression_boundary(expression, next)?)
409                        }
410                        None => None,
411                    };
412
413                    validate_reversible_expression(expression, next_boundary.as_ref())?;
414                    parts.push(ReversibleTemplatePart::Expression(
415                        ReversibleTemplateExpression {
416                            operator: expression.operator(),
417                            variables,
418                            next_boundary,
419                        },
420                    ));
421                }
422            }
423        }
424
425        Ok(Self {
426            template: template.clone(),
427            parts,
428        })
429    }
430
431    /// Returns the original RFC 6570 template.
432    #[must_use]
433    pub fn template(&self) -> &UriTemplate {
434        &self.template
435    }
436
437    /// Expands only the scalar value shape declared by this compiled matcher.
438    pub fn expand(&self, values: &TemplateValues) -> Result<String, UriTemplateError> {
439        for part in &self.parts {
440            let ReversibleTemplatePart::Expression(expression) = part else {
441                continue;
442            };
443            for variable in &expression.variables {
444                let Some(value) = values.get(&variable.name) else {
445                    continue;
446                };
447                let TemplateValue::Scalar(value) = value else {
448                    return Err(UriTemplateError::NonScalarMatchValue {
449                        variable: variable.name.clone(),
450                    });
451                };
452                if value.is_empty()
453                    && matches!(
454                        expression.operator,
455                        UriTemplateOperator::Simple | UriTemplateOperator::Reserved
456                    )
457                {
458                    return Err(UriTemplateError::AmbiguousEmptyScalar {
459                        variable: variable.name.clone(),
460                    });
461                }
462                if matches!(
463                    expression.operator,
464                    UriTemplateOperator::Reserved | UriTemplateOperator::Fragment
465                ) && contains_pct_encoded_triplet(value)
466                {
467                    return Err(UriTemplateError::PreescapedReservedMatchValue {
468                        variable: variable.name.clone(),
469                    });
470                }
471            }
472        }
473        self.template.expand(values)
474    }
475
476    /// Reverse-matches an exact URI wire string and returns its scalar bindings.
477    ///
478    /// `Ok(None)` means the URI is outside this template's language. Any
479    /// successful result is replayed through [`Self::expand`] and compared to
480    /// the original bytes before it is returned.
481    pub fn match_uri(&self, uri: &str) -> Result<Option<TemplateValues>, UriTemplateError> {
482        if uri.len() > MAX_URI_TEMPLATE_MATCH_INPUT_BYTES {
483            return Err(UriTemplateError::MatchInputTooLong {
484                actual: uri.len(),
485                maximum: MAX_URI_TEMPLATE_MATCH_INPUT_BYTES,
486            });
487        }
488
489        let mut offset = 0;
490        let mut values = TemplateValues::new();
491        for part in &self.parts {
492            match part {
493                ReversibleTemplatePart::Literal(literal) => {
494                    let Some(remainder) = uri.get(offset..) else {
495                        return Ok(None);
496                    };
497                    let Some(remainder) = remainder.strip_prefix(literal) else {
498                        return Ok(None);
499                    };
500                    offset = uri.len() - remainder.len();
501                }
502                ReversibleTemplatePart::Expression(expression) => {
503                    let Some(remainder) = uri.get(offset..) else {
504                        return Ok(None);
505                    };
506                    let Some((captures, consumed)) =
507                        reverse_match_expression(expression, remainder)
508                    else {
509                        return Ok(None);
510                    };
511                    offset = offset.saturating_add(consumed);
512                    for (name, capture) in captures {
513                        values.insert(name, TemplateValue::Scalar(capture));
514                    }
515                }
516            }
517        }
518        if offset != uri.len() {
519            return Ok(None);
520        }
521        if self.expand(&values)? != uri {
522            return Ok(None);
523        }
524        Ok(Some(values))
525    }
526}
527
528impl TryFrom<UriTemplate> for ReversibleResourceTemplate {
529    type Error = UriTemplateError;
530
531    fn try_from(template: UriTemplate) -> Result<Self, Self::Error> {
532        Self::compile(template)
533    }
534}
535
536impl TryFrom<&UriTemplate> for ReversibleResourceTemplate {
537    type Error = UriTemplateError;
538
539    fn try_from(template: &UriTemplate) -> Result<Self, Self::Error> {
540        Self::from_template(template)
541    }
542}
543
544#[derive(Clone, Debug, Eq, PartialEq)]
545enum ReversibleTemplatePart {
546    Literal(String),
547    Expression(ReversibleTemplateExpression),
548}
549
550#[derive(Clone, Debug, Eq, PartialEq)]
551struct ReversibleTemplateExpression {
552    operator: UriTemplateOperator,
553    variables: Vec<ReversibleTemplateVariable>,
554    next_boundary: Option<ReversibleBoundary>,
555}
556
557#[derive(Clone, Debug, Eq, PartialEq)]
558struct ReversibleTemplateVariable {
559    name: String,
560    encoded_name: String,
561}
562
563/// A wire boundary that separates one reversible capture from the next part.
564#[derive(Clone, Debug, Eq, PartialEq)]
565enum ReversibleBoundary {
566    /// Literal source text following the capture.
567    Literal(String),
568    /// The distinctive opening bytes of a following expression.
569    ExpressionPrefix(String),
570}
571
572impl ReversibleBoundary {
573    // Not const: `String` -> `&str` deref coercion is non-const on the pinned nightly.
574    fn as_str(&self) -> &str {
575        match self {
576            Self::Literal(value) | Self::ExpressionPrefix(value) => value,
577        }
578    }
579
580    const fn permits_absent_following_expression(&self) -> bool {
581        matches!(self, Self::ExpressionPrefix(_))
582    }
583}
584
585/// One ordered segment in a [`UriTemplate`].
586#[derive(Clone, Debug, Eq, PartialEq)]
587pub enum UriTemplatePart {
588    /// Literal source text outside a template expression.
589    Literal(String),
590    /// A parsed expression.
591    Expression(UriTemplateExpression),
592}
593
594/// One parsed RFC 6570 expression.
595#[derive(Clone, Debug, Eq, PartialEq)]
596pub struct UriTemplateExpression {
597    operator: UriTemplateOperator,
598    variables: Vec<UriTemplateVariable>,
599}
600
601impl UriTemplateExpression {
602    /// Returns the expression operator.
603    #[must_use]
604    pub const fn operator(&self) -> UriTemplateOperator {
605        self.operator
606    }
607
608    /// Returns the variable specifications in source order.
609    #[must_use]
610    pub fn variables(&self) -> &[UriTemplateVariable] {
611        &self.variables
612    }
613}
614
615/// The Level 4 expression operator.
616#[derive(Clone, Copy, Debug, Eq, PartialEq)]
617pub enum UriTemplateOperator {
618    /// `{var}` simple string expansion.
619    Simple,
620    /// `{+var}` reserved string expansion.
621    Reserved,
622    /// `{#var}` fragment expansion.
623    Fragment,
624    /// `{.var}` label expansion.
625    Label,
626    /// `{/var}` path-segment expansion.
627    Path,
628    /// `{;var}` matrix/path-parameter expansion.
629    PathParameter,
630    /// `{?var}` form-style query expansion.
631    Query,
632    /// `{&var}` form-style query continuation.
633    QueryContinuation,
634}
635
636impl UriTemplateOperator {
637    /// Returns the operator character, or `None` for simple expansion.
638    #[must_use]
639    pub const fn character(self) -> Option<char> {
640        match self {
641            Self::Simple => None,
642            Self::Reserved => Some('+'),
643            Self::Fragment => Some('#'),
644            Self::Label => Some('.'),
645            Self::Path => Some('/'),
646            Self::PathParameter => Some(';'),
647            Self::Query => Some('?'),
648            Self::QueryContinuation => Some('&'),
649        }
650    }
651
652    const fn properties(self) -> OperatorProperties {
653        match self {
654            Self::Simple => OperatorProperties::new("", ",", false, "", false),
655            Self::Reserved => OperatorProperties::new("", ",", false, "", true),
656            Self::Fragment => OperatorProperties::new("#", ",", false, "", true),
657            Self::Label => OperatorProperties::new(".", ".", false, "", false),
658            Self::Path => OperatorProperties::new("/", "/", false, "", false),
659            Self::PathParameter => OperatorProperties::new(";", ";", true, "", false),
660            Self::Query => OperatorProperties::new("?", "&", true, "=", false),
661            Self::QueryContinuation => OperatorProperties::new("&", "&", true, "=", false),
662        }
663    }
664}
665
666/// One named variable and optional Level 4 modifier.
667#[derive(Clone, Debug, Eq, PartialEq)]
668pub struct UriTemplateVariable {
669    name: String,
670    modifier: Option<UriTemplateModifier>,
671}
672
673impl UriTemplateVariable {
674    /// Returns the exact, case-sensitive RFC 6570 variable name.
675    #[must_use]
676    pub fn name(&self) -> &str {
677        &self.name
678    }
679
680    /// Returns the optional Level 4 modifier.
681    #[must_use]
682    pub const fn modifier(&self) -> Option<UriTemplateModifier> {
683        self.modifier
684    }
685}
686
687/// One RFC 6570 Level 4 variable modifier.
688#[derive(Clone, Copy, Debug, Eq, PartialEq)]
689pub enum UriTemplateModifier {
690    /// Restrict a scalar variable to this many Unicode code points.
691    Prefix(usize),
692    /// Expand every list member or associative pair independently.
693    Explode,
694}
695
696/// A reason a syntactically valid template cannot be used for reverse match.
697#[derive(Clone, Debug, Eq, PartialEq)]
698pub enum UriTemplateMatchRejection {
699    /// An expression binds more than one value without an invertible shape.
700    MultipleVariables,
701    /// Two captures touch without a literal boundary.
702    AdjacentCaptures,
703    /// A variable is bound more than once by the same matcher.
704    DuplicateVariable {
705        /// The repeated variable name.
706        variable: String,
707    },
708    /// A prefix modifier discards data that a reverse match cannot restore.
709    LossyPrefix {
710        /// The affected variable name.
711        variable: String,
712    },
713    /// A composite explode modifier has no declared, unique inverse.
714    ///
715    /// Scalar `*` values are admitted because their RFC 6570 expansion is
716    /// identical to an unmodified scalar; composites are rejected at the
717    /// scalar dispatch boundary instead.
718    ExplodedComposite {
719        /// The affected variable name.
720        variable: String,
721    },
722    /// A following literal can also occur inside the capture language.
723    AmbiguousBoundary,
724    /// A reserved or fragment expansion has a following capture boundary.
725    UnboundedReservedCapture,
726}
727
728/// One typed reason template parsing or expansion failed.
729#[derive(Clone, Debug, Eq, PartialEq)]
730pub enum UriTemplateError {
731    /// Source text exceeded the fixed parser bound.
732    SourceTooLong {
733        /// Observed UTF-8 bytes.
734        actual: usize,
735        /// Maximum UTF-8 bytes.
736        maximum: usize,
737    },
738    /// Parsing would retain too many parts.
739    TooManyParts {
740        /// Maximum part count.
741        maximum: usize,
742    },
743    /// Parsing would retain too many expressions.
744    TooManyExpressions {
745        /// Maximum expression count.
746        maximum: usize,
747    },
748    /// An expression exceeded its variable-specification bound.
749    TooManyVariables {
750        /// Maximum variable specifications.
751        maximum: usize,
752    },
753    /// A variable name exceeded its byte bound.
754    VariableNameTooLong {
755        /// Observed UTF-8 bytes.
756        actual: usize,
757        /// Maximum UTF-8 bytes.
758        maximum: usize,
759    },
760    /// A literal used a character outside RFC 6570's literal grammar.
761    InvalidLiteral {
762        /// Byte offset in the source text.
763        offset: usize,
764    },
765    /// An expression opened without a matching closing brace.
766    UnclosedExpression {
767        /// Byte offset of the opening brace.
768        offset: usize,
769    },
770    /// An expression attempted to nest another expression.
771    NestedExpression {
772        /// Byte offset of the nested opening brace.
773        offset: usize,
774    },
775    /// A closing brace appeared outside an expression.
776    UnexpectedCloseBrace {
777        /// Byte offset of the closing brace.
778        offset: usize,
779    },
780    /// An expression did not contain a variable specification.
781    EmptyExpression {
782        /// Byte offset immediately after the opening brace.
783        offset: usize,
784    },
785    /// An expression used an RFC-reserved, unsupported operator.
786    UnsupportedOperator {
787        /// The reserved operator character.
788        operator: char,
789        /// Byte offset of the operator.
790        offset: usize,
791    },
792    /// A variable specification did not match the RFC 6570 grammar.
793    InvalidVariable {
794        /// Byte offset of the variable specification.
795        offset: usize,
796    },
797    /// A prefix modifier did not match the RFC's positive four-digit grammar.
798    InvalidPrefix {
799        /// Byte offset of the prefix modifier.
800        offset: usize,
801    },
802    /// A caller tried to relax a fixed expansion limit or selected zero.
803    InvalidExpansionLimit {
804        /// The invalid configuration field.
805        field: &'static str,
806        /// Requested value.
807        actual: usize,
808        /// Fixed ceiling.
809        maximum: usize,
810    },
811    /// A supplied scalar, list member, map key, or map value was too large.
812    ValueTooLong {
813        /// Observed UTF-8 bytes.
814        actual: usize,
815        /// Configured maximum UTF-8 bytes.
816        maximum: usize,
817    },
818    /// Expansion would inspect too many composite members.
819    TooManyCompositeItems {
820        /// Observed total members.
821        actual: usize,
822        /// Configured maximum members.
823        maximum: usize,
824    },
825    /// Expansion output would exceed the configured byte bound.
826    ExpansionTooLarge {
827        /// Bytes that would be present after the append.
828        actual: usize,
829        /// Configured maximum output bytes.
830        maximum: usize,
831    },
832    /// A prefix modifier was applied to a non-scalar value.
833    PrefixAppliedToComposite {
834        /// The affected variable name.
835        variable: String,
836    },
837    /// An associative value supplied the same key more than once.
838    DuplicateAssociativeKey {
839        /// The repeated key.
840        key: String,
841    },
842    /// A template is valid RFC 6570 syntax but has no deterministic inverse.
843    NonReversibleTemplate {
844        /// The specific reason compilation was rejected.
845        reason: UriTemplateMatchRejection,
846    },
847    /// A reversible expansion received a list or associative value.
848    NonScalarMatchValue {
849        /// The variable that requires a scalar value.
850        variable: String,
851    },
852    /// An empty simple or reserved scalar is indistinguishable from undefined.
853    AmbiguousEmptyScalar {
854        /// The affected variable name.
855        variable: String,
856    },
857    /// A candidate URI exceeded the fixed reverse-match input bound.
858    MatchInputTooLong {
859        /// Observed UTF-8 bytes.
860        actual: usize,
861        /// Fixed maximum UTF-8 bytes.
862        maximum: usize,
863    },
864    /// A reversible reserved or fragment value contained a pre-escaped triplet.
865    PreescapedReservedMatchValue {
866        /// The affected variable name.
867        variable: String,
868    },
869}
870
871impl fmt::Display for UriTemplateError {
872    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
873        match self {
874            Self::SourceTooLong { actual, maximum } => {
875                write!(
876                    formatter,
877                    "URI template is {actual} bytes; maximum is {maximum}"
878                )
879            }
880            Self::TooManyParts { maximum } => {
881                write!(formatter, "URI template exceeds {maximum} parsed parts")
882            }
883            Self::TooManyExpressions { maximum } => {
884                write!(formatter, "URI template exceeds {maximum} expressions")
885            }
886            Self::TooManyVariables { maximum } => {
887                write!(
888                    formatter,
889                    "URI template expression exceeds {maximum} variables"
890                )
891            }
892            Self::VariableNameTooLong { actual, maximum } => {
893                write!(
894                    formatter,
895                    "URI template variable is {actual} bytes; maximum is {maximum}"
896                )
897            }
898            Self::InvalidLiteral { offset } => {
899                write!(formatter, "invalid URI template literal at byte {offset}")
900            }
901            Self::UnclosedExpression { offset } => {
902                write!(
903                    formatter,
904                    "unclosed URI template expression at byte {offset}"
905                )
906            }
907            Self::NestedExpression { offset } => {
908                write!(formatter, "nested URI template expression at byte {offset}")
909            }
910            Self::UnexpectedCloseBrace { offset } => {
911                write!(
912                    formatter,
913                    "unexpected URI template closing brace at byte {offset}"
914                )
915            }
916            Self::EmptyExpression { offset } => {
917                write!(formatter, "empty URI template expression at byte {offset}")
918            }
919            Self::UnsupportedOperator { operator, offset } => write!(
920                formatter,
921                "unsupported URI template operator {operator:?} at byte {offset}"
922            ),
923            Self::InvalidVariable { offset } => {
924                write!(formatter, "invalid URI template variable at byte {offset}")
925            }
926            Self::InvalidPrefix { offset } => {
927                write!(
928                    formatter,
929                    "invalid URI template prefix modifier at byte {offset}"
930                )
931            }
932            Self::InvalidExpansionLimit {
933                field,
934                actual,
935                maximum,
936            } => write!(formatter, "{field} is {actual}; maximum is {maximum}"),
937            Self::ValueTooLong { actual, maximum } => {
938                write!(
939                    formatter,
940                    "URI template value is {actual} bytes; maximum is {maximum}"
941                )
942            }
943            Self::TooManyCompositeItems { actual, maximum } => write!(
944                formatter,
945                "URI template expansion has {actual} composite members; maximum is {maximum}"
946            ),
947            Self::ExpansionTooLarge { actual, maximum } => {
948                write!(
949                    formatter,
950                    "URI template expansion is {actual} bytes; maximum is {maximum}"
951                )
952            }
953            Self::PrefixAppliedToComposite { variable } => write!(
954                formatter,
955                "URI template prefix modifier cannot be applied to composite variable {variable:?}"
956            ),
957            Self::DuplicateAssociativeKey { key } => {
958                write!(
959                    formatter,
960                    "URI template associative key {key:?} is duplicated"
961                )
962            }
963            Self::NonReversibleTemplate { reason } => {
964                write!(
965                    formatter,
966                    "URI template cannot be reverse matched: {reason}"
967                )
968            }
969            Self::NonScalarMatchValue { variable } => write!(
970                formatter,
971                "reversible URI template variable {variable:?} requires a scalar value"
972            ),
973            Self::AmbiguousEmptyScalar { variable } => write!(
974                formatter,
975                "reversible URI template variable {variable:?} cannot use an empty scalar"
976            ),
977            Self::MatchInputTooLong { actual, maximum } => write!(
978                formatter,
979                "URI template match input is {actual} bytes; maximum is {maximum}"
980            ),
981            Self::PreescapedReservedMatchValue { variable } => write!(
982                formatter,
983                "reversible URI template variable {variable:?} cannot contain a pre-escaped percent triplet"
984            ),
985        }
986    }
987}
988
989impl std::error::Error for UriTemplateError {}
990
991impl fmt::Display for UriTemplateMatchRejection {
992    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
993        match self {
994            Self::MultipleVariables => {
995                formatter.write_str("an expression binds multiple variables")
996            }
997            Self::AdjacentCaptures => formatter.write_str("adjacent expressions have no boundary"),
998            Self::DuplicateVariable { variable } => {
999                write!(formatter, "variable {variable:?} is bound more than once")
1000            }
1001            Self::LossyPrefix { variable } => {
1002                write!(
1003                    formatter,
1004                    "variable {variable:?} uses a lossy prefix modifier"
1005                )
1006            }
1007            Self::ExplodedComposite { variable } => {
1008                write!(
1009                    formatter,
1010                    "variable {variable:?} uses an exploded composite modifier"
1011                )
1012            }
1013            Self::AmbiguousBoundary => {
1014                formatter.write_str("a capture overlaps its following literal boundary")
1015            }
1016            Self::UnboundedReservedCapture => {
1017                formatter.write_str("a reserved capture must be terminal")
1018            }
1019        }
1020    }
1021}
1022
1023#[derive(Clone, Copy)]
1024struct OperatorProperties {
1025    first: &'static str,
1026    separator: &'static str,
1027    named: bool,
1028    if_empty: &'static str,
1029    allow_reserved: bool,
1030}
1031
1032impl OperatorProperties {
1033    const fn new(
1034        first: &'static str,
1035        separator: &'static str,
1036        named: bool,
1037        if_empty: &'static str,
1038        allow_reserved: bool,
1039    ) -> Self {
1040        Self {
1041            first,
1042            separator,
1043            named,
1044            if_empty,
1045            allow_reserved,
1046        }
1047    }
1048
1049    fn is_form_style(self) -> bool {
1050        matches!(self.first, "?" | "&")
1051    }
1052}
1053
1054#[derive(Default)]
1055struct ExpansionState {
1056    composite_items: usize,
1057}
1058
1059impl ExpansionState {
1060    fn inspect_composite(
1061        &mut self,
1062        count: usize,
1063        limits: UriTemplateExpansionLimits,
1064    ) -> Result<(), UriTemplateError> {
1065        let actual = self.composite_items.saturating_add(count);
1066        if actual > limits.max_composite_items() {
1067            return Err(UriTemplateError::TooManyCompositeItems {
1068                actual,
1069                maximum: limits.max_composite_items(),
1070            });
1071        }
1072        self.composite_items = actual;
1073        Ok(())
1074    }
1075}
1076
1077struct ExpressionWriter<'a> {
1078    output: &'a mut String,
1079    properties: OperatorProperties,
1080    wrote_value: bool,
1081    limits: UriTemplateExpansionLimits,
1082}
1083
1084impl<'a> ExpressionWriter<'a> {
1085    fn new(
1086        output: &'a mut String,
1087        properties: OperatorProperties,
1088        limits: UriTemplateExpansionLimits,
1089    ) -> Self {
1090        Self {
1091            output,
1092            properties,
1093            wrote_value: false,
1094            limits,
1095        }
1096    }
1097
1098    fn begin_value(&mut self) -> Result<(), UriTemplateError> {
1099        let delimiter = if self.wrote_value {
1100            self.properties.separator
1101        } else {
1102            self.properties.first
1103        };
1104        append_output(self.output, delimiter, self.limits)?;
1105        self.wrote_value = true;
1106        Ok(())
1107    }
1108
1109    fn write_value(
1110        &mut self,
1111        name: &str,
1112        value: &str,
1113        logically_empty: bool,
1114    ) -> Result<(), UriTemplateError> {
1115        self.begin_value()?;
1116        if self.properties.named {
1117            append_variable_name(self.output, name, self.limits)?;
1118            append_output(
1119                self.output,
1120                if logically_empty {
1121                    self.properties.if_empty
1122                } else {
1123                    "="
1124                },
1125                self.limits,
1126            )?;
1127        }
1128        append_encoded_value(
1129            self.output,
1130            value,
1131            self.properties.allow_reserved,
1132            self.limits,
1133        )
1134    }
1135
1136    fn write_associative_pair(&mut self, key: &str, value: &str) -> Result<(), UriTemplateError> {
1137        self.begin_value()?;
1138        if self.properties.named {
1139            append_encoded_value(
1140                self.output,
1141                key,
1142                self.properties.allow_reserved,
1143                self.limits,
1144            )?;
1145            append_output(
1146                self.output,
1147                if value.is_empty() {
1148                    self.properties.if_empty
1149                } else {
1150                    "="
1151                },
1152                self.limits,
1153            )?;
1154            return append_encoded_value(
1155                self.output,
1156                value,
1157                self.properties.allow_reserved,
1158                self.limits,
1159            );
1160        }
1161
1162        append_encoded_value(
1163            self.output,
1164            key,
1165            self.properties.allow_reserved,
1166            self.limits,
1167        )?;
1168        if !value.is_empty() || self.properties.is_form_style() {
1169            append_output(self.output, "=", self.limits)?;
1170        }
1171        append_encoded_value(
1172            self.output,
1173            value,
1174            self.properties.allow_reserved,
1175            self.limits,
1176        )
1177    }
1178}
1179
1180fn push_part(
1181    parts: &mut Vec<UriTemplatePart>,
1182    part: UriTemplatePart,
1183) -> Result<(), UriTemplateError> {
1184    if parts.len() == MAX_URI_TEMPLATE_PARTS {
1185        return Err(UriTemplateError::TooManyParts {
1186            maximum: MAX_URI_TEMPLATE_PARTS,
1187        });
1188    }
1189    parts.push(part);
1190    Ok(())
1191}
1192
1193fn push_literal(
1194    parts: &mut Vec<UriTemplatePart>,
1195    literal: &str,
1196    offset: usize,
1197) -> Result<(), UriTemplateError> {
1198    if literal.is_empty() {
1199        return Ok(());
1200    }
1201    if let Some(relative_offset) = invalid_literal_offset(literal) {
1202        return Err(UriTemplateError::InvalidLiteral {
1203            offset: offset + relative_offset,
1204        });
1205    }
1206    push_part(parts, UriTemplatePart::Literal(literal.to_owned()))
1207}
1208
1209fn parse_expression(
1210    source: &str,
1211    source_offset: usize,
1212) -> Result<UriTemplateExpression, UriTemplateError> {
1213    let bytes = source.as_bytes();
1214    let Some(&first) = bytes.first() else {
1215        return Err(UriTemplateError::EmptyExpression {
1216            offset: source_offset,
1217        });
1218    };
1219    let (operator, variable_start) = match first {
1220        b'+' => (UriTemplateOperator::Reserved, 1),
1221        b'#' => (UriTemplateOperator::Fragment, 1),
1222        b'.' => (UriTemplateOperator::Label, 1),
1223        b'/' => (UriTemplateOperator::Path, 1),
1224        b';' => (UriTemplateOperator::PathParameter, 1),
1225        b'?' => (UriTemplateOperator::Query, 1),
1226        b'&' => (UriTemplateOperator::QueryContinuation, 1),
1227        b'=' | b',' | b'!' | b'@' | b'|' => {
1228            return Err(UriTemplateError::UnsupportedOperator {
1229                operator: char::from(first),
1230                offset: source_offset,
1231            });
1232        }
1233        _ => (UriTemplateOperator::Simple, 0),
1234    };
1235    let variables_source = &source[variable_start..];
1236    if variables_source.is_empty() {
1237        return Err(UriTemplateError::EmptyExpression {
1238            offset: source_offset + variable_start,
1239        });
1240    }
1241
1242    let mut variables = Vec::new();
1243    let mut variable_offset = source_offset + variable_start;
1244    for variable_source in variables_source.split(',') {
1245        if variables.len() == MAX_URI_TEMPLATE_VARIABLES_PER_EXPRESSION {
1246            return Err(UriTemplateError::TooManyVariables {
1247                maximum: MAX_URI_TEMPLATE_VARIABLES_PER_EXPRESSION,
1248            });
1249        }
1250        variables.push(parse_variable(variable_source, variable_offset)?);
1251        variable_offset += variable_source.len() + 1;
1252    }
1253    Ok(UriTemplateExpression {
1254        operator,
1255        variables,
1256    })
1257}
1258
1259fn parse_variable(
1260    source: &str,
1261    source_offset: usize,
1262) -> Result<UriTemplateVariable, UriTemplateError> {
1263    if source.is_empty() {
1264        return Err(UriTemplateError::InvalidVariable {
1265            offset: source_offset,
1266        });
1267    }
1268
1269    let (name, modifier) = if let Some(name) = source.strip_suffix('*') {
1270        if name.is_empty() || name.contains(':') {
1271            return Err(UriTemplateError::InvalidVariable {
1272                offset: source_offset,
1273            });
1274        }
1275        (name, Some(UriTemplateModifier::Explode))
1276    } else if let Some((name, prefix)) = source.split_once(':') {
1277        let prefix_offset = source_offset + name.len();
1278        if !is_valid_prefix(prefix) {
1279            return Err(UriTemplateError::InvalidPrefix {
1280                offset: prefix_offset,
1281            });
1282        }
1283        let length = prefix
1284            .parse::<usize>()
1285            .map_err(|_| UriTemplateError::InvalidPrefix {
1286                offset: prefix_offset,
1287            })?;
1288        (name, Some(UriTemplateModifier::Prefix(length)))
1289    } else {
1290        (source, None)
1291    };
1292
1293    if name.len() > MAX_URI_TEMPLATE_VARIABLE_NAME_BYTES {
1294        return Err(UriTemplateError::VariableNameTooLong {
1295            actual: name.len(),
1296            maximum: MAX_URI_TEMPLATE_VARIABLE_NAME_BYTES,
1297        });
1298    }
1299    if !is_valid_variable_name(name) {
1300        return Err(UriTemplateError::InvalidVariable {
1301            offset: source_offset,
1302        });
1303    }
1304
1305    Ok(UriTemplateVariable {
1306        name: name.to_owned(),
1307        modifier,
1308    })
1309}
1310
1311fn is_valid_prefix(prefix: &str) -> bool {
1312    let bytes = prefix.as_bytes();
1313    bytes.len() <= 4
1314        && bytes
1315            .first()
1316            .is_some_and(|byte| matches!(*byte, b'1'..=b'9'))
1317        && bytes.iter().all(u8::is_ascii_digit)
1318        && prefix
1319            .parse::<usize>()
1320            .is_ok_and(|length| length <= MAX_URI_TEMPLATE_PREFIX_LENGTH)
1321}
1322
1323fn is_valid_variable_name(name: &str) -> bool {
1324    !name.is_empty() && name.split('.').all(is_valid_variable_name_segment)
1325}
1326
1327fn is_valid_variable_name_segment(segment: &str) -> bool {
1328    if segment.is_empty() {
1329        return false;
1330    }
1331    let bytes = segment.as_bytes();
1332    let mut index = 0;
1333    while index < bytes.len() {
1334        if bytes[index].is_ascii_alphanumeric() || bytes[index] == b'_' {
1335            index += 1;
1336        } else if bytes[index] == b'%'
1337            && index + 2 < bytes.len()
1338            && bytes[index + 1].is_ascii_hexdigit()
1339            && bytes[index + 2].is_ascii_hexdigit()
1340        {
1341            index += 3;
1342        } else {
1343            return false;
1344        }
1345    }
1346    true
1347}
1348
1349fn invalid_literal_offset(literal: &str) -> Option<usize> {
1350    let bytes = literal.as_bytes();
1351    let mut index = 0;
1352    while index < bytes.len() {
1353        let byte = bytes[index];
1354        if byte.is_ascii() {
1355            if byte == b'%' {
1356                if index + 2 >= bytes.len()
1357                    || !bytes[index + 1].is_ascii_hexdigit()
1358                    || !bytes[index + 2].is_ascii_hexdigit()
1359                {
1360                    return Some(index);
1361                }
1362                index += 3;
1363            } else if is_valid_literal_byte(byte) {
1364                index += 1;
1365            } else {
1366                return Some(index);
1367            }
1368        } else {
1369            let Some(character) = literal[index..].chars().next() else {
1370                return Some(index);
1371            };
1372            if !is_valid_literal_character(character) {
1373                return Some(index);
1374            }
1375            index += character.len_utf8();
1376        }
1377    }
1378    None
1379}
1380
1381fn is_valid_literal_character(character: char) -> bool {
1382    matches!(
1383        u32::from(character),
1384        0x00A0..=0xD7FF
1385            | 0xE000..=0xF8FF
1386            | 0xF900..=0xFDCF
1387            | 0xFDF0..=0xFFEF
1388            | 0x10000..=0x1FFFD
1389            | 0x20000..=0x2FFFD
1390            | 0x30000..=0x3FFFD
1391            | 0x40000..=0x4FFFD
1392            | 0x50000..=0x5FFFD
1393            | 0x60000..=0x6FFFD
1394            | 0x70000..=0x7FFFD
1395            | 0x80000..=0x8FFFD
1396            | 0x90000..=0x9FFFD
1397            | 0xA0000..=0xAFFFD
1398            | 0xB0000..=0xBFFFD
1399            | 0xC0000..=0xCFFFD
1400            | 0xD0000..=0xDFFFD
1401            | 0xE1000..=0xEFFFD
1402            | 0xF0000..=0xFFFFD
1403            | 0x100000..=0x10FFFD
1404    )
1405}
1406
1407fn is_valid_literal_byte(byte: u8) -> bool {
1408    matches!(
1409        byte,
1410        b'!'
1411            | b'#'..=b'$'
1412            | b'&'
1413            | b'('..=b';'
1414            | b'='
1415            | b'?'..=b'['
1416            | b']'
1417            | b'_'
1418            | b'a'..=b'z'
1419            | b'~'
1420    )
1421}
1422
1423fn expand_expression(
1424    output: &mut String,
1425    expression: &UriTemplateExpression,
1426    values: &TemplateValues,
1427    limits: UriTemplateExpansionLimits,
1428    state: &mut ExpansionState,
1429) -> Result<(), UriTemplateError> {
1430    let properties = expression.operator.properties();
1431    let mut writer = ExpressionWriter::new(output, properties, limits);
1432    for variable in &expression.variables {
1433        let Some(value) = values.get(variable.name()) else {
1434            continue;
1435        };
1436        expand_variable(&mut writer, variable, value, state)?;
1437    }
1438    Ok(())
1439}
1440
1441fn expand_variable(
1442    writer: &mut ExpressionWriter<'_>,
1443    variable: &UriTemplateVariable,
1444    value: &TemplateValue,
1445    state: &mut ExpansionState,
1446) -> Result<(), UriTemplateError> {
1447    match value {
1448        TemplateValue::Scalar(value) => {
1449            let value = scalar_prefix(value, variable, writer.limits)?;
1450            writer.write_value(variable.name(), value, value.is_empty())
1451        }
1452        TemplateValue::List(values) => {
1453            if values.is_empty() {
1454                return Ok(());
1455            }
1456            state.inspect_composite(values.len(), writer.limits)?;
1457            for value in values.iter().flatten() {
1458                check_value_bound(value, writer.limits)?;
1459            }
1460            if !values.iter().any(Option::is_some) {
1461                return Ok(());
1462            }
1463            reject_prefix_on_composite(variable)?;
1464            if variable.modifier() == Some(UriTemplateModifier::Explode) {
1465                for value in values.iter().flatten() {
1466                    writer.write_value(variable.name(), value, value.is_empty())?;
1467                }
1468                return Ok(());
1469            }
1470
1471            writer.begin_value()?;
1472            if writer.properties.named {
1473                append_variable_name(writer.output, variable.name(), writer.limits)?;
1474                append_output(writer.output, "=", writer.limits)?;
1475            }
1476            let mut first = true;
1477            for value in values.iter().flatten() {
1478                if !first {
1479                    append_output(writer.output, ",", writer.limits)?;
1480                }
1481                append_encoded_value(
1482                    writer.output,
1483                    value,
1484                    writer.properties.allow_reserved,
1485                    writer.limits,
1486                )?;
1487                first = false;
1488            }
1489            Ok(())
1490        }
1491        TemplateValue::Associative(values) => {
1492            if values.is_empty() {
1493                return Ok(());
1494            }
1495            state.inspect_composite(values.len(), writer.limits)?;
1496            let mut unique_keys = BTreeSet::new();
1497            for (key, value) in values {
1498                if !unique_keys.insert(key.as_str()) {
1499                    return Err(UriTemplateError::DuplicateAssociativeKey { key: key.clone() });
1500                }
1501                check_value_bound(key, writer.limits)?;
1502                if let Some(value) = value {
1503                    check_value_bound(value, writer.limits)?;
1504                }
1505            }
1506            if !values.iter().any(|(_, value)| value.is_some()) {
1507                return Ok(());
1508            }
1509            reject_prefix_on_composite(variable)?;
1510            if variable.modifier() == Some(UriTemplateModifier::Explode) {
1511                for (key, value) in values {
1512                    let Some(value) = value else {
1513                        continue;
1514                    };
1515                    writer.write_associative_pair(key, value)?;
1516                }
1517                return Ok(());
1518            }
1519
1520            writer.begin_value()?;
1521            if writer.properties.named {
1522                append_variable_name(writer.output, variable.name(), writer.limits)?;
1523                append_output(writer.output, "=", writer.limits)?;
1524            }
1525            let mut first = true;
1526            for (key, value) in values {
1527                let Some(value) = value else {
1528                    continue;
1529                };
1530                if !first {
1531                    append_output(writer.output, ",", writer.limits)?;
1532                }
1533                append_encoded_value(
1534                    writer.output,
1535                    key,
1536                    writer.properties.allow_reserved,
1537                    writer.limits,
1538                )?;
1539                append_output(writer.output, ",", writer.limits)?;
1540                append_encoded_value(
1541                    writer.output,
1542                    value,
1543                    writer.properties.allow_reserved,
1544                    writer.limits,
1545                )?;
1546                first = false;
1547            }
1548            Ok(())
1549        }
1550    }
1551}
1552
1553fn scalar_prefix<'a>(
1554    value: &'a str,
1555    variable: &UriTemplateVariable,
1556    limits: UriTemplateExpansionLimits,
1557) -> Result<&'a str, UriTemplateError> {
1558    check_value_bound(value, limits)?;
1559    let value = match variable.modifier() {
1560        Some(UriTemplateModifier::Prefix(length)) => &value[..prefix_end(value, length)],
1561        Some(UriTemplateModifier::Explode) | None => value,
1562    };
1563    Ok(value)
1564}
1565
1566fn reject_prefix_on_composite(variable: &UriTemplateVariable) -> Result<(), UriTemplateError> {
1567    if matches!(variable.modifier(), Some(UriTemplateModifier::Prefix(_))) {
1568        return Err(UriTemplateError::PrefixAppliedToComposite {
1569            variable: variable.name().to_owned(),
1570        });
1571    }
1572    Ok(())
1573}
1574
1575fn prefix_end(value: &str, maximum_characters: usize) -> usize {
1576    let bytes = value.as_bytes();
1577    let mut index = 0;
1578    let mut characters = 0;
1579    while index < bytes.len() && characters < maximum_characters {
1580        if bytes[index] == b'%'
1581            && index + 2 < bytes.len()
1582            && bytes[index + 1].is_ascii_hexdigit()
1583            && bytes[index + 2].is_ascii_hexdigit()
1584        {
1585            index += pct_encoded_character_width(&bytes[index..]);
1586        } else {
1587            let Some(character) = value[index..].chars().next() else {
1588                break;
1589            };
1590            index += character.len_utf8();
1591        }
1592        characters += 1;
1593    }
1594    index
1595}
1596
1597fn pct_encoded_character_width(source: &[u8]) -> usize {
1598    let first = decode_pct_encoded_octet(source);
1599    let utf8_octets = first.map_or(1, utf8_sequence_width);
1600    if !(2..=4).contains(&utf8_octets) || source.len() < utf8_octets * 3 {
1601        return 3;
1602    }
1603
1604    let mut decoded = [0_u8; 4];
1605    for (index, slot) in decoded.iter_mut().take(utf8_octets).enumerate() {
1606        let offset = index * 3;
1607        let Some(octet) = decode_pct_encoded_octet(&source[offset..]) else {
1608            return 3;
1609        };
1610        *slot = octet;
1611    }
1612    if std::str::from_utf8(&decoded[..utf8_octets])
1613        .is_ok_and(|decoded| decoded.chars().count() == 1)
1614    {
1615        utf8_octets * 3
1616    } else {
1617        3
1618    }
1619}
1620
1621fn decode_pct_encoded_octet(source: &[u8]) -> Option<u8> {
1622    if source.len() < 3 || source[0] != b'%' {
1623        return None;
1624    }
1625    Some((hex_value(source[1])? << 4) | hex_value(source[2])?)
1626}
1627
1628const fn hex_value(byte: u8) -> Option<u8> {
1629    match byte {
1630        b'0'..=b'9' => Some(byte - b'0'),
1631        b'A'..=b'F' => Some(byte - b'A' + 10),
1632        b'a'..=b'f' => Some(byte - b'a' + 10),
1633        _ => None,
1634    }
1635}
1636
1637const fn utf8_sequence_width(first: u8) -> usize {
1638    match first {
1639        0xC2..=0xDF => 2,
1640        0xE0..=0xEF => 3,
1641        0xF0..=0xF4 => 4,
1642        _ => 1,
1643    }
1644}
1645
1646fn check_value_bound(
1647    value: &str,
1648    limits: UriTemplateExpansionLimits,
1649) -> Result<(), UriTemplateError> {
1650    if value.len() > limits.max_value_bytes() {
1651        return Err(UriTemplateError::ValueTooLong {
1652            actual: value.len(),
1653            maximum: limits.max_value_bytes(),
1654        });
1655    }
1656    Ok(())
1657}
1658
1659fn append_encoded_literal(
1660    output: &mut String,
1661    literal: &str,
1662    limits: UriTemplateExpansionLimits,
1663) -> Result<(), UriTemplateError> {
1664    append_percent_encoded(output, literal, true, true, limits)
1665}
1666
1667fn append_variable_name(
1668    output: &mut String,
1669    name: &str,
1670    limits: UriTemplateExpansionLimits,
1671) -> Result<(), UriTemplateError> {
1672    append_percent_encoded(output, name, false, true, limits)
1673}
1674
1675fn append_encoded_value(
1676    output: &mut String,
1677    value: &str,
1678    allow_reserved: bool,
1679    limits: UriTemplateExpansionLimits,
1680) -> Result<(), UriTemplateError> {
1681    append_percent_encoded(output, value, allow_reserved, allow_reserved, limits)
1682}
1683
1684fn append_percent_encoded(
1685    output: &mut String,
1686    source: &str,
1687    allow_reserved: bool,
1688    preserve_pct_encoded: bool,
1689    limits: UriTemplateExpansionLimits,
1690) -> Result<(), UriTemplateError> {
1691    let bytes = source.as_bytes();
1692    let mut index = 0;
1693    while index < bytes.len() {
1694        let byte = bytes[index];
1695        if preserve_pct_encoded
1696            && byte == b'%'
1697            && index + 2 < bytes.len()
1698            && bytes[index + 1].is_ascii_hexdigit()
1699            && bytes[index + 2].is_ascii_hexdigit()
1700        {
1701            append_output(output, &source[index..index + 3], limits)?;
1702            index += 3;
1703        } else if is_unreserved(byte) || (allow_reserved && is_reserved(byte)) {
1704            append_byte(output, byte, limits)?;
1705            index += 1;
1706        } else {
1707            append_percent_triplet(output, byte, limits)?;
1708            index += 1;
1709        }
1710    }
1711    Ok(())
1712}
1713
1714fn append_output(
1715    output: &mut String,
1716    fragment: &str,
1717    limits: UriTemplateExpansionLimits,
1718) -> Result<(), UriTemplateError> {
1719    let actual = output.len().saturating_add(fragment.len());
1720    if actual > limits.max_output_bytes() {
1721        return Err(UriTemplateError::ExpansionTooLarge {
1722            actual,
1723            maximum: limits.max_output_bytes(),
1724        });
1725    }
1726    output.push_str(fragment);
1727    Ok(())
1728}
1729
1730fn append_byte(
1731    output: &mut String,
1732    byte: u8,
1733    limits: UriTemplateExpansionLimits,
1734) -> Result<(), UriTemplateError> {
1735    let actual = output.len().saturating_add(1);
1736    if actual > limits.max_output_bytes() {
1737        return Err(UriTemplateError::ExpansionTooLarge {
1738            actual,
1739            maximum: limits.max_output_bytes(),
1740        });
1741    }
1742    output.push(char::from(byte));
1743    Ok(())
1744}
1745
1746fn append_percent_triplet(
1747    output: &mut String,
1748    byte: u8,
1749    limits: UriTemplateExpansionLimits,
1750) -> Result<(), UriTemplateError> {
1751    const HEX: &[u8; 16] = b"0123456789ABCDEF";
1752    let actual = output.len().saturating_add(3);
1753    if actual > limits.max_output_bytes() {
1754        return Err(UriTemplateError::ExpansionTooLarge {
1755            actual,
1756            maximum: limits.max_output_bytes(),
1757        });
1758    }
1759    output.push('%');
1760    output.push(char::from(HEX[usize::from(byte >> 4)]));
1761    output.push(char::from(HEX[usize::from(byte & 0x0f)]));
1762    Ok(())
1763}
1764
1765const fn is_unreserved(byte: u8) -> bool {
1766    byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~')
1767}
1768
1769const fn is_reserved(byte: u8) -> bool {
1770    matches!(
1771        byte,
1772        b':' | b'/'
1773            | b'?'
1774            | b'#'
1775            | b'['
1776            | b']'
1777            | b'@'
1778            | b'!'
1779            | b'$'
1780            | b'&'
1781            | b'\''
1782            | b'('
1783            | b')'
1784            | b'*'
1785            | b'+'
1786            | b','
1787            | b';'
1788            | b'='
1789    )
1790}
1791
1792fn validate_reversible_expression(
1793    expression: &UriTemplateExpression,
1794    next_boundary: Option<&ReversibleBoundary>,
1795) -> Result<(), UriTemplateError> {
1796    match expression.operator() {
1797        UriTemplateOperator::Reserved | UriTemplateOperator::Fragment
1798            if next_boundary.is_some() =>
1799        {
1800            Err(UriTemplateError::NonReversibleTemplate {
1801                reason: UriTemplateMatchRejection::UnboundedReservedCapture,
1802            })
1803        }
1804        UriTemplateOperator::Simple
1805        | UriTemplateOperator::Label
1806        | UriTemplateOperator::Path
1807        | UriTemplateOperator::PathParameter
1808        | UriTemplateOperator::Query
1809        | UriTemplateOperator::QueryContinuation => {
1810            if next_boundary.is_some_and(|boundary| {
1811                !boundary
1812                    .as_str()
1813                    .as_bytes()
1814                    .first()
1815                    .is_some_and(|byte| is_reserved(*byte))
1816            }) {
1817                return Err(UriTemplateError::NonReversibleTemplate {
1818                    reason: UriTemplateMatchRejection::AmbiguousBoundary,
1819                });
1820            }
1821            Ok(())
1822        }
1823        UriTemplateOperator::Reserved | UriTemplateOperator::Fragment => Ok(()),
1824    }
1825}
1826
1827fn reversible_adjacent_expression_boundary(
1828    expression: &UriTemplateExpression,
1829    next: &UriTemplateExpression,
1830) -> Result<ReversibleBoundary, UriTemplateError> {
1831    let prefix = match next.operator() {
1832        // These expansions emit no leading wire marker, so an adjacent prior
1833        // scalar has no unique point at which to stop.
1834        UriTemplateOperator::Simple | UriTemplateOperator::Reserved => {
1835            return Err(UriTemplateError::NonReversibleTemplate {
1836                reason: UriTemplateMatchRejection::AdjacentCaptures,
1837            });
1838        }
1839        UriTemplateOperator::Fragment => "#".to_owned(),
1840        UriTemplateOperator::Label => ".".to_owned(),
1841        UriTemplateOperator::Path => {
1842            // `{/first}{/second}` cannot distinguish a present first value
1843            // from a present second value when the other value is absent.
1844            if expression.operator() == UriTemplateOperator::Path {
1845                return Err(UriTemplateError::NonReversibleTemplate {
1846                    reason: UriTemplateMatchRejection::AmbiguousBoundary,
1847                });
1848            }
1849            "/".to_owned()
1850        }
1851        UriTemplateOperator::PathParameter => ";".to_owned(),
1852        UriTemplateOperator::Query => "?".to_owned(),
1853        UriTemplateOperator::QueryContinuation => "&".to_owned(),
1854    };
1855
1856    Ok(ReversibleBoundary::ExpressionPrefix(prefix))
1857}
1858
1859fn reverse_match_expression(
1860    expression: &ReversibleTemplateExpression,
1861    remainder: &str,
1862) -> Option<(Vec<(String, String)>, usize)> {
1863    if expression.variables.len() > 1 {
1864        return reverse_match_named_expression(expression, remainder);
1865    }
1866    let variable = expression.variables.first()?;
1867    let (capture, consumed) = match expression.operator {
1868        UriTemplateOperator::Simple => {
1869            reverse_match_unprefixed(remainder, expression.next_boundary.as_ref())
1870        }
1871        UriTemplateOperator::Reserved => reverse_match_unprefixed(remainder, None),
1872        UriTemplateOperator::Fragment => {
1873            reverse_match_prefixed(remainder, "#", expression.next_boundary.as_ref())
1874        }
1875        UriTemplateOperator::Label => {
1876            reverse_match_prefixed(remainder, ".", expression.next_boundary.as_ref())
1877        }
1878        UriTemplateOperator::Path => {
1879            reverse_match_prefixed(remainder, "/", expression.next_boundary.as_ref())
1880        }
1881        UriTemplateOperator::PathParameter => reverse_match_named(
1882            remainder,
1883            ";",
1884            variable,
1885            false,
1886            expression.next_boundary.as_ref(),
1887        ),
1888        UriTemplateOperator::Query => reverse_match_named(
1889            remainder,
1890            "?",
1891            variable,
1892            true,
1893            expression.next_boundary.as_ref(),
1894        ),
1895        UriTemplateOperator::QueryContinuation => reverse_match_named(
1896            remainder,
1897            "&",
1898            variable,
1899            true,
1900            expression.next_boundary.as_ref(),
1901        ),
1902    }?;
1903    let captures = capture
1904        .map(|capture| vec![(variable.name.clone(), capture)])
1905        .unwrap_or_default();
1906    Some((captures, consumed))
1907}
1908
1909fn reverse_match_unprefixed(
1910    remainder: &str,
1911    next_boundary: Option<&ReversibleBoundary>,
1912) -> Option<(Option<String>, usize)> {
1913    let (capture, consumed) = reverse_capture_to_boundary(remainder, next_boundary)?;
1914    if capture.is_empty() {
1915        return Some((None, 0));
1916    }
1917    Some((Some(decode_percent_triplets_once(capture)?), consumed))
1918}
1919
1920fn reverse_match_prefixed(
1921    remainder: &str,
1922    prefix: &str,
1923    next_boundary: Option<&ReversibleBoundary>,
1924) -> Option<(Option<String>, usize)> {
1925    let Some(after_prefix) = remainder.strip_prefix(prefix) else {
1926        return Some((None, 0));
1927    };
1928    let Some((capture, consumed)) = reverse_capture_to_boundary(after_prefix, next_boundary) else {
1929        // A path expression and its following literal may both begin with
1930        // `/`. If no complete literal boundary follows the consumed marker,
1931        // leave it for the literal part: the expression is undefined.
1932        return Some((None, 0));
1933    };
1934    Some((
1935        Some(decode_percent_triplets_once(capture)?),
1936        prefix.len().saturating_add(consumed),
1937    ))
1938}
1939
1940fn reverse_match_named(
1941    remainder: &str,
1942    marker: &str,
1943    variable: &ReversibleTemplateVariable,
1944    requires_equals: bool,
1945    next_boundary: Option<&ReversibleBoundary>,
1946) -> Option<(Option<String>, usize)> {
1947    let mut prefix = String::with_capacity(marker.len() + variable.encoded_name.len() + 1);
1948    prefix.push_str(marker);
1949    prefix.push_str(&variable.encoded_name);
1950    let Some(after_name) = remainder.strip_prefix(&prefix) else {
1951        return Some((None, 0));
1952    };
1953
1954    let (after_equals, equals_len) = if requires_equals {
1955        if let Some(after_equals) = after_name.strip_prefix('=') {
1956            (after_equals, 1)
1957        } else if after_name.is_empty() {
1958            (after_name, 0)
1959        } else {
1960            return Some((None, 0));
1961        }
1962    } else if let Some(after_equals) = after_name.strip_prefix('=') {
1963        (after_equals, 1)
1964    } else if after_name.is_empty() || after_name.starts_with(';') {
1965        (after_name, 0)
1966    } else {
1967        return Some((None, 0));
1968    };
1969    let Some((capture, consumed)) = reverse_capture_to_boundary(after_equals, next_boundary) else {
1970        return Some((None, 0));
1971    };
1972    Some((
1973        Some(decode_percent_triplets_once(capture)?),
1974        prefix
1975            .len()
1976            .saturating_add(equals_len)
1977            .saturating_add(consumed),
1978    ))
1979}
1980
1981fn reverse_match_named_expression(
1982    expression: &ReversibleTemplateExpression,
1983    remainder: &str,
1984) -> Option<(Vec<(String, String)>, usize)> {
1985    let properties = expression.operator.properties();
1986    debug_assert!(properties.named);
1987
1988    let requires_equals = matches!(
1989        expression.operator,
1990        UriTemplateOperator::Query | UriTemplateOperator::QueryContinuation
1991    );
1992    let mut captures = Vec::with_capacity(expression.variables.len());
1993    let mut offset = 0;
1994    let mut wrote_value = false;
1995
1996    for variable in &expression.variables {
1997        let marker = if wrote_value {
1998            properties.separator
1999        } else {
2000            properties.first
2001        };
2002        let remainder = remainder.get(offset..)?;
2003        let Some(after_marker) = remainder.strip_prefix(marker) else {
2004            continue;
2005        };
2006        let Some(after_name) = after_marker.strip_prefix(&variable.encoded_name) else {
2007            continue;
2008        };
2009
2010        let (after_equals, equals_len) = if requires_equals {
2011            let after_equals = after_name.strip_prefix('=')?;
2012            (after_equals, 1)
2013        } else if let Some(after_equals) = after_name.strip_prefix('=') {
2014            (after_equals, 1)
2015        } else if after_name.is_empty() || after_name.starts_with(properties.separator) {
2016            (after_name, 0)
2017        } else {
2018            continue;
2019        };
2020
2021        let capture_len = reversible_named_capture_len(
2022            after_equals,
2023            properties.separator,
2024            expression.next_boundary.as_ref(),
2025        );
2026        let capture = after_equals.get(..capture_len)?;
2027        let value = decode_percent_triplets_once(capture)?;
2028        offset = offset
2029            .saturating_add(marker.len())
2030            .saturating_add(variable.encoded_name.len())
2031            .saturating_add(equals_len)
2032            .saturating_add(capture_len);
2033        captures.push((variable.name.clone(), value));
2034        wrote_value = true;
2035    }
2036
2037    Some((captures, offset))
2038}
2039
2040fn reversible_named_capture_len(
2041    remainder: &str,
2042    separator: &str,
2043    next_boundary: Option<&ReversibleBoundary>,
2044) -> usize {
2045    let mut capture_len = remainder.len();
2046    if let Some(offset) = remainder.find(separator) {
2047        capture_len = capture_len.min(offset);
2048    }
2049    if let Some(offset) = next_boundary.and_then(|boundary| remainder.find(boundary.as_str())) {
2050        capture_len = capture_len.min(offset);
2051    }
2052    capture_len
2053}
2054
2055fn reverse_capture_to_boundary<'a>(
2056    remainder: &'a str,
2057    next_boundary: Option<&ReversibleBoundary>,
2058) -> Option<(&'a str, usize)> {
2059    match next_boundary {
2060        Some(boundary) => {
2061            if let Some(offset) = remainder.find(boundary.as_str()) {
2062                Some((&remainder[..offset], offset))
2063            } else if boundary.permits_absent_following_expression() {
2064                Some((remainder, remainder.len()))
2065            } else {
2066                None
2067            }
2068        }
2069        None => Some((remainder, remainder.len())),
2070    }
2071}
2072
2073fn decode_percent_triplets_once(capture: &str) -> Option<String> {
2074    let bytes = capture.as_bytes();
2075    let mut decoded = Vec::with_capacity(bytes.len());
2076    let mut index = 0;
2077    while index < bytes.len() {
2078        if bytes[index] == b'%' {
2079            let high = *bytes.get(index + 1)?;
2080            let low = *bytes.get(index + 2)?;
2081            decoded.push((hex_value(high)? << 4) | hex_value(low)?);
2082            index += 3;
2083        } else {
2084            decoded.push(bytes[index]);
2085            index += 1;
2086        }
2087    }
2088    String::from_utf8(decoded).ok()
2089}
2090
2091fn contains_pct_encoded_triplet(value: &str) -> bool {
2092    let bytes = value.as_bytes();
2093    bytes.windows(3).any(|window| {
2094        window[0] == b'%' && window[1].is_ascii_hexdigit() && window[2].is_ascii_hexdigit()
2095    })
2096}
2097
2098#[cfg(test)]
2099mod tests {
2100    use super::*;
2101
2102    fn values() -> TemplateValues {
2103        let mut values = TemplateValues::new();
2104        values.insert("var".to_owned(), TemplateValue::scalar("value"));
2105        values.insert("hello".to_owned(), TemplateValue::scalar("Hello World!"));
2106        values.insert("path".to_owned(), TemplateValue::scalar("/foo/bar"));
2107        values.insert(
2108            "list".to_owned(),
2109            TemplateValue::list(vec![
2110                "red".to_owned(),
2111                "green".to_owned(),
2112                "blue".to_owned(),
2113            ]),
2114        );
2115        values.insert(
2116            "keys".to_owned(),
2117            TemplateValue::associative(vec![
2118                ("semi".to_owned(), ";".to_owned()),
2119                ("dot".to_owned(), ".".to_owned()),
2120                ("comma".to_owned(), ",".to_owned()),
2121            ]),
2122        );
2123        values
2124    }
2125
2126    #[test]
2127    fn rfc6570_level_four_expansion_positive() {
2128        let values = values();
2129        let examples = [
2130            ("{var:3}", "val"),
2131            ("{+path}", "/foo/bar"),
2132            ("{#hello}", "#Hello%20World!"),
2133            ("X{.list*}", "X.red.green.blue"),
2134            ("{/list*,path:4}", "/red/green/blue/%2Ffoo"),
2135            ("{;keys*}", ";semi=%3B;dot=.;comma=%2C"),
2136            ("{?keys*}", "?semi=%3B&dot=.&comma=%2C"),
2137            (
2138                "?fixed=yes{&list*}",
2139                "?fixed=yes&list=red&list=green&list=blue",
2140            ),
2141            ("{keys}", "semi,%3B,dot,.,comma,%2C"),
2142            ("{+keys*}", "semi=;,dot=.,comma=,"),
2143            ("{hello}", "Hello%20World%21"),
2144        ];
2145
2146        for (template, expected) in examples {
2147            let parsed = UriTemplate::parse(template).expect("RFC 6570 example parses");
2148            assert_eq!(parsed.source(), template);
2149            assert_eq!(
2150                parsed.expand(&values).expect("RFC example expands"),
2151                expected
2152            );
2153        }
2154    }
2155
2156    #[test]
2157    fn rfc6570_unicode_and_existing_percent_triplets_positive() {
2158        let template = UriTemplate::parse("https://example.test/é%27s/{term}/{+encoded}")
2159            .expect("printable Unicode literal and RFC expressions parse");
2160        let mut values = TemplateValues::new();
2161        values.insert("term".to_owned(), TemplateValue::scalar("café"));
2162        values.insert("encoded".to_owned(), TemplateValue::scalar("50%25"));
2163
2164        assert_eq!(
2165            template
2166                .expand(&values)
2167                .expect("Unicode expands as UTF-8 percent triplets"),
2168            "https://example.test/%C3%A9%27s/caf%C3%A9/50%25"
2169        );
2170        assert_eq!(
2171            UriTemplate::parse("{encoded}")
2172                .expect("simple expansion parses")
2173                .expand(&values)
2174                .expect("simple expansion percent-encodes a supplied triplet once"),
2175            "50%2525"
2176        );
2177    }
2178
2179    #[test]
2180    fn rfc6570_literal_percent_triplets_are_admitted_without_normalizing_bare_percent() {
2181        let template = UriTemplate::parse("mcp://resource/reports%2Fdaily")
2182            .expect("a complete literal percent triplet is valid RFC 6570");
2183        assert_eq!(
2184            template
2185                .expand(&TemplateValues::new())
2186                .expect("a valid literal expands unchanged"),
2187            "mcp://resource/reports%2Fdaily"
2188        );
2189
2190        assert_eq!(
2191            UriTemplate::parse("mcp://resource/reports%Qdaily"),
2192            Err(UriTemplateError::InvalidLiteral {
2193                offset: "mcp://resource/reports".len(),
2194            }),
2195            "changing only the final percent triplet to a bare percent rejects rather than rewriting it"
2196        );
2197    }
2198
2199    #[test]
2200    fn rfc6570_literal_grammar_accepts_final_ucschar_and_rejects_nearby_exclusions() {
2201        let admitted = format!(
2202            "mcp://resource/{}",
2203            char::from_u32(0xE1000).expect("E1000 is a Unicode scalar")
2204        );
2205        UriTemplate::parse(&admitted).expect("the first scalar in RFC 6570's final ucschar range");
2206
2207        let excluded_ucschar = format!(
2208            "mcp://resource/{}",
2209            char::from_u32(0xE0FFF).expect("E0FFF is a Unicode scalar")
2210        );
2211        assert!(matches!(
2212            UriTemplate::parse(&excluded_ucschar),
2213            Err(UriTemplateError::InvalidLiteral { .. })
2214        ));
2215        assert!(matches!(
2216            UriTemplate::parse("mcp://resource/raw'apostrophe"),
2217            Err(UriTemplateError::InvalidLiteral { .. })
2218        ));
2219        UriTemplate::parse("mcp://resource/encoded%27apostrophe")
2220            .expect("pct-encoded apostrophe remains a valid literal");
2221    }
2222
2223    #[test]
2224    fn rfc6570_literal_grammar_admits_supplementary_iprivate_ranges() {
2225        for scalar in [0xF0000, 0xFFFFD, 0x100000, 0x10FFFD] {
2226            let template = format!(
2227                "mcp://resource/{}",
2228                char::from_u32(scalar).expect("the RFC 3987 iprivate scalar is valid Unicode"),
2229            );
2230            UriTemplate::parse(&template)
2231                .expect("RFC 6570 literals admit every supplementary iprivate endpoint");
2232        }
2233
2234        for scalar in [0xEFFFE, 0xEFFFF, 0xFFFFE, 0x10FFFE] {
2235            let template = format!(
2236                "mcp://resource/{}",
2237                char::from_u32(scalar).expect("the adjacent scalar is valid Unicode"),
2238            );
2239            assert!(matches!(
2240                UriTemplate::parse(&template),
2241                Err(UriTemplateError::InvalidLiteral { .. })
2242            ));
2243        }
2244    }
2245
2246    #[test]
2247    fn rfc6570_bounds_apply_before_ownership_and_prefix_projection() {
2248        let oversized_source = "x".repeat(MAX_URI_TEMPLATE_BYTES + 1);
2249        assert_eq!(
2250            UriTemplate::parse(&oversized_source),
2251            Err(UriTemplateError::SourceTooLong {
2252                actual: MAX_URI_TEMPLATE_BYTES + 1,
2253                maximum: MAX_URI_TEMPLATE_BYTES,
2254            })
2255        );
2256
2257        let template = UriTemplate::parse("{value:1}").expect("prefix expression parses");
2258        let mut values = TemplateValues::new();
2259        values.insert("value".to_owned(), TemplateValue::scalar("ab"));
2260        let limits =
2261            UriTemplateExpansionLimits::new(16, 1, 1).expect("one-byte scalar limit is valid");
2262        assert_eq!(
2263            template.expand_with_limits(&values, limits),
2264            Err(UriTemplateError::ValueTooLong {
2265                actual: 2,
2266                maximum: 1,
2267            })
2268        );
2269    }
2270
2271    #[test]
2272    fn rfc6570_prefix_counts_percent_encoded_utf8_as_one_unicode_scalar() {
2273        let template = UriTemplate::parse("{+value:1}").expect("prefix expression parses");
2274        let mut values = TemplateValues::new();
2275        values.insert("value".to_owned(), TemplateValue::scalar("%C3%A9clair"));
2276
2277        assert_eq!(
2278            template
2279                .expand(&values)
2280                .expect("encoded Unicode scalar is not split between octets"),
2281            "%C3%A9"
2282        );
2283    }
2284
2285    #[test]
2286    fn rfc6570_undefined_composite_members_are_ignored_without_losing_empty_values() {
2287        let template = UriTemplate::parse("{?list*,keys*}").expect("composite expression parses");
2288        let mut values = TemplateValues::new();
2289        values.insert(
2290            "list".to_owned(),
2291            TemplateValue::list_with_undefined(vec![
2292                None,
2293                Some("one".to_owned()),
2294                Some(String::new()),
2295                None,
2296            ]),
2297        );
2298        values.insert(
2299            "keys".to_owned(),
2300            TemplateValue::associative_with_undefined(vec![
2301                ("ignored".to_owned(), None),
2302                ("second".to_owned(), Some("2".to_owned())),
2303            ]),
2304        );
2305
2306        assert_eq!(
2307            template
2308                .expand(&values)
2309                .expect("only defined composite members expand"),
2310            "?list=one&list=&second=2"
2311        );
2312
2313        values.insert(
2314            "list".to_owned(),
2315            TemplateValue::list_with_undefined(vec![None]),
2316        );
2317        values.insert(
2318            "keys".to_owned(),
2319            TemplateValue::associative_with_undefined(vec![("ignored".to_owned(), None)]),
2320        );
2321        assert_eq!(
2322            template
2323                .expand(&values)
2324                .expect("all-undefined composites are undefined variables"),
2325            ""
2326        );
2327        assert_eq!(
2328            UriTemplate::parse("{list:1}{keys:1}")
2329                .expect("prefix modifiers parse independently of runtime value types")
2330                .expand(&values)
2331                .expect("an undefined composite is ignored before modifier semantics"),
2332            ""
2333        );
2334    }
2335
2336    #[test]
2337    fn rfc6570_associative_order_is_lossless_and_duplicates_fail_closed() {
2338        let template = UriTemplate::parse("{keys}").expect("associative expression parses");
2339        let mut values = TemplateValues::new();
2340        values.insert(
2341            "keys".to_owned(),
2342            TemplateValue::associative(vec![
2343                ("second".to_owned(), "2".to_owned()),
2344                ("first".to_owned(), "1".to_owned()),
2345            ]),
2346        );
2347        assert_eq!(
2348            template.expand(&values).expect("ordered pairs expand"),
2349            "second,2,first,1"
2350        );
2351
2352        let before = values.clone();
2353        values.insert(
2354            "keys".to_owned(),
2355            TemplateValue::associative(vec![
2356                ("same".to_owned(), "1".to_owned()),
2357                ("same".to_owned(), "2".to_owned()),
2358            ]),
2359        );
2360        let planted_before = values.clone();
2361        assert_eq!(
2362            template.expand(&values),
2363            Err(UriTemplateError::DuplicateAssociativeKey {
2364                key: "same".to_owned(),
2365            })
2366        );
2367        assert_ne!(
2368            planted_before, before,
2369            "negative changes only the associative input"
2370        );
2371        assert_eq!(
2372            values, planted_before,
2373            "rejected expansion leaves caller values unchanged"
2374        );
2375    }
2376
2377    #[test]
2378    fn rfc6570_level_four_near_negative_rejects_invalid_prefix_without_mutation() {
2379        let accepted = "mcp://resources/{term:3}{?cursor}";
2380        let planted = "mcp://resources/{term:0}{?cursor}";
2381        let parsed = UriTemplate::parse(accepted).expect("positive control parses");
2382        let before = parsed.clone();
2383
2384        let error = UriTemplate::parse(planted)
2385            .expect_err("changing only the positive prefix length to zero must reject");
2386        assert!(matches!(error, UriTemplateError::InvalidPrefix { .. }));
2387        assert_eq!(
2388            parsed, before,
2389            "rejected input cannot mutate an admitted AST"
2390        );
2391        assert_eq!(parsed.source(), accepted);
2392    }
2393
2394    #[test]
2395    fn rfc6570_level_four_near_negative_rejects_composite_overflow_without_mutation() {
2396        let template = UriTemplate::parse("{?items*}").expect("positive control parses");
2397        let mut values = TemplateValues::new();
2398        values.insert(
2399            "items".to_owned(),
2400            TemplateValue::list(vec!["one".to_owned(), "two".to_owned()]),
2401        );
2402        let before = values.clone();
2403        let limits = UriTemplateExpansionLimits::new(128, 1, 16)
2404            .expect("the stricter configured limits are valid");
2405
2406        let error = template
2407            .expand_with_limits(&values, limits)
2408            .expect_err("changing only the allowed member count must reject the same input");
2409        assert_eq!(
2410            error,
2411            UriTemplateError::TooManyCompositeItems {
2412                actual: 2,
2413                maximum: 1,
2414            }
2415        );
2416        assert_eq!(
2417            values, before,
2418            "rejected expansion leaves caller values unchanged"
2419        );
2420    }
2421
2422    #[test]
2423    fn reversible_resource_template_decodes_populated_and_absent_path_captures() {
2424        let template = UriTemplate::parse("mcp://resource{/collection}/manifest{?revision}")
2425            .expect("the RFC 6570 template parses");
2426        let matcher = template
2427            .compile_reversible()
2428            .expect("separated scalar captures compile deterministically");
2429        let mut values = TemplateValues::new();
2430        values.insert(
2431            "collection".to_owned(),
2432            TemplateValue::scalar("books/fiction"),
2433        );
2434        values.insert("revision".to_owned(), TemplateValue::scalar("2026-08-09"));
2435
2436        let uri = matcher
2437            .expand(&values)
2438            .expect("declared scalar values expand");
2439        assert_eq!(
2440            uri,
2441            "mcp://resource/books%2Ffiction/manifest?revision=2026-08-09"
2442        );
2443        assert_eq!(
2444            matcher
2445                .match_uri(&uri)
2446                .expect("reverse matching is bounded"),
2447            Some(values),
2448            "a populated path capture decodes its percent triplet exactly once"
2449        );
2450
2451        let omitted = TemplateValues::new();
2452        let omitted_uri = matcher
2453            .expand(&omitted)
2454            .expect("undefined values omit their whole expressions");
2455        assert_eq!(omitted_uri, "mcp://resource/manifest");
2456        assert_eq!(
2457            matcher
2458                .match_uri(&omitted_uri)
2459                .expect("omitted values reverse match"),
2460            Some(omitted),
2461            "an omitted path capture does not consume its following literal"
2462        );
2463    }
2464
2465    #[test]
2466    fn reversible_resource_template_round_trips_raw_scalar_input() {
2467        let matcher = UriTemplate::parse("mcp://resource/{value}")
2468            .expect("the simple template parses")
2469            .compile_reversible()
2470            .expect("a terminal simple capture is deterministic");
2471        let mut values = TemplateValues::new();
2472        values.insert("value".to_owned(), TemplateValue::scalar("books/fiction"));
2473
2474        let uri = matcher
2475            .expand(&values)
2476            .expect("simple expansion percent-encodes a raw slash");
2477        assert_eq!(uri, "mcp://resource/books%2Ffiction");
2478        assert_eq!(
2479            matcher
2480                .match_uri(&uri)
2481                .expect("reverse matching is bounded"),
2482            Some(values),
2483            "reverse matching decodes the URI triplet back to the raw scalar"
2484        );
2485    }
2486
2487    #[test]
2488    fn reversible_resource_template_preescaped_simple_scalar_is_not_preescaped() {
2489        let matcher = UriTemplate::parse("mcp://resource/{value}")
2490            .expect("the simple template parses")
2491            .compile_reversible()
2492            .expect("a terminal simple capture is deterministic");
2493        let mut values = TemplateValues::new();
2494        values.insert("value".to_owned(), TemplateValue::scalar("books%2Ffiction"));
2495
2496        let uri = matcher
2497            .expand(&values)
2498            .expect("simple expansion encodes a percent as data");
2499        assert_eq!(uri, "mcp://resource/books%252Ffiction");
2500        assert_ne!(uri, "mcp://resource/books%2Ffiction");
2501        assert_eq!(
2502            matcher
2503                .match_uri(&uri)
2504                .expect("reverse matching is bounded"),
2505            Some(values),
2506            "one decode restores the preescaped scalar without treating it as wire syntax"
2507        );
2508    }
2509
2510    #[test]
2511    fn reversible_resource_template_reserved_and_fragment_raw_values_round_trip() {
2512        let cases = [
2513            (
2514                "mcp://resource/{+value}",
2515                "docs/guide?draft=true",
2516                "mcp://resource/docs/guide?draft=true",
2517            ),
2518            (
2519                "mcp://resource{#value}",
2520                "docs/guide?draft=true",
2521                "mcp://resource#docs/guide?draft=true",
2522            ),
2523        ];
2524
2525        for (template, raw_value, expected_uri) in cases {
2526            let matcher = UriTemplate::parse(template)
2527                .expect("the reserved or fragment template parses")
2528                .compile_reversible()
2529                .expect("a terminal reserved or fragment capture is deterministic");
2530            let mut values = TemplateValues::new();
2531            values.insert("value".to_owned(), TemplateValue::scalar(raw_value));
2532
2533            let uri = matcher
2534                .expand(&values)
2535                .expect("raw reserved characters expand without pre-escaping");
2536            assert_eq!(uri, expected_uri);
2537            assert_eq!(
2538                matcher
2539                    .match_uri(&uri)
2540                    .expect("reverse matching is bounded"),
2541                Some(values),
2542                "the raw scalar round-trips through the {template} inverse"
2543            );
2544        }
2545    }
2546
2547    #[test]
2548    fn reversible_resource_template_reserved_and_fragment_preescaped_scalars_reject_without_mutation()
2549     {
2550        for template in ["mcp://resource/{+value}", "mcp://resource{#value}"] {
2551            let matcher = UriTemplate::parse(template)
2552                .expect("the reserved or fragment template parses")
2553                .compile_reversible()
2554                .expect("a terminal reserved or fragment capture is deterministic");
2555            let matcher_before = matcher.clone();
2556            let mut values = TemplateValues::new();
2557            values.insert("value".to_owned(), TemplateValue::scalar("docs%2Fguide"));
2558            let values_before = values.clone();
2559
2560            assert_eq!(
2561                matcher.expand(&values),
2562                Err(UriTemplateError::PreescapedReservedMatchValue {
2563                    variable: "value".to_owned(),
2564                }),
2565                "a preescaped scalar is ambiguous for the {template} inverse"
2566            );
2567            assert_eq!(
2568                matcher, matcher_before,
2569                "rejected expansion changes no matcher state"
2570            );
2571            assert_eq!(
2572                values, values_before,
2573                "rejected expansion changes no caller values"
2574            );
2575        }
2576    }
2577
2578    #[test]
2579    fn reversible_resource_template_scalar_explode_operator_matrix_round_trips() {
2580        let cases = [
2581            ("mcp://resource/{value*}", "mcp://resource/one%2Ftwo"),
2582            ("mcp://resource/{+value*}", "mcp://resource/one/two"),
2583            ("mcp://resource{#value*}", "mcp://resource#one/two"),
2584            ("mcp://resource{.value*}", "mcp://resource.one%2Ftwo"),
2585            ("mcp://resource{/value*}", "mcp://resource/one%2Ftwo"),
2586            ("mcp://resource{;value*}", "mcp://resource;value=one%2Ftwo"),
2587            ("mcp://resource{?value*}", "mcp://resource?value=one%2Ftwo"),
2588            (
2589                "mcp://resource?fixed=true{&value*}",
2590                "mcp://resource?fixed=true&value=one%2Ftwo",
2591            ),
2592        ];
2593
2594        for (source, expected_uri) in cases {
2595            let matcher = UriTemplate::parse(source)
2596                .expect("each RFC 6570 operator parses")
2597                .compile_reversible()
2598                .expect("an exploded scalar has the same reversible wire form as a scalar");
2599            let values =
2600                TemplateValues::from([("value".to_owned(), TemplateValue::scalar("one/two"))]);
2601            let uri = matcher
2602                .expand(&values)
2603                .expect("the admitted scalar values expand");
2604            assert_eq!(uri, expected_uri, "{source}");
2605            assert_eq!(
2606                matcher.match_uri(&uri).expect("matching is bounded"),
2607                Some(values),
2608                "{source} extracts the same scalar binding it expanded"
2609            );
2610        }
2611    }
2612
2613    #[test]
2614    fn reversible_resource_template_named_multi_variable_expression_round_trips_omission() {
2615        let matcher = UriTemplate::parse("mcp://resource{?first*,second*}")
2616            .expect("the named Level 4 expression parses")
2617            .compile_reversible()
2618            .expect("wire names make named scalar omissions reversible");
2619        let cases = [
2620            (TemplateValues::new(), "mcp://resource"),
2621            (
2622                TemplateValues::from([("first".to_owned(), TemplateValue::scalar("one/two"))]),
2623                "mcp://resource?first=one%2Ftwo",
2624            ),
2625            (
2626                TemplateValues::from([("second".to_owned(), TemplateValue::scalar("three"))]),
2627                "mcp://resource?second=three",
2628            ),
2629            (
2630                TemplateValues::from([
2631                    ("first".to_owned(), TemplateValue::scalar("one/two")),
2632                    ("second".to_owned(), TemplateValue::scalar("three")),
2633                ]),
2634                "mcp://resource?first=one%2Ftwo&second=three",
2635            ),
2636        ];
2637
2638        for (values, expected_uri) in cases {
2639            let uri = matcher.expand(&values).expect("named scalar values expand");
2640            assert_eq!(uri, expected_uri);
2641            assert_eq!(
2642                matcher.match_uri(&uri).expect("matching is bounded"),
2643                Some(values),
2644                "the names distinguish omitted variables from present ones"
2645            );
2646        }
2647    }
2648
2649    #[test]
2650    fn reversible_resource_template_named_multi_variable_stops_at_next_expression() {
2651        let matcher = UriTemplate::parse("mcp://resource{?first*,second*}{#fragment*}")
2652            .expect("the adjacent named and fragment expressions parse")
2653            .compile_reversible()
2654            .expect("their distinct wire markers make the boundary reversible");
2655        let values = TemplateValues::from([
2656            ("first".to_owned(), TemplateValue::scalar("one")),
2657            ("second".to_owned(), TemplateValue::scalar("two")),
2658            ("fragment".to_owned(), TemplateValue::scalar("three/four")),
2659        ]);
2660        let uri = matcher
2661            .expand(&values)
2662            .expect("the named values and following fragment expand");
2663        assert_eq!(uri, "mcp://resource?first=one&second=two#three/four");
2664        assert_eq!(
2665            matcher.match_uri(&uri).expect("matching is bounded"),
2666            Some(values),
2667            "the query capture stops before the following fragment marker"
2668        );
2669    }
2670
2671    #[test]
2672    fn reversible_resource_template_near_negative_rejects_lossy_or_composite_without_mutation() {
2673        let accepted = "mcp://resource{/collection}/manifest{?revision}";
2674        let planted_lossy = "mcp://resource{/collection:3}/manifest{?revision}";
2675        let matcher = UriTemplate::parse(accepted)
2676            .expect("positive control parses")
2677            .compile_reversible()
2678            .expect("positive control compiles");
2679        let matcher_before = matcher.clone();
2680        let mut values = TemplateValues::new();
2681        values.insert("collection".to_owned(), TemplateValue::scalar("books"));
2682        values.insert("revision".to_owned(), TemplateValue::scalar("1"));
2683        let values_before = values.clone();
2684
2685        let error = UriTemplate::parse(planted_lossy)
2686            .expect("changing only the scalar modifier remains valid RFC 6570")
2687            .compile_reversible()
2688            .expect_err("a lossy prefix has no deterministic reverse match");
2689        assert_eq!(
2690            error,
2691            UriTemplateError::NonReversibleTemplate {
2692                reason: UriTemplateMatchRejection::LossyPrefix {
2693                    variable: "collection".to_owned(),
2694                },
2695            }
2696        );
2697        assert_eq!(
2698            matcher, matcher_before,
2699            "rejected compilation changes no matcher state"
2700        );
2701        assert_eq!(
2702            values, values_before,
2703            "rejected compilation changes no caller values"
2704        );
2705
2706        values.insert(
2707            "collection".to_owned(),
2708            TemplateValue::list(vec!["books".to_owned()]),
2709        );
2710        let planted_values_before = values.clone();
2711        assert_eq!(
2712            matcher.expand(&values),
2713            Err(UriTemplateError::NonScalarMatchValue {
2714                variable: "collection".to_owned(),
2715            }),
2716            "changing only the declared scalar into a composite must reject"
2717        );
2718        assert_eq!(
2719            values, planted_values_before,
2720            "rejected composite expansion leaves caller values unchanged"
2721        );
2722    }
2723
2724    #[test]
2725    fn reversible_resource_template_match_input_bound_rejects_before_capture() {
2726        let matcher = UriTemplate::parse("mcp://resource/{value*}")
2727            .expect("the scalar Level 4 template parses")
2728            .compile_reversible()
2729            .expect("the scalar Level 4 template compiles");
2730        let matcher_before = matcher.clone();
2731        let prefix = "mcp://resource/";
2732        let oversized_uri = format!(
2733            "{prefix}{}",
2734            "x".repeat(MAX_URI_TEMPLATE_MATCH_INPUT_BYTES - prefix.len() + 1)
2735        );
2736
2737        assert_eq!(
2738            matcher.match_uri(&oversized_uri),
2739            Err(UriTemplateError::MatchInputTooLong {
2740                actual: MAX_URI_TEMPLATE_MATCH_INPUT_BYTES + 1,
2741                maximum: MAX_URI_TEMPLATE_MATCH_INPUT_BYTES,
2742            })
2743        );
2744        assert_eq!(
2745            matcher, matcher_before,
2746            "a rejected candidate cannot mutate the compiled matcher"
2747        );
2748    }
2749
2750    #[test]
2751    fn reversible_resource_template_adjacent_path_query_round_trips_exactly() {
2752        let accepted = "mcp://resource{/collection}{?revision}";
2753        let planted_ambiguous = "mcp://resource{/collection}{/revision}";
2754        let matcher = UriTemplate::parse(accepted)
2755            .expect("the positive control parses")
2756            .compile_reversible()
2757            .expect("a path capture is separable from the following query marker");
2758        let matcher_before = matcher.clone();
2759        let mut values = TemplateValues::new();
2760        values.insert(
2761            "collection".to_owned(),
2762            TemplateValue::scalar("books/fiction"),
2763        );
2764        values.insert("revision".to_owned(), TemplateValue::scalar("2026-08-09"));
2765        let values_before = values.clone();
2766
2767        let uri = matcher
2768            .expand(&values)
2769            .expect("the separated scalar values expand");
2770        assert_eq!(uri, "mcp://resource/books%2Ffiction?revision=2026-08-09");
2771        let round_tripped = matcher
2772            .match_uri(&uri)
2773            .expect("reverse matching is bounded")
2774            .expect("the exact expansion belongs to the matcher language");
2775        assert_eq!(round_tripped, values);
2776        assert_eq!(
2777            matcher
2778                .expand(&round_tripped)
2779                .expect("the recovered bindings re-expand"),
2780            uri,
2781            "a reversible match must preserve the exact wire URI"
2782        );
2783
2784        let mut path_only = TemplateValues::new();
2785        path_only.insert("collection".to_owned(), TemplateValue::scalar("books"));
2786        let path_only_uri = matcher
2787            .expand(&path_only)
2788            .expect("the query expression may be absent");
2789        assert_eq!(path_only_uri, "mcp://resource/books");
2790        assert_eq!(
2791            matcher
2792                .match_uri(&path_only_uri)
2793                .expect("matching an absent following expression is bounded"),
2794            Some(path_only),
2795            "the first capture remains exact when the query marker is absent"
2796        );
2797
2798        let error = UriTemplate::parse(planted_ambiguous)
2799            .expect(
2800                "changing only the following query marker to a path marker remains valid RFC 6570",
2801            )
2802            .compile_reversible()
2803            .expect_err("identical path markers cannot distinguish an omitted adjacent scalar");
2804        assert_eq!(
2805            error,
2806            UriTemplateError::NonReversibleTemplate {
2807                reason: UriTemplateMatchRejection::AmbiguousBoundary,
2808            }
2809        );
2810        assert_eq!(
2811            matcher, matcher_before,
2812            "rejected adjacent syntax changes no admitted matcher state"
2813        );
2814        assert_eq!(
2815            values, values_before,
2816            "rejected adjacent syntax changes no caller bindings"
2817        );
2818    }
2819
2820    #[test]
2821    fn reversible_resource_template_rejects_adjacent_captures() {
2822        let template = UriTemplate::parse("mcp://resource/{first}{second}")
2823            .expect("the syntactically valid template parses");
2824        assert_eq!(
2825            template.compile_reversible(),
2826            Err(UriTemplateError::NonReversibleTemplate {
2827                reason: UriTemplateMatchRejection::AdjacentCaptures,
2828            }),
2829            "adjacent captures admit multiple splits and cannot become a handler matcher"
2830        );
2831    }
2832}