diff --git a/src/items/mod.rs b/src/items/mod.rs index 51fe78a..18908d0 100644 --- a/src/items/mod.rs +++ b/src/items/mod.rs @@ -33,6 +33,7 @@ mod ordinal; mod relative; mod time; mod weekday; + mod epoch { use winnow::{combinator::preceded, ModalResult, Parser}; @@ -41,6 +42,7 @@ mod epoch { s(preceded("@", dec_int)).parse_next(input) } } + mod timezone { use super::time; use winnow::ModalResult; @@ -53,12 +55,11 @@ mod timezone { use chrono::NaiveDate; use chrono::{DateTime, Datelike, FixedOffset, TimeZone, Timelike}; -use winnow::error::{StrContext, StrContextValue}; use winnow::{ ascii::{digit1, multispace0}, combinator::{alt, delimited, not, opt, peek, preceded, repeat, separated, trace}, - error::{ContextError, ErrMode, ParserError}, - stream::AsChar, + error::{AddContext, ContextError, ErrMode, ParserError, StrContext, StrContextValue}, + stream::{AsChar, Stream}, token::{none_of, one_of, take_while}, ModalResult, Parser, }; @@ -193,6 +194,14 @@ pub fn parse_one(input: &mut &str) -> ModalResult { .parse_next(input) } +fn expect_error(input: &mut &str, reason: &'static str) -> ErrMode { + ErrMode::Cut(ContextError::new()).add_context( + input, + &input.checkpoint(), + StrContext::Expected(StrContextValue::Description(reason)), + ) +} + pub fn parse(input: &mut &str) -> ModalResult> { let mut items = Vec::new(); let mut date_seen = false; @@ -206,13 +215,10 @@ pub fn parse(input: &mut &str) -> ModalResult> { match item { Item::DateTime(ref dt) => { if date_seen || time_seen { - let mut ctx_err = ContextError::new(); - ctx_err.push(StrContext::Expected( - winnow::error::StrContextValue::Description( - "date or time cannot appear more than once", - ), + return Err(expect_error( + input, + "date or time cannot appear more than once", )); - return Err(ErrMode::Backtrack(ctx_err)); } date_seen = true; @@ -223,11 +229,7 @@ pub fn parse(input: &mut &str) -> ModalResult> { } Item::Date(ref d) => { if date_seen { - let mut ctx_err = ContextError::new(); - ctx_err.push(StrContext::Expected(StrContextValue::Description( - "date cannot appear more than once", - ))); - return Err(ErrMode::Backtrack(ctx_err)); + return Err(expect_error(input, "date cannot appear more than once")); } date_seen = true; @@ -235,33 +237,27 @@ pub fn parse(input: &mut &str) -> ModalResult> { year_seen = true; } } - Item::Time(_) => { + Item::Time(ref t) => { if time_seen { - let mut ctx_err = ContextError::new(); - ctx_err.push(StrContext::Expected(StrContextValue::Description( - "time cannot appear more than once", - ))); - return Err(ErrMode::Backtrack(ctx_err)); + return Err(expect_error(input, "time cannot appear more than once")); } time_seen = true; + if t.offset.is_some() { + tz_seen = true; + } } Item::Year(_) => { if year_seen { - let mut ctx_err = ContextError::new(); - ctx_err.push(StrContext::Expected(StrContextValue::Description( - "year cannot appear more than once", - ))); - return Err(ErrMode::Backtrack(ctx_err)); + return Err(expect_error(input, "year cannot appear more than once")); } year_seen = true; } Item::TimeZone(_) => { if tz_seen { - let mut ctx_err = ContextError::new(); - ctx_err.push(StrContext::Expected(StrContextValue::Description( + return Err(expect_error( + input, "timezone cannot appear more than once", - ))); - return Err(ErrMode::Backtrack(ctx_err)); + )); } tz_seen = true; } @@ -276,7 +272,7 @@ pub fn parse(input: &mut &str) -> ModalResult> { space.parse_next(input)?; if !input.is_empty() { - return Err(ErrMode::Backtrack(ContextError::new())); + return Err(expect_error(input, "unexpected input")); } Ok(items) @@ -303,7 +299,7 @@ fn with_timezone_restore( offset: time::Offset, at: DateTime, ) -> Option> { - let offset: FixedOffset = chrono::FixedOffset::from(offset); + let offset: FixedOffset = chrono::FixedOffset::try_from(offset).ok()?; let copy = at; let x = at .with_timezone(&offset) @@ -360,7 +356,9 @@ fn at_date_inner(date: Vec, mut d: DateTime) -> Option { - let offset = offset.map(chrono::FixedOffset::from).unwrap_or(*d.offset()); + let offset = offset + .and_then(|o| chrono::FixedOffset::try_from(o).ok()) + .unwrap_or(*d.offset()); d = new_date( year.map(|x| x as i32).unwrap_or(d.year()), @@ -379,7 +377,10 @@ fn at_date_inner(date: Vec, mut d: DateTime) -> Option { - let offset = offset.map(chrono::FixedOffset::from).unwrap_or(*d.offset()); + let offset = offset + .and_then(|o| chrono::FixedOffset::try_from(o).ok()) + .unwrap_or(*d.offset()); + d = new_date( d.year(), d.month(), @@ -535,4 +536,46 @@ mod tests { test_eq_fmt("%Y-%m-%d %H:%M:%S %Z", "Jul 17 06:14:49 2024 BRT"), ); } + + #[test] + fn invalid() { + let result = parse(&mut "2025-05-19 2024-05-20 06:14:49"); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("date or time cannot appear more than once")); + + let result = parse(&mut "2025-05-19 2024-05-20"); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("date cannot appear more than once")); + + let result = parse(&mut "06:14:49 06:14:49"); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("time cannot appear more than once")); + + let result = parse(&mut "2025-05-19 2024"); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("year cannot appear more than once")); + + let result = parse(&mut "2025-05-19 +00:00 +01:00"); + assert!(result.is_err()); + assert!(result + .unwrap_err() + .to_string() + .contains("timezone cannot appear more than once")); + + let result = parse(&mut "2025-05-19 abcdef"); + assert!(result.is_err()); + assert!(result.unwrap_err().to_string().contains("unexpected input")); + } } diff --git a/src/items/time.rs b/src/items/time.rs index 6177f97..7fa8d3f 100644 --- a/src/items/time.rs +++ b/src/items/time.rs @@ -50,6 +50,8 @@ use winnow::{ ModalResult, Parser, }; +use crate::ParseDateTimeError; + use super::{dec_uint, relative, s}; #[derive(PartialEq, Debug, Clone, Default)] @@ -95,22 +97,33 @@ impl Offset { } } -impl From for chrono::FixedOffset { - fn from( +impl TryFrom for chrono::FixedOffset { + type Error = ParseDateTimeError; + + fn try_from( Offset { negative, hours, minutes, }: Offset, - ) -> Self { + ) -> Result { let secs = hours * 3600 + minutes * 60; - if negative { - FixedOffset::west_opt(secs.try_into().expect("secs overflow")) - .expect("timezone overflow") + let offset = if negative { + FixedOffset::west_opt( + secs.try_into() + .map_err(|_| ParseDateTimeError::InvalidInput)?, + ) + .ok_or(ParseDateTimeError::InvalidInput)? } else { - FixedOffset::east_opt(secs.try_into().unwrap()).unwrap() - } + FixedOffset::east_opt( + secs.try_into() + .map_err(|_| ParseDateTimeError::InvalidInput)?, + ) + .ok_or(ParseDateTimeError::InvalidInput)? + }; + + Ok(offset) } } diff --git a/src/lib.rs b/src/lib.rs index 9462a75..3e36bf0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -266,6 +266,12 @@ mod tests { .and_utc(); assert_eq!(actual, expected); } + + #[test] + fn offset_overflow() { + assert!(parse_datetime("m+12").is_err()); + assert!(parse_datetime("24:00").is_err()); + } } #[cfg(test)]