Created
September 20, 2025 19:51
-
-
Save sby1ce/bd5cd467982d172dd7d8d52b488a4176 to your computer and use it in GitHub Desktop.
Incomplete RFC3339 parsing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| use chrono::{format::Parsed, DateTime, NaiveDate, NaiveTime, Utc}; | |
| const INVALID: &str = "invalid"; | |
| const TOO_SHORT: &str = "too short"; | |
| struct Numbers { | |
| year: i32, | |
| month: u32, | |
| day: u32, | |
| hour: u32, | |
| minute: u32, | |
| second: u32, | |
| } | |
| fn parse(s: &str) -> Result<Numbers, &'static str> { | |
| #[inline] | |
| fn get_digit<T: From<u8>>(bytes: &[u8], index: usize) -> Result<T, &'static str> { | |
| match bytes.get(index) { | |
| Some(c) if c.is_ascii_digit() => Ok(T::from(c - b'0')), | |
| _ => Err(INVALID), | |
| } | |
| } | |
| let bytes: &[u8] = s.as_bytes(); | |
| // Y4 - M2 - D2 T H2 : m2 : S2 Z | |
| if bytes.len() < 20 { | |
| return Err(TOO_SHORT); | |
| } | |
| let year: i32 = { | |
| let y1: i32 = get_digit(bytes, 0)?; | |
| let y2: i32 = get_digit(bytes, 1)?; | |
| let y3: i32 = get_digit(bytes, 2)?; | |
| let y4: i32 = get_digit(bytes, 3)?; | |
| y1 * 1000 + y2 * 100 + y3 * 10 + y4 | |
| }; | |
| let month: u32 = { | |
| let m1: u32 = get_digit(bytes, 5)?; | |
| let m2: u32 = get_digit(bytes, 6)?; | |
| m1 * 10 + m2 | |
| }; | |
| let day: u32 = { | |
| let d1: u32 = get_digit(bytes, 8)?; | |
| let d2: u32 = get_digit(bytes, 9)?; | |
| d1 * 10 + d2 | |
| }; | |
| let hour: u32 = { | |
| let h1: u32 = get_digit(bytes, 11)?; | |
| let h2: u32 = get_digit(bytes, 12)?; | |
| h1 * 10 + h2 | |
| }; | |
| let minute: u32 = { | |
| let m1: u32 = get_digit(bytes, 14)?; | |
| let m2: u32 = get_digit(bytes, 15)?; | |
| m1 * 10 + m2 | |
| }; | |
| let second: u32 = { | |
| let m1: u32 = get_digit(bytes, 17)?; | |
| let m2: u32 = get_digit(bytes, 18)?; | |
| m1 * 10 + m2 | |
| }; | |
| if bytes.get(4) != Some(&b'-') | |
| || bytes.get(7) != Some(&b'-') | |
| || !matches!(bytes.get(10), Some(&b't' | &b'T' | &b' ')) | |
| || bytes.get(13) != Some(&b':') | |
| || bytes.get(16) != Some(&b':') | |
| { | |
| return Err(INVALID); | |
| } | |
| Ok(Numbers { year, month, day, hour, minute, second }) | |
| } | |
| pub fn using_parsed(s: &str) -> Result<DateTime<Utc>, &'static str> { | |
| let Numbers { year, month, day, hour, minute, second } = parse(s)?; | |
| let mut parsed = Parsed::new(); | |
| parsed.set_year(year.into()).unwrap(); | |
| parsed.set_month(month.into()).unwrap(); | |
| parsed.set_day(day.into()).unwrap(); | |
| parsed.set_hour(hour.into()).unwrap(); | |
| parsed.set_minute(minute.into()).unwrap(); | |
| parsed.set_second(second.into()).unwrap(); | |
| parsed.set_offset(0).unwrap(); | |
| Ok(parsed.to_datetime().unwrap().to_utc()) | |
| } | |
| pub fn using_naive(s: &str) -> Result<DateTime<Utc>, &'static str> { | |
| let Numbers { year, month, day, hour, minute, second } = parse(s)?; | |
| let date = NaiveDate::from_ymd_opt(year, month, day).unwrap(); | |
| let time = NaiveTime::from_hms_opt(hour, minute, second).unwrap(); | |
| Ok(date.and_time(time).and_utc()) | |
| } | |
| #[cfg(test)] | |
| mod test { | |
| use super::*; | |
| #[test] | |
| fn test() { | |
| let datetime = Utc::now().to_rfc3339(); | |
| let parsed = using_parsed(&datetime).unwrap(); | |
| let naive = using_naive(&datetime).unwrap(); | |
| assert_eq!(parsed, naive); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment