1 // Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
2 // SPDX-License-Identifier: MIT OR Apache-2.0
4 //! HTTP authentication. Currently meant for clients; to be extended for servers.
6 //! As described in the following documents and specifications:
8 //! * [MDN documentation](https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication).
9 //! * [RFC 7235](https://datatracker.ietf.org/doc/html/rfc7235):
10 //! Hypertext Transfer Protocol (HTTP/1.1): Authentication.
11 //! * [RFC 7617](https://datatracker.ietf.org/doc/html/rfc7617):
12 //! The 'Basic' HTTP Authentication Scheme
13 //! * [RFC 7616](https://datatracker.ietf.org/doc/html/rfc7616):
14 //! HTTP Digest Access Authentication
16 //! This framework is primarily used with HTTP, as suggested by the name. It is
17 //! also used by some other protocols such as RTSP.
21 //! | feature | default? | description |
22 //! |-----------------|----------|-------------------------------------------------|
23 //! | `basic-scheme` | yes | support for the `Basic` auth scheme |
24 //! | `digest-scheme` | yes | support for the `Digest` auth scheme |
25 //! | `http` | no | convenient conversion from [`http`] crate types |
29 //! In most cases, callers only need to use [`PasswordClient`] and
30 //! [`PasswordParams`] to handle `Basic` and `Digest` authentication schemes.
36 use std::convert::TryFrom as _;
37 use http_auth::PasswordClient;
39 let WWW_AUTHENTICATE_VAL = "UnsupportedSchemeA, Basic realm=\"foo\", UnsupportedSchemeB";
40 let mut pw_client = http_auth::PasswordClient::try_from(WWW_AUTHENTICATE_VAL).unwrap();
41 assert!(matches!(pw_client, http_auth::PasswordClient::Basic(_)));
42 let response = pw_client.respond(&http_auth::PasswordParams {
44 password: "open sesame",
49 assert_eq!(response, "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
54 //! The `http` feature allows parsing all `WWW-Authenticate` headers within a
55 //! [`http::HeaderMap`] in one call.
61 # use std::convert::TryFrom as _;
62 use http::header::{HeaderMap, WWW_AUTHENTICATE};
63 # use http_auth::PasswordClient;
65 let mut headers = HeaderMap::new();
66 headers.append(WWW_AUTHENTICATE, "UnsupportedSchemeA".parse().unwrap());
67 headers.append(WWW_AUTHENTICATE, "Basic realm=\"foo\", UnsupportedSchemeB".parse().unwrap());
69 let mut pw_client = PasswordClient::try_from(headers.get_all(WWW_AUTHENTICATE)).unwrap();
70 assert!(matches!(pw_client, http_auth::PasswordClient::Basic(_)));
74 #![cfg_attr(docsrs, feature(doc_cfg))]
76 use std
::convert
::TryFrom
;
80 #[cfg(feature = "basic-scheme")]
81 #[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))]
84 #[cfg(feature = "digest-scheme")]
85 #[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))]
88 pub use parser
::ChallengeParser
;
90 #[cfg(feature = "basic-scheme")]
91 #[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))]
92 pub use crate::basic
::BasicClient
;
94 #[cfg(feature = "digest-scheme")]
95 #[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))]
96 pub use crate::digest
::DigestClient
;
98 // Must match build.rs exactly.
99 const C_TCHAR
: u8 = 1;
100 const C_QDTEXT
: u8 = 2;
101 const C_ESCAPABLE
: u8 = 4;
104 #[cfg_attr(not(feature = "digest-scheme"), allow(unused))]
105 const C_ATTR
: u8 = 16;
107 /// Returns a bitmask of `C_*` values indicating character classes.
108 fn char_classes(b
: u8) -> u8 {
109 // This table is built by build.rs.
110 const TABLE
: &[u8; 128] = include_bytes
!(concat
!(env
!("OUT_DIR"), "/char_class_table.bin"));
111 *TABLE
.get(usize::from(b
)).unwrap_or(&0)
114 /// Parsed challenge (scheme and body) using references to the original header value.
115 /// Produced by [`crate::parser::ChallengeParser`].
117 /// This is not directly useful for responding to a challenge; it's an
118 /// intermediary for constructing a client that knows how to respond to a specific
119 /// challenge scheme. In most cases, callers should construct a [`PasswordClient`]
120 /// without directly using `ChallengeRef`.
122 /// Only supports the param form, not the apocryphal `token68` form, as described
123 /// in [`crate::parser::ChallengeParser`].
124 #[derive(Clone, Eq, PartialEq)]
125 pub struct ChallengeRef
<'i
> {
126 /// The scheme name, which should be compared case-insensitively.
129 /// Zero or more parameters.
131 /// These are represented as a `Vec` of key-value pairs rather than a
132 /// map. Given that the parameters are generally only used once when
133 /// constructing a challenge client and each challenge only supports a few
134 /// parameter types, it's more efficient in terms of CPU usage and code size
135 /// to scan through them directly.
136 pub params
: Vec
<ChallengeParamRef
<'i
>>,
139 impl<'i
> ChallengeRef
<'i
> {
140 pub fn new(scheme
: &'i
str) -> Self {
148 impl<'i
> std
::fmt
::Debug
for ChallengeRef
<'i
> {
149 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
150 f
.debug_struct("ChallengeRef")
151 .field("scheme", &self.scheme
)
152 .field("params", &ParamsPrinter(&self.params
))
157 type ChallengeParamRef
<'i
> = (&'i
str, ParamValue
<'i
>);
159 struct ParamsPrinter
<'i
>(&'i
[ChallengeParamRef
<'i
>]);
161 impl<'i
> std
::fmt
::Debug
for ParamsPrinter
<'i
> {
162 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
164 .entries(self.0.iter
().map(|&(ref k
, ref v
)| (k
, v
)))
169 /// Builds a [`PasswordClient`] from the supplied challenges; create via
170 /// [`PasswordClient::builder`].
172 /// Often you can just use [`PasswordClient`]'s [`TryFrom`] implementations
173 /// to convert from a parsed challenge ([`crate::ChallengeRef`]) or
174 /// unparsed challenges (`str`, [`http::header::HeaderValue`], or
175 /// [`http::header::GetAll`]).
177 /// The builder allows more flexibility. For example, if you are using a HTTP
178 /// library which is not based on a `http` crate, you might need to create
179 /// a `PasswordClient` from an iterator over multiple `WWW-Authenticate`
180 /// headers. You can feed each to [`PasswordClientBuilder::challenges`].
182 /// Prefers `Digest` over `Basic`, consistent with the [RFC 7235 section
183 /// 2.1](https://datatracker.ietf.org/doc/html/rfc7235#section-2.1) advice
184 /// for a user-agent to pick the most secure auth-scheme it understands.
186 /// When there are multiple `Digest` challenges, currently uses the first,
187 /// consistent with the [RFC 7616 section
188 /// 3.7](https://datatracker.ietf.org/doc/html/rfc7616#section-3.7)
189 /// advice to "use the first challenge it supports, unless a local policy
190 /// dictates otherwise". In the future, it may prioritize by algorithm.
192 /// Ignores parse errors as long as there's at least one parseable, supported
201 use http_auth::PasswordClient;
202 let client = PasswordClient::builder()
203 .challenges("UnsupportedSchemeA, Basic realm=\"foo\", UnsupportedSchemeB")
204 .challenges("Digest \
205 realm=\"http-auth@example.org\", \
206 qop=\"auth, auth-int\", \
208 nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
209 opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"")
212 assert!(matches!(client, PasswordClient::Digest(_)));
217 pub struct PasswordClientBuilder(
218 /// The current result:
219 /// * `Some(Ok(_))` if there is a suitable client.
220 /// * `Some(Err(_))` if there is no suitable client and has been a parse error.
221 /// * `None` otherwise.
222 Option
<Result
<PasswordClient
, String
>>,
225 impl PasswordClientBuilder
{
226 /// Considers all challenges from the given [`http::HeaderValue`] challenge list.
227 #[cfg(feature = "http")]
228 #[cfg_attr(docsrs, doc(cfg(feature = "http")))]
229 pub fn header_value(mut self, value
: &http
::HeaderValue
) -> Self {
234 match value
.to_str() {
235 Ok(v
) => self = self.challenges(v
),
236 Err(_
) if matches
!(self.0, None
) => self.0 = Some(Err("non-ASCII header value".into())),
243 /// Returns true if no more challenges need to be examined.
244 #[cfg(feature = "digest-scheme")]
245 fn complete(&self) -> bool
{
246 matches
!(self.0, Some(Ok(PasswordClient
::Digest(_
))))
249 /// Returns true if no more challenges need to be examined.
250 #[cfg(not(feature = "digest-scheme"))]
251 fn complete(&self) -> bool
{
252 matches
!(self.0, Some(Ok(_
)))
255 /// Considers all challenges from the given `&str` challenge list.
256 pub fn challenges(mut self, value
: &str) -> Self {
257 let mut parser
= ChallengeParser
::new(value
);
258 while !self.complete() {
259 match parser
.next() {
260 Some(Ok(c
)) => self = self.challenge(&c
),
261 Some(Err(e
)) if self.0.is_none
() => self.0 = Some(Err(e
.to_string())),
268 /// Considers a single challenge.
269 pub fn challenge(mut self, challenge
: &ChallengeRef
<'_
>) -> Self {
274 #[cfg(feature = "digest-scheme")]
275 if challenge
.scheme
.eq_ignore_ascii_case("Digest") {
276 match DigestClient
::try_from(challenge
) {
277 Ok(c
) => self.0 = Some(Ok(PasswordClient
::Digest(c
))),
278 Err(e
) if self.0.is_none
() => self.0 = Some(Err(e
)),
284 #[cfg(feature = "basic-scheme")]
285 if challenge
.scheme
.eq_ignore_ascii_case("Basic") && !matches
!(self.0, Some(Ok(_
))) {
286 match BasicClient
::try_from(challenge
) {
287 Ok(c
) => self.0 = Some(Ok(PasswordClient
::Basic(c
))),
288 Err(e
) if self.0.is_none
() => self.0 = Some(Err(e
)),
294 if self.0.is_none
() {
295 self.0 = Some(Err(format
!("Unsupported scheme {:?}", challenge
.scheme
)));
301 /// Returns a new [`PasswordClient`] or fails.
302 pub fn build(self) -> Result
<PasswordClient
, String
> {
303 self.0.unwrap_or_else
(|| Err("no challenges given".into()))
307 /// Client for responding to a password challenge.
309 /// Typically created via [`TryFrom`] implementations for a parsed challenge
310 /// ([`crate::ChallengeRef`]) or unparsed challenges (`str`,
311 /// [`http::header::HeaderValue`], or [`http::header::GetAll`]). See full
312 /// example in the [crate-level documentation](crate).
314 /// For more complex scenarios, see [`PasswordClientBuilder`].
315 #[derive(Debug, Eq, PartialEq)]
317 pub enum PasswordClient
{
318 #[cfg(feature = "basic-scheme")]
319 #[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))]
322 #[cfg(feature = "digest-scheme")]
323 #[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))]
324 Digest(DigestClient
),
327 /// Tries to create a `PasswordClient` from the single supplied challenge.
329 /// This is a convenience wrapper around [`PasswordClientBuilder`].
330 impl TryFrom
<&ChallengeRef
<'_
>> for PasswordClient
{
333 fn try_from(value
: &ChallengeRef
<'_
>) -> Result
<Self, Self::Error
> {
334 #[cfg(feature = "basic-scheme")]
335 if value
.scheme
.eq_ignore_ascii_case("Basic") {
336 return Ok(PasswordClient
::Basic(BasicClient
::try_from(value
)?
));
338 #[cfg(feature = "digest-scheme")]
339 if value
.scheme
.eq_ignore_ascii_case("Digest") {
340 return Ok(PasswordClient
::Digest(DigestClient
::try_from(value
)?
));
343 Err(format
!("unsupported challenge scheme {:?}", value
.scheme
))
347 /// Tries to create a `PasswordClient` forom the supplied `str` challenge list.
349 /// This is a convenience wrapper around [`PasswordClientBuilder`].
350 impl TryFrom
<&str> for PasswordClient
{
354 fn try_from(value
: &str) -> Result
<Self, Self::Error
> {
355 PasswordClient
::builder().challenges(value
).build()
359 /// Tries to create a `PasswordClient` from the supplied `HeaderValue` challenge list.
361 /// This is a convenience wrapper around [`PasswordClientBuilder`].
362 #[cfg(feature = "http")]
363 #[cfg_attr(docsrs, doc(cfg(feature = "http")))]
364 impl TryFrom
<&http
::HeaderValue
> for PasswordClient
{
368 fn try_from(value
: &http
::HeaderValue
) -> Result
<Self, Self::Error
> {
369 PasswordClient
::builder().header_value(value
).build()
373 /// Tries to create a `PasswordClient` from the supplied `http::header::GetAll` challenge lists.
375 /// This is a convenience wrapper around [`PasswordClientBuilder`].
376 #[cfg(feature = "http")]
377 #[cfg_attr(docsrs, doc(cfg(feature = "http")))]
378 impl TryFrom
<http
::header
::GetAll
<'_
, http
::HeaderValue
>> for PasswordClient
{
381 fn try_from(value
: http
::header
::GetAll
<'_
, http
::HeaderValue
>) -> Result
<Self, Self::Error
> {
382 let mut builder
= PasswordClient
::builder();
384 builder
= builder
.header_value(v
);
390 impl PasswordClient
{
391 /// Builds a new `PasswordClient`.
393 /// See example at [`PasswordClientBuilder`].
394 pub fn builder() -> PasswordClientBuilder
{
395 PasswordClientBuilder
::default()
398 /// Responds to the challenge with the supplied parameters.
400 /// The caller should use the returned string as an `Authorization` or
401 /// `Proxy-Authorization` header value.
402 #[allow(unused_variables)] // p is unused with no features.
403 pub fn respond(&mut self, p
: &PasswordParams
) -> Result
<String
, String
> {
405 #[cfg(feature = "basic-scheme")]
406 Self::Basic(c
) => Ok(c
.respond(p
.username
, p
.password
)),
407 #[cfg(feature = "digest-scheme")]
408 Self::Digest(c
) => c
.respond(p
),
410 // Rust 1.55 + --no-default-features produces a "non-exhaustive
411 // patterns" error without this. I think this is a rustc bug given
412 // that the enum is empty in this case. Work around it.
413 #[cfg(not(any(feature = "basic-scheme", feature = "digest-scheme")))]
419 /// Parameters for responding to a password challenge.
421 /// This is cheap to construct; callers generally use a fresh `PasswordParams`
422 /// for each request.
424 /// The caller is responsible for supplying parameters in the correct
425 /// format. Servers may expect character data to be in Unicode Normalization
426 /// Form C as noted in [RFC 7617 section
427 /// 2.1](https://datatracker.ietf.org/doc/html/rfc7617#section-2.1) for the
428 /// `Basic` scheme and [RFC 7616 section
429 /// 4](https://datatracker.ietf.org/doc/html/rfc7616#section-4) for the `Digest`
432 /// Note that most of these fields are only needed for [`DigestClient`]. Callers
433 /// that only care about the `Basic` challenge scheme can use
434 /// [`BasicClient::respond`] directly with only username and password.
435 #[derive(Copy, Clone, Debug, Eq, PartialEq)]
436 pub struct PasswordParams
<'a
> {
437 pub username
: &'a
str,
438 pub password
: &'a
str,
440 /// The URI from the Request-URI of the Request-Line, as described in
441 /// [RFC 2617 section 3.2.2](https://datatracker.ietf.org/doc/html/rfc2617#section-3.2.2).
443 /// [RFC 2617 section
444 /// 3.2.2.5](https://datatracker.ietf.org/doc/html/rfc2617#section-3.2.2.5),
445 /// which says the following:
446 /// > This may be `*`, an `absoluteURL` or an `abs_path` as specified in
447 /// > section 5.1.2 of [RFC 2616](https://datatracker.ietf.org/doc/html/rfc2616),
448 /// > but it MUST agree with the Request-URI. In particular, it MUST
449 /// > be an `absoluteURL` if the Request-URI is an `absoluteURL`.
451 /// [RFC 7616 section 3.4](https://datatracker.ietf.org/doc/html/rfc7616#section-3.4)
452 /// describes this as the "Effective Request URI", which is *always* an
453 /// absolute form. This may be a mistake. [Section
454 /// 3.4.6](https://datatracker.ietf.org/doc/html/rfc7616#section-3.4.6)
455 /// matches RFC 2617 section 3.2.2.5, and [Appendix
456 /// A](https://datatracker.ietf.org/doc/html/rfc7616#appendix-A) doesn't
457 /// mention a change from RFC 2617.
460 /// The HTTP method, such as `GET`.
462 /// When using the `http` crate, use the return value of
463 /// [`http::Method::as_str`].
466 /// The entity body, if available. Use `Some(&[])` for HTTP methods with no
469 /// When `None`, `Digest` challenges will only be able to use
470 /// [`crate::digest::Qop::Auth`], not
471 /// [`crate::digest::Qop::AuthInt`].
472 pub body
: Option
<&'a
[u8]>,
475 /// Parses a list of challenges into a `Vec`.
477 /// Most callers don't need to directly parse; see [`PasswordClient`] instead.
479 /// This is a shorthand for `parser::ChallengeParser::new(input).collect()`. Use
480 /// [`crate::parser::ChallengeParser`] directly when you want to parse lazily,
481 /// avoid allocation, and/or see any well-formed challenges before an error.
486 /// use http_auth::{parse_challenges, ChallengeRef, ParamValue};
488 /// // When all challenges are well-formed, returns them.
490 /// parse_challenges("UnsupportedSchemeA, Basic realm=\"foo\"").unwrap(),
493 /// scheme: "UnsupportedSchemeA",
498 /// params: vec![("realm", ParamValue::try_from_escaped("foo").unwrap())],
503 /// // Returns `Err` if there is a syntax error anywhere in the input.
504 /// parse_challenges("UnsupportedSchemeA, Basic realm=\"foo\", error error").unwrap_err();
507 pub fn parse_challenges(input
: &str) -> Result
<Vec
<ChallengeRef
>, parser
::Error
> {
508 parser
::ChallengeParser
::new(input
).collect()
511 /// Parsed challenge parameter value used within [`ChallengeRef`].
512 #[derive(Copy, Clone, Eq, PartialEq)]
513 pub struct ParamValue
<'i
> {
514 /// The number of backslash escapes in a quoted-text parameter; 0 for a plain token.
517 /// The escaped string, which must be pure ASCII (no bytes >= 128) and be
518 /// consistent with `escapes`.
522 impl<'i
> ParamValue
<'i
> {
523 /// Tries to create a new `ParamValue` from an escaped sequence, primarily for testing.
525 /// Validates the sequence and counts the number of escapes.
526 pub fn try_from_escaped(escaped
: &'i
str) -> Result
<Self, String
> {
529 while pos
< escaped
.len() {
530 let slash
= memchr
::memchr(b'
\\'
, &escaped
.as_bytes()[pos
..]).map(|off
| pos
+ off
);
531 for i
in pos
..slash
.unwrap_or(escaped
.len()) {
532 if (char_classes(escaped
.as_bytes()[i
]) & C_QDTEXT
) == 0 {
533 return Err(format
!("{:?} has non-qdtext at byte {}", escaped
, i
));
536 if let Some(slash
) = slash
{
538 if escaped
.len() <= slash
+ 1 {
539 return Err(format
!("{:?} ends at a quoted-pair escape", escaped
));
541 if (char_classes(escaped
.as_bytes()[slash
+ 1]) & C_ESCAPABLE
) == 0 {
543 "{:?} has an invalid quote-pair escape at byte {}",
553 Ok(Self { escaped, escapes }
)
556 /// Creates a new param, panicking if invariants are not satisfied.
557 /// This not part of the stable API; it's just for the fuzz tester to use.
559 pub fn new(escapes
: usize, escaped
: &'i
str) -> Self {
561 for escape
in 0..escapes
{
562 match memchr
::memchr(b'
\\'
, &escaped
.as_bytes()[pos
..]) {
563 Some(rel_pos
) => pos
+= rel_pos
+ 2,
565 "expected {} backslashes in {:?}, ran out after {}",
566 escapes
, escaped
, escape
570 if memchr
::memchr(b'
\\'
, &escaped
.as_bytes()[pos
..]).is_some() {
572 "expected {} backslashes in {:?}, are more",
576 ParamValue { escapes, escaped }
579 /// Appends the unescaped form of this parameter to the supplied string.
580 pub fn append_unescaped(&self, to
: &mut String
) {
581 to
.reserve(self.escaped
.len() - self.escapes
);
582 let mut first_unwritten
= 0;
583 for _
in 0..self.escapes
{
584 let i
= match memchr
::memchr(b'
\\'
, &self.escaped
.as_bytes()[first_unwritten
..]) {
585 Some(rel_i
) => first_unwritten
+ rel_i
,
586 None
=> panic
!("bad ParamValues; not as many backslash escapes as promised"),
588 to
.push_str(&self.escaped
[first_unwritten
..i
]);
589 to
.push_str(&self.escaped
[i
+ 1..i
+ 2]);
590 first_unwritten
= i
+ 2;
592 to
.push_str(&self.escaped
[first_unwritten
..]);
595 /// Returns the unescaped length of this parameter; cheap.
597 pub fn unescaped_len(&self) -> usize {
598 self.escaped
.len() - self.escapes
601 /// Returns the unescaped form of this parameter as a fresh `String`.
602 pub fn to_unescaped(&self) -> String
{
603 let mut to
= String
::new();
604 self.append_unescaped(&mut to
);
608 /// Returns the unescaped form of this parameter, possibly appending it to `scratch`.
609 #[cfg(feature = "digest-scheme")]
610 fn unescaped_with_scratch
<'tmp
>(&self, scratch
: &'tmp
mut String
) -> &'tmp
str
614 if self.escapes
== 0 {
617 let start
= scratch
.len();
618 self.append_unescaped(scratch
);
623 /// Returns the escaped string, unquoted.
625 pub fn as_escaped(&self) -> &'i
str {
630 impl<'i
> std
::fmt
::Debug
for ParamValue
<'i
> {
631 fn fmt(&self, f
: &mut std
::fmt
::Formatter
<'_
>) -> std
::fmt
::Result
{
632 write
!(f
, "\"{}\"", self.escaped
)
638 use crate::ParamValue
;
639 use crate::{C_ATTR, C_ESCAPABLE, C_OWS, C_QDTEXT, C_TCHAR}
;
641 /// Prints the character classes of all ASCII bytes from the table.
644 /// $ cargo test -- --nocapture tests::table
648 // Print the table to allow human inspection.
649 println
!("oct dec hex char tchar qdtext escapable ows attr");
651 let classes
= crate::char_classes(b
);
653 |class
: u8, label
: &'
static str| if (classes
& class
) != 0 { label }
else { "" }
;
655 "{:03o} {:>3} 0x{:02x} {:8} {:5} {:6} {:9} {:3} {:4}",
659 format
!("{:?}", char::from(b
)),
660 if_class(C_TCHAR
, "tchar"),
661 if_class(C_QDTEXT
, "qdtext"),
662 if_class(C_ESCAPABLE
, "escapable"),
663 if_class(C_OWS
, "ows"),
664 if_class(C_ATTR
, "attr")
667 // Do basic sanity checks: all tchar and ows should be qdtext; all
668 // qdtext should be escapable.
669 assert
!(classes
& (C_TCHAR
| C_QDTEXT
) != C_TCHAR
);
670 assert
!(classes
& (C_OWS
| C_QDTEXT
) != C_OWS
);
671 assert
!(classes
& (C_QDTEXT
| C_ESCAPABLE
) != C_QDTEXT
);
676 fn try_from_escaped() {
677 assert_eq
!(ParamValue
::try_from_escaped("").unwrap().escapes
, 0);
678 assert_eq
!(ParamValue
::try_from_escaped("foo").unwrap().escapes
, 0);
679 assert_eq
!(ParamValue
::try_from_escaped("\\\"").unwrap().escapes
, 1);
681 ParamValue
::try_from_escaped("foo\\\"bar").unwrap().escapes
,
685 ParamValue
::try_from_escaped("foo\\\"bar\\\"baz")
690 ParamValue
::try_from_escaped("\\").unwrap_err(); // ends in slash
691 ParamValue
::try_from_escaped("\"").unwrap_err(); // not valid qdtext
692 ParamValue
::try_from_escaped("\n").unwrap_err(); // not valid qdtext
693 ParamValue
::try_from_escaped("\\\n").unwrap_err(); // not valid escape
741 escaped
: "\\foo\\ba\\r"