1use crate::data::{FiniteNumber, Patch};
3use crate::pagination::{
4 CollectOptions, CollectionResult, Completion, Items, Page, Pages, Truncation,
5};
6use crate::{Client, ClientBuilder, Error, ErrorKind, RequestOptions, Result, Session};
7use futures_util::{Stream, StreamExt};
8use serde::Serialize;
9use serde_json::Value;
10use std::{fmt, future::Future, hash::Hash, pin::Pin};
11
12const SAFE_INTEGER: u64 = 9_007_199_254_740_991;
13pub(crate) const MAX_HELPER_BYTES: usize = 8 * 1024 * 1024;
14pub(crate) const MAX_HELPER_NODES: usize = 100_000;
15pub(crate) const MAX_HELPER_DEPTH: usize = 64;
16
17pub(crate) fn bounded_json(input: &Value) -> Result<()> {
18 let mut pending = vec![(input, 0usize)];
19 let mut nodes = 0usize;
20 while let Some((value, depth)) = pending.pop() {
21 nodes += 1;
22 if depth > MAX_HELPER_DEPTH || nodes > MAX_HELPER_NODES {
23 return Err(Error::new(ErrorKind::Limit));
24 }
25 let children = match value {
26 Value::Array(v) => v.len(),
27 Value::Object(v) => v.len(),
28 _ => 0,
29 };
30 if nodes.saturating_add(pending.len()).saturating_add(children) > MAX_HELPER_NODES {
31 return Err(Error::new(ErrorKind::Limit));
32 }
33 match value {
34 Value::Array(values) => pending.extend(values.iter().map(|v| (v, depth + 1))),
35 Value::Object(values) => pending.extend(values.values().map(|v| (v, depth + 1))),
36 Value::Number(number) if number.as_f64().is_none_or(|v| !v.is_finite()) => {
37 return Err(Error::new(ErrorKind::Validation));
38 }
39 _ => {}
40 }
41 }
42 crate::engine::serialize_bounded(input, MAX_HELPER_BYTES)?;
43 Ok(())
44}
45
46pub fn init(builder: ClientBuilder) -> Result<Client> {
48 builder.build()
49}
50
51#[derive(Clone, Default)]
52#[non_exhaustive]
53pub struct ListingOptions {
54 pub skip: Option<u64>,
55 pub limit: Option<u32>,
56 pub search: Option<String>,
57}
58impl fmt::Debug for ListingOptions {
59 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60 f.write_str("ListingOptions([REDACTED])")
61 }
62}
63pub fn serialize_listing(options: Option<&ListingOptions>) -> Result<Vec<(String, String)>> {
65 let mut query = Vec::new();
66 if let Some(options) = options {
67 if let Some(skip) = options.skip {
68 if skip > SAFE_INTEGER {
69 return Err(Error::new(ErrorKind::Validation));
70 }
71 query.push(("skip".into(), skip.to_string()));
72 }
73 if let Some(limit) = options.limit {
74 query.push(("limit".into(), limit.to_string()));
75 }
76 if let Some(search) = &options.search {
77 if search.len() > crate::config::MAX_URL_BYTES || search.chars().any(char::is_control) {
78 return Err(Error::new(ErrorKind::Validation));
79 }
80 query.push(("search".into(), search.clone()));
81 }
82 }
83 Ok(query)
84}
85
86pub fn paginate<'a, T: 'a, C, F, Fut>(
88 initial: C,
89 fetch: F,
90 max_pages: usize,
91 session: Session,
92 options: RequestOptions,
93) -> Result<Items<'a, T, C>>
94where
95 C: Clone + Eq + Hash + Serialize + Send + 'a,
96 F: FnMut(C, RequestOptions) -> Fut + Send + 'a,
97 Fut: Future<Output = Result<Page<T, C>>> + Send + 'a,
98{
99 Ok(Pages::new(initial, options, session, max_pages, fetch)?.items())
100}
101
102#[derive(Clone, Copy)]
103#[non_exhaustive]
104pub struct WalkAllOptions {
105 pub limit: u32,
106 pub collect: CollectOptions,
107}
108impl Default for WalkAllOptions {
109 fn default() -> Self {
110 Self {
111 limit: 50,
112 collect: CollectOptions::default(),
113 }
114 }
115}
116#[derive(Clone, Copy)]
117#[non_exhaustive]
118pub struct SpringWalkOptions {
119 pub size: u32,
120 pub collect: CollectOptions,
121}
122impl Default for SpringWalkOptions {
123 fn default() -> Self {
124 Self {
125 size: 50,
126 collect: CollectOptions::default(),
127 }
128 }
129}
130pub struct SkipPage<T> {
131 pub items: Vec<T>,
132 pub total: Option<FiniteNumber>,
133}
134pub struct SpringPage<T> {
135 pub items: Vec<T>,
136 pub last: bool,
137}
138crate::redaction::redacted_debug!(WalkAllOptions, SpringWalkOptions);
139impl<T> fmt::Debug for SkipPage<T> {
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
141 f.write_str("SkipPage([REDACTED])")
142 }
143}
144impl<T> fmt::Debug for SpringPage<T> {
145 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
146 f.write_str("SpringPage([REDACTED])")
147 }
148}
149
150pub async fn collect_skip_pages<'a, T: Serialize + Send + 'a, F, Fut>(
151 mut fetch: F,
152 walk: WalkAllOptions,
153 session: Session,
154 options: RequestOptions,
155) -> CollectionResult<T, u64>
156where
157 F: FnMut(u64, u32, RequestOptions) -> Fut + Send + 'a,
158 Fut: Future<Output = Result<SkipPage<T>>> + Send + 'a,
159{
160 walk.collect.validate()?;
161 if walk.limit == 0 {
162 return Err(Error::new(ErrorKind::Validation).into());
163 }
164 let pages = Pages::new(
165 0u64,
166 options,
167 session,
168 walk.collect.max_pages,
169 move |skip, options| {
170 let future = fetch(skip, walk.limit, options);
171 async move {
172 let page = future.await?;
173 let next = skip
174 .checked_add(page.items.len() as u64)
175 .filter(|v| *v <= SAFE_INTEGER)
176 .ok_or_else(|| Error::new(ErrorKind::Pagination))?;
177 let total = page
178 .total
179 .map(|number| {
180 let value = number
181 .as_f64()
182 .ok_or_else(|| Error::new(ErrorKind::Pagination))?;
183 if value < 0.0 || value > SAFE_INTEGER as f64 || value.fract() != 0.0 {
184 return Err(Error::new(ErrorKind::Pagination));
185 }
186 Ok(value as u64)
187 })
188 .transpose()?;
189 let more = !page.items.is_empty()
190 && total.map_or(page.items.len() == walk.limit as usize, |total| {
191 next < total
192 });
193 Ok(Page {
194 items: page.items,
195 next: more.then_some(next),
196 })
197 }
198 },
199 )?;
200 pages.collect(walk.collect).await
201}
202pub async fn collect_spring_pages<'a, T: Serialize + Send + 'a, F, Fut>(
203 mut fetch: F,
204 walk: SpringWalkOptions,
205 session: Session,
206 options: RequestOptions,
207) -> CollectionResult<T, u64>
208where
209 F: FnMut(u64, u32, RequestOptions) -> Fut + Send + 'a,
210 Fut: Future<Output = Result<SpringPage<T>>> + Send + 'a,
211{
212 walk.collect.validate()?;
213 if walk.size == 0 {
214 return Err(Error::new(ErrorKind::Validation).into());
215 }
216 let pages = Pages::new(
217 0u64,
218 options,
219 session,
220 walk.collect.max_pages,
221 move |number, options| {
222 let future = fetch(number, walk.size, options);
223 async move {
224 let page = future.await?;
225 if !page.last && page.items.is_empty() {
226 return Err(Error::new(ErrorKind::Pagination));
227 }
228 let next = if page.last {
229 None
230 } else {
231 Some(
232 number
233 .checked_add(1)
234 .filter(|v| *v <= SAFE_INTEGER)
235 .ok_or_else(|| Error::new(ErrorKind::Pagination))?,
236 )
237 };
238 Ok(Page {
239 items: page.items,
240 next,
241 })
242 }
243 },
244 )?;
245 pages.collect(walk.collect).await
246}
247
248pub struct StreamListing<S: Stream> {
250 stream: Pin<Box<S>>,
251 pending: Option<S::Item>,
252 ended: bool,
253}
254impl<S: Stream> StreamListing<S> {
255 pub fn new(stream: S) -> Self {
256 Self {
257 stream: Box::pin(stream),
258 pending: None,
259 ended: false,
260 }
261 }
262}
263impl<S: Stream> fmt::Debug for StreamListing<S> {
264 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
265 f.write_str("StreamListing([REDACTED])")
266 }
267}
268pub struct StreamCollection<T> {
269 pub items: Vec<T>,
270 pub completion: Completion,
271 pub wire_bytes: usize,
272}
273impl<T> StreamCollection<T> {
274 pub fn is_complete(&self) -> bool {
275 self.completion == Completion::Complete
276 }
277}
278impl<T> fmt::Debug for StreamCollection<T> {
279 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
280 f.debug_struct("StreamCollection")
281 .field("item_count", &self.items.len())
282 .field("completion", &self.completion)
283 .finish()
284 }
285}
286pub struct StreamCollectionError<T> {
287 error: Error,
288 partial: Box<StreamCollection<T>>,
289}
290impl<T> StreamCollectionError<T> {
291 pub fn error(&self) -> &Error {
292 &self.error
293 }
294 pub fn partial(&self) -> &StreamCollection<T> {
295 &self.partial
296 }
297 pub fn into_error(self) -> Error {
298 self.error
299 }
300 pub fn into_parts(self) -> (Error, StreamCollection<T>) {
301 (self.error, *self.partial)
302 }
303 fn interrupted(error: Error, mut partial: StreamCollection<T>) -> Self {
304 partial.completion = Completion::Interrupted;
305 Self {
306 error,
307 partial: Box::new(partial),
308 }
309 }
310}
311impl<T> From<Error> for StreamCollectionError<T> {
312 fn from(error: Error) -> Self {
313 Self::interrupted(
314 error,
315 StreamCollection {
316 items: Vec::new(),
317 completion: Completion::Interrupted,
318 wire_bytes: 2,
319 },
320 )
321 }
322}
323impl<T> fmt::Debug for StreamCollectionError<T> {
324 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
325 f.debug_struct("StreamCollectionError")
326 .field("error", &self.error)
327 .field("partial", &self.partial)
328 .finish()
329 }
330}
331impl<T> fmt::Display for StreamCollectionError<T> {
332 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
333 self.error.fmt(f)
334 }
335}
336impl<T> std::error::Error for StreamCollectionError<T> {
337 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
338 Some(&self.error)
339 }
340}
341pub type StreamCollectionResult<T> =
342 std::result::Result<StreamCollection<T>, StreamCollectionError<T>>;
343pub async fn collect_all<S, T>(
346 listing: &mut StreamListing<S>,
347 caps: CollectOptions,
348 session: &Session,
349 options: &RequestOptions,
350) -> StreamCollectionResult<T>
351where
352 S: Stream<Item = Result<T>>,
353 T: Serialize,
354{
355 caps.validate()?;
356 let mut result = StreamCollection {
357 items: Vec::new(),
358 completion: Completion::Complete,
359 wire_bytes: 2,
360 };
361 loop {
362 if let Err(error) = options.run(session, async { Ok(()) }).await {
363 return Err(StreamCollectionError::interrupted(error, result));
364 }
365 if listing.ended {
366 return Ok(result);
367 }
368 if result.items.len() >= caps.max_items {
369 result.completion = Completion::Truncated(Truncation::Items);
370 return Ok(result);
371 }
372 let item = if let Some(item) = listing.pending.take() {
373 Some(item)
374 } else {
375 match options
376 .run(session, async { Ok(listing.stream.next().await) })
377 .await
378 {
379 Ok(item) => item,
380 Err(error) => {
381 return Err(StreamCollectionError::interrupted(error, result));
382 }
383 }
384 };
385 let Some(item) = item else {
386 listing.ended = true;
387 return Ok(result);
388 };
389 let item = match item {
390 Ok(item) => item,
391 Err(error) => {
392 return Err(StreamCollectionError::interrupted(error, result));
393 }
394 };
395 let comma = usize::from(!result.items.is_empty());
396 let remaining = caps
397 .max_bytes
398 .saturating_sub(result.wire_bytes)
399 .saturating_sub(comma);
400 match crate::engine::serialize_bounded(&item, remaining) {
401 Ok(bytes) => {
402 result.wire_bytes += bytes.len() + comma;
403 result.items.push(item);
404 }
405 Err(error) if error.kind() == ErrorKind::Limit => {
406 listing.pending = Some(Ok(item));
407 result.completion = Completion::Truncated(Truncation::Bytes);
408 return Ok(result);
409 }
410 Err(error) => {
411 listing.pending = Some(Ok(item));
412 return Err(StreamCollectionError::interrupted(error, result));
413 }
414 }
415 }
416}
417
418pub fn json_nullable<T>(
420 input: Option<&Value>,
421 inner: impl FnOnce(&Value) -> Result<T>,
422) -> Result<Patch<T>> {
423 match input {
424 None => Ok(Patch::Unset),
425 Some(Value::Null) => Ok(Patch::Null),
426 Some(value) => {
427 bounded_json(value)?;
428 inner(value).map(Patch::Value)
429 }
430 }
431}
432
433#[cfg(feature = "models")]
435pub fn page_schema<T>(
436 input: &Value,
437 mut inner: impl FnMut(&Value) -> Result<T>,
438) -> Result<crate::dlp::models::Page<T>> {
439 bounded_json(input)?;
440 let source: crate::dlp::models::Page<crate::data::JsonValue> =
441 crate::json_decode::from_value(input.clone())
442 .map_err(|_| Error::new(ErrorKind::Validation))?;
443 let mut page = crate::dlp::models::Page::new(
444 source
445 .content
446 .iter()
447 .map(|item| inner(item.as_value()))
448 .collect::<Result<_>>()?,
449 );
450 page.empty = source.empty;
451 page.first = source.first;
452 page.last = source.last;
453 page.number = source.number;
454 page.number_of_elements = source.number_of_elements;
455 page.pageable = source.pageable;
456 page.size = source.size;
457 page.sort = source.sort;
458 page.total_elements = source.total_elements;
459 page.total_pages = source.total_pages;
460 page.extensions = source.extensions;
461 Ok(page)
462}
463
464#[cfg(feature = "iam")]
466pub fn generate_workspace_scope_name(name: &str) -> Result<String> {
467 if name.len() > 4096 {
468 return Err(Error::new(ErrorKind::Limit));
469 }
470 let mut stem = String::new();
471 let mut separator = false;
472 for ch in name.chars().flat_map(char::to_lowercase) {
473 if ch.is_ascii_lowercase() || ch.is_ascii_digit() {
474 if separator && !stem.is_empty() {
475 stem.push('_');
476 }
477 stem.push(ch);
478 separator = false;
479 } else {
480 separator = true;
481 }
482 }
483 if stem.is_empty() {
484 stem.push_str("workspace");
485 }
486 const ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz0123456789";
487 let mut suffix = String::new();
488 for _ in 0..16 {
490 let mut bytes = [0u8; 16];
491 getrandom::fill(&mut bytes).map_err(|_| Error::new(ErrorKind::Io))?;
492 for byte in bytes {
493 if byte < 252 {
494 suffix.push(ALPHABET[usize::from(byte) % 36] as char);
495 if suffix.len() == 6 {
496 return Ok(format!("ws_{stem}_{suffix}"));
497 }
498 }
499 }
500 }
501 Err(Error::new(ErrorKind::Io))
502}