Skip to main content

plist/
date.rs

1use std::{
2    fmt,
3    time::{Duration, SystemTime, UNIX_EPOCH},
4};
5use time::{OffsetDateTime, UtcDateTime, UtcOffset, format_description::well_known::Rfc3339};
6
7/// A UTC timestamp used for serialization to and from the plist date type.
8///
9/// Note that while this type implements `Serialize` and `Deserialize` it will behave strangely if
10/// used with serializers from outside this crate.
11#[derive(Clone, Copy, Eq, Hash, PartialEq)]
12pub struct Date {
13    inner: SystemTime,
14}
15
16/// An error indicating that a string was not a valid XML plist date.
17#[derive(Debug)]
18#[non_exhaustive]
19pub struct InvalidXmlDate;
20
21#[derive(Debug, PartialEq)]
22pub(crate) struct OverflowOrNanDate;
23
24impl Date {
25    /// The unix timestamp of the plist epoch.
26    const PLIST_EPOCH_UNIX_TIMESTAMP: Duration = Duration::from_secs(978_307_200);
27
28    /// Converts an XML plist date string to a `Date`.
29    pub fn from_xml_format(date: &str) -> Result<Self, InvalidXmlDate> {
30        let offset: OffsetDateTime = OffsetDateTime::parse(date, &Rfc3339)
31            .map_err(|_| InvalidXmlDate)?
32            .to_offset(UtcOffset::UTC);
33        Ok(Date {
34            inner: offset.into(),
35        })
36    }
37
38    /// Converts the `Date` to an XML plist date string.
39    pub fn to_xml_format(&self) -> String {
40        let datetime = Date::to_utc_date_time(self.inner)
41            .expect("all constructors verify that date can be represented as a `UtcDateTime`");
42        datetime.format(&Rfc3339).unwrap()
43    }
44
45    pub(crate) fn from_seconds_since_plist_epoch(
46        timestamp: f64,
47    ) -> Result<Date, OverflowOrNanDate> {
48        let Ok(dur_since_plist_epoch) = Duration::try_from_secs_f64(timestamp.abs()) else {
49            return Err(OverflowOrNanDate);
50        };
51
52        // `timestamp` is the number of seconds since the plist epoch of 1/1/2001 00:00:00.
53        let plist_epoch = UNIX_EPOCH + Date::PLIST_EPOCH_UNIX_TIMESTAMP;
54        let is_negative = timestamp < 0.0;
55
56        let inner = if is_negative {
57            plist_epoch.checked_sub(dur_since_plist_epoch)
58        } else {
59            plist_epoch.checked_add(dur_since_plist_epoch)
60        };
61
62        let inner = inner.ok_or(OverflowOrNanDate)?;
63
64        // `time` which we use for parsing and printing dates supports a smaller date range than
65        // `SystemTime`.
66        if Date::to_utc_date_time(inner).is_none() {
67            return Err(OverflowOrNanDate);
68        }
69
70        Ok(Date { inner })
71    }
72
73    pub(crate) fn as_seconds_since_plist_epoch(&self) -> f64 {
74        let plist_epoch = UNIX_EPOCH + Date::PLIST_EPOCH_UNIX_TIMESTAMP;
75        match self.inner.duration_since(plist_epoch) {
76            Ok(duration) => duration.as_secs_f64(),
77            Err(err) => -err.duration().as_secs_f64(),
78        }
79    }
80
81    fn to_utc_date_time(date: SystemTime) -> Option<UtcDateTime> {
82        match date.duration_since(UNIX_EPOCH) {
83            Ok(duration) => UtcDateTime::UNIX_EPOCH.checked_add(duration.try_into().ok()?),
84            Err(err) => UtcDateTime::UNIX_EPOCH.checked_sub(err.duration().try_into().ok()?),
85        }
86    }
87}
88
89impl fmt::Debug for Date {
90    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
91        write!(f, "{}", self.to_xml_format())
92    }
93}
94
95impl From<SystemTime> for Date {
96    fn from(date: SystemTime) -> Self {
97        Date { inner: date }
98    }
99}
100
101impl From<Date> for SystemTime {
102    fn from(val: Date) -> Self {
103        val.inner
104    }
105}
106
107impl fmt::Display for InvalidXmlDate {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        f.write_str("String was not a valid XML plist date")
110    }
111}
112
113impl std::error::Error for InvalidXmlDate {}
114
115#[cfg(feature = "serde")]
116pub mod serde_impls {
117    use serde::{
118        de::{Deserialize, Deserializer, Error, Unexpected, Visitor},
119        ser::{Serialize, Serializer},
120    };
121    use std::fmt;
122
123    use crate::Date;
124
125    pub const DATE_NEWTYPE_STRUCT_NAME: &str = "PLIST-DATE";
126
127    impl Serialize for Date {
128        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
129        where
130            S: Serializer,
131        {
132            let date_str = self.to_xml_format();
133            serializer.serialize_newtype_struct(DATE_NEWTYPE_STRUCT_NAME, &date_str)
134        }
135    }
136
137    struct DateNewtypeVisitor;
138
139    impl<'de> Visitor<'de> for DateNewtypeVisitor {
140        type Value = Date;
141
142        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
143            formatter.write_str("a plist date newtype")
144        }
145
146        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
147        where
148            E: Error,
149        {
150            DateStrVisitor.visit_str(v)
151        }
152
153        fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
154        where
155            D: Deserializer<'de>,
156        {
157            deserializer.deserialize_str(DateStrVisitor)
158        }
159    }
160
161    struct DateStrVisitor;
162
163    impl Visitor<'_> for DateStrVisitor {
164        type Value = Date;
165
166        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
167            formatter.write_str("a plist date string")
168        }
169
170        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
171        where
172            E: Error,
173        {
174            Date::from_xml_format(v).map_err(|_| E::invalid_value(Unexpected::Str(v), &self))
175        }
176    }
177
178    impl<'de> Deserialize<'de> for Date {
179        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
180        where
181            D: Deserializer<'de>,
182        {
183            deserializer.deserialize_newtype_struct(DATE_NEWTYPE_STRUCT_NAME, DateNewtypeVisitor)
184        }
185    }
186}
187
188#[cfg(test)]
189mod testing {
190    use super::*;
191
192    #[test]
193    fn date_roundtrip() {
194        let date_str = "1981-05-16T11:32:06Z";
195
196        let date = Date::from_xml_format(date_str).expect("should parse");
197
198        let generated_str = date.to_xml_format();
199
200        assert_eq!(date_str, generated_str);
201    }
202
203    #[test]
204    fn far_past_date() {
205        let date_str = "1920-01-01T00:00:00Z";
206        Date::from_xml_format(date_str).expect("should parse");
207    }
208
209    #[test]
210    fn overflowing_binary_dates_dont_panic() {
211        assert_eq!(
212            Date::from_seconds_since_plist_epoch(f64::INFINITY),
213            Err(OverflowOrNanDate)
214        );
215
216        assert_eq!(
217            Date::from_seconds_since_plist_epoch(f64::NEG_INFINITY),
218            Err(OverflowOrNanDate)
219        );
220
221        assert_eq!(
222            Date::from_seconds_since_plist_epoch(f64::MAX),
223            Err(OverflowOrNanDate)
224        );
225
226        assert_eq!(
227            Date::from_seconds_since_plist_epoch(f64::MIN),
228            Err(OverflowOrNanDate)
229        );
230
231        assert_eq!(
232            Date::from_seconds_since_plist_epoch(1e12),
233            Err(OverflowOrNanDate)
234        );
235
236        assert_eq!(
237            Date::from_seconds_since_plist_epoch(-1e12),
238            Err(OverflowOrNanDate)
239        );
240    }
241}