Skip to content

Commit

Permalink
Correctly format HTTPDate using time-rs
Browse files Browse the repository at this point in the history
Format strings for time-rs are an original and more complex syntax than
any of the strptime inspired examples that preceeded it.

This syntax must be parsed before it can be used in a formatter, so we
invoke a proc-macro to do that at compile time rather than every time
the formatter is required.

As time-rs requires an MSRV of 1.51, we must also increase our minimum
supported version.
  • Loading branch information
bradfier committed Jan 12, 2022
1 parent 2975190 commit 75ac775
Show file tree
Hide file tree
Showing 3 changed files with 24 additions and 8 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yaml
Expand Up @@ -32,7 +32,7 @@ jobs:
rust:
- stable
- nightly
- 1.48
- 1.51
features:
- default
- ssl
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Expand Up @@ -20,7 +20,7 @@ chunked_transfer = "1"
openssl = { version = "0.10", optional = true }
url = "2"
log = "0.4"
time = { version = "0.3", features = [ "std", "formatting" ] }
time = { version = "0.3", features = [ "std", "formatting", "macros", "parsing" ] }

[dev-dependencies]
rustc-serialize = "0.3"
Expand Down
28 changes: 22 additions & 6 deletions src/common.rs
Expand Up @@ -2,7 +2,7 @@ use ascii::{AsciiStr, AsciiString, FromAsciiError};
use std::cmp::Ordering;
use std::fmt::{self, Display, Formatter};
use std::str::FromStr;
use time::{format_description, OffsetDateTime};
use time::OffsetDateTime;

/// Status code of a request or response.
#[derive(Eq, PartialEq, Copy, Clone, Debug, Ord, PartialOrd)]
Expand Down Expand Up @@ -394,7 +394,7 @@ impl From<(u8, u8)> for HTTPVersion {
}
}

/// Represents the current date, expressed in RFC 1123 format, e.g. Sun, 06 Nov 1994 08:49:37 GMT
/// Represents the current date, expressed in RFC 7231 (IMF-fixdate) format, e.g. Sun, 06 Nov 1994 08:49:37 GMT
#[allow(clippy::upper_case_acronyms)]
pub struct HTTPDate {
d: OffsetDateTime,
Expand All @@ -408,20 +408,27 @@ impl HTTPDate {
}
}

/// Format description for emitting a [`time::PrimitiveDateTime`] or [`time::OffsetDateTime`] in the format required by RFC7231
///
/// `format_description!` is a procedural macro, but since `time-rs` doesn't provide any other way
/// to construct the format description slice in a `const` context we haven't much choice.
const IMF_FIXDATE_FORMAT: &[time::format_description::FormatItem<'static>] = time::macros::format_description!(
"[weekday repr:short], [day padding:zero] [month repr:short] [year repr:full] [hour repr:24 padding:zero]:[minute padding:zero]:[second padding:zero] GMT"
);

impl ToString for HTTPDate {
fn to_string(&self) -> String {
// Note: This can probably be made Self::format however parse is not a const function, making this difficult.
let format = format_description::parse("%a, %e %b %Y %H:%M:%S GMT")
.expect("Cannot fail. The format string is correct.");
self.d
.format(&format)
.format(&IMF_FIXDATE_FORMAT)
.expect("Cannot fail with this format under any reasonable conditions.")
}
}

#[cfg(test)]
mod test {
use super::Header;
use crate::common::HTTPDate;
use time::OffsetDateTime;

#[test]
fn test_parse_header() {
Expand All @@ -433,6 +440,15 @@ mod test {
assert!("hello world".parse::<Header>().is_err());
}

#[test]
fn formats_date_correctly() {
let http_date = HTTPDate {
d: OffsetDateTime::from_unix_timestamp(420895020).unwrap(),
};

assert_eq!(http_date.to_string(), "Wed, 04 May 1983 11:17:00 GMT")
}

#[test]
fn test_parse_header_with_doublecolon() {
let header: Header = "Time: 20: 34".parse().unwrap();
Expand Down

0 comments on commit 75ac775

Please sign in to comment.