| // Copyright (C) 2021 Scott Lamb <[email protected]> |
| // SPDX-License-Identifier: MIT OR Apache-2.0 |
| |
| //! HTTP authentication. Currently meant for clients; to be extended for servers. |
| //! |
| //! As described in the following documents and specifications: |
| //! |
| //! * [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication). |
| //! * [RFC 7235](https://datatracker.ietf.org/doc/html/rfc7235): |
| //! Hypertext Transfer Protocol (HTTP/1.1): Authentication. |
| //! * [RFC 7617](https://datatracker.ietf.org/doc/html/rfc7617): |
| //! The 'Basic' HTTP Authentication Scheme |
| //! * [RFC 7616](https://datatracker.ietf.org/doc/html/rfc7616): |
| //! HTTP Digest Access Authentication |
| //! |
| //! This framework is primarily used with HTTP, as suggested by the name. It is |
| //! also used by some other protocols such as RTSP. |
| //! |
| //! ## Cargo Features |
| //! |
| //! | feature | default? | description | |
| //! |-----------------|----------|--------------------------------------------------------------| |
| //! | `basic-scheme` | yes | support for the `Basic` auth scheme | |
| //! | `digest-scheme` | yes | support for the `Digest` auth scheme | |
| //! | `http` | no | convenient conversion from `http` crate types, version 0.2 | |
| //! | `http10` | no | convenient conversion from `http` crate types, version 1.0 | |
| //! |
| //! ## Example |
| //! |
| //! In most cases, callers only need to use [`PasswordClient`] and |
| //! [`PasswordParams`] to handle `Basic` and `Digest` authentication schemes. |
| //! |
| #![cfg_attr( |
| any(feature = "http", feature = "http10"), |
| doc = r##" |
| ```rust |
| use std::convert::TryFrom as _; |
| use http_auth::PasswordClient; |
| |
| let WWW_AUTHENTICATE_VAL = "UnsupportedSchemeA, Basic realm=\"foo\", UnsupportedSchemeB"; |
| let mut pw_client = http_auth::PasswordClient::try_from(WWW_AUTHENTICATE_VAL).unwrap(); |
| assert!(matches!(pw_client, http_auth::PasswordClient::Basic(_))); |
| let response = pw_client.respond(&http_auth::PasswordParams { |
| username: "Aladdin", |
| password: "open sesame", |
| uri: "/", |
| method: "GET", |
| body: Some(&[]), |
| }).unwrap(); |
| assert_eq!(response, "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ=="); |
| ``` |
| "## |
| )] |
| //! |
| //! The `http` or `http10` features allow parsing all `WWW-Authenticate` headers within a |
| //! [`http::HeaderMap`] in one call. |
| //! |
| #![cfg_attr( |
| any(feature = "http", feature = "http10"), |
| doc = r##" |
| ```rust |
| # use std::convert::TryFrom as _; |
| use http::header::{HeaderMap, WWW_AUTHENTICATE}; |
| # use http_auth::PasswordClient; |
| |
| let mut headers = HeaderMap::new(); |
| headers.append(WWW_AUTHENTICATE, "UnsupportedSchemeA".parse().unwrap()); |
| headers.append(WWW_AUTHENTICATE, "Basic realm=\"foo\", UnsupportedSchemeB".parse().unwrap()); |
| |
| let mut pw_client = PasswordClient::try_from(headers.get_all(WWW_AUTHENTICATE)).unwrap(); |
| assert!(matches!(pw_client, http_auth::PasswordClient::Basic(_))); |
| ``` |
| "## |
| )] |
| #![cfg_attr(docsrs, feature(doc_cfg))] |
| |
| use std::convert::TryFrom; |
| |
| pub mod parser; |
| |
| #[cfg(feature = "basic-scheme")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))] |
| pub mod basic; |
| |
| #[cfg(feature = "digest-scheme")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))] |
| pub mod digest; |
| |
| mod table; |
| |
| pub use parser::ChallengeParser; |
| |
| #[cfg(feature = "basic-scheme")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))] |
| pub use crate::basic::BasicClient; |
| |
| #[cfg(feature = "digest-scheme")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))] |
| pub use crate::digest::DigestClient; |
| |
| use crate::table::{char_classes, C_ESCAPABLE, C_OWS, C_QDTEXT, C_TCHAR}; |
| |
| #[cfg(feature = "digest-scheme")] |
| use crate::table::C_ATTR; |
| |
| /// Parsed challenge (scheme and body) using references to the original header value. |
| /// Produced by [`crate::parser::ChallengeParser`]. |
| /// |
| /// This is not directly useful for responding to a challenge; it's an |
| /// intermediary for constructing a client that knows how to respond to a specific |
| /// challenge scheme. In most cases, callers should construct a [`PasswordClient`] |
| /// without directly using `ChallengeRef`. |
| /// |
| /// Only supports the param form, not the apocryphal `token68` form, as described |
| /// in [`crate::parser::ChallengeParser`]. |
| #[derive(Clone, Eq, PartialEq)] |
| pub struct ChallengeRef<'i> { |
| /// The scheme name, which should be compared case-insensitively. |
| pub scheme: &'i str, |
| |
| /// Zero or more parameters. |
| /// |
| /// These are represented as a `Vec` of key-value pairs rather than a |
| /// map. Given that the parameters are generally only used once when |
| /// constructing a challenge client and each challenge only supports a few |
| /// parameter types, it's more efficient in terms of CPU usage and code size |
| /// to scan through them directly. |
| pub params: Vec<ChallengeParamRef<'i>>, |
| } |
| |
| impl<'i> ChallengeRef<'i> { |
| pub fn new(scheme: &'i str) -> Self { |
| ChallengeRef { |
| scheme, |
| params: Vec::new(), |
| } |
| } |
| } |
| |
| impl<'i> std::fmt::Debug for ChallengeRef<'i> { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| f.debug_struct("ChallengeRef") |
| .field("scheme", &self.scheme) |
| .field("params", &ParamsPrinter(&self.params)) |
| .finish() |
| } |
| } |
| |
| type ChallengeParamRef<'i> = (&'i str, ParamValue<'i>); |
| |
| struct ParamsPrinter<'i>(&'i [ChallengeParamRef<'i>]); |
| |
| impl<'i> std::fmt::Debug for ParamsPrinter<'i> { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| f.debug_map().entries(self.0.iter().copied()).finish() |
| } |
| } |
| |
| /// Builds a [`PasswordClient`] from the supplied challenges; create via |
| /// [`PasswordClient::builder`]. |
| /// |
| /// Often you can just use [`PasswordClient`]'s [`TryFrom`] implementations |
| /// to convert from a parsed challenge ([`crate::ChallengeRef`]) or |
| /// unparsed challenges (`str`, [`http::header::HeaderValue`], or |
| /// [`http::header::GetAll`]). |
| /// |
| /// The builder allows more flexibility. For example, if you are using a HTTP |
| /// library which is not based on a `http` crate, you might need to create |
| /// a `PasswordClient` from an iterator over multiple `WWW-Authenticate` |
| /// headers. You can feed each to [`PasswordClientBuilder::challenges`]. |
| /// |
| /// Prefers `Digest` over `Basic`, consistent with the [RFC 7235 section |
| /// 2.1](https://datatracker.ietf.org/doc/html/rfc7235#section-2.1) advice |
| /// for a user-agent to pick the most secure auth-scheme it understands. |
| /// |
| /// When there are multiple `Digest` challenges, currently uses the first, |
| /// consistent with the [RFC 7616 section |
| /// 3.7](https://datatracker.ietf.org/doc/html/rfc7616#section-3.7) |
| /// advice to "use the first challenge it supports, unless a local policy |
| /// dictates otherwise". In the future, it may prioritize by algorithm. |
| /// |
| /// Ignores parse errors as long as there's at least one parseable, supported |
| /// challenge. |
| /// |
| /// ## Example |
| /// |
| #[cfg_attr( |
| feature = "digest", |
| doc = r##" |
| ```rust |
| use http_auth::PasswordClient; |
| let client = PasswordClient::builder() |
| .challenges("UnsupportedSchemeA, Basic realm=\"foo\", UnsupportedSchemeB") |
| .challenges("Digest \ |
| realm=\"http-auth@example.org\", \ |
| qop=\"auth, auth-int\", \ |
| algorithm=MD5, \ |
| nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \ |
| opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"") |
| .build() |
| .unwrap(); |
| assert!(matches!(client, PasswordClient::Digest(_))); |
| ``` |
| "## |
| )] |
| #[derive(Default)] |
| pub struct PasswordClientBuilder( |
| /// The current result: |
| /// * `Some(Ok(_))` if there is a suitable client. |
| /// * `Some(Err(_))` if there is no suitable client and has been a parse error. |
| /// * `None` otherwise. |
| Option<Result<PasswordClient, String>>, |
| ); |
| |
| /// An error returned by [`HeaderValue::to_str`]. |
| pub struct ToStrError { |
| _priv: (), |
| } |
| |
| /// A trait for the parts needed from http crate 0.2 or 1.0's `HeaderValue` type. |
| #[cfg(any(feature = "http", feature = "http10"))] |
| pub trait HeaderValue { |
| fn to_str(&self) -> Result<&str, ToStrError>; |
| } |
| |
| #[cfg(feature = "http")] |
| impl HeaderValue for http::HeaderValue { |
| fn to_str(&self) -> Result<&str, ToStrError> { |
| self.to_str().map_err(|_| ToStrError { _priv: () }) |
| } |
| } |
| |
| #[cfg(feature = "http10")] |
| impl HeaderValue for http10::HeaderValue { |
| fn to_str(&self) -> Result<&str, ToStrError> { |
| self.to_str().map_err(|_| ToStrError { _priv: () }) |
| } |
| } |
| |
| impl PasswordClientBuilder { |
| /// Considers all challenges from the given [`http::HeaderValue`] challenge list. |
| #[cfg(any(feature = "http", feature = "http10"))] |
| #[cfg_attr(docsrs, doc(cfg(any(feature = "http", feature = "http10"))))] |
| pub fn header_value<V: HeaderValue>(mut self, value: &V) -> Self { |
| if self.complete() { |
| return self; |
| } |
| |
| match value.to_str() { |
| Ok(v) => self = self.challenges(v), |
| Err(_) if matches!(self.0, None) => self.0 = Some(Err("non-ASCII header value".into())), |
| _ => {} |
| } |
| |
| self |
| } |
| |
| /// Returns true if no more challenges need to be examined. |
| #[cfg(feature = "digest-scheme")] |
| fn complete(&self) -> bool { |
| matches!(self.0, Some(Ok(PasswordClient::Digest(_)))) |
| } |
| |
| /// Returns true if no more challenges need to be examined. |
| #[cfg(not(feature = "digest-scheme"))] |
| fn complete(&self) -> bool { |
| matches!(self.0, Some(Ok(_))) |
| } |
| |
| /// Considers all challenges from the given `&str` challenge list. |
| pub fn challenges(mut self, value: &str) -> Self { |
| let mut parser = ChallengeParser::new(value); |
| while !self.complete() { |
| match parser.next() { |
| Some(Ok(c)) => self = self.challenge(&c), |
| Some(Err(e)) if self.0.is_none() => self.0 = Some(Err(e.to_string())), |
| _ => break, |
| } |
| } |
| self |
| } |
| |
| /// Considers a single challenge. |
| pub fn challenge(mut self, challenge: &ChallengeRef<'_>) -> Self { |
| if self.complete() { |
| return self; |
| } |
| |
| #[cfg(feature = "digest-scheme")] |
| if challenge.scheme.eq_ignore_ascii_case("Digest") { |
| match DigestClient::try_from(challenge) { |
| Ok(c) => self.0 = Some(Ok(PasswordClient::Digest(c))), |
| Err(e) if self.0.is_none() => self.0 = Some(Err(e)), |
| _ => {} |
| } |
| return self; |
| } |
| |
| #[cfg(feature = "basic-scheme")] |
| if challenge.scheme.eq_ignore_ascii_case("Basic") && !matches!(self.0, Some(Ok(_))) { |
| match BasicClient::try_from(challenge) { |
| Ok(c) => self.0 = Some(Ok(PasswordClient::Basic(c))), |
| Err(e) if self.0.is_none() => self.0 = Some(Err(e)), |
| _ => {} |
| } |
| return self; |
| } |
| |
| if self.0.is_none() { |
| self.0 = Some(Err(format!("Unsupported scheme {:?}", challenge.scheme))); |
| } |
| |
| self |
| } |
| |
| /// Returns a new [`PasswordClient`] or fails. |
| pub fn build(self) -> Result<PasswordClient, String> { |
| self.0.unwrap_or_else(|| Err("no challenges given".into())) |
| } |
| } |
| |
| /// Client for responding to a password challenge. |
| /// |
| /// Typically created via [`TryFrom`] implementations for a parsed challenge |
| /// ([`crate::ChallengeRef`]) or unparsed challenges (`str`, |
| /// [`http::header::HeaderValue`], or [`http::header::GetAll`]). See full |
| /// example in the [crate-level documentation](crate). |
| /// |
| /// For more complex scenarios, see [`PasswordClientBuilder`]. |
| #[derive(Debug, Eq, PartialEq)] |
| #[non_exhaustive] |
| pub enum PasswordClient { |
| #[cfg(feature = "basic-scheme")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))] |
| Basic(BasicClient), |
| |
| #[cfg(feature = "digest-scheme")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))] |
| Digest(DigestClient), |
| } |
| |
| /// Tries to create a `PasswordClient` from the single supplied challenge. |
| /// |
| /// This is a convenience wrapper around [`PasswordClientBuilder`]. |
| impl TryFrom<&ChallengeRef<'_>> for PasswordClient { |
| type Error = String; |
| |
| fn try_from(value: &ChallengeRef<'_>) -> Result<Self, Self::Error> { |
| #[cfg(feature = "basic-scheme")] |
| if value.scheme.eq_ignore_ascii_case("Basic") { |
| return Ok(PasswordClient::Basic(BasicClient::try_from(value)?)); |
| } |
| #[cfg(feature = "digest-scheme")] |
| if value.scheme.eq_ignore_ascii_case("Digest") { |
| return Ok(PasswordClient::Digest(DigestClient::try_from(value)?)); |
| } |
| |
| Err(format!("unsupported challenge scheme {:?}", value.scheme)) |
| } |
| } |
| |
| /// Tries to create a `PasswordClient` from the supplied `str` challenge list. |
| /// |
| /// This is a convenience wrapper around [`PasswordClientBuilder`]. |
| impl TryFrom<&str> for PasswordClient { |
| type Error = String; |
| |
| #[inline] |
| fn try_from(value: &str) -> Result<Self, Self::Error> { |
| PasswordClient::builder().challenges(value).build() |
| } |
| } |
| |
| /// Tries to create a `PasswordClient` from the supplied `HeaderValue` challenge list. |
| /// |
| /// This is a convenience wrapper around [`PasswordClientBuilder`]. |
| #[cfg(feature = "http")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "http")))] |
| impl TryFrom<&http::HeaderValue> for PasswordClient { |
| type Error = String; |
| |
| #[inline] |
| fn try_from(value: &http::HeaderValue) -> Result<Self, Self::Error> { |
| PasswordClient::builder().header_value(value).build() |
| } |
| } |
| |
| /// Tries to create a `PasswordClient` from the supplied `HeaderValue` challenge list. |
| /// |
| /// This is a convenience wrapper around [`PasswordClientBuilder`]. |
| #[cfg(feature = "http10")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "http10")))] |
| impl TryFrom<&http10::HeaderValue> for PasswordClient { |
| type Error = String; |
| |
| #[inline] |
| fn try_from(value: &http10::HeaderValue) -> Result<Self, Self::Error> { |
| PasswordClient::builder().header_value(value).build() |
| } |
| } |
| |
| /// Tries to create a `PasswordClient` from the supplied `http::header::GetAll` challenge lists. |
| /// |
| /// This is a convenience wrapper around [`PasswordClientBuilder`]. |
| #[cfg(feature = "http")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "http")))] |
| impl TryFrom<http::header::GetAll<'_, http::HeaderValue>> for PasswordClient { |
| type Error = String; |
| |
| fn try_from(value: http::header::GetAll<'_, http::HeaderValue>) -> Result<Self, Self::Error> { |
| let mut builder = PasswordClient::builder(); |
| for v in value { |
| builder = builder.header_value(v); |
| } |
| builder.build() |
| } |
| } |
| |
| /// Tries to create a `PasswordClient` from the supplied `http::header::GetAll` challenge lists. |
| /// |
| /// This is a convenience wrapper around [`PasswordClientBuilder`]. |
| #[cfg(feature = "http10")] |
| #[cfg_attr(docsrs, doc(cfg(feature = "http10")))] |
| impl TryFrom<http10::header::GetAll<'_, http10::HeaderValue>> for PasswordClient { |
| type Error = String; |
| |
| fn try_from( |
| value: http10::header::GetAll<'_, http10::HeaderValue>, |
| ) -> Result<Self, Self::Error> { |
| let mut builder = PasswordClient::builder(); |
| for v in value { |
| builder = builder.header_value(v); |
| } |
| builder.build() |
| } |
| } |
| |
| impl PasswordClient { |
| /// Builds a new `PasswordClient`. |
| /// |
| /// See example at [`PasswordClientBuilder`]. |
| pub fn builder() -> PasswordClientBuilder { |
| PasswordClientBuilder::default() |
| } |
| |
| /// Responds to the challenge with the supplied parameters. |
| /// |
| /// The caller should use the returned string as an `Authorization` or |
| /// `Proxy-Authorization` header value. |
| #[allow(unused_variables)] // p is unused with no features. |
| pub fn respond(&mut self, p: &PasswordParams) -> Result<String, String> { |
| match self { |
| #[cfg(feature = "basic-scheme")] |
| Self::Basic(c) => Ok(c.respond(p.username, p.password)), |
| #[cfg(feature = "digest-scheme")] |
| Self::Digest(c) => c.respond(p), |
| |
| // Rust 1.55 + --no-default-features produces a "non-exhaustive |
| // patterns" error without this. I think this is a rustc bug given |
| // that the enum is empty in this case. Work around it. |
| #[cfg(not(any(feature = "basic-scheme", feature = "digest-scheme")))] |
| _ => unreachable!(), |
| } |
| } |
| } |
| |
| /// Parameters for responding to a password challenge. |
| /// |
| /// This is cheap to construct; callers generally use a fresh `PasswordParams` |
| /// for each request. |
| /// |
| /// The caller is responsible for supplying parameters in the correct |
| /// format. Servers may expect character data to be in Unicode Normalization |
| /// Form C as noted in [RFC 7617 section |
| /// 2.1](https://datatracker.ietf.org/doc/html/rfc7617#section-2.1) for the |
| /// `Basic` scheme and [RFC 7616 section |
| /// 4](https://datatracker.ietf.org/doc/html/rfc7616#section-4) for the `Digest` |
| /// scheme. |
| /// |
| /// Note that most of these fields are only needed for [`DigestClient`]. Callers |
| /// that only care about the `Basic` challenge scheme can use |
| /// [`BasicClient::respond`] directly with only username and password. |
| #[derive(Copy, Clone, Debug, Eq, PartialEq)] |
| pub struct PasswordParams<'a> { |
| pub username: &'a str, |
| pub password: &'a str, |
| |
| /// The URI from the Request-URI of the Request-Line, as described in |
| /// [RFC 2617 section 3.2.2](https://datatracker.ietf.org/doc/html/rfc2617#section-3.2.2). |
| /// |
| /// [RFC 2617 section |
| /// 3.2.2.5](https://datatracker.ietf.org/doc/html/rfc2617#section-3.2.2.5), |
| /// which says the following: |
| /// > This may be `*`, an `absoluteURL` or an `abs_path` as specified in |
| /// > section 5.1.2 of [RFC 2616](https://datatracker.ietf.org/doc/html/rfc2616), |
| /// > but it MUST agree with the Request-URI. In particular, it MUST |
| /// > be an `absoluteURL` if the Request-URI is an `absoluteURL`. |
| /// |
| /// [RFC 7616 section 3.4](https://datatracker.ietf.org/doc/html/rfc7616#section-3.4) |
| /// describes this as the "Effective Request URI", which is *always* an |
| /// absolute form. This may be a mistake. [Section |
| /// 3.4.6](https://datatracker.ietf.org/doc/html/rfc7616#section-3.4.6) |
| /// matches RFC 2617 section 3.2.2.5, and [Appendix |
| /// A](https://datatracker.ietf.org/doc/html/rfc7616#appendix-A) doesn't |
| /// mention a change from RFC 2617. |
| pub uri: &'a str, |
| |
| /// The HTTP method, such as `GET`. |
| /// |
| /// When using the `http` crate, use the return value of |
| /// [`http::Method::as_str`]. |
| pub method: &'a str, |
| |
| /// The entity body, if available. Use `Some(&[])` for HTTP methods with no |
| /// body. |
| /// |
| /// When `None`, `Digest` challenges will only be able to use |
| /// [`crate::digest::Qop::Auth`], not |
| /// [`crate::digest::Qop::AuthInt`]. |
| pub body: Option<&'a [u8]>, |
| } |
| |
| /// Parses a list of challenges into a `Vec`. |
| /// |
| /// Most callers don't need to directly parse; see [`PasswordClient`] instead. |
| /// |
| /// This is a shorthand for `parser::ChallengeParser::new(input).collect()`. Use |
| /// [`crate::parser::ChallengeParser`] directly when you want to parse lazily, |
| /// avoid allocation, and/or see any well-formed challenges before an error. |
| /// |
| /// ## Example |
| /// |
| /// ```rust |
| /// use http_auth::{parse_challenges, ChallengeRef, ParamValue}; |
| /// |
| /// // When all challenges are well-formed, returns them. |
| /// assert_eq!( |
| /// parse_challenges("UnsupportedSchemeA, Basic realm=\"foo\"").unwrap(), |
| /// vec![ |
| /// ChallengeRef { |
| /// scheme: "UnsupportedSchemeA", |
| /// params: vec![], |
| /// }, |
| /// ChallengeRef { |
| /// scheme: "Basic", |
| /// params: vec![("realm", ParamValue::try_from_escaped("foo").unwrap())], |
| /// }, |
| /// ], |
| /// ); |
| /// |
| /// // Returns `Err` if there is a syntax error anywhere in the input. |
| /// parse_challenges("UnsupportedSchemeA, Basic realm=\"foo\", error error").unwrap_err(); |
| /// ``` |
| #[inline] |
| pub fn parse_challenges(input: &str) -> Result<Vec<ChallengeRef>, parser::Error> { |
| parser::ChallengeParser::new(input).collect() |
| } |
| |
| /// Parsed challenge parameter value used within [`ChallengeRef`]. |
| #[derive(Copy, Clone, Eq, PartialEq)] |
| pub struct ParamValue<'i> { |
| /// The number of backslash escapes in a quoted-text parameter; 0 for a plain token. |
| escapes: usize, |
| |
| /// The escaped string, which must be pure ASCII (no bytes >= 128) and be |
| /// consistent with `escapes`. |
| escaped: &'i str, |
| } |
| |
| impl<'i> ParamValue<'i> { |
| /// Tries to create a new `ParamValue` from an escaped sequence, primarily for testing. |
| /// |
| /// Validates the sequence and counts the number of escapes. |
| pub fn try_from_escaped(escaped: &'i str) -> Result<Self, String> { |
| let mut escapes = 0; |
| let mut pos = 0; |
| while pos < escaped.len() { |
| let slash = memchr::memchr(b'\\', &escaped.as_bytes()[pos..]).map(|off| pos + off); |
| for i in pos..slash.unwrap_or(escaped.len()) { |
| if (char_classes(escaped.as_bytes()[i]) & C_QDTEXT) == 0 { |
| return Err(format!("{:?} has non-qdtext at byte {}", escaped, i)); |
| } |
| } |
| if let Some(slash) = slash { |
| escapes += 1; |
| if escaped.len() <= slash + 1 { |
| return Err(format!("{:?} ends at a quoted-pair escape", escaped)); |
| } |
| if (char_classes(escaped.as_bytes()[slash + 1]) & C_ESCAPABLE) == 0 { |
| return Err(format!( |
| "{:?} has an invalid quote-pair escape at byte {}", |
| escaped, |
| slash + 1 |
| )); |
| } |
| pos = slash + 2; |
| } else { |
| break; |
| } |
| } |
| Ok(Self { escaped, escapes }) |
| } |
| |
| /// Creates a new param, panicking if invariants are not satisfied. |
| /// This is not part of the stable API; it's just for the fuzz tester to use. |
| #[doc(hidden)] |
| pub fn new(escapes: usize, escaped: &'i str) -> Self { |
| let mut pos = 0; |
| for escape in 0..escapes { |
| match memchr::memchr(b'\\', &escaped.as_bytes()[pos..]) { |
| Some(rel_pos) => pos += rel_pos + 2, |
| None => panic!( |
| "expected {} backslashes in {:?}, ran out after {}", |
| escapes, escaped, escape |
| ), |
| }; |
| } |
| if memchr::memchr(b'\\', &escaped.as_bytes()[pos..]).is_some() { |
| panic!( |
| "expected {} backslashes in {:?}, are more", |
| escapes, escaped |
| ); |
| } |
| ParamValue { escapes, escaped } |
| } |
| |
| /// Appends the unescaped form of this parameter to the supplied string. |
| pub fn append_unescaped(&self, to: &mut String) { |
| to.reserve(self.escaped.len() - self.escapes); |
| let mut first_unwritten = 0; |
| for _ in 0..self.escapes { |
| let i = match memchr::memchr(b'\\', &self.escaped.as_bytes()[first_unwritten..]) { |
| Some(rel_i) => first_unwritten + rel_i, |
| None => panic!("bad ParamValues; not as many backslash escapes as promised"), |
| }; |
| to.push_str(&self.escaped[first_unwritten..i]); |
| to.push_str(&self.escaped[i + 1..i + 2]); |
| first_unwritten = i + 2; |
| } |
| to.push_str(&self.escaped[first_unwritten..]); |
| } |
| |
| /// Returns the unescaped length of this parameter; cheap. |
| #[inline] |
| pub fn unescaped_len(&self) -> usize { |
| self.escaped.len() - self.escapes |
| } |
| |
| /// Returns the unescaped form of this parameter as a fresh `String`. |
| pub fn to_unescaped(&self) -> String { |
| let mut to = String::new(); |
| self.append_unescaped(&mut to); |
| to |
| } |
| |
| /// Returns the unescaped form of this parameter, possibly appending it to `scratch`. |
| #[cfg(feature = "digest-scheme")] |
| fn unescaped_with_scratch<'tmp>(&self, scratch: &'tmp mut String) -> &'tmp str |
| where |
| 'i: 'tmp, |
| { |
| if self.escapes == 0 { |
| self.escaped |
| } else { |
| let start = scratch.len(); |
| self.append_unescaped(scratch); |
| &scratch[start..] |
| } |
| } |
| |
| /// Returns the escaped string, unquoted. |
| #[inline] |
| pub fn as_escaped(&self) -> &'i str { |
| self.escaped |
| } |
| } |
| |
| impl<'i> std::fmt::Debug for ParamValue<'i> { |
| fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
| write!(f, "\"{}\"", self.escaped) |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use crate::ParamValue; |
| use crate::{C_ATTR, C_ESCAPABLE, C_OWS, C_QDTEXT, C_TCHAR}; |
| |
| /// Prints the character classes of all ASCII bytes from the table. |
| /// |
| /// ```console |
| /// $ cargo test -- --nocapture tests::table |
| /// ``` |
| #[test] |
| fn table() { |
| // Print the table to allow human inspection. |
| println!("oct dec hex char tchar qdtext escapable ows attr"); |
| for b in 0..128 { |
| let classes = crate::char_classes(b); |
| let if_class = |
| |class: u8, label: &'static str| if (classes & class) != 0 { label } else { "" }; |
| println!( |
| "{:03o} {:>3} 0x{:02x} {:8} {:5} {:6} {:9} {:3} {:4}", |
| b, |
| b, |
| b, |
| format!("{:?}", char::from(b)), |
| if_class(C_TCHAR, "tchar"), |
| if_class(C_QDTEXT, "qdtext"), |
| if_class(C_ESCAPABLE, "escapable"), |
| if_class(C_OWS, "ows"), |
| if_class(C_ATTR, "attr") |
| ); |
| |
| // Do basic sanity checks: all tchar and ows should be qdtext; all |
| // qdtext should be escapable. |
| assert!(classes & (C_TCHAR | C_QDTEXT) != C_TCHAR); |
| assert!(classes & (C_OWS | C_QDTEXT) != C_OWS); |
| assert!(classes & (C_QDTEXT | C_ESCAPABLE) != C_QDTEXT); |
| } |
| } |
| |
| #[test] |
| fn try_from_escaped() { |
| assert_eq!(ParamValue::try_from_escaped("").unwrap().escapes, 0); |
| assert_eq!(ParamValue::try_from_escaped("foo").unwrap().escapes, 0); |
| assert_eq!(ParamValue::try_from_escaped("\\\"").unwrap().escapes, 1); |
| assert_eq!( |
| ParamValue::try_from_escaped("foo\\\"bar").unwrap().escapes, |
| 1 |
| ); |
| assert_eq!( |
| ParamValue::try_from_escaped("foo\\\"bar\\\"baz") |
| .unwrap() |
| .escapes, |
| 2 |
| ); |
| ParamValue::try_from_escaped("\\").unwrap_err(); // ends in slash |
| ParamValue::try_from_escaped("\"").unwrap_err(); // not valid qdtext |
| ParamValue::try_from_escaped("\n").unwrap_err(); // not valid qdtext |
| ParamValue::try_from_escaped("\\\n").unwrap_err(); // not valid escape |
| } |
| |
| #[test] |
| fn unescape() { |
| assert_eq!( |
| &ParamValue { |
| escapes: 0, |
| escaped: "" |
| } |
| .to_unescaped(), |
| "" |
| ); |
| assert_eq!( |
| &ParamValue { |
| escapes: 0, |
| escaped: "foo" |
| } |
| .to_unescaped(), |
| "foo" |
| ); |
| assert_eq!( |
| &ParamValue { |
| escapes: 1, |
| escaped: "\\foo" |
| } |
| .to_unescaped(), |
| "foo" |
| ); |
| assert_eq!( |
| &ParamValue { |
| escapes: 1, |
| escaped: "fo\\o" |
| } |
| .to_unescaped(), |
| "foo" |
| ); |
| assert_eq!( |
| &ParamValue { |
| escapes: 1, |
| escaped: "foo\\bar" |
| } |
| .to_unescaped(), |
| "foobar" |
| ); |
| assert_eq!( |
| &ParamValue { |
| escapes: 3, |
| escaped: "\\foo\\ba\\r" |
| } |
| .to_unescaped(), |
| "foobar" |
| ); |
| } |
| } |