#![cfg(feature = "time")]
use std::{cmp::Ordering, convert::TryFrom, str::from_utf8};
use time::{
Date, PrimitiveDateTime, Time,
error::{Parse, TryFromParsed},
format_description::{
Component, FormatItem,
modifier::{self, Subsecond},
},
};
use crate::value::Value;
use super::{FromValue, FromValueError, ParseIr, parse_mysql_time_string};
static FULL_YEAR: modifier::Year = {
let mut year_modifier = modifier::Year::default();
year_modifier.padding = modifier::Padding::Zero;
year_modifier.repr = modifier::YearRepr::Full;
year_modifier.iso_week_based = false;
year_modifier.sign_is_mandatory = false;
year_modifier
};
static ZERO_PADDED_MONTH: modifier::Month = {
let mut month_modifier = modifier::Month::default();
month_modifier.padding = modifier::Padding::Zero;
month_modifier.repr = modifier::MonthRepr::Numerical;
month_modifier.case_sensitive = true;
month_modifier
};
static ZERO_PADDED_DAY: modifier::Day = {
let mut day_modifier = modifier::Day::default();
day_modifier.padding = modifier::Padding::Zero;
day_modifier
};
static ZERO_PADDED_HOUR: modifier::Hour = {
let mut hour_modifier = modifier::Hour::default();
hour_modifier.padding = modifier::Padding::Zero;
hour_modifier.is_12_hour_clock = false;
hour_modifier
};
static ZERO_PADDED_MINUTE: modifier::Minute = {
let mut minute_modifier = modifier::Minute::default();
minute_modifier.padding = modifier::Padding::Zero;
minute_modifier
};
static ZERO_PADDED_SECOND: modifier::Second = {
let mut second_modifier = modifier::Second::default();
second_modifier.padding = modifier::Padding::Zero;
second_modifier
};
static DATE_FORMAT: &[FormatItem<'static>] = &[
FormatItem::Component(Component::Year(FULL_YEAR)),
FormatItem::Literal(b"-"),
FormatItem::Component(Component::Month(ZERO_PADDED_MONTH)),
FormatItem::Literal(b"-"),
FormatItem::Component(Component::Day(ZERO_PADDED_DAY)),
];
static TIME_FORMAT: &[FormatItem<'static>] = &[
FormatItem::Component(Component::Hour(ZERO_PADDED_HOUR)),
FormatItem::Literal(b":"),
FormatItem::Component(Component::Minute(ZERO_PADDED_MINUTE)),
FormatItem::Literal(b":"),
FormatItem::Component(Component::Second(ZERO_PADDED_SECOND)),
];
static TIME_FORMAT_MICRO: &[FormatItem<'static>] = &[
FormatItem::Component(Component::Hour(ZERO_PADDED_HOUR)),
FormatItem::Literal(b":"),
FormatItem::Component(Component::Minute(ZERO_PADDED_MINUTE)),
FormatItem::Literal(b":"),
FormatItem::Component(Component::Second(ZERO_PADDED_SECOND)),
FormatItem::Literal(b"."),
FormatItem::Component(Component::Subsecond(Subsecond::default())),
];
static DATE_TIME_FORMAT: &[FormatItem<'static>] = &[
FormatItem::Component(Component::Year(FULL_YEAR)),
FormatItem::Literal(b"-"),
FormatItem::Component(Component::Month(ZERO_PADDED_MONTH)),
FormatItem::Literal(b"-"),
FormatItem::Component(Component::Day(ZERO_PADDED_DAY)),
FormatItem::Literal(b" "),
FormatItem::Component(Component::Hour(ZERO_PADDED_HOUR)),
FormatItem::Literal(b":"),
FormatItem::Component(Component::Minute(ZERO_PADDED_MINUTE)),
FormatItem::Literal(b":"),
FormatItem::Component(Component::Second(ZERO_PADDED_SECOND)),
];
static DATE_TIME_FORMAT_MICRO: &[FormatItem<'static>] = &[
FormatItem::Component(Component::Year(FULL_YEAR)),
FormatItem::Literal(b"-"),
FormatItem::Component(Component::Month(ZERO_PADDED_MONTH)),
FormatItem::Literal(b"-"),
FormatItem::Component(Component::Day(ZERO_PADDED_DAY)),
FormatItem::Literal(b" "),
FormatItem::Component(Component::Hour(ZERO_PADDED_HOUR)),
FormatItem::Literal(b":"),
FormatItem::Component(Component::Minute(ZERO_PADDED_MINUTE)),
FormatItem::Literal(b":"),
FormatItem::Component(Component::Second(ZERO_PADDED_SECOND)),
FormatItem::Literal(b"."),
FormatItem::Component(Component::Subsecond(Subsecond::default())),
];
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl TryFrom<Value> for ParseIr<PrimitiveDateTime> {
type Error = FromValueError;
fn try_from(v: Value) -> Result<Self, Self::Error> {
match v {
Value::Date(year, month, day, hour, minute, second, micros) => {
match create_primitive_date_time(year, month, day, hour, minute, second, micros) {
Some(x) => Ok(ParseIr(x, v)),
None => Err(FromValueError(v)),
}
}
Value::Bytes(ref bytes) => match parse_mysql_datetime_string_with_time(bytes) {
Ok(x) => Ok(ParseIr(x, v)),
Err(_) => Err(FromValueError(v)),
},
v => Err(FromValueError(v)),
}
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl From<ParseIr<PrimitiveDateTime>> for PrimitiveDateTime {
fn from(value: ParseIr<PrimitiveDateTime>) -> Self {
value.commit()
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl From<ParseIr<PrimitiveDateTime>> for Value {
fn from(value: ParseIr<PrimitiveDateTime>) -> Self {
value.rollback()
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl FromValue for PrimitiveDateTime {
type Intermediate = ParseIr<PrimitiveDateTime>;
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl TryFrom<Value> for ParseIr<Date> {
type Error = FromValueError;
fn try_from(v: Value) -> Result<Self, Self::Error> {
match v {
Value::Date(year, month, day, _, _, _, _) => {
let mon = match time::Month::try_from(month) {
Ok(month) => month,
Err(_) => return Err(FromValueError(v)),
};
match Date::from_calendar_date(year as i32, mon, day) {
Ok(x) => Ok(ParseIr(x, v)),
Err(_) => Err(FromValueError(v)),
}
}
Value::Bytes(ref bytes) => {
match from_utf8(bytes)
.ok()
.and_then(|s| Date::parse(s, DATE_FORMAT).ok())
{
Some(x) => Ok(ParseIr(x, v)),
None => Err(FromValueError(v)),
}
}
v => Err(FromValueError(v)),
}
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl From<ParseIr<Date>> for Date {
fn from(value: ParseIr<Date>) -> Self {
value.commit()
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl From<ParseIr<Date>> for Value {
fn from(value: ParseIr<Date>) -> Self {
value.rollback()
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl FromValue for Date {
type Intermediate = ParseIr<Date>;
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl TryFrom<Value> for ParseIr<Time> {
type Error = FromValueError;
fn try_from(v: Value) -> Result<Self, Self::Error> {
match v {
Value::Time(false, 0, h, m, s, u) => match Time::from_hms_micro(h, m, s, u) {
Ok(x) => Ok(ParseIr(x, v)),
Err(_) => Err(FromValueError(v)),
},
Value::Bytes(ref bytes) => match parse_mysql_time_string_with_time(bytes) {
Ok(x) => Ok(ParseIr(x, v)),
Err(_) => Err(FromValueError(v)),
},
v => Err(FromValueError(v)),
}
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl From<ParseIr<Time>> for Time {
fn from(value: ParseIr<Time>) -> Self {
value.commit()
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl From<ParseIr<Time>> for Value {
fn from(value: ParseIr<Time>) -> Self {
value.rollback()
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl FromValue for Time {
type Intermediate = ParseIr<Time>;
}
fn create_primitive_date_time(
year: u16,
month: u8,
day: u8,
hour: u8,
minute: u8,
second: u8,
micros: u32,
) -> Option<PrimitiveDateTime> {
let mon = time::Month::try_from(month).ok()?;
if let Ok(date) = Date::from_calendar_date(year as i32, mon, day)
&& let Ok(time) = Time::from_hms_micro(hour, minute, second, micros)
{
return Some(PrimitiveDateTime::new(date, time));
}
None
}
pub(crate) fn parse_mysql_datetime_string_with_time(
bytes: &[u8],
) -> Result<PrimitiveDateTime, Parse> {
from_utf8(bytes)
.map_err(|_| Parse::TryFromParsed(TryFromParsed::InsufficientInformation))
.and_then(|s| {
if s.len() > 19 {
PrimitiveDateTime::parse(s, DATE_TIME_FORMAT_MICRO)
} else if s.len() == 19 {
PrimitiveDateTime::parse(&s[..19], DATE_TIME_FORMAT)
} else if s.len() >= 10 {
PrimitiveDateTime::parse(s, DATE_FORMAT)
} else {
Err(Parse::TryFromParsed(TryFromParsed::InsufficientInformation))
}
})
}
fn parse_mysql_time_string_with_time(bytes: &[u8]) -> Result<Time, Parse> {
from_utf8(bytes)
.map_err(|_| Parse::TryFromParsed(TryFromParsed::InsufficientInformation))
.and_then(|s| match s.len().cmp(&8) {
Ordering::Less => Err(Parse::TryFromParsed(TryFromParsed::InsufficientInformation)),
Ordering::Equal => Time::parse(s, TIME_FORMAT),
Ordering::Greater => Time::parse(s, TIME_FORMAT_MICRO),
})
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl TryFrom<Value> for ParseIr<time::Duration> {
type Error = FromValueError;
fn try_from(v: Value) -> Result<Self, Self::Error> {
match v {
Value::Time(is_neg, days, hours, minutes, seconds, microseconds) => {
let duration = time::Duration::days(days.into())
+ time::Duration::hours(hours.into())
+ time::Duration::minutes(minutes.into())
+ time::Duration::seconds(seconds.into())
+ time::Duration::microseconds(microseconds.into());
Ok(ParseIr(if is_neg { -duration } else { duration }, v))
}
Value::Bytes(ref val_bytes) => {
let duration = match parse_mysql_time_string(val_bytes) {
Some((is_neg, hours, minutes, seconds, microseconds)) => {
let duration = time::Duration::hours(hours.into())
+ time::Duration::minutes(minutes.into())
+ time::Duration::seconds(seconds.into())
+ time::Duration::microseconds(microseconds.into());
if is_neg { -duration } else { duration }
}
_ => return Err(FromValueError(v)),
};
Ok(ParseIr(duration, v))
}
_ => Err(FromValueError(v)),
}
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl From<ParseIr<time::Duration>> for time::Duration {
fn from(value: ParseIr<time::Duration>) -> Self {
value.commit()
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl From<ParseIr<time::Duration>> for Value {
fn from(value: ParseIr<time::Duration>) -> Self {
value.rollback()
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl FromValue for time::Duration {
type Intermediate = ParseIr<time::Duration>;
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl From<PrimitiveDateTime> for Value {
fn from(x: PrimitiveDateTime) -> Value {
Value::Date(
x.year() as u16,
x.month() as u8,
x.day(),
x.hour(),
x.minute(),
x.second(),
x.microsecond(),
)
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl From<Date> for Value {
fn from(x: Date) -> Value {
Value::Date(x.year() as u16, x.month() as u8, x.day(), 0, 0, 0, 0)
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl From<Time> for Value {
fn from(x: Time) -> Value {
Value::Time(false, 0, x.hour(), x.minute(), x.second(), x.microsecond())
}
}
#[cfg_attr(docsrs, doc(cfg(feature = "time")))]
impl From<time::Duration> for Value {
fn from(mut x: time::Duration) -> Value {
let negative = x < time::Duration::ZERO;
if negative {
x = -x;
}
let days = x.whole_days() as u32;
x = x - time::Duration::days(x.whole_days());
let hours = x.whole_hours() as u8;
x = x - time::Duration::hours(x.whole_hours());
let minutes = x.whole_minutes() as u8;
x = x - time::Duration::minutes(x.whole_minutes());
let seconds = x.whole_seconds() as u8;
x = x - time::Duration::seconds(x.whole_seconds());
let microseconds = x.whole_microseconds() as u32;
Value::Time(negative, days, hours, minutes, seconds, microseconds)
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use time::error::ParseFromDescription;
use super::*;
proptest! {
#[test]
fn parse_mysql_time_string_doesnt_crash(s in r"\PC*") {
let _ = parse_mysql_time_string_with_time(s.as_bytes());
}
#[test]
fn parse_mysql_datetime_string_doesnt_crash(s in r"\PC*") {
let _ = parse_mysql_datetime_string_with_time(s.as_bytes());
}
#[test]
fn parse_mysql_time_string_parses_correctly(
h in 0u32..60,
i in 0u32..60,
s in 0u32..60,
have_us in 0..2,
us in 0u32..1000000,
) {
let time_string = format!(
"{:02}:{:02}:{:02}{}",
h, i, s,
if have_us == 1 {
format!(".{:06}", us).trim_end_matches('0').to_owned()
} else {
"".into()
}
);
match parse_mysql_time_string_with_time(time_string.as_bytes()) {
Ok(time) => {
assert_eq!(
(
time.hour() as u32,
time.minute() as u32,
time.second() as u32,
time.microsecond(),
),
(h, i, s, if have_us == 1 { us } else { 0 }));
},
Err(err) => {
match err {
Parse::ParseFromDescription(ParseFromDescription::InvalidComponent("second")) => assert!(s > 59),
Parse::ParseFromDescription(ParseFromDescription::InvalidComponent("minute")) => assert!(i > 59),
Parse::ParseFromDescription(ParseFromDescription::InvalidComponent("hour")) => assert!(h > 23),
Parse::TryFromParsed(TryFromParsed::ComponentRange(_)) |
Parse::TryFromParsed(TryFromParsed::InsufficientInformation) => {
if Time::from_hms_micro(h as u8, 0, 0, 0).is_err() {
assert!(h > 23);
} else if Time::from_hms_micro(0, i as u8, 0, 0).is_err() {
assert!(i > 59);
} else if Time::from_hms_micro(0, 0, s as u8, 0).is_err() {
assert!(s > 59);
}
},
err => {
panic!("Failed to parse time `{}` due to an unknown reason. {}", time_string, err);
}
}
}
}
}
#[test]
fn parse_mysql_datetime_string_parses_correctly(
y in 0u32..10000,
m in 0u32..12,
d in 1u32..32,
h in 0u32..60,
i in 0u32..60,
s in 0u32..60,
have_us in 0..2,
us in 0u32..1000000,
) {
let time_string = format!(
"{:04}-{:02}-{:02} {:02}:{:02}:{:02}{}",
y, m, d, h, i, s,
if have_us == 1 {
format!(".{:06}", us).trim_end_matches('0').to_owned()
} else {
"".into()
}
);
match parse_mysql_datetime_string_with_time(time_string.as_bytes()) {
Ok(datetime) => {
assert_eq!(
(
datetime.year() as u32,
datetime.month() as u32,
datetime.day() as u32,
datetime.hour() as u32,
datetime.minute() as u32,
datetime.second() as u32,
datetime.microsecond(),
),
(y, m, d, h, i, s, if have_us == 1 { us } else { 0 }));
},
Err(err) => {
match err {
Parse::ParseFromDescription(ParseFromDescription::InvalidComponent("second")) => assert!(s > 59),
Parse::ParseFromDescription(ParseFromDescription::InvalidComponent("minute")) => assert!(i > 59),
Parse::ParseFromDescription(ParseFromDescription::InvalidComponent("hour")) => assert!(h > 23),
Parse::ParseFromDescription(ParseFromDescription::InvalidComponent("dayofmonth")) => assert!(!(1..=31).contains(&d)),
Parse::ParseFromDescription(ParseFromDescription::InvalidComponent("month")) => assert!(m < 1000 || m <= 12),
Parse::ParseFromDescription(ParseFromDescription::InvalidComponent("year")) => assert!(y != 0 && !(1000..=9999).contains(&y)),
Parse::TryFromParsed(TryFromParsed::ComponentRange(_)) |
Parse::TryFromParsed(TryFromParsed::InsufficientInformation) => {
if Date::from_calendar_date(y as i32, time::Month::January, 1).is_err() {
assert!(y != 0 && !(1000..=9999).contains(&y));
} else if time::Month::try_from(m as u8).is_err() {
assert!(m < 1000 || m <= 12);
} else if Date::from_calendar_date(0, time::Month::January, d as u8).is_err() {
assert!(!(1..=31).contains(&d));
} else if Time::from_hms_micro(h as u8, 0, 0, 0).is_err() {
assert!(h > 23);
} else if Time::from_hms_micro(0, i as u8, 0, 0).is_err() {
assert!(i > 59);
} else if Time::from_hms_micro(0, 0, s as u8, 0).is_err() {
assert!(s > 59);
}
},
err => {
panic!("Failed to parse time `{}` due to an unknown reason. {}", time_string, err);
}
}
}
}
}
}
}