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 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
461fn digit(chars: &mut str::Chars<'_>) -> Result<u8, DatetimeParseError> {
462 match chars.next() {
463 Some(c: char) if c.is_ascii_digit() => Ok(c as u8 - b'0'),
464 _ => Err(DatetimeParseError {}),
465 }
466}
467
468#[cfg(feature = "serde")]
469impl ser::Serialize for Datetime {
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: ::SerializeStruct = serializer.serialize_struct(NAME, len:1)?;
477 s.serialize_field(FIELD, &self.to_string())?;
478 s.end()
479 }
480}
481
482#[cfg(feature = "serde")]
483impl<'de> de::Deserialize<'de> for Datetime {
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")]
516struct DatetimeKey;
517
518#[cfg(feature = "serde")]
519impl<'de> de::Deserialize<'de> for DatetimeKey {
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")]
552pub struct DatetimeFromString {
553 pub value: Datetime,
554}
555
556#[cfg(feature = "serde")]
557impl<'de> de::Deserialize<'de> for DatetimeFromString {
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
586impl fmt::Display for DatetimeParseError {
587 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
588 "failed to parse datetime".fmt(f)
589 }
590}
591
592impl error::Error for DatetimeParseError {}
593