plist/
date.rs

1use std::{
2    fmt,
3    time::{Duration, SystemTime, UNIX_EPOCH},
4};
5use time::{format_description::well_known::Rfc3339, OffsetDateTime, UtcOffset};
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
21pub(crate) struct InfiniteOrNanDate;
22
23impl Date {
24    /// The unix timestamp of the plist epoch.
25    const PLIST_EPOCH_UNIX_TIMESTAMP: Duration = Duration::from_secs(978_307_200);
26
27    /// Converts an XML plist date string to a `Date`.
28    pub fn from_xml_format(date: &str) -> Result<Self, InvalidXmlDate> {
29        let offset: OffsetDateTime = OffsetDateTime::parse(date, &Rfc3339)
30            .map_err(|_| InvalidXmlDate)?
31            .to_offset(UtcOffset::UTC);
32        Ok(Date {
33            inner: offset.into(),
34        })
35    }
36
37    /// Converts the `Date` to an XML plist date string.
38    pub fn to_xml_format(&self) -> String {
39        let datetime: OffsetDateTime = self.inner.into();
40        datetime.format(&Rfc3339).unwrap()
41    }
42
43    pub(crate) fn from_seconds_since_plist_epoch(
44        timestamp: f64,
45    ) -> Result<Date, InfiniteOrNanDate> {
46        // `timestamp` is the number of seconds since the plist epoch of 1/1/2001 00:00:00.
47        let plist_epoch = UNIX_EPOCH + Date::PLIST_EPOCH_UNIX_TIMESTAMP;
48
49        if !timestamp.is_finite() {
50            return Err(InfiniteOrNanDate);
51        }
52
53        let is_negative = timestamp < 0.0;
54        let timestamp = timestamp.abs();
55        let seconds = timestamp.floor() as u64;
56        let subsec_nanos = (timestamp.fract() * 1e9) as u32;
57
58        let dur_since_plist_epoch = Duration::new(seconds, subsec_nanos);
59
60        let inner = if is_negative {
61            plist_epoch.checked_sub(dur_since_plist_epoch)
62        } else {
63            plist_epoch.checked_add(dur_since_plist_epoch)
64        };
65
66        let inner = inner.ok_or(InfiniteOrNanDate)?;
67
68        Ok(Date { inner })
69    }
70
71    pub(crate) fn as_seconds_since_plist_epoch(&self) -> f64 {
72        // needed until #![feature(duration_float)] is stabilized
73        fn as_secs_f64(d: Duration) -> f64 {
74            const NANOS_PER_SEC: f64 = 1_000_000_000.00;
75            (d.as_secs() as f64) + f64::from(d.subsec_nanos()) / NANOS_PER_SEC
76        }
77
78        let plist_epoch = UNIX_EPOCH + Date::PLIST_EPOCH_UNIX_TIMESTAMP;
79        match self.inner.duration_since(plist_epoch) {
80            Ok(dur_since_plist_epoch) => as_secs_f64(dur_since_plist_epoch),
81            Err(err) => -as_secs_f64(err.duration()),
82        }
83    }
84}
85
86impl fmt::Debug for Date {
87    fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
88        write!(f, "{}", self.to_xml_format())
89    }
90}
91
92impl From<SystemTime> for Date {
93    fn from(date: SystemTime) -> Self {
94        Date { inner: date }
95    }
96}
97
98impl From<Date> for SystemTime {
99    fn from(val: Date) -> Self {
100        val.inner
101    }
102}
103
104impl fmt::Display for InvalidXmlDate {
105    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
106        f.write_str("String was not a valid XML plist date")
107    }
108}
109
110impl std::error::Error for InvalidXmlDate {}
111
112#[cfg(feature = "serde")]
113pub mod serde_impls {
114    use serde::{
115        de::{Deserialize, Deserializer, Error, Unexpected, Visitor},
116        ser::{Serialize, Serializer},
117    };
118    use std::fmt;
119
120    use crate::Date;
121
122    pub const DATE_NEWTYPE_STRUCT_NAME: &str = "PLIST-DATE";
123
124    impl Serialize for Date {
125        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
126        where
127            S: Serializer,
128        {
129            let date_str = self.to_xml_format();
130            serializer.serialize_newtype_struct(DATE_NEWTYPE_STRUCT_NAME, &date_str)
131        }
132    }
133
134    struct DateNewtypeVisitor;
135
136    impl<'de> Visitor<'de> for DateNewtypeVisitor {
137        type Value = Date;
138
139        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
140            formatter.write_str("a plist date newtype")
141        }
142
143        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
144        where
145            E: Error,
146        {
147            DateStrVisitor.visit_str(v)
148        }
149
150        fn visit_newtype_struct<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
151        where
152            D: Deserializer<'de>,
153        {
154            deserializer.deserialize_str(DateStrVisitor)
155        }
156    }
157
158    struct DateStrVisitor;
159
160    impl<'de> Visitor<'de> for DateStrVisitor {
161        type Value = Date;
162
163        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
164            formatter.write_str("a plist date string")
165        }
166
167        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
168        where
169            E: Error,
170        {
171            Date::from_xml_format(v).map_err(|_| E::invalid_value(Unexpected::Str(v), &self))
172        }
173    }
174
175    impl<'de> Deserialize<'de> for Date {
176        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
177        where
178            D: Deserializer<'de>,
179        {
180            deserializer.deserialize_newtype_struct(DATE_NEWTYPE_STRUCT_NAME, DateNewtypeVisitor)
181        }
182    }
183}
184
185#[cfg(test)]
186mod testing {
187    use super::*;
188
189    #[test]
190    fn date_roundtrip() {
191        let date_str = "1981-05-16T11:32:06Z";
192
193        let date = Date::from_xml_format(date_str).expect("should parse");
194
195        let generated_str = date.to_xml_format();
196
197        assert_eq!(date_str, generated_str);
198    }
199
200    #[test]
201    fn far_past_date() {
202        let date_str = "1920-01-01T00:00:00Z";
203        Date::from_xml_format(date_str).expect("should parse");
204    }
205}