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(Self::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 Self {
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")]
85fn duration_to_string(secs: i64, nanos: i32) -> alloc::string::String {
86 buffa::json_helpers::wkt::fmt_duration(secs, nanos)
89 .expect("Duration validated before formatting")
90}
91
92#[cfg(feature = "json")]
93fn parse_duration_string(s: &str) -> Option<(i64, i32)> {
94 buffa::json_helpers::wkt::parse_duration(s).ok()
95}
96
97#[cfg(feature = "json")]
98fn is_valid_duration(secs: i64, nanos: i32) -> bool {
99 buffa::json_helpers::wkt::validate_duration(secs, nanos).is_ok()
100}
101
102#[cfg(feature = "json")]
105impl serde::Serialize for Duration {
106 fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
113 use alloc::format;
114 if !is_valid_duration(self.seconds, self.nanos) {
115 return Err(serde::ser::Error::custom(format!(
116 "invalid Duration: seconds={}, nanos={} is out of range",
117 self.seconds, self.nanos
118 )));
119 }
120 s.serialize_str(&duration_to_string(self.seconds, self.nanos))
121 }
122}
123
124#[cfg(feature = "json")]
125impl<'de> serde::Deserialize<'de> for Duration {
126 fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
128 use alloc::{format, string::String};
129 let s: String = serde::Deserialize::deserialize(d)?;
130 let (seconds, nanos) = parse_duration_string(&s)
131 .ok_or_else(|| serde::de::Error::custom(format!("invalid Duration string: {s}")))?;
132 Ok(Self {
133 seconds,
134 nanos,
135 ..Default::default()
136 })
137 }
138}
139
140impl Duration {
141 pub fn from_secs(seconds: i64) -> Self {
143 Self {
144 seconds,
145 nanos: 0,
146 ..Default::default()
147 }
148 }
149
150 pub fn from_secs_nanos(seconds: i64, nanos: i32) -> Self {
160 debug_assert!(
162 (-999_999_999..=999_999_999).contains(&nanos),
163 "nanos ({nanos}) must be in [-999_999_999, 999_999_999]"
164 );
165 debug_assert!(
166 !((seconds > 0 && nanos < 0) || (seconds < 0 && nanos > 0)),
167 "nanos sign must be consistent with seconds sign"
168 );
169 Self {
170 seconds,
171 nanos,
172 ..Default::default()
173 }
174 }
175
176 pub fn from_secs_nanos_checked(seconds: i64, nanos: i32) -> Option<Self> {
179 if !(-999_999_999..=999_999_999).contains(&nanos) {
181 return None;
182 }
183 if (seconds > 0 && nanos < 0) || (seconds < 0 && nanos > 0) {
184 return None;
185 }
186 Some(Self {
187 seconds,
188 nanos,
189 ..Default::default()
190 })
191 }
192
193 pub fn from_millis(millis: i64) -> Self {
198 Self {
199 seconds: millis / 1_000,
200 nanos: ((millis % 1_000) * 1_000_000) as i32,
203 ..Default::default()
204 }
205 }
206
207 pub fn from_micros(micros: i64) -> Self {
209 Self {
210 seconds: micros / 1_000_000,
211 nanos: ((micros % 1_000_000) * 1_000) as i32,
214 ..Default::default()
215 }
216 }
217
218 pub fn from_nanos(nanos: i64) -> Self {
220 Self {
221 seconds: nanos / 1_000_000_000,
222 nanos: (nanos % 1_000_000_000) as i32,
224 ..Default::default()
225 }
226 }
227}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[cfg(feature = "std")]
234 #[test]
235 fn std_duration_roundtrip() {
236 let d = std::time::Duration::new(300, 500_000_000);
237 let proto: Duration = d.into();
238 assert_eq!(proto.seconds, 300);
239 assert_eq!(proto.nanos, 500_000_000);
240 let back: std::time::Duration = proto.try_into().unwrap();
241 assert_eq!(back, d);
242 }
243
244 #[cfg(feature = "std")]
245 #[test]
246 fn zero_duration_roundtrip() {
247 let d = std::time::Duration::ZERO;
248 let proto: Duration = d.into();
249 let back: std::time::Duration = proto.try_into().unwrap();
250 assert_eq!(back, d);
251 }
252
253 #[cfg(feature = "std")]
254 #[test]
255 fn negative_duration_rejected() {
256 let neg = Duration {
257 seconds: -5,
258 nanos: 0,
259 ..Default::default()
260 };
261 let result: Result<std::time::Duration, _> = neg.try_into();
262 assert_eq!(result, Err(DurationError::NegativeDuration));
263 }
264
265 #[cfg(feature = "std")]
266 #[test]
267 fn invalid_nanos_rejected() {
268 let bad = Duration {
269 seconds: 1,
270 nanos: 1_000_000_000,
271 ..Default::default()
272 };
273 let result: Result<std::time::Duration, _> = bad.try_into();
274 assert_eq!(result, Err(DurationError::InvalidNanos));
275 }
276
277 #[test]
280 fn from_millis_positive() {
281 let d = Duration::from_millis(1_500);
282 assert_eq!(d.seconds, 1);
283 assert_eq!(d.nanos, 500_000_000);
284 }
285
286 #[test]
287 fn from_millis_negative() {
288 let d = Duration::from_millis(-1_500);
289 assert_eq!(d.seconds, -1);
290 assert_eq!(d.nanos, -500_000_000);
291 }
292
293 #[test]
294 fn from_millis_exact_seconds() {
295 let d = Duration::from_millis(2_000);
296 assert_eq!(d.seconds, 2);
297 assert_eq!(d.nanos, 0);
298 }
299
300 #[test]
301 fn from_micros_positive() {
302 let d = Duration::from_micros(1_500_000);
303 assert_eq!(d.seconds, 1);
304 assert_eq!(d.nanos, 500_000_000);
305 }
306
307 #[test]
308 fn from_micros_negative() {
309 let d = Duration::from_micros(-750);
310 assert_eq!(d.seconds, 0);
311 assert_eq!(d.nanos, -750_000);
312 }
313
314 #[test]
315 fn from_nanos_positive() {
316 let d = Duration::from_nanos(1_500_000_000);
317 assert_eq!(d.seconds, 1);
318 assert_eq!(d.nanos, 500_000_000);
319 }
320
321 #[test]
322 fn from_nanos_negative() {
323 let d = Duration::from_nanos(-2_000_000_000);
324 assert_eq!(d.seconds, -2);
325 assert_eq!(d.nanos, 0);
326 }
327
328 #[test]
329 fn from_nanos_sub_second() {
330 let d = Duration::from_nanos(999_999_999);
331 assert_eq!(d.seconds, 0);
332 assert_eq!(d.nanos, 999_999_999);
333 }
334
335 #[test]
336 fn from_millis_i64_min() {
337 let d = Duration::from_millis(i64::MIN);
341 assert_eq!(d.nanos, -808_000_000_i32);
342 }
343
344 #[test]
345 fn from_millis_i64_max() {
346 let d = Duration::from_millis(i64::MAX);
348 assert_eq!(d.nanos, 807_000_000_i32);
349 }
350
351 #[test]
352 fn from_micros_i64_min() {
353 let d = Duration::from_micros(i64::MIN);
356 assert_eq!(d.nanos, -775_808_000_i32);
357 }
358
359 #[test]
360 fn from_nanos_i64_min() {
361 let d = Duration::from_nanos(i64::MIN);
363 assert_eq!(d.nanos, -854_775_808_i32);
364 }
365
366 #[test]
367 fn from_nanos_i64_max() {
368 let d = Duration::from_nanos(i64::MAX);
370 assert_eq!(d.nanos, 854_775_807_i32);
371 }
372
373 #[cfg(feature = "std")]
376 #[test]
377 fn nanos_i32_min_is_invalid() {
378 let bad = Duration {
382 seconds: 0,
383 nanos: i32::MIN,
384 ..Default::default()
385 };
386 let result: Result<std::time::Duration, _> = bad.try_into();
387 assert_eq!(result, Err(DurationError::InvalidNanos));
388 }
389
390 #[cfg(feature = "std")]
391 #[test]
392 fn negative_seconds_and_negative_nanos_is_negative_duration() {
393 let neg = Duration {
396 seconds: -5,
397 nanos: -500_000_000,
398 ..Default::default()
399 };
400 let result: Result<std::time::Duration, _> = neg.try_into();
401 assert_eq!(result, Err(DurationError::NegativeDuration));
402 }
403
404 #[test]
407 fn from_secs_zero() {
408 let d = Duration::from_secs(0);
409 assert_eq!(d.seconds, 0);
410 assert_eq!(d.nanos, 0);
411 }
412
413 #[test]
414 fn from_secs_positive() {
415 let d = Duration::from_secs(300);
416 assert_eq!(d.seconds, 300);
417 assert_eq!(d.nanos, 0);
418 }
419
420 #[test]
421 fn from_secs_negative() {
422 let d = Duration::from_secs(-7);
423 assert_eq!(d.seconds, -7);
424 assert_eq!(d.nanos, 0);
425 }
426
427 #[test]
430 fn from_secs_nanos_checked_valid_positive() {
431 let d = Duration::from_secs_nanos_checked(1, 999_999_999).unwrap();
432 assert_eq!(d.seconds, 1);
433 assert_eq!(d.nanos, 999_999_999);
434 }
435
436 #[test]
437 fn from_secs_nanos_checked_valid_negative() {
438 let d = Duration::from_secs_nanos_checked(-1, -999_999_999).unwrap();
439 assert_eq!(d.seconds, -1);
440 assert_eq!(d.nanos, -999_999_999);
441 }
442
443 #[test]
444 fn from_secs_nanos_checked_nanos_out_of_range() {
445 assert!(Duration::from_secs_nanos_checked(1, 1_000_000_000).is_none());
446 }
447
448 #[test]
449 fn from_secs_nanos_checked_i32_min_nanos_is_none() {
450 assert!(Duration::from_secs_nanos_checked(0, i32::MIN).is_none());
452 }
453
454 #[test]
455 fn from_secs_nanos_checked_sign_mismatch_is_none() {
456 assert!(Duration::from_secs_nanos_checked(-1, 1).is_none());
457 assert!(Duration::from_secs_nanos_checked(1, -1).is_none());
458 }
459
460 #[test]
461 fn from_secs_nanos_checked_zero_seconds_allows_negative_nanos() {
462 let d = Duration::from_secs_nanos_checked(0, -500_000_000).unwrap();
464 assert_eq!(d.seconds, 0);
465 assert_eq!(d.nanos, -500_000_000);
466 }
467
468 #[test]
471 fn from_secs_nanos_valid() {
472 let d = Duration::from_secs_nanos(2, 500_000_000);
473 assert_eq!(d.seconds, 2);
474 assert_eq!(d.nanos, 500_000_000);
475 }
476
477 #[cfg(feature = "std")]
480 #[test]
481 fn large_std_duration_saturates_to_i64_max_seconds() {
482 let huge = std::time::Duration::from_secs(u64::MAX);
486 let proto: Duration = huge.into();
487 assert_eq!(proto.seconds, i64::MAX);
488 assert_eq!(proto.nanos, 0);
490 }
491
492 #[cfg(feature = "json")]
495 mod serde_tests {
496 use super::*;
497
498 #[test]
499 fn duration_zero_roundtrip() {
500 let d = Duration::from_secs(0);
501 let json = serde_json::to_string(&d).unwrap();
502 assert_eq!(json, r#""0s""#);
503 let back: Duration = serde_json::from_str(&json).unwrap();
504 assert_eq!(back.seconds, 0);
505 assert_eq!(back.nanos, 0);
506 }
507
508 #[test]
509 fn duration_positive_whole_seconds_roundtrip() {
510 let d = Duration::from_secs(300);
511 let json = serde_json::to_string(&d).unwrap();
512 assert_eq!(json, r#""300s""#);
513 let back: Duration = serde_json::from_str(&json).unwrap();
514 assert_eq!(back.seconds, 300);
515 assert_eq!(back.nanos, 0);
516 }
517
518 #[test]
519 fn duration_millis_precision_roundtrip() {
520 let d = Duration::from_secs_nanos(1, 500_000_000);
521 let json = serde_json::to_string(&d).unwrap();
522 assert_eq!(json, r#""1.500s""#);
523 let back: Duration = serde_json::from_str(&json).unwrap();
524 assert_eq!(back.seconds, 1);
525 assert_eq!(back.nanos, 500_000_000);
526 }
527
528 #[test]
529 fn duration_micros_precision_roundtrip() {
530 let d = Duration::from_secs_nanos(0, 1_000);
531 let json = serde_json::to_string(&d).unwrap();
532 assert_eq!(json, r#""0.000001s""#);
533 let back: Duration = serde_json::from_str(&json).unwrap();
534 assert_eq!(back.nanos, 1_000);
535 }
536
537 #[test]
538 fn duration_nanos_precision_roundtrip() {
539 let d = Duration::from_secs_nanos(0, 1);
540 let json = serde_json::to_string(&d).unwrap();
541 assert_eq!(json, r#""0.000000001s""#);
542 let back: Duration = serde_json::from_str(&json).unwrap();
543 assert_eq!(back.nanos, 1);
544 }
545
546 #[test]
547 fn duration_negative_roundtrip() {
548 let d = Duration::from_secs_nanos(-1, -500_000_000);
549 let json = serde_json::to_string(&d).unwrap();
550 assert_eq!(json, r#""-1.500s""#);
551 let back: Duration = serde_json::from_str(&json).unwrap();
552 assert_eq!(back.seconds, -1);
553 assert_eq!(back.nanos, -500_000_000);
554 }
555
556 #[test]
557 fn duration_invalid_string_is_error() {
558 let result: Result<Duration, _> = serde_json::from_str(r#""1.5""#); assert!(result.is_err());
560 }
561
562 #[test]
563 fn parse_duration_rejects_double_sign() {
564 assert_eq!(parse_duration_string("--5s"), None);
567 assert_eq!(parse_duration_string("-+5s"), None);
568 assert_eq!(parse_duration_string("+5s"), None); assert_eq!(parse_duration_string("--5.5s"), None);
572 assert_eq!(parse_duration_string("-5s"), Some((-5, 0)));
574 }
575
576 #[test]
577 fn parse_duration_rejects_non_digit_fractional() {
578 assert_eq!(parse_duration_string("5.-3s"), None, "minus in frac");
582 assert_eq!(parse_duration_string("5.+3s"), None, "plus in frac");
583 assert_eq!(parse_duration_string("-5.-3s"), None, "double neg frac");
584 assert_eq!(parse_duration_string("5.3as"), None, "alpha in frac");
585 assert_eq!(parse_duration_string("5. s"), None, "space in frac");
586 assert_eq!(parse_duration_string("5.3s"), Some((5, 300_000_000)));
588 assert_eq!(parse_duration_string("-5.3s"), Some((-5, -300_000_000)));
589 }
590 }
591
592 #[cfg(feature = "std")]
593 #[test]
594 fn negative_nanos_on_positive_seconds_is_invalid_nanos() {
595 let bad = Duration {
598 seconds: 5,
599 nanos: -1,
600 ..Default::default()
601 };
602 let result: Result<std::time::Duration, _> = bad.try_into();
603 assert_eq!(result, Err(DurationError::InvalidNanos));
604 }
605}