toml/
datetime.rs

1use std::error;
2use std::fmt;
3use std::str::{self, FromStr};
4
5use serde::{de, ser};
6
7/// A parsed TOML datetime value
8///
9/// This structure is intended to represent the datetime primitive type that can
10/// be encoded into TOML documents. This type is a parsed version that contains
11/// all metadata internally.
12///
13/// Currently this type is intentionally conservative and only supports
14/// `to_string` as an accessor. Over time though it's intended that it'll grow
15/// more support!
16///
17/// Note that if you're using `Deserialize` to deserialize a TOML document, you
18/// can use this as a placeholder for where you're expecting a datetime to be
19/// specified.
20///
21/// Also note though that while this type implements `Serialize` and
22/// `Deserialize` it's only recommended to use this type with the TOML format,
23/// otherwise encoded in other formats it may look a little odd.
24///
25/// Depending on how the option values are used, this struct will correspond
26/// with one of the following four datetimes from the [TOML v1.0.0 spec]:
27///
28/// | `date`    | `time`    | `offset`  | TOML type          |
29/// | --------- | --------- | --------- | ------------------ |
30/// | `Some(_)` | `Some(_)` | `Some(_)` | [Offset Date-Time] |
31/// | `Some(_)` | `Some(_)` | `None`    | [Local Date-Time]  |
32/// | `Some(_)` | `None`    | `None`    | [Local Date]       |
33/// | `None`    | `Some(_)` | `None`    | [Local Time]       |
34///
35/// **1. Offset Date-Time**: If all the optional values are used, `Datetime`
36/// corresponds to an [Offset Date-Time]. From the TOML v1.0.0 spec:
37///
38/// > To unambiguously represent a specific instant in time, you may use an
39/// > RFC 3339 formatted date-time with offset.
40/// >
41/// > ```toml
42/// > odt1 = 1979-05-27T07:32:00Z
43/// > odt2 = 1979-05-27T00:32:00-07:00
44/// > odt3 = 1979-05-27T00:32:00.999999-07:00
45/// > ```
46/// >
47/// > For the sake of readability, you may replace the T delimiter between date
48/// > and time with a space character (as permitted by RFC 3339 section 5.6).
49/// >
50/// > ```toml
51/// > odt4 = 1979-05-27 07:32:00Z
52/// > ```
53///
54/// **2. Local Date-Time**: If `date` and `time` are given but `offset` is
55/// `None`, `Datetime` corresponds to a [Local Date-Time]. From the spec:
56///
57/// > If you omit the offset from an RFC 3339 formatted date-time, it will
58/// > represent the given date-time without any relation to an offset or
59/// > timezone. It cannot be converted to an instant in time without additional
60/// > information. Conversion to an instant, if required, is implementation-
61/// > specific.
62/// >
63/// > ```toml
64/// > ldt1 = 1979-05-27T07:32:00
65/// > ldt2 = 1979-05-27T00:32:00.999999
66/// > ```
67///
68/// **3. Local Date**: If only `date` is given, `Datetime` corresponds to a
69/// [Local Date]; see the docs for [`Date`].
70///
71/// **4. Local Time**: If only `time` is given, `Datetime` corresponds to a
72/// [Local Time]; see the docs for [`Time`].
73///
74/// [TOML v1.0.0 spec]: https://toml.io/en/v1.0.0
75/// [Offset Date-Time]: https://toml.io/en/v1.0.0#offset-date-time
76/// [Local Date-Time]: https://toml.io/en/v1.0.0#local-date-time
77/// [Local Date]: https://toml.io/en/v1.0.0#local-date
78/// [Local Time]: https://toml.io/en/v1.0.0#local-time
79#[derive(PartialEq, Eq, Clone)]
80pub struct Datetime {
81    /// Optional date.
82    /// Required for: *Offset Date-Time*, *Local Date-Time*, *Local Date*.
83    pub date: Option<Date>,
84
85    /// Optional time.
86    /// Required for: *Offset Date-Time*, *Local Date-Time*, *Local Time*.
87    pub time: Option<Time>,
88
89    /// Optional offset.
90    /// Required for: *Offset Date-Time*.
91    pub offset: Option<Offset>,
92}
93
94/// Error returned from parsing a `Datetime` in the `FromStr` implementation.
95#[derive(Debug, Clone)]
96pub struct DatetimeParseError {
97    _private: (),
98}
99
100// Currently serde itself doesn't have a datetime type, so we map our `Datetime`
101// to a special valid in the serde data model. Namely one with these special
102// fields/struct names.
103//
104// In general the TOML encoder/decoder will catch this and not literally emit
105// these strings but rather emit datetimes as they're intended.
106pub const FIELD: &str = "$__toml_private_datetime";
107pub const NAME: &str = "$__toml_private_Datetime";
108
109/// A parsed TOML date value
110///
111/// May be part of a [`Datetime`]. Alone, `Date` corresponds to a [Local Date].
112/// From the TOML v1.0.0 spec:
113///
114/// > If you include only the date portion of an RFC 3339 formatted date-time,
115/// > it will represent that entire day without any relation to an offset or
116/// > timezone.
117/// >
118/// > ```toml
119/// > ld1 = 1979-05-27
120/// > ```
121///
122/// [Local Date]: https://toml.io/en/v1.0.0#local-date
123#[derive(PartialEq, Eq, Clone)]
124pub struct Date {
125    /// Year: four digits
126    pub year: u16,
127    /// Month: 1 to 12
128    pub month: u8,
129    /// Day: 1 to {28, 29, 30, 31} (based on month/year)
130    pub day: u8,
131}
132
133/// A parsed TOML time value
134///
135/// May be part of a [`Datetime`]. Alone, `Time` corresponds to a [Local Time].
136/// From the TOML v1.0.0 spec:
137///
138/// > If you include only the time portion of an RFC 3339 formatted date-time,
139/// > it will represent that time of day without any relation to a specific
140/// > day or any offset or timezone.
141/// >
142/// > ```toml
143/// > lt1 = 07:32:00
144/// > lt2 = 00:32:00.999999
145/// > ```
146/// >
147/// > Millisecond precision is required. Further precision of fractional
148/// > seconds is implementation-specific. If the value contains greater
149/// > precision than the implementation can support, the additional precision
150/// > must be truncated, not rounded.
151///
152/// [Local Time]: https://toml.io/en/v1.0.0#local-time
153#[derive(PartialEq, Eq, Clone)]
154pub struct Time {
155    /// Hour: 0 to 23
156    pub hour: u8,
157    /// Minute: 0 to 59
158    pub minute: u8,
159    /// Second: 0 to {58, 59, 60} (based on leap second rules)
160    pub second: u8,
161    /// Nanosecond: 0 to 999_999_999
162    pub nanosecond: u32,
163}
164
165/// A parsed TOML time offset
166///
167#[derive(PartialEq, Eq, Clone)]
168pub enum Offset {
169    /// > A suffix which, when applied to a time, denotes a UTC offset of 00:00;
170    /// > often spoken "Zulu" from the ICAO phonetic alphabet representation of
171    /// > the letter "Z". --- [RFC 3339 section 2]
172    ///
173    /// [RFC 3339 section 2]: https://datatracker.ietf.org/doc/html/rfc3339#section-2
174    Z,
175
176    /// Offset between local time and UTC
177    Custom {
178        /// Hours: -12 to +12
179        hours: i8,
180
181        /// Minutes: 0 to 59
182        minutes: u8,
183    },
184}
185
186impl fmt::Debug for Datetime {
187    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188        fmt::Display::fmt(self, f)
189    }
190}
191
192impl fmt::Display for Datetime {
193    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
194        if let Some(ref date) = self.date {
195            write!(f, "{}", date)?;
196        }
197        if let Some(ref time) = self.time {
198            if self.date.is_some() {
199                write!(f, "T")?;
200            }
201            write!(f, "{}", time)?;
202        }
203        if let Some(ref offset) = self.offset {
204            write!(f, "{}", offset)?;
205        }
206        Ok(())
207    }
208}
209
210impl fmt::Display for Date {
211    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212        write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
213    }
214}
215
216impl fmt::Display for Time {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        write!(f, "{:02}:{:02}:{:02}", self.hour, self.minute, self.second)?;
219        if self.nanosecond != 0 {
220            let s = format!("{:09}", self.nanosecond);
221            write!(f, ".{}", s.trim_end_matches('0'))?;
222        }
223        Ok(())
224    }
225}
226
227impl fmt::Display for Offset {
228    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
229        match *self {
230            Offset::Z => write!(f, "Z"),
231            Offset::Custom { hours, minutes } => write!(f, "{:+03}:{:02}", hours, minutes),
232        }
233    }
234}
235
236impl FromStr for Datetime {
237    type Err = DatetimeParseError;
238
239    fn from_str(date: &str) -> Result<Datetime, DatetimeParseError> {
240        // Accepted formats:
241        //
242        // 0000-00-00T00:00:00.00Z
243        // 0000-00-00T00:00:00.00
244        // 0000-00-00
245        // 00:00:00.00
246        if date.len() < 3 {
247            return Err(DatetimeParseError { _private: () });
248        }
249        let mut offset_allowed = true;
250        let mut chars = date.chars();
251
252        // First up, parse the full date if we can
253        let full_date = if chars.clone().nth(2) == Some(':') {
254            offset_allowed = false;
255            None
256        } else {
257            let y1 = u16::from(digit(&mut chars)?);
258            let y2 = u16::from(digit(&mut chars)?);
259            let y3 = u16::from(digit(&mut chars)?);
260            let y4 = u16::from(digit(&mut chars)?);
261
262            match chars.next() {
263                Some('-') => {}
264                _ => return Err(DatetimeParseError { _private: () }),
265            }
266
267            let m1 = digit(&mut chars)?;
268            let m2 = digit(&mut chars)?;
269
270            match chars.next() {
271                Some('-') => {}
272                _ => return Err(DatetimeParseError { _private: () }),
273            }
274
275            let d1 = digit(&mut chars)?;
276            let d2 = digit(&mut chars)?;
277
278            let date = Date {
279                year: y1 * 1000 + y2 * 100 + y3 * 10 + y4,
280                month: m1 * 10 + m2,
281                day: d1 * 10 + d2,
282            };
283
284            if date.month < 1 || date.month > 12 {
285                return Err(DatetimeParseError { _private: () });
286            }
287            if date.day < 1 || date.day > 31 {
288                return Err(DatetimeParseError { _private: () });
289            }
290
291            Some(date)
292        };
293
294        // Next parse the "partial-time" if available
295        let next = chars.clone().next();
296        let partial_time = if full_date.is_some()
297            && (next == Some('T') || next == Some('t') || next == Some(' '))
298        {
299            chars.next();
300            true
301        } else {
302            full_date.is_none()
303        };
304
305        let time = if partial_time {
306            let h1 = digit(&mut chars)?;
307            let h2 = digit(&mut chars)?;
308            match chars.next() {
309                Some(':') => {}
310                _ => return Err(DatetimeParseError { _private: () }),
311            }
312            let m1 = digit(&mut chars)?;
313            let m2 = digit(&mut chars)?;
314            match chars.next() {
315                Some(':') => {}
316                _ => return Err(DatetimeParseError { _private: () }),
317            }
318            let s1 = digit(&mut chars)?;
319            let s2 = digit(&mut chars)?;
320
321            let mut nanosecond = 0;
322            if chars.clone().next() == Some('.') {
323                chars.next();
324                let whole = chars.as_str();
325
326                let mut end = whole.len();
327                for (i, byte) in whole.bytes().enumerate() {
328                    match byte {
329                        b'0'..=b'9' => {
330                            if i < 9 {
331                                let p = 10_u32.pow(8 - i as u32);
332                                nanosecond += p * u32::from(byte - b'0');
333                            }
334                        }
335                        _ => {
336                            end = i;
337                            break;
338                        }
339                    }
340                }
341                if end == 0 {
342                    return Err(DatetimeParseError { _private: () });
343                }
344                chars = whole[end..].chars();
345            }
346
347            let time = Time {
348                hour: h1 * 10 + h2,
349                minute: m1 * 10 + m2,
350                second: s1 * 10 + s2,
351                nanosecond,
352            };
353
354            if time.hour > 24 {
355                return Err(DatetimeParseError { _private: () });
356            }
357            if time.minute > 59 {
358                return Err(DatetimeParseError { _private: () });
359            }
360            if time.second > 59 {
361                return Err(DatetimeParseError { _private: () });
362            }
363            if time.nanosecond > 999_999_999 {
364                return Err(DatetimeParseError { _private: () });
365            }
366
367            Some(time)
368        } else {
369            offset_allowed = false;
370            None
371        };
372
373        // And finally, parse the offset
374        let offset = if offset_allowed {
375            let next = chars.clone().next();
376            if next == Some('Z') || next == Some('z') {
377                chars.next();
378                Some(Offset::Z)
379            } else if next.is_none() {
380                None
381            } else {
382                let sign = match next {
383                    Some('+') => 1,
384                    Some('-') => -1,
385                    _ => return Err(DatetimeParseError { _private: () }),
386                };
387                chars.next();
388                let h1 = digit(&mut chars)? as i8;
389                let h2 = digit(&mut chars)? as i8;
390                match chars.next() {
391                    Some(':') => {}
392                    _ => return Err(DatetimeParseError { _private: () }),
393                }
394                let m1 = digit(&mut chars)?;
395                let m2 = digit(&mut chars)?;
396
397                Some(Offset::Custom {
398                    hours: sign * (h1 * 10 + h2),
399                    minutes: m1 * 10 + m2,
400                })
401            }
402        } else {
403            None
404        };
405
406        // Return an error if we didn't hit eof, otherwise return our parsed
407        // date
408        if chars.next().is_some() {
409            return Err(DatetimeParseError { _private: () });
410        }
411
412        Ok(Datetime {
413            date: full_date,
414            time,
415            offset,
416        })
417    }
418}
419
420fn digit(chars: &mut str::Chars<'_>) -> Result<u8, DatetimeParseError> {
421    match chars.next() {
422        Some(c) if ('0'..='9').contains(&c) => Ok(c as u8 - b'0'),
423        _ => Err(DatetimeParseError { _private: () }),
424    }
425}
426
427impl ser::Serialize for Datetime {
428    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
429    where
430        S: ser::Serializer,
431    {
432        use serde::ser::SerializeStruct;
433
434        let mut s = serializer.serialize_struct(NAME, 1)?;
435        s.serialize_field(FIELD, &self.to_string())?;
436        s.end()
437    }
438}
439
440impl<'de> de::Deserialize<'de> for Datetime {
441    fn deserialize<D>(deserializer: D) -> Result<Datetime, D::Error>
442    where
443        D: de::Deserializer<'de>,
444    {
445        struct DatetimeVisitor;
446
447        impl<'de> de::Visitor<'de> for DatetimeVisitor {
448            type Value = Datetime;
449
450            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
451                formatter.write_str("a TOML datetime")
452            }
453
454            fn visit_map<V>(self, mut visitor: V) -> Result<Datetime, V::Error>
455            where
456                V: de::MapAccess<'de>,
457            {
458                let value = visitor.next_key::<DatetimeKey>()?;
459                if value.is_none() {
460                    return Err(de::Error::custom("datetime key not found"));
461                }
462                let v: DatetimeFromString = visitor.next_value()?;
463                Ok(v.value)
464            }
465        }
466
467        static FIELDS: [&str; 1] = [FIELD];
468        deserializer.deserialize_struct(NAME, &FIELDS, DatetimeVisitor)
469    }
470}
471
472struct DatetimeKey;
473
474impl<'de> de::Deserialize<'de> for DatetimeKey {
475    fn deserialize<D>(deserializer: D) -> Result<DatetimeKey, D::Error>
476    where
477        D: de::Deserializer<'de>,
478    {
479        struct FieldVisitor;
480
481        impl<'de> de::Visitor<'de> for FieldVisitor {
482            type Value = ();
483
484            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
485                formatter.write_str("a valid datetime field")
486            }
487
488            fn visit_str<E>(self, s: &str) -> Result<(), E>
489            where
490                E: de::Error,
491            {
492                if s == FIELD {
493                    Ok(())
494                } else {
495                    Err(de::Error::custom("expected field with custom name"))
496                }
497            }
498        }
499
500        deserializer.deserialize_identifier(FieldVisitor)?;
501        Ok(DatetimeKey)
502    }
503}
504
505pub struct DatetimeFromString {
506    pub value: Datetime,
507}
508
509impl<'de> de::Deserialize<'de> for DatetimeFromString {
510    fn deserialize<D>(deserializer: D) -> Result<DatetimeFromString, D::Error>
511    where
512        D: de::Deserializer<'de>,
513    {
514        struct Visitor;
515
516        impl<'de> de::Visitor<'de> for Visitor {
517            type Value = DatetimeFromString;
518
519            fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
520                formatter.write_str("string containing a datetime")
521            }
522
523            fn visit_str<E>(self, s: &str) -> Result<DatetimeFromString, E>
524            where
525                E: de::Error,
526            {
527                match s.parse() {
528                    Ok(date) => Ok(DatetimeFromString { value: date }),
529                    Err(e) => Err(de::Error::custom(e)),
530                }
531            }
532        }
533
534        deserializer.deserialize_str(Visitor)
535    }
536}
537
538impl fmt::Display for DatetimeParseError {
539    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
540        "failed to parse datetime".fmt(f)
541    }
542}
543
544impl error::Error for DatetimeParseError {}