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}