Skip to main content

actix_router/
resource.rs

1use std::{
2    borrow::{Borrow, Cow},
3    collections::HashMap,
4    hash::{BuildHasher, Hash, Hasher},
5    mem,
6};
7
8use tracing::error;
9
10use crate::{
11    path::PathItem,
12    regex_set::{escape, Regex, RegexSet},
13    IntoPatterns, Patterns, Resource, ResourcePath,
14};
15
16const MAX_DYNAMIC_SEGMENTS: usize = 16;
17
18/// Regex flags to allow '.' in regex to match '\n'
19///
20/// See the docs under: https://docs.rs/regex/1/regex/#grouping-and-flags
21const REGEX_FLAGS: &str = "(?s-m)";
22
23/// Describes the set of paths that match to a resource.
24///
25/// `ResourceDef`s are effectively a way to transform the a custom resource pattern syntax into
26/// suitable regular expressions from which to check matches with paths and capture portions of a
27/// matched path into variables. Common cases are on a fast path that avoids going through the
28/// regex engine.
29///
30///
31/// # Pattern Format and Matching Behavior
32/// Resource pattern is defined as a string of zero or more _segments_ where each segment is
33/// preceded by a slash `/`.
34///
35/// This means that pattern string __must__ either be empty or begin with a slash (`/`). This also
36/// implies that a trailing slash in pattern defines an empty segment. For example, the pattern
37/// `"/user/"` has two segments: `["user", ""]`
38///
39/// A key point to understand is that `ResourceDef` matches segments, not strings. Segments are
40/// matched individually. For example, the pattern `/user/` is not considered a prefix for the path
41/// `/user/123/456`, because the second segment doesn't match: `["user", ""]`
42/// vs `["user", "123", "456"]`.
43///
44/// This definition is consistent with the definition of absolute URL path in
45/// [RFC 3986 ยง3.3](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3)
46///
47///
48/// # Static Resources
49/// A static resource is the most basic type of definition. Pass a pattern to [new][Self::new].
50/// Conforming paths must match the pattern exactly.
51///
52/// ## Examples
53/// ```
54/// # use actix_router::ResourceDef;
55/// let resource = ResourceDef::new("/home");
56///
57/// assert!(resource.is_match("/home"));
58///
59/// assert!(!resource.is_match("/home/"));
60/// assert!(!resource.is_match("/home/new"));
61/// assert!(!resource.is_match("/homes"));
62/// assert!(!resource.is_match("/search"));
63/// ```
64///
65/// # Dynamic Segments
66/// Also known as "path parameters". Resources can define sections of a pattern that be extracted
67/// from a conforming path, if it conforms to (one of) the resource pattern(s).
68///
69/// The marker for a dynamic segment is curly braces wrapping an identifier. For example,
70/// `/user/{id}` would match paths like `/user/123` or `/user/james` and be able to extract the user
71/// IDs "123" and "james", respectively.
72///
73/// However, this resource pattern (`/user/{id}`) would, not cover `/user/123/stars` (unless
74/// constructed as a prefix; see next section) since the default pattern for segments matches all
75/// characters until it finds a `/` character (or the end of the path). Custom segment patterns are
76/// covered further down.
77///
78/// Dynamic segments do not need to be delimited by `/` characters, they can be defined within a
79/// path segment. For example, `/rust-is-{opinion}` can match the paths `/rust-is-cool` and
80/// `/rust-is-hard`.
81///
82/// For information on capturing segment values from paths or other custom resource types,
83/// see [`capture_match_info`][Self::capture_match_info]
84/// and [`capture_match_info_fn`][Self::capture_match_info_fn].
85///
86/// A resource definition can contain at most 16 dynamic segments.
87///
88/// ## Examples
89/// ```
90/// use actix_router::{Path, ResourceDef};
91///
92/// let resource = ResourceDef::prefix("/user/{id}");
93///
94/// assert!(resource.is_match("/user/123"));
95/// assert!(!resource.is_match("/user"));
96/// assert!(!resource.is_match("/user/"));
97///
98/// let mut path = Path::new("/user/123");
99/// resource.capture_match_info(&mut path);
100/// assert_eq!(path.get("id").unwrap(), "123");
101/// ```
102///
103/// # Prefix Resources
104/// A prefix resource is defined as pattern that can match just the start of a path, up to a
105/// segment boundary.
106///
107/// Prefix patterns with a trailing slash may have an unexpected, though correct, behavior.
108/// They define and therefore require an empty segment in order to match. It is easier to understand
109/// this behavior after reading the [matching behavior section]. Examples are given below.
110///
111/// The empty pattern (`""`), as a prefix, matches any path.
112///
113/// Prefix resources can contain dynamic segments.
114///
115/// ## Examples
116/// ```
117/// # use actix_router::ResourceDef;
118/// let resource = ResourceDef::prefix("/home");
119/// assert!(resource.is_match("/home"));
120/// assert!(resource.is_match("/home/new"));
121/// assert!(!resource.is_match("/homes"));
122///
123/// // prefix pattern with a trailing slash
124/// let resource = ResourceDef::prefix("/user/{id}/");
125/// assert!(resource.is_match("/user/123/"));
126/// assert!(resource.is_match("/user/123//stars"));
127/// assert!(!resource.is_match("/user/123/stars"));
128/// assert!(!resource.is_match("/user/123"));
129/// ```
130///
131/// # Custom Regex Segments
132/// Dynamic segments can be customised to only match a specific regular expression. It can be
133/// helpful to do this if resource definitions would otherwise conflict and cause one to
134/// be inaccessible.
135///
136/// The regex used when capturing segment values can be specified explicitly using this syntax:
137/// `{name:regex}`. For example, `/user/{id:\d+}` will only match paths where the user ID
138/// is numeric.
139///
140/// The regex could potentially match multiple segments. If this is not wanted, then care must be
141/// taken to avoid matching a slash `/`. It is guaranteed, however, that the match ends at a
142/// segment boundary; the pattern `r"(/|$)` is always appended to the regex.
143///
144/// By default, dynamic segments use this regex: `[^/]+`. This shows why it is the case, as shown in
145/// the earlier section, that segments capture a slice of the path up to the next `/` character.
146///
147/// Custom regex segments can be used in static and prefix resource definition variants.
148///
149/// ## Examples
150/// ```
151/// # use actix_router::ResourceDef;
152/// let resource = ResourceDef::new(r"/user/{id:\d+}");
153/// assert!(resource.is_match("/user/123"));
154/// assert!(resource.is_match("/user/314159"));
155/// assert!(!resource.is_match("/user/abc"));
156/// ```
157///
158/// # Tail Segments
159/// As a shortcut to defining a custom regex for matching _all_ remaining characters (not just those
160/// up until a `/` character), there is a special pattern to match (and capture) the remaining
161/// path portion.
162///
163/// To do this, use the segment pattern: `{name}*`. Since a tail segment also has a name, values are
164/// extracted in the same way as non-tail dynamic segments.
165///
166/// ## Examples
167/// ```
168/// # use actix_router::{Path, ResourceDef};
169/// let resource = ResourceDef::new("/blob/{tail}*");
170/// assert!(resource.is_match("/blob/HEAD/Cargo.toml"));
171/// assert!(resource.is_match("/blob/HEAD/README.md"));
172///
173/// let mut path = Path::new("/blob/main/LICENSE");
174/// resource.capture_match_info(&mut path);
175/// assert_eq!(path.get("tail").unwrap(), "main/LICENSE");
176/// ```
177///
178/// # Multi-Pattern Resources
179/// For resources that can map to multiple distinct paths, it may be suitable to use
180/// multi-pattern resources by passing an array/vec to [`new`][Self::new]. They will be combined
181/// into a regex set which is usually quicker to check matches on than checking each
182/// pattern individually.
183///
184/// Multi-pattern resources can contain dynamic segments just like single pattern ones.
185/// However, take care to use consistent and semantically-equivalent segment names; it could affect
186/// expectations in the router using these definitions and cause runtime panics.
187///
188/// ## Examples
189/// ```
190/// # use actix_router::ResourceDef;
191/// let resource = ResourceDef::new(["/home", "/index"]);
192/// assert!(resource.is_match("/home"));
193/// assert!(resource.is_match("/index"));
194/// ```
195///
196/// # Trailing Slashes
197/// It should be noted that this library takes no steps to normalize intra-path or trailing slashes.
198/// As such, all resource definitions implicitly expect a pre-processing step to normalize paths if
199/// you wish to accommodate "recoverable" path errors. Below are several examples of resource-path
200/// pairs that would not be compatible.
201///
202/// ## Examples
203/// ```
204/// # use actix_router::ResourceDef;
205/// assert!(!ResourceDef::new("/root").is_match("/root/"));
206/// assert!(!ResourceDef::new("/root/").is_match("/root"));
207/// assert!(!ResourceDef::prefix("/root/").is_match("/root"));
208/// ```
209///
210/// [matching behavior section]: #pattern-format-and-matching-behavior
211#[derive(Clone, Debug)]
212pub struct ResourceDef {
213    id: u16,
214
215    /// Optional name of resource.
216    name: Option<String>,
217
218    /// Pattern that generated the resource definition.
219    patterns: Patterns,
220
221    is_prefix: bool,
222
223    /// Pattern type.
224    pat_type: PatternType,
225
226    /// List of segments that compose the pattern, in order.
227    segments: Vec<PatternSegment>,
228}
229
230#[derive(Debug, Clone, PartialEq)]
231enum PatternSegment {
232    /// Literal slice of pattern.
233    Const(String),
234
235    /// Name of dynamic segment.
236    Var(String),
237}
238
239#[derive(Debug, Clone)]
240#[allow(clippy::large_enum_variant)]
241enum PatternType {
242    /// Single constant/literal segment.
243    Static(String),
244
245    /// Single regular expression and list of dynamic segment names.
246    Dynamic(Regex, Vec<&'static str>),
247
248    /// Regular expression set and list of component expressions plus dynamic segment names.
249    DynamicSet(RegexSet, Vec<(Regex, Vec<&'static str>)>),
250}
251
252impl ResourceDef {
253    /// Constructs a new resource definition from patterns.
254    ///
255    /// Multi-pattern resources can be constructed by providing a slice (or vec) of patterns.
256    ///
257    /// # Panics
258    /// Panics if any path patterns are malformed.
259    ///
260    /// # Examples
261    /// ```
262    /// use actix_router::ResourceDef;
263    ///
264    /// let resource = ResourceDef::new("/user/{id}");
265    /// assert!(resource.is_match("/user/123"));
266    /// assert!(!resource.is_match("/user/123/stars"));
267    /// assert!(!resource.is_match("user/1234"));
268    /// assert!(!resource.is_match("/foo"));
269    ///
270    /// let resource = ResourceDef::new(["/profile", "/user/{id}"]);
271    /// assert!(resource.is_match("/profile"));
272    /// assert!(resource.is_match("/user/123"));
273    /// assert!(!resource.is_match("user/123"));
274    /// assert!(!resource.is_match("/foo"));
275    /// ```
276    pub fn new<T: IntoPatterns>(paths: T) -> Self {
277        Self::construct(paths, false)
278    }
279
280    /// Constructs a new resource definition using a pattern that performs prefix matching.
281    ///
282    /// More specifically, the regular expressions generated for matching are different when using
283    /// this method vs using `new`; they will not be appended with the `$` meta-character that
284    /// matches the end of an input.
285    ///
286    /// Although it will compile and run correctly, it is meaningless to construct a prefix
287    /// resource definition with a tail segment; use [`new`][Self::new] in this case.
288    ///
289    /// # Panics
290    /// Panics if path pattern is malformed.
291    ///
292    /// # Examples
293    /// ```
294    /// use actix_router::ResourceDef;
295    ///
296    /// let resource = ResourceDef::prefix("/user/{id}");
297    /// assert!(resource.is_match("/user/123"));
298    /// assert!(resource.is_match("/user/123/stars"));
299    /// assert!(!resource.is_match("user/123"));
300    /// assert!(!resource.is_match("user/123/stars"));
301    /// assert!(!resource.is_match("/foo"));
302    /// ```
303    pub fn prefix<T: IntoPatterns>(paths: T) -> Self {
304        ResourceDef::construct(paths, true)
305    }
306
307    /// Constructs a new resource definition using a string pattern that performs prefix matching,
308    /// ensuring a leading `/` if pattern is not empty.
309    ///
310    /// # Panics
311    /// Panics if path pattern is malformed.
312    ///
313    /// # Examples
314    /// ```
315    /// use actix_router::ResourceDef;
316    ///
317    /// let resource = ResourceDef::root_prefix("user/{id}");
318    ///
319    /// assert_eq!(&resource, &ResourceDef::prefix("/user/{id}"));
320    /// assert_eq!(&resource, &ResourceDef::root_prefix("/user/{id}"));
321    /// assert_ne!(&resource, &ResourceDef::new("user/{id}"));
322    /// assert_ne!(&resource, &ResourceDef::new("/user/{id}"));
323    ///
324    /// assert!(resource.is_match("/user/123"));
325    /// assert!(!resource.is_match("user/123"));
326    /// ```
327    pub fn root_prefix(path: &str) -> Self {
328        ResourceDef::prefix(insert_slash(path).into_owned())
329    }
330
331    /// Returns a numeric resource ID.
332    ///
333    /// If not explicitly set using [`set_id`][Self::set_id], this will return `0`.
334    ///
335    /// # Examples
336    /// ```
337    /// # use actix_router::ResourceDef;
338    /// let mut resource = ResourceDef::new("/root");
339    /// assert_eq!(resource.id(), 0);
340    ///
341    /// resource.set_id(42);
342    /// assert_eq!(resource.id(), 42);
343    /// ```
344    pub fn id(&self) -> u16 {
345        self.id
346    }
347
348    /// Set numeric resource ID.
349    ///
350    /// # Examples
351    /// ```
352    /// # use actix_router::ResourceDef;
353    /// let mut resource = ResourceDef::new("/root");
354    /// resource.set_id(42);
355    /// assert_eq!(resource.id(), 42);
356    /// ```
357    pub fn set_id(&mut self, id: u16) {
358        self.id = id;
359    }
360
361    /// Returns resource definition name, if set.
362    ///
363    /// # Examples
364    /// ```
365    /// # use actix_router::ResourceDef;
366    /// let mut resource = ResourceDef::new("/root");
367    /// assert!(resource.name().is_none());
368    ///
369    /// resource.set_name("root");
370    /// assert_eq!(resource.name().unwrap(), "root");
371    pub fn name(&self) -> Option<&str> {
372        self.name.as_deref()
373    }
374
375    /// Assigns a new name to the resource.
376    ///
377    /// # Panics
378    /// Panics if `name` is an empty string.
379    ///
380    /// # Examples
381    /// ```
382    /// # use actix_router::ResourceDef;
383    /// let mut resource = ResourceDef::new("/root");
384    /// resource.set_name("root");
385    /// assert_eq!(resource.name().unwrap(), "root");
386    /// ```
387    pub fn set_name(&mut self, name: impl Into<String>) {
388        let name = name.into();
389
390        assert!(!name.is_empty(), "resource name should not be empty");
391
392        self.name = Some(name)
393    }
394
395    /// Returns `true` if pattern type is prefix.
396    ///
397    /// # Examples
398    /// ```
399    /// # use actix_router::ResourceDef;
400    /// assert!(ResourceDef::prefix("/user").is_prefix());
401    /// assert!(!ResourceDef::new("/user").is_prefix());
402    /// ```
403    pub fn is_prefix(&self) -> bool {
404        self.is_prefix
405    }
406
407    /// Returns the pattern string that generated the resource definition.
408    ///
409    /// If definition is constructed with multiple patterns, the first pattern is returned. To get
410    /// all patterns, use [`patterns_iter`][Self::pattern_iter]. If resource has 0 patterns,
411    /// returns `None`.
412    ///
413    /// # Examples
414    /// ```
415    /// # use actix_router::ResourceDef;
416    /// let mut resource = ResourceDef::new("/user/{id}");
417    /// assert_eq!(resource.pattern().unwrap(), "/user/{id}");
418    ///
419    /// let mut resource = ResourceDef::new(["/profile", "/user/{id}"]);
420    /// assert_eq!(resource.pattern(), Some("/profile"));
421    pub fn pattern(&self) -> Option<&str> {
422        match &self.patterns {
423            Patterns::Single(pattern) => Some(pattern.as_str()),
424            Patterns::List(patterns) => patterns.first().map(AsRef::as_ref),
425        }
426    }
427
428    /// Returns iterator of pattern strings that generated the resource definition.
429    ///
430    /// # Examples
431    /// ```
432    /// # use actix_router::ResourceDef;
433    /// let mut resource = ResourceDef::new("/root");
434    /// let mut iter = resource.pattern_iter();
435    /// assert_eq!(iter.next().unwrap(), "/root");
436    /// assert!(iter.next().is_none());
437    ///
438    /// let mut resource = ResourceDef::new(["/root", "/backup"]);
439    /// let mut iter = resource.pattern_iter();
440    /// assert_eq!(iter.next().unwrap(), "/root");
441    /// assert_eq!(iter.next().unwrap(), "/backup");
442    /// assert!(iter.next().is_none());
443    pub fn pattern_iter(&self) -> impl Iterator<Item = &str> {
444        struct PatternIter<'a> {
445            patterns: &'a Patterns,
446            list_idx: usize,
447            done: bool,
448        }
449
450        impl<'a> Iterator for PatternIter<'a> {
451            type Item = &'a str;
452
453            fn next(&mut self) -> Option<Self::Item> {
454                match &self.patterns {
455                    Patterns::Single(pattern) => {
456                        if self.done {
457                            return None;
458                        }
459
460                        self.done = true;
461                        Some(pattern.as_str())
462                    }
463                    Patterns::List(patterns) if patterns.is_empty() => None,
464                    Patterns::List(patterns) => match patterns.get(self.list_idx) {
465                        Some(pattern) => {
466                            self.list_idx += 1;
467                            Some(pattern.as_str())
468                        }
469                        None => {
470                            // fast path future call
471                            self.done = true;
472                            None
473                        }
474                    },
475                }
476            }
477
478            fn size_hint(&self) -> (usize, Option<usize>) {
479                match &self.patterns {
480                    Patterns::Single(_) => (1, Some(1)),
481                    Patterns::List(patterns) => (patterns.len(), Some(patterns.len())),
482                }
483            }
484        }
485
486        PatternIter {
487            patterns: &self.patterns,
488            list_idx: 0,
489            done: false,
490        }
491    }
492
493    /// Joins two resources.
494    ///
495    /// Resulting resource is prefix if `other` is prefix.
496    ///
497    /// # Examples
498    /// ```
499    /// # use actix_router::ResourceDef;
500    /// let joined = ResourceDef::prefix("/root").join(&ResourceDef::prefix("/seg"));
501    /// assert_eq!(joined, ResourceDef::prefix("/root/seg"));
502    /// ```
503    pub fn join(&self, other: &ResourceDef) -> ResourceDef {
504        let patterns = self
505            .pattern_iter()
506            .flat_map(move |this| other.pattern_iter().map(move |other| (this, other)))
507            .map(|(this, other)| {
508                let mut pattern = String::with_capacity(this.len() + other.len());
509                pattern.push_str(this);
510                pattern.push_str(other);
511                pattern
512            })
513            .collect::<Vec<_>>();
514
515        match patterns.len() {
516            1 => ResourceDef::construct(&patterns[0], other.is_prefix()),
517            _ => ResourceDef::construct(patterns, other.is_prefix()),
518        }
519    }
520
521    /// Returns `true` if `path` matches this resource.
522    ///
523    /// The behavior of this method depends on how the `ResourceDef` was constructed. For example,
524    /// static resources will not be able to match as many paths as dynamic and prefix resources.
525    /// See [`ResourceDef`] struct docs for details on resource definition types.
526    ///
527    /// This method will always agree with [`find_match`][Self::find_match] on whether the path
528    /// matches or not.
529    ///
530    /// # Examples
531    /// ```
532    /// use actix_router::ResourceDef;
533    ///
534    /// // static resource
535    /// let resource = ResourceDef::new("/user");
536    /// assert!(resource.is_match("/user"));
537    /// assert!(!resource.is_match("/users"));
538    /// assert!(!resource.is_match("/user/123"));
539    /// assert!(!resource.is_match("/foo"));
540    ///
541    /// // dynamic resource
542    /// let resource = ResourceDef::new("/user/{user_id}");
543    /// assert!(resource.is_match("/user/123"));
544    /// assert!(!resource.is_match("/user/123/stars"));
545    ///
546    /// // prefix resource
547    /// let resource = ResourceDef::prefix("/root");
548    /// assert!(resource.is_match("/root"));
549    /// assert!(resource.is_match("/root/leaf"));
550    /// assert!(!resource.is_match("/roots"));
551    ///
552    /// // more examples are shown in the `ResourceDef` struct docs
553    /// ```
554    #[inline]
555    pub fn is_match(&self, path: &str) -> bool {
556        // this function could be expressed as:
557        // `self.find_match(path).is_some()`
558        // but this skips some checks and uses potentially faster regex methods
559
560        match &self.pat_type {
561            PatternType::Static(pattern) => self.static_match(pattern, path).is_some(),
562            PatternType::Dynamic(re, _) => re.is_match(path),
563            PatternType::DynamicSet(re, _) => re.is_match(path),
564        }
565    }
566
567    /// Tries to match `path` to this resource, returning the position in the path where the
568    /// match ends.
569    ///
570    /// This method will always agree with [`is_match`][Self::is_match] on whether the path matches
571    /// or not.
572    ///
573    /// # Examples
574    /// ```
575    /// use actix_router::ResourceDef;
576    ///
577    /// // static resource
578    /// let resource = ResourceDef::new("/user");
579    /// assert_eq!(resource.find_match("/user"), Some(5));
580    /// assert!(resource.find_match("/user/").is_none());
581    /// assert!(resource.find_match("/user/123").is_none());
582    /// assert!(resource.find_match("/foo").is_none());
583    ///
584    /// // constant prefix resource
585    /// let resource = ResourceDef::prefix("/user");
586    /// assert_eq!(resource.find_match("/user"), Some(5));
587    /// assert_eq!(resource.find_match("/user/"), Some(5));
588    /// assert_eq!(resource.find_match("/user/123"), Some(5));
589    ///
590    /// // dynamic prefix resource
591    /// let resource = ResourceDef::prefix("/user/{id}");
592    /// assert_eq!(resource.find_match("/user/123"), Some(9));
593    /// assert_eq!(resource.find_match("/user/1234/"), Some(10));
594    /// assert_eq!(resource.find_match("/user/12345/stars"), Some(11));
595    /// assert!(resource.find_match("/user/").is_none());
596    ///
597    /// // multi-pattern resource
598    /// let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]);
599    /// assert_eq!(resource.find_match("/user/123"), Some(9));
600    /// assert_eq!(resource.find_match("/profile/1234"), Some(13));
601    /// ```
602    pub fn find_match(&self, path: &str) -> Option<usize> {
603        match &self.pat_type {
604            PatternType::Static(pattern) => self.static_match(pattern, path),
605
606            PatternType::Dynamic(re, _) => Some(re.captures(path)?[1].len()),
607
608            PatternType::DynamicSet(re, params) => {
609                let idx = re.first_match_idx(path)?;
610                let (ref pattern, _) = params[idx];
611                Some(pattern.captures(path)?[1].len())
612            }
613        }
614    }
615
616    /// Collects dynamic segment values into `resource`.
617    ///
618    /// Returns `true` if `path` matches this resource.
619    ///
620    /// # Examples
621    /// ```
622    /// use actix_router::{Path, ResourceDef};
623    ///
624    /// let resource = ResourceDef::prefix("/user/{id}");
625    /// let mut path = Path::new("/user/123/stars");
626    /// assert!(resource.capture_match_info(&mut path));
627    /// assert_eq!(path.get("id").unwrap(), "123");
628    /// assert_eq!(path.unprocessed(), "/stars");
629    ///
630    /// let resource = ResourceDef::new("/blob/{path}*");
631    /// let mut path = Path::new("/blob/HEAD/Cargo.toml");
632    /// assert!(resource.capture_match_info(&mut path));
633    /// assert_eq!(path.get("path").unwrap(), "HEAD/Cargo.toml");
634    /// assert_eq!(path.unprocessed(), "");
635    /// ```
636    pub fn capture_match_info<R: Resource>(&self, resource: &mut R) -> bool {
637        self.capture_match_info_fn(resource, |_| true)
638    }
639
640    /// Collects dynamic segment values into `resource` after matching paths and executing
641    /// check function.
642    ///
643    /// The check function is given a reference to the passed resource and optional arbitrary data.
644    /// This is useful if you want to conditionally match on some non-path related aspect of the
645    /// resource type.
646    ///
647    /// Returns `true` if resource path matches this resource definition _and_ satisfies the
648    /// given check function.
649    ///
650    /// # Examples
651    /// ```
652    /// use actix_router::{Path, ResourceDef};
653    ///
654    /// fn try_match(resource: &ResourceDef, path: &mut Path<&str>) -> bool {
655    ///     let admin_allowed = std::env::var("ADMIN_ALLOWED").is_ok();
656    ///
657    ///     resource.capture_match_info_fn(
658    ///         path,
659    ///         // when env var is not set, reject when path contains "admin"
660    ///         |path| !(!admin_allowed && path.as_str().contains("admin")),
661    ///     )
662    /// }
663    ///
664    /// let resource = ResourceDef::prefix("/user/{id}");
665    ///
666    /// // path matches; segment values are collected into path
667    /// let mut path = Path::new("/user/james/stars");
668    /// assert!(try_match(&resource, &mut path));
669    /// assert_eq!(path.get("id").unwrap(), "james");
670    /// assert_eq!(path.unprocessed(), "/stars");
671    ///
672    /// // path matches but fails check function; no segments are collected
673    /// let mut path = Path::new("/user/admin/stars");
674    /// assert!(!try_match(&resource, &mut path));
675    /// assert_eq!(path.unprocessed(), "/user/admin/stars");
676    /// ```
677    pub fn capture_match_info_fn<R, F>(&self, resource: &mut R, check_fn: F) -> bool
678    where
679        R: Resource,
680        F: FnOnce(&R) -> bool,
681    {
682        let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default();
683        let path = resource.resource_path();
684        let path_str = path.unprocessed();
685
686        let (matched_len, matched_vars) = match &self.pat_type {
687            PatternType::Static(pattern) => match self.static_match(pattern, path_str) {
688                Some(len) => (len, None),
689                None => return false,
690            },
691
692            PatternType::Dynamic(re, names) => {
693                let captures = match re.captures(path.unprocessed()) {
694                    Some(captures) => captures,
695                    _ => return false,
696                };
697
698                for (no, name) in names.iter().enumerate() {
699                    if let Some(m) = captures.name(name) {
700                        segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
701                    } else {
702                        error!("Dynamic path match but not all segments found: {}", name);
703                        return false;
704                    }
705                }
706
707                (captures[1].len(), Some(names))
708            }
709
710            PatternType::DynamicSet(re, params) => {
711                let path = path.unprocessed();
712                let (pattern, names) = match re.first_match_idx(path) {
713                    Some(idx) => &params[idx],
714                    _ => return false,
715                };
716
717                let captures = match pattern.captures(path.path()) {
718                    Some(captures) => captures,
719                    _ => return false,
720                };
721
722                for (no, name) in names.iter().enumerate() {
723                    if let Some(m) = captures.name(name) {
724                        segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
725                    } else {
726                        error!("Dynamic path match but not all segments found: {}", name);
727                        return false;
728                    }
729                }
730
731                (captures[1].len(), Some(names))
732            }
733        };
734
735        if !check_fn(resource) {
736            return false;
737        }
738
739        // Modify `path` to skip matched part and store matched segments
740        let path = resource.resource_path();
741
742        if let Some(vars) = matched_vars {
743            for i in 0..vars.len() {
744                path.add(vars[i], mem::take(&mut segments[i]));
745            }
746        }
747
748        path.skip(matched_len as u16);
749
750        true
751    }
752
753    /// Assembles resource path using a closure that maps variable segment names to values.
754    fn build_resource_path<F, I>(&self, path: &mut String, mut vars: F) -> bool
755    where
756        F: FnMut(&str) -> Option<I>,
757        I: AsRef<str>,
758    {
759        for segment in &self.segments {
760            match segment {
761                PatternSegment::Const(val) => path.push_str(val),
762                PatternSegment::Var(name) => match vars(name) {
763                    Some(val) => path.push_str(val.as_ref()),
764                    _ => return false,
765                },
766            }
767        }
768
769        true
770    }
771
772    /// Assembles full resource path from iterator of dynamic segment values.
773    ///
774    /// Returns `true` on success.
775    ///
776    /// For multi-pattern resources, the first pattern is used under the assumption that it would be
777    /// equivalent to any other choice.
778    ///
779    /// # Examples
780    /// ```
781    /// # use actix_router::ResourceDef;
782    /// let mut s = String::new();
783    /// let resource = ResourceDef::new("/user/{id}/post/{title}");
784    ///
785    /// assert!(resource.resource_path_from_iter(&mut s, &["123", "my-post"]));
786    /// assert_eq!(s, "/user/123/post/my-post");
787    /// ```
788    pub fn resource_path_from_iter<I>(&self, path: &mut String, values: I) -> bool
789    where
790        I: IntoIterator,
791        I::Item: AsRef<str>,
792    {
793        let mut iter = values.into_iter();
794        self.build_resource_path(path, |_| iter.next())
795    }
796
797    /// Assembles resource path from map of dynamic segment values.
798    ///
799    /// Returns `true` on success.
800    ///
801    /// For multi-pattern resources, the first pattern is used under the assumption that it would be
802    /// equivalent to any other choice.
803    ///
804    /// # Examples
805    /// ```
806    /// # use std::collections::HashMap;
807    /// # use actix_router::ResourceDef;
808    /// let mut s = String::new();
809    /// let resource = ResourceDef::new("/user/{id}/post/{title}");
810    ///
811    /// let mut map = HashMap::new();
812    /// map.insert("id", "123");
813    /// map.insert("title", "my-post");
814    ///
815    /// assert!(resource.resource_path_from_map(&mut s, &map));
816    /// assert_eq!(s, "/user/123/post/my-post");
817    /// ```
818    pub fn resource_path_from_map<K, V, S>(
819        &self,
820        path: &mut String,
821        values: &HashMap<K, V, S>,
822    ) -> bool
823    where
824        K: Borrow<str> + Eq + Hash,
825        V: AsRef<str>,
826        S: BuildHasher,
827    {
828        self.build_resource_path(path, |name| values.get(name))
829    }
830
831    /// Returns true if `prefix` acts as a proper prefix (i.e., separated by a slash) in `path`.
832    fn static_match(&self, pattern: &str, path: &str) -> Option<usize> {
833        let rem = path.strip_prefix(pattern)?;
834
835        match self.is_prefix {
836            // resource is not a prefix so an exact match is needed
837            false if rem.is_empty() => Some(pattern.len()),
838
839            // resource is a prefix so rem should start with a path delimiter
840            true if rem.is_empty() || rem.starts_with('/') => Some(pattern.len()),
841
842            // otherwise, no match
843            _ => None,
844        }
845    }
846
847    fn construct<T: IntoPatterns>(paths: T, is_prefix: bool) -> Self {
848        let patterns = paths.patterns();
849
850        let (pat_type, segments) = match &patterns {
851            Patterns::Single(pattern) => ResourceDef::parse(pattern, is_prefix, false),
852
853            // since zero length pattern sets are possible
854            // just return a useless `ResourceDef`
855            Patterns::List(patterns) if patterns.is_empty() => (
856                PatternType::DynamicSet(RegexSet::empty(), Vec::new()),
857                Vec::new(),
858            ),
859
860            Patterns::List(patterns) => {
861                let mut re_set = Vec::with_capacity(patterns.len());
862                let mut pattern_data = Vec::new();
863                let mut segments = None;
864
865                for pattern in patterns {
866                    match ResourceDef::parse(pattern, is_prefix, true) {
867                        (PatternType::Dynamic(re, names), segs) => {
868                            re_set.push(re.as_str().to_owned());
869                            pattern_data.push((re, names));
870                            segments.get_or_insert(segs);
871                        }
872                        _ => unreachable!(),
873                    }
874                }
875
876                let pattern_re_set = RegexSet::new(re_set);
877                let segments = segments.unwrap_or_default();
878
879                (
880                    PatternType::DynamicSet(pattern_re_set, pattern_data),
881                    segments,
882                )
883            }
884        };
885
886        ResourceDef {
887            id: 0,
888            name: None,
889            patterns,
890            is_prefix,
891            pat_type,
892            segments,
893        }
894    }
895
896    /// Parses a dynamic segment definition from a pattern.
897    ///
898    /// The returned tuple includes:
899    /// - the segment descriptor, either `Var` or `Tail`
900    /// - the segment's regex to check values against
901    /// - the remaining, unprocessed string slice
902    /// - whether the parsed parameter represents a tail pattern
903    ///
904    /// # Panics
905    /// Panics if given patterns does not contain a dynamic segment.
906    fn parse_param(pattern: &str) -> (PatternSegment, String, &str, bool) {
907        const DEFAULT_PATTERN: &str = "[^/]+";
908        const DEFAULT_PATTERN_TAIL: &str = ".*";
909
910        let mut params_nesting = 0usize;
911        let close_idx = pattern
912            .find(|c| match c {
913                '{' => {
914                    params_nesting += 1;
915                    false
916                }
917                '}' => {
918                    params_nesting -= 1;
919                    params_nesting == 0
920                }
921                _ => false,
922            })
923            .unwrap_or_else(|| {
924                panic!(
925                    r#"Pattern "{}" contains malformed dynamic segment"#,
926                    pattern
927                )
928            });
929
930        let (mut param, mut unprocessed) = pattern.split_at(close_idx + 1);
931
932        // remove outer curly brackets
933        param = &param[1..param.len() - 1];
934
935        let tail = unprocessed == "*";
936
937        let (name, pattern) = match param.find(':') {
938            Some(idx) => {
939                assert!(!tail, "custom regex is not supported for tail match");
940
941                let (name, pattern) = param.split_at(idx);
942                (name, &pattern[1..])
943            }
944            None => (
945                param,
946                if tail {
947                    unprocessed = &unprocessed[1..];
948                    DEFAULT_PATTERN_TAIL
949                } else {
950                    DEFAULT_PATTERN
951                },
952            ),
953        };
954
955        let segment = PatternSegment::Var(name.to_string());
956        let regex = format!(r"(?P<{name}>{pattern})");
957
958        (segment, regex, unprocessed, tail)
959    }
960
961    /// Parse `pattern` using `is_prefix` and `force_dynamic` flags.
962    ///
963    /// Parameters:
964    /// - `is_prefix`: Use `true` if `pattern` should be treated as a prefix; i.e., a conforming
965    ///   path will be a match even if it has parts remaining to process
966    /// - `force_dynamic`: Use `true` to disallow the return of static and prefix segments.
967    ///
968    /// The returned tuple includes:
969    /// - the pattern type detected, either `Static`, `Prefix`, or `Dynamic`
970    /// - a list of segment descriptors from the pattern
971    fn parse(
972        pattern: &str,
973        is_prefix: bool,
974        force_dynamic: bool,
975    ) -> (PatternType, Vec<PatternSegment>) {
976        if !force_dynamic && pattern.find('{').is_none() && !pattern.ends_with('*') {
977            // pattern is static
978            return (
979                PatternType::Static(pattern.to_owned()),
980                vec![PatternSegment::Const(pattern.to_owned())],
981            );
982        }
983
984        let mut unprocessed = pattern;
985        let mut segments = Vec::new();
986        let mut re = format!("{}^", REGEX_FLAGS);
987        let mut dyn_segment_count = 0;
988        let mut has_tail_segment = false;
989
990        while let Some(idx) = unprocessed.find('{') {
991            let (prefix, rem) = unprocessed.split_at(idx);
992
993            segments.push(PatternSegment::Const(prefix.to_owned()));
994            re.push_str(&escape(prefix));
995
996            let (param_pattern, re_part, rem, tail) = Self::parse_param(rem);
997
998            if tail {
999                has_tail_segment = true;
1000            }
1001
1002            segments.push(param_pattern);
1003            re.push_str(&re_part);
1004
1005            unprocessed = rem;
1006            dyn_segment_count += 1;
1007        }
1008
1009        if is_prefix && has_tail_segment {
1010            // tail segments in prefixes have no defined semantics
1011
1012            #[cfg(not(test))]
1013            tracing::warn!(
1014                "Prefix resources should not have tail segments. \
1015                Use `ResourceDef::new` constructor. \
1016                This may become a panic in the future."
1017            );
1018
1019            // panic in tests to make this case detectable
1020            #[cfg(test)]
1021            panic!("Prefix resource definitions should not have tail segments");
1022        }
1023
1024        #[allow(clippy::literal_string_with_formatting_args)]
1025        if unprocessed.ends_with('*') {
1026            // unnamed tail segment
1027
1028            #[cfg(not(test))]
1029            tracing::warn!(
1030                "Tail segments must have names. \
1031                Consider `.../{{tail}}*`. \
1032                This may become a panic in the future."
1033            );
1034
1035            // panic in tests to make this case detectable
1036            #[cfg(test)]
1037            panic!("Tail segments must have names");
1038        } else if !has_tail_segment && !unprocessed.is_empty() {
1039            // prevent `Const("")` element from being added after last dynamic segment
1040
1041            segments.push(PatternSegment::Const(unprocessed.to_owned()));
1042            re.push_str(&escape(unprocessed));
1043        }
1044
1045        assert!(
1046            dyn_segment_count <= MAX_DYNAMIC_SEGMENTS,
1047            "Only {} dynamic segments are allowed, provided: {}",
1048            MAX_DYNAMIC_SEGMENTS,
1049            dyn_segment_count
1050        );
1051
1052        // Store the pattern in capture group #1 to have context info outside it
1053        let mut re = format!("({})", re);
1054
1055        // Ensure the match ends at a segment boundary
1056        if !has_tail_segment {
1057            if is_prefix {
1058                re.push_str(r"(/|$)");
1059            } else {
1060                re.push('$');
1061            }
1062        }
1063
1064        let re = match Regex::new(&re) {
1065            Ok(re) => re,
1066            Err(err) => panic!("Wrong path pattern: \"{}\" {}", pattern, err),
1067        };
1068
1069        // `Bok::leak(Box::new(name))` is an intentional memory leak. In typical applications the
1070        // routing table is only constructed once (per worker) so leak is bounded. If you are
1071        // constructing `ResourceDef`s more than once in your application's lifecycle you would
1072        // expect a linear increase in leaked memory over time.
1073        let names = re
1074            .capture_names()
1075            .filter_map(|name| name.map(|name| Box::leak(Box::new(name.to_owned())).as_str()))
1076            .collect();
1077
1078        (PatternType::Dynamic(re, names), segments)
1079    }
1080}
1081
1082impl Eq for ResourceDef {}
1083
1084impl PartialEq for ResourceDef {
1085    fn eq(&self, other: &ResourceDef) -> bool {
1086        self.patterns == other.patterns && self.is_prefix == other.is_prefix
1087    }
1088}
1089
1090impl Hash for ResourceDef {
1091    fn hash<H: Hasher>(&self, state: &mut H) {
1092        self.patterns.hash(state);
1093    }
1094}
1095
1096impl<'a> From<&'a str> for ResourceDef {
1097    fn from(path: &'a str) -> ResourceDef {
1098        ResourceDef::new(path)
1099    }
1100}
1101
1102impl From<String> for ResourceDef {
1103    fn from(path: String) -> ResourceDef {
1104        ResourceDef::new(path)
1105    }
1106}
1107
1108pub(crate) fn insert_slash(path: &str) -> Cow<'_, str> {
1109    if !path.is_empty() && !path.starts_with('/') {
1110        let mut new_path = String::with_capacity(path.len() + 1);
1111        new_path.push('/');
1112        new_path.push_str(path);
1113        Cow::Owned(new_path)
1114    } else {
1115        Cow::Borrowed(path)
1116    }
1117}
1118
1119#[cfg(test)]
1120mod tests {
1121    use super::*;
1122    use crate::Path;
1123
1124    #[test]
1125    fn equivalence() {
1126        assert_eq!(
1127            ResourceDef::root_prefix("/root"),
1128            ResourceDef::prefix("/root")
1129        );
1130        assert_eq!(
1131            ResourceDef::root_prefix("root"),
1132            ResourceDef::prefix("/root")
1133        );
1134        assert_eq!(
1135            ResourceDef::root_prefix("/{id}"),
1136            ResourceDef::prefix("/{id}")
1137        );
1138        assert_eq!(
1139            ResourceDef::root_prefix("{id}"),
1140            ResourceDef::prefix("/{id}")
1141        );
1142
1143        assert_eq!(ResourceDef::new("/"), ResourceDef::new(["/"]));
1144        assert_eq!(ResourceDef::new("/"), ResourceDef::new(vec!["/"]));
1145        assert_eq!(
1146            ResourceDef::from("/from-str"),
1147            ResourceDef::new("/from-str")
1148        );
1149        assert_eq!(
1150            ResourceDef::from("/from-string".to_owned()),
1151            ResourceDef::new("/from-string")
1152        );
1153
1154        assert_ne!(ResourceDef::new(""), ResourceDef::prefix(""));
1155        assert_ne!(ResourceDef::new("/"), ResourceDef::prefix("/"));
1156        assert_ne!(ResourceDef::new("/{id}"), ResourceDef::prefix("/{id}"));
1157    }
1158
1159    #[test]
1160    fn metadata_accessors_update_resource_identity() {
1161        let mut resource = ResourceDef::new("/root");
1162        assert_eq!(resource.id(), 0);
1163        resource.set_id(42);
1164        assert_eq!(resource.id(), 42);
1165        assert!(!resource.is_prefix());
1166
1167        assert!(resource.name().is_none());
1168        resource.set_name("root");
1169        assert_eq!(resource.name(), Some("root"));
1170    }
1171
1172    #[test]
1173    fn pattern_iterators_report_registered_patterns() {
1174        let resource = ResourceDef::new("/root");
1175        assert_eq!(resource.pattern(), Some("/root"));
1176        let mut patterns = resource.pattern_iter();
1177        assert_eq!(patterns.size_hint(), (1, Some(1)));
1178        assert_eq!(patterns.next(), Some("/root"));
1179        assert_eq!(patterns.next(), None);
1180
1181        let resource = ResourceDef::new(["/first", "/second"]);
1182        assert_eq!(resource.pattern(), Some("/first"));
1183        let mut patterns = resource.pattern_iter();
1184        assert_eq!(patterns.size_hint(), (2, Some(2)));
1185        assert_eq!(patterns.next(), Some("/first"));
1186        assert_eq!(patterns.next(), Some("/second"));
1187        assert_eq!(patterns.next(), None);
1188        assert_eq!(patterns.next(), None);
1189    }
1190
1191    #[test]
1192    fn empty_pattern_sets_never_match() {
1193        let empty = ResourceDef::new(Vec::<&str>::new());
1194        assert_eq!(empty.pattern(), None);
1195        assert_eq!(empty.pattern_iter().next(), None);
1196        assert!(!empty.is_match("/root"));
1197        assert_eq!(empty.find_match("/root"), None);
1198    }
1199
1200    #[test]
1201    fn equal_resources_have_equal_hashes() {
1202        fn hash(resource: &ResourceDef) -> u64 {
1203            let mut hasher = std::collections::hash_map::DefaultHasher::new();
1204            resource.hash(&mut hasher);
1205            hasher.finish()
1206        }
1207
1208        let mut first = ResourceDef::new("/root");
1209        first.set_id(1);
1210        first.set_name("first");
1211
1212        let mut second = ResourceDef::new("/root");
1213        second.set_id(2);
1214        second.set_name("second");
1215
1216        assert_eq!(first, second);
1217        assert_eq!(hash(&first), hash(&second));
1218    }
1219
1220    #[test]
1221    #[should_panic(expected = "resource name should not be empty")]
1222    fn empty_resource_names_are_rejected() {
1223        ResourceDef::new("/root").set_name("");
1224    }
1225
1226    #[test]
1227    fn parse_static() {
1228        let re = ResourceDef::new("");
1229
1230        assert!(!re.is_prefix());
1231
1232        assert!(re.is_match(""));
1233        assert!(!re.is_match("/"));
1234        assert_eq!(re.find_match(""), Some(0));
1235        assert_eq!(re.find_match("/"), None);
1236
1237        let re = ResourceDef::new("/");
1238        assert!(re.is_match("/"));
1239        assert!(!re.is_match(""));
1240        assert!(!re.is_match("/foo"));
1241
1242        let re = ResourceDef::new("/name");
1243        assert!(re.is_match("/name"));
1244        assert!(!re.is_match("/name1"));
1245        assert!(!re.is_match("/name/"));
1246        assert!(!re.is_match("/name~"));
1247
1248        let mut path = Path::new("/name");
1249        assert!(re.capture_match_info(&mut path));
1250        assert_eq!(path.unprocessed(), "");
1251
1252        assert_eq!(re.find_match("/name"), Some(5));
1253        assert_eq!(re.find_match("/name1"), None);
1254        assert_eq!(re.find_match("/name/"), None);
1255        assert_eq!(re.find_match("/name~"), None);
1256
1257        let re = ResourceDef::new("/name/");
1258        assert!(re.is_match("/name/"));
1259        assert!(!re.is_match("/name"));
1260        assert!(!re.is_match("/name/gs"));
1261
1262        let re = ResourceDef::new("/user/profile");
1263        assert!(re.is_match("/user/profile"));
1264        assert!(!re.is_match("/user/profile/profile"));
1265
1266        let mut path = Path::new("/user/profile");
1267        assert!(re.capture_match_info(&mut path));
1268        assert_eq!(path.unprocessed(), "");
1269    }
1270
1271    #[test]
1272    fn parse_param() {
1273        let re = ResourceDef::new("/user/{id}");
1274        assert!(re.is_match("/user/profile"));
1275        assert!(re.is_match("/user/2345"));
1276        assert!(!re.is_match("/user/2345/"));
1277        assert!(!re.is_match("/user/2345/sdg"));
1278
1279        let mut path = Path::new("/user/profile");
1280        assert!(re.capture_match_info(&mut path));
1281        assert_eq!(path.get("id").unwrap(), "profile");
1282        assert_eq!(path.unprocessed(), "");
1283
1284        let mut path = Path::new("/user/1245125");
1285        assert!(re.capture_match_info(&mut path));
1286        assert_eq!(path.get("id").unwrap(), "1245125");
1287        assert_eq!(path.unprocessed(), "");
1288
1289        let re = ResourceDef::new("/v{version}/resource/{id}");
1290        assert!(re.is_match("/v1/resource/320120"));
1291        assert!(!re.is_match("/v/resource/1"));
1292        assert!(!re.is_match("/resource"));
1293
1294        let mut path = Path::new("/v151/resource/adage32");
1295        assert!(re.capture_match_info(&mut path));
1296        assert_eq!(path.get("version").unwrap(), "151");
1297        assert_eq!(path.get("id").unwrap(), "adage32");
1298        assert_eq!(path.unprocessed(), "");
1299
1300        let re = ResourceDef::new("/{id:[[:digit:]]{6}}");
1301        assert!(re.is_match("/012345"));
1302        assert!(!re.is_match("/012"));
1303        assert!(!re.is_match("/01234567"));
1304        assert!(!re.is_match("/XXXXXX"));
1305
1306        let mut path = Path::new("/012345");
1307        assert!(re.capture_match_info(&mut path));
1308        assert_eq!(path.get("id").unwrap(), "012345");
1309        assert_eq!(path.unprocessed(), "");
1310    }
1311
1312    #[allow(clippy::cognitive_complexity)]
1313    #[test]
1314    fn dynamic_set() {
1315        let re = ResourceDef::new(vec![
1316            "/user/{id}",
1317            "/v{version}/resource/{id}",
1318            "/{id:[[:digit:]]{6}}",
1319            "/static",
1320        ]);
1321        assert!(re.is_match("/user/profile"));
1322        assert!(re.is_match("/user/2345"));
1323        assert!(!re.is_match("/user/2345/"));
1324        assert!(!re.is_match("/user/2345/sdg"));
1325
1326        let mut path = Path::new("/user/profile");
1327        assert!(re.capture_match_info(&mut path));
1328        assert_eq!(path.get("id").unwrap(), "profile");
1329        assert_eq!(path.unprocessed(), "");
1330
1331        let mut path = Path::new("/user/1245125");
1332        assert!(re.capture_match_info(&mut path));
1333        assert_eq!(path.get("id").unwrap(), "1245125");
1334        assert_eq!(path.unprocessed(), "");
1335
1336        assert!(re.is_match("/v1/resource/320120"));
1337        assert!(!re.is_match("/v/resource/1"));
1338        assert!(!re.is_match("/resource"));
1339
1340        let mut path = Path::new("/v151/resource/adage32");
1341        assert!(re.capture_match_info(&mut path));
1342        assert_eq!(path.get("version").unwrap(), "151");
1343        assert_eq!(path.get("id").unwrap(), "adage32");
1344
1345        assert!(re.is_match("/012345"));
1346        assert!(!re.is_match("/012"));
1347        assert!(!re.is_match("/01234567"));
1348        assert!(!re.is_match("/XXXXXX"));
1349
1350        assert!(re.is_match("/static"));
1351        assert!(!re.is_match("/a/static"));
1352        assert!(!re.is_match("/static/a"));
1353
1354        let mut path = Path::new("/012345");
1355        assert!(re.capture_match_info(&mut path));
1356        assert_eq!(path.get("id").unwrap(), "012345");
1357
1358        let re = ResourceDef::new([
1359            "/user/{id}",
1360            "/v{version}/resource/{id}",
1361            "/{id:[[:digit:]]{6}}",
1362        ]);
1363        assert!(re.is_match("/user/profile"));
1364        assert!(re.is_match("/user/2345"));
1365        assert!(!re.is_match("/user/2345/"));
1366        assert!(!re.is_match("/user/2345/sdg"));
1367
1368        let re = ResourceDef::new([
1369            "/user/{id}".to_string(),
1370            "/v{version}/resource/{id}".to_string(),
1371            "/{id:[[:digit:]]{6}}".to_string(),
1372        ]);
1373        assert!(re.is_match("/user/profile"));
1374        assert!(re.is_match("/user/2345"));
1375        assert!(!re.is_match("/user/2345/"));
1376        assert!(!re.is_match("/user/2345/sdg"));
1377    }
1378
1379    #[test]
1380    fn dynamic_set_prefix() {
1381        let re = ResourceDef::prefix(vec!["/u/{id}", "/{id:[[:digit:]]{3}}"]);
1382
1383        assert_eq!(re.find_match("/u/abc"), Some(6));
1384        assert_eq!(re.find_match("/u/abc/123"), Some(6));
1385        assert_eq!(re.find_match("/s/user/profile"), None);
1386
1387        assert_eq!(re.find_match("/123"), Some(4));
1388        assert_eq!(re.find_match("/123/456"), Some(4));
1389        assert_eq!(re.find_match("/12345"), None);
1390
1391        let mut path = Path::new("/151/res");
1392        assert!(re.capture_match_info(&mut path));
1393        assert_eq!(path.get("id").unwrap(), "151");
1394        assert_eq!(path.unprocessed(), "/res");
1395    }
1396
1397    #[test]
1398    fn parse_tail() {
1399        let re = ResourceDef::new("/user/-{id}*");
1400
1401        let mut path = Path::new("/user/-profile");
1402        assert!(re.capture_match_info(&mut path));
1403        assert_eq!(path.get("id").unwrap(), "profile");
1404
1405        let mut path = Path::new("/user/-2345");
1406        assert!(re.capture_match_info(&mut path));
1407        assert_eq!(path.get("id").unwrap(), "2345");
1408
1409        let mut path = Path::new("/user/-2345/");
1410        assert!(re.capture_match_info(&mut path));
1411        assert_eq!(path.get("id").unwrap(), "2345/");
1412
1413        let mut path = Path::new("/user/-2345/sdg");
1414        assert!(re.capture_match_info(&mut path));
1415        assert_eq!(path.get("id").unwrap(), "2345/sdg");
1416    }
1417
1418    #[test]
1419    fn static_tail() {
1420        let re = ResourceDef::new("/user{tail}*");
1421        assert!(re.is_match("/users"));
1422        assert!(re.is_match("/user-foo"));
1423        assert!(re.is_match("/user/profile"));
1424        assert!(re.is_match("/user/2345"));
1425        assert!(re.is_match("/user/2345/"));
1426        assert!(re.is_match("/user/2345/sdg"));
1427        assert!(!re.is_match("/foo/profile"));
1428
1429        let re = ResourceDef::new("/user/{tail}*");
1430        assert!(re.is_match("/user/profile"));
1431        assert!(re.is_match("/user/2345"));
1432        assert!(re.is_match("/user/2345/"));
1433        assert!(re.is_match("/user/2345/sdg"));
1434        assert!(!re.is_match("/foo/profile"));
1435    }
1436
1437    #[test]
1438    fn dynamic_tail() {
1439        let re = ResourceDef::new("/user/{id}/{tail}*");
1440        assert!(!re.is_match("/user/2345"));
1441        let mut path = Path::new("/user/2345/sdg");
1442        assert!(re.capture_match_info(&mut path));
1443        assert_eq!(path.get("id").unwrap(), "2345");
1444        assert_eq!(path.get("tail").unwrap(), "sdg");
1445        assert_eq!(path.unprocessed(), "");
1446    }
1447
1448    #[test]
1449    fn capture_check_can_reject_a_match_without_mutating_path() {
1450        let resource = ResourceDef::prefix("/user/{id}");
1451        let mut path = Path::new("/user/admin/stars");
1452
1453        assert!(
1454            !resource.capture_match_info_fn(&mut path, |path| { !path.as_str().contains("admin") })
1455        );
1456        assert_eq!(path.unprocessed(), "/user/admin/stars");
1457        assert!(path.is_empty());
1458    }
1459
1460    #[test]
1461    fn dynamic_set_capture_rejects_unmatched_paths() {
1462        let resource = ResourceDef::new(["/user/{id}", r"/number/{id:\d+}"]);
1463        let mut path = Path::new("/");
1464        assert!(!resource.capture_match_info(&mut path));
1465        assert_eq!(path.unprocessed(), "/");
1466    }
1467
1468    #[allow(clippy::literal_string_with_formatting_args)]
1469    #[test]
1470    fn newline_patterns_and_paths() {
1471        let re = ResourceDef::new("/user/a\nb");
1472        assert!(re.is_match("/user/a\nb"));
1473        assert!(!re.is_match("/user/a\nb/profile"));
1474
1475        let re = ResourceDef::new("/a{x}b/test/a{y}b");
1476        let mut path = Path::new("/a\nb/test/a\nb");
1477        assert!(re.capture_match_info(&mut path));
1478        assert_eq!(path.get("x").unwrap(), "\n");
1479        assert_eq!(path.get("y").unwrap(), "\n");
1480
1481        let re = ResourceDef::new("/user/{tail}*");
1482        assert!(re.is_match("/user/a\nb/"));
1483
1484        let re = ResourceDef::new("/user/{id}*");
1485        let mut path = Path::new("/user/a\nb/a\nb");
1486        assert!(re.capture_match_info(&mut path));
1487        assert_eq!(path.get("id").unwrap(), "a\nb/a\nb");
1488
1489        let re = ResourceDef::new("/user/{id:.*}");
1490        let mut path = Path::new("/user/a\nb/a\nb");
1491        assert!(re.capture_match_info(&mut path));
1492        assert_eq!(path.get("id").unwrap(), "a\nb/a\nb");
1493    }
1494
1495    #[cfg(feature = "http")]
1496    #[test]
1497    fn parse_urlencoded_param() {
1498        let re = ResourceDef::new("/user/{id}/test");
1499
1500        let mut path = Path::new("/user/2345/test");
1501        assert!(re.capture_match_info(&mut path));
1502        assert_eq!(path.get("id").unwrap(), "2345");
1503
1504        let mut path = Path::new("/user/qwe%25/test");
1505        assert!(re.capture_match_info(&mut path));
1506        assert_eq!(path.get("id").unwrap(), "qwe%25");
1507
1508        let uri = http::Uri::try_from("/user/qwe%25/test").unwrap();
1509        let mut path = Path::new(uri);
1510        assert!(re.capture_match_info(&mut path));
1511        assert_eq!(path.get("id").unwrap(), "qwe%25");
1512    }
1513
1514    #[test]
1515    fn prefix_static() {
1516        let re = ResourceDef::prefix("/name");
1517
1518        assert!(re.is_prefix());
1519
1520        assert!(re.is_match("/name"));
1521        assert!(re.is_match("/name/"));
1522        assert!(re.is_match("/name/test/test"));
1523        assert!(!re.is_match("/name1"));
1524        assert!(!re.is_match("/name~"));
1525
1526        let mut path = Path::new("/name");
1527        assert!(re.capture_match_info(&mut path));
1528        assert_eq!(path.unprocessed(), "");
1529
1530        let mut path = Path::new("/name/test");
1531        assert!(re.capture_match_info(&mut path));
1532        assert_eq!(path.unprocessed(), "/test");
1533
1534        assert_eq!(re.find_match("/name"), Some(5));
1535        assert_eq!(re.find_match("/name/"), Some(5));
1536        assert_eq!(re.find_match("/name/test/test"), Some(5));
1537        assert_eq!(re.find_match("/name1"), None);
1538        assert_eq!(re.find_match("/name~"), None);
1539
1540        let re = ResourceDef::prefix("/name/");
1541        assert!(re.is_match("/name/"));
1542        assert!(re.is_match("/name//gs"));
1543        assert!(!re.is_match("/name/gs"));
1544        assert!(!re.is_match("/name"));
1545
1546        let mut path = Path::new("/name/gs");
1547        assert!(!re.capture_match_info(&mut path));
1548
1549        let mut path = Path::new("/name//gs");
1550        assert!(re.capture_match_info(&mut path));
1551        assert_eq!(path.unprocessed(), "/gs");
1552
1553        let re = ResourceDef::root_prefix("name/");
1554        assert!(re.is_match("/name/"));
1555        assert!(re.is_match("/name//gs"));
1556        assert!(!re.is_match("/name/gs"));
1557        assert!(!re.is_match("/name"));
1558
1559        let mut path = Path::new("/name/gs");
1560        assert!(!re.capture_match_info(&mut path));
1561    }
1562
1563    #[test]
1564    fn prefix_dynamic() {
1565        let re = ResourceDef::prefix("/{name}");
1566
1567        assert!(re.is_prefix());
1568
1569        assert!(re.is_match("/name/"));
1570        assert!(re.is_match("/name/gs"));
1571        assert!(re.is_match("/name"));
1572
1573        assert_eq!(re.find_match("/name/"), Some(5));
1574        assert_eq!(re.find_match("/name/gs"), Some(5));
1575        assert_eq!(re.find_match("/name"), Some(5));
1576        assert_eq!(re.find_match(""), None);
1577
1578        let mut path = Path::new("/test2/");
1579        assert!(re.capture_match_info(&mut path));
1580        assert_eq!(&path["name"], "test2");
1581        assert_eq!(&path[0], "test2");
1582        assert_eq!(path.unprocessed(), "/");
1583
1584        let mut path = Path::new("/test2/subpath1/subpath2/index.html");
1585        assert!(re.capture_match_info(&mut path));
1586        assert_eq!(&path["name"], "test2");
1587        assert_eq!(&path[0], "test2");
1588        assert_eq!(path.unprocessed(), "/subpath1/subpath2/index.html");
1589
1590        let resource = ResourceDef::prefix("/user");
1591        // input string shorter than prefix
1592        assert!(resource.find_match("/foo").is_none());
1593    }
1594
1595    #[test]
1596    fn prefix_empty() {
1597        let re = ResourceDef::prefix("");
1598
1599        assert!(re.is_prefix());
1600
1601        assert!(re.is_match(""));
1602        assert!(re.is_match("/"));
1603        assert!(re.is_match("/name/test/test"));
1604    }
1605
1606    #[test]
1607    fn build_path_list() {
1608        let mut s = String::new();
1609        let resource = ResourceDef::new("/user/{item1}/test");
1610        assert!(resource.resource_path_from_iter(&mut s, &mut ["user1"].iter()));
1611        assert_eq!(s, "/user/user1/test");
1612
1613        let mut s = String::new();
1614        let resource = ResourceDef::new("/user/{item1}/{item2}/test");
1615        assert!(resource.resource_path_from_iter(&mut s, &mut ["item", "item2"].iter()));
1616        assert_eq!(s, "/user/item/item2/test");
1617
1618        let mut s = String::new();
1619        let resource = ResourceDef::new("/user/{item1}/{item2}");
1620        assert!(resource.resource_path_from_iter(&mut s, &mut ["item", "item2"].iter()));
1621        assert_eq!(s, "/user/item/item2");
1622
1623        let mut s = String::new();
1624        let resource = ResourceDef::new("/user/{item1}/{item2}/");
1625        assert!(resource.resource_path_from_iter(&mut s, &mut ["item", "item2"].iter()));
1626        assert_eq!(s, "/user/item/item2/");
1627
1628        let mut s = String::new();
1629        assert!(!resource.resource_path_from_iter(&mut s, &mut ["item"].iter()));
1630
1631        let mut s = String::new();
1632        assert!(resource.resource_path_from_iter(&mut s, &mut ["item", "item2"].iter()));
1633        assert_eq!(s, "/user/item/item2/");
1634        assert!(!resource.resource_path_from_iter(&mut s, &mut ["item"].iter()));
1635
1636        let mut s = String::new();
1637
1638        assert!(resource.resource_path_from_iter(
1639            &mut s,
1640            #[allow(clippy::useless_vec)]
1641            &mut vec!["item", "item2"].iter()
1642        ));
1643        assert_eq!(s, "/user/item/item2/");
1644    }
1645
1646    #[test]
1647    fn multi_pattern_build_path() {
1648        let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]);
1649        let mut s = String::new();
1650        assert!(resource.resource_path_from_iter(&mut s, &mut ["123"].iter()));
1651        assert_eq!(s, "/user/123");
1652    }
1653
1654    #[test]
1655    fn multi_pattern_capture_segment_values() {
1656        let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]);
1657
1658        let mut path = Path::new("/user/123");
1659        assert!(resource.capture_match_info(&mut path));
1660        assert!(path.get("id").is_some());
1661
1662        let mut path = Path::new("/profile/123");
1663        assert!(resource.capture_match_info(&mut path));
1664        assert!(path.get("id").is_some());
1665
1666        let resource = ResourceDef::new(["/user/{id}", "/profile/{uid}"]);
1667
1668        let mut path = Path::new("/user/123");
1669        assert!(resource.capture_match_info(&mut path));
1670        assert!(path.get("id").is_some());
1671        assert!(path.get("uid").is_none());
1672
1673        let mut path = Path::new("/profile/123");
1674        assert!(resource.capture_match_info(&mut path));
1675        assert!(path.get("id").is_none());
1676        assert!(path.get("uid").is_some());
1677    }
1678
1679    #[test]
1680    fn dynamic_prefix_proper_segmentation() {
1681        let resource = ResourceDef::prefix(r"/id/{id:\d{3}}");
1682
1683        assert!(resource.is_match("/id/123"));
1684        assert!(resource.is_match("/id/123/foo"));
1685        assert!(!resource.is_match("/id/1234"));
1686        assert!(!resource.is_match("/id/123a"));
1687
1688        assert_eq!(resource.find_match("/id/123"), Some(7));
1689        assert_eq!(resource.find_match("/id/123/foo"), Some(7));
1690        assert_eq!(resource.find_match("/id/1234"), None);
1691        assert_eq!(resource.find_match("/id/123a"), None);
1692    }
1693
1694    #[test]
1695    fn build_path_map() {
1696        let resource = ResourceDef::new("/user/{item1}/{item2}/");
1697
1698        let mut map = HashMap::new();
1699        map.insert("item1", "item");
1700
1701        let mut s = String::new();
1702        assert!(!resource.resource_path_from_map(&mut s, &map));
1703
1704        map.insert("item2", "item2");
1705
1706        let mut s = String::new();
1707        assert!(resource.resource_path_from_map(&mut s, &map));
1708        assert_eq!(s, "/user/item/item2/");
1709    }
1710
1711    #[test]
1712    fn build_path_tail() {
1713        let resource = ResourceDef::new("/user/{item1}*");
1714
1715        let mut s = String::new();
1716        assert!(!resource.resource_path_from_iter(&mut s, &mut [""; 0].iter()));
1717
1718        let mut s = String::new();
1719        assert!(resource.resource_path_from_iter(&mut s, &mut ["user1"].iter()));
1720        assert_eq!(s, "/user/user1");
1721
1722        let mut s = String::new();
1723        let mut map = HashMap::new();
1724        map.insert("item1", "item");
1725        assert!(resource.resource_path_from_map(&mut s, &map));
1726        assert_eq!(s, "/user/item");
1727    }
1728
1729    #[test]
1730    fn prefix_trailing_slash() {
1731        // The prefix "/abc/" matches two segments: ["user", ""]
1732
1733        // These are not prefixes
1734        let re = ResourceDef::prefix("/abc/");
1735        assert_eq!(re.find_match("/abc/def"), None);
1736        assert_eq!(re.find_match("/abc//def"), Some(5));
1737
1738        let re = ResourceDef::prefix("/{id}/");
1739        assert_eq!(re.find_match("/abc/def"), None);
1740        assert_eq!(re.find_match("/abc//def"), Some(5));
1741    }
1742
1743    #[test]
1744    fn join() {
1745        // test joined defs match the same paths as each component separately
1746
1747        fn seq_find_match(re1: &ResourceDef, re2: &ResourceDef, path: &str) -> Option<usize> {
1748            let len1 = re1.find_match(path)?;
1749            let len2 = re2.find_match(&path[len1..])?;
1750            Some(len1 + len2)
1751        }
1752
1753        macro_rules! join_test {
1754            ($pat1:expr, $pat2:expr => $($test:expr),+) => {{
1755                let pat1 = $pat1;
1756                let pat2 = $pat2;
1757                $({
1758                    let _path = $test;
1759                    let (re1, re2) = (ResourceDef::prefix(pat1), ResourceDef::new(pat2));
1760                    let _seq = seq_find_match(&re1, &re2, _path);
1761                    let _join = re1.join(&re2).find_match(_path);
1762                    assert_eq!(
1763                        _seq, _join,
1764                        "patterns: prefix {:?}, {:?}; mismatch on \"{}\"; seq={:?}; join={:?}",
1765                        pat1, pat2, _path, _seq, _join
1766                    );
1767                    assert!(!re1.join(&re2).is_prefix());
1768
1769                    let (re1, re2) = (ResourceDef::prefix(pat1), ResourceDef::prefix(pat2));
1770                    let _seq = seq_find_match(&re1, &re2, _path);
1771                    let _join = re1.join(&re2).find_match(_path);
1772                    assert_eq!(
1773                        _seq, _join,
1774                        "patterns: prefix {:?}, prefix {:?}; mismatch on \"{}\"; seq={:?}; join={:?}",
1775                        pat1, pat2, _path, _seq, _join
1776                    );
1777                    assert!(re1.join(&re2).is_prefix());
1778                })+
1779            }}
1780        }
1781
1782        join_test!("", "" => "", "/hello", "/");
1783        join_test!("/user", "" => "", "/user", "/user/123", "/user11", "user", "user/123");
1784        join_test!("",  "/user" => "", "/user", "foo", "/user11", "user", "user/123");
1785        join_test!("/user",  "/xx" => "", "",  "/", "/user", "/xx", "/userxx", "/user/xx");
1786
1787        join_test!(["/ver/{v}", "/v{v}"], ["/req/{req}", "/{req}"] => "/v1/abc",
1788                   "/ver/1/abc", "/v1/req/abc", "/ver/1/req/abc", "/v1/abc/def",
1789                   "/ver1/req/abc/def", "", "/", "/v1/");
1790    }
1791
1792    #[test]
1793    fn match_methods_agree() {
1794        macro_rules! match_methods_agree {
1795            ($pat:expr => $($test:expr),+) => {{
1796                match_methods_agree!(finish $pat, ResourceDef::new($pat), $($test),+);
1797            }};
1798            (prefix $pat:expr => $($test:expr),+) => {{
1799                match_methods_agree!(finish $pat, ResourceDef::prefix($pat), $($test),+);
1800            }};
1801            (finish $pat:expr, $re:expr, $($test:expr),+) => {{
1802                let re = $re;
1803                $({
1804                    let _is = re.is_match($test);
1805                    let _find = re.find_match($test).is_some();
1806                    assert_eq!(
1807                        _is, _find,
1808                        "pattern: {:?}; mismatch on \"{}\"; is={}; find={}",
1809                        $pat, $test, _is, _find
1810                    );
1811                })+
1812            }}
1813        }
1814
1815        match_methods_agree!("" => "", "/", "/foo");
1816        match_methods_agree!("/" => "", "/", "/foo");
1817        match_methods_agree!("/user" => "user", "/user", "/users", "/user/123", "/foo");
1818        match_methods_agree!("/v{v}" => "v", "/v", "/v1", "/v222", "/foo");
1819        match_methods_agree!(["/v{v}", "/version/{v}"] => "/v", "/v1", "/version", "/version/1", "/foo");
1820
1821        match_methods_agree!("/path{tail}*" => "/path", "/path1", "/path/123");
1822        match_methods_agree!("/path/{tail}*" => "/path", "/path1", "/path/123");
1823
1824        match_methods_agree!(prefix "" => "", "/", "/foo");
1825        match_methods_agree!(prefix "/user" => "user", "/user", "/users", "/user/123", "/foo");
1826        match_methods_agree!(prefix r"/id/{id:\d{3}}" => "/id/123", "/id/1234");
1827        match_methods_agree!(["/v{v}", "/ver/{v}"] => "", "s/v", "/v1", "/v1/xx", "/ver/i3/5", "/ver/1");
1828    }
1829
1830    #[test]
1831    #[should_panic]
1832    fn duplicate_segment_name() {
1833        ResourceDef::new("/user/{id}/post/{id}");
1834    }
1835
1836    #[test]
1837    #[should_panic]
1838    fn invalid_dynamic_segment_delimiter() {
1839        ResourceDef::new("/user/{username");
1840    }
1841
1842    #[test]
1843    #[should_panic]
1844    fn invalid_dynamic_segment_name() {
1845        ResourceDef::new("/user/{}");
1846    }
1847
1848    #[test]
1849    #[should_panic]
1850    fn invalid_too_many_dynamic_segments() {
1851        // valid
1852        ResourceDef::new("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}");
1853
1854        // panics
1855        ResourceDef::new("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}");
1856    }
1857
1858    #[test]
1859    #[should_panic]
1860    fn invalid_custom_regex_for_tail() {
1861        ResourceDef::new(r"/{tail:\d+}*");
1862    }
1863
1864    #[test]
1865    #[should_panic]
1866    fn invalid_unnamed_tail_segment() {
1867        ResourceDef::new("/*");
1868    }
1869
1870    #[test]
1871    #[should_panic]
1872    fn prefix_plus_tail_match_disallowed() {
1873        ResourceDef::prefix("/user/{id}*");
1874    }
1875}