├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md ├── benches └── benchmarks.rs ├── fuzz ├── .gitignore ├── Cargo.toml └── fuzz_targets │ ├── fuzz_conversion.rs │ └── fuzz_parse.rs └── src ├── date.rs └── lib.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: Continuous integration 4 | 5 | jobs: 6 | check-test: 7 | name: Check and test crate 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: dtolnay/rust-toolchain@stable 12 | - run: cargo check --all-targets 13 | - run: cargo test 14 | 15 | clippy-fmt: 16 | name: Run Clippy and format code 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | - uses: dtolnay/rust-toolchain@stable 21 | with: 22 | components: clippy, rustfmt 23 | - run: cargo clippy --all-targets -- -D warnings 24 | - run: cargo fmt --all --check 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Cargo.lock 3 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "httpdate" 3 | version = "1.0.3" 4 | authors = ["Pyfisch "] 5 | license = "MIT OR Apache-2.0" 6 | description = "HTTP date parsing and formatting" 7 | keywords = ["http", "date", "time", "simple", "timestamp"] 8 | readme = "README.md" 9 | repository = "https://github.com/pyfisch/httpdate" 10 | edition = "2021" 11 | rust-version = "1.56" 12 | 13 | [dev-dependencies] 14 | criterion = "0.5" 15 | 16 | [[bench]] 17 | name = "benchmarks" 18 | harness = false 19 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Pyfisch 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Date and time utils for HTTP. 2 | 3 | [![Build Status](https://github.com/pyfisch/httpdate/actions/workflows/ci.yml/badge.svg)](https://github.com/pyfisch/httpdate/actions/workflows/ci.yml) 4 | [![Crates.io](https://img.shields.io/crates/v/httpdate.svg)](https://crates.io/crates/httpdate) 5 | [![Documentation](https://docs.rs/httpdate/badge.svg)](https://docs.rs/httpdate) 6 | 7 | Multiple HTTP header fields store timestamps. 8 | For example a response created on May 15, 2015 may contain the header 9 | `Date: Fri, 15 May 2015 15:34:21 GMT`. Since the timestamp does not 10 | contain any timezone or leap second information it is equvivalent to 11 | writing 1431696861 Unix time. Rust’s `SystemTime` is used to store 12 | these timestamps. 13 | 14 | This crate provides two public functions: 15 | 16 | * `parse_http_date` to parse a HTTP datetime string to a system time 17 | * `fmt_http_date` to format a system time to a IMF-fixdate 18 | 19 | In addition it exposes the `HttpDate` type that can be used to parse 20 | and format timestamps. Convert a sytem time to `HttpDate` and vice versa. 21 | The `HttpDate` (8 bytes) is smaller than `SystemTime` (16 bytes) and 22 | using the display impl avoids a temporary allocation. 23 | 24 | Read the [blog post](https://pyfisch.org/blog/http-datetime-handling/) to learn 25 | more. 26 | 27 | Fuzz it by installing *cargo-fuzz* and running `cargo fuzz run fuzz_target_1`. 28 | -------------------------------------------------------------------------------- /benches/benchmarks.rs: -------------------------------------------------------------------------------- 1 | use criterion::{black_box, criterion_group, criterion_main, Criterion}; 2 | 3 | pub fn parse_imf_fixdate(c: &mut Criterion) { 4 | c.bench_function("parse_imf_fixdate", |b| { 5 | b.iter(|| { 6 | let d = black_box("Sun, 06 Nov 1994 08:49:37 GMT"); 7 | black_box(httpdate::parse_http_date(d)).unwrap(); 8 | }) 9 | }); 10 | } 11 | 12 | pub fn parse_rfc850_date(c: &mut Criterion) { 13 | c.bench_function("parse_rfc850_date", |b| { 14 | b.iter(|| { 15 | let d = black_box("Sunday, 06-Nov-94 08:49:37 GMT"); 16 | black_box(httpdate::parse_http_date(d)).unwrap(); 17 | }) 18 | }); 19 | } 20 | 21 | pub fn parse_asctime(c: &mut Criterion) { 22 | c.bench_function("parse_asctime", |b| { 23 | b.iter(|| { 24 | let d = black_box("Sun Nov 6 08:49:37 1994"); 25 | black_box(httpdate::parse_http_date(d)).unwrap(); 26 | }) 27 | }); 28 | } 29 | 30 | struct BlackBoxWrite; 31 | 32 | impl std::fmt::Write for BlackBoxWrite { 33 | fn write_str(&mut self, s: &str) -> Result<(), std::fmt::Error> { 34 | black_box(s); 35 | Ok(()) 36 | } 37 | } 38 | 39 | pub fn encode_date(c: &mut Criterion) { 40 | c.bench_function("encode_date", |b| { 41 | let d = "Wed, 21 Oct 2015 07:28:00 GMT"; 42 | black_box(httpdate::parse_http_date(d)).unwrap(); 43 | b.iter(|| { 44 | use std::fmt::Write; 45 | let _ = write!(BlackBoxWrite, "{}", d); 46 | }) 47 | }); 48 | } 49 | 50 | criterion_group!( 51 | benches, 52 | parse_imf_fixdate, 53 | parse_rfc850_date, 54 | parse_asctime, 55 | encode_date 56 | ); 57 | criterion_main!(benches); 58 | -------------------------------------------------------------------------------- /fuzz/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | target 3 | corpus 4 | artifacts 5 | -------------------------------------------------------------------------------- /fuzz/Cargo.toml: -------------------------------------------------------------------------------- 1 | 2 | [package] 3 | name = "httpdate-fuzz" 4 | version = "0.0.1" 5 | authors = ["Automatically generated"] 6 | publish = false 7 | 8 | [package.metadata] 9 | cargo-fuzz = true 10 | 11 | [dependencies.httpdate] 12 | path = ".." 13 | [dependencies.libfuzzer-sys] 14 | git = "https://github.com/rust-fuzz/libfuzzer-sys.git" 15 | 16 | # Prevent this from interfering with workspaces 17 | [workspace] 18 | members = ["."] 19 | 20 | [[bin]] 21 | name = "fuzz_parse" 22 | path = "fuzz_targets/fuzz_parse.rs" 23 | 24 | [[bin]] 25 | name = "fuzz_conversion" 26 | path = "fuzz_targets/fuzz_conversion.rs" 27 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/fuzz_conversion.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | 3 | #[macro_use] extern crate libfuzzer_sys; 4 | extern crate httpdate; 5 | 6 | use httpdate::HttpDate; 7 | use std::time::{SystemTime, UNIX_EPOCH}; 8 | use std::convert::TryInto; 9 | 10 | fuzz_target!(|data: &[u8]| { 11 | // Skip this round if data is not enough 12 | if data.len() < 8 { 13 | return; 14 | } 15 | 16 | // Create system time object 17 | let secs_since_epoch = u64::from_le_bytes(data[0..8].try_into().unwrap_or_default()); 18 | let duration = std::time::Duration::from_secs(secs_since_epoch); 19 | let system_time = match UNIX_EPOCH.checked_add(duration) { 20 | Some(time) => time, 21 | None => return, 22 | }; 23 | 24 | // Skip value that could make HttpDate panic 25 | if secs_since_epoch >= 253402300800 { 26 | return; 27 | } 28 | 29 | // Fuzz other functions 30 | let http_date = HttpDate::from(system_time); 31 | let _ = SystemTime::from(http_date); 32 | let _ = http_date.to_string(); 33 | 34 | // Fuzz partial_cmp if enough data is left 35 | if data.len() >= 16 { 36 | let other_secs_since_epoch = u64::from_le_bytes(data[8..16].try_into().unwrap_or_default()); 37 | let other_duration = std::time::Duration::from_secs(other_secs_since_epoch); 38 | let other_system_time = match UNIX_EPOCH.checked_add(other_duration) { 39 | Some(time) => time, 40 | None => return, 41 | }; 42 | 43 | // Skip value that could make HttpDate panic 44 | if other_secs_since_epoch >= 253402300800 { 45 | return; 46 | } 47 | 48 | let other_http_date = HttpDate::from(other_system_time); 49 | let _ = http_date.partial_cmp(&other_http_date); 50 | } 51 | }); 52 | -------------------------------------------------------------------------------- /fuzz/fuzz_targets/fuzz_parse.rs: -------------------------------------------------------------------------------- 1 | #![no_main] 2 | #[macro_use] extern crate libfuzzer_sys; 3 | extern crate httpdate; 4 | 5 | use std::str; 6 | 7 | use httpdate::{parse_http_date, fmt_http_date}; 8 | 9 | fuzz_target!(|data: &[u8]| { 10 | if let Ok(s) = str::from_utf8(data) { 11 | if let Ok(d) = parse_http_date(s) { 12 | let o = fmt_http_date(d); 13 | assert!(!o.is_empty()); 14 | assert_eq!(parse_http_date(&o).expect("formatting to round trip"), d); 15 | } 16 | } 17 | }); 18 | -------------------------------------------------------------------------------- /src/date.rs: -------------------------------------------------------------------------------- 1 | use std::cmp; 2 | use std::fmt::{self, Display, Formatter}; 3 | use std::str::FromStr; 4 | use std::time::{Duration, SystemTime, UNIX_EPOCH}; 5 | 6 | use crate::Error; 7 | 8 | /// HTTP timestamp type. 9 | /// 10 | /// Parse using `FromStr` impl. 11 | /// Format using the `Display` trait. 12 | /// Convert timestamp into/from `SytemTime` to use. 13 | /// Supports comparsion and sorting. 14 | #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] 15 | pub struct HttpDate { 16 | /// 0...59 17 | sec: u8, 18 | /// 0...59 19 | min: u8, 20 | /// 0...23 21 | hour: u8, 22 | /// 1...31 23 | day: u8, 24 | /// 1...12 25 | mon: u8, 26 | /// 1970...9999 27 | year: u16, 28 | /// 1...7 29 | wday: u8, 30 | } 31 | 32 | impl HttpDate { 33 | fn is_valid(&self) -> bool { 34 | self.sec < 60 35 | && self.min < 60 36 | && self.hour < 24 37 | && self.day > 0 38 | && self.day < 32 39 | && self.mon > 0 40 | && self.mon <= 12 41 | && self.year >= 1970 42 | && self.year <= 9999 43 | && &HttpDate::from(SystemTime::from(*self)) == self 44 | } 45 | } 46 | 47 | impl From for HttpDate { 48 | fn from(v: SystemTime) -> HttpDate { 49 | let dur = v 50 | .duration_since(UNIX_EPOCH) 51 | .expect("all times should be after the epoch"); 52 | let secs_since_epoch = dur.as_secs(); 53 | 54 | if secs_since_epoch >= 253402300800 { 55 | // year 9999 56 | panic!("date must be before year 9999"); 57 | } 58 | 59 | /* 2000-03-01 (mod 400 year, immediately after feb29 */ 60 | const LEAPOCH: i64 = 11017; 61 | const DAYS_PER_400Y: i64 = 365 * 400 + 97; 62 | const DAYS_PER_100Y: i64 = 365 * 100 + 24; 63 | const DAYS_PER_4Y: i64 = 365 * 4 + 1; 64 | 65 | let days = (secs_since_epoch / 86400) as i64 - LEAPOCH; 66 | let secs_of_day = secs_since_epoch % 86400; 67 | 68 | let mut qc_cycles = days / DAYS_PER_400Y; 69 | let mut remdays = days % DAYS_PER_400Y; 70 | 71 | if remdays < 0 { 72 | remdays += DAYS_PER_400Y; 73 | qc_cycles -= 1; 74 | } 75 | 76 | let mut c_cycles = remdays / DAYS_PER_100Y; 77 | if c_cycles == 4 { 78 | c_cycles -= 1; 79 | } 80 | remdays -= c_cycles * DAYS_PER_100Y; 81 | 82 | let mut q_cycles = remdays / DAYS_PER_4Y; 83 | if q_cycles == 25 { 84 | q_cycles -= 1; 85 | } 86 | remdays -= q_cycles * DAYS_PER_4Y; 87 | 88 | let mut remyears = remdays / 365; 89 | if remyears == 4 { 90 | remyears -= 1; 91 | } 92 | remdays -= remyears * 365; 93 | 94 | let mut year = 2000 + remyears + 4 * q_cycles + 100 * c_cycles + 400 * qc_cycles; 95 | 96 | let months = [31, 30, 31, 30, 31, 31, 30, 31, 30, 31, 31, 29]; 97 | let mut mon = 0; 98 | for mon_len in months.iter() { 99 | mon += 1; 100 | if remdays < *mon_len { 101 | break; 102 | } 103 | remdays -= *mon_len; 104 | } 105 | let mday = remdays + 1; 106 | let mon = if mon + 2 > 12 { 107 | year += 1; 108 | mon - 10 109 | } else { 110 | mon + 2 111 | }; 112 | 113 | let mut wday = (3 + days) % 7; 114 | if wday <= 0 { 115 | wday += 7 116 | }; 117 | 118 | HttpDate { 119 | sec: (secs_of_day % 60) as u8, 120 | min: ((secs_of_day % 3600) / 60) as u8, 121 | hour: (secs_of_day / 3600) as u8, 122 | day: mday as u8, 123 | mon: mon as u8, 124 | year: year as u16, 125 | wday: wday as u8, 126 | } 127 | } 128 | } 129 | 130 | impl From for SystemTime { 131 | fn from(v: HttpDate) -> SystemTime { 132 | let leap_years = 133 | ((v.year - 1) - 1968) / 4 - ((v.year - 1) - 1900) / 100 + ((v.year - 1) - 1600) / 400; 134 | let mut ydays = match v.mon { 135 | 1 => 0, 136 | 2 => 31, 137 | 3 => 59, 138 | 4 => 90, 139 | 5 => 120, 140 | 6 => 151, 141 | 7 => 181, 142 | 8 => 212, 143 | 9 => 243, 144 | 10 => 273, 145 | 11 => 304, 146 | 12 => 334, 147 | _ => unreachable!(), 148 | } + v.day as u64 149 | - 1; 150 | if is_leap_year(v.year) && v.mon > 2 { 151 | ydays += 1; 152 | } 153 | let days = (v.year as u64 - 1970) * 365 + leap_years as u64 + ydays; 154 | UNIX_EPOCH 155 | + Duration::from_secs( 156 | v.sec as u64 + v.min as u64 * 60 + v.hour as u64 * 3600 + days * 86400, 157 | ) 158 | } 159 | } 160 | 161 | impl FromStr for HttpDate { 162 | type Err = Error; 163 | 164 | fn from_str(s: &str) -> Result { 165 | if !s.is_ascii() { 166 | return Err(Error(())); 167 | } 168 | let x = s.trim().as_bytes(); 169 | let date = parse_imf_fixdate(x) 170 | .or_else(|_| parse_rfc850_date(x)) 171 | .or_else(|_| parse_asctime(x))?; 172 | if !date.is_valid() { 173 | return Err(Error(())); 174 | } 175 | Ok(date) 176 | } 177 | } 178 | 179 | impl Display for HttpDate { 180 | fn fmt(&self, f: &mut Formatter) -> fmt::Result { 181 | let wday = match self.wday { 182 | 1 => b"Mon", 183 | 2 => b"Tue", 184 | 3 => b"Wed", 185 | 4 => b"Thu", 186 | 5 => b"Fri", 187 | 6 => b"Sat", 188 | 7 => b"Sun", 189 | _ => unreachable!(), 190 | }; 191 | 192 | let mon = match self.mon { 193 | 1 => b"Jan", 194 | 2 => b"Feb", 195 | 3 => b"Mar", 196 | 4 => b"Apr", 197 | 5 => b"May", 198 | 6 => b"Jun", 199 | 7 => b"Jul", 200 | 8 => b"Aug", 201 | 9 => b"Sep", 202 | 10 => b"Oct", 203 | 11 => b"Nov", 204 | 12 => b"Dec", 205 | _ => unreachable!(), 206 | }; 207 | 208 | let mut buf: [u8; 29] = *b" , 00 0000 00:00:00 GMT"; 209 | buf[0] = wday[0]; 210 | buf[1] = wday[1]; 211 | buf[2] = wday[2]; 212 | buf[5] = b'0' + (self.day / 10); 213 | buf[6] = b'0' + (self.day % 10); 214 | buf[8] = mon[0]; 215 | buf[9] = mon[1]; 216 | buf[10] = mon[2]; 217 | buf[12] = b'0' + (self.year / 1000) as u8; 218 | buf[13] = b'0' + (self.year / 100 % 10) as u8; 219 | buf[14] = b'0' + (self.year / 10 % 10) as u8; 220 | buf[15] = b'0' + (self.year % 10) as u8; 221 | buf[17] = b'0' + (self.hour / 10); 222 | buf[18] = b'0' + (self.hour % 10); 223 | buf[20] = b'0' + (self.min / 10); 224 | buf[21] = b'0' + (self.min % 10); 225 | buf[23] = b'0' + (self.sec / 10); 226 | buf[24] = b'0' + (self.sec % 10); 227 | f.write_str(std::str::from_utf8(&buf[..]).unwrap()) 228 | } 229 | } 230 | 231 | impl Ord for HttpDate { 232 | fn cmp(&self, other: &HttpDate) -> cmp::Ordering { 233 | SystemTime::from(*self).cmp(&SystemTime::from(*other)) 234 | } 235 | } 236 | 237 | impl PartialOrd for HttpDate { 238 | fn partial_cmp(&self, other: &HttpDate) -> Option { 239 | Some(self.cmp(other)) 240 | } 241 | } 242 | 243 | fn toint_1(x: u8) -> Result { 244 | let result = x.wrapping_sub(b'0'); 245 | if result < 10 { 246 | Ok(result) 247 | } else { 248 | Err(Error(())) 249 | } 250 | } 251 | 252 | fn toint_2(s: &[u8]) -> Result { 253 | let high = s[0].wrapping_sub(b'0'); 254 | let low = s[1].wrapping_sub(b'0'); 255 | 256 | if high < 10 && low < 10 { 257 | Ok(high * 10 + low) 258 | } else { 259 | Err(Error(())) 260 | } 261 | } 262 | 263 | #[allow(clippy::many_single_char_names)] 264 | fn toint_4(s: &[u8]) -> Result { 265 | let a = u16::from(s[0].wrapping_sub(b'0')); 266 | let b = u16::from(s[1].wrapping_sub(b'0')); 267 | let c = u16::from(s[2].wrapping_sub(b'0')); 268 | let d = u16::from(s[3].wrapping_sub(b'0')); 269 | 270 | if a < 10 && b < 10 && c < 10 && d < 10 { 271 | Ok(a * 1000 + b * 100 + c * 10 + d) 272 | } else { 273 | Err(Error(())) 274 | } 275 | } 276 | 277 | fn parse_imf_fixdate(s: &[u8]) -> Result { 278 | // Example: `Sun, 06 Nov 1994 08:49:37 GMT` 279 | if s.len() != 29 || &s[25..] != b" GMT" || s[16] != b' ' || s[19] != b':' || s[22] != b':' { 280 | return Err(Error(())); 281 | } 282 | Ok(HttpDate { 283 | sec: toint_2(&s[23..25])?, 284 | min: toint_2(&s[20..22])?, 285 | hour: toint_2(&s[17..19])?, 286 | day: toint_2(&s[5..7])?, 287 | mon: match &s[7..12] { 288 | b" Jan " => 1, 289 | b" Feb " => 2, 290 | b" Mar " => 3, 291 | b" Apr " => 4, 292 | b" May " => 5, 293 | b" Jun " => 6, 294 | b" Jul " => 7, 295 | b" Aug " => 8, 296 | b" Sep " => 9, 297 | b" Oct " => 10, 298 | b" Nov " => 11, 299 | b" Dec " => 12, 300 | _ => return Err(Error(())), 301 | }, 302 | year: toint_4(&s[12..16])?, 303 | wday: match &s[..5] { 304 | b"Mon, " => 1, 305 | b"Tue, " => 2, 306 | b"Wed, " => 3, 307 | b"Thu, " => 4, 308 | b"Fri, " => 5, 309 | b"Sat, " => 6, 310 | b"Sun, " => 7, 311 | _ => return Err(Error(())), 312 | }, 313 | }) 314 | } 315 | 316 | fn parse_rfc850_date(s: &[u8]) -> Result { 317 | // Example: `Sunday, 06-Nov-94 08:49:37 GMT` 318 | if s.len() < 23 { 319 | return Err(Error(())); 320 | } 321 | 322 | fn wday<'a>(s: &'a [u8], wday: u8, name: &'static [u8]) -> Option<(u8, &'a [u8])> { 323 | if &s[0..name.len()] == name { 324 | return Some((wday, &s[name.len()..])); 325 | } 326 | None 327 | } 328 | let (wday, s) = wday(s, 1, b"Monday, ") 329 | .or_else(|| wday(s, 2, b"Tuesday, ")) 330 | .or_else(|| wday(s, 3, b"Wednesday, ")) 331 | .or_else(|| wday(s, 4, b"Thursday, ")) 332 | .or_else(|| wday(s, 5, b"Friday, ")) 333 | .or_else(|| wday(s, 6, b"Saturday, ")) 334 | .or_else(|| wday(s, 7, b"Sunday, ")) 335 | .ok_or(Error(()))?; 336 | if s.len() != 22 || s[12] != b':' || s[15] != b':' || &s[18..22] != b" GMT" { 337 | return Err(Error(())); 338 | } 339 | let mut year = u16::from(toint_2(&s[7..9])?); 340 | if year < 70 { 341 | year += 2000; 342 | } else { 343 | year += 1900; 344 | } 345 | Ok(HttpDate { 346 | sec: toint_2(&s[16..18])?, 347 | min: toint_2(&s[13..15])?, 348 | hour: toint_2(&s[10..12])?, 349 | day: toint_2(&s[0..2])?, 350 | mon: match &s[2..7] { 351 | b"-Jan-" => 1, 352 | b"-Feb-" => 2, 353 | b"-Mar-" => 3, 354 | b"-Apr-" => 4, 355 | b"-May-" => 5, 356 | b"-Jun-" => 6, 357 | b"-Jul-" => 7, 358 | b"-Aug-" => 8, 359 | b"-Sep-" => 9, 360 | b"-Oct-" => 10, 361 | b"-Nov-" => 11, 362 | b"-Dec-" => 12, 363 | _ => return Err(Error(())), 364 | }, 365 | year, 366 | wday, 367 | }) 368 | } 369 | 370 | fn parse_asctime(s: &[u8]) -> Result { 371 | // Example: `Sun Nov 6 08:49:37 1994` 372 | if s.len() != 24 || s[10] != b' ' || s[13] != b':' || s[16] != b':' || s[19] != b' ' { 373 | return Err(Error(())); 374 | } 375 | Ok(HttpDate { 376 | sec: toint_2(&s[17..19])?, 377 | min: toint_2(&s[14..16])?, 378 | hour: toint_2(&s[11..13])?, 379 | day: { 380 | let x = &s[8..10]; 381 | { 382 | if x[0] == b' ' { 383 | toint_1(x[1]) 384 | } else { 385 | toint_2(x) 386 | } 387 | }? 388 | }, 389 | mon: match &s[4..8] { 390 | b"Jan " => 1, 391 | b"Feb " => 2, 392 | b"Mar " => 3, 393 | b"Apr " => 4, 394 | b"May " => 5, 395 | b"Jun " => 6, 396 | b"Jul " => 7, 397 | b"Aug " => 8, 398 | b"Sep " => 9, 399 | b"Oct " => 10, 400 | b"Nov " => 11, 401 | b"Dec " => 12, 402 | _ => return Err(Error(())), 403 | }, 404 | year: toint_4(&s[20..24])?, 405 | wday: match &s[0..4] { 406 | b"Mon " => 1, 407 | b"Tue " => 2, 408 | b"Wed " => 3, 409 | b"Thu " => 4, 410 | b"Fri " => 5, 411 | b"Sat " => 6, 412 | b"Sun " => 7, 413 | _ => return Err(Error(())), 414 | }, 415 | }) 416 | } 417 | 418 | fn is_leap_year(y: u16) -> bool { 419 | y % 4 == 0 && (y % 100 != 0 || y % 400 == 0) 420 | } 421 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Date and time utils for HTTP. 2 | //! 3 | //! Multiple HTTP header fields store timestamps. 4 | //! For example a response created on May 15, 2015 may contain the header 5 | //! `Date: Fri, 15 May 2015 15:34:21 GMT`. Since the timestamp does not 6 | //! contain any timezone or leap second information it is equvivalent to 7 | //! writing 1431696861 Unix time. Rust’s `SystemTime` is used to store 8 | //! these timestamps. 9 | //! 10 | //! This crate provides two public functions: 11 | //! 12 | //! * `parse_http_date` to parse a HTTP datetime string to a system time 13 | //! * `fmt_http_date` to format a system time to a IMF-fixdate 14 | //! 15 | //! In addition it exposes the `HttpDate` type that can be used to parse 16 | //! and format timestamps. Convert a sytem time to `HttpDate` and vice versa. 17 | //! The `HttpDate` (8 bytes) is smaller than `SystemTime` (16 bytes) and 18 | //! using the display impl avoids a temporary allocation. 19 | #![forbid(unsafe_code)] 20 | 21 | use std::error; 22 | use std::fmt::{self, Display, Formatter}; 23 | use std::io; 24 | use std::time::SystemTime; 25 | 26 | pub use date::HttpDate; 27 | 28 | mod date; 29 | 30 | /// An opaque error type for all parsing errors. 31 | #[derive(Debug)] 32 | pub struct Error(()); 33 | 34 | impl error::Error for Error {} 35 | 36 | impl Display for Error { 37 | fn fmt(&self, f: &mut Formatter) -> Result<(), fmt::Error> { 38 | f.write_str("string contains no or an invalid date") 39 | } 40 | } 41 | 42 | impl From for io::Error { 43 | fn from(e: Error) -> io::Error { 44 | io::Error::new(io::ErrorKind::Other, e) 45 | } 46 | } 47 | 48 | /// Parse a date from an HTTP header field. 49 | /// 50 | /// Supports the preferred IMF-fixdate and the legacy RFC 805 and 51 | /// ascdate formats. Two digit years are mapped to dates between 52 | /// 1970 and 2069. 53 | pub fn parse_http_date(s: &str) -> Result { 54 | s.parse::().map(|d| d.into()) 55 | } 56 | 57 | /// Format a date to be used in a HTTP header field. 58 | /// 59 | /// Dates are formatted as IMF-fixdate: `Fri, 15 May 2015 15:34:21 GMT`. 60 | pub fn fmt_http_date(d: SystemTime) -> String { 61 | format!("{}", HttpDate::from(d)) 62 | } 63 | 64 | #[cfg(test)] 65 | mod tests { 66 | use std::str; 67 | use std::time::{Duration, UNIX_EPOCH}; 68 | 69 | use super::{fmt_http_date, parse_http_date, HttpDate}; 70 | 71 | #[test] 72 | fn test_rfc_example() { 73 | let d = UNIX_EPOCH + Duration::from_secs(784111777); 74 | assert_eq!( 75 | d, 76 | parse_http_date("Sun, 06 Nov 1994 08:49:37 GMT").expect("#1") 77 | ); 78 | assert_eq!( 79 | d, 80 | parse_http_date("Sunday, 06-Nov-94 08:49:37 GMT").expect("#2") 81 | ); 82 | assert_eq!(d, parse_http_date("Sun Nov 6 08:49:37 1994").expect("#3")); 83 | } 84 | 85 | #[test] 86 | fn test2() { 87 | let d = UNIX_EPOCH + Duration::from_secs(1475419451); 88 | assert_eq!( 89 | d, 90 | parse_http_date("Sun, 02 Oct 2016 14:44:11 GMT").expect("#1") 91 | ); 92 | assert!(parse_http_date("Sun Nov 10 08:00:00 1000").is_err()); 93 | assert!(parse_http_date("Sun Nov 10 08*00:00 2000").is_err()); 94 | assert!(parse_http_date("Sunday, 06-Nov-94 08+49:37 GMT").is_err()); 95 | } 96 | 97 | #[test] 98 | fn test3() { 99 | let mut d = UNIX_EPOCH; 100 | assert_eq!(d, parse_http_date("Thu, 01 Jan 1970 00:00:00 GMT").unwrap()); 101 | d += Duration::from_secs(3600); 102 | assert_eq!(d, parse_http_date("Thu, 01 Jan 1970 01:00:00 GMT").unwrap()); 103 | d += Duration::from_secs(86400); 104 | assert_eq!(d, parse_http_date("Fri, 02 Jan 1970 01:00:00 GMT").unwrap()); 105 | d += Duration::from_secs(2592000); 106 | assert_eq!(d, parse_http_date("Sun, 01 Feb 1970 01:00:00 GMT").unwrap()); 107 | d += Duration::from_secs(2592000); 108 | assert_eq!(d, parse_http_date("Tue, 03 Mar 1970 01:00:00 GMT").unwrap()); 109 | d += Duration::from_secs(31536005); 110 | assert_eq!(d, parse_http_date("Wed, 03 Mar 1971 01:00:05 GMT").unwrap()); 111 | d += Duration::from_secs(15552000); 112 | assert_eq!(d, parse_http_date("Mon, 30 Aug 1971 01:00:05 GMT").unwrap()); 113 | d += Duration::from_secs(6048000); 114 | assert_eq!(d, parse_http_date("Mon, 08 Nov 1971 01:00:05 GMT").unwrap()); 115 | d += Duration::from_secs(864000000); 116 | assert_eq!(d, parse_http_date("Fri, 26 Mar 1999 01:00:05 GMT").unwrap()); 117 | } 118 | 119 | #[test] 120 | fn test_fmt() { 121 | let d = UNIX_EPOCH; 122 | assert_eq!(fmt_http_date(d), "Thu, 01 Jan 1970 00:00:00 GMT"); 123 | let d = UNIX_EPOCH + Duration::from_secs(1475419451); 124 | assert_eq!(fmt_http_date(d), "Sun, 02 Oct 2016 14:44:11 GMT"); 125 | } 126 | 127 | #[allow(dead_code)] 128 | fn testcase(data: &[u8]) { 129 | if let Ok(s) = str::from_utf8(data) { 130 | println!("{:?}", s); 131 | if let Ok(d) = parse_http_date(s) { 132 | let o = fmt_http_date(d); 133 | assert!(!o.is_empty()); 134 | } 135 | } 136 | } 137 | 138 | #[test] 139 | fn size_of() { 140 | assert_eq!(::std::mem::size_of::(), 8); 141 | } 142 | 143 | #[test] 144 | fn test_date_comparison() { 145 | let a = UNIX_EPOCH + Duration::from_secs(784111777); 146 | let b = a + Duration::from_secs(30); 147 | assert!(a < b); 148 | let a_date: HttpDate = a.into(); 149 | let b_date: HttpDate = b.into(); 150 | assert!(a_date < b_date); 151 | assert_eq!(a_date.cmp(&b_date), ::std::cmp::Ordering::Less) 152 | } 153 | 154 | #[test] 155 | fn test_parse_bad_date() { 156 | // 1994-11-07 is actually a Monday 157 | let parsed = "Sun, 07 Nov 1994 08:48:37 GMT".parse::(); 158 | assert!(parsed.is_err()) 159 | } 160 | } 161 | --------------------------------------------------------------------------------