Skip to main content

rama_net/uri/
resolve.rs

1//! RFC 3986 §5.2 reference resolution.
2//!
3//! Resolve a reference URI against a base URI to produce a target URI.
4
5use crate::std::sync::Arc;
6
7use super::owned::OwnedUriRef;
8use super::parser::MAX_URI_LEN;
9use super::{Uri, UriInner};
10
11use rama_core::bytes::BytesMut;
12
13/// Errors from [`Uri::resolve`](super::Uri::resolve) / [`Uri::resolve_strict`](super::Uri::resolve_strict).
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum ResolveError {
16    /// The base URI has no scheme. RFC 3986 §5.2.1 requires the base
17    /// to be an absolute URI.
18    BaseHasNoScheme,
19    /// The base or reference is the asterisk-form (`*`), which is an
20    /// HTTP request-target and has no meaning as a URI base or
21    /// reference.
22    AsteriskNotResolvable,
23    /// The resolved URI exceeds the parser's length cap.
24    ResultTooLong { len: usize },
25    /// (Strict only) A `..` segment would pop past the path root.
26    /// Graceful mode silently clamps at root.
27    DotSegmentTraversalPastRoot,
28}
29
30impl core::fmt::Display for ResolveError {
31    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
32        match self {
33            Self::BaseHasNoScheme => f.write_str("base URI has no scheme"),
34            Self::AsteriskNotResolvable => {
35                f.write_str("asterisk-form URI cannot be used as a base or reference")
36            }
37            Self::ResultTooLong { len } => write!(f, "resolved URI is {len} bytes — exceeds cap"),
38            Self::DotSegmentTraversalPastRoot => {
39                f.write_str("`..` segment would traverse past path root (strict mode)")
40            }
41        }
42    }
43}
44
45impl core::error::Error for ResolveError {}
46
47/// Resolution mode. Internal — the public API exposes `resolve` (graceful)
48/// and `resolve_strict`.
49#[derive(Clone, Copy, PartialEq, Eq)]
50pub(super) enum ResolveMode {
51    /// Browser-compatible: apply the §5.2.2 scheme-matching loophole;
52    /// silently clamp excess `..` at root.
53    Graceful,
54    /// RFC §5.2.2 strict: no scheme-matching loophole; reject `..`
55    /// traversal past root.
56    Strict,
57}
58
59/// Entry point. `Uri::resolve` / `Uri::resolve_strict` funnel through here.
60pub(super) fn resolve(base: &Uri, reference: &Uri, mode: ResolveMode) -> Result<Uri, ResolveError> {
61    // ---- input validation -----------------------------------------------
62
63    if matches!(base.inner, UriInner::Asterisk) || matches!(reference.inner, UriInner::Asterisk) {
64        return Err(ResolveError::AsteriskNotResolvable);
65    }
66    if base.scheme().is_none() {
67        return Err(ResolveError::BaseHasNoScheme);
68    }
69
70    // Materialise both inputs into Owned snapshots. Cheap when the source
71    // is already Owned (Arc-clone); copies the component bytes for Lazy.
72    // Destructure so the branches below can move fields without cloning.
73    let OwnedUriRef {
74        scheme: b_scheme,
75        authority: b_authority,
76        path: b_path,
77        query: b_query,
78        fragment: _,
79    } = base.as_owned_components();
80    let OwnedUriRef {
81        scheme: r_scheme,
82        authority: r_authority,
83        path: r_path,
84        query: r_query,
85        fragment: r_fragment,
86    } = reference.as_owned_components();
87
88    // ---- scheme-matching loophole (§5.2.2 non-strict) -------------------
89    //
90    // In graceful mode, if R has a scheme equal to B's, treat R as if it
91    // had no scheme. Strict mode skips this and keeps R.scheme.
92    let r_has_effective_scheme = match (mode, &r_scheme, &b_scheme) {
93        (ResolveMode::Graceful, Some(r_s), Some(b_s)) if r_s == b_s => false,
94        _ => r_scheme.is_some(),
95    };
96
97    // ---- recompose target components per §5.2.2 -------------------------
98
99    let (t_scheme, t_authority, t_path, t_query) = if r_has_effective_scheme {
100        // Branch 1: R has scheme — use R verbatim (path is dot-removed).
101        (
102            r_scheme,
103            r_authority,
104            remove_dot_segments(&r_path, mode)?,
105            r_query.map(|q| q.bytes),
106        )
107    } else if r_authority.is_some() {
108        // Branch 2: R has authority but no scheme — inherit B's scheme.
109        (
110            b_scheme,
111            r_authority,
112            remove_dot_segments(&r_path, mode)?,
113            r_query.map(|q| q.bytes),
114        )
115    } else if r_path.is_empty() {
116        // Branch 3: same-document / query-only / fragment-only reference.
117        // Inherit B's authority and path. Query: R's if defined, else B's.
118        let query = r_query
119            .map(|q| q.bytes)
120            .or_else(|| b_query.map(|q| q.bytes));
121        (b_scheme, b_authority, b_path, query)
122    } else {
123        // Branch 4: R has a non-empty relative path.
124        let raw_path = if r_path.starts_with(b"/") {
125            // 4a: R.path is absolute-path → use as-is.
126            r_path
127        } else {
128            // 4b: merge with B.path, then dot-remove.
129            merge_paths(b_authority.is_some(), &b_path, &r_path)
130        };
131        (
132            b_scheme,
133            b_authority,
134            remove_dot_segments(&raw_path, mode)?,
135            r_query.map(|q| q.bytes),
136        )
137    };
138
139    // Fragment always comes from the reference (§5.2.2).
140    let t_fragment = r_fragment.map(|f| f.bytes);
141
142    let owned = OwnedUriRef {
143        scheme: t_scheme,
144        authority: t_authority,
145        path: t_path,
146        query: t_query.map(|bytes| super::Query { bytes }),
147        fragment: t_fragment.map(|bytes| super::Fragment { bytes }),
148    };
149
150    // Cap the result so it can round-trip through the parser (which uses
151    // u16 offsets internally).
152    let total = serialized_len(&owned);
153    if total > MAX_URI_LEN {
154        return Err(ResolveError::ResultTooLong { len: total });
155    }
156
157    Ok(Uri {
158        inner: UriInner::Owned(Arc::new(owned)),
159    })
160}
161
162// ---------------------------------------------------------------------------
163// §5.2.3 merge_paths
164// ---------------------------------------------------------------------------
165
166/// Combine base path with a relative reference path per RFC 3986 §5.2.3.
167fn merge_paths(b_has_authority: bool, b_path: &[u8], r_path: &[u8]) -> BytesMut {
168    if b_has_authority && b_path.is_empty() {
169        // Special case: empty base path with authority — prepend "/".
170        let mut out = BytesMut::with_capacity(1 + r_path.len());
171        out.extend_from_slice(b"/");
172        out.extend_from_slice(r_path);
173        return out;
174    }
175    // "B.path up to and including the last `/`" — empty if no `/`.
176    let cutoff = memchr::memrchr(b'/', b_path).map_or(0, |i| i + 1);
177    let mut out = BytesMut::with_capacity(cutoff + r_path.len());
178    out.extend_from_slice(&b_path[..cutoff]);
179    out.extend_from_slice(r_path);
180    out
181}
182
183// ---------------------------------------------------------------------------
184// §5.2.4 remove_dot_segments
185// ---------------------------------------------------------------------------
186
187/// Walk `input` once, applying the §5.2.4 dot-segment removal rules to
188/// produce a normalised path.
189fn remove_dot_segments(input: &[u8], mode: ResolveMode) -> Result<BytesMut, ResolveError> {
190    let mut output = BytesMut::with_capacity(input.len());
191    let mut i = 0;
192
193    while i < input.len() {
194        let rest = &input[i..];
195
196        // 2A: drop leading "../" or "./".
197        if rest.starts_with(b"../") {
198            i += 3;
199            continue;
200        }
201        if rest.starts_with(b"./") {
202            i += 2;
203            continue;
204        }
205
206        // 2B: "/./"  → "/"  (advance past "/.", leaving "/" at position i+2)
207        if rest.starts_with(b"/./") {
208            i += 2;
209            continue;
210        }
211        // 2B: "/."   → "/"  (end of input — emit "/" and finish)
212        if rest == b"/." {
213            output.extend_from_slice(b"/");
214            break;
215        }
216
217        // 2C: "/../" → "/" (pop last output segment, advance past "/..")
218        if rest.starts_with(b"/../") {
219            pop_last_segment(&mut output, mode)?;
220            i += 3;
221            continue;
222        }
223        // 2C: "/.."  → "/" (end of input — pop then emit "/")
224        if rest == b"/.." {
225            pop_last_segment(&mut output, mode)?;
226            output.extend_from_slice(b"/");
227            break;
228        }
229
230        // 2D: input is exactly "." or ".." → drop.
231        if rest == b"." || rest == b".." {
232            break;
233        }
234
235        // 2E: move the first path segment to output. The segment is
236        // (optional leading `/`) + (chars up to next `/` or end).
237        let seg_end = if rest[0] == b'/' {
238            // Find next `/` after the leading one, or end of input.
239            memchr::memchr(b'/', &rest[1..]).map_or(rest.len(), |p| p + 1)
240        } else {
241            memchr::memchr(b'/', rest).unwrap_or(rest.len())
242        };
243        output.extend_from_slice(&rest[..seg_end]);
244        i += seg_end;
245    }
246
247    Ok(output)
248}
249
250/// Remove the last segment and its preceding `/` from `output`. Empty
251/// output is a no-op under graceful mode; under strict, it's an error.
252fn pop_last_segment(output: &mut BytesMut, mode: ResolveMode) -> Result<(), ResolveError> {
253    if let Some(last_slash) = memchr::memrchr(b'/', output) {
254        output.truncate(last_slash);
255        return Ok(());
256    }
257    if !output.is_empty() {
258        // Single segment with no leading `/` — clear it.
259        output.clear();
260        return Ok(());
261    }
262    // Output is empty.
263    match mode {
264        ResolveMode::Strict => Err(ResolveError::DotSegmentTraversalPastRoot),
265        ResolveMode::Graceful => Ok(()),
266    }
267}
268
269// ---------------------------------------------------------------------------
270/// Apply RFC 3986 §5.2.4 dot-segment removal to a standalone path,
271/// graceful mode (`..` past root is silently clamped). Returned buffer
272/// is always representable; the underlying machinery only ever errors
273/// under strict mode, which this wrapper hard-pins to graceful.
274///
275/// Used by [`crate::uri::canonicalize`] for §6.2.2.3 path-segment
276/// normalization.
277pub(super) fn remove_dot_segments_graceful(input: &[u8]) -> BytesMut {
278    // Graceful mode never errors — `pop_last_segment` is the only error
279    // site and it only fires under `ResolveMode::Strict`.
280    remove_dot_segments(input, ResolveMode::Graceful).unwrap_or_else(|_| {
281        // Defence-in-depth: any future change that would let graceful
282        // mode error trips here loudly rather than silently returning
283        // an empty path.
284        debug_assert!(false, "graceful remove_dot_segments must not error");
285        BytesMut::from(input)
286    })
287}
288
289// ---------------------------------------------------------------------------
290// Size estimation for the cap check
291// ---------------------------------------------------------------------------
292
293/// Estimate the serialized byte length of `owned` (matches what `Display`
294/// would emit). Used to enforce the [`MAX_URI_LEN`] cap on the resolved
295/// URI without an extra `to_string()` allocation.
296fn serialized_len(owned: &OwnedUriRef) -> usize {
297    use core::fmt::Write as _;
298
299    let mut n = 0;
300    if let Some(scheme) = &owned.scheme {
301        n += scheme.as_str().len() + 1; // ":" suffix
302    }
303    if let Some(auth) = &owned.authority {
304        n += 2; // "//"
305        if let Some(ui) = &auth.user_info {
306            n += ui.as_bytes().len() + 1; // "@" suffix
307        }
308        // Host length without materialising a `String`: drive `Display`
309        // into a write-counting adapter that just accumulates the byte
310        // length of every fragment.
311        let mut counter = FmtLenCounter(0);
312        #[expect(
313            clippy::let_underscore_must_use,
314            reason = "FmtLenCounter::write_str is infallible by construction"
315        )]
316        let _ = write!(&mut counter, "{}", auth.address.host);
317        n += counter.0;
318        match auth.address.port {
319            crate::address::OptPort::Unset => {}
320            crate::address::OptPort::Empty => {
321                n += 1; // bare ":"
322            }
323            crate::address::OptPort::Set(port) => {
324                n += 1 + port_decimal_len(port); // ":" + digits
325            }
326        }
327    }
328    n += owned.path.len();
329    if let Some(q) = &owned.query {
330        n += 1 + q.bytes.len(); // "?" + bytes
331    }
332    if let Some(f) = &owned.fragment {
333        n += 1 + f.bytes.len(); // "#" + bytes
334    }
335    n
336}
337
338/// [`fmt::Write`] sink that accumulates the byte length of everything
339/// written to it, discarding the bytes themselves. Lets [`serialized_len`]
340/// compute a `Host` (or any other `Display`) rendered size with zero
341/// allocation.
342struct FmtLenCounter(usize);
343
344impl core::fmt::Write for FmtLenCounter {
345    fn write_str(&mut self, s: &str) -> core::fmt::Result {
346        self.0 += s.len();
347        Ok(())
348    }
349}
350
351#[inline]
352fn port_decimal_len(port: u16) -> usize {
353    match port {
354        0..=9 => 1,
355        10..=99 => 2,
356        100..=999 => 3,
357        1000..=9999 => 4,
358        _ => 5,
359    }
360}