1use std::{
2 fmt,
3 time::{Duration, SystemTime, UNIX_EPOCH},
4};
5use time::{OffsetDateTime, UtcDateTime, UtcOffset, format_description::well_known::Rfc3339};
6
7#[derive(Clone, Copy, Eq, Hash, PartialEq)]
12pub struct Date {
13 inner: SystemTime,
14}
15
16#[derive(Debug)]
18#[non_exhaustive]
19pub struct InvalidXmlDate;
20
21#[derive(Debug, PartialEq)]
22pub(crate) struct OverflowOrNanDate;
23
24impl Date {
25 const PLIST_EPOCH_UNIX_TIMESTAMP: Duration = Duration::from_secs(978_307_200);
27
28 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 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 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 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}