cloud_sdk/async_resource/
value.rs1use core::{cmp::Ordering, fmt};
2
3pub const MAX_ASYNC_ID_BYTES: usize = 256;
5pub const MAX_ASYNC_TEXT_BYTES: usize = 4096;
7pub const MAX_ASYNC_LINK_BYTES: usize = 4096;
9pub const MAX_ASYNC_PROGRESS_STEPS: usize = 1024;
11pub const MAX_ASYNC_ERRORS: usize = 1024;
13pub const MAX_ASYNC_EVENTS: usize = 1024;
15
16#[derive(Clone, Copy, Debug, Eq, PartialEq)]
18pub enum AsyncResourceValidationError {
19 EmptyId,
21 IdTooLong,
23 InvalidId,
25 EmptyText,
27 TextTooLong,
29 TextControl,
31 EmptyLink,
33 LinkTooLong,
35 InvalidLink,
37 InvalidTimestamp,
39 TimestampOrder,
41 TerminalTimeMismatch,
43 TooManyProgressSteps,
45 TooManyErrors,
47 TooManyEvents,
49}
50
51impl_static_error!(AsyncResourceValidationError,
52 Self::EmptyId => "asynchronous resource identifier is empty",
53 Self::IdTooLong => "asynchronous resource identifier exceeds its hard limit",
54 Self::InvalidId => "asynchronous resource identifier is invalid",
55 Self::EmptyText => "asynchronous resource text is empty",
56 Self::TextTooLong => "asynchronous resource text exceeds its hard limit",
57 Self::TextControl => "asynchronous resource text contains a control character",
58 Self::EmptyLink => "asynchronous resource link is empty",
59 Self::LinkTooLong => "asynchronous resource link exceeds its hard limit",
60 Self::InvalidLink => "asynchronous resource link is invalid",
61 Self::InvalidTimestamp => "asynchronous resource timestamp is invalid",
62 Self::TimestampOrder => "asynchronous resource timestamps are incoherent",
63 Self::TerminalTimeMismatch => "asynchronous resource completion time contradicts its status",
64 Self::TooManyProgressSteps => "asynchronous task has too many progress steps",
65 Self::TooManyErrors => "asynchronous task has too many errors",
66 Self::TooManyEvents => "asynchronous resource batch has too many events",
67);
68
69macro_rules! sensitive_value {
70 ($doc:literal, $name:ident, $maximum:ident, $empty:ident, $long:ident, $validate:expr) => {
71 #[doc = $doc]
72 #[derive(Clone, Copy, Eq, PartialEq)]
73 pub struct $name<'a>(&'a str);
74
75 impl<'a> $name<'a> {
76 pub fn new(value: &'a str) -> Result<Self, AsyncResourceValidationError> {
78 if value.is_empty() {
79 return Err(AsyncResourceValidationError::$empty);
80 }
81 if value.len() > $maximum {
82 return Err(AsyncResourceValidationError::$long);
83 }
84 ($validate)(value)?;
85 Ok(Self(value))
86 }
87
88 pub fn with_str<R>(self, inspect: impl FnOnce(&str) -> R) -> R {
90 inspect(self.0)
91 }
92 }
93
94 impl fmt::Debug for $name<'_> {
95 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
96 formatter.write_str(concat!(stringify!($name), "([redacted])"))
97 }
98 }
99 };
100}
101
102sensitive_value!(
103 "Bounded opaque asynchronous-resource identifier with redacted diagnostics.",
104 AsyncResourceId,
105 MAX_ASYNC_ID_BYTES,
106 EmptyId,
107 IdTooLong,
108 |value: &str| {
109 if !value.bytes().all(|byte| byte.is_ascii_graphic()) {
110 return Err(AsyncResourceValidationError::InvalidId);
111 }
112 Ok(())
113 }
114);
115
116sensitive_value!(
117 "Bounded sensitive asynchronous-resource text with redacted diagnostics.",
118 AsyncResourceText,
119 MAX_ASYNC_TEXT_BYTES,
120 EmptyText,
121 TextTooLong,
122 |value: &str| {
123 if value.chars().any(char::is_control) {
124 return Err(AsyncResourceValidationError::TextControl);
125 }
126 Ok(())
127 }
128);
129
130sensitive_value!(
131 "Bounded non-executable related-resource link with redacted diagnostics.",
132 AsyncResourceLink,
133 MAX_ASYNC_LINK_BYTES,
134 EmptyLink,
135 LinkTooLong,
136 |value: &str| {
137 if value.chars().any(char::is_whitespace) || value.chars().any(char::is_control) {
138 return Err(AsyncResourceValidationError::InvalidLink);
139 }
140 Ok(())
141 }
142);
143
144#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
145struct TimestampParts {
146 year: u16,
147 month: u8,
148 day: u8,
149 hour: u8,
150 minute: u8,
151 second: u8,
152 nanosecond: u32,
153}
154
155#[derive(Clone, Copy)]
157pub struct AsyncResourceTimestamp<'a> {
158 value: &'a str,
159 parts: TimestampParts,
160}
161
162impl PartialEq for AsyncResourceTimestamp<'_> {
163 fn eq(&self, other: &Self) -> bool {
164 self.parts == other.parts
165 }
166}
167
168impl Eq for AsyncResourceTimestamp<'_> {}
169
170impl<'a> AsyncResourceTimestamp<'a> {
171 pub fn parse(value: &'a str) -> Result<Self, AsyncResourceValidationError> {
175 let bytes = value.as_bytes();
176 if !(20..=30).contains(&bytes.len())
177 || bytes.get(4) != Some(&b'-')
178 || bytes.get(7) != Some(&b'-')
179 || bytes.get(10) != Some(&b'T')
180 || bytes.get(13) != Some(&b':')
181 || bytes.get(16) != Some(&b':')
182 || bytes.last() != Some(&b'Z')
183 {
184 return Err(AsyncResourceValidationError::InvalidTimestamp);
185 }
186 let year = u16::try_from(decimal(bytes, 0, 4)?)
187 .map_err(|_| AsyncResourceValidationError::InvalidTimestamp)?;
188 let month = u8::try_from(decimal(bytes, 5, 2)?)
189 .map_err(|_| AsyncResourceValidationError::InvalidTimestamp)?;
190 let day = u8::try_from(decimal(bytes, 8, 2)?)
191 .map_err(|_| AsyncResourceValidationError::InvalidTimestamp)?;
192 let hour = u8::try_from(decimal(bytes, 11, 2)?)
193 .map_err(|_| AsyncResourceValidationError::InvalidTimestamp)?;
194 let minute = u8::try_from(decimal(bytes, 14, 2)?)
195 .map_err(|_| AsyncResourceValidationError::InvalidTimestamp)?;
196 let second = u8::try_from(decimal(bytes, 17, 2)?)
197 .map_err(|_| AsyncResourceValidationError::InvalidTimestamp)?;
198 let nanosecond = fraction(bytes)?;
199 if year == 0
200 || !(1..=12).contains(&month)
201 || day == 0
202 || day > days_in_month(year, month)
203 || hour > 23
204 || minute > 59
205 || second > 59
206 {
207 return Err(AsyncResourceValidationError::InvalidTimestamp);
208 }
209 Ok(Self {
210 value,
211 parts: TimestampParts {
212 year,
213 month,
214 day,
215 hour,
216 minute,
217 second,
218 nanosecond,
219 },
220 })
221 }
222
223 pub fn with_str<R>(self, inspect: impl FnOnce(&str) -> R) -> R {
225 inspect(self.value)
226 }
227
228 pub(super) fn compare(self, other: Self) -> Ordering {
229 self.parts.cmp(&other.parts)
230 }
231}
232
233impl fmt::Debug for AsyncResourceTimestamp<'_> {
234 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
235 formatter.write_str("AsyncResourceTimestamp([redacted])")
236 }
237}
238
239fn decimal(bytes: &[u8], start: usize, len: usize) -> Result<u32, AsyncResourceValidationError> {
240 let mut value = 0_u32;
241 let end = start
242 .checked_add(len)
243 .ok_or(AsyncResourceValidationError::InvalidTimestamp)?;
244 for byte in bytes
245 .get(start..end)
246 .ok_or(AsyncResourceValidationError::InvalidTimestamp)?
247 {
248 if !byte.is_ascii_digit() {
249 return Err(AsyncResourceValidationError::InvalidTimestamp);
250 }
251 let digit = byte
252 .checked_sub(b'0')
253 .ok_or(AsyncResourceValidationError::InvalidTimestamp)?;
254 value = value
255 .checked_mul(10)
256 .and_then(|current| current.checked_add(u32::from(digit)))
257 .ok_or(AsyncResourceValidationError::InvalidTimestamp)?;
258 }
259 Ok(value)
260}
261
262fn fraction(bytes: &[u8]) -> Result<u32, AsyncResourceValidationError> {
263 if bytes.len() == 20 {
264 return Ok(0);
265 }
266 if bytes.get(19) != Some(&b'.') {
267 return Err(AsyncResourceValidationError::InvalidTimestamp);
268 }
269 let end = bytes
270 .len()
271 .checked_sub(1)
272 .ok_or(AsyncResourceValidationError::InvalidTimestamp)?;
273 let digits = bytes
274 .get(20..end)
275 .ok_or(AsyncResourceValidationError::InvalidTimestamp)?;
276 if digits.is_empty() || digits.len() > 9 || !digits.iter().all(u8::is_ascii_digit) {
277 return Err(AsyncResourceValidationError::InvalidTimestamp);
278 }
279 let mut value = 0_u32;
280 for byte in digits {
281 let digit = byte
282 .checked_sub(b'0')
283 .ok_or(AsyncResourceValidationError::InvalidTimestamp)?;
284 value = value
285 .checked_mul(10)
286 .and_then(|current| current.checked_add(u32::from(digit)))
287 .ok_or(AsyncResourceValidationError::InvalidTimestamp)?;
288 }
289 let exponent = 9_usize
290 .checked_sub(digits.len())
291 .and_then(|value| u32::try_from(value).ok())
292 .ok_or(AsyncResourceValidationError::InvalidTimestamp)?;
293 let scale = 10_u32
294 .checked_pow(exponent)
295 .ok_or(AsyncResourceValidationError::InvalidTimestamp)?;
296 value
297 .checked_mul(scale)
298 .ok_or(AsyncResourceValidationError::InvalidTimestamp)
299}
300
301const fn days_in_month(year: u16, month: u8) -> u8 {
302 match month {
303 2 if year.is_multiple_of(400) || (year.is_multiple_of(4) && !year.is_multiple_of(100)) => {
304 29
305 }
306 2 => 28,
307 4 | 6 | 9 | 11 => 30,
308 _ => 31,
309 }
310}