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#[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 #[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#[derive(Clone, Copy, Eq, PartialEq)]
62pub enum ProviderLinkExecutionError<E> {
63 Pagination(PaginationError),
65 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
89pub 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 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 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 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}