Skip to main content

rama_net/uri/
path_matcher.rs

1//! Infallible path-pattern matching.
2//!
3//! [`PathPattern`] compiles a small brace-based glob/capture syntax and matches
4//! it against a [`PathRef`](super::PathRef) segment-by-segment, decode-aware and
5//! (by default) case-sensitive. Compilation never fails: anything that isn't a
6//! recognized brace token is treated as a literal. The only metacharacters are
7//! `{`, `}` and `?` — none of which is a valid unencoded URI path byte — so
8//! `*`, `:`, `.`, `+` etc. are all literals. See [`PathPattern`] for the full
9//! syntax.
10
11use core::{
12    cmp::Reverse,
13    fmt,
14    hash::{Hash, Hasher},
15};
16
17use crate::std::{
18    borrow::{Cow, ToOwned},
19    boxed::Box,
20    string::String,
21    vec,
22    vec::Vec,
23};
24
25use super::component_input::IntoUriComponent;
26use super::path::{PathMatchOptions, PathRef, byte_starts_with, maybe_decode, strip_leading_slash};
27use crate::byte_sets::is_pattern_name_byte;
28use crate::input_ext::PathInputExt;
29
30use rama_core::{
31    Service,
32    extensions::{Extension, ExtensionsRef},
33};
34use rama_utils::collections::smallvec::SmallVec;
35
36type EncodedSegment<'a> = Cow<'a, str>;
37
38/// A compiled path pattern.
39///
40/// Construct via [`PathPattern::new`] / [`new_with_opts`](Self::new_with_opts)
41/// and test paths with [`is_match`](Self::is_match) / [`captures`](Self::captures).
42///
43/// # Syntax
44///
45/// A pattern is split on `/` into segments. The only metacharacters are `{`,
46/// `}` and `?`; everything else (`*`, `:`, `.`, `+`, …) is a literal. Within a
47/// segment:
48///
49/// - **literal** text must equal the (decoded) path segment value;
50/// - `{name}` captures a non-empty run under `name`: a whole segment when alone
51///   (`{id}`), or the run bounded by surrounding literals when affixed
52///   (`{pkg}.json` captures the part before `.json`, `v{ver}-rc` the part
53///   between);
54/// - `{}` is an anonymous non-empty wildcard run, not captured (`{}.txt`);
55/// - `?` makes the immediately preceding element optional (zero-or-one):
56///   `a?` is an optional `a`, `{}?` an optional run, `{name}?` an optional
57///   capture, and a trailing `/?` an optional trailing slash. A whole segment
58///   made only of `{name}?` or `{}?` is itself optional, so `/foo/{name}?/bar`
59///   matches `/foo/john/bar`, `/foo//bar`, and `/foo/bar`;
60/// - `{*}`, as a *whole* segment, is an anonymous catch-all matching one or
61///   more path segments, available '/'-joined and decoded via
62///   [`PathCaptures::glob`]. It may appear in the middle of a pattern;
63/// - `{*name}`, as a *whole* segment, is the **named** catch-all: same 1+
64///   segment match as `{*}`, but the run is recorded under `name` (read back,
65///   '/'-joined and decoded, via [`PathCaptures::get`]). So `{name}` stays
66///   within a segment; `{*name}` spans segments.
67///
68/// An unclosed `{`, or a brace group whose body isn't a valid token, is taken
69/// literally. `{*}`/`{*name}` are catch-alls only as a *whole* segment.
70///
71/// Trailing slash is explicit: `/a` matches only `/a`, `/a/` matches only
72/// `/a/`, and `/a/?` matches both.
73///
74/// ```
75/// use rama_net::uri::{PathPattern, PathRef};
76///
77/// let pat = PathPattern::new("/p2/{vendor}/{pkg}.json");
78/// let caps = pat.captures(PathRef::from_raw_str("/p2/acme/widget.json")).unwrap();
79/// assert_eq!(caps.get("vendor"), Some("acme"));
80/// assert_eq!(caps.get("pkg"), Some("widget"));
81/// assert!(pat.captures(PathRef::from_raw_str("/p2/acme/widget.txt")).is_none());
82///
83/// let assets = PathPattern::new("/assets/{*}");
84/// assert!(assets.is_match(PathRef::from_raw_str("/assets/css/app.css")));
85/// assert!(!assets.is_match(PathRef::from_raw_str("/assets")));
86///
87/// // `{*name}` is the named catch-all (read back via `get`).
88/// let files = PathPattern::new("/files/{*rest}");
89/// let caps = files.captures(PathRef::from_raw_str("/files/a/b/c.txt")).unwrap();
90/// assert_eq!(caps.get("rest"), Some("a/b/c.txt"));
91/// ```
92#[derive(Debug, Clone)]
93pub struct PathPattern {
94    segments: Vec<PatternSegment>,
95    /// Capture names are appended here at compile time; [`Element`] capture
96    /// kinds index into it. Owning the names here is what lets
97    /// [`PathCaptures`] borrow them for `'a`.
98    name_bytes: Vec<u8>,
99    trailing: TrailingSlash,
100    opts: PathMatchOptions,
101    /// `true` when no segment binds a name and there is no catch-all — the
102    /// alloc-free [`is_match`](PathPattern::is_match) fast path applies.
103    capture_free: bool,
104    /// `true` for a prefix matcher ([`new_prefix`](PathPattern::new_prefix)):
105    /// the pattern must match a *leading* run of the path's segments, and any
106    /// trailing segments and trailing-slash are ignored.
107    prefix: bool,
108}
109
110/// Coarse classification of a compiled [`PathPattern`] segment, exposed via
111/// [`PathPattern::segment_kinds`] so callers (e.g. a router) can reason about
112/// route specificity without re-parsing the pattern syntax themselves.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
114pub enum PathPatternSegmentKind {
115    /// A fixed string: the segment matches exactly one literal value.
116    Literal,
117    /// Within-segment dynamic (a capture/wildcard or optional element) still
118    /// bound to exactly one path segment.
119    Dynamic,
120    /// A whole-segment catch-all (`{*}` / `{*name}`) spanning 1+ segments.
121    CatchAll,
122}
123
124/// Specificity metadata for one compiled [`PathPattern`] segment.
125///
126/// This lets callers rank overlapping patterns without re-parsing the pattern
127/// syntax. The broad [`kind`](Self::kind) preserves the usual ordering
128/// (literal > dynamic > catch-all), while the counters let a router break ties
129/// between dynamic segments such as `{name}` and `{name}.json`.
130#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
131pub struct PathPatternSegmentSpecificity {
132    /// Coarse segment kind.
133    pub kind: PathPatternSegmentKind,
134    /// Number of fixed literal bytes inside the segment.
135    pub literal_bytes: usize,
136    /// Number of wildcard/capture runs inside the segment.
137    pub dynamic_parts: usize,
138    /// Number of optional elements inside the segment.
139    pub optional_parts: usize,
140}
141
142/// Policy for a path's trailing slash, derived from the pattern's own
143/// trailing form (explicit, never inferred).
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
145enum TrailingSlash {
146    /// Pattern has no trailing `/`: the path must not either.
147    Forbidden,
148    /// Pattern ends in `/`: the path must too.
149    Required,
150    /// Pattern ends in `/?`: both forms accepted.
151    Optional,
152}
153
154impl TrailingSlash {
155    /// Does this policy accept a path that does (`true`) / doesn't (`false`)
156    /// carry a trailing slash?
157    fn accepts(self, path_has_slash: bool) -> bool {
158        match self {
159            Self::Forbidden => !path_has_slash,
160            Self::Required => path_has_slash,
161            Self::Optional => true,
162        }
163    }
164}
165
166/// One `/`-delimited unit of a compiled pattern.
167#[derive(Debug, Clone)]
168enum PatternSegment {
169    /// `{*}` — matches one or more whole path segments (anonymous; read back
170    /// via [`PathCaptures::glob`]).
171    CatchAll,
172    /// `{*name}` — like [`CatchAll`](Self::CatchAll) but records the matched,
173    /// '/'-joined run under `name` (read back via [`PathCaptures::get`]).
174    NamedCatchAll { name_start: usize, name_len: usize },
175    /// A sequence of within-segment elements matched against a single path
176    /// segment via greedy backtracking. Inline-sized for the common one- or
177    /// two-element segment (a bare literal, capture, or `{pkg}.ext` pair).
178    Normal {
179        elems: SmallVec<[Element; 2]>,
180        /// Count of *ambiguity sources* in `elems`: wildcard runs
181        /// (`Star`/`Capture`) plus `optional` elements. Each is a backtrack
182        /// point; with two or more of them the greedy recursion can revisit
183        /// the same `(element, hay)` state exponentially, so we memoize
184        /// failures. With at most one source the recursion is linear, so we
185        /// skip the memo (and its allocation) entirely. Precomputed here so
186        /// the hot path doesn't rescan the element list per match.
187        ambiguity: usize,
188    },
189}
190
191/// A within-segment matching element.
192#[derive(Debug, Clone)]
193struct Element {
194    kind: ElementKind,
195    /// `?` suffix: the element may match zero occurrences.
196    optional: bool,
197}
198
199#[derive(Debug, Clone)]
200enum ElementKind {
201    /// Literal bytes, compared against the decoded path-segment bytes. Boxed:
202    /// fixed after compilation, so the `Vec` capacity word is dead weight.
203    Literal(Box<[u8]>),
204    /// Anonymous wildcard run (`{}`, 1+ chars within the segment).
205    Star,
206    /// Named wildcard run that records what it matched. The name is the
207    /// `name_bytes[start..start+len]` slice of the owning [`PathPattern`].
208    Capture { name_start: usize, name_len: usize },
209}
210
211impl PartialEq for PathPattern {
212    fn eq(&self, other: &Self) -> bool {
213        self.trailing == other.trailing
214            && self.opts == other.opts
215            && self.prefix == other.prefix
216            && self.segments.len() == other.segments.len()
217            && self.segments.iter().zip(&other.segments).all(|(a, b)| {
218                pattern_segments_eq(
219                    a,
220                    &self.name_bytes,
221                    b,
222                    &other.name_bytes,
223                    self.opts.ignore_ascii_case,
224                )
225            })
226    }
227}
228
229impl Eq for PathPattern {}
230
231impl Hash for PathPattern {
232    fn hash<H: Hasher>(&self, state: &mut H) {
233        self.trailing.hash(state);
234        self.opts.hash(state);
235        self.prefix.hash(state);
236        self.segments.len().hash(state);
237        for segment in &self.segments {
238            hash_pattern_segment(
239                segment,
240                &self.name_bytes,
241                self.opts.ignore_ascii_case,
242                state,
243            );
244        }
245    }
246}
247
248fn pattern_segments_eq(
249    a: &PatternSegment,
250    a_names: &[u8],
251    b: &PatternSegment,
252    b_names: &[u8],
253    ignore_ascii_case: bool,
254) -> bool {
255    match (a, b) {
256        (PatternSegment::CatchAll, PatternSegment::CatchAll) => true,
257        (
258            PatternSegment::NamedCatchAll {
259                name_start: a_start,
260                name_len: a_len,
261            },
262            PatternSegment::NamedCatchAll {
263                name_start: b_start,
264                name_len: b_len,
265            },
266        ) => {
267            let a = &a_names[*a_start..*a_start + *a_len];
268            let b = &b_names[*b_start..*b_start + *b_len];
269            a == b
270        }
271        (
272            PatternSegment::Normal {
273                elems: a_elems,
274                ambiguity: a_ambiguity,
275            },
276            PatternSegment::Normal {
277                elems: b_elems,
278                ambiguity: b_ambiguity,
279            },
280        ) => {
281            a_ambiguity == b_ambiguity
282                && a_elems.len() == b_elems.len()
283                && a_elems
284                    .iter()
285                    .zip(b_elems)
286                    .all(|(a, b)| elements_eq(a, a_names, b, b_names, ignore_ascii_case))
287        }
288        _ => false,
289    }
290}
291
292fn elements_eq(
293    a: &Element,
294    a_names: &[u8],
295    b: &Element,
296    b_names: &[u8],
297    ignore_ascii_case: bool,
298) -> bool {
299    a.optional == b.optional
300        && element_kinds_eq(&a.kind, a_names, &b.kind, b_names, ignore_ascii_case)
301}
302
303fn element_kinds_eq(
304    a: &ElementKind,
305    a_names: &[u8],
306    b: &ElementKind,
307    b_names: &[u8],
308    ignore_ascii_case: bool,
309) -> bool {
310    match (a, b) {
311        (ElementKind::Literal(a), ElementKind::Literal(b)) => literal_eq(a, b, ignore_ascii_case),
312        (ElementKind::Star, ElementKind::Star) => true,
313        (
314            ElementKind::Capture {
315                name_start: a_start,
316                name_len: a_len,
317            },
318            ElementKind::Capture {
319                name_start: b_start,
320                name_len: b_len,
321            },
322        ) => {
323            let a = &a_names[*a_start..*a_start + *a_len];
324            let b = &b_names[*b_start..*b_start + *b_len];
325            a == b
326        }
327        _ => false,
328    }
329}
330
331fn literal_eq(a: &[u8], b: &[u8], ignore_ascii_case: bool) -> bool {
332    if ignore_ascii_case {
333        a.eq_ignore_ascii_case(b)
334    } else {
335        a == b
336    }
337}
338
339fn hash_pattern_segment<H: Hasher>(
340    segment: &PatternSegment,
341    names: &[u8],
342    ignore_ascii_case: bool,
343    state: &mut H,
344) {
345    match segment {
346        PatternSegment::CatchAll => 0u8.hash(state),
347        PatternSegment::NamedCatchAll {
348            name_start,
349            name_len,
350        } => {
351            1u8.hash(state);
352            names[*name_start..*name_start + *name_len].hash(state);
353        }
354        PatternSegment::Normal { elems, ambiguity } => {
355            2u8.hash(state);
356            ambiguity.hash(state);
357            elems.len().hash(state);
358            for element in elems {
359                hash_element(element, names, ignore_ascii_case, state);
360            }
361        }
362    }
363}
364
365fn hash_element<H: Hasher>(
366    element: &Element,
367    names: &[u8],
368    ignore_ascii_case: bool,
369    state: &mut H,
370) {
371    element.optional.hash(state);
372    match &element.kind {
373        ElementKind::Literal(literal) => {
374            0u8.hash(state);
375            hash_literal(literal, ignore_ascii_case, state);
376        }
377        ElementKind::Star => 1u8.hash(state),
378        ElementKind::Capture {
379            name_start,
380            name_len,
381        } => {
382            2u8.hash(state);
383            names[*name_start..*name_start + *name_len].hash(state);
384        }
385    }
386}
387
388fn hash_literal<H: Hasher>(literal: &[u8], ignore_ascii_case: bool, state: &mut H) {
389    if ignore_ascii_case {
390        literal.len().hash(state);
391        for byte in literal {
392            byte.to_ascii_lowercase().hash(state);
393        }
394    } else {
395        literal.hash(state);
396    }
397}
398
399fn pattern_segment_capture_free(segment: &PatternSegment) -> bool {
400    match segment {
401        PatternSegment::CatchAll | PatternSegment::NamedCatchAll { .. } => false,
402        PatternSegment::Normal { elems, .. } => elems
403            .iter()
404            .all(|element| !matches!(element.kind, ElementKind::Capture { .. })),
405    }
406}
407
408/// A typed prefix router for URI paths.
409///
410/// Routes are compiled as [`PathPattern`] prefix matchers and looked up directly
411/// against [`PathRef`], so matching does not require callers to lower-case,
412/// trim, split, or allocate string lookup keys on the request path.
413#[derive(Debug, Clone)]
414pub struct PathRouter<T> {
415    root: PathRouteNode<T>,
416    len: usize,
417}
418
419#[derive(Debug, Clone)]
420struct PathRouteNode<T> {
421    routes: Vec<PathRoute<T>>,
422    children: Vec<PathRouteEdge<T>>,
423}
424
425#[derive(Debug, Clone)]
426struct PathRouteEdge<T> {
427    segment: PatternSegment,
428    name_bytes: Vec<u8>,
429    opts: PathMatchOptions,
430    rank: PathRouterSegmentRank,
431    child: Box<PathRouteNode<T>>,
432}
433
434#[derive(Debug, Clone)]
435struct PathRoute<T> {
436    pattern: PathPattern,
437    specificity: Box<[PathRouterSegmentRank]>,
438    has_captures: bool,
439    value: T,
440}
441
442#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
443struct PathRouterSegmentRank {
444    kind: u8,
445    literal_bytes: usize,
446    fewer_dynamic_parts: Reverse<usize>,
447    fewer_optional_parts: Reverse<usize>,
448}
449
450/// Result of a successful [`PathRouter::match_prefix`] lookup.
451#[derive(Debug)]
452pub struct PathRouteMatch<'a, 'p, T> {
453    value: &'a T,
454    matched_segment_count: usize,
455    captures: PathCaptures<'a, 'p>,
456}
457
458/// Decoded captures inserted by [`PathRouter`]'s [`Service`] implementation.
459#[derive(Debug, Clone, Default, Extension)]
460#[extension(tags(net))]
461pub struct PathRouteCaptures {
462    params: SmallVec<[(String, String); 4]>,
463    glob: Option<String>,
464}
465
466/// Error produced by [`PathRouter`] when used as a [`Service`].
467#[derive(Debug, Clone, Copy, PartialEq, Eq)]
468pub enum PathRouterError<E> {
469    /// No registered path matched the input.
470    NotFound,
471    /// The matched service failed.
472    Inner(E),
473}
474
475impl<E> fmt::Display for PathRouterError<E>
476where
477    E: fmt::Display,
478{
479    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
480        match self {
481            Self::NotFound => f.write_str("no path route matched input"),
482            Self::Inner(err) => err.fmt(f),
483        }
484    }
485}
486
487impl<E> core::error::Error for PathRouterError<E> where E: core::error::Error + 'static {}
488
489impl<T> Default for PathRouteNode<T> {
490    fn default() -> Self {
491        Self {
492            routes: Vec::new(),
493            children: Vec::new(),
494        }
495    }
496}
497
498impl<T> Default for PathRouter<T> {
499    fn default() -> Self {
500        Self::new()
501    }
502}
503
504impl<T> PathRouter<T> {
505    /// Create an empty path router.
506    #[must_use]
507    pub fn new() -> Self {
508        Self {
509            root: PathRouteNode::default(),
510            len: 0,
511        }
512    }
513
514    /// Returns `true` when no routes are registered.
515    #[must_use]
516    pub fn is_empty(&self) -> bool {
517        self.len == 0
518    }
519
520    /// Number of registered routes.
521    #[must_use]
522    pub fn len(&self) -> usize {
523        self.len
524    }
525
526    /// Insert a prefix route using default [`PathMatchOptions`].
527    ///
528    /// If `pattern` ends in a whole-segment catch-all (`{*}` / `{*name}`), the
529    /// catch-all is treated as "the nested target owns the remainder" and is
530    /// not counted as part of the matched prefix.
531    pub fn insert_prefix(&mut self, pattern: impl IntoUriComponent, value: T) -> Option<T> {
532        self.insert_prefix_with_opts(pattern, PathMatchOptions::default(), value)
533    }
534
535    /// Insert a prefix route using explicit [`PathMatchOptions`].
536    ///
537    /// Re-inserting an equivalent compiled pattern replaces the stored value.
538    pub fn insert_prefix_with_opts(
539        &mut self,
540        pattern: impl IntoUriComponent,
541        opts: PathMatchOptions,
542        value: T,
543    ) -> Option<T> {
544        let mut pattern = PathPattern::new_prefix_with_opts(pattern, opts);
545        pattern.drop_trailing_catch_all();
546        let has_captures = !pattern.capture_free;
547        let specificity = path_router_specificity(&pattern).into_boxed_slice();
548
549        let mut node = &mut self.root;
550        for (segment, rank) in pattern.segments.iter().zip(specificity.iter().copied()) {
551            let child_idx = if let Some(idx) = node.children.iter().position(|edge| {
552                edge.opts == pattern.opts
553                    && pattern_segments_eq(
554                        &edge.segment,
555                        &edge.name_bytes,
556                        segment,
557                        &pattern.name_bytes,
558                        pattern.opts.ignore_ascii_case,
559                    )
560            }) {
561                idx
562            } else {
563                let idx = node.children.partition_point(|edge| edge.rank >= rank);
564                let (segment, name_bytes) =
565                    clone_pattern_segment_with_local_names(segment, &pattern.name_bytes);
566                node.children.insert(
567                    idx,
568                    PathRouteEdge {
569                        segment,
570                        name_bytes,
571                        opts: pattern.opts,
572                        rank,
573                        child: Box::default(),
574                    },
575                );
576                idx
577            };
578            node = &mut node.children[child_idx].child;
579        }
580
581        if let Some(route) = node
582            .routes
583            .iter_mut()
584            .find(|route| route.pattern == pattern)
585        {
586            return Some(core::mem::replace(&mut route.value, value));
587        }
588
589        let pos = node
590            .routes
591            .partition_point(|route| route.specificity.as_ref() >= specificity.as_ref());
592        node.routes.insert(
593            pos,
594            PathRoute {
595                pattern,
596                specificity,
597                has_captures,
598                value,
599            },
600        );
601        self.len += 1;
602        None
603    }
604
605    /// Match `path` against the most specific registered prefix.
606    #[must_use]
607    pub fn match_prefix<'a, 'p>(&'a self, path: PathRef<'p>) -> Option<PathRouteMatch<'a, 'p, T>> {
608        let segments: SmallVec<[EncodedSegment<'p>; 8]> = path
609            .segments()
610            .map(|segment| segment.as_encoded_str())
611            .collect();
612        let segments = prefix_content_segments(&segments);
613        let matched = self.root.match_prefix(segments, 0)?;
614        let captures = if matched.route.has_captures {
615            matched.route.pattern.captures(path)?
616        } else {
617            PathCaptures::empty(&matched.route.pattern.name_bytes)
618        };
619        Some(PathRouteMatch {
620            value: &matched.route.value,
621            matched_segment_count: matched.consumed,
622            captures,
623        })
624    }
625
626    /// Match `path` against the most specific registered route only when the
627    /// route covers the complete path.
628    #[must_use]
629    pub fn match_exact<'a, 'p>(&'a self, path: PathRef<'p>) -> Option<PathRouteMatch<'a, 'p, T>> {
630        let segments: SmallVec<[EncodedSegment<'p>; 8]> = path
631            .segments()
632            .map(|segment| segment.as_encoded_str())
633            .collect();
634        let segments = prefix_content_segments(&segments);
635        let matched = self.root.match_prefix(segments, 0)?;
636        let captures = matched.route.pattern.captures_exact(path)?;
637        Some(PathRouteMatch {
638            value: &matched.route.value,
639            matched_segment_count: matched.consumed,
640            captures,
641        })
642    }
643}
644
645impl<Input, T> Service<Input> for PathRouter<T>
646where
647    Input: ExtensionsRef + PathInputExt + Send + 'static,
648    T: Service<Input>,
649{
650    type Output = T::Output;
651    type Error = PathRouterError<T::Error>;
652
653    async fn serve(&self, input: Input) -> Result<Self::Output, Self::Error> {
654        let Some((service, captures)) = self.match_service(input.path_ref()) else {
655            return Err(PathRouterError::NotFound);
656        };
657        if !captures.is_empty() {
658            input.extensions().insert(captures);
659        }
660        service.serve(input).await.map_err(PathRouterError::Inner)
661    }
662}
663
664impl<T> PathRouter<T> {
665    fn match_service<'a>(&'a self, path: PathRef<'_>) -> Option<(&'a T, PathRouteCaptures)> {
666        let matched = self.match_prefix(path)?;
667        let captures = PathRouteCaptures::from_captures(matched.captures());
668        Some((matched.value(), captures))
669    }
670}
671
672#[derive(Debug, Clone, Copy)]
673struct PathRouteCandidate<'a, T> {
674    route: &'a PathRoute<T>,
675    consumed: usize,
676    skipped_optional_segments: usize,
677}
678
679impl<T> PathRouteNode<T> {
680    fn match_prefix<'a>(
681        &'a self,
682        segments: &[EncodedSegment<'_>],
683        index: usize,
684    ) -> Option<PathRouteCandidate<'a, T>> {
685        let mut best = self.routes.first().map(|route| PathRouteCandidate {
686            route,
687            consumed: index,
688            skipped_optional_segments: 0,
689        });
690
691        for edge in &self.children {
692            match &edge.segment {
693                PatternSegment::Normal { elems, ambiguity } => {
694                    if let Some(segment) = segments.get(index) {
695                        let mut sink = Sink::Ignore;
696                        if match_segment(
697                            elems,
698                            *ambiguity,
699                            segment.as_ref().as_bytes(),
700                            edge.opts,
701                            &mut sink,
702                        ) && let Some(candidate) = edge.child.match_prefix(segments, index + 1)
703                        {
704                            best = best_route(best, candidate);
705                        }
706                    }
707                    if optional_whole_segment_binding(elems).is_some()
708                        && let Some(mut candidate) = edge.child.match_prefix(segments, index)
709                    {
710                        candidate.skipped_optional_segments += 1;
711                        best = best_route(best, candidate);
712                    }
713                }
714                PatternSegment::CatchAll | PatternSegment::NamedCatchAll { .. } => {
715                    for next in index + 1..=segments.len() {
716                        if let Some(candidate) = edge.child.match_prefix(segments, next) {
717                            best = best_route(best, candidate);
718                        }
719                    }
720                }
721            }
722        }
723
724        best
725    }
726}
727
728fn best_route<'a, T>(
729    current: Option<PathRouteCandidate<'a, T>>,
730    candidate: PathRouteCandidate<'a, T>,
731) -> Option<PathRouteCandidate<'a, T>> {
732    let Some(current) = current else {
733        return Some(candidate);
734    };
735    if candidate.consumed > current.consumed
736        || (candidate.consumed == current.consumed
737            && (candidate.skipped_optional_segments < current.skipped_optional_segments
738                || (candidate.skipped_optional_segments == current.skipped_optional_segments
739                    && candidate.route.specificity.as_ref() > current.route.specificity.as_ref())))
740    {
741        Some(candidate)
742    } else {
743        Some(current)
744    }
745}
746
747#[cfg(test)]
748mod path_router_candidate_tests {
749    use super::*;
750
751    fn test_route(pattern: &str, value: &'static str) -> PathRoute<&'static str> {
752        let pattern = PathPattern::new_prefix(pattern);
753        let specificity = path_router_specificity(&pattern).into_boxed_slice();
754        PathRoute {
755            pattern,
756            specificity,
757            has_captures: false,
758            value,
759        }
760    }
761
762    #[test]
763    fn best_route_prefers_fewer_skipped_optional_segments_when_consumed_ties() {
764        let skipped = test_route("/root/{name}?", "skipped");
765        let direct = test_route("/root", "direct");
766
767        let best = best_route(
768            Some(PathRouteCandidate {
769                route: &skipped,
770                consumed: 1,
771                skipped_optional_segments: 1,
772            }),
773            PathRouteCandidate {
774                route: &direct,
775                consumed: 1,
776                skipped_optional_segments: 0,
777            },
778        )
779        .unwrap();
780
781        assert_eq!(best.route.value, "direct");
782    }
783
784    #[test]
785    fn best_route_uses_specificity_after_consumed_and_skipped_tie() {
786        let dynamic = test_route("/{tenant}/settings", "dynamic");
787        let literal = test_route("/acme/{section}", "literal");
788
789        let best = best_route(
790            Some(PathRouteCandidate {
791                route: &dynamic,
792                consumed: 2,
793                skipped_optional_segments: 0,
794            }),
795            PathRouteCandidate {
796                route: &literal,
797                consumed: 2,
798                skipped_optional_segments: 0,
799            },
800        )
801        .unwrap();
802
803        assert_eq!(best.route.value, "literal");
804    }
805}
806
807fn prefix_content_segments<'s, 'p>(segments: &'s [EncodedSegment<'p>]) -> &'s [EncodedSegment<'p>] {
808    match segments {
809        [only] if only.is_empty() => &segments[..0],
810        [head @ .., last] if last.is_empty() => head,
811        _ => segments,
812    }
813}
814
815fn clone_pattern_segment_with_local_names(
816    segment: &PatternSegment,
817    names: &[u8],
818) -> (PatternSegment, Vec<u8>) {
819    let mut local_names = Vec::new();
820    let segment = match segment {
821        PatternSegment::CatchAll => PatternSegment::CatchAll,
822        PatternSegment::NamedCatchAll {
823            name_start,
824            name_len,
825        } => {
826            local_names.extend_from_slice(&names[*name_start..*name_start + *name_len]);
827            PatternSegment::NamedCatchAll {
828                name_start: 0,
829                name_len: *name_len,
830            }
831        }
832        PatternSegment::Normal { elems, ambiguity } => PatternSegment::Normal {
833            elems: elems
834                .iter()
835                .map(|element| clone_element_with_local_names(element, names, &mut local_names))
836                .collect(),
837            ambiguity: *ambiguity,
838        },
839    };
840    (segment, local_names)
841}
842
843fn clone_element_with_local_names(
844    element: &Element,
845    names: &[u8],
846    local_names: &mut Vec<u8>,
847) -> Element {
848    let kind = match &element.kind {
849        ElementKind::Literal(literal) => ElementKind::Literal(literal.clone()),
850        ElementKind::Star => ElementKind::Star,
851        ElementKind::Capture {
852            name_start,
853            name_len,
854        } => {
855            let local_start = local_names.len();
856            local_names.extend_from_slice(&names[*name_start..*name_start + *name_len]);
857            ElementKind::Capture {
858                name_start: local_start,
859                name_len: *name_len,
860            }
861        }
862    };
863    Element {
864        kind,
865        optional: element.optional,
866    }
867}
868
869impl<'a, 'p, T> PathRouteMatch<'a, 'p, T> {
870    /// Value stored for the matched route.
871    #[must_use]
872    pub fn value(&self) -> &'a T {
873        self.value
874    }
875
876    /// Number of path segments covered by the matched prefix.
877    #[must_use]
878    pub fn matched_segment_count(&self) -> usize {
879        self.matched_segment_count
880    }
881
882    /// Captures produced while matching the prefix.
883    #[must_use]
884    pub fn captures(&self) -> &PathCaptures<'a, 'p> {
885        &self.captures
886    }
887
888    /// Decompose into owned match parts.
889    #[must_use]
890    pub fn into_parts(self) -> (&'a T, usize, PathCaptures<'a, 'p>) {
891        (self.value, self.matched_segment_count, self.captures)
892    }
893}
894
895impl PathRouteCaptures {
896    fn from_captures(captures: &PathCaptures<'_, '_>) -> Self {
897        Self {
898            params: captures
899                .iter()
900                .map(|(name, value)| (name.to_owned(), value.to_owned()))
901                .collect(),
902            glob: captures.glob().map(str::to_owned),
903        }
904    }
905
906    /// The decoded value captured under `name`, or `None` if absent.
907    #[must_use]
908    pub fn get(&self, name: &str) -> Option<&str> {
909        self.params
910            .iter()
911            .find(|(key, _)| key == name)
912            .map(|(_, value)| value.as_str())
913    }
914
915    /// The decoded value captured under `name`, or `None` if absent or empty.
916    #[must_use]
917    pub fn get_non_empty(&self, name: &str) -> Option<&str> {
918        self.get(name).filter(|value| !value.is_empty())
919    }
920
921    /// Iterator over decoded named captures in match order.
922    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
923        self.params
924            .iter()
925            .map(|(name, value)| (name.as_str(), value.as_str()))
926    }
927
928    /// The decoded anonymous `{*}` capture, if present.
929    #[must_use]
930    pub fn glob(&self) -> Option<&str> {
931        self.glob.as_deref()
932    }
933
934    /// `true` when no named param and no anonymous glob were captured.
935    #[must_use]
936    pub fn is_empty(&self) -> bool {
937        self.params.is_empty() && self.glob.is_none()
938    }
939}
940
941fn path_router_specificity(pattern: &PathPattern) -> Vec<PathRouterSegmentRank> {
942    pattern
943        .segment_specificity()
944        .map(|spec| PathRouterSegmentRank {
945            kind: match spec.kind {
946                PathPatternSegmentKind::Literal => 2,
947                PathPatternSegmentKind::Dynamic => 1,
948                PathPatternSegmentKind::CatchAll => 0,
949            },
950            literal_bytes: spec.literal_bytes,
951            fewer_dynamic_parts: Reverse(spec.dynamic_parts),
952            fewer_optional_parts: Reverse(spec.optional_parts),
953        })
954        .collect()
955}
956
957impl PathPattern {
958    /// Compile a path pattern. Infallible: anything not a recognized meta
959    /// token is a literal.
960    ///
961    /// ```
962    /// use rama_net::uri::{PathPattern, PathRef};
963    ///
964    /// let pat = PathPattern::new("/backend-api/codex/responses");
965    /// assert!(pat.is_match(PathRef::from_raw_str("/backend-api/codex/responses")));
966    /// assert!(!pat.is_match(PathRef::from_raw_str("/backend-api/codex")));
967    /// ```
968    #[must_use]
969    pub fn new(pattern: impl IntoUriComponent) -> Self {
970        Self::new_with_opts(pattern, PathMatchOptions::default())
971    }
972
973    /// [`new`](Self::new) with explicit [`PathMatchOptions`]. The matcher
974    /// honors `ignore_ascii_case` and `percent_decode`; `partial` is
975    /// irrelevant and ignored.
976    ///
977    /// ```
978    /// use rama_net::uri::{PathMatchOptions, PathPattern, PathRef};
979    ///
980    /// let opts = PathMatchOptions {
981    ///     ignore_ascii_case: true,
982    ///     ..Default::default()
983    /// };
984    /// let pat = PathPattern::new_with_opts("/api/v2", opts);
985    /// assert!(pat.is_match(PathRef::from_raw_str("/API/v2")));
986    /// ```
987    #[must_use]
988    #[expect(
989        clippy::needless_pass_by_value,
990        reason = "by-value matches IntoUriComponent's signature on sibling APIs; this impl only borrows the input"
991    )]
992    pub fn new_with_opts(pattern: impl IntoUriComponent, opts: PathMatchOptions) -> Self {
993        let raw = pattern.as_uri_component_bytes();
994        Self::compile(&raw, opts, false)
995    }
996
997    /// Compile a **prefix** matcher: the pattern must match a leading run of the
998    /// path's segments; any trailing segments and the path's trailing slash are
999    /// ignored. So `/api` matches `/api`, `/api/`, and `/api/users` — but not
1000    /// `/apixyz` (segments are matched whole).
1001    ///
1002    /// ```
1003    /// use rama_net::uri::{PathPattern, PathRef};
1004    ///
1005    /// let api = PathPattern::new_prefix("/api");
1006    /// assert!(api.is_match(PathRef::from_raw_str("/api")));
1007    /// assert!(api.is_match(PathRef::from_raw_str("/api/users/42")));
1008    /// assert!(!api.is_match(PathRef::from_raw_str("/apixyz")));
1009    /// ```
1010    #[must_use]
1011    pub fn new_prefix(pattern: impl IntoUriComponent) -> Self {
1012        Self::new_prefix_with_opts(pattern, PathMatchOptions::default())
1013    }
1014
1015    /// [`new_prefix`](Self::new_prefix) with explicit [`PathMatchOptions`].
1016    #[must_use]
1017    #[expect(
1018        clippy::needless_pass_by_value,
1019        reason = "by-value matches IntoUriComponent's signature on sibling APIs; this impl only borrows the input"
1020    )]
1021    pub fn new_prefix_with_opts(pattern: impl IntoUriComponent, opts: PathMatchOptions) -> Self {
1022        let raw = pattern.as_uri_component_bytes();
1023        Self::compile(&raw, opts, true)
1024    }
1025
1026    fn compile(raw: &[u8], mut opts: PathMatchOptions, prefix: bool) -> Self {
1027        opts.partial = false;
1028        // Trailing-slash policy is read off the raw pattern *before* the
1029        // leading slash is stripped, so the bare-root `/` (which becomes empty
1030        // after stripping) still registers as a required trailing slash.
1031        let (raw, trailing) = if let Some(rest) = raw.strip_suffix(b"/?") {
1032            (rest, TrailingSlash::Optional)
1033        } else if let Some(rest) = raw.strip_suffix(b"/") {
1034            (rest, TrailingSlash::Required)
1035        } else {
1036            (raw, TrailingSlash::Forbidden)
1037        };
1038        let body = strip_leading_slash(raw);
1039
1040        let mut name_bytes: Vec<u8> = Vec::new();
1041        let mut segments = Vec::new();
1042        let mut capture_free = true;
1043
1044        // Split on `/`. An empty `body` (root pattern `/`) yields no
1045        // segments, which together with `TrailingSlash::Required` matches
1046        // exactly `/`.
1047        if !body.is_empty() {
1048            for seg in body.split(|&b| b == b'/') {
1049                match parse_catchall(seg) {
1050                    Some(CatchAll::Anon) => {
1051                        capture_free = false;
1052                        segments.push(PatternSegment::CatchAll);
1053                        continue;
1054                    }
1055                    Some(CatchAll::Named(name)) => {
1056                        capture_free = false;
1057                        let name_start = name_bytes.len();
1058                        name_bytes.extend_from_slice(name);
1059                        segments.push(PatternSegment::NamedCatchAll {
1060                            name_start,
1061                            name_len: name.len(),
1062                        });
1063                        continue;
1064                    }
1065                    None => {}
1066                }
1067                let elements = parse_segment(seg, &mut name_bytes, &mut capture_free);
1068                if optional_whole_segment_binding(&elements).is_some() {
1069                    capture_free = false;
1070                }
1071                let ambiguity = elements
1072                    .iter()
1073                    .filter(|e| {
1074                        e.optional
1075                            || matches!(e.kind, ElementKind::Star | ElementKind::Capture { .. })
1076                    })
1077                    .count();
1078                segments.push(PatternSegment::Normal {
1079                    elems: elements,
1080                    ambiguity,
1081                });
1082            }
1083        }
1084
1085        Self {
1086            segments,
1087            name_bytes,
1088            trailing,
1089            opts,
1090            capture_free,
1091            prefix,
1092        }
1093    }
1094
1095    fn drop_trailing_catch_all(&mut self) -> bool {
1096        let Some(PatternSegment::CatchAll | PatternSegment::NamedCatchAll { .. }) =
1097            self.segments.last()
1098        else {
1099            return false;
1100        };
1101        self.segments.pop();
1102        self.capture_free = self.segments.iter().all(pattern_segment_capture_free);
1103        true
1104    }
1105
1106    /// The [kind](PathPatternSegmentKind) of each `/`-delimited pattern segment,
1107    /// in order — so callers can classify segments (literal vs dynamic vs
1108    /// catch-all) straight from the compiled pattern instead of re-parsing the
1109    /// syntax. A bare-root pattern (`/`) yields an empty iterator.
1110    ///
1111    /// ```
1112    /// use rama_net::uri::{PathPattern, PathPatternSegmentKind as K};
1113    ///
1114    /// let kinds: Vec<_> = PathPattern::new("/users/{id}/{*rest}").segment_kinds().collect();
1115    /// assert_eq!(kinds, [K::Literal, K::Dynamic, K::CatchAll]);
1116    /// // An invalid catch-all body is a literal, exactly as the matcher treats it.
1117    /// let kinds: Vec<_> = PathPattern::new("/api/{*bad name}").segment_kinds().collect();
1118    /// assert_eq!(kinds, [K::Literal, K::Literal]);
1119    /// ```
1120    pub fn segment_kinds(&self) -> impl ExactSizeIterator<Item = PathPatternSegmentKind> + '_ {
1121        self.segment_specificity().map(|spec| spec.kind)
1122    }
1123
1124    /// Specificity metadata for each `/`-delimited pattern segment, in order.
1125    /// This is a richer version of [`segment_kinds`](Self::segment_kinds) for
1126    /// callers that need stable precedence among overlapping dynamic patterns.
1127    ///
1128    /// ```
1129    /// use rama_net::uri::{PathPattern, PathPatternSegmentKind as K};
1130    ///
1131    /// let specs: Vec<_> = PathPattern::new("/files/{name}.json")
1132    ///     .segment_specificity()
1133    ///     .collect();
1134    /// assert_eq!(specs[0].kind, K::Literal);
1135    /// assert_eq!(specs[1].kind, K::Dynamic);
1136    /// assert_eq!(specs[1].literal_bytes, 5);
1137    /// assert_eq!(specs[1].dynamic_parts, 1);
1138    /// ```
1139    pub fn segment_specificity(
1140        &self,
1141    ) -> impl ExactSizeIterator<Item = PathPatternSegmentSpecificity> + '_ {
1142        self.segments.iter().map(|seg| match seg {
1143            PatternSegment::CatchAll | PatternSegment::NamedCatchAll { .. } => {
1144                PathPatternSegmentSpecificity {
1145                    kind: PathPatternSegmentKind::CatchAll,
1146                    literal_bytes: 0,
1147                    dynamic_parts: 1,
1148                    optional_parts: 0,
1149                }
1150            }
1151            PatternSegment::Normal { elems, ambiguity } => {
1152                let literal_bytes = elems
1153                    .iter()
1154                    .map(|el| match &el.kind {
1155                        ElementKind::Literal(lit) => lit.len(),
1156                        ElementKind::Star | ElementKind::Capture { .. } => 0,
1157                    })
1158                    .sum();
1159                let dynamic_parts = elems
1160                    .iter()
1161                    .filter(|el| matches!(el.kind, ElementKind::Star | ElementKind::Capture { .. }))
1162                    .count();
1163                let optional_parts = elems.iter().filter(|el| el.optional).count();
1164                PathPatternSegmentSpecificity {
1165                    // `ambiguity == 0` means no wildcard/capture/optional
1166                    // element, so the segment is a fixed string.
1167                    kind: if *ambiguity == 0 {
1168                        PathPatternSegmentKind::Literal
1169                    } else {
1170                        PathPatternSegmentKind::Dynamic
1171                    },
1172                    literal_bytes,
1173                    dynamic_parts,
1174                    optional_parts,
1175                }
1176            }
1177        })
1178    }
1179
1180    /// `true` when `path` matches. Allocation-free when the pattern has no
1181    /// captures and no catch-all.
1182    ///
1183    /// ```
1184    /// use rama_net::uri::{PathPattern, PathRef};
1185    ///
1186    /// let pat = PathPattern::new("/files/{}.txt");
1187    /// assert!(pat.is_match(PathRef::from_raw_str("/files/readme.txt")));
1188    /// assert!(!pat.is_match(PathRef::from_raw_str("/files/readme.md")));
1189    /// ```
1190    #[must_use]
1191    pub fn is_match(&self, path: PathRef<'_>) -> bool {
1192        // The fast path assumes a full, both-ends-anchored match; prefix matching
1193        // needs the segment-sequence engine, so route it through `captures`.
1194        if self.capture_free && !self.prefix {
1195            self.is_match_fast(path)
1196        } else {
1197            self.captures(path).is_some()
1198        }
1199    }
1200
1201    /// Allocation-free match for capture-free patterns. A capture-free
1202    /// pattern has no catch-all, so every pattern segment matches exactly one path
1203    /// segment positionally — no backtracking across segments, no `Vec`, no
1204    /// captured-value strings.
1205    fn is_match_fast(&self, path: PathRef<'_>) -> bool {
1206        // Walk the path segments with one-segment lookahead so the trailing-`/`
1207        // marker can be classified without materializing the list.
1208        let mut path_iter = path.segments().peekable();
1209        let mut pat_iter = self.segments.iter();
1210        let mut content_count = 0usize;
1211        let mut ignore = Sink::Ignore;
1212
1213        let trailing = loop {
1214            let Some(seg) = path_iter.next() else {
1215                // No (more) segments: no trailing slash.
1216                break false;
1217            };
1218            let is_last = path_iter.peek().is_none();
1219            // The root path `/` is a lone empty segment: it carries the root
1220            // slash but no content. A final empty segment after real content is
1221            // the trailing-`/` marker. Either way it is not matched as content.
1222            if seg.is_empty() && is_last && (content_count >= 1 || self.segments.is_empty()) {
1223                break true;
1224            }
1225            match pat_iter.next() {
1226                Some(PatternSegment::Normal { elems, ambiguity }) => {
1227                    if !match_segment(
1228                        elems,
1229                        *ambiguity,
1230                        seg.as_encoded_str().as_ref().as_bytes(),
1231                        self.opts,
1232                        &mut ignore,
1233                    ) {
1234                        return false;
1235                    }
1236                }
1237                // Either the pattern ran out of segments (path has a real one
1238                // left) or a catch-all snuck in — impossible for a capture-free
1239                // pattern, but a defensive miss either way.
1240                None | Some(PatternSegment::CatchAll | PatternSegment::NamedCatchAll { .. }) => {
1241                    return false;
1242                }
1243            }
1244            content_count += 1;
1245        };
1246
1247        // All path content consumed: the pattern must be exhausted and the
1248        // observed trailing slash must satisfy the policy.
1249        pat_iter.next().is_none() && self.trailing.accepts(trailing)
1250    }
1251
1252    /// Match and return captured values, or `None` when `path` doesn't
1253    /// match. Uses inline storage for the common small number of bindings.
1254    ///
1255    /// ```
1256    /// use rama_net::uri::{PathPattern, PathRef};
1257    ///
1258    /// let pat = PathPattern::new("/simple/{name}/?");
1259    /// let caps = pat.captures(PathRef::from_raw_str("/simple/requests")).unwrap();
1260    /// assert_eq!(caps.get("name"), Some("requests"));
1261    /// ```
1262    #[must_use]
1263    pub fn captures<'p>(&self, path: PathRef<'p>) -> Option<PathCaptures<'_, 'p>> {
1264        self.captures_with_prefix_mode(path, self.prefix)
1265    }
1266
1267    fn captures_exact<'p>(&self, path: PathRef<'p>) -> Option<PathCaptures<'_, 'p>> {
1268        self.captures_with_prefix_mode(path, false)
1269    }
1270
1271    fn captures_with_prefix_mode<'p>(
1272        &self,
1273        path: PathRef<'p>,
1274        prefix: bool,
1275    ) -> Option<PathCaptures<'_, 'p>> {
1276        // Inline the segment list: most paths have a handful of segments, so
1277        // this keeps the capturing path off the allocator in the common case.
1278        let all: SmallVec<[EncodedSegment<'p>; 8]> =
1279            path.segments().map(|s| s.as_encoded_str()).collect();
1280        // A prefix match ignores trailing segments + trailing-slash policy, so
1281        // it matches against all segments; a full match trims the trailing-`/`
1282        // marker and enforces the policy.
1283        let segs: &[EncodedSegment<'p>] = if prefix {
1284            &all
1285        } else {
1286            self.check_trailing(&all)?
1287        };
1288        let mut bindings: SmallVec<[Binding<'p>; 4]> = SmallVec::new();
1289        let mut sink = Sink::Record(&mut bindings);
1290        let mut seq_memo = SeqMemo::new(&self.segments, segs.len());
1291        if match_sequence(
1292            &self.segments,
1293            segs,
1294            self.opts,
1295            &mut sink,
1296            &mut seq_memo,
1297            prefix,
1298        ) {
1299            Some(PathCaptures {
1300                name_bytes: &self.name_bytes,
1301                bindings,
1302            })
1303        } else {
1304            None
1305        }
1306    }
1307
1308    /// Validate the trailing-slash policy and return the content segments
1309    /// (with any trailing-`/` empty marker removed), or `None` if the policy
1310    /// rejects the path.
1311    ///
1312    /// `PathRef::segments()` yields a trailing empty segment for a trailing
1313    /// `/` (so `/a/` -> ["a", ""]); we consume that here rather than letting
1314    /// it leak into element matching. The bare root `/` is a lone empty
1315    /// segment that carries the root slash but no content.
1316    fn check_trailing<'s, 'p>(
1317        &self,
1318        segs: &'s [EncodedSegment<'p>],
1319    ) -> Option<&'s [EncodedSegment<'p>]> {
1320        // Root `/` (lone empty segment) carries the slash but no content; a
1321        // final empty segment after real content is the trailing-`/` marker.
1322        let last_empty = segs.last().is_some_and(|s| s.is_empty());
1323        let is_root = segs.len() == 1 && last_empty && self.segments.is_empty();
1324        let (content, has_slash) = if is_root {
1325            (&segs[..0], true)
1326        } else if last_empty && segs.len() >= 2 {
1327            (&segs[..segs.len() - 1], true)
1328        } else {
1329            (segs, false)
1330        };
1331        self.trailing.accepts(has_slash).then_some(content)
1332    }
1333}
1334
1335/// A recorded capture: name slice into the pattern's `name_bytes`
1336/// (`name_len == 0` for the anonymous glob), plus the matched, decoded value.
1337#[derive(Debug, Clone)]
1338struct Binding<'p> {
1339    name_start: usize,
1340    name_len: usize,
1341    value: Cow<'p, str>,
1342    /// `true` for the `{*}` catch-all's joined value.
1343    is_glob: bool,
1344}
1345
1346/// Where matched capture values go. The `is_match` fast path discards them
1347/// without allocating; `captures` records them.
1348enum Sink<'b, 'p> {
1349    Ignore,
1350    Record(&'b mut SmallVec<[Binding<'p>; 4]>),
1351}
1352
1353impl<'p> Sink<'_, 'p> {
1354    /// Insert a binding at index `idx`, preserving left-to-right order when a
1355    /// run records itself after its tail already pushed bindings.
1356    fn insert_at(&mut self, idx: usize, b: Binding<'p>) {
1357        if let Sink::Record(v) = self {
1358            v.insert(idx, b);
1359        }
1360    }
1361    fn len(&self) -> usize {
1362        match self {
1363            Sink::Ignore => 0,
1364            Sink::Record(v) => v.len(),
1365        }
1366    }
1367    fn truncate(&mut self, n: usize) {
1368        if let Sink::Record(v) = self {
1369            v.truncate(n);
1370        }
1371    }
1372}
1373
1374/// Captured values from a successful [`PathPattern::captures`] match.
1375///
1376/// Capture names borrow from the compiled pattern (`'a`). Values are always
1377/// percent-decoded (per the pattern's options); the `'p` value lifetime ties
1378/// them to the matched path so a future zero-copy fast path can borrow,
1379/// though today every value is owned.
1380///
1381/// ```
1382/// use rama_net::uri::{PathPattern, PathRef};
1383///
1384/// let pat = PathPattern::new("/p2/{*}/{file}.txt");
1385/// let caps = pat.captures(PathRef::from_raw_str("/p2/a/b/c.txt")).unwrap();
1386/// assert_eq!(caps.glob(), Some("a/b"));
1387/// assert_eq!(caps.get("file"), Some("c"));
1388/// ```
1389#[derive(Debug, Clone)]
1390pub struct PathCaptures<'a, 'p> {
1391    name_bytes: &'a [u8],
1392    bindings: SmallVec<[Binding<'p>; 4]>,
1393}
1394
1395impl<'a, 'p> PathCaptures<'a, 'p> {
1396    fn empty(name_bytes: &'a [u8]) -> Self {
1397        Self {
1398            name_bytes,
1399            bindings: SmallVec::new(),
1400        }
1401    }
1402
1403    fn name_of(&self, b: &Binding<'p>) -> &'a str {
1404        let raw = &self.name_bytes[b.name_start..b.name_start + b.name_len];
1405        // Safety: capture names are pattern bytes copied verbatim; the
1406        // accepted name bytes are all ASCII (see `is_name_byte`).
1407        unsafe { core::str::from_utf8_unchecked(raw) }
1408    }
1409
1410    /// The decoded value captured under `name`, or `None` if `name` was not
1411    /// bound. The `{*}` catch-all is reachable via [`glob`](Self::glob), not
1412    /// here.
1413    #[must_use]
1414    pub fn get(&self, name: &str) -> Option<&str> {
1415        self.bindings
1416            .iter()
1417            .find(|b| !b.is_glob && b.name_len != 0 && self.name_of(b) == name)
1418            .map(|b| b.value.as_ref())
1419    }
1420
1421    /// The decoded value captured under `name`, or `None` if absent or empty.
1422    #[must_use]
1423    pub fn get_non_empty(&self, name: &str) -> Option<&str> {
1424        self.get(name).filter(|value| !value.is_empty())
1425    }
1426
1427    /// Iterator over `(name, decoded value)` for every named (non-glob)
1428    /// capture, in match order.
1429    pub fn iter(&self) -> impl Iterator<Item = (&str, &str)> {
1430        self.bindings
1431            .iter()
1432            .filter(|b| !b.is_glob && b.name_len != 0)
1433            .map(|b| (self.name_of(b), b.value.as_ref()))
1434    }
1435
1436    /// The `{*}` catch-all value, '/'-joined and decoded, or `None` when the
1437    /// pattern has no catch-all (or it didn't match).
1438    #[must_use]
1439    pub fn glob(&self) -> Option<&str> {
1440        self.bindings
1441            .iter()
1442            .find(|b| b.is_glob)
1443            .map(|b| b.value.as_ref())
1444    }
1445
1446    /// `true` when there are no captures and no catch-all value.
1447    #[must_use]
1448    pub fn is_empty(&self) -> bool {
1449        self.bindings.is_empty()
1450    }
1451}
1452
1453// ----------------------------------------------------------------------
1454// Compilation helpers
1455// ----------------------------------------------------------------------
1456
1457/// A whole-segment catch-all token.
1458enum CatchAll<'a> {
1459    /// `{*}`
1460    Anon,
1461    /// `{*name}` (name is non-empty, all [`is_pattern_name_byte`]).
1462    Named(&'a [u8]),
1463}
1464
1465/// If `seg` is a whole-segment catch-all (`{*}` or `{*name}`), classify it.
1466/// Returns `None` for anything else — including mid-segment `{*…}` (handled as
1467/// a literal by [`parse_segment`]) and `{*bad name}` — which fall through.
1468fn parse_catchall(seg: &[u8]) -> Option<CatchAll<'_>> {
1469    let inner = seg.strip_prefix(b"{*")?.strip_suffix(b"}")?;
1470    if inner.is_empty() {
1471        return Some(CatchAll::Anon);
1472    }
1473    inner
1474        .iter()
1475        .all(|&b| is_pattern_name_byte(b))
1476        .then_some(CatchAll::Named(inner))
1477}
1478
1479/// Parse one (non catch-all) pattern segment into a sequence of elements.
1480///
1481/// Scans for `{…}` brace groups (`{}` -> [`Star`](ElementKind::Star),
1482/// `{name}` -> [`Capture`](ElementKind::Capture)); everything outside braces is
1483/// literal. An unclosed `{`, or a group whose body isn't a valid token, is kept
1484/// literal. `name_bytes` accumulates capture-name bytes; element name indices
1485/// point into it. `capture_free` is cleared whenever a named capture is seen.
1486fn parse_segment(
1487    seg: &[u8],
1488    name_bytes: &mut Vec<u8>,
1489    capture_free: &mut bool,
1490) -> SmallVec<[Element; 2]> {
1491    let mut elements: SmallVec<[Element; 2]> = SmallVec::new();
1492    let mut literal: Vec<u8> = Vec::new();
1493    let mut i = 0;
1494
1495    // Flush any pending literal run into an element.
1496    macro_rules! flush_literal {
1497        () => {
1498            if !literal.is_empty() {
1499                elements.push(Element {
1500                    kind: ElementKind::Literal(core::mem::take(&mut literal).into_boxed_slice()),
1501                    optional: false,
1502                });
1503            }
1504        };
1505    }
1506
1507    while i < seg.len() {
1508        match seg[i] {
1509            b'{' => {
1510                // A `{…}` group is a token only when it closes and its body is
1511                // a valid name (or empty); otherwise the `{` is a literal byte.
1512                if let Some((kind, next)) = parse_brace(seg, i, name_bytes, capture_free) {
1513                    flush_literal!();
1514                    elements.push(Element {
1515                        kind,
1516                        optional: false,
1517                    });
1518                    i = next;
1519                } else {
1520                    literal.push(b'{');
1521                    i += 1;
1522                }
1523            }
1524            b'?' => {
1525                // `?` makes the immediately preceding element optional.
1526                if let Some(last) = literal.pop() {
1527                    // A pending literal run takes precedence: `?` binds only its
1528                    // final byte. Flush the head literal, then push the final
1529                    // byte as its own optional literal element.
1530                    flush_literal!();
1531                    elements.push(Element {
1532                        kind: ElementKind::Literal(Box::from([last])),
1533                        optional: true,
1534                    });
1535                } else if let Some(last) = elements.last_mut() {
1536                    last.optional = true;
1537                } else {
1538                    // Leading `?` with nothing before it is a literal `?`.
1539                    literal.push(b'?');
1540                }
1541                i += 1;
1542            }
1543            other => {
1544                literal.push(other);
1545                i += 1;
1546            }
1547        }
1548    }
1549    flush_literal!();
1550    elements
1551}
1552
1553/// Parse a within-segment `{…}` group starting at `seg[open] == '{'`. On a
1554/// valid token returns its [`ElementKind`] and the index just past the closing
1555/// `}`. `{}` -> Star, `{name}` -> Capture (name = non-empty
1556/// [`is_pattern_name_byte`] run). Returns `None` (so the caller keeps `{`
1557/// literal) for an unclosed brace or any non-name body — including `{*…}`,
1558/// which is a catch-all only as a whole segment.
1559fn parse_brace(
1560    seg: &[u8],
1561    open: usize,
1562    name_bytes: &mut Vec<u8>,
1563    capture_free: &mut bool,
1564) -> Option<(ElementKind, usize)> {
1565    let close = open + 1 + seg[open + 1..].iter().position(|&b| b == b'}')?;
1566    let inner = &seg[open + 1..close];
1567    let next = close + 1;
1568    if inner.is_empty() {
1569        return Some((ElementKind::Star, next));
1570    }
1571    if !inner.iter().all(|&b| is_pattern_name_byte(b)) {
1572        return None;
1573    }
1574    let name_start = name_bytes.len();
1575    name_bytes.extend_from_slice(inner);
1576    *capture_free = false;
1577    Some((
1578        ElementKind::Capture {
1579            name_start,
1580            name_len: inner.len(),
1581        },
1582        next,
1583    ))
1584}
1585
1586// ----------------------------------------------------------------------
1587// Matching
1588// ----------------------------------------------------------------------
1589//
1590// Both match entry points share one greedy recursion. Without guards that
1591// recursion is exponential: a wildcard run tries every split point and an
1592// optional element forks, so a segment with many such *ambiguity sources*
1593// (or a pattern with many `{*}`) revisits the same `(position)` state over and
1594// over. Each level fixes that by memoizing *failed* states. Failure-only memo
1595// is sound because capture recording happens solely on the unique success
1596// path: a state proven unmatchable can never later succeed, so caching it
1597// cannot drop or corrupt a binding. The memo grid is allocated only for the
1598// pathological shapes (>= 2 ambiguity sources in a segment, >= 2 `{*}` in a
1599// pattern); simpler shapes recurse linearly with no allocation.
1600
1601/// Dense 2D failure set (`rows × cols`), one bit per `(row, col)` state packed
1602/// into `u64` words — 8× tighter than a `bool` grid and fewer cache lines to
1603/// touch during the recursion. Only built for the pathological shapes that need
1604/// a memo; simpler patterns never allocate one.
1605struct BitGrid {
1606    words: Box<[u64]>,
1607    cols: usize,
1608}
1609
1610impl BitGrid {
1611    fn new(rows: usize, cols: usize) -> Self {
1612        let words = vec![0u64; (rows * cols).div_ceil(64)].into_boxed_slice();
1613        Self { words, cols }
1614    }
1615
1616    #[inline]
1617    fn bit(&self, row: usize, col: usize) -> (usize, u64) {
1618        let idx = row * self.cols + col;
1619        (idx >> 6, 1u64 << (idx & 63))
1620    }
1621
1622    #[inline]
1623    fn get(&self, row: usize, col: usize) -> bool {
1624        let (word, mask) = self.bit(row, col);
1625        self.words[word] & mask != 0
1626    }
1627
1628    #[inline]
1629    fn set(&mut self, row: usize, col: usize) {
1630        let (word, mask) = self.bit(row, col);
1631        self.words[word] |= mask;
1632    }
1633}
1634
1635/// Failure memo for the cross-segment catch-all search, keyed on the *remaining*
1636/// `(pattern, path-segment)` counts. Only allocated when a pattern has two or
1637/// more catch-alls (a single one can't revisit states).
1638enum SeqMemo {
1639    None,
1640    Grid {
1641        grid: BitGrid,
1642        base_pats: usize,
1643        base_segs: usize,
1644    },
1645}
1646
1647impl SeqMemo {
1648    fn new(pats: &[PatternSegment], n_segs: usize) -> Self {
1649        let catch_alls = pats
1650            .iter()
1651            .filter(|p| {
1652                matches!(
1653                    p,
1654                    PatternSegment::CatchAll | PatternSegment::NamedCatchAll { .. }
1655                )
1656            })
1657            .count();
1658        if catch_alls >= 2 {
1659            Self::Grid {
1660                grid: BitGrid::new(pats.len() + 1, n_segs + 1),
1661                base_pats: pats.len(),
1662                base_segs: n_segs,
1663            }
1664        } else {
1665            Self::None
1666        }
1667    }
1668
1669    /// `true` if the state with `pats_left`/`segs_left` remaining is known to
1670    /// fail. Both args are suffix lengths of the originals, so the advance from
1671    /// the start (the grid row/col) is `base − left`.
1672    fn is_failed(&self, pats_left: usize, segs_left: usize) -> bool {
1673        match self {
1674            Self::None => false,
1675            Self::Grid {
1676                grid,
1677                base_pats,
1678                base_segs,
1679            } => grid.get(base_pats - pats_left, base_segs - segs_left),
1680        }
1681    }
1682
1683    fn mark_failed(&mut self, pats_left: usize, segs_left: usize) {
1684        if let Self::Grid {
1685            grid,
1686            base_pats,
1687            base_segs,
1688        } = self
1689        {
1690            grid.set(*base_pats - pats_left, *base_segs - segs_left);
1691        }
1692    }
1693}
1694
1695/// Match a sequence of pattern segments against the path segments, with
1696/// backtracking across `{*}` catch-alls. Returns `true` on a match. When
1697/// `prefix` is set, a path tail left over after the pattern is exhausted is
1698/// accepted (leading-run match) instead of requiring full consumption.
1699fn match_sequence<'p>(
1700    pats: &[PatternSegment],
1701    segs: &[EncodedSegment<'p>],
1702    opts: PathMatchOptions,
1703    sink: &mut Sink<'_, 'p>,
1704    memo: &mut SeqMemo,
1705    prefix: bool,
1706) -> bool {
1707    if memo.is_failed(pats.len(), segs.len()) {
1708        return false;
1709    }
1710
1711    let matched = match pats.split_first() {
1712        None => prefix || segs.is_empty(),
1713        Some((PatternSegment::CatchAll, rest)) => {
1714            match_catch_all(None, rest, segs, opts, sink, memo, prefix)
1715        }
1716        Some((
1717            PatternSegment::NamedCatchAll {
1718                name_start,
1719                name_len,
1720            },
1721            rest,
1722        )) => match_catch_all(
1723            Some((*name_start, *name_len)),
1724            rest,
1725            segs,
1726            opts,
1727            sink,
1728            memo,
1729            prefix,
1730        ),
1731        Some((PatternSegment::Normal { elems, ambiguity }, rest)) => {
1732            let mark = sink.len();
1733            if let Some((seg, segs_rest)) = segs.split_first()
1734                && match_segment(elems, *ambiguity, seg.as_ref().as_bytes(), opts, sink)
1735                && match_sequence(rest, segs_rest, opts, sink, memo, prefix)
1736            {
1737                return true;
1738            }
1739            sink.truncate(mark);
1740
1741            if let Some(binding) = optional_whole_segment_binding(elems) {
1742                let mark = sink.len();
1743                if let OptionalWholeSegment::Capture {
1744                    name_start,
1745                    name_len,
1746                } = binding
1747                {
1748                    sink.insert_at(
1749                        mark,
1750                        Binding {
1751                            name_start,
1752                            name_len,
1753                            value: Cow::Borrowed(""),
1754                            is_glob: false,
1755                        },
1756                    );
1757                }
1758                if match_sequence(rest, segs, opts, sink, memo, prefix) {
1759                    return true;
1760                }
1761                sink.truncate(mark);
1762            }
1763            false
1764        }
1765    };
1766
1767    if !matched {
1768        memo.mark_failed(pats.len(), segs.len());
1769    }
1770    matched
1771}
1772
1773/// A segment made solely from `{name}?` or `{}?` can be skipped completely.
1774/// Named captures bind as an empty string to keep omitted and empty segment
1775/// content observable the same way through [`PathCaptures::get`].
1776#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1777enum OptionalWholeSegment {
1778    Anonymous,
1779    Capture { name_start: usize, name_len: usize },
1780}
1781
1782fn optional_whole_segment_binding(elems: &[Element]) -> Option<OptionalWholeSegment> {
1783    let [
1784        Element {
1785            kind,
1786            optional: true,
1787        },
1788    ] = elems
1789    else {
1790        return None;
1791    };
1792    match kind {
1793        ElementKind::Star => Some(OptionalWholeSegment::Anonymous),
1794        ElementKind::Capture {
1795            name_start,
1796            name_len,
1797        } => Some(OptionalWholeSegment::Capture {
1798            name_start: *name_start,
1799            name_len: *name_len,
1800        }),
1801        ElementKind::Literal(_) => None,
1802    }
1803}
1804
1805/// Match a catch-all (`{*}` or `{*name}`) against `segs`, then the remaining
1806/// `rest` patterns against the tail. Consumes 1+ path segments, shortest first,
1807/// growing until the tail matches. On success records the matched run, '/'-joined
1808/// and decoded — as the anonymous glob when `name` is `None`, else a named binding.
1809fn match_catch_all<'p>(
1810    name: Option<(usize, usize)>,
1811    rest: &[PatternSegment],
1812    segs: &[EncodedSegment<'p>],
1813    opts: PathMatchOptions,
1814    sink: &mut Sink<'_, 'p>,
1815    memo: &mut SeqMemo,
1816    prefix: bool,
1817) -> bool {
1818    for take in 1..=segs.len() {
1819        let mark = sink.len();
1820        if match_sequence(rest, &segs[take..], opts, sink, memo, prefix) {
1821            // Record only once the tail matched, so discarded attempts cost nothing.
1822            let value = join_decoded(&segs[..take], opts.percent_decode);
1823            let (name_start, name_len, is_glob) = match name {
1824                Some((start, len)) => (start, len, false),
1825                None => (0, 0, true),
1826            };
1827            sink.insert_at(
1828                mark,
1829                Binding {
1830                    name_start,
1831                    name_len,
1832                    value,
1833                    is_glob,
1834                },
1835            );
1836            return true;
1837        }
1838        sink.truncate(mark);
1839    }
1840    false
1841}
1842
1843/// Match one pattern segment's elements against one path segment's bytes.
1844///
1845/// The path segment is decoded once (per `percent_decode`) up front, then
1846/// elements are matched against the decoded bytes via greedy backtracking.
1847/// `ambiguity` is the segment's precomputed backtrack-source count; >= 2
1848/// switches on failure memoization.
1849fn match_segment<'p>(
1850    elems: &[Element],
1851    ambiguity: usize,
1852    raw_seg: &[u8],
1853    opts: PathMatchOptions,
1854    sink: &mut Sink<'_, 'p>,
1855) -> bool {
1856    let decoded = maybe_decode(raw_seg, opts.percent_decode);
1857    if ambiguity >= 2 {
1858        let mut memo = ElemMemo::new(elems.len(), decoded.len());
1859        match_elems(elems, &decoded, opts, sink, &mut Some(&mut memo))
1860    } else {
1861        match_elems(elems, &decoded, opts, sink, &mut None)
1862    }
1863}
1864
1865/// Failure memo for within-segment matching, keyed on the *remaining*
1866/// `(element, hay-byte)` counts.
1867struct ElemMemo {
1868    grid: BitGrid,
1869    base_elems: usize,
1870    base_hay: usize,
1871}
1872
1873impl ElemMemo {
1874    fn new(n_elems: usize, n_hay: usize) -> Self {
1875        Self {
1876            grid: BitGrid::new(n_elems + 1, n_hay + 1),
1877            base_elems: n_elems,
1878            base_hay: n_hay,
1879        }
1880    }
1881    fn is_failed(&self, elems_left: usize, hay_left: usize) -> bool {
1882        self.grid
1883            .get(self.base_elems - elems_left, self.base_hay - hay_left)
1884    }
1885    fn mark_failed(&mut self, elems_left: usize, hay_left: usize) {
1886        self.grid
1887            .set(self.base_elems - elems_left, self.base_hay - hay_left);
1888    }
1889}
1890
1891/// Greedy-with-backtracking match of `elems` against the (already decoded)
1892/// `hay` bytes. Captures record decoded substrings. `memo`, when present,
1893/// caches failed `(elems, hay)` states so the recursion stays polynomial.
1894fn match_elems<'p>(
1895    elems: &[Element],
1896    hay: &[u8],
1897    opts: PathMatchOptions,
1898    sink: &mut Sink<'_, 'p>,
1899    memo: &mut Option<&mut ElemMemo>,
1900) -> bool {
1901    if let Some(m) = memo
1902        && m.is_failed(elems.len(), hay.len())
1903    {
1904        return false;
1905    }
1906
1907    let matched = match elems.split_first() {
1908        None => hay.is_empty(),
1909        Some((el, rest)) => match &el.kind {
1910            ElementKind::Literal(lit) => {
1911                (byte_starts_with(hay, lit, opts.ignore_ascii_case)
1912                    && match_elems(rest, &hay[lit.len()..], opts, sink, memo))
1913                    // Optional literal: skip it entirely.
1914                    || (el.optional && match_elems(rest, hay, opts, sink, memo))
1915            }
1916            ElementKind::Star => {
1917                match_run(None, rest, hay, opts, sink, memo)
1918                    || (el.optional && match_elems(rest, hay, opts, sink, memo))
1919            }
1920            ElementKind::Capture {
1921                name_start,
1922                name_len,
1923            } => {
1924                match_run(Some((*name_start, *name_len)), rest, hay, opts, sink, memo)
1925                    || (el.optional
1926                        && match_empty_capture(
1927                            (*name_start, *name_len),
1928                            rest,
1929                            hay,
1930                            opts,
1931                            sink,
1932                            memo,
1933                        ))
1934            }
1935        },
1936    };
1937
1938    if !matched && let Some(m) = memo {
1939        m.mark_failed(elems.len(), hay.len());
1940    }
1941    matched
1942}
1943
1944/// Match a wildcard run (anonymous `{}` or named capture) followed by `rest`.
1945/// Greedy: try the longest run first, shrinking on backtrack. For a named
1946/// capture, record the matched (decoded) substring as a binding.
1947fn match_run<'p>(
1948    name: Option<(usize, usize)>,
1949    rest: &[Element],
1950    hay: &[u8],
1951    opts: PathMatchOptions,
1952    sink: &mut Sink<'_, 'p>,
1953    memo: &mut Option<&mut ElemMemo>,
1954) -> bool {
1955    // Try every split point, longest run first (greedy).
1956    for take in (1..=hay.len()).rev() {
1957        let mark = sink.len();
1958        if match_elems(rest, &hay[take..], opts, sink, memo) {
1959            if let Some((name_start, name_len)) = name {
1960                // `hay` is already decoded; just own the slice.
1961                let value = decoded_owned(&hay[..take]);
1962                // Insert before whatever `rest` recorded so order stays L-to-R.
1963                sink.insert_at(
1964                    mark,
1965                    Binding {
1966                        name_start,
1967                        name_len,
1968                        value,
1969                        is_glob: false,
1970                    },
1971                );
1972            }
1973            return true;
1974        }
1975        sink.truncate(mark);
1976    }
1977    false
1978}
1979
1980/// Match an optional named capture as an empty run, recording an empty binding
1981/// only on the successful tail path.
1982fn match_empty_capture<'p>(
1983    name: (usize, usize),
1984    rest: &[Element],
1985    hay: &[u8],
1986    opts: PathMatchOptions,
1987    sink: &mut Sink<'_, 'p>,
1988    memo: &mut Option<&mut ElemMemo>,
1989) -> bool {
1990    let mark = sink.len();
1991    if match_elems(rest, hay, opts, sink, memo) {
1992        sink.insert_at(
1993            mark,
1994            Binding {
1995                name_start: name.0,
1996                name_len: name.1,
1997                value: Cow::Borrowed(""),
1998                is_glob: false,
1999            },
2000        );
2001        true
2002    } else {
2003        sink.truncate(mark);
2004        false
2005    }
2006}
2007
2008/// '/'-join decoded segment values into an owned string, in a single pass
2009/// (no intermediate `Vec<String>`).
2010fn join_decoded<'p>(segs: &[EncodedSegment<'p>], decode: bool) -> Cow<'p, str> {
2011    // Decoded length ≤ raw length; pre-size for raw bytes plus separators.
2012    let cap = segs.iter().map(|s| s.len()).sum::<usize>() + segs.len();
2013    let mut out = String::with_capacity(cap);
2014    for (i, s) in segs.iter().enumerate() {
2015        if i > 0 {
2016            out.push('/');
2017        }
2018        out.push_str(&String::from_utf8_lossy(&maybe_decode(
2019            s.as_ref().as_bytes(),
2020            decode,
2021        )));
2022    }
2023    Cow::Owned(out)
2024}
2025
2026/// Own an already-decoded byte slice as a string, replacing invalid UTF-8
2027/// (reachable: a decoded `%ff` is byte `0xFF`) with U+FFFD.
2028fn decoded_owned<'p>(bytes: &[u8]) -> Cow<'p, str> {
2029    Cow::Owned(String::from_utf8_lossy(bytes).into_owned())
2030}