1use std::str::FromStr;
2
3use smallvec::SmallVec;
4
5use crate::Time;
6
7#[derive(thiserror::Error, Debug, Clone)]
8#[allow(missing_docs)]
9pub enum Error {
10 #[error("Could not convert a duration into a date")]
11 RelativeTimeConversion,
12 #[error("Date string can not be parsed")]
13 InvalidDateString { input: String },
14 #[error("The heat-death of the universe happens before this date")]
15 InvalidDate(#[from] std::num::TryFromIntError),
16 #[error("Current time is missing but required to handle relative dates.")]
17 MissingCurrentTime,
18}
19
20#[derive(Default, Clone)]
23pub struct TimeBuf {
24 buf: SmallVec<[u8; Time::MAX.size()]>,
25}
26
27impl TimeBuf {
28 pub fn as_str(&self) -> &str {
31 let time_bytes = self.buf.as_slice();
34 #[allow(unsafe_code)]
35 unsafe {
36 std::str::from_utf8_unchecked(time_bytes)
37 }
38 }
39
40 fn clear(&mut self) {
42 self.buf.clear();
43 }
44}
45
46impl std::io::Write for TimeBuf {
47 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
48 self.buf.write(buf)
49 }
50
51 fn flush(&mut self) -> std::io::Result<()> {
52 self.buf.flush()
53 }
54}
55
56impl Time {
57 pub fn to_str<'a>(&self, buf: &'a mut TimeBuf) -> &'a str {
60 buf.clear();
61 self.write_to(buf)
62 .expect("write to memory of just the right size cannot fail");
63 buf.as_str()
64 }
65}
66
67impl FromStr for Time {
68 type Err = Error;
69
70 fn from_str(s: &str) -> Result<Self, Self::Err> {
71 crate::parse_header(s).ok_or_else(|| Error::InvalidDateString { input: s.into() })
72 }
73}
74
75pub(crate) mod function {
76 use std::{str::FromStr, time::SystemTime};
77
78 use jiff::{civil::Date, fmt::rfc2822, tz::TimeZone, Zoned};
79
80 use crate::{
81 parse::{relative, Error},
82 time::format::{DEFAULT, GITOXIDE, ISO8601, ISO8601_STRICT, SHORT},
83 OffsetInSeconds, SecondsSinceUnixEpoch, Time,
84 };
85
86 pub fn parse(input: &str, now: Option<SystemTime>) -> Result<Time, Error> {
148 Ok(if let Ok(val) = Date::strptime(SHORT.0, input) {
149 let val = val
150 .to_zoned(TimeZone::UTC)
151 .map_err(|_| Error::InvalidDateString { input: input.into() })?;
152 Time::new(val.timestamp().as_second(), val.offset().seconds())
153 } else if let Ok(val) = rfc2822_relaxed(input) {
154 Time::new(val.timestamp().as_second(), val.offset().seconds())
155 } else if let Ok(val) = strptime_relaxed(ISO8601.0, input) {
156 Time::new(val.timestamp().as_second(), val.offset().seconds())
157 } else if let Ok(val) = strptime_relaxed(ISO8601_STRICT.0, input) {
158 Time::new(val.timestamp().as_second(), val.offset().seconds())
159 } else if let Ok(val) = strptime_relaxed(GITOXIDE.0, input) {
160 Time::new(val.timestamp().as_second(), val.offset().seconds())
161 } else if let Ok(val) = strptime_relaxed(DEFAULT.0, input) {
162 Time::new(val.timestamp().as_second(), val.offset().seconds())
163 } else if let Ok(val) = SecondsSinceUnixEpoch::from_str(input) {
164 Time::new(val, 0)
166 } else if let Some(val) = relative::parse(input, now).transpose()? {
167 Time::new(val.timestamp().as_second(), val.offset().seconds())
168 } else if let Some(val) = parse_raw(input) {
169 val
171 } else {
172 return Err(Error::InvalidDateString { input: input.into() });
173 })
174 }
175
176 pub fn parse_header(input: &str) -> Option<Time> {
182 pub enum Sign {
183 Plus,
184 Minus,
185 }
186 fn parse_offset(offset: &str) -> Option<OffsetInSeconds> {
187 if (offset.len() != 5) && (offset.len() != 7) {
188 return None;
189 }
190 let sign = match offset.get(..1)? {
191 "-" => Some(Sign::Minus),
192 "+" => Some(Sign::Plus),
193 _ => None,
194 }?;
195 if offset.as_bytes().get(1).is_some_and(|b| !b.is_ascii_digit()) {
196 return None;
197 }
198 let hours: i32 = offset.get(1..3)?.parse().ok()?;
199 let minutes: i32 = offset.get(3..5)?.parse().ok()?;
200 let offset_seconds: i32 = if offset.len() == 7 {
201 offset.get(5..7)?.parse().ok()?
202 } else {
203 0
204 };
205 let mut offset_in_seconds = hours * 3600 + minutes * 60 + offset_seconds;
206 if matches!(sign, Sign::Minus) {
207 offset_in_seconds *= -1;
208 }
209 Some(offset_in_seconds)
210 }
211
212 if input.contains(':') {
213 return None;
214 }
215 let mut split = input.split_whitespace();
216 let seconds = split.next()?;
217 let seconds = match seconds.parse::<SecondsSinceUnixEpoch>() {
218 Ok(s) => s,
219 Err(_err) => {
220 let first_digits: String = seconds.chars().take_while(char::is_ascii_digit).collect();
222 first_digits.parse().ok()?
223 }
224 };
225 let offset = match split.next() {
226 None => 0,
227 Some(offset) => {
228 if split.next().is_some() {
229 0
230 } else {
231 parse_offset(offset).unwrap_or_default()
232 }
233 }
234 };
235 let time = Time { seconds, offset };
236 Some(time)
237 }
238
239 fn parse_raw(input: &str) -> Option<Time> {
256 let mut split = input.split_whitespace();
257 let seconds = split.next()?.parse::<SecondsSinceUnixEpoch>().ok()?;
258 let offset_str = split.next()?;
259 if split.next().is_some() {
260 return None;
261 }
262 let offset_len = offset_str.len();
263 if offset_len != 5 && offset_len != 7 {
264 return None;
265 }
266 let sign: i32 = match offset_str.get(..1)? {
267 "-" => Some(-1),
268 "+" => Some(1),
269 _ => None,
270 }?;
271 let hours: u8 = offset_str.get(1..3)?.parse().ok()?;
272 let minutes: u8 = offset_str.get(3..5)?.parse().ok()?;
273 let offset_seconds: u8 = if offset_len == 7 {
274 offset_str.get(5..7)?.parse().ok()?
275 } else {
276 0
277 };
278 if hours > 14 || (minutes != 0 && minutes != 15 && minutes != 30 && minutes != 45) || offset_seconds != 0 {
279 return None;
280 }
281 let offset: i32 = sign * ((hours as i32) * 3600 + (minutes as i32) * 60);
282 Time { seconds, offset }.into()
283 }
284
285 fn strptime_relaxed(fmt: &str, input: &str) -> Result<Zoned, jiff::Error> {
290 let mut tm = jiff::fmt::strtime::parse(fmt, input)?;
291 tm.set_weekday(None);
292 tm.to_zoned()
293 }
294
295 fn rfc2822_relaxed(input: &str) -> Result<Zoned, jiff::Error> {
298 static P: rfc2822::DateTimeParser = rfc2822::DateTimeParser::new().relaxed_weekday(true);
299 P.parse_zoned(input)
300 }
301
302 #[cfg(test)]
303 mod tests {
304 use super::*;
305
306 #[test]
307 fn parse_raw_valid() {
308 for (valid, expected_seconds, expected_offset) in [
311 ("12345 +0000", 12345, 0),
312 ("-1234567 +0000", -1234567, 0),
313 ("+1234567 -000000", 1234567, 0),
314 (" +0 -000000 ", 0, 0),
315 ("\t-0\t-0000\t", 0, 0),
316 ("\n-0\r\n-0000\n", 0, 0),
317 ] {
318 assert_eq!(
319 parse_raw(valid),
320 Some(Time {
321 seconds: expected_seconds,
322 offset: expected_offset
323 }),
324 "should succeed: '{valid}'"
325 );
326 }
327 }
328
329 #[test]
330 fn parse_raw_invalid() {
331 for (bad_date_str, message) in [
332 ("123456 !0600", "invalid sign - must be + or -"),
333 ("123456 0600", "missing offset sign"),
334 ("123456 +060", "positive offset too short"),
335 ("123456 -060", "negative offset too short"),
336 ("123456 +06000", "not enough offset seconds"),
337 ("123456 --060", "duplicate offset sign with correct offset length"),
338 ("123456 -+060", "multiple offset signs with correct offset length"),
339 ("123456 --0600", "multiple offset signs, but incorrect offset length"),
340 ("123456 +-06000", "multiple offset signs with correct offset length"),
341 ("123456 +-0600", "multiple offset signs with incorrect offset length"),
342 ("123456 +-060", "multiple offset signs with correct offset length"),
343 ("123456 +10030", "invalid offset length with one 'second' field"),
344 ("123456 06000", "invalid offset length, missing sign"),
345 ("123456 +0600 extra", "extra field past offset"),
346 ("123456 +0600 2005", "extra field past offset that looks like year"),
347 ("123456+0600", "missing space between unix timestamp and offset"),
348 (
349 "123456 + 600",
350 "extra spaces between sign and offset (which also is too short)",
351 ),
352 ("123456 -1500", "negative offset hours out of bounds"),
353 ("123456 +1500", "positive offset hours out of bounds"),
354 ("123456 +6600", "positive offset hours out of bounds"),
355 ("123456 +0660", "invalid offset minutes"),
356 ("123456 +060010", "positive offset seconds is allowed but only if zero"),
357 ("123456 -060010", "negative offset seconds is allowed but only if zero"),
358 ("123456 +0075", "positive offset minutes invalid"),
359 ("++123456 +0000", "duplicate timestamp sign"),
360 ("--123456 +0000", "duplicate timestamp sign"),
361 ("1234567 -+1+1+0", "unsigned offset parsing rejects '+'"),
362 ] {
363 assert!(
364 parse_raw(bad_date_str).is_none(),
365 "should fail: '{bad_date_str}': {message}"
366 );
367 }
368 }
369 }
370}
371
372mod relative {
373 use std::{str::FromStr, time::SystemTime};
374
375 use jiff::{tz::TimeZone, Span, Timestamp, Zoned};
376
377 use crate::parse::Error;
378
379 fn parse_inner(input: &str) -> Option<Result<Span, Error>> {
380 let mut split = input.split_whitespace();
381 let units = i64::from_str(split.next()?).ok()?;
382 let period = split.next()?;
383 if split.next()? != "ago" {
384 return None;
385 }
386 span(period, units)
387 }
388
389 pub(crate) fn parse(input: &str, now: Option<SystemTime>) -> Option<Result<Zoned, Error>> {
390 parse_inner(input).map(|result| {
391 let span = result?;
392 if span.is_negative() {
397 return Err(Error::RelativeTimeConversion);
398 }
399 now.ok_or(Error::MissingCurrentTime).and_then(|now| {
400 let ts = Timestamp::try_from(now).map_err(|_| Error::RelativeTimeConversion)?;
401 let zdt = ts.to_zoned(TimeZone::UTC);
408 zdt.checked_sub(span).map_err(|_| Error::RelativeTimeConversion)
409 })
410 })
411 }
412
413 fn span(period: &str, units: i64) -> Option<Result<Span, Error>> {
414 let period = period.strip_suffix('s').unwrap_or(period);
415 let result = match period {
416 "second" => Span::new().try_seconds(units),
417 "minute" => Span::new().try_minutes(units),
418 "hour" => Span::new().try_hours(units),
419 "day" => Span::new().try_days(units),
420 "week" => Span::new().try_weeks(units),
421 "month" => Span::new().try_months(units),
422 "year" => Span::new().try_years(units),
423 _anything => Span::new().try_seconds(units),
425 };
426 Some(result.map_err(|_| Error::RelativeTimeConversion))
427 }
428
429 #[cfg(test)]
430 mod tests {
431 use super::*;
432
433 #[test]
434 fn two_weeks_ago() {
435 let actual = parse_inner("2 weeks ago").unwrap().unwrap();
436 assert_eq!(actual.fieldwise(), Span::new().weeks(2));
437 }
438 }
439}