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