Skip to content

Commit e5cc951

Browse files
authored
fix(prost-types): Converting DateTime to Timestamp is fallible (#1095)
The converstion from the private type DateTime to public type Timestamp is fallible when the DateTime is invalid. All code paths that used `DateTime::into::<Timestamp>()` first check whether the DateTime is valid and then do the conversion, therefore this problem was not visible. #893 points out that some conversions panic, however all these DateTime are invalid. Solution: Replace `impl From<DateTime> for Timestamp` with `TryFrom` and remove the manual `is_valid` checks before the conversion. I added the test cases from the issue and roundtrip test between DateTime and Timestamp.
1 parent 23d9fd7 commit e5cc951

File tree

2 files changed

+150
-18
lines changed

2 files changed

+150
-18
lines changed

prost-types/src/datetime.rs

Lines changed: 149 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ use core::fmt;
55

66
use crate::Duration;
77
use crate::Timestamp;
8+
use crate::TimestampError;
89

910
/// A point in time, represented as a date and time in the UTC timezone.
1011
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
@@ -502,9 +503,7 @@ pub(crate) fn parse_timestamp(s: &str) -> Option<Timestamp> {
502503
..DateTime::default()
503504
};
504505

505-
ensure!(date_time.is_valid());
506-
507-
return Some(Timestamp::from(date_time));
506+
return Timestamp::try_from(date_time).ok();
508507
}
509508

510509
// Accept either 'T' or ' ' as delimiter between date and time.
@@ -534,9 +533,7 @@ pub(crate) fn parse_timestamp(s: &str) -> Option<Timestamp> {
534533
nanos,
535534
};
536535

537-
ensure!(date_time.is_valid());
538-
539-
let Timestamp { seconds, nanos } = Timestamp::from(date_time);
536+
let Timestamp { seconds, nanos } = Timestamp::try_from(date_time).ok()?;
540537

541538
let seconds =
542539
seconds.checked_sub(i64::from(offset_hour) * 3600 + i64::from(offset_minute) * 60)?;
@@ -575,21 +572,27 @@ pub(crate) fn parse_duration(s: &str) -> Option<Duration> {
575572
Some(Duration { seconds, nanos })
576573
}
577574

578-
impl From<DateTime> for Timestamp {
579-
fn from(date_time: DateTime) -> Timestamp {
575+
impl TryFrom<DateTime> for Timestamp {
576+
type Error = TimestampError;
577+
578+
fn try_from(date_time: DateTime) -> Result<Timestamp, TimestampError> {
579+
if !date_time.is_valid() {
580+
return Err(TimestampError::InvalidDateTime);
581+
}
580582
let seconds = date_time_to_seconds(&date_time);
581583
let nanos = date_time.nanos;
582-
Timestamp {
584+
Ok(Timestamp {
583585
seconds,
584586
nanos: nanos as i32,
585-
}
587+
})
586588
}
587589
}
588590

589591
#[cfg(test)]
590592
mod tests {
591593
use super::*;
592594
use proptest::prelude::*;
595+
use prost::alloc::format;
593596

594597
#[test]
595598
fn test_min_max() {
@@ -734,16 +737,16 @@ mod tests {
734737

735738
// Leap day
736739
assert_eq!(
737-
"2020-02-29T01:02:03.00Z".parse::<Timestamp>().unwrap(),
738-
Timestamp::from(DateTime {
740+
"2020-02-29T01:02:03.00Z".parse::<Timestamp>(),
741+
Timestamp::try_from(DateTime {
739742
year: 2020,
740743
month: 2,
741744
day: 29,
742745
hour: 1,
743746
minute: 2,
744747
second: 3,
745748
nanos: 0,
746-
}),
749+
})
747750
);
748751

749752
// Test extensions to RFC 3339.
@@ -852,6 +855,94 @@ mod tests {
852855
assert!("1️⃣s".parse::<Duration>().is_err());
853856
}
854857

858+
#[test]
859+
fn check_invalid_datetimes() {
860+
assert_eq!(
861+
Timestamp::try_from(DateTime {
862+
year: i64::from_le_bytes([178, 2, 0, 0, 0, 0, 0, 128]),
863+
month: 2,
864+
day: 2,
865+
hour: 8,
866+
minute: 58,
867+
second: 8,
868+
nanos: u32::from_le_bytes([0, 0, 0, 50]),
869+
}),
870+
Err(TimestampError::InvalidDateTime)
871+
);
872+
assert_eq!(
873+
Timestamp::try_from(DateTime {
874+
year: i64::from_le_bytes([132, 7, 0, 0, 0, 0, 0, 128]),
875+
month: 2,
876+
day: 2,
877+
hour: 8,
878+
minute: 58,
879+
second: 8,
880+
nanos: u32::from_le_bytes([0, 0, 0, 50]),
881+
}),
882+
Err(TimestampError::InvalidDateTime)
883+
);
884+
assert_eq!(
885+
Timestamp::try_from(DateTime {
886+
year: i64::from_le_bytes([80, 96, 32, 240, 99, 0, 32, 180]),
887+
month: 1,
888+
day: 18,
889+
hour: 19,
890+
minute: 26,
891+
second: 8,
892+
nanos: u32::from_le_bytes([0, 0, 0, 50]),
893+
}),
894+
Err(TimestampError::InvalidDateTime)
895+
);
896+
assert_eq!(
897+
Timestamp::try_from(DateTime {
898+
year: DateTime::MIN.year - 1,
899+
month: 0,
900+
day: 0,
901+
hour: 0,
902+
minute: 0,
903+
second: 0,
904+
nanos: 0,
905+
}),
906+
Err(TimestampError::InvalidDateTime)
907+
);
908+
assert_eq!(
909+
Timestamp::try_from(DateTime {
910+
year: i64::MIN,
911+
month: 0,
912+
day: 0,
913+
hour: 0,
914+
minute: 0,
915+
second: 0,
916+
nanos: 0,
917+
}),
918+
Err(TimestampError::InvalidDateTime)
919+
);
920+
assert_eq!(
921+
Timestamp::try_from(DateTime {
922+
year: DateTime::MAX.year + 1,
923+
month: u8::MAX,
924+
day: u8::MAX,
925+
hour: u8::MAX,
926+
minute: u8::MAX,
927+
second: u8::MAX,
928+
nanos: u32::MAX,
929+
}),
930+
Err(TimestampError::InvalidDateTime)
931+
);
932+
assert_eq!(
933+
Timestamp::try_from(DateTime {
934+
year: i64::MAX,
935+
month: u8::MAX,
936+
day: u8::MAX,
937+
hour: u8::MAX,
938+
minute: u8::MAX,
939+
second: u8::MAX,
940+
nanos: u32::MAX,
941+
}),
942+
Err(TimestampError::InvalidDateTime)
943+
);
944+
}
945+
855946
proptest! {
856947
#[cfg(feature = "std")]
857948
#[test]
@@ -883,5 +974,50 @@ mod tests {
883974
"{}", duration.to_string()
884975
);
885976
}
977+
978+
#[test]
979+
fn check_timestamp_roundtrip_with_date_time(
980+
seconds in i64::arbitrary(),
981+
nanos in i32::arbitrary(),
982+
) {
983+
let timestamp = Timestamp { seconds, nanos };
984+
let date_time = DateTime::from(timestamp);
985+
let roundtrip = Timestamp::try_from(date_time).unwrap();
986+
987+
let mut normalized_timestamp = timestamp;
988+
normalized_timestamp.normalize();
989+
990+
prop_assert_eq!(normalized_timestamp, roundtrip);
991+
}
992+
993+
#[test]
994+
fn check_date_time_roundtrip_with_timestamp(
995+
year in i64::arbitrary(),
996+
month in u8::arbitrary(),
997+
day in u8::arbitrary(),
998+
hour in u8::arbitrary(),
999+
minute in u8::arbitrary(),
1000+
second in u8::arbitrary(),
1001+
nanos in u32::arbitrary(),
1002+
) {
1003+
let date_time = DateTime {
1004+
year,
1005+
month,
1006+
day,
1007+
hour,
1008+
minute,
1009+
second,
1010+
nanos
1011+
};
1012+
1013+
if date_time.is_valid() {
1014+
let timestamp = Timestamp::try_from(date_time).unwrap();
1015+
let roundtrip = DateTime::from(timestamp);
1016+
1017+
prop_assert_eq!(date_time, roundtrip);
1018+
} else {
1019+
prop_assert_eq!(Timestamp::try_from(date_time), Err(TimestampError::InvalidDateTime));
1020+
}
1021+
}
8861022
}
8871023
}

prost-types/src/timestamp.rs

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -99,11 +99,7 @@ impl Timestamp {
9999
nanos,
100100
};
101101

102-
if date_time.is_valid() {
103-
Ok(Timestamp::from(date_time))
104-
} else {
105-
Err(TimestampError::InvalidDateTime)
106-
}
102+
Timestamp::try_from(date_time)
107103
}
108104
}
109105

0 commit comments

Comments
 (0)