├── .circleci └── config.yml ├── .gitignore ├── Cargo.toml ├── LICENSE-APACHE ├── LICENSE-MIT ├── README.md └── src ├── impls ├── accept.rs ├── accept_encoding.rs ├── allow.rs ├── auth_scheme.rs ├── authorization.rs ├── content_coding.rs ├── content_encoding.rs ├── content_length.rs ├── content_type.rs ├── credentials.rs ├── host.rs ├── http_date.rs ├── mod.rs ├── proxy_authorization.rs ├── quality.rs ├── retry_after.rs └── token68.rs ├── lib.rs └── util.rs /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | restore_registry: &RESTORE_REGISTRY 2 | restore_cache: 3 | key: registry 4 | save_registry: &SAVE_REGISTRY 5 | save_cache: 6 | key: registry-{{ .BuildNum }} 7 | paths: 8 | - /usr/local/cargo/registry/index 9 | deps_key: &DEPS_KEY 10 | key: deps-{{ checksum "~/rust-version" }}-{{ checksum "Cargo.lock" }} 11 | restore_deps: &RESTORE_DEPS 12 | restore_cache: 13 | <<: *DEPS_KEY 14 | save_deps: &SAVE_DEPS 15 | save_cache: 16 | <<: *DEPS_KEY 17 | paths: 18 | - target 19 | - /usr/local/cargo/registry/cache 20 | 21 | version: 2 22 | jobs: 23 | build: 24 | docker: 25 | - image: rust:1.39.0-slim 26 | environment: 27 | RUSTFLAGS: -D warnings 28 | steps: 29 | - checkout 30 | - *RESTORE_REGISTRY 31 | - run: cargo generate-lockfile 32 | - *SAVE_REGISTRY 33 | - run: rustc --version > ~/rust-version 34 | - *RESTORE_DEPS 35 | - run: cargo test 36 | - *SAVE_DEPS 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target/ 3 | **/*.rs.bk 4 | Cargo.lock 5 | /.vscode/ 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "typed-headers" 3 | version = "0.2.0" 4 | authors = ["Steven Fackler "] 5 | license = "MIT/Apache-2.0" 6 | description = "Typed HTTP header serialization and deserialization." 7 | repository = "https://github.com/sfackler/typed-headers" 8 | readme = "README.md" 9 | edition = "2018" 10 | 11 | [dependencies] 12 | base64 = "0.11" 13 | bytes = "0.5.2" 14 | chrono = "0.4" 15 | http = "0.2" 16 | mime = "0.3" 17 | -------------------------------------------------------------------------------- /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 | 203 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017 The tokio-io-timeout Developers 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 all 11 | 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 THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # typed-headers 2 | 3 | [![CircleCI](https://circleci.com/gh/sfackler/typed-headers.svg?style=shield)](https://circleci.com/gh/sfackler/typed-headers) 4 | 5 | [Documentation](https://docs.rs/typed-headers) 6 | 7 | Typed HTTP header serialization and deserialization. 8 | -------------------------------------------------------------------------------- /src/impls/accept.rs: -------------------------------------------------------------------------------- 1 | use http::header::ACCEPT; 2 | use mime::Mime; 3 | 4 | use super::QualityItem; 5 | 6 | header! { 7 | /// `Accept` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.2) 8 | /// 9 | /// The `Accept` header field can be used by user agents to specify 10 | /// response media types that are acceptable. Accept header fields can 11 | /// be used to indicate that the request is specifically limited to a 12 | /// small set of desired types, as in the case of a request for an 13 | /// in-line image 14 | /// 15 | /// # ABNF 16 | /// 17 | /// ```text 18 | /// Accept = #( media-range [ accept-params ] ) 19 | /// 20 | /// media-range = ( "*/*" 21 | /// / ( type "/" "*" ) 22 | /// / ( type "/" subtype ) 23 | /// ) *( OWS ";" OWS parameter ) 24 | /// accept-params = weight *( accept-ext ) 25 | /// accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ] 26 | /// ``` 27 | /// 28 | /// # Example values 29 | /// * `audio/*; q=0.2, audio/basic` 30 | /// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c` 31 | (Accept, ACCEPT) => (QualityItem)* 32 | } 33 | 34 | #[cfg(test)] 35 | mod test { 36 | use crate::{util, Quality, QualityItem}; 37 | 38 | use super::*; 39 | 40 | #[test] 41 | fn rfc1() { 42 | util::test_round_trip( 43 | &Accept(vec![ 44 | QualityItem::new("audio/*".parse().unwrap(), Quality::from_u16(200)), 45 | QualityItem::new("audio/basic".parse().unwrap(), Quality::from_u16(1000)), 46 | ]), 47 | &["audio/*; q=0.2, audio/basic"], 48 | ); 49 | } 50 | 51 | #[test] 52 | fn rfc2() { 53 | util::test_round_trip( 54 | &Accept(vec![ 55 | QualityItem::new("text/plain".parse().unwrap(), Quality::from_u16(500)), 56 | QualityItem::new("text/html".parse().unwrap(), Quality::from_u16(1000)), 57 | QualityItem::new("text/x-dvi".parse().unwrap(), Quality::from_u16(800)), 58 | QualityItem::new("text/x-c".parse().unwrap(), Quality::from_u16(1000)), 59 | ]), 60 | &["text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"], 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/impls/accept_encoding.rs: -------------------------------------------------------------------------------- 1 | use http::header::ACCEPT_ENCODING; 2 | 3 | use super::{ContentCoding, QualityItem}; 4 | 5 | header! { 6 | /// `Accept-Encoding` header, defined in 7 | /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.4) 8 | /// 9 | /// The `Accept-Encoding` header field can be used by user agents to 10 | /// indicate what response content-codings are 11 | /// acceptable in the response. An `identity` token is used as a synonym 12 | /// for "no encoding" in order to communicate when no encoding is 13 | /// preferred. 14 | /// 15 | /// # ABNF 16 | /// 17 | /// ```text 18 | /// Accept-Encoding = #( codings [ weight ] ) 19 | /// codings = content-coding / "identity" / "*" 20 | /// ``` 21 | /// 22 | /// # Example values 23 | /// * `compress, gzip` 24 | /// * `` 25 | /// * `*` 26 | /// * `compress;q=0.5, gzip;q=1` 27 | /// * `gzip;q=1.0, identity; q=0.5, *;q=0` 28 | (AcceptEncoding, ACCEPT_ENCODING) => (QualityItem)* 29 | } 30 | -------------------------------------------------------------------------------- /src/impls/allow.rs: -------------------------------------------------------------------------------- 1 | use http::header::ALLOW; 2 | 3 | use http::Method; 4 | 5 | header! { 6 | /// `Allow` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.4.1) 7 | /// 8 | /// The `Allow` header field lists the set of methods advertised as 9 | /// supported by the target resource. The purpose of this field is 10 | /// strictly to inform the recipient of valid request methods associated 11 | /// with the resource. 12 | /// 13 | /// # ABNF 14 | /// 15 | /// ```text 16 | /// Allow = #method 17 | /// ``` 18 | /// 19 | /// # Example values 20 | /// * `GET, HEAD, PUT` 21 | /// * `OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH, fOObAr` 22 | /// * `` 23 | (Allow, ALLOW) => (Method)* 24 | } 25 | -------------------------------------------------------------------------------- /src/impls/auth_scheme.rs: -------------------------------------------------------------------------------- 1 | token! { 2 | /// An authorization scheme. 3 | AuthScheme => { 4 | /// Basic authentication, as defined in [RFC7617]. 5 | /// 6 | /// [RFC7617]: https://tools.ietf.org/html/rfc7617 7 | BASIC => "Basic" => [], 8 | /// Bearer authentication, as defined in [RFC6750]. 9 | /// 10 | /// [RFC6750]: https://tools.ietf.org/html/rfc6750 11 | BEARER => "Bearer" => [], 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/impls/authorization.rs: -------------------------------------------------------------------------------- 1 | use http::header::AUTHORIZATION; 2 | 3 | use super::Credentials; 4 | 5 | header! { 6 | /// `Authorization` header, defined in [RFC7235](https://tools.ietf.org/html/rfc7235#section-4.2) 7 | /// 8 | /// The `Authorization` header field allows a user agent to authenticate 9 | /// itself with an origin server -- usually, but not necessarily, after 10 | /// receiving a 401 (Unauthorized) response. Its value consists of 11 | /// credentials containing the authentication information of the user 12 | /// agent for the realm of the resource being requested. 13 | /// 14 | /// # ABNF 15 | /// 16 | /// ```text 17 | /// Authorization = credentials 18 | /// ``` 19 | /// 20 | /// # Example values 21 | /// * `Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==` 22 | /// * `Bearer fpKL54jvWmEGVoRdCNjG` 23 | (Authorization, AUTHORIZATION) => [Credentials] 24 | } 25 | 26 | #[cfg(test)] 27 | mod test { 28 | use super::*; 29 | use crate::{util, Token68}; 30 | 31 | #[test] 32 | fn rfc1() { 33 | util::test_round_trip( 34 | &Authorization(Credentials::basic("Aladdin", "open sesame").unwrap()), 35 | &["Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="], 36 | ); 37 | } 38 | 39 | #[test] 40 | fn rfc2() { 41 | util::test_round_trip( 42 | &Authorization(Credentials::bearer( 43 | Token68::new("fpKL54jvWmEGVoRdCNjG").unwrap(), 44 | )), 45 | &["Bearer fpKL54jvWmEGVoRdCNjG"], 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/impls/content_coding.rs: -------------------------------------------------------------------------------- 1 | token! { 2 | /// A content coding, used in the `Accept-Encoding` and `Content-Encoding` headers. 3 | ContentCoding => { 4 | /// The Brotli coding, as specified in [RFC7932]. 5 | /// 6 | /// [RFC7932]: https://tools.ietf.org/html/rfc7932 7 | BROTLI => "br" => [], 8 | /// The Gzip coding, as specified in [RFC7230]. 9 | /// 10 | /// [RFC7230]: https://tools.ietf.org/html/rfc7230#section-4.2.3 11 | GZIP => "gzip" => ["x-gzip"], 12 | /// The Deflate coding, as specified in [RFC7230]. 13 | /// 14 | /// [RFC7230]: https://tools.ietf.org/html/rfc7230#section-4.2.2 15 | DEFLATE => "deflate" => [], 16 | /// The Compress coding, as specified in [RFC7230]. 17 | /// 18 | /// [RFC7230]: https://tools.ietf.org/html/rfc7230#section-4.2.1 19 | COMPRESS => "compress" => ["x-compress"], 20 | /// The identity coding. 21 | IDENTITY => "identity" => [], 22 | /// A wildcard, used in the `Accept-Encoding` header to indicate that all codings are acceptable. 23 | STAR => "*" => [], 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/impls/content_encoding.rs: -------------------------------------------------------------------------------- 1 | use http::header::CONTENT_ENCODING; 2 | 3 | use super::ContentCoding; 4 | 5 | header! { 6 | /// `Content-Encoding` header, defined in 7 | /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-3.1.2.2) 8 | /// 9 | /// The `Content-Encoding` header field indicates what content codings 10 | /// have been applied to the representation, beyond those inherent in the 11 | /// media type, and thus what decoding mechanisms have to be applied in 12 | /// order to obtain data in the media type referenced by the Content-Type 13 | /// header field. Content-Encoding is primarily used to allow a 14 | /// representation's data to be compressed without losing the identity of 15 | /// its underlying media type. 16 | /// 17 | /// # ABNF 18 | /// 19 | /// ```text 20 | /// Content-Encoding = 1#content-coding 21 | /// ``` 22 | /// 23 | /// # Example values 24 | /// 25 | /// * `gzip` 26 | (ContentEncoding, CONTENT_ENCODING) => (ContentCoding)+ 27 | } 28 | -------------------------------------------------------------------------------- /src/impls/content_length.rs: -------------------------------------------------------------------------------- 1 | use http::header::{self, HeaderName, HeaderValue, CONTENT_LENGTH}; 2 | use std::ops::{Deref, DerefMut}; 3 | 4 | use crate::{util, Error, Header, ToValues}; 5 | 6 | /// `Content-Length` header, defined in 7 | /// [RFC7230](http://tools.ietf.org/html/rfc7230#section-3.3.2) 8 | /// 9 | /// When a message does not have a `Transfer-Encoding` header field, a 10 | /// Content-Length header field can provide the anticipated size, as a 11 | /// decimal number of octets, for a potential payload body. For messages 12 | /// that do include a payload body, the Content-Length field-value 13 | /// provides the framing information necessary for determining where the 14 | /// body (and message) ends. For messages that do not include a payload 15 | /// body, the Content-Length indicates the size of the selected 16 | /// representation. 17 | /// 18 | /// # ABNF 19 | /// 20 | /// ```text 21 | /// Content-Length = 1*DIGIT 22 | /// ``` 23 | /// 24 | /// # Example values 25 | /// 26 | /// * `3495` 27 | #[derive(Clone, Debug, PartialEq)] 28 | pub struct ContentLength(pub u64); 29 | 30 | impl Deref for ContentLength { 31 | type Target = u64; 32 | 33 | #[inline] 34 | fn deref(&self) -> &u64 { 35 | &self.0 36 | } 37 | } 38 | 39 | impl DerefMut for ContentLength { 40 | #[inline] 41 | fn deref_mut(&mut self) -> &mut u64 { 42 | &mut self.0 43 | } 44 | } 45 | 46 | impl Header for ContentLength { 47 | #[inline] 48 | fn name() -> &'static HeaderName { 49 | &CONTENT_LENGTH 50 | } 51 | 52 | // RFC 7230 permits multiple identical copies of Content-Length, and there apparently exist 53 | // implementations that produce that! 54 | // https://github.com/request/request/issues/2091#issuecomment-328715113 55 | #[inline] 56 | fn from_values( 57 | values: &mut header::ValueIter, 58 | ) -> Result, Error> { 59 | let mut length = None; 60 | 61 | for value in values { 62 | let value = value.to_str().map_err(|_| Error::invalid_value())?; 63 | if value.trim().is_empty() { 64 | return Err(Error::invalid_value()); 65 | } 66 | 67 | for elem in value.split(',') { 68 | let elem = elem.trim(); 69 | if elem.is_empty() { 70 | continue; 71 | } 72 | 73 | let elem = elem.parse().map_err(|_| Error::invalid_value())?; 74 | match length { 75 | Some(length) if length != elem => return Err(Error::invalid_value()), 76 | Some(_) => {} 77 | None => length = Some(elem), 78 | } 79 | } 80 | } 81 | 82 | Ok(length.map(ContentLength)) 83 | } 84 | 85 | #[inline] 86 | fn to_values(&self, values: &mut ToValues) { 87 | util::encode_single_value(&self.0, values); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/impls/content_type.rs: -------------------------------------------------------------------------------- 1 | use http::header::CONTENT_TYPE; 2 | 3 | use mime::Mime; 4 | 5 | header! { 6 | /// `Content-Type` header, defined in 7 | /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-3.1.1.5) 8 | /// 9 | /// The `Content-Type` header field indicates the media type of the 10 | /// associated representation: either the representation enclosed in the 11 | /// message payload or the selected representation, as determined by the 12 | /// message semantics. The indicated media type defines both the data 13 | /// format and how that data is intended to be processed by a recipient, 14 | /// within the scope of the received message semantics, after any content 15 | /// codings indicated by Content-Encoding are decoded. 16 | /// 17 | /// # ABNF 18 | /// 19 | /// ```text 20 | /// Content-Type = media-type 21 | /// ``` 22 | /// 23 | /// # Example values 24 | /// 25 | /// * `text/html; charset=utf-8` 26 | /// * `application/json` 27 | (ContentType, CONTENT_TYPE) => [Mime] 28 | } 29 | -------------------------------------------------------------------------------- /src/impls/credentials.rs: -------------------------------------------------------------------------------- 1 | use base64; 2 | use std::fmt; 3 | use std::str::FromStr; 4 | 5 | use crate::Error; 6 | use super::{AuthScheme, Token68}; 7 | 8 | #[derive(Debug, Clone, PartialEq, Eq)] 9 | enum Info { 10 | None, 11 | Token68(Token68), 12 | // AuthParams(Vec<(String, String)>), 13 | } 14 | 15 | /// Authentication credentials, as described in [RFC7235]. 16 | /// 17 | /// [RFC7235]: https://tools.ietf.org/html/rfc7235#section-2.1 18 | #[derive(Debug, Clone, PartialEq, Eq)] 19 | pub struct Credentials { 20 | scheme: AuthScheme, 21 | info: Info, 22 | } 23 | 24 | impl Credentials { 25 | /// Creates credentials from just an auth-scheme. 26 | #[inline] 27 | pub fn from_auth_scheme(scheme: AuthScheme) -> Credentials { 28 | Credentials { 29 | scheme, 30 | info: Info::None, 31 | } 32 | } 33 | 34 | /// Creates credentials from an auth-scheme and token68 data. 35 | #[inline] 36 | pub fn from_token68(scheme: AuthScheme, token: Token68) -> Credentials { 37 | Credentials { 38 | scheme, 39 | info: Info::Token68(token), 40 | } 41 | } 42 | 43 | /// Creates Bearer authentication credentials as described in [RFC6750]. 44 | /// 45 | /// [RFC6750]: https://tools.ietf.org/html/rfc6750 46 | #[inline] 47 | pub fn bearer(token: Token68) -> Credentials { 48 | Credentials::from_token68(AuthScheme::BEARER, token) 49 | } 50 | 51 | /// Creates Basic authentication credentials as described in [RFC7617]. 52 | /// 53 | /// [RFC7671]: https://tools.ietf.org/html/rfc7617 54 | #[inline] 55 | pub fn basic(user_id: &str, password: &str) -> Result { 56 | if user_id.contains(':') || has_ctr(user_id) { 57 | return Err(Error::invalid_value()); 58 | } 59 | 60 | if has_ctr(password) { 61 | return Err(Error::invalid_value()); 62 | } 63 | 64 | let token = format!("{}:{}", user_id, password); 65 | let token = base64::encode(token.as_bytes()); 66 | let token = Token68(token); 67 | 68 | Ok(Credentials::from_token68(AuthScheme::BASIC, token)) 69 | } 70 | 71 | /// Returns the auth-scheme associated with the credentials. 72 | #[inline] 73 | pub fn scheme(&self) -> &AuthScheme { 74 | &self.scheme 75 | } 76 | 77 | /// Returns the token68 value associated with the credentials if present. 78 | #[inline] 79 | pub fn token68(&self) -> Option<&Token68> { 80 | match self.info { 81 | Info::None => None, 82 | Info::Token68(ref token) => Some(token), 83 | } 84 | } 85 | 86 | /// Returns the bearer token if this contains Bearer credentials. 87 | #[inline] 88 | pub fn as_bearer(&self) -> Option<&Token68> { 89 | if self.scheme != AuthScheme::BEARER { 90 | return None; 91 | } 92 | 93 | self.token68() 94 | } 95 | } 96 | 97 | impl fmt::Display for Credentials { 98 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 99 | match self.info { 100 | Info::None => fmt.write_str(self.scheme.as_str()), 101 | Info::Token68(ref token) => write!(fmt, "{} {}", self.scheme, token), 102 | } 103 | } 104 | } 105 | 106 | impl FromStr for Credentials { 107 | type Err = Error; 108 | 109 | fn from_str(s: &str) -> Result { 110 | let mut it = s.splitn(2, ' '); 111 | let auth_scheme = it 112 | .next() 113 | .unwrap() 114 | .parse::() 115 | .map_err(|_| Error::invalid_value())?; 116 | 117 | let info = match it.next() { 118 | Some(info) => info, 119 | None => return Ok(Credentials::from_auth_scheme(auth_scheme)), 120 | }; 121 | 122 | let info = info.trim_start_matches(' '); 123 | 124 | match info.parse::() { 125 | Ok(token) => Ok(Credentials::from_token68(auth_scheme, token)), 126 | // FIXME parse out auth-params 127 | Err(_) => return Err(Error::invalid_value()), 128 | } 129 | } 130 | } 131 | 132 | fn has_ctr(s: &str) -> bool { 133 | s.as_bytes().iter().any(u8::is_ascii_control) 134 | } 135 | -------------------------------------------------------------------------------- /src/impls/host.rs: -------------------------------------------------------------------------------- 1 | use bytes::Bytes; 2 | use http::header::{self, HeaderName, HeaderValue, HOST}; 3 | use http::uri::Authority; 4 | 5 | use crate::{Error, Header, ToValues}; 6 | 7 | /// The `Host` header, defined in [RFC7230]. 8 | /// 9 | /// The "Host" header field in a request provides the host and port 10 | /// information from the target URI, enabling the origin server to 11 | /// distinguish among resources while servicing requests for multiple 12 | /// host names on a single IP address. 13 | /// 14 | /// # ABNF 15 | /// 16 | /// ```text 17 | /// Host = uri-host [ ":" port ] 18 | /// ``` 19 | /// 20 | /// [RFC7230]: https://tools.ietf.org/html/rfc7230#section-5.4 21 | #[derive(Debug, Clone)] 22 | pub struct Host { 23 | host: String, 24 | port: Option, 25 | } 26 | 27 | impl Host { 28 | /// Creates a Host header from a hostname and optional port. 29 | #[inline] 30 | pub fn new(host: &'static str, port: Option) -> Result { 31 | // go through authority to validate the hostname 32 | let authority = match port { 33 | Some(port) => Bytes::from(format!("{}:{}", host, port)), 34 | None => Bytes::from(host), 35 | }; 36 | let authority = Authority::from_maybe_shared(authority).map_err(|_| Error::invalid_value())?; 37 | 38 | Ok(Host::from_authority(&authority)) 39 | } 40 | 41 | /// Creates a Host header from a URI authority component. 42 | /// 43 | /// The userinfo portion of the authority is not included in the header. 44 | #[inline] 45 | pub fn from_authority(authority: &Authority) -> Host { 46 | Host { 47 | host: authority.host().to_string(), 48 | port: authority.port_u16(), 49 | } 50 | } 51 | 52 | /// Returns the host. 53 | #[inline] 54 | pub fn host(&self) -> &str { 55 | &self.host 56 | } 57 | 58 | /// Returns the port. 59 | #[inline] 60 | pub fn port(&self) -> Option { 61 | self.port 62 | } 63 | } 64 | 65 | impl Header for Host { 66 | #[inline] 67 | fn name() -> &'static HeaderName { 68 | &HOST 69 | } 70 | 71 | #[inline] 72 | fn from_values<'a>( 73 | values: &mut header::ValueIter<'a, HeaderValue>, 74 | ) -> Result, Error> { 75 | let value = match values.next() { 76 | Some(value) => value, 77 | None => return Ok(None), 78 | }; 79 | 80 | let authority = Authority::from_maybe_shared(Bytes::copy_from_slice(value.as_bytes())) 81 | .map_err(|_| Error::invalid_value())?; 82 | // host header can't contain userinfo 83 | if authority.as_str().contains('@') { 84 | return Err(Error::invalid_value()); 85 | } 86 | 87 | Ok(Some(Host::from_authority(&authority))) 88 | } 89 | 90 | #[inline] 91 | fn to_values(&self, values: &mut ToValues) { 92 | let value = match self.port { 93 | Some(port) => HeaderValue::from_str(&format!("{}:{}", self.host, port)), 94 | None => HeaderValue::from_str(&self.host), 95 | }; 96 | let value = value.expect("should have already validated contents"); 97 | 98 | values.append(value); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/impls/http_date.rs: -------------------------------------------------------------------------------- 1 | use chrono::{DateTime, NaiveDateTime, Utc}; 2 | use std::fmt; 3 | use std::str::FromStr; 4 | use std::time::SystemTime; 5 | 6 | use crate::Error; 7 | 8 | const IMF_FIXDATE_PATTERN: &'static str = "%a, %d %b %Y %T GMT"; 9 | const RFC850_DATE_PATTERN: &'static str = "%A, %d-%b-%y %T GMT"; 10 | const ASCTIME_DATE_PATTERN: &'static str = "%a %b %e %T %Y"; 11 | 12 | #[derive(Debug, Clone, PartialEq, Eq)] 13 | pub struct HttpDate(DateTime); 14 | 15 | impl From for HttpDate { 16 | #[inline] 17 | fn from(t: SystemTime) -> HttpDate { 18 | HttpDate(DateTime::from(t)) 19 | } 20 | } 21 | 22 | impl From for SystemTime { 23 | #[inline] 24 | fn from(t: HttpDate) -> SystemTime { 25 | SystemTime::from(t.0) 26 | } 27 | } 28 | 29 | impl FromStr for HttpDate { 30 | type Err = Error; 31 | 32 | fn from_str(s: &str) -> Result { 33 | let naive = NaiveDateTime::parse_from_str(s, IMF_FIXDATE_PATTERN) 34 | .or_else(|_| NaiveDateTime::parse_from_str(s, RFC850_DATE_PATTERN)) 35 | .or_else(|_| NaiveDateTime::parse_from_str(s, ASCTIME_DATE_PATTERN)) 36 | .map_err(|_| Error::invalid_value())?; 37 | 38 | Ok(HttpDate(DateTime::from_utc(naive, Utc))) 39 | } 40 | } 41 | 42 | impl fmt::Display for HttpDate { 43 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 44 | fmt::Display::fmt(&self.0.format(IMF_FIXDATE_PATTERN), fmt) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/impls/mod.rs: -------------------------------------------------------------------------------- 1 | pub use self::accept::Accept; 2 | pub use self::accept_encoding::AcceptEncoding; 3 | pub use self::allow::Allow; 4 | pub use self::auth_scheme::AuthScheme; 5 | pub use self::authorization::Authorization; 6 | pub use self::content_coding::ContentCoding; 7 | pub use self::content_encoding::ContentEncoding; 8 | pub use self::content_length::ContentLength; 9 | pub use self::content_type::ContentType; 10 | pub use self::credentials::Credentials; 11 | pub use self::host::Host; 12 | pub use self::http_date::HttpDate; 13 | pub use self::proxy_authorization::ProxyAuthorization; 14 | pub use self::quality::{Quality, QualityItem}; 15 | pub use self::retry_after::RetryAfter; 16 | pub use self::token68::Token68; 17 | 18 | macro_rules! header { 19 | // #rule 20 | ($(#[$a:meta])*($id:ident, $n:expr) => ($item:ty)*) => { 21 | $(#[$a])* 22 | #[derive(Clone, Debug, PartialEq)] 23 | pub struct $id(pub ::std::vec::Vec<$item>); 24 | header!(@deref $id => Vec<$item>); 25 | header!(@derefmut $id => Vec<$item>); 26 | impl $crate::Header for $id { 27 | #[inline] 28 | fn name() -> &'static $crate::http::header::HeaderName { 29 | &$n 30 | } 31 | 32 | #[inline] 33 | fn from_values( 34 | values: &mut $crate::http::header::ValueIter<$crate::http::header::HeaderValue>, 35 | ) -> ::std::result::Result<::std::option::Option<$id>, $crate::Error> 36 | { 37 | $crate::util::parse_comma_delimited(values).map(|r| r.map($id)) 38 | } 39 | 40 | #[inline] 41 | fn to_values(&self, values: &mut $crate::ToValues) { 42 | $crate::util::encode_comma_delimited(&self.0, values); 43 | } 44 | } 45 | }; 46 | // 1#rule 47 | ($(#[$a:meta])*($id:ident, $n:expr) => ($item:ty)+) => { 48 | $(#[$a])* 49 | #[derive(Clone, Debug, PartialEq)] 50 | pub struct $id(::std::vec::Vec<$item>); 51 | header!(@deref $id => Vec<$item>); 52 | 53 | impl $id { 54 | #[inline] 55 | pub fn new(values: Vec<$item>) -> ::std::result::Result<$id, $crate::Error> { 56 | if values.is_empty() { 57 | Err($crate::Error::too_few_values()) 58 | } else { 59 | Ok($id(values)) 60 | } 61 | } 62 | } 63 | 64 | impl $crate::Header for $id { 65 | #[inline] 66 | fn name() -> &'static $crate::http::header::HeaderName { 67 | &$n 68 | } 69 | 70 | #[inline] 71 | fn from_values( 72 | values: &mut $crate::http::header::ValueIter<$crate::http::header::HeaderValue>, 73 | ) -> ::std::result::Result<::std::option::Option<$id>, $crate::Error> 74 | { 75 | match $crate::util::parse_comma_delimited(values)? { 76 | Some(values) => $id::new(values).map(Some), 77 | None => Ok(None), 78 | } 79 | } 80 | 81 | #[inline] 82 | fn to_values(&self, values: &mut $crate::ToValues) { 83 | $crate::util::encode_comma_delimited(&self.0, values); 84 | } 85 | } 86 | 87 | impl ::std::convert::From<$item> for $id { 88 | #[inline] 89 | fn from(value: $item) -> $id { 90 | $id(vec![value]) 91 | } 92 | } 93 | }; 94 | // single value 95 | ($(#[$a:meta])*($id:ident, $n:expr) => [$value:ty]) => { 96 | $(#[$a])* 97 | #[derive(Clone, Debug, PartialEq)] 98 | pub struct $id(pub $value); 99 | header!(@deref $id => $value); 100 | header!(@derefmut $id => $value); 101 | impl $crate::Header for $id { 102 | #[inline] 103 | fn name() -> &'static $crate::http::header::HeaderName { 104 | &$n 105 | } 106 | 107 | #[inline] 108 | fn from_values( 109 | values: &mut $crate::http::header::ValueIter<$crate::http::header::HeaderValue>, 110 | ) -> ::std::result::Result<::std::option::Option<$id>, $crate::Error> 111 | { 112 | $crate::util::parse_single_value(values).map(|r| r.map($id)) 113 | } 114 | 115 | #[inline] 116 | fn to_values(&self, values: &mut $crate::ToValues) { 117 | $crate::util::encode_single_value(&self.0, values); 118 | } 119 | } 120 | }; 121 | (@deref $id:ident => $t:ty) => { 122 | impl ::std::ops::Deref for $id { 123 | type Target = $t; 124 | 125 | #[inline] 126 | fn deref(&self) -> &$t { 127 | &self.0 128 | } 129 | } 130 | }; 131 | (@derefmut $id:ident => $t:ty) => { 132 | impl ::std::ops::DerefMut for $id { 133 | #[inline] 134 | fn deref_mut(&mut self) -> &mut $t { 135 | &mut self.0 136 | } 137 | } 138 | }; 139 | } 140 | 141 | macro_rules! token { 142 | ( 143 | $(#[$attr:meta])* $name:ident => { 144 | $( 145 | $(#[$variant_attr:meta])* 146 | $variant:ident => $s:expr => [$($alias:expr),*], 147 | )* 148 | } 149 | ) => { 150 | #[derive(Debug, Clone, PartialEq, Eq)] 151 | #[allow(non_camel_case_types)] 152 | enum Inner { 153 | $( 154 | $variant, 155 | )* 156 | Other(String), 157 | } 158 | 159 | $(#[$attr])* 160 | #[derive(Debug, Clone, PartialEq, Eq)] 161 | pub struct $name(Inner); 162 | 163 | impl $name { 164 | $( 165 | $(#[$variant_attr])* 166 | pub const $variant: $name = $name(Inner::$variant); 167 | )* 168 | 169 | /// Constructs a new instance of this value from a string. 170 | /// 171 | /// An error is returned if the string is not a valid token. 172 | pub fn new(s: &str) -> ::std::result::Result<$name, $crate::Error> { 173 | $( 174 | if s.eq_ignore_ascii_case($s) { 175 | return Ok($name(Inner::$variant)); 176 | } 177 | 178 | $( 179 | if s.eq_ignore_ascii_case($alias) { 180 | return Ok($name(Inner::$variant)); 181 | } 182 | )* 183 | )* 184 | 185 | if $crate::util::is_token(s) { 186 | Ok($name(Inner::Other(s.to_ascii_lowercase()))) 187 | } else { 188 | Err($crate::Error::invalid_value()) 189 | } 190 | } 191 | 192 | /// Returns the string representation of this token. 193 | pub fn as_str(&self) -> &str { 194 | match self.0 { 195 | $( 196 | Inner::$variant => $s, 197 | )* 198 | Inner::Other(ref s) => s, 199 | } 200 | } 201 | } 202 | 203 | impl ::std::fmt::Display for $name { 204 | fn fmt(&self, fmt: &mut ::std::fmt::Formatter) -> ::std::fmt::Result { 205 | fmt.write_str(self.as_str()) 206 | } 207 | } 208 | 209 | impl ::std::str::FromStr for $name { 210 | type Err = $crate::Error; 211 | 212 | fn from_str(s: &str) -> ::std::result::Result<$name, $crate::Error> { 213 | $name::new(s) 214 | } 215 | } 216 | } 217 | } 218 | 219 | mod accept; 220 | mod accept_encoding; 221 | mod allow; 222 | mod auth_scheme; 223 | mod authorization; 224 | mod content_coding; 225 | mod content_encoding; 226 | mod content_length; 227 | mod content_type; 228 | mod credentials; 229 | mod host; 230 | mod http_date; 231 | mod proxy_authorization; 232 | mod quality; 233 | mod retry_after; 234 | mod token68; 235 | -------------------------------------------------------------------------------- /src/impls/proxy_authorization.rs: -------------------------------------------------------------------------------- 1 | use http::header::PROXY_AUTHORIZATION; 2 | 3 | use super::Credentials; 4 | 5 | header! { 6 | /// `Proxy-Authorization` header, defined in [RFC7235](https://tools.ietf.org/html/rfc7235#section-4.4) 7 | /// 8 | /// The `Proxy-Authorization` header field allows a user agent to authenticate 9 | /// itself with an HTTP proxy -- usually, but not necessarily, after 10 | /// receiving a 407 (Proxy Authentication Required) response and the 11 | /// `Proxy-Authenticate` header. Its value consists of credentials containing 12 | /// the authentication information of the user agent for the realm of the 13 | /// resource being requested. 14 | /// 15 | /// # ABNF 16 | /// 17 | /// ```text 18 | /// Authorization = credentials 19 | /// ``` 20 | /// 21 | /// # Example values 22 | /// * `Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==` 23 | /// * `Bearer fpKL54jvWmEGVoRdCNjG` 24 | (ProxyAuthorization, PROXY_AUTHORIZATION) => [Credentials] 25 | } 26 | 27 | #[cfg(test)] 28 | mod test { 29 | use super::*; 30 | use crate::{util, Token68}; 31 | 32 | #[test] 33 | fn rfc1() { 34 | util::test_round_trip( 35 | &ProxyAuthorization(Credentials::basic("Aladdin", "open sesame").unwrap()), 36 | &["Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="], 37 | ); 38 | } 39 | 40 | #[test] 41 | fn rfc2() { 42 | util::test_round_trip( 43 | &ProxyAuthorization(Credentials::bearer( 44 | Token68::new("fpKL54jvWmEGVoRdCNjG").unwrap(), 45 | )), 46 | &["Bearer fpKL54jvWmEGVoRdCNjG"], 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/impls/quality.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::slice; 3 | use std::str::{self, FromStr}; 4 | 5 | /// A value paired with its "quality" as defined in [RFC7231]. 6 | /// 7 | /// Quality items are used in content negotiation headers such as `Accept` and `Accept-Encoding`. 8 | /// 9 | /// [RFC7231]: https://tools.ietf.org/html/rfc7231#section-5.3 10 | #[derive(Debug, Clone, PartialEq)] 11 | pub struct QualityItem { 12 | pub item: T, 13 | pub quality: Quality, 14 | } 15 | 16 | impl QualityItem { 17 | /// Creates a new quality item. 18 | pub fn new(item: T, quality: Quality) -> QualityItem { 19 | QualityItem { item, quality } 20 | } 21 | } 22 | 23 | impl fmt::Display for QualityItem 24 | where 25 | T: fmt::Display, 26 | { 27 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 28 | fmt::Display::fmt(&self.item, fmt)?; 29 | match self.quality.0 { 30 | 1000 => Ok(()), 31 | 0 => fmt.write_str("; q=0"), 32 | mut x => { 33 | fmt.write_str("; q=0.")?; 34 | let mut digits = *b"000"; 35 | digits[2] = (x % 10) as u8 + b'0'; 36 | x /= 10; 37 | digits[1] = (x % 10) as u8 + b'0'; 38 | x /= 10; 39 | digits[0] = (x % 10) as u8 + b'0'; 40 | 41 | let s = unsafe { str::from_utf8_unchecked(&digits[..]) }; 42 | fmt.write_str(s.trim_end_matches('0')) 43 | } 44 | } 45 | } 46 | } 47 | 48 | impl FromStr for QualityItem 49 | where 50 | T: FromStr, 51 | { 52 | type Err = T::Err; 53 | 54 | fn from_str(mut s: &str) -> Result, T::Err> { 55 | let quality = match WeightParser::parse(s) { 56 | Some((remaining, quality)) => { 57 | s = &s[..remaining]; 58 | quality 59 | } 60 | None => Quality(1000), 61 | }; 62 | 63 | let item = s.parse()?; 64 | 65 | Ok(QualityItem { item, quality }) 66 | } 67 | } 68 | 69 | struct WeightParser<'a>(slice::Iter<'a, u8>); 70 | 71 | impl<'a> WeightParser<'a> { 72 | fn parse(s: &'a str) -> Option<(usize, Quality)> { 73 | let mut parser = WeightParser(s.as_bytes().iter()); 74 | let qvalue = parser.qvalue()?; 75 | parser.eat(b'=')?; 76 | parser.eat(b'q').or_else(|| parser.eat(b'Q'))?; 77 | parser.ows(); 78 | parser.eat(b';')?; 79 | parser.ows(); 80 | let remaining = parser.0.as_slice().len(); 81 | Some((remaining, Quality(qvalue))) 82 | } 83 | 84 | fn qvalue(&mut self) -> Option { 85 | let mut qvalue = match self.digit() { 86 | Some(v @ 0) | Some(v @ 1) if self.peek() == Some(b'=') => return Some(v * 1000), 87 | Some(v) => v, 88 | None if self.peek() == Some(b'.') => 0, 89 | None => return None, 90 | }; 91 | 92 | match self.digit() { 93 | Some(digit1) => match self.digit() { 94 | Some(digit2) => qvalue += digit1 * 10 + digit2 * 100, 95 | None => { 96 | qvalue *= 10; 97 | qvalue += digit1 * 100; 98 | } 99 | }, 100 | None => qvalue *= 100, 101 | } 102 | 103 | self.eat(b'.')?; 104 | 105 | match self.peek()? { 106 | b'0' => { 107 | self.next(); 108 | Some(qvalue) 109 | } 110 | b'1' if qvalue == 0 => { 111 | self.next(); 112 | Some(1000) 113 | } 114 | _ => None, 115 | } 116 | } 117 | 118 | fn digit(&mut self) -> Option { 119 | match self.peek()? { 120 | v @ b'0'..=b'9' => { 121 | self.next(); 122 | Some((v - b'0') as u16) 123 | } 124 | _ => None, 125 | } 126 | } 127 | 128 | fn ows(&mut self) { 129 | loop { 130 | match self.peek() { 131 | Some(b' ') | Some(b'\t') => { 132 | self.next(); 133 | } 134 | _ => break, 135 | } 136 | } 137 | } 138 | 139 | fn peek(&self) -> Option { 140 | self.0.clone().next_back().cloned() 141 | } 142 | 143 | fn next(&mut self) -> Option { 144 | self.0.next_back().cloned() 145 | } 146 | 147 | fn eat(&mut self, value: u8) -> Option<()> { 148 | if self.peek() == Some(value) { 149 | self.next(); 150 | Some(()) 151 | } else { 152 | None 153 | } 154 | } 155 | } 156 | 157 | /// A quality value, as specified in [RFC7231]. 158 | /// 159 | /// Quality values are decimal numbers between 0 and 1 (inclusive) with up to 3 fractional digits of precision. 160 | /// 161 | /// [RFC7231]: https://tools.ietf.org/html/rfc7231#section-5.3.1 162 | #[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)] 163 | pub struct Quality(u16); 164 | 165 | impl Quality { 166 | /// Creates a quality value from a value between 0 and 1000 inclusive. 167 | /// 168 | /// This is semantically divided by 1000 to produce a value between 0 and 1. 169 | /// 170 | /// # Panics 171 | /// 172 | /// Panics if the value is greater than 1000. 173 | pub fn from_u16(quality: u16) -> Quality { 174 | assert!(quality <= 1000); 175 | Quality(quality) 176 | } 177 | 178 | /// Returns the quality multiplied by 1000 as an integer. 179 | pub fn as_u16(&self) -> u16 { 180 | self.0 181 | } 182 | } 183 | 184 | #[cfg(test)] 185 | mod test { 186 | use super::*; 187 | use crate::Error; 188 | 189 | #[derive(Debug, Clone, PartialEq)] 190 | struct Item; 191 | 192 | impl fmt::Display for Item { 193 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 194 | fmt.write_str("item") 195 | } 196 | } 197 | 198 | impl FromStr for Item { 199 | type Err = Error; 200 | 201 | fn from_str(s: &str) -> Result { 202 | if s == "item" { 203 | Ok(Item) 204 | } else { 205 | Err(Error::invalid_value()) 206 | } 207 | } 208 | } 209 | 210 | fn qitem(quality: u16) -> QualityItem { 211 | QualityItem { 212 | item: Item, 213 | quality: Quality(quality), 214 | } 215 | } 216 | 217 | #[test] 218 | fn parse_ok() { 219 | assert_eq!(qitem(1000), "item".parse().unwrap()); 220 | assert_eq!(qitem(1000), "item; q=1".parse().unwrap()); 221 | assert_eq!(qitem(1000), "item; Q=1".parse().unwrap()); 222 | assert_eq!(qitem(1000), "item ;q=1".parse().unwrap()); 223 | assert_eq!(qitem(1000), "item; q=1.".parse().unwrap()); 224 | assert_eq!(qitem(1000), "item; q=1.0".parse().unwrap()); 225 | assert_eq!(qitem(1000), "item; q=1.00".parse().unwrap()); 226 | assert_eq!(qitem(1000), "item; q=1.000".parse().unwrap()); 227 | 228 | assert_eq!(qitem(0), "item; q=0".parse().unwrap()); 229 | assert_eq!(qitem(0), "item; q=0.".parse().unwrap()); 230 | assert_eq!(qitem(0), "item; q=0.0".parse().unwrap()); 231 | assert_eq!(qitem(0), "item; q=0.00".parse().unwrap()); 232 | assert_eq!(qitem(0), "item; q=0.000".parse().unwrap()); 233 | 234 | assert_eq!(qitem(100), "item; q=0.1".parse().unwrap()); 235 | assert_eq!(qitem(100), "item; q=0.10".parse().unwrap()); 236 | assert_eq!(qitem(100), "item; q=0.100".parse().unwrap()); 237 | assert_eq!(qitem(120), "item; q=0.12".parse().unwrap()); 238 | assert_eq!(qitem(120), "item; q=0.120".parse().unwrap()); 239 | assert_eq!(qitem(123), "item; q=0.123".parse().unwrap()); 240 | } 241 | 242 | #[test] 243 | fn parse_err() { 244 | assert!("item; q=".parse::>().is_err()); 245 | assert!("item; q=.1".parse::>().is_err()); 246 | assert!("item; q=1.1".parse::>().is_err()); 247 | assert!("item; q=1.01".parse::>().is_err()); 248 | assert!("item; q=1.001".parse::>().is_err()); 249 | assert!("item; q=0.0001".parse::>().is_err()); 250 | } 251 | 252 | #[test] 253 | fn display() { 254 | assert_eq!(qitem(1000).to_string(), "item"); 255 | assert_eq!(qitem(0).to_string(), "item; q=0"); 256 | assert_eq!(qitem(1).to_string(), "item; q=0.001"); 257 | assert_eq!(qitem(10).to_string(), "item; q=0.01"); 258 | assert_eq!(qitem(100).to_string(), "item; q=0.1"); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/impls/retry_after.rs: -------------------------------------------------------------------------------- 1 | use http::header::{HeaderName, HeaderValue, ValueIter, RETRY_AFTER}; 2 | 3 | use crate::{Error, Header, ToValues}; 4 | use super::HttpDate; 5 | 6 | #[derive(Debug, Clone, PartialEq, Eq)] 7 | pub enum RetryAfter { 8 | DelaySeconds(u64), 9 | HttpDate(HttpDate), 10 | } 11 | 12 | impl Header for RetryAfter { 13 | fn name() -> &'static HeaderName { 14 | &RETRY_AFTER 15 | } 16 | 17 | #[inline] 18 | fn from_values<'a>( 19 | values: &mut ValueIter<'a, HeaderValue>, 20 | ) -> Result, Error> { 21 | let value = match values.next() { 22 | Some(value) => value, 23 | None => return Ok(None), 24 | }; 25 | 26 | let value = value.to_str().map_err(|_| Error::invalid_value())?; 27 | 28 | value 29 | .parse::() 30 | .ok() 31 | .map(RetryAfter::DelaySeconds) 32 | .or_else(|| value.parse::().ok().map(RetryAfter::HttpDate)) 33 | .map(Some) 34 | .ok_or_else(|| Error::invalid_value()) 35 | } 36 | 37 | #[inline] 38 | fn to_values(&self, values: &mut ToValues) { 39 | let s = match *self { 40 | RetryAfter::DelaySeconds(delay) => delay.to_string(), 41 | RetryAfter::HttpDate(ref date) => date.to_string(), 42 | }; 43 | let value = HeaderValue::from_str(&s).expect("retry-after should be valid"); 44 | values.append(value); 45 | } 46 | } 47 | 48 | #[cfg(test)] 49 | mod test { 50 | use super::*; 51 | use crate::util; 52 | 53 | #[test] 54 | fn rfc1() { 55 | util::test_round_trip(&RetryAfter::DelaySeconds(120), &["120"]); 56 | } 57 | 58 | #[test] 59 | fn rfc2() { 60 | util::test_round_trip( 61 | &RetryAfter::HttpDate("Fri, 31 Dec 1999 23:59:59 GMT".parse().unwrap()), 62 | &["Fri, 31 Dec 1999 23:59:59 GMT"], 63 | ); 64 | } 65 | 66 | #[test] 67 | fn rfc850() { 68 | util::test_decode( 69 | &["Sunday, 06-Nov-94 08:49:37 GMT"], 70 | &RetryAfter::HttpDate("Sun, 06 Nov 1994 08:49:37 GMT".parse().unwrap()), 71 | ); 72 | } 73 | 74 | #[test] 75 | fn asctime() { 76 | util::test_decode( 77 | &["Sun Nov 6 08:49:37 1994"], 78 | &RetryAfter::HttpDate("Sun, 06 Nov 1994 08:49:37 GMT".parse().unwrap()), 79 | ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/impls/token68.rs: -------------------------------------------------------------------------------- 1 | use std::error::Error; 2 | use std::fmt; 3 | use std::str::FromStr; 4 | 5 | /// A base68 value as defined in [RFC7235]. 6 | /// 7 | /// [RFC7235]: https://tools.ietf.org/html/rfc7235#section-2.1 8 | #[derive(Debug, Clone, PartialEq, Eq)] 9 | pub struct Token68(pub(crate) String); 10 | 11 | impl Token68 { 12 | /// Constructs a new base68 value. 13 | #[inline] 14 | pub fn new(s: &str) -> Result { 15 | let trimmed = s.trim_end_matches('='); 16 | 17 | if trimmed.is_empty() { 18 | return Err(InvalidToken68(())); 19 | } 20 | 21 | let ok = trimmed.as_bytes().iter().all(|b| match b { 22 | b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'-' | b'.' | b'_' | b'~' | b'+' | b'/' => { 23 | true 24 | } 25 | _ => false, 26 | }); 27 | 28 | if ok { 29 | Ok(Token68(s.to_string())) 30 | } else { 31 | Err(InvalidToken68(())) 32 | } 33 | } 34 | 35 | /// Returns the string the value as a string. 36 | #[inline] 37 | pub fn as_str(&self) -> &str { 38 | &self.0 39 | } 40 | } 41 | 42 | impl fmt::Display for Token68 { 43 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 44 | fmt.write_str(self.as_str()) 45 | } 46 | } 47 | 48 | impl FromStr for Token68 { 49 | type Err = InvalidToken68; 50 | 51 | #[inline] 52 | fn from_str(s: &str) -> Result { 53 | Token68::new(s) 54 | } 55 | } 56 | 57 | #[derive(Debug)] 58 | pub struct InvalidToken68(()); 59 | 60 | impl fmt::Display for InvalidToken68 { 61 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 62 | fmt.write_str("invalid base68") 63 | } 64 | } 65 | 66 | impl Error for InvalidToken68 { 67 | fn description(&self) -> &str { 68 | "invalid base68" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Typed HTTP header serialization and deserialization. 2 | //! 3 | //! This crate is still in its early, experimental stages. It currently takes a fairly pedantic view of parsing, and 4 | //! tries to support exactly what's specified in the HTTP RFCs. 5 | //! 6 | //! The `HeaderMapExt` extension trait provides new methods on the `http::HeaderMap` type to insert, retrieve, and 7 | //! remove headers in a typed manner. 8 | #![doc(html_root_url = "https://docs.rs/typed-headers/0.1")] 9 | 10 | extern crate base64; 11 | extern crate bytes; 12 | extern crate chrono; 13 | 14 | pub extern crate http; 15 | pub extern crate mime; 16 | 17 | use http::header::{self, HeaderMap, HeaderName, HeaderValue}; 18 | use std::error; 19 | use std::fmt; 20 | use std::mem; 21 | 22 | pub use impls::*; 23 | 24 | mod impls; 25 | pub mod util; 26 | 27 | pub trait Header { 28 | /// Returns the name of this header. 29 | /// 30 | /// The `http` crate provides constants for all standard header names. Implementations for 31 | /// nonstandard headers can use the `lazy_static` crate to obtain a static reference to a 32 | /// `HeaderName`. 33 | fn name() -> &'static HeaderName; 34 | 35 | /// Parses the header from the raw value bytes. 36 | /// 37 | /// The iterator may be empty, which indicates that the header was not present, and `Ok(None)` 38 | /// should be returned. 39 | /// 40 | /// If the iterator is not exhausted when this function returns, it will be treated as a parse 41 | /// error. 42 | fn from_values<'a>( 43 | values: &mut header::ValueIter<'a, HeaderValue>, 44 | ) -> Result, Error> 45 | where 46 | Self: Sized; 47 | 48 | /// Serializes the header to raw values. 49 | /// 50 | /// Each call to `values.append` adds a header entry. Almost all headers should only append a 51 | /// single value. `Set-Cookie` is a rare exception. 52 | /// 53 | /// This method is infallible. Header implementations should ensure at construction time that 54 | /// they will be able to successfully serialize. 55 | fn to_values(&self, values: &mut ToValues); 56 | } 57 | 58 | #[derive(Debug)] 59 | enum ErrorKind { 60 | InvalidValue, 61 | TooFewValues, 62 | TooManyValues, 63 | } 64 | 65 | /// An error serializing or deserializing a header. 66 | #[derive(Debug)] 67 | pub struct Error(ErrorKind); 68 | 69 | impl Error { 70 | #[inline] 71 | pub fn invalid_value() -> Error { 72 | Error(ErrorKind::InvalidValue) 73 | } 74 | 75 | #[inline] 76 | pub fn too_few_values() -> Error { 77 | Error(ErrorKind::TooFewValues) 78 | } 79 | 80 | #[inline] 81 | pub fn too_many_values() -> Error { 82 | Error(ErrorKind::TooManyValues) 83 | } 84 | } 85 | 86 | impl fmt::Display for Error { 87 | fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { 88 | let s = match self.0 { 89 | ErrorKind::InvalidValue => "invalid header value", 90 | ErrorKind::TooFewValues => "too few header values", 91 | ErrorKind::TooManyValues => "too many header values", 92 | }; 93 | fmt.write_str(s) 94 | } 95 | } 96 | 97 | impl error::Error for Error { 98 | fn description(&self) -> &str { 99 | "header error" 100 | } 101 | } 102 | 103 | enum ToValuesState<'a> { 104 | First(header::Entry<'a, HeaderValue>), 105 | Latter(header::OccupiedEntry<'a, HeaderValue>), 106 | Tmp, 107 | } 108 | 109 | pub struct ToValues<'a>(ToValuesState<'a>); 110 | 111 | impl<'a> ToValues<'a> { 112 | pub fn append(&mut self, value: HeaderValue) { 113 | let entry = match mem::replace(&mut self.0, ToValuesState::Tmp) { 114 | ToValuesState::First(header::Entry::Occupied(mut e)) => { 115 | e.insert(value); 116 | e 117 | } 118 | ToValuesState::First(header::Entry::Vacant(e)) => e.insert_entry(value), 119 | ToValuesState::Latter(mut e) => { 120 | e.append(value); 121 | e 122 | } 123 | ToValuesState::Tmp => unreachable!(), 124 | }; 125 | self.0 = ToValuesState::Latter(entry); 126 | } 127 | } 128 | 129 | /// An extension trait adding typed getters and setters to `HeaderMap`. 130 | pub trait HeaderMapExt { 131 | /// Retrieves the specified header from the map, if present. 132 | fn typed_get(&self) -> Result, Error> 133 | where 134 | H: Header; 135 | 136 | /// Inserts the provided header into the map. 137 | /// 138 | /// This overwrites any existing entries for that header. 139 | fn typed_insert(&mut self, header: &H) 140 | where 141 | H: Header; 142 | 143 | /// Removes and returns the specified header from the map, if present. 144 | /// 145 | /// The header will be removed even if it doesn't successfully parse. 146 | fn typed_remove(&mut self) -> Result, Error> 147 | where 148 | H: Header; 149 | } 150 | 151 | impl HeaderMapExt for HeaderMap { 152 | fn typed_get(&self) -> Result, Error> 153 | where 154 | H: Header, 155 | { 156 | let mut values = self.get_all(H::name()).iter(); 157 | match H::from_values(&mut values) { 158 | Ok(header) => match values.next() { 159 | Some(_) => Err(Error::too_many_values()), 160 | None => Ok(header), 161 | }, 162 | Err(e) => Err(e), 163 | } 164 | } 165 | 166 | fn typed_insert(&mut self, header: &H) 167 | where 168 | H: Header, 169 | { 170 | let entry = self.entry(H::name()); 171 | let mut values = ToValues(ToValuesState::First(entry)); 172 | header.to_values(&mut values); 173 | } 174 | 175 | fn typed_remove(&mut self) -> Result, Error> 176 | where 177 | H: Header, 178 | { 179 | match self.entry(H::name()) { 180 | header::Entry::Occupied(entry) => { 181 | let r = H::from_values(&mut entry.iter()); 182 | entry.remove(); 183 | r 184 | } 185 | header::Entry::Vacant(_) => Ok(None), 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use http::header::{self, HeaderMap, HeaderValue}; 2 | use std::error; 3 | use std::fmt::{self, Write}; 4 | use std::str::FromStr; 5 | 6 | use crate::{Error, Header, HeaderMapExt, ToValues}; 7 | 8 | #[inline] 9 | pub fn is_token(s: &str) -> bool { 10 | if s.is_empty() { 11 | return false; 12 | } 13 | 14 | s.as_bytes().iter().all(|b| match *b { 15 | b'a'..=b'z' 16 | | b'A'..=b'Z' 17 | | b'0'..=b'9' 18 | | b'!' 19 | | b'#' 20 | | b'$' 21 | | b'%' 22 | | b'&' 23 | | b'\'' 24 | | b'*' 25 | | b'+' 26 | | b'-' 27 | | b'.' 28 | | b'^' 29 | | b'_' 30 | | b'`' 31 | | b'|' 32 | | b'~' => true, 33 | _ => false, 34 | }) 35 | } 36 | 37 | pub fn parse_single_value( 38 | values: &mut header::ValueIter, 39 | ) -> Result, Error> 40 | where 41 | T: FromStr, 42 | T::Err: Into>, 43 | { 44 | match values.next() { 45 | Some(value) => { 46 | let value = value 47 | .to_str() 48 | .map_err(|_| Error::invalid_value())? 49 | .trim() 50 | .parse() 51 | .map_err(|_| Error::invalid_value())?; 52 | Ok(Some(value)) 53 | } 54 | None => Ok(None), 55 | } 56 | } 57 | 58 | pub fn encode_single_value(value: &T, values: &mut ToValues) 59 | where 60 | T: fmt::Display, 61 | { 62 | let value = value.to_string(); 63 | let value = HeaderValue::from_str(&value).expect("failed to encode header"); 64 | values.append(value); 65 | } 66 | 67 | pub fn parse_comma_delimited( 68 | values: &mut header::ValueIter, 69 | ) -> Result>, Error> 70 | where 71 | T: FromStr, 72 | T::Err: Into>, 73 | { 74 | let mut out = vec![]; 75 | let mut empty = true; 76 | for value in values { 77 | empty = false; 78 | 79 | let value = value.to_str().map_err(|_| Error::invalid_value())?; 80 | for elem in value.split(',') { 81 | let elem = elem.trim(); 82 | if elem.is_empty() { 83 | continue; 84 | } 85 | 86 | let elem = elem.parse().map_err(|_| Error::invalid_value())?; 87 | out.push(elem); 88 | } 89 | } 90 | 91 | if empty { 92 | Ok(None) 93 | } else { 94 | Ok(Some(out)) 95 | } 96 | } 97 | 98 | pub fn encode_comma_delimited(elements: I, values: &mut ToValues) 99 | where 100 | I: IntoIterator, 101 | I::Item: fmt::Display, 102 | { 103 | let mut out = String::new(); 104 | let mut it = elements.into_iter(); 105 | if let Some(elem) = it.next() { 106 | write!(out, "{}", elem).unwrap(); 107 | 108 | for elem in it { 109 | write!(out, ", {}", elem).unwrap(); 110 | } 111 | } 112 | let value = HeaderValue::from_str(&out).expect("failed to encode header"); 113 | values.append(value); 114 | } 115 | 116 | pub fn test_decode(values: &[&str], expected: &H) 117 | where 118 | H: Header + PartialEq + fmt::Debug, 119 | { 120 | let mut map = HeaderMap::new(); 121 | for value in values { 122 | let value = HeaderValue::from_str(value).unwrap(); 123 | map.append(H::name().clone(), value); 124 | } 125 | 126 | let header = map.typed_get::().unwrap().unwrap(); 127 | assert_eq!(&header, expected); 128 | } 129 | 130 | pub fn test_encode(header: &H, expected: &[&str]) 131 | where 132 | H: Header, 133 | { 134 | let mut map = HeaderMap::new(); 135 | map.typed_insert(header); 136 | 137 | let values = map.get_all(H::name()).iter().collect::>(); 138 | assert_eq!(values.len(), expected.len()); 139 | for (value, expected) in values.iter().zip(expected) { 140 | assert_eq!(value, expected); 141 | } 142 | } 143 | 144 | pub fn test_round_trip(header: &H, expected: &[&str]) 145 | where 146 | H: Header + PartialEq + fmt::Debug, 147 | { 148 | let mut map = HeaderMap::new(); 149 | map.typed_insert(header); 150 | 151 | let values = map.get_all(H::name()).iter().collect::>(); 152 | assert_eq!(values.len(), expected.len()); 153 | for (value, expected) in values.iter().zip(expected) { 154 | assert_eq!(value, expected); 155 | } 156 | 157 | let actual = map.typed_get::().unwrap().unwrap(); 158 | assert_eq!(header, &actual); 159 | } 160 | --------------------------------------------------------------------------------