diff options
Diffstat (limited to 'vendor/http-auth/src/lib.rs')
-rw-r--r-- | vendor/http-auth/src/lib.rs | 747 |
1 files changed, 747 insertions, 0 deletions
diff --git a/vendor/http-auth/src/lib.rs b/vendor/http-auth/src/lib.rs new file mode 100644 index 000000000..13a657ebb --- /dev/null +++ b/vendor/http-auth/src/lib.rs @@ -0,0 +1,747 @@ +// Copyright (C) 2021 Scott Lamb <slamb@slamb.org> +// 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 | +//! +//! ## Example +//! +//! In most cases, callers only need to use [`PasswordClient`] and +//! [`PasswordParams`] to handle `Basic` and `Digest` authentication schemes. +//! +#![cfg_attr( + feature = "http", + 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` feature allows parsing all `WWW-Authenticate` headers within a +//! [`http::HeaderMap`] in one call. +//! +#![cfg_attr( + feature = "http", + 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; + +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; + +// Must match build.rs exactly. +const C_TCHAR: u8 = 1; +const C_QDTEXT: u8 = 2; +const C_ESCAPABLE: u8 = 4; +const C_OWS: u8 = 8; + +#[cfg_attr(not(feature = "digest-scheme"), allow(unused))] +const C_ATTR: u8 = 16; + +/// Returns a bitmask of `C_*` values indicating character classes. +fn char_classes(b: u8) -> u8 { + // This table is built by build.rs. + const TABLE: &[u8; 128] = include_bytes!(concat!(env!("OUT_DIR"), "/char_class_table.bin")); + *TABLE.get(usize::from(b)).unwrap_or(&0) +} + +/// 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().map(|&(ref k, ref v)| (k, v))) + .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>>, +); + +impl PasswordClientBuilder { + /// Considers all challenges from the given [`http::HeaderValue`] challenge list. + #[cfg(feature = "http")] + #[cfg_attr(docsrs, doc(cfg(feature = "http")))] + pub fn header_value(mut self, value: &http::HeaderValue) -> 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` forom 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 `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() + } +} + +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 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" + ); + } +} |