Skip to main content

structured_proxy/transcode/
path.rs

1//! `google.api.http` path templates in the form the router mounts them.
2//!
3//! The template grammar (google/api/http.proto) is
4//! `Template = "/" Segments [ Verb ]` with `Verb = ":" LITERAL`. axum (matchit)
5//! matches a literal after a variable in no segment, so a verb that follows a
6//! variable or a wildcard is split off here and matched by the transcoded
7//! router itself; a verb after a literal stays part of that literal.
8
9use std::borrow::Cow;
10
11/// Where one binding is mounted.
12#[derive(Debug, Clone)]
13pub(crate) struct MountedPath {
14    /// The axum path (`/v1/{name}`), without a verb that follows a variable.
15    pub(crate) axum: String,
16    /// `axum` with every capture unnamed: the bindings that share it share one
17    /// route, since the router tells their paths apart by shape only.
18    pub(crate) shape: String,
19    /// The capture names of `axum`, in path order.
20    pub(crate) captures: Vec<String>,
21    /// For each capture, whether it stands for a bare `*` or `**`: the router
22    /// needs a name for it, but it binds no field (google/api/http.proto).
23    pub(crate) unbound: Vec<bool>,
24    /// For each capture, whether it is (part of) a variable matching several
25    /// segments, whose value keeps `%2F` encoded (google/api/http.proto).
26    pub(crate) multi_segment: Vec<bool>,
27    /// The custom verb after the last segment's variable, `:` included.
28    pub(crate) verb: Option<String>,
29    /// Whether the last capture may be empty: a `**` matches zero or more
30    /// segments (google/api/http.proto), any other variable at least one.
31    pub(crate) empty_last: bool,
32    /// The field template of the last capture when the router cannot hold it
33    /// (`{name=shelves/*/books/*}` mounted as a catch-all): its segments, which
34    /// the matched value must follow.
35    pub(crate) last_template: Option<Vec<String>>,
36    /// The index of the last segment, the one the last capture starts at.
37    pub(crate) last_segment: usize,
38    /// Why the router cannot hold this template faithfully, when it cannot.
39    pub(crate) unsupported: Option<String>,
40    /// The fields bound to a multi-segment template before the last segment,
41    /// spelled out in the path: each is put back together from its parts.
42    pub(crate) composites: Vec<Composite>,
43    /// The literals of those templates that hold a percent-escape, as
44    /// written, with the index of their path segment. The router compares a
45    /// literal byte for byte, while `%3a` and `%3A` are one octet (RFC 3986
46    /// §6.2.2.1), so such a segment is mounted as a capture and compared by
47    /// the transcoded routes.
48    pub(crate) literals: Vec<(usize, String)>,
49}
50
51/// A field whose template (`{parent=publishers/*}`) the path spells out
52/// segment by segment.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub(crate) struct Composite {
55    pub(crate) field: String,
56    pub(crate) parts: Vec<Part>,
57}
58
59/// One segment of a [`Composite`].
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub(crate) enum Part {
62    /// A literal of the template without a percent-escape.
63    Literal(String),
64    /// A `*` of the template, or a literal with a percent-escape: the capture
65    /// at this position of the path.
66    Capture(usize),
67}
68
69impl MountedPath {
70    /// Mount `template`, a `google.api.http` path template.
71    pub(crate) fn new(template: &str) -> Self {
72        let (base, verb) = split_verb(template);
73        let segments = split_top_level(base);
74        let last = segments.len() - 1;
75        let mut axum = String::with_capacity(base.len());
76        let mut shape = String::with_capacity(base.len());
77        let mut captures = Vec::new();
78        let mut unbound = Vec::new();
79        let mut multi_segment = Vec::new();
80        let mut composites = Vec::new();
81        let mut literals = Vec::new();
82        let mut unsupported = None;
83        // Segments written so far: the last capture starts at this index.
84        let mut written = 0;
85        let empty_last = ends_in_double_wildcard(segments[last]);
86        let last_template = field_template(segments[last])
87            .filter(|template| *template != "*" && *template != "**")
88            .map(|template| {
89                split_top_level(template)
90                    .into_iter()
91                    .map(str::to_owned)
92                    .collect()
93            });
94        let mut last_segment = 0;
95        for (idx, segment) in segments.iter().enumerate() {
96            if idx == last {
97                last_segment = written;
98            }
99            let mut push_segment = |axum: &mut String, shape: &mut String| {
100                if written > 0 {
101                    axum.push('/');
102                    shape.push('/');
103                }
104                written += 1;
105            };
106            // A multi-segment template before the last segment: the router
107            // holds no capture across segments there, so its segments are
108            // written out; `**` among them has no such form.
109            if field_template(segment).is_some_and(|template| template.contains(['{', '}'])) {
110                unsupported = Some(format!(
111                    "`{segment}` nests a variable in a field template, which holds none"
112                ));
113            }
114            // `**` must be the last part of the path (google/api/http.proto);
115            // anywhere else it would also make matching try every split.
116            if idx == last
117                && field_template(segment)
118                    .is_some_and(|template| template.split('/').rev().skip(1).any(|p| p == "**"))
119            {
120                unsupported = Some(format!(
121                    "`{segment}` puts `**` before another segment; it must be the last one"
122                ));
123            }
124            let inner = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}'));
125            if let Some((field, template)) = inner
126                .and_then(|inner| inner.split_once('='))
127                .filter(|(_, template)| idx != last && *template != "*")
128            {
129                if template.split('/').any(|part| part == "**") {
130                    unsupported = Some(format!(
131                        "`**` in `{segment}` before the last segment matches any number of \
132                         segments, which the router cannot hold there"
133                    ));
134                } else {
135                    // A template of one segment (`{name=foo}`) is a variable of
136                    // one segment, decoded in full.
137                    let multi = template.contains('/');
138                    let mut parts = Vec::new();
139                    for (k, part) in template.split('/').enumerate() {
140                        push_segment(&mut axum, &mut shape);
141                        // An escaped literal is a capture to the router, so its
142                        // case does not matter (RFC 3986 §6.2.2.1); like any
143                        // capture, it cannot share its position with a
144                        // catch-all, which the router then refuses.
145                        let escaped = part.contains('%');
146                        if part == "*" || escaped {
147                            let name = format!("{field}.{k}");
148                            axum.push('{');
149                            axum.push_str(&name);
150                            axum.push('}');
151                            shape.push_str("{}");
152                            captures.push(name);
153                            unbound.push(true);
154                            multi_segment.push(multi);
155                        } else {
156                            axum.push_str(part);
157                            shape.push_str(part);
158                        }
159                        if escaped {
160                            // The index of this segment: one per `/` before it.
161                            let at = axum.bytes().filter(|&b| b == b'/').count();
162                            literals.push((at, part.to_owned()));
163                        }
164                        // An escaped literal is taken from the request, decoded
165                        // as the variable is: a kept `%2F` keeps its case there.
166                        parts.push(if part == "*" || escaped {
167                            Part::Capture(captures.len() - 1)
168                        } else {
169                            Part::Literal(part.to_owned())
170                        });
171                    }
172                    composites.push(Composite {
173                        field: field.to_owned(),
174                        parts,
175                    });
176                    continue;
177                }
178            } else if idx != last && is_double_wildcard(segment) {
179                unsupported = Some(format!(
180                    "`{segment}` before the last segment matches any number of segments, \
181                     which the router cannot hold there"
182                ));
183            }
184            push_segment(&mut axum, &mut shape);
185            match convert_segment(segment, idx, idx == last) {
186                Segment::Literal(literal) => {
187                    axum.push_str(literal);
188                    shape.push_str(literal);
189                }
190                Segment::Capture { name, catch_all } => {
191                    let open = if catch_all { "{*" } else { "{" };
192                    axum.push_str(open);
193                    axum.push_str(&name);
194                    axum.push('}');
195                    shape.push_str(open);
196                    shape.push('}');
197                    captures.push(name);
198                    unbound.push(*segment == "*" || *segment == "**");
199                    // A catch-all may hold a template of one segment; what
200                    // counts is the template.
201                    multi_segment.push(catch_all && spans_segments(segment));
202                }
203            }
204        }
205        Self {
206            axum,
207            shape,
208            captures,
209            unbound,
210            multi_segment,
211            verb: verb.map(str::to_owned),
212            empty_last,
213            last_template,
214            last_segment,
215            unsupported,
216            composites,
217            literals,
218        }
219    }
220
221    /// Whether the router can serve this template as written: a template it
222    /// cannot hold faithfully, or a path axum would refuse, is not served.
223    pub(crate) fn routable(&self) -> Result<(), String> {
224        match &self.unsupported {
225            Some(reason) => Err(reason.clone()),
226            None => mountable(&self.axum),
227        }
228    }
229
230    /// The route as written, verb included: what route policies match and
231    /// what the log names.
232    pub(crate) fn display(&self) -> Cow<'_, str> {
233        match &self.verb {
234            None => Cow::Borrowed(&self.axum),
235            Some(verb) => Cow::Owned(format!("{}{verb}", self.axum)),
236        }
237    }
238}
239
240/// Convert a `google.api.http` path template to axum 0.8 path syntax.
241///
242/// The proto `{param}` form IS axum 0.8's native capture syntax, so plain
243/// single-segment params pass through verbatim. Only field-path templates and
244/// bare wildcards need rewriting (axum 0.7 used `:param`; 0.8 uses `{param}`
245/// and rejects any segment starting with `:`):
246/// - `{name=*}`  (single segment)      -> `{name}`
247/// - `{name=**}` (multi-segment) -> `{*name}` (axum catch-all)
248/// - bare `*` segment            -> `{N}`, N its segment index
249/// - bare `**` segment           -> `{*N}` (axum catch-all)
250///
251/// A custom verb after a variable or wildcard (`{name}:cancel`) is not part of
252/// the result: axum cannot match it, so the transcoded router checks it
253/// itself. A verb after a literal (`/v1/nodes:batch`) stays in that literal.
254pub fn proto_path_to_axum(path: &str) -> String {
255    MountedPath::new(path).axum
256}
257
258/// Whether axum can register `axum_path` on its own: what `Router::route`
259/// checks before it inserts, then the insertion into an empty matchit router,
260/// the router axum matches with. A path that fails would panic at
261/// `Router::route`. Conflicts between paths are a separate check.
262pub(crate) fn mountable(axum_path: &str) -> Result<(), String> {
263    if !axum_path.starts_with('/') {
264        return Err("a path template must start with '/'".to_string());
265    }
266    if axum_path
267        .split('/')
268        .any(|segment| segment.starts_with(':') || segment.starts_with('*'))
269    {
270        return Err("a path segment must not start with ':' or '*'".to_string());
271    }
272    // A variable is a whole segment (google/api/http.proto `Segment`), checked
273    // here rather than left to the router, which may accept text around one.
274    let mut captures = 0usize;
275    for segment in axum_path.split('/') {
276        let Some(at) = segment.find(['{', '}']) else {
277            continue;
278        };
279        let inner = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}'));
280        if at != 0 || inner.is_none_or(|inner| inner.contains(['{', '}'])) {
281            return Err(format!("`{segment}` is not a whole-segment variable"));
282        }
283        if !segment.starts_with("{*") {
284            captures += 1;
285        }
286    }
287    // The router renames captures `a` to `z` and panics on a 26th (catch-alls
288    // are not renamed).
289    if captures > 25 {
290        return Err(format!(
291            "{captures} variables; the router holds at most 25 before the last"
292        ));
293    }
294    matchit::Router::new()
295        .insert(axum_path, ())
296        .map_err(|e| e.to_string())
297}
298
299/// Split the custom verb off `template` when its last segment is a variable
300/// or a wildcard: `/v1/{name}:cancel` is `/v1/{name}` and `:cancel`. An empty
301/// verb is not one (`Verb = ":" LITERAL`), and anything else after a variable
302/// stays in the template for the router to reject.
303fn split_verb(template: &str) -> (&str, Option<&str>) {
304    let segments = split_top_level(template);
305    let last = segments[segments.len() - 1];
306    let start = template.len() - last.len();
307    let colon = if last.starts_with('{') {
308        closing_brace(last).map(|close| close + 1)
309    } else if last.starts_with("**") {
310        Some(2)
311    } else if last.starts_with('*') {
312        Some(1)
313    } else {
314        None
315    };
316    match colon {
317        Some(colon) if last[colon..].starts_with(':') && last.len() > colon + 1 => {
318            let at = start + colon;
319            (&template[..at], Some(&template[at..]))
320        }
321        _ => (template, None),
322    }
323}
324
325/// `raw` percent-decoded as the value of a variable matching several segments
326/// is: every escape but `%2F` and `%2f`, which stay as written
327/// (google/api/http.proto). Borrowed when there is nothing to decode.
328pub(crate) fn decode_multi_segment(raw: &str) -> Cow<'_, [u8]> {
329    let is_slash =
330        |escape: &[u8]| escape[0] == b'%' && escape[1] == b'2' && escape[2] | 0x20 == b'f';
331    let bytes = raw.as_bytes();
332    if !bytes.windows(3).any(is_slash) {
333        return percent_encoding::percent_decode(bytes).into();
334    }
335    let mut decoded = Vec::with_capacity(bytes.len());
336    let mut rest = bytes;
337    while let Some(at) = rest.windows(3).position(is_slash) {
338        decoded.extend(percent_encoding::percent_decode(&rest[..at]));
339        decoded.extend_from_slice(&rest[at..at + 3]);
340        rest = &rest[at + 3..];
341    }
342    decoded.extend(percent_encoding::percent_decode(rest));
343    Cow::Owned(decoded)
344}
345
346/// Whether `segment` is a variable over several segments: `**`, or a field
347/// template with more than one segment or a `**`.
348fn spans_segments(segment: &str) -> bool {
349    segment == "**"
350        || field_template(segment)
351            .is_some_and(|template| template.contains('/') || template == "**")
352}
353
354/// Whether `segment` is `**` or a variable bound to it (`{name=**}`).
355fn is_double_wildcard(segment: &str) -> bool {
356    segment == "**" || field_template(segment) == Some("**")
357}
358
359/// Whether `segment` ends in a `**`, alone or as the last component of a
360/// field template (`{name=shelves/**}`): it may then match no segment more
361/// (google/api/http.proto).
362fn ends_in_double_wildcard(segment: &str) -> bool {
363    is_double_wildcard(segment)
364        || field_template(segment).is_some_and(|template| template.ends_with("/**"))
365}
366
367/// The template a variable segment binds its field to: `shelves/*` of
368/// `{name=shelves/*}`, none for a plain `{name}` or a literal.
369fn field_template(segment: &str) -> Option<&str> {
370    segment
371        .strip_prefix('{')
372        .and_then(|s| s.strip_suffix('}'))
373        .and_then(|inner| inner.split_once('='))
374        .map(|(_, template)| template)
375}
376
377/// The index of the `}` closing the `{` that opens `segment`.
378fn closing_brace(segment: &str) -> Option<usize> {
379    let mut depth = 0usize;
380    for (i, ch) in segment.char_indices() {
381        match ch {
382            '{' => depth += 1,
383            '}' => {
384                depth = depth.checked_sub(1)?;
385                if depth == 0 {
386                    return Some(i);
387                }
388            }
389            _ => {}
390        }
391    }
392    None
393}
394
395/// Split a path on `/` boundaries that are NOT inside a `{...}` brace span.
396///
397/// google.api.http field templates can embed slashes inside a single capture
398/// (e.g. the AIP-127 resource name `{name=shelves/*/books/*}`), so a naive
399/// `str::split('/')` would fracture the brace span into invalid fragments.
400/// Tracking brace depth keeps each capture intact. Never empty.
401fn split_top_level(path: &str) -> Vec<&str> {
402    let mut segments = Vec::new();
403    let mut depth = 0usize;
404    let mut start = 0usize;
405
406    for (i, ch) in path.char_indices() {
407        match ch {
408            '{' => depth += 1,
409            // Decrement only on a matched brace; a stray `}` (malformed input)
410            // is treated as a literal rather than driving depth negative.
411            '}' if depth > 0 => depth -= 1,
412            '/' if depth == 0 => {
413                segments.push(&path[start..i]);
414                start = i + 1;
415            }
416            _ => {}
417        }
418    }
419    segments.push(&path[start..]);
420    segments
421}
422
423/// One top-level path segment in axum form.
424enum Segment<'a> {
425    Literal(&'a str),
426    Capture { name: String, catch_all: bool },
427}
428
429/// Convert a single top-level path segment from proto template to axum 0.8 form.
430///
431/// `is_last` indicates the terminal segment: axum permits a catch-all capture
432/// (`{*name}`) only there. Anywhere else one comes only from a template
433/// [`MountedPath::new`] already marked unsupported, which is mounted nowhere.
434fn convert_segment(segment: &str, idx: usize, is_last: bool) -> Segment<'_> {
435    if let Some(inner) = segment.strip_prefix('{').and_then(|s| s.strip_suffix('}')) {
436        // Brace capture, possibly with a `name=template` field path.
437        if let Some((name, template)) = inner.split_once('=') {
438            return match template {
439                // Single-segment field path collapses to a plain capture.
440                "*" => capture(name, false),
441                // Multi-segment catch-all maps to axum's `{*name}` (terminal only).
442                "**" => capture(name, is_last),
443                // A template of one segment (`{name=foo}`) matches one
444                // segment: a plain capture, whose value the transcoded routes
445                // check against the template, so it shares a route with a
446                // plain variable there rather than a catch-all beside it.
447                template if !template.contains('/') => capture(name, false),
448                // Templates with interspersed literals (`{name=shelves/*/books/*}`)
449                // have no axum form: axum cannot bind literal segments into one
450                // capture. The last segment is mounted as a catch-all and the
451                // transcoded routes check the value against the template.
452                _ => capture(name, is_last),
453            };
454        }
455        // Plain `{name}` is already valid axum 0.8 syntax.
456        return capture(inner, false);
457    }
458
459    // Bare wildcards: named by position, so they never collide with each
460    // other, and starting with a digit, so never with a field path (whose
461    // names are identifiers) or a spelled-out template's `{field}.{k}`.
462    match segment {
463        "**" => capture(&idx.to_string(), is_last),
464        "*" => capture(&idx.to_string(), false),
465        literal => Segment::Literal(literal),
466    }
467}
468
469fn capture(name: &str, catch_all: bool) -> Segment<'static> {
470    Segment::Capture {
471        name: name.to_owned(),
472        catch_all,
473    }
474}