Skip to main content

cloud_sdk/pagination/
link.rs

1use core::fmt;
2
3use cloud_sdk_sanitization::{SecretBuffer, sanitize_bytes, sanitize_value};
4
5use crate::Method;
6use crate::authentication::{
7    AsyncAuthenticatedTransport, AuthenticatedRequest, AuthenticationScopePolicy,
8    BlockingAuthenticatedTransport,
9};
10use crate::operation::OperationId;
11use crate::transport::{
12    BoundTransport, EndpointIdentity, EndpointScheme, RawResponsePolicy, RequestPath,
13    RequestTarget, ResponseWriter, TransportRequest,
14};
15
16use super::{PaginationError, PaginationLimits};
17
18/// Immutable operation boundary used to validate provider pagination links.
19#[derive(Clone, Copy)]
20pub struct ProviderLinkBinding<'a> {
21    endpoint: EndpointIdentity<'a>,
22    method: Method,
23    operation: OperationId,
24    path: RequestPath<'a>,
25}
26
27impl<'a> ProviderLinkBinding<'a> {
28    /// Binds links to one endpoint, method, operation, and exact path pattern.
29    #[must_use]
30    pub const fn new(
31        endpoint: EndpointIdentity<'a>,
32        method: Method,
33        operation: OperationId,
34        path: RequestPath<'a>,
35    ) -> Self {
36        Self {
37            endpoint,
38            method,
39            operation,
40            path,
41        }
42    }
43}
44
45impl fmt::Debug for ProviderLinkBinding<'_> {
46    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47        formatter
48            .debug_struct("ProviderLinkBinding")
49            .field("endpoint", &self.endpoint)
50            .field("method", &self.method)
51            .field("operation", &self.operation)
52            .field("path", &"[redacted]")
53            .finish()
54    }
55}
56
57/// Provider-link validation or authenticated transport failure.
58///
59/// Transport details remain available through pattern matching but are always
60/// redacted from `Debug` and `Display` diagnostics.
61#[derive(Clone, Copy, Eq, PartialEq)]
62pub enum ProviderLinkExecutionError<E> {
63    /// Link state, endpoint, method, or operation validation failed.
64    Pagination(PaginationError),
65    /// The endpoint-verified authenticated transport failed.
66    Transport(E),
67}
68
69impl<E> fmt::Debug for ProviderLinkExecutionError<E> {
70    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
71        match self {
72            Self::Pagination(error) => formatter.debug_tuple("Pagination").field(error).finish(),
73            Self::Transport(_) => formatter.write_str("Transport([redacted])"),
74        }
75    }
76}
77
78impl<E> fmt::Display for ProviderLinkExecutionError<E> {
79    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
80        formatter.write_str(match self {
81            Self::Pagination(_) => "provider pagination link validation failed",
82            Self::Transport(_) => "provider pagination link transport failed",
83        })
84    }
85}
86
87impl<E> core::error::Error for ProviderLinkExecutionError<E> {}
88
89/// Cleanup-owning operation-bound provider pagination link.
90///
91/// Exact validated origin-form path/query bytes are copied without decoding,
92/// re-encoding, reordering, or conversion into a structured query. This type
93/// is intentionally neither `Copy` nor `Clone`.
94///
95/// ```compile_fail
96/// use cloud_sdk::pagination::ValidatedProviderLink;
97/// fn require_copy<T: Copy>() {}
98/// require_copy::<ValidatedProviderLink<'static, 'static>>();
99/// ```
100///
101/// ```compile_fail
102/// use cloud_sdk::pagination::ValidatedProviderLink;
103/// use cloud_sdk::transport::RequestQuery;
104///
105/// fn into_structured<'a>(link: ValidatedProviderLink<'a, 'a>) -> RequestQuery<'a> {
106///     link.into()
107/// }
108/// ```
109pub struct ValidatedProviderLink<'storage, 'endpoint> {
110    target: SecretBuffer<'storage>,
111    target_len: usize,
112    endpoint: EndpointIdentity<'endpoint>,
113    method: Method,
114    operation: OperationId,
115}
116
117impl<'storage, 'endpoint> ValidatedProviderLink<'storage, 'endpoint> {
118    /// Atomically validates and transfers an absolute or origin-form link.
119    ///
120    /// Source and complete destination storage are cleared on every failure;
121    /// source is also cleared after successful transfer. Absolute links must
122    /// match the exact bound scheme and authority. The resulting target must
123    /// match the exact operation path.
124    pub fn transfer_from(
125        source: &mut [u8],
126        destination: &'storage mut [u8],
127        binding: ProviderLinkBinding<'endpoint>,
128        limits: PaginationLimits,
129    ) -> Result<Self, PaginationError> {
130        sanitize_bytes(destination);
131        let source = SecretBuffer::new(source);
132        let mut destination = SecretBuffer::new(destination);
133        if source.as_slice().is_empty() {
134            return Err(PaginationError::MissingState);
135        }
136        if source.as_slice().len() > limits.max_state_bytes() {
137            return Err(PaginationError::StateTooLong);
138        }
139        let link = core::str::from_utf8(source.as_slice())
140            .map_err(|_| PaginationError::InvalidProviderLink)?;
141        let raw_target = extract_target(link, binding.endpoint)?;
142        let target = RequestTarget::from_provider_link(raw_target)
143            .map_err(|_| PaginationError::InvalidProviderLink)?;
144        if target.path() != binding.path {
145            return Err(PaginationError::ProviderLinkPathChanged);
146        }
147        let output = destination
148            .as_mut_slice()
149            .get_mut(..raw_target.len())
150            .ok_or(PaginationError::OutputTooSmall)?;
151        output.copy_from_slice(raw_target.as_bytes());
152        Ok(Self {
153            target: destination,
154            target_len: raw_target.len(),
155            endpoint: binding.endpoint,
156            method: binding.method,
157            operation: binding.operation,
158        })
159    }
160
161    /// Validates and executes one authenticated blocking continuation request.
162    ///
163    /// Endpoint verification and execution use the same transport object, so
164    /// callers cannot separate the destination check from request dispatch.
165    pub fn execute_blocking<T>(
166        &self,
167        transport: &T,
168        method: Method,
169        operation: OperationId,
170        authentication: AuthenticationScopePolicy<'_>,
171        response_policy: RawResponsePolicy<'_>,
172        response: &mut ResponseWriter<'_>,
173    ) -> Result<(), ProviderLinkExecutionError<T::Error>>
174    where
175        T: BoundTransport + BlockingAuthenticatedTransport,
176    {
177        let request = self
178            .request_for(transport, method, operation)
179            .map_err(ProviderLinkExecutionError::Pagination)?;
180        transport
181            .send_authenticated(
182                AuthenticatedRequest::new(request, authentication, response_policy),
183                response,
184            )
185            .map_err(ProviderLinkExecutionError::Transport)
186    }
187
188    /// Validates and executes one authenticated asynchronous continuation request.
189    ///
190    /// Endpoint verification and execution use the same transport object. The
191    /// returned future remains executor-neutral and borrows the link state until
192    /// the transport attempt completes or is cancelled.
193    pub async fn execute_async<'transport, 'request, 'policy, 'writer, T>(
194        &'request self,
195        transport: &'transport T,
196        method: Method,
197        operation: OperationId,
198        authentication: AuthenticationScopePolicy<'policy>,
199        response_policy: RawResponsePolicy<'policy>,
200        response: &'writer mut ResponseWriter<'_>,
201    ) -> Result<(), ProviderLinkExecutionError<T::Error>>
202    where
203        T: BoundTransport + AsyncAuthenticatedTransport,
204        'transport: 'writer,
205        'request: 'writer,
206        'policy: 'writer,
207    {
208        let request = self
209            .request_for(transport, method, operation)
210            .map_err(ProviderLinkExecutionError::Pagination)?;
211        transport
212            .send_authenticated(
213                AuthenticatedRequest::new(request, authentication, response_policy),
214                response,
215            )
216            .await
217            .map_err(ProviderLinkExecutionError::Transport)
218    }
219
220    fn request_for<'request, T: BoundTransport>(
221        &'request self,
222        transport: &T,
223        method: Method,
224        operation: OperationId,
225    ) -> Result<TransportRequest<'request>, PaginationError> {
226        let actual = transport
227            .endpoint_identity()
228            .map_err(|_| PaginationError::ProviderLinkAuthorityChanged)?;
229        if actual != self.endpoint {
230            return Err(PaginationError::ProviderLinkAuthorityChanged);
231        }
232        if method != self.method {
233            return Err(PaginationError::ProviderLinkMethodChanged);
234        }
235        if operation != self.operation {
236            return Err(PaginationError::ProviderLinkOperationChanged);
237        }
238        let bytes = self
239            .target
240            .as_slice()
241            .get(..self.target_len)
242            .ok_or(PaginationError::InvalidProviderLink)?;
243        let value =
244            core::str::from_utf8(bytes).map_err(|_| PaginationError::InvalidProviderLink)?;
245        let target = RequestTarget::from_provider_link(value)
246            .map_err(|_| PaginationError::InvalidProviderLink)?;
247        Ok(TransportRequest::new(method, target))
248    }
249}
250
251impl Drop for ValidatedProviderLink<'_, '_> {
252    fn drop(&mut self) {
253        sanitize_value(&mut self.target_len);
254    }
255}
256
257impl fmt::Debug for ValidatedProviderLink<'_, '_> {
258    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
259        formatter
260            .debug_struct("ValidatedProviderLink")
261            .field("target", &"[redacted]")
262            .field("endpoint", &self.endpoint)
263            .field("method", &self.method)
264            .field("operation", &self.operation)
265            .finish()
266    }
267}
268
269fn extract_target<'a>(
270    link: &'a str,
271    endpoint: EndpointIdentity<'_>,
272) -> Result<&'a str, PaginationError> {
273    if link.contains('#') {
274        return Err(PaginationError::ProviderLinkFragment);
275    }
276    if link.starts_with('/') {
277        return Ok(link);
278    }
279    let (scheme, remainder) = if let Some(value) = link.strip_prefix("https://") {
280        (EndpointScheme::Https, value)
281    } else if let Some(value) = link.strip_prefix("http://") {
282        (EndpointScheme::Http, value)
283    } else {
284        return Err(PaginationError::InvalidProviderLink);
285    };
286    if scheme != endpoint.scheme() {
287        return Err(PaginationError::ProviderLinkSchemeChanged);
288    }
289    let path_start = remainder
290        .find('/')
291        .ok_or(PaginationError::InvalidProviderLink)?;
292    let authority = remainder
293        .get(..path_start)
294        .ok_or(PaginationError::InvalidProviderLink)?;
295    if authority.contains('@') {
296        return Err(PaginationError::ProviderLinkUserinfo);
297    }
298    validate_authority(authority, endpoint)?;
299    remainder
300        .get(path_start..)
301        .ok_or(PaginationError::InvalidProviderLink)
302}
303
304fn validate_authority(
305    authority: &str,
306    endpoint: EndpointIdentity<'_>,
307) -> Result<(), PaginationError> {
308    let (host, port) = split_authority(authority, endpoint.scheme())?;
309    let candidate = EndpointIdentity::new(endpoint.scheme(), host, port, endpoint.base_path())
310        .map_err(|_| PaginationError::ProviderLinkAuthorityChanged)?;
311    if candidate != endpoint {
312        return Err(PaginationError::ProviderLinkAuthorityChanged);
313    }
314    Ok(())
315}
316
317fn split_authority(
318    authority: &str,
319    scheme: EndpointScheme,
320) -> Result<(&str, u16), PaginationError> {
321    let default_port = match scheme {
322        EndpointScheme::Http => 80,
323        EndpointScheme::Https => 443,
324    };
325    if authority.starts_with('[') {
326        let close = authority
327            .find(']')
328            .ok_or(PaginationError::ProviderLinkAuthorityChanged)?;
329        let host_end = close
330            .checked_add(1)
331            .ok_or(PaginationError::ProviderLinkAuthorityChanged)?;
332        let host = authority
333            .get(..host_end)
334            .ok_or(PaginationError::ProviderLinkAuthorityChanged)?;
335        let suffix = authority
336            .get(host_end..)
337            .ok_or(PaginationError::ProviderLinkAuthorityChanged)?;
338        let port = if suffix.is_empty() {
339            default_port
340        } else {
341            parse_port(
342                suffix
343                    .strip_prefix(':')
344                    .ok_or(PaginationError::ProviderLinkAuthorityChanged)?,
345            )?
346        };
347        return Ok((host, port));
348    }
349    if let Some((host, port)) = authority.rsplit_once(':') {
350        if host.contains(':') {
351            return Err(PaginationError::ProviderLinkAuthorityChanged);
352        }
353        Ok((host, parse_port(port)?))
354    } else {
355        Ok((authority, default_port))
356    }
357}
358
359fn parse_port(value: &str) -> Result<u16, PaginationError> {
360    if value.is_empty() || !value.bytes().all(|byte| byte.is_ascii_digit()) {
361        return Err(PaginationError::ProviderLinkAuthorityChanged);
362    }
363    value
364        .parse::<u16>()
365        .ok()
366        .filter(|port| *port != 0)
367        .ok_or(PaginationError::ProviderLinkAuthorityChanged)
368}