1use crate::google::protobuf::Duration;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
7pub enum DurationError {
8 #[error("negative protobuf Duration cannot be converted to std::time::Duration")]
10 NegativeDuration,
11 #[error("nanos field has invalid value or sign mismatch with seconds")]
17 InvalidNanos,
18}
19
20#[cfg(feature = "std")]
21impl TryFrom<Duration> for std::time::Duration {
22 type Error = DurationError;
23
24 fn try_from(d: Duration) -> Result<Self, Self::Error> {
36 if !(-999_999_999..=999_999_999).contains(&d.nanos) {
39 return Err(DurationError::InvalidNanos);
40 }
41 let sign_mismatch = (d.seconds > 0 && d.nanos < 0) || (d.seconds < 0 && d.nanos > 0);
43 if sign_mismatch {
44 return Err(DurationError::InvalidNanos);
45 }
46 if d.seconds < 0 || d.nanos < 0 {
48 return Err(DurationError::NegativeDuration);
49 }
50 Ok(std::time::Duration::new(d.seconds as u64, d.nanos as u32))
51 }
52}
53
54#[cfg(feature = "std")]
55impl From<std::time::Duration> for Duration {
56 fn from(d: std::time::Duration) -> Self {
64 Duration {
65 seconds: d.as_secs().min(i64::MAX as u64) as i64,
67 nanos: d.subsec_nanos() as i32,
68 ..Default::default()
69 }
70 }
71}
72
73#[cfg(feature = "json")]
81fn duration_to_string(secs: i64, nanos: i32) -> alloc::string::String {
82 use alloc::format;
83 use alloc::string::String;
84 let negative = secs < 0 || (secs == 0 && nanos < 0);
85 let abs_secs = secs.unsigned_abs();
86 let abs_nanos = nanos.unsigned_abs();
87 let sign = if negative { "-" } else { "" };
88 let frac = if abs_nanos == 0 {
89 String::new()
90 } else if abs_nanos % 1_000_000 == 0 {
91 format!(".{:03}", abs_nanos / 1_000_000)
92 } else if abs_nanos % 1_000 == 0 {
93 format!(".{:06}", abs_nanos / 1_000)
94 } else {
95 format!(".{:09}", abs_nanos)
96 };
97 format!("{sign}{abs_secs}{frac}s")
98}
99
100#[cfg(feature = "json")]
103fn parse_duration_string(s: &str) -> Option<(i64, i32)> {
104 let body = s.strip_suffix('s')?;
105 let negative = body.starts_with('-');
106 let body = if negative {
107 body.strip_prefix('-')?
108 } else {
109 body
110 };
111 if body.starts_with(['-', '+']) {
114 return None;
115 }
116
117 let (sec_str, nano_str) = match body.find('.') {
118 Some(dot) => (&body[..dot], &body[dot + 1..]),
119 None => (body, ""),
120 };
121
122 let abs_secs: i64 = sec_str.parse().ok()?;
123 let abs_nanos: i32 = if nano_str.is_empty() {
124 0
125 } else {
126 if nano_str.len() > 9 || !nano_str.bytes().all(|b| b.is_ascii_digit()) {
129 return None;
130 }
131 let n: i32 = nano_str.parse().ok()?;
132 n * 10_i32.pow(9 - nano_str.len() as u32)
133 };
134
135 let (secs, nanos) = if negative {
136 (-abs_secs, -abs_nanos)
137 } else {
138 (abs_secs, abs_nanos)
139 };
140 if !is_valid_duration(secs, nanos) {
141 return None;
142 }
143 Some((secs, nanos))
144}
145
146#[cfg(feature = "json")]
150const MAX_DURATION_SECS: i64 = 315_576_000_000;
151
152#[cfg(feature = "json")]
153fn is_valid_duration(secs: i64, nanos: i32) -> bool {
154 if !(-999_999_999..=999_999_999).contains(&nanos) {
155 return false;
156 }
157 if !(-MAX_DURATION_SECS..=MAX_DURATION_SECS).contains(&secs) {
158 return false;
159 }
160 if (secs > 0 && nanos < 0) || (secs < 0 && nanos > 0) {
162 return false;
163 }
164 true
165}
166
167#[cfg(feature = "json")]
168impl serde::Serialize for Duration {
169 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
176 use alloc::format;
177 if !is_valid_duration(self.seconds, self.nanos) {
178 return Err(serde::ser::Error::custom(format!(
179 "invalid Duration: seconds={}, nanos={} is out of range",
180 self.seconds, self.nanos
181 )));
182 }
183 s.serialize_str(&duration_to_string(self.seconds, self.nanos))
184 }
185}
186
187#[cfg(feature = "json")]
188impl<'de> serde::Deserialize<'de> for Duration {
189 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
191 use alloc::{format, string::String};
192 let s: String = serde::Deserialize::deserialize(d)?;
193 let (seconds, nanos) = parse_duration_string(&s)
194 .ok_or_else(|| serde::de::Error::custom(format!("invalid Duration string: {s}")))?;
195 Ok(Duration {
196 seconds,
197 nanos,
198 ..Default::default()
199 })
200 }
201}
202
203impl Duration {
204 pub fn from_secs(seconds: i64) -> Self {
206 Duration {
207 seconds,
208 nanos: 0,
209 ..Default::default()
210 }
211 }
212
213 pub fn from_secs_nanos(seconds: i64, nanos: i32) -> Self {
223 debug_assert!(
225 (-999_999_999..=999_999_999).contains(&nanos),
226 "nanos ({nanos}) must be in [-999_999_999, 999_999_999]"
227 );
228 debug_assert!(
229 !((seconds > 0 && nanos < 0) || (seconds < 0 && nanos > 0)),
230 "nanos sign must be consistent with seconds sign"
231 );
232 Duration {
233 seconds,
234 nanos,
235 ..Default::default()
236 }
237 }
238
239 pub fn from_secs_nanos_checked(seconds: i64, nanos: i32) -> Option<Self> {
242 if !(-999_999_999..=999_999_999).contains(&nanos) {
244 return None;
245 }
246 if (seconds > 0 && nanos < 0) || (seconds < 0 && nanos > 0) {
247 return None;
248 }
249 Some(Duration {
250 seconds,
251 nanos,
252 ..Default::default()
253 })
254 }
255
256 pub fn from_millis(millis: i64) -> Self {
261 Duration {
262 seconds: millis / 1_000,
263 nanos: ((millis % 1_000) * 1_000_000) as i32,
266 ..Default::default()
267 }
268 }
269
270 pub fn from_micros(micros: i64) -> Self {
272 Duration {
273 seconds: micros / 1_000_000,
274 nanos: ((micros % 1_000_000) * 1_000) as i32,
277 ..Default::default()
278 }
279 }
280
281 pub fn from_nanos(nanos: i64) -> Self {
283 Duration {
284 seconds: nanos / 1_000_000_000,
285 nanos: (nanos % 1_000_000_000) as i32,
287 ..Default::default()
288 }
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use super::*;
295
296 #[cfg(feature = "std")]
297 #[test]
298 fn std_duration_roundtrip() {
299 let d = std::time::Duration::new(300, 500_000_000);
300 let proto: Duration = d.into();
301 assert_eq!(proto.seconds, 300);
302 assert_eq!(proto.nanos, 500_000_000);
303 let back: std::time::Duration = proto.try_into().unwrap();
304 assert_eq!(back, d);
305 }
306
307 #[cfg(feature = "std")]
308 #[test]
309 fn zero_duration_roundtrip() {
310 let d = std::time::Duration::ZERO;
311 let proto: Duration = d.into();
312 let back: std::time::Duration = proto.try_into().unwrap();
313 assert_eq!(back, d);
314 }
315
316 #[cfg(feature = "std")]
317 #[test]
318 fn negative_duration_rejected() {
319 let neg = Duration {
320 seconds: -5,
321 nanos: 0,
322 ..Default::default()
323 };
324 let result: Result<std::time::Duration, _> = neg.try_into();
325 assert_eq!(result, Err(DurationError::NegativeDuration));
326 }
327
328 #[cfg(feature = "std")]
329 #[test]
330 fn invalid_nanos_rejected() {
331 let bad = Duration {
332 seconds: 1,
333 nanos: 1_000_000_000,
334 ..Default::default()
335 };
336 let result: Result<std::time::Duration, _> = bad.try_into();
337 assert_eq!(result, Err(DurationError::InvalidNanos));
338 }
339
340 #[test]
343 fn from_millis_positive() {
344 let d = Duration::from_millis(1_500);
345 assert_eq!(d.seconds, 1);
346 assert_eq!(d.nanos, 500_000_000);
347 }
348
349 #[test]
350 fn from_millis_negative() {
351 let d = Duration::from_millis(-1_500);
352 assert_eq!(d.seconds, -1);
353 assert_eq!(d.nanos, -500_000_000);
354 }
355
356 #[test]
357 fn from_millis_exact_seconds() {
358 let d = Duration::from_millis(2_000);
359 assert_eq!(d.seconds, 2);
360 assert_eq!(d.nanos, 0);
361 }
362
363 #[test]
364 fn from_micros_positive() {
365 let d = Duration::from_micros(1_500_000);
366 assert_eq!(d.seconds, 1);
367 assert_eq!(d.nanos, 500_000_000);
368 }
369
370 #[test]
371 fn from_micros_negative() {
372 let d = Duration::from_micros(-750);
373 assert_eq!(d.seconds, 0);
374 assert_eq!(d.nanos, -750_000);
375 }
376
377 #[test]
378 fn from_nanos_positive() {
379 let d = Duration::from_nanos(1_500_000_000);
380 assert_eq!(d.seconds, 1);
381 assert_eq!(d.nanos, 500_000_000);
382 }
383
384 #[test]
385 fn from_nanos_negative() {
386 let d = Duration::from_nanos(-2_000_000_000);
387 assert_eq!(d.seconds, -2);
388 assert_eq!(d.nanos, 0);
389 }
390
391 #[test]
392 fn from_nanos_sub_second() {
393 let d = Duration::from_nanos(999_999_999);
394 assert_eq!(d.seconds, 0);
395 assert_eq!(d.nanos, 999_999_999);
396 }
397
398 #[test]
399 fn from_millis_i64_min() {
400 let d = Duration::from_millis(i64::MIN);
404 assert_eq!(d.nanos, -808_000_000_i32);
405 }
406
407 #[test]
408 fn from_millis_i64_max() {
409 let d = Duration::from_millis(i64::MAX);
411 assert_eq!(d.nanos, 807_000_000_i32);
412 }
413
414 #[test]
415 fn from_micros_i64_min() {
416 let d = Duration::from_micros(i64::MIN);
419 assert_eq!(d.nanos, -775_808_000_i32);
420 }
421
422 #[test]
423 fn from_nanos_i64_min() {
424 let d = Duration::from_nanos(i64::MIN);
426 assert_eq!(d.nanos, -854_775_808_i32);
427 }
428
429 #[test]
430 fn from_nanos_i64_max() {
431 let d = Duration::from_nanos(i64::MAX);
433 assert_eq!(d.nanos, 854_775_807_i32);
434 }
435
436 #[cfg(feature = "std")]
439 #[test]
440 fn nanos_i32_min_is_invalid() {
441 let bad = Duration {
445 seconds: 0,
446 nanos: i32::MIN,
447 ..Default::default()
448 };
449 let result: Result<std::time::Duration, _> = bad.try_into();
450 assert_eq!(result, Err(DurationError::InvalidNanos));
451 }
452
453 #[cfg(feature = "std")]
454 #[test]
455 fn negative_seconds_and_negative_nanos_is_negative_duration() {
456 let neg = Duration {
459 seconds: -5,
460 nanos: -500_000_000,
461 ..Default::default()
462 };
463 let result: Result<std::time::Duration, _> = neg.try_into();
464 assert_eq!(result, Err(DurationError::NegativeDuration));
465 }
466
467 #[test]
470 fn from_secs_zero() {
471 let d = Duration::from_secs(0);
472 assert_eq!(d.seconds, 0);
473 assert_eq!(d.nanos, 0);
474 }
475
476 #[test]
477 fn from_secs_positive() {
478 let d = Duration::from_secs(300);
479 assert_eq!(d.seconds, 300);
480 assert_eq!(d.nanos, 0);
481 }
482
483 #[test]
484 fn from_secs_negative() {
485 let d = Duration::from_secs(-7);
486 assert_eq!(d.seconds, -7);
487 assert_eq!(d.nanos, 0);
488 }
489
490 #[test]
493 fn from_secs_nanos_checked_valid_positive() {
494 let d = Duration::from_secs_nanos_checked(1, 999_999_999).unwrap();
495 assert_eq!(d.seconds, 1);
496 assert_eq!(d.nanos, 999_999_999);
497 }
498
499 #[test]
500 fn from_secs_nanos_checked_valid_negative() {
501 let d = Duration::from_secs_nanos_checked(-1, -999_999_999).unwrap();
502 assert_eq!(d.seconds, -1);
503 assert_eq!(d.nanos, -999_999_999);
504 }
505
506 #[test]
507 fn from_secs_nanos_checked_nanos_out_of_range() {
508 assert!(Duration::from_secs_nanos_checked(1, 1_000_000_000).is_none());
509 }
510
511 #[test]
512 fn from_secs_nanos_checked_i32_min_nanos_is_none() {
513 assert!(Duration::from_secs_nanos_checked(0, i32::MIN).is_none());
515 }
516
517 #[test]
518 fn from_secs_nanos_checked_sign_mismatch_is_none() {
519 assert!(Duration::from_secs_nanos_checked(-1, 1).is_none());
520 assert!(Duration::from_secs_nanos_checked(1, -1).is_none());
521 }
522
523 #[test]
524 fn from_secs_nanos_checked_zero_seconds_allows_negative_nanos() {
525 let d = Duration::from_secs_nanos_checked(0, -500_000_000).unwrap();
527 assert_eq!(d.seconds, 0);
528 assert_eq!(d.nanos, -500_000_000);
529 }
530
531 #[test]
534 fn from_secs_nanos_valid() {
535 let d = Duration::from_secs_nanos(2, 500_000_000);
536 assert_eq!(d.seconds, 2);
537 assert_eq!(d.nanos, 500_000_000);
538 }
539
540 #[cfg(feature = "std")]
543 #[test]
544 fn large_std_duration_saturates_to_i64_max_seconds() {
545 let huge = std::time::Duration::from_secs(u64::MAX);
549 let proto: Duration = huge.into();
550 assert_eq!(proto.seconds, i64::MAX);
551 assert_eq!(proto.nanos, 0);
553 }
554
555 #[cfg(feature = "json")]
558 mod serde_tests {
559 use super::*;
560
561 #[test]
562 fn duration_zero_roundtrip() {
563 let d = Duration::from_secs(0);
564 let json = serde_json::to_string(&d).unwrap();
565 assert_eq!(json, r#""0s""#);
566 let back: Duration = serde_json::from_str(&json).unwrap();
567 assert_eq!(back.seconds, 0);
568 assert_eq!(back.nanos, 0);
569 }
570
571 #[test]
572 fn duration_positive_whole_seconds_roundtrip() {
573 let d = Duration::from_secs(300);
574 let json = serde_json::to_string(&d).unwrap();
575 assert_eq!(json, r#""300s""#);
576 let back: Duration = serde_json::from_str(&json).unwrap();
577 assert_eq!(back.seconds, 300);
578 assert_eq!(back.nanos, 0);
579 }
580
581 #[test]
582 fn duration_millis_precision_roundtrip() {
583 let d = Duration::from_secs_nanos(1, 500_000_000);
584 let json = serde_json::to_string(&d).unwrap();
585 assert_eq!(json, r#""1.500s""#);
586 let back: Duration = serde_json::from_str(&json).unwrap();
587 assert_eq!(back.seconds, 1);
588 assert_eq!(back.nanos, 500_000_000);
589 }
590
591 #[test]
592 fn duration_micros_precision_roundtrip() {
593 let d = Duration::from_secs_nanos(0, 1_000);
594 let json = serde_json::to_string(&d).unwrap();
595 assert_eq!(json, r#""0.000001s""#);
596 let back: Duration = serde_json::from_str(&json).unwrap();
597 assert_eq!(back.nanos, 1_000);
598 }
599
600 #[test]
601 fn duration_nanos_precision_roundtrip() {
602 let d = Duration::from_secs_nanos(0, 1);
603 let json = serde_json::to_string(&d).unwrap();
604 assert_eq!(json, r#""0.000000001s""#);
605 let back: Duration = serde_json::from_str(&json).unwrap();
606 assert_eq!(back.nanos, 1);
607 }
608
609 #[test]
610 fn duration_negative_roundtrip() {
611 let d = Duration::from_secs_nanos(-1, -500_000_000);
612 let json = serde_json::to_string(&d).unwrap();
613 assert_eq!(json, r#""-1.500s""#);
614 let back: Duration = serde_json::from_str(&json).unwrap();
615 assert_eq!(back.seconds, -1);
616 assert_eq!(back.nanos, -500_000_000);
617 }
618
619 #[test]
620 fn duration_invalid_string_is_error() {
621 let result: Result<Duration, _> = serde_json::from_str(r#""1.5""#); assert!(result.is_err());
623 }
624
625 #[test]
626 fn parse_duration_rejects_double_sign() {
627 assert_eq!(parse_duration_string("--5s"), None);
630 assert_eq!(parse_duration_string("-+5s"), None);
631 assert_eq!(parse_duration_string("+5s"), None); assert_eq!(parse_duration_string("--5.5s"), None);
635 assert_eq!(parse_duration_string("-5s"), Some((-5, 0)));
637 }
638
639 #[test]
640 fn parse_duration_rejects_non_digit_fractional() {
641 assert_eq!(parse_duration_string("5.-3s"), None, "minus in frac");
645 assert_eq!(parse_duration_string("5.+3s"), None, "plus in frac");
646 assert_eq!(parse_duration_string("-5.-3s"), None, "double neg frac");
647 assert_eq!(parse_duration_string("5.3as"), None, "alpha in frac");
648 assert_eq!(parse_duration_string("5. s"), None, "space in frac");
649 assert_eq!(parse_duration_string("5.3s"), Some((5, 300_000_000)));
651 assert_eq!(parse_duration_string("-5.3s"), Some((-5, -300_000_000)));
652 }
653 }
654
655 #[cfg(feature = "std")]
656 #[test]
657 fn negative_nanos_on_positive_seconds_is_invalid_nanos() {
658 let bad = Duration {
661 seconds: 5,
662 nanos: -1,
663 ..Default::default()
664 };
665 let result: Result<std::time::Duration, _> = bad.try_into();
666 assert_eq!(result, Err(DurationError::InvalidNanos));
667 }
668}