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: &Date) = self.date {
195 write!(f, "{}", date)?;
196 }
197 if let Some(ref time: &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: &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: String = 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: i8, minutes: u8 } => 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: char) 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: ::SerializeStruct = serializer.serialize_struct(NAME, len: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 {}
545