agent_first_data/
validation.rs1pub fn parse_size(s: &str) -> Option<u64> {
2 const MAX_SAFE_INTEGER: u64 = 9_007_199_254_740_991;
3 let s = s.trim();
4 if s.is_empty() {
5 return None;
6 }
7 let units = [
8 ("KiB", 1024u64),
9 ("MiB", 1024u64.pow(2)),
10 ("GiB", 1024u64.pow(3)),
11 ("TiB", 1024u64.pow(4)),
12 ("kB", 1000u64),
13 ("MB", 1000u64.pow(2)),
14 ("GB", 1000u64.pow(3)),
15 ("TB", 1000u64.pow(4)),
16 ("B", 1u64),
17 ];
18 let (unit, mult) = units
19 .iter()
20 .find(|(unit, _mult)| s.strip_suffix(unit).is_some())?;
21 let num_str = &s[..s.len() - unit.len()];
22 if num_str.is_empty() || !is_decimal_number(num_str) {
23 return None;
24 }
25 if let Ok(n) = num_str.parse::<u64>() {
26 let result = n.checked_mul(*mult)?;
27 return (result <= MAX_SAFE_INTEGER).then_some(result);
28 }
29 if !num_str.contains('.') && !num_str.contains('e') && !num_str.contains('E') {
31 return None;
32 }
33 let f: f64 = num_str.parse().ok()?;
34 if f < 0.0 || f.is_nan() || f.is_infinite() {
35 return None;
36 }
37 let result = f * *mult as f64;
38 if result > MAX_SAFE_INTEGER as f64 {
39 return None;
40 }
41 Some(result as u64)
42}
43
44fn is_decimal_number(s: &str) -> bool {
45 let bytes = s.as_bytes();
46 let mut i = 0;
47 let mut digits = 0;
48 while i < bytes.len() && bytes[i].is_ascii_digit() {
49 i += 1;
50 digits += 1;
51 }
52 if i < bytes.len() && bytes[i] == b'.' {
53 i += 1;
54 while i < bytes.len() && bytes[i].is_ascii_digit() {
55 i += 1;
56 digits += 1;
57 }
58 }
59 if digits == 0 {
60 return false;
61 }
62 if i < bytes.len() && matches!(bytes[i], b'e' | b'E') {
63 i += 1;
64 if i < bytes.len() && matches!(bytes[i], b'+' | b'-') {
65 i += 1;
66 }
67 let exp_start = i;
68 while i < bytes.len() && bytes[i].is_ascii_digit() {
69 i += 1;
70 }
71 if i == exp_start {
72 return false;
73 }
74 }
75 i == bytes.len()
76}
77
78pub fn normalize_utc_offset(s: &str) -> Option<String> {
84 let s = s.trim();
85 if s.eq_ignore_ascii_case("utc") || s.eq_ignore_ascii_case("z") {
86 return Some("UTC".to_string());
87 }
88 let sign = match s.as_bytes().first()? {
89 b'+' => '+',
90 b'-' => '-',
91 _ => return None,
92 };
93 let body = &s[1..];
94 let (hours, minutes) = parse_utc_offset_body(body)?;
95 if hours > 23 || minutes > 59 {
96 return None;
97 }
98 if hours == 0 && minutes == 0 {
99 return Some("UTC".to_string());
100 }
101 Some(format!("{sign}{hours:02}:{minutes:02}"))
102}
103
104pub fn is_valid_rfc3339_date(s: &str) -> bool {
106 let bytes = s.as_bytes();
107 if bytes.len() != 10 || bytes[4] != b'-' || bytes[7] != b'-' {
108 return false;
109 }
110 let Some(year) = parse_ascii_u16_bytes(&bytes[0..4]) else {
111 return false;
112 };
113 let Some(month) = parse_ascii_u8_bytes(&bytes[5..7]) else {
114 return false;
115 };
116 let Some(day) = parse_ascii_u8_bytes(&bytes[8..10]) else {
117 return false;
118 };
119 (1..=12).contains(&month) && (1..=days_in_month(year, month)).contains(&day)
120}
121
122pub fn is_valid_rfc3339_time(s: &str) -> bool {
127 let bytes = s.as_bytes();
128 if bytes.len() < 8 || bytes[2] != b':' || bytes[5] != b':' {
129 return false;
130 }
131 let Some(hour) = parse_ascii_u8_bytes(&bytes[0..2]) else {
132 return false;
133 };
134 let Some(minute) = parse_ascii_u8_bytes(&bytes[3..5]) else {
135 return false;
136 };
137 let Some(second) = parse_ascii_u8_bytes(&bytes[6..8]) else {
138 return false;
139 };
140 if hour > 23 || minute > 59 || second > 59 {
141 return false;
142 }
143 if bytes.len() == 8 {
144 return true;
145 }
146 bytes[8] == b'.' && bytes.len() > 9 && bytes[9..].iter().all(u8::is_ascii_digit)
147}
148
149fn parse_utc_offset_body(body: &str) -> Option<(u8, u8)> {
150 if body.is_empty() {
151 return None;
152 }
153 if let Some((hours, minutes)) = body.split_once(':') {
154 if hours.is_empty() || hours.len() > 2 || minutes.len() != 2 {
155 return None;
156 }
157 return Some((parse_ascii_u8(hours)?, parse_ascii_u8(minutes)?));
158 }
159 if !body.bytes().all(|b| b.is_ascii_digit()) {
160 return None;
161 }
162 match body.len() {
163 1 | 2 => Some((parse_ascii_u8(body)?, 0)),
164 4 => Some((parse_ascii_u8(&body[..2])?, parse_ascii_u8(&body[2..])?)),
165 _ => None,
166 }
167}
168
169fn parse_ascii_u8(s: &str) -> Option<u8> {
170 if s.is_empty() || !s.bytes().all(|b| b.is_ascii_digit()) {
171 return None;
172 }
173 s.parse().ok()
174}
175
176fn parse_ascii_u8_bytes(bytes: &[u8]) -> Option<u8> {
177 let n = parse_ascii_u16_bytes(bytes)?;
178 u8::try_from(n).ok()
179}
180
181fn parse_ascii_u16_bytes(bytes: &[u8]) -> Option<u16> {
182 if bytes.is_empty() || !bytes.iter().all(u8::is_ascii_digit) {
183 return None;
184 }
185 let mut value = 0u16;
186 for byte in bytes {
187 value = value.checked_mul(10)?;
188 value = value.checked_add(u16::from(byte - b'0'))?;
189 }
190 Some(value)
191}
192
193fn days_in_month(year: u16, month: u8) -> u8 {
194 match month {
195 1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
196 4 | 6 | 9 | 11 => 30,
197 2 if is_leap_year(year) => 29,
198 2 => 28,
199 _ => 0,
200 }
201}
202
203fn is_leap_year(year: u16) -> bool {
204 let year = u32::from(year);
205 year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
206}