1use std::error;
2use std::fmt;
3use std::str::{self, FromStr};
4
5#[cfg(feature = "serde")]
6use 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)]
81pub 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]
98pub 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")]
108pub const FIELD: &str = "$__toml_private_datetime";
109#[doc(hidden)]
110#[cfg(feature = "serde")]
111pub 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)]
128pub 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)]
158pub 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)]
172pub 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
187impl From<Date> for Datetime {
188 fn from(other: Date) -> Self {
189 Datetime {
190 date: Some(other),
191 time: None,
192 offset: None,
193 }
194 }
195}
196
197impl From<Time> for Datetime {
198 fn from(other: Time) -> Self {
199 Datetime {
200 date: None,
201 time: Some(other),
202 offset: None,
203 }
204 }
205}
206
207impl fmt::Display for Datetime {
208 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
209 if let Some(ref date: &Date) = self.date {
210 write!(f, "{}", date)?;
211 }
212 if let Some(ref time: &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: &Offset) = self.offset {
219 write!(f, "{}", offset)?;
220 }
221 Ok(())
222 }
223}
224
225impl fmt::Display for Date {
226 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
227 write!(f, "{:04}-{:02}-{:02}", self.year, self.month, self.day)
228 }
229}
230
231impl fmt::Display for Time {
232 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: String = format!("{:09}", self.nanosecond);
236 write!(f, ".{}", s.trim_end_matches('0'))?;
237 }
238 Ok(())
239 }
240}
241
242impl fmt::Display for Offset {
243 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
244 match *self {
245 Offset::Z => write!(f, "Z"),
246 Offset::Custom { mut minutes: i16 } => {
247 let mut sign: char = '+';
248 if minutes < 0 {
249 minutes *= -1;
250 sign = '-';
251 }
252 let hours: i16 = minutes / 60;
253 let minutes: i16 = minutes % 60;
254 write!(f, "{}{:02}:{:02}", sign, hours, minutes)
255 }
256 }
257 }
258}
259
260impl FromStr for Datetime {
261 type Err = DatetimeParseError;
262
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 if date.day < 1 || date.day > 31 {
312 return Err(DatetimeParseError {});
313 }
314
315 Some(date)
316 };
317
318 // Next parse the "partial-time" if available
319 let next = chars.clone().next();
320 let partial_time = if full_date.is_some()
321 && (next == Some('T') || next == Some('t') || next == Some(' '))
322 {
323 chars.next();
324 true
325 } else {
326 full_date.is_none()
327 };
328
329 let time = if partial_time {
330 let h1 = digit(&mut chars)?;
331 let h2 = digit(&mut chars)?;
332 match chars.next() {
333 Some(':') => {}
334 _ => return Err(DatetimeParseError {}),
335 }
336 let m1 = digit(&mut chars)?;
337 let m2 = digit(&mut chars)?;
338 match chars.next() {
339 Some(':') => {}
340 _ => return Err(DatetimeParseError {}),
341 }
342 let s1 = digit(&mut chars)?;
343 let s2 = digit(&mut chars)?;
344
345 let mut nanosecond = 0;
346 if chars.clone().next() == Some('.') {
347 chars.next();
348 let whole = chars.as_str();
349
350 let mut end = whole.len();
351 for (i, byte) in whole.bytes().enumerate() {
352 match byte {
353 b'0'..=b'9' => {
354 if i < 9 {
355 let p = 10_u32.pow(8 - i as u32);
356 nanosecond += p * u32::from(byte - b'0');
357 }
358 }
359 _ => {
360 end = i;
361 break;
362 }
363 }
364 }
365 if end == 0 {
366 return Err(DatetimeParseError {});
367 }
368 chars = whole[end..].chars();
369 }
370
371 let time = Time {
372 hour: h1 * 10 + h2,
373 minute: m1 * 10 + m2,
374 second: s1 * 10 + s2,
375 nanosecond,
376 };
377
378 if time.hour > 24 {
379 return Err(DatetimeParseError {});
380 }
381 if time.minute > 59 {
382 return Err(DatetimeParseError {});
383 }
384 if time.second > 59 {
385 return Err(DatetimeParseError {});
386 }
387 if time.nanosecond > 999_999_999 {
388 return Err(DatetimeParseError {});
389 }
390
391 Some(time)
392 } else {
393 offset_allowed = false;
394 None
395 };
396
397 // And finally, parse the offset
398 let offset = if offset_allowed {
399 let next = chars.clone().next();
400 if next == Some('Z') || next == Some('z') {
401 chars.next();
402 Some(Offset::Z)
403 } else if next.is_none() {
404 None
405 } else {
406 let sign = match next {
407 Some('+') => 1,
408 Some('-') => -1,
409 _ => return Err(DatetimeParseError {}),
410 };
411 chars.next();
412 let h1 = digit(&mut chars)? as i16;
413 let h2 = digit(&mut chars)? as i16;
414 match chars.next() {
415 Some(':') => {}
416 _ => return Err(DatetimeParseError {}),
417 }
418 let m1 = digit(&mut chars)? as i16;
419 let m2 = digit(&mut chars)? as i16;
420
421 let hours = h1 * 10 + h2;
422 let minutes = m1 * 10 + m2;
423
424 let total_minutes = sign * (hours * 60 + minutes);
425
426 if !((-24 * 60)..=(24 * 60)).contains(&total_minutes) {
427 return Err(DatetimeParseError {});
428 }
429
430 Some(Offset::Custom {
431 minutes: total_minutes,
432 })
433 }
434 } else {
435 None
436 };
437
438 // Return an error if we didn't hit eof, otherwise return our parsed
439 // date
440 if chars.next().is_some() {
441 return Err(DatetimeParseError {});
442 }
443
444 Ok(Datetime {
445 date: full_date,
446 time,
447 offset,
448 })
449 }
450}
451
452fn digit(chars: &mut str::Chars<'_>) -> Result<u8, DatetimeParseError> {
453 match chars.next() {
454 Some(c: char) if ('0'..='9').contains(&c) => Ok(c as u8 - b'0'),
455 _ => Err(DatetimeParseError {}),
456 }
457}
458
459#[cfg(feature = "serde")]
460impl ser::Serialize for Datetime {
461 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
462 where
463 S: ser::Serializer,
464 {
465 use serde::ser::SerializeStruct;
466
467 let mut s: ::SerializeStruct = serializer.serialize_struct(NAME, len:1)?;
468 s.serialize_field(FIELD, &self.to_string())?;
469 s.end()
470 }
471}
472
473#[cfg(feature = "serde")]
474impl<'de> de::Deserialize<'de> for Datetime {
475 fn deserialize<D>(deserializer: D) -> Result<Datetime, D::Error>
476 where
477 D: de::Deserializer<'de>,
478 {
479 struct DatetimeVisitor;
480
481 impl<'de> de::Visitor<'de> for DatetimeVisitor {
482 type Value = Datetime;
483
484 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
485 formatter.write_str("a TOML datetime")
486 }
487
488 fn visit_map<V>(self, mut visitor: V) -> Result<Datetime, V::Error>
489 where
490 V: de::MapAccess<'de>,
491 {
492 let value = visitor.next_key::<DatetimeKey>()?;
493 if value.is_none() {
494 return Err(de::Error::custom("datetime key not found"));
495 }
496 let v: DatetimeFromString = visitor.next_value()?;
497 Ok(v.value)
498 }
499 }
500
501 static FIELDS: [&str; 1] = [FIELD];
502 deserializer.deserialize_struct(NAME, &FIELDS, DatetimeVisitor)
503 }
504}
505
506#[cfg(feature = "serde")]
507struct DatetimeKey;
508
509#[cfg(feature = "serde")]
510impl<'de> de::Deserialize<'de> for DatetimeKey {
511 fn deserialize<D>(deserializer: D) -> Result<DatetimeKey, D::Error>
512 where
513 D: de::Deserializer<'de>,
514 {
515 struct FieldVisitor;
516
517 impl<'de> de::Visitor<'de> for FieldVisitor {
518 type Value = ();
519
520 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
521 formatter.write_str("a valid datetime field")
522 }
523
524 fn visit_str<E>(self, s: &str) -> Result<(), E>
525 where
526 E: de::Error,
527 {
528 if s == FIELD {
529 Ok(())
530 } else {
531 Err(de::Error::custom("expected field with custom name"))
532 }
533 }
534 }
535
536 deserializer.deserialize_identifier(FieldVisitor)?;
537 Ok(DatetimeKey)
538 }
539}
540
541#[doc(hidden)]
542#[cfg(feature = "serde")]
543pub struct DatetimeFromString {
544 pub value: Datetime,
545}
546
547#[cfg(feature = "serde")]
548impl<'de> de::Deserialize<'de> for DatetimeFromString {
549 fn deserialize<D>(deserializer: D) -> Result<DatetimeFromString, D::Error>
550 where
551 D: de::Deserializer<'de>,
552 {
553 struct Visitor;
554
555 impl<'de> de::Visitor<'de> for Visitor {
556 type Value = DatetimeFromString;
557
558 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
559 formatter.write_str("string containing a datetime")
560 }
561
562 fn visit_str<E>(self, s: &str) -> Result<DatetimeFromString, E>
563 where
564 E: de::Error,
565 {
566 match s.parse() {
567 Ok(date) => Ok(DatetimeFromString { value: date }),
568 Err(e) => Err(de::Error::custom(e)),
569 }
570 }
571 }
572
573 deserializer.deserialize_str(Visitor)
574 }
575}
576
577impl fmt::Display for DatetimeParseError {
578 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
579 "failed to parse datetime".fmt(f)
580 }
581}
582
583impl error::Error for DatetimeParseError {}
584