1use std::num::{NonZeroU32, NonZeroU64};
8
9use crate::{Result, UtilsError};
10
11#[must_use]
16pub fn non_zero_u32(value: u32) -> NonZeroU32 {
17 NonZeroU32::new(value).unwrap_or(NonZeroU32::MIN)
18}
19
20#[must_use]
24pub fn non_zero_u64(value: u64) -> NonZeroU64 {
25 NonZeroU64::new(value).unwrap_or(NonZeroU64::MIN)
26}
27
28pub fn bytes_to_usize(bytes: i64, value_name: &str) -> Result<usize> {
34 i64_to_usize(bytes, value_name)
35}
36
37pub fn i32_to_usize(value: i32, value_name: &str) -> Result<usize> {
43 usize::try_from(value).map_err(|_| {
44 UtilsError::numeric_conversion(format!("{value_name} cannot be negative: {value}"))
45 })
46}
47
48pub fn i64_to_i32(value: i64, value_name: &str) -> Result<i32> {
54 i32::try_from(value).map_err(|_| {
55 UtilsError::numeric_conversion(format!("{value_name} is outside i32 range: {value}"))
56 })
57}
58
59pub fn i64_to_usize(value: i64, value_name: &str) -> Result<usize> {
65 usize::try_from(value).map_err(|_| {
66 UtilsError::numeric_conversion(format!(
67 "{value_name} exceeds platform usize range or is negative: {value}"
68 ))
69 })
70}
71
72pub fn i64_to_u64(value: i64, value_name: &str) -> Result<u64> {
78 u64::try_from(value).map_err(|_| {
79 UtilsError::numeric_conversion(format!("{value_name} cannot be negative: {value}"))
80 })
81}
82
83pub fn u128_to_u64(value: u128, value_name: &str) -> Result<u64> {
89 u64::try_from(value).map_err(|_| {
90 UtilsError::numeric_conversion(format!("{value_name} exceeds u64 range: {value}"))
91 })
92}
93
94#[must_use]
96pub fn u128_to_u64_saturating(value: u128) -> u64 {
97 u64::try_from(value).unwrap_or(u64::MAX)
98}
99
100pub fn f64_seconds_to_u64_millis(seconds: f64, value_name: &str) -> Result<u64> {
107 if !seconds.is_finite() {
108 return Err(UtilsError::invalid_value(format!(
109 "{value_name} must be finite: {seconds}"
110 )));
111 }
112 if seconds < 0.0 {
113 return Err(UtilsError::invalid_value(format!(
114 "{value_name} cannot be negative: {seconds}"
115 )));
116 }
117
118 let duration = std::time::Duration::try_from_secs_f64(seconds).map_err(|_| {
119 UtilsError::invalid_value(format!("{value_name} exceeds duration range: {seconds}"))
120 })?;
121 let rounded_duration = duration
122 .checked_add(std::time::Duration::from_micros(500))
123 .ok_or_else(|| {
124 UtilsError::invalid_value(format!("{value_name} exceeds duration range: {seconds}"))
125 })?;
126
127 u128_to_u64(rounded_duration.as_millis(), value_name)
128}
129
130pub fn u32_to_usize(value: u32, value_name: &str) -> Result<usize> {
136 usize::try_from(value).map_err(|_| {
137 UtilsError::numeric_conversion(format!(
138 "{value_name} exceeds platform usize range: {value}"
139 ))
140 })
141}
142
143#[must_use]
147pub fn u32_to_i64(value: u32) -> i64 {
148 i64::from(value)
149}
150
151pub fn u32_to_i32(value: u32, value_name: &str) -> Result<i32> {
157 i32::try_from(value).map_err(|_| {
158 UtilsError::numeric_conversion(format!("{value_name} exceeds i32 range: {value}"))
159 })
160}
161
162pub fn u64_to_i64(value: u64, value_name: &str) -> Result<i64> {
168 i64::try_from(value).map_err(|_| {
169 UtilsError::numeric_conversion(format!("{value_name} exceeds i64 range: {value}"))
170 })
171}
172
173pub fn u64_to_usize(value: u64, value_name: &str) -> Result<usize> {
179 usize::try_from(value).map_err(|_| {
180 UtilsError::numeric_conversion(format!(
181 "{value_name} exceeds platform usize range: {value}"
182 ))
183 })
184}
185
186pub fn usize_to_i32(value: usize, value_name: &str) -> Result<i32> {
192 i32::try_from(value).map_err(|_| {
193 UtilsError::numeric_conversion(format!("{value_name} exceeds i32 range: {value}"))
194 })
195}
196
197pub fn usize_to_i64(value: usize, value_name: &str) -> Result<i64> {
206 i64::try_from(value).map_err(|_| {
207 UtilsError::numeric_conversion(format!("{value_name} exceeds i64 range: {value}"))
208 })
209}
210
211pub fn usize_to_u32(value: usize, value_name: &str) -> Result<u32> {
217 u32::try_from(value).map_err(|_| {
218 UtilsError::numeric_conversion(format!("{value_name} exceeds u32 range: {value}"))
219 })
220}
221
222pub fn usize_to_u64(value: usize, value_name: &str) -> Result<u64> {
228 u64::try_from(value).map_err(|_| {
229 UtilsError::numeric_conversion(format!("{value_name} exceeds u64 range: {value}"))
230 })
231}
232
233pub fn calc_total_chunks(total_size: i64, chunk_size: i64, context: &str) -> Result<i32> {
240 if total_size < 0 {
241 return Err(UtilsError::invalid_value(format!(
242 "{context} total_size cannot be negative: {total_size}"
243 )));
244 }
245 if chunk_size <= 0 {
246 return Err(UtilsError::invalid_value(format!(
247 "{context} chunk_size must be positive, got {chunk_size}"
248 )));
249 }
250
251 let adjusted = total_size.checked_add(chunk_size - 1).ok_or_else(|| {
252 UtilsError::invalid_value(format!("{context} total_size is too large: {total_size}"))
253 })?;
254 let chunks = adjusted / chunk_size;
255
256 i32::try_from(chunks).map_err(|_| {
257 UtilsError::invalid_value(format!("{context} requires too many chunks: {chunks}"))
258 })
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn bytes_to_usize_accepts_positive_values() {
267 assert_eq!(bytes_to_usize(5_242_880, "chunk_size").unwrap(), 5_242_880);
268 }
269
270 #[test]
271 fn non_zero_helpers_preserve_positive_values() {
272 assert_eq!(non_zero_u32(7).get(), 7);
273 assert_eq!(non_zero_u64(9).get(), 9);
274 }
275
276 #[test]
277 fn non_zero_helpers_fallback_to_min_for_zero() {
278 assert_eq!(non_zero_u32(0), NonZeroU32::MIN);
279 assert_eq!(non_zero_u64(0), NonZeroU64::MIN);
280 }
281
282 #[test]
283 fn bytes_to_usize_rejects_negative_values() {
284 let err = bytes_to_usize(-1, "chunk_size").unwrap_err();
285 assert!(matches!(err, UtilsError::NumericConversion(_)));
286 }
287
288 #[test]
289 fn i32_to_usize_rejects_negative_values() {
290 let err = i32_to_usize(-1, "total_chunks").unwrap_err();
291 assert!(matches!(err, UtilsError::NumericConversion(_)));
292 }
293
294 #[test]
295 fn i64_to_i32_accepts_bounds_and_rejects_overflow() {
296 assert_eq!(i64_to_i32(i64::from(i32::MIN), "value").unwrap(), i32::MIN);
297 assert_eq!(i64_to_i32(i64::from(i32::MAX), "value").unwrap(), i32::MAX);
298
299 let positive = i64_to_i32(i64::from(i32::MAX) + 1, "value").unwrap_err();
300 assert!(matches!(positive, UtilsError::NumericConversion(_)));
301
302 let negative = i64_to_i32(i64::from(i32::MIN) - 1, "value").unwrap_err();
303 assert!(matches!(negative, UtilsError::NumericConversion(_)));
304 }
305
306 #[test]
307 fn i64_to_usize_accepts_zero_and_rejects_negative_values() {
308 assert_eq!(i64_to_usize(0, "offset").unwrap(), 0);
309 assert_eq!(i64_to_usize(42, "offset").unwrap(), 42);
310
311 let err = i64_to_usize(-1, "offset").unwrap_err();
312 assert!(matches!(err, UtilsError::NumericConversion(_)));
313 }
314
315 #[test]
316 fn i64_to_u64_accepts_positive_values() {
317 assert_eq!(i64_to_u64(42, "content_length").unwrap(), 42);
318 }
319
320 #[test]
321 fn i64_to_u64_rejects_negative_values() {
322 let err = i64_to_u64(-1, "content_length").unwrap_err();
323 assert!(matches!(err, UtilsError::NumericConversion(_)));
324 }
325
326 #[test]
327 fn u128_to_u64_accepts_bounds_and_rejects_overflow() {
328 assert_eq!(u128_to_u64(0, "size").unwrap(), 0);
329 assert_eq!(u128_to_u64(u128::from(u64::MAX), "size").unwrap(), u64::MAX);
330
331 let err = u128_to_u64(u128::from(u64::MAX) + 1, "size").unwrap_err();
332 assert!(matches!(err, UtilsError::NumericConversion(_)));
333 }
334
335 #[test]
336 fn u128_to_u64_saturating_clamps_overflow() {
337 assert_eq!(u128_to_u64_saturating(0), 0);
338 assert_eq!(u128_to_u64_saturating(u128::from(u64::MAX)), u64::MAX);
339 assert_eq!(u128_to_u64_saturating(u128::from(u64::MAX) + 1), u64::MAX);
340 }
341
342 #[test]
343 fn f64_seconds_to_u64_millis_rounds_to_nearest_millisecond() {
344 assert_eq!(f64_seconds_to_u64_millis(1.2344, "duration").unwrap(), 1234);
345 assert_eq!(f64_seconds_to_u64_millis(1.2345, "duration").unwrap(), 1235);
346 assert_eq!(f64_seconds_to_u64_millis(0.0004, "duration").unwrap(), 0);
347 assert_eq!(f64_seconds_to_u64_millis(0.0005, "duration").unwrap(), 1);
348 }
349
350 #[test]
351 fn f64_seconds_to_u64_millis_accepts_zero() {
352 assert_eq!(f64_seconds_to_u64_millis(0.0, "duration").unwrap(), 0);
353 }
354
355 #[test]
356 fn f64_seconds_to_u64_millis_rejects_invalid_values() {
357 let negative = f64_seconds_to_u64_millis(-1.0, "duration").unwrap_err();
358 assert!(matches!(negative, UtilsError::InvalidValue(_)));
359
360 let nan = f64_seconds_to_u64_millis(f64::NAN, "duration").unwrap_err();
361 assert!(matches!(nan, UtilsError::InvalidValue(_)));
362
363 let infinity = f64_seconds_to_u64_millis(f64::INFINITY, "duration").unwrap_err();
364 assert!(matches!(infinity, UtilsError::InvalidValue(_)));
365 }
366
367 #[test]
368 fn f64_seconds_to_u64_millis_rejects_u64_millis_overflow() {
369 let overflow_seconds = "18446744073709552".parse::<f64>().unwrap();
370 let err = f64_seconds_to_u64_millis(overflow_seconds, "duration").unwrap_err();
371 assert!(matches!(err, UtilsError::NumericConversion(_)));
372 }
373
374 #[test]
375 fn u32_conversions_are_lossless_on_supported_targets() {
376 assert_eq!(u32_to_i32(0, "value").unwrap(), 0);
377 assert_eq!(u32_to_i32(i32::MAX as u32, "value").unwrap(), i32::MAX);
378 assert_eq!(u32_to_i64(u32::MAX), i64::from(u32::MAX));
379 assert_eq!(u32_to_usize(0, "value").unwrap(), 0);
380
381 #[cfg(any(target_pointer_width = "32", target_pointer_width = "64"))]
382 assert_eq!(u32_to_usize(u32::MAX, "value").unwrap(), u32::MAX as usize);
383 }
384
385 #[test]
386 fn u32_to_i32_rejects_overflow() {
387 let overflow = u32::try_from(i32::MAX)
388 .unwrap_or(u32::MAX)
389 .saturating_add(1);
390 let err = u32_to_i32(overflow, "value").unwrap_err();
391 assert!(matches!(err, UtilsError::NumericConversion(_)));
392 }
393
394 #[test]
395 fn usize_to_i32_rejects_overflow() {
396 let overflow = usize::try_from(i32::MAX)
397 .unwrap_or(usize::MAX)
398 .saturating_add(1);
399 let err = usize_to_i32(overflow, "uploaded_part_count").unwrap_err();
400 assert!(matches!(err, UtilsError::NumericConversion(_)));
401 }
402
403 #[test]
404 fn usize_to_u32_accepts_bounds_and_rejects_overflow() {
405 assert_eq!(usize_to_u32(0, "part_count").unwrap(), 0);
406
407 let max = usize::try_from(u32::MAX).unwrap_or(usize::MAX);
408 assert_eq!(usize_to_u32(max, "part_count").unwrap(), u32::MAX);
409
410 if let Some(overflow) = max.checked_add(1) {
411 let err = usize_to_u32(overflow, "part_count").unwrap_err();
412 assert!(matches!(err, UtilsError::NumericConversion(_)));
413 }
414 }
415
416 #[test]
417 fn usize_to_i64_accepts_small_values() {
418 assert_eq!(usize_to_i64(1024, "body_len").unwrap(), 1024);
419 }
420
421 #[test]
422 fn usize_to_u64_accepts_common_values() {
423 assert_eq!(usize_to_u64(0, "test").unwrap(), 0);
424 #[cfg(target_pointer_width = "64")]
425 assert_eq!(usize_to_u64(usize::MAX, "test").unwrap(), u64::MAX);
426 }
427
428 #[test]
429 fn u64_to_i64_accepts_within_i64_range() {
430 assert_eq!(u64_to_i64(0, "test").unwrap(), 0);
431 let max_i64_as_u64 = u64::try_from(i64::MAX).unwrap_or(u64::MAX);
432 assert_eq!(u64_to_i64(max_i64_as_u64, "test").unwrap(), i64::MAX);
433 }
434
435 #[test]
436 fn u64_to_i64_rejects_overflow() {
437 let overflow = u64::try_from(i64::MAX)
438 .unwrap_or(u64::MAX)
439 .saturating_add(1);
440 let err = u64_to_i64(overflow, "test").unwrap_err();
441 assert!(matches!(err, UtilsError::NumericConversion(_)));
442 }
443
444 #[test]
445 fn u64_to_usize_accepts_within_platform_range() {
446 assert_eq!(u64_to_usize(0, "test").unwrap(), 0);
447 #[cfg(target_pointer_width = "64")]
448 assert_eq!(u64_to_usize(u64::MAX, "test").unwrap(), usize::MAX);
449 }
451
452 #[test]
453 #[cfg(target_pointer_width = "32")]
454 fn u64_to_usize_rejects_overflow() {
455 let err = u64_to_usize(u64::MAX, "cursor_value").unwrap_err();
457 assert!(matches!(err, UtilsError::NumericConversion(_)));
458 }
459
460 #[test]
461 fn calc_total_chunks_rounds_up() {
462 assert_eq!(
463 calc_total_chunks(10_485_761, 5_242_880, "multipart upload").unwrap(),
464 3
465 );
466 }
467
468 #[test]
469 fn calc_total_chunks_handles_exact_division() {
470 assert_eq!(
471 calc_total_chunks(10_485_760, 5_242_880, "multipart upload").unwrap(),
472 2
473 );
474 }
475
476 #[test]
477 fn calc_total_chunks_allows_zero_size() {
478 assert_eq!(calc_total_chunks(0, 5, "multipart upload").unwrap(), 0);
479 }
480
481 #[test]
482 fn calc_total_chunks_rejects_negative_total_size() {
483 let err = calc_total_chunks(-1, 5, "multipart upload").unwrap_err();
484 assert!(matches!(err, UtilsError::InvalidValue(_)));
485 }
486
487 #[test]
488 fn calc_total_chunks_rejects_non_positive_chunk_size() {
489 let err = calc_total_chunks(10, 0, "multipart upload").unwrap_err();
490 assert!(matches!(err, UtilsError::InvalidValue(_)));
491 }
492
493 #[test]
494 fn calc_total_chunks_rejects_i32_overflow() {
495 let overflow_total_size = (i64::from(i32::MAX) + 1) * 5;
496 let err = calc_total_chunks(overflow_total_size, 1, "multipart upload").unwrap_err();
497 assert!(matches!(err, UtilsError::InvalidValue(_)));
498 }
499}