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