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