Skip to content

Commit

Permalink
Encode offset of -00:00
Browse files Browse the repository at this point in the history
  • Loading branch information
pitdicker committed Apr 28, 2023
1 parent f28c6f2 commit d2d5956
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 68 deletions.
2 changes: 1 addition & 1 deletion src/datetime/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -407,7 +407,7 @@ fn test_datetime_rfc2822_and_rfc3339() {
);
assert_eq!(
DateTime::parse_from_rfc2822("Wed, 18 Feb 2015 23:16:09 -0000"),
Ok(FixedOffset::east_opt(0).unwrap().with_ymd_and_hms(2015, 2, 18, 23, 16, 9).unwrap())
Ok(FixedOffset::OFFSET_UNKNOWN.with_ymd_and_hms(2015, 2, 18, 23, 16, 9).unwrap())
);
assert_eq!(
DateTime::parse_from_rfc3339("2015-02-18T23:16:09Z"),
Expand Down
10 changes: 5 additions & 5 deletions src/format/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ use crate::{Month, ParseMonthError, ParseWeekdayError, Weekday};
pub(crate) mod locales;

pub use parse::parse;
pub use parsed::Parsed;
pub use parsed::{Parsed, NO_OFFSET_INFO};
/// L10n locales.
#[cfg(feature = "unstable-locales")]
pub use pure_rust_locales::Locale;
Expand Down Expand Up @@ -714,16 +714,16 @@ fn format_inner(
#[cfg(any(feature = "alloc", feature = "std", test))]
fn write_local_minus_utc(
result: &mut String,
off: FixedOffset,
offset: FixedOffset,
allow_zulu: bool,
colon_type: Colons,
) -> fmt::Result {
let off = off.local_minus_utc();
if allow_zulu && off == 0 {
let off = offset.local_minus_utc();
if allow_zulu && off == 0 && !offset.no_offset_info() {
result.push('Z');
return Ok(());
}
let (sign, off) = if off < 0 { ('-', -off) } else { ('+', off) };
let (sign, off) = if off < 0 || offset.no_offset_info() { ('-', -off) } else { ('+', off) };
result.push(sign);

write_hundreds(result, (off / 3600) as u8)?;
Expand Down
44 changes: 24 additions & 20 deletions src/format/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ use super::scan;
use super::{Fixed, InternalFixed, InternalInternal, Item, Numeric, Pad, Parsed};
use super::{ParseError, ParseErrorKind, ParseResult};
use super::{BAD_FORMAT, INVALID, NOT_ENOUGH, OUT_OF_RANGE, TOO_LONG, TOO_SHORT};
use crate::format::parsed::NO_OFFSET_INFO;
use crate::{DateTime, FixedOffset, Weekday};

fn set_weekday_with_num_days_from_sunday(p: &mut Parsed, v: i64) -> ParseResult<()> {
Expand Down Expand Up @@ -144,10 +145,8 @@ fn parse_rfc2822<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult<(&'a st
}

s = scan::space(s)?; // mandatory
if let Some(offset) = try_consume!(scan::timezone_offset_2822(s)) {
// only set the offset when it is definitely known (i.e. not `-0000`)
parsed.set_offset(i64::from(offset))?;
}
let offset = try_consume!(scan::timezone_offset_2822(s));
parsed.set_offset(i64::from(offset.unwrap_or(NO_OFFSET_INFO)))?;

// optional comments
while let Ok((s_out, ())) = scan::comment_2822(s) {
Expand Down Expand Up @@ -216,10 +215,7 @@ fn parse_rfc3339<'a>(parsed: &mut Parsed, mut s: &'a str) -> ParseResult<(&'a st
}

let offset = try_consume!(scan::timezone_offset_zulu(s, |s| scan::char(s, b':')));
if offset <= -86_400 || offset >= 86_400 {
return Err(OUT_OF_RANGE);
}
parsed.set_offset(i64::from(offset))?;
parsed.set_offset(i64::from(offset.unwrap_or(NO_OFFSET_INFO)))?;

Ok((s, ()))
}
Expand Down Expand Up @@ -428,15 +424,19 @@ where
s.trim_left(),
scan::colon_or_space
));
parsed.set_offset(i64::from(offset)).map_err(|e| (s, e))?;
parsed
.set_offset(i64::from(offset.unwrap_or(NO_OFFSET_INFO)))
.map_err(|e| (s, e))?;
}

&TimezoneOffsetColonZ | &TimezoneOffsetZ => {
let offset = try_consume!(scan::timezone_offset_zulu(
s.trim_left(),
scan::colon_or_space
));
parsed.set_offset(i64::from(offset)).map_err(|e| (s, e))?;
parsed
.set_offset(i64::from(offset.unwrap_or(NO_OFFSET_INFO)))
.map_err(|e| (s, e))?;
}
&Internal(InternalFixed {
val: InternalInternal::TimezoneOffsetPermissive,
Expand All @@ -445,7 +445,9 @@ where
s.trim_left(),
scan::colon_or_space
));
parsed.set_offset(i64::from(offset)).map_err(|e| (s, e))?;
parsed
.set_offset(i64::from(offset.unwrap_or(NO_OFFSET_INFO)))
.map_err(|e| (s, e))?;
}

&RFC2822 => try_consume!(parse_rfc2822(parsed, s)),
Expand Down Expand Up @@ -737,7 +739,7 @@ fn test_parse() {

// fixed: timezone offsets
check!("+00:00", [fix!(TimezoneOffset)]; offset: 0);
check!("-00:00", [fix!(TimezoneOffset)]; offset: 0);
check!("-00:00", [fix!(TimezoneOffset)]; offset: NO_OFFSET_INFO);
check!("+00:01", [fix!(TimezoneOffset)]; offset: 60);
check!("-00:01", [fix!(TimezoneOffset)]; offset: -60);
check!("+00:30", [fix!(TimezoneOffset)]; offset: 30 * 60);
Expand Down Expand Up @@ -819,7 +821,6 @@ fn test_parse() {
#[cfg(test)]
#[test]
fn test_rfc2822() {
use super::NOT_ENOUGH;
use super::*;
use crate::offset::FixedOffset;
use crate::DateTime;
Expand All @@ -843,6 +844,7 @@ fn test_rfc2822() {
("20 Jan 2015 17:35:20 -0800", Ok("Tue, 20 Jan 2015 17:35:20 -0800")), // no day of week
("20 JAN 2015 17:35:20 -0800", Ok("Tue, 20 Jan 2015 17:35:20 -0800")), // upper case month
("Tue, 20 Jan 2015 17:35 -0800", Ok("Tue, 20 Jan 2015 17:35:00 -0800")), // no second
("20 Jan 2015 17:35:20 -0000", Ok("Tue, 20 Jan 2015 17:35:20 -0000")), // -0000 offset
("11 Sep 2001 09:45:00 EST", Ok("Tue, 11 Sep 2001 09:45:00 -0500")),
("30 Feb 2015 17:35:20 -0800", Err(OUT_OF_RANGE)), // bad day of month
("Tue, 20 Jan 2015", Err(TOO_SHORT)), // omitted fields
Expand All @@ -853,7 +855,7 @@ fn test_rfc2822() {
("Tue, 20 Jan 2015 17:35:90 -0800", Err(OUT_OF_RANGE)), // bad second
("Tue, 20 Jan 2015 17:35:20 -0890", Err(OUT_OF_RANGE)), // bad offset
("6 Jun 1944 04:00:00Z", Err(INVALID)), // bad offset (zulu not allowed)
("Tue, 20 Jan 2015 17:35:20 HAS", Err(NOT_ENOUGH)), // bad named time zone
("Tue, 20 Jan 2015 17:35:20 HAS", Err(INVALID)), // bad named time zone
// named timezones that have specific timezone offsets
// see https://www.rfc-editor.org/rfc/rfc2822#section-4.3
("Tue, 20 Jan 2015 17:35:20 GMT", Ok("Tue, 20 Jan 2015 17:35:20 +0000")),
Expand All @@ -868,14 +870,15 @@ fn test_rfc2822() {
("Tue, 20 Jan 2015 17:35:20 PDT", Ok("Tue, 20 Jan 2015 17:35:20 -0700")),
("Tue, 20 Jan 2015 17:35:20 PST", Ok("Tue, 20 Jan 2015 17:35:20 -0800")),
("Tue, 20 Jan 2015 17:35:20 pst", Ok("Tue, 20 Jan 2015 17:35:20 -0800")),
// named single-letter military timezones must fallback to +0000
// Z is the only single-letter military timezones that maps to +0000
("Tue, 20 Jan 2015 17:35:20 Z", Ok("Tue, 20 Jan 2015 17:35:20 +0000")),
("Tue, 20 Jan 2015 17:35:20 A", Ok("Tue, 20 Jan 2015 17:35:20 +0000")),
("Tue, 20 Jan 2015 17:35:20 a", Ok("Tue, 20 Jan 2015 17:35:20 +0000")),
("Tue, 20 Jan 2015 17:35:20 K", Ok("Tue, 20 Jan 2015 17:35:20 +0000")),
("Tue, 20 Jan 2015 17:35:20 k", Ok("Tue, 20 Jan 2015 17:35:20 +0000")),
// other named single-letter military timezones must fallback to -0000
("Tue, 20 Jan 2015 17:35:20 A", Ok("Tue, 20 Jan 2015 17:35:20 -0000")),
("Tue, 20 Jan 2015 17:35:20 a", Ok("Tue, 20 Jan 2015 17:35:20 -0000")),
("Tue, 20 Jan 2015 17:35:20 K", Ok("Tue, 20 Jan 2015 17:35:20 -0000")),
("Tue, 20 Jan 2015 17:35:20 k", Ok("Tue, 20 Jan 2015 17:35:20 -0000")),
// named single-letter timezone "J" is specifically not valid
("Tue, 20 Jan 2015 17:35:20 J", Err(NOT_ENOUGH)),
("Tue, 20 Jan 2015 17:35:20 J", Err(INVALID)),
];

fn rfc2822_to_datetime(date: &str) -> ParseResult<DateTime<FixedOffset>> {
Expand Down Expand Up @@ -958,6 +961,7 @@ fn test_rfc3339() {
let testdates = [
("2015-01-20T17:35:20-08:00", Ok("2015-01-20T17:35:20-08:00")), // normal case
("1944-06-06T04:04:00Z", Ok("1944-06-06T04:04:00+00:00")), // D-day
("2015-01-20T17:35:20-00:00", Ok("2015-01-20T17:35:20-00:00")), // offset -00:00
("2001-09-11T09:45:00-08:00", Ok("2001-09-11T09:45:00-08:00")),
("2015-01-20T17:35:20.001-08:00", Ok("2015-01-20T17:35:20.001-08:00")),
("2015-01-20T17:35:20.000031-08:00", Ok("2015-01-20T17:35:20.000031-08:00")),
Expand Down
23 changes: 18 additions & 5 deletions src/format/parsed.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ pub struct Parsed {
_dummy: (),
}

/// Constant to be used with [`Parsed::set_offset`] to indicate there is an offset of `00:00`, but
/// that is because the relation to local time is unknown (`-00:00` in RFC 3339 and 2822).
pub const NO_OFFSET_INFO: i32 = i32::MIN;

/// Checks if `old` is either empty or has the same value as `new` (i.e. "consistent"),
/// and if it is empty, set `old` to `new` as well.
#[inline]
Expand Down Expand Up @@ -614,7 +618,12 @@ impl Parsed {

/// Returns a parsed fixed time zone offset out of given fields.
pub fn to_fixed_offset(&self) -> ParseResult<FixedOffset> {
self.offset.and_then(FixedOffset::east_opt).ok_or(OUT_OF_RANGE)
let offset = self.offset.ok_or(NOT_ENOUGH)?;
if offset == NO_OFFSET_INFO {
Ok(FixedOffset::OFFSET_UNKNOWN)
} else {
FixedOffset::east_opt(offset).ok_or(OUT_OF_RANGE)
}
}

/// Returns a parsed timezone-aware date and time out of given fields.
Expand All @@ -625,15 +634,19 @@ impl Parsed {
/// Either way those fields have to be consistent to each other.
pub fn to_datetime(&self) -> ParseResult<DateTime<FixedOffset>> {
let offset = self.offset.ok_or(NOT_ENOUGH)?;
let datetime = self.to_naive_datetime_with_offset(offset)?;
let offset = FixedOffset::east_opt(offset).ok_or(OUT_OF_RANGE)?;
let (offset_i32, fixed_offset) = if offset == NO_OFFSET_INFO {
(0, FixedOffset::OFFSET_UNKNOWN)
} else {
(offset, FixedOffset::east_opt(offset).ok_or(OUT_OF_RANGE)?)
};
let datetime = self.to_naive_datetime_with_offset(offset_i32)?;

// this is used to prevent an overflow when calling FixedOffset::from_local_datetime
datetime
.checked_sub_signed(OldDuration::seconds(i64::from(offset.local_minus_utc())))
.checked_sub_signed(OldDuration::seconds(i64::from(offset_i32)))
.ok_or(OUT_OF_RANGE)?;

match offset.from_local_datetime(&datetime) {
match fixed_offset.from_local_datetime(&datetime) {
LocalResult::None => Err(IMPOSSIBLE),
LocalResult::Single(t) => Ok(t),
LocalResult::Ambiguous(..) => Err(NOT_ENOUGH),
Expand Down
50 changes: 27 additions & 23 deletions src/format/scan.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,9 @@ pub(super) fn colon_or_space(s: &str) -> ParseResult<&str> {
///
/// The additional `colon` may be used to parse a mandatory or optional `:`
/// between hours and minutes, and should return either a new suffix or `Err` when parsing fails.
pub(super) fn timezone_offset<F>(s: &str, consume_colon: F) -> ParseResult<(&str, i32)>
///
/// May return `None` which indicates no offset data is available (i.e. `-0000`).
pub(super) fn timezone_offset<F>(s: &str, consume_colon: F) -> ParseResult<(&str, Option<i32>)>
where
F: FnMut(&str) -> ParseResult<&str>,
{
Expand All @@ -218,7 +220,7 @@ fn timezone_offset_internal<F>(
mut s: &str,
mut consume_colon: F,
allow_missing_minutes: bool,
) -> ParseResult<(&str, i32)>
) -> ParseResult<(&str, Option<i32>)>
where
F: FnMut(&str) -> ParseResult<&str>,
{
Expand Down Expand Up @@ -268,22 +270,27 @@ where
};

let seconds = hours * 3600 + minutes * 60;
Ok((s, if negative { -seconds } else { seconds }))

if seconds == 0 && negative {
return Ok((s, None));
}
Ok((s, Some(if negative { -seconds } else { seconds })))
}

/// Same as `timezone_offset` but also allows for `z`/`Z` which is the same as `+00:00`.
pub(super) fn timezone_offset_zulu<F>(s: &str, colon: F) -> ParseResult<(&str, i32)>
/// May return `None` which indicates no offset data is available (i.e. `-0000`).
pub(super) fn timezone_offset_zulu<F>(s: &str, colon: F) -> ParseResult<(&str, Option<i32>)>
where
F: FnMut(&str) -> ParseResult<&str>,
{
let bytes = s.as_bytes();
match bytes.first() {
Some(&b'z') | Some(&b'Z') => Ok((&s[1..], 0)),
Some(&b'z') | Some(&b'Z') => Ok((&s[1..], Some(0))),
Some(&b'u') | Some(&b'U') => {
if bytes.len() >= 3 {
let (b, c) = (bytes[1], bytes[2]);
match (b | 32, c | 32) {
(b't', b'c') => Ok((&s[3..], 0)),
(b't', b'c') => Ok((&s[3..], Some(0))),
_ => Err(INVALID),
}
} else {
Expand All @@ -296,18 +303,18 @@ where

/// Same as `timezone_offset` but also allows for `z`/`Z` which is the same as
/// `+00:00`, and allows missing minutes entirely.
pub(super) fn timezone_offset_permissive<F>(s: &str, colon: F) -> ParseResult<(&str, i32)>
pub(super) fn timezone_offset_permissive<F>(s: &str, colon: F) -> ParseResult<(&str, Option<i32>)>
where
F: FnMut(&str) -> ParseResult<&str>,
{
match s.as_bytes().first() {
Some(&b'z') | Some(&b'Z') => Ok((&s[1..], 0)),
Some(&b'z') | Some(&b'Z') => Ok((&s[1..], Some(0))),
_ => timezone_offset_internal(s, colon, true),
}
}

/// Same as `timezone_offset` but also allows for RFC 2822 legacy timezones.
/// May return `None` which indicates an insufficient offset data (i.e. `-0000`).
/// May return `None` which indicates no offset data is available (i.e. `-0000`).
/// See [RFC 2822 Section 4.3].
///
/// [RFC 2822 Section 4.3]: https://tools.ietf.org/html/rfc2822#section-4.3
Expand All @@ -325,30 +332,27 @@ pub(super) fn timezone_offset_2822(s: &str) -> ParseResult<(&str, Option<i32>)>
let name = &s.as_bytes()[..upto];
let s = &s[upto..];
let offset_hours = |o| Ok((s, Some(o * 3600)));
if equals(name, "gmt") || equals(name, "ut") {
offset_hours(0)
if equals(name, "gmt") || equals(name, "ut") || equals(name, "z") || equals(name, "Z") {
return offset_hours(0);
} else if equals(name, "edt") {
offset_hours(-4)
return offset_hours(-4);
} else if equals(name, "est") || equals(name, "cdt") {
offset_hours(-5)
return offset_hours(-5);
} else if equals(name, "cst") || equals(name, "mdt") {
offset_hours(-6)
return offset_hours(-6);
} else if equals(name, "mst") || equals(name, "pdt") {
offset_hours(-7)
return offset_hours(-7);
} else if equals(name, "pst") {
offset_hours(-8)
return offset_hours(-8);
} else if name.len() == 1 {
match name[0] {
if let b'a'..=b'i' | b'k'..=b'y' | b'A'..=b'I' | b'K'..=b'Y' = name[0] {
// recommended by RFC 2822: consume but treat it as -0000
b'a'..=b'i' | b'k'..=b'z' | b'A'..=b'I' | b'K'..=b'Z' => offset_hours(0),
_ => Ok((s, None)),
return Ok((s, None));
}
} else {
Ok((s, None))
}
Err(INVALID)
} else {
let (s_, offset) = timezone_offset(s, |s| Ok(s))?;
Ok((s_, Some(offset)))
timezone_offset(s, |s| Ok(s))
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -537,7 +537,7 @@ fn test_type_sizes() {
assert_eq!(size_of::<DateTime<Utc>>(), 12);
assert_eq!(size_of::<DateTime<FixedOffset>>(), 16);
assert_eq!(size_of::<DateTime<Local>>(), 16);
assert_eq!(size_of::<Option<DateTime<FixedOffset>>>(), 20);
assert_eq!(size_of::<Option<DateTime<FixedOffset>>>(), 16);

assert_eq!(size_of::<Date<Utc>>(), 4);
assert_eq!(size_of::<Date<FixedOffset>>(), 8);
Expand Down

0 comments on commit d2d5956

Please sign in to comment.