http_auth/
lib.rs

1// Copyright (C) 2021 Scott Lamb <slamb@slamb.org>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! HTTP authentication. Currently meant for clients; to be extended for servers.
5//!
6//! As described in the following documents and specifications:
7//!
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
15//!
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.
18//!
19//! ## Cargo Features
20//!
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, version 0.2 |
26//! | `http10`        | no       | convenient conversion from `http` crate types, version 1.0 |
27//!
28//! ## Example
29//!
30//! In most cases, callers only need to use [`PasswordClient`] and
31//! [`PasswordParams`] to handle `Basic` and `Digest` authentication schemes.
32//!
33#![cfg_attr(
34    any(feature = "http", feature = "http10"),
35    doc = r##"
36```rust
37use std::convert::TryFrom as _;
38use http_auth::PasswordClient;
39
40let WWW_AUTHENTICATE_VAL = "UnsupportedSchemeA, Basic realm=\"foo\", UnsupportedSchemeB";
41let mut pw_client = http_auth::PasswordClient::try_from(WWW_AUTHENTICATE_VAL).unwrap();
42assert!(matches!(pw_client, http_auth::PasswordClient::Basic(_)));
43let response = pw_client.respond(&http_auth::PasswordParams {
44    username: "Aladdin",
45    password: "open sesame",
46    uri: "/",
47    method: "GET",
48    body: Some(&[]),
49}).unwrap();
50assert_eq!(response, "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
51```
52"##
53)]
54//!
55//! The `http` or `http10` features allow parsing all `WWW-Authenticate` headers within a
56//! [`http::HeaderMap`] in one call.
57//!
58#![cfg_attr(
59    any(feature = "http", feature = "http10"),
60    doc = r##"
61```rust
62# use std::convert::TryFrom as _;
63use http::header::{HeaderMap, WWW_AUTHENTICATE};
64# use http_auth::PasswordClient;
65
66let mut headers = HeaderMap::new();
67headers.append(WWW_AUTHENTICATE, "UnsupportedSchemeA".parse().unwrap());
68headers.append(WWW_AUTHENTICATE, "Basic realm=\"foo\", UnsupportedSchemeB".parse().unwrap());
69
70let mut pw_client = PasswordClient::try_from(headers.get_all(WWW_AUTHENTICATE)).unwrap();
71assert!(matches!(pw_client, http_auth::PasswordClient::Basic(_)));
72```
73"##
74)]
75#![cfg_attr(docsrs, feature(doc_cfg))]
76
77use std::convert::TryFrom;
78
79pub mod parser;
80
81#[cfg(feature = "basic-scheme")]
82#[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))]
83pub mod basic;
84
85#[cfg(feature = "digest-scheme")]
86#[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))]
87pub mod digest;
88
89mod table;
90
91pub use parser::ChallengeParser;
92
93#[cfg(feature = "basic-scheme")]
94#[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))]
95pub use crate::basic::BasicClient;
96
97#[cfg(feature = "digest-scheme")]
98#[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))]
99pub use crate::digest::DigestClient;
100
101use crate::table::{char_classes, C_ESCAPABLE, C_OWS, C_QDTEXT, C_TCHAR};
102
103#[cfg(feature = "digest-scheme")]
104use crate::table::C_ATTR;
105
106/// Parsed challenge (scheme and body) using references to the original header value.
107/// Produced by [`crate::parser::ChallengeParser`].
108///
109/// This is not directly useful for responding to a challenge; it's an
110/// intermediary for constructing a client that knows how to respond to a specific
111/// challenge scheme. In most cases, callers should construct a [`PasswordClient`]
112/// without directly using `ChallengeRef`.
113///
114/// Only supports the param form, not the apocryphal `token68` form, as described
115/// in [`crate::parser::ChallengeParser`].
116#[derive(Clone, Eq, PartialEq)]
117pub struct ChallengeRef<'i> {
118    /// The scheme name, which should be compared case-insensitively.
119    pub scheme: &'i str,
120
121    /// Zero or more parameters.
122    ///
123    /// These are represented as a `Vec` of key-value pairs rather than a
124    /// map. Given that the parameters are generally only used once when
125    /// constructing a challenge client and each challenge only supports a few
126    /// parameter types, it's more efficient in terms of CPU usage and code size
127    /// to scan through them directly.
128    pub params: Vec<ChallengeParamRef<'i>>,
129}
130
131impl<'i> ChallengeRef<'i> {
132    pub fn new(scheme: &'i str) -> Self {
133        ChallengeRef {
134            scheme,
135            params: Vec::new(),
136        }
137    }
138}
139
140impl<'i> std::fmt::Debug for ChallengeRef<'i> {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        f.debug_struct("ChallengeRef")
143            .field("scheme", &self.scheme)
144            .field("params", &ParamsPrinter(&self.params))
145            .finish()
146    }
147}
148
149type ChallengeParamRef<'i> = (&'i str, ParamValue<'i>);
150
151struct ParamsPrinter<'i>(&'i [ChallengeParamRef<'i>]);
152
153impl<'i> std::fmt::Debug for ParamsPrinter<'i> {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        f.debug_map().entries(self.0.iter().copied()).finish()
156    }
157}
158
159/// Builds a [`PasswordClient`] from the supplied challenges; create via
160/// [`PasswordClient::builder`].
161///
162/// Often you can just use [`PasswordClient`]'s [`TryFrom`] implementations
163/// to convert from a parsed challenge ([`crate::ChallengeRef`]) or
164/// unparsed challenges (`str`, [`http::header::HeaderValue`], or
165/// [`http::header::GetAll`]).
166///
167/// The builder allows more flexibility. For example, if you are using a HTTP
168/// library which is not based on a `http` crate, you might need to create
169/// a `PasswordClient` from an iterator over multiple `WWW-Authenticate`
170/// headers. You can feed each to [`PasswordClientBuilder::challenges`].
171///
172/// Prefers `Digest` over `Basic`, consistent with the [RFC 7235 section
173/// 2.1](https://datatracker.ietf.org/doc/html/rfc7235#section-2.1) advice
174/// for a user-agent to pick the most secure auth-scheme it understands.
175///
176/// When there are multiple `Digest` challenges, currently uses the first,
177/// consistent with the [RFC 7616 section
178/// 3.7](https://datatracker.ietf.org/doc/html/rfc7616#section-3.7)
179/// advice to "use the first challenge it supports, unless a local policy
180/// dictates otherwise". In the future, it may prioritize by algorithm.
181///
182/// Ignores parse errors as long as there's at least one parseable, supported
183/// challenge.
184///
185/// ## Example
186///
187#[cfg_attr(
188    feature = "digest",
189    doc = r##"
190```rust
191use http_auth::PasswordClient;
192let client = PasswordClient::builder()
193    .challenges("UnsupportedSchemeA, Basic realm=\"foo\", UnsupportedSchemeB")
194    .challenges("Digest \
195                 realm=\"http-auth@example.org\", \
196                 qop=\"auth, auth-int\", \
197                 algorithm=MD5, \
198                 nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
199                 opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"")
200    .build()
201    .unwrap();
202assert!(matches!(client, PasswordClient::Digest(_)));
203```
204"##
205)]
206#[derive(Default)]
207pub struct PasswordClientBuilder(
208    /// The current result:
209    /// *   `Some(Ok(_))` if there is a suitable client.
210    /// *   `Some(Err(_))` if there is no suitable client and has been a parse error.
211    /// *   `None` otherwise.
212    Option<Result<PasswordClient, String>>,
213);
214
215/// An error returned by [`HeaderValue::to_str`].
216pub struct ToStrError {
217    _priv: (),
218}
219
220/// A trait for the parts needed from http crate 0.2 or 1.0's `HeaderValue` type.
221#[cfg(any(feature = "http", feature = "http10"))]
222pub trait HeaderValue {
223    fn to_str(&self) -> Result<&str, ToStrError>;
224}
225
226#[cfg(feature = "http")]
227impl HeaderValue for http::HeaderValue {
228    fn to_str(&self) -> Result<&str, ToStrError> {
229        self.to_str().map_err(|_| ToStrError { _priv: () })
230    }
231}
232
233#[cfg(feature = "http10")]
234impl HeaderValue for http10::HeaderValue {
235    fn to_str(&self) -> Result<&str, ToStrError> {
236        self.to_str().map_err(|_| ToStrError { _priv: () })
237    }
238}
239
240impl PasswordClientBuilder {
241    /// Considers all challenges from the given [`http::HeaderValue`] challenge list.
242    #[cfg(any(feature = "http", feature = "http10"))]
243    #[cfg_attr(docsrs, doc(cfg(any(feature = "http", feature = "http10"))))]
244    pub fn header_value<V: HeaderValue>(mut self, value: &V) -> Self {
245        if self.complete() {
246            return self;
247        }
248
249        match value.to_str() {
250            Ok(v) => self = self.challenges(v),
251            Err(_) if matches!(self.0, None) => self.0 = Some(Err("non-ASCII header value".into())),
252            _ => {}
253        }
254
255        self
256    }
257
258    /// Returns true if no more challenges need to be examined.
259    #[cfg(feature = "digest-scheme")]
260    fn complete(&self) -> bool {
261        matches!(self.0, Some(Ok(PasswordClient::Digest(_))))
262    }
263
264    /// Returns true if no more challenges need to be examined.
265    #[cfg(not(feature = "digest-scheme"))]
266    fn complete(&self) -> bool {
267        matches!(self.0, Some(Ok(_)))
268    }
269
270    /// Considers all challenges from the given `&str` challenge list.
271    pub fn challenges(mut self, value: &str) -> Self {
272        let mut parser = ChallengeParser::new(value);
273        while !self.complete() {
274            match parser.next() {
275                Some(Ok(c)) => self = self.challenge(&c),
276                Some(Err(e)) if self.0.is_none() => self.0 = Some(Err(e.to_string())),
277                _ => break,
278            }
279        }
280        self
281    }
282
283    /// Considers a single challenge.
284    pub fn challenge(mut self, challenge: &ChallengeRef<'_>) -> Self {
285        if self.complete() {
286            return self;
287        }
288
289        #[cfg(feature = "digest-scheme")]
290        if challenge.scheme.eq_ignore_ascii_case("Digest") {
291            match DigestClient::try_from(challenge) {
292                Ok(c) => self.0 = Some(Ok(PasswordClient::Digest(c))),
293                Err(e) if self.0.is_none() => self.0 = Some(Err(e)),
294                _ => {}
295            }
296            return self;
297        }
298
299        #[cfg(feature = "basic-scheme")]
300        if challenge.scheme.eq_ignore_ascii_case("Basic") && !matches!(self.0, Some(Ok(_))) {
301            match BasicClient::try_from(challenge) {
302                Ok(c) => self.0 = Some(Ok(PasswordClient::Basic(c))),
303                Err(e) if self.0.is_none() => self.0 = Some(Err(e)),
304                _ => {}
305            }
306            return self;
307        }
308
309        if self.0.is_none() {
310            self.0 = Some(Err(format!("Unsupported scheme {:?}", challenge.scheme)));
311        }
312
313        self
314    }
315
316    /// Returns a new [`PasswordClient`] or fails.
317    pub fn build(self) -> Result<PasswordClient, String> {
318        self.0.unwrap_or_else(|| Err("no challenges given".into()))
319    }
320}
321
322/// Client for responding to a password challenge.
323///
324/// Typically created via [`TryFrom`] implementations for a parsed challenge
325/// ([`crate::ChallengeRef`]) or unparsed challenges (`str`,
326/// [`http::header::HeaderValue`], or [`http::header::GetAll`]). See full
327/// example in the [crate-level documentation](crate).
328///
329/// For more complex scenarios, see [`PasswordClientBuilder`].
330#[derive(Debug, Eq, PartialEq)]
331#[non_exhaustive]
332pub enum PasswordClient {
333    #[cfg(feature = "basic-scheme")]
334    #[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))]
335    Basic(BasicClient),
336
337    #[cfg(feature = "digest-scheme")]
338    #[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))]
339    Digest(DigestClient),
340}
341
342/// Tries to create a `PasswordClient` from the single supplied challenge.
343///
344/// This is a convenience wrapper around [`PasswordClientBuilder`].
345impl TryFrom<&ChallengeRef<'_>> for PasswordClient {
346    type Error = String;
347
348    fn try_from(value: &ChallengeRef<'_>) -> Result<Self, Self::Error> {
349        #[cfg(feature = "basic-scheme")]
350        if value.scheme.eq_ignore_ascii_case("Basic") {
351            return Ok(PasswordClient::Basic(BasicClient::try_from(value)?));
352        }
353        #[cfg(feature = "digest-scheme")]
354        if value.scheme.eq_ignore_ascii_case("Digest") {
355            return Ok(PasswordClient::Digest(DigestClient::try_from(value)?));
356        }
357
358        Err(format!("unsupported challenge scheme {:?}", value.scheme))
359    }
360}
361
362/// Tries to create a `PasswordClient` from the supplied `str` challenge list.
363///
364/// This is a convenience wrapper around [`PasswordClientBuilder`].
365impl TryFrom<&str> for PasswordClient {
366    type Error = String;
367
368    #[inline]
369    fn try_from(value: &str) -> Result<Self, Self::Error> {
370        PasswordClient::builder().challenges(value).build()
371    }
372}
373
374/// Tries to create a `PasswordClient` from the supplied `HeaderValue` challenge list.
375///
376/// This is a convenience wrapper around [`PasswordClientBuilder`].
377#[cfg(feature = "http")]
378#[cfg_attr(docsrs, doc(cfg(feature = "http")))]
379impl TryFrom<&http::HeaderValue> for PasswordClient {
380    type Error = String;
381
382    #[inline]
383    fn try_from(value: &http::HeaderValue) -> Result<Self, Self::Error> {
384        PasswordClient::builder().header_value(value).build()
385    }
386}
387
388/// Tries to create a `PasswordClient` from the supplied `HeaderValue` challenge list.
389///
390/// This is a convenience wrapper around [`PasswordClientBuilder`].
391#[cfg(feature = "http10")]
392#[cfg_attr(docsrs, doc(cfg(feature = "http10")))]
393impl TryFrom<&http10::HeaderValue> for PasswordClient {
394    type Error = String;
395
396    #[inline]
397    fn try_from(value: &http10::HeaderValue) -> Result<Self, Self::Error> {
398        PasswordClient::builder().header_value(value).build()
399    }
400}
401
402/// Tries to create a `PasswordClient` from the supplied `http::header::GetAll` challenge lists.
403///
404/// This is a convenience wrapper around [`PasswordClientBuilder`].
405#[cfg(feature = "http")]
406#[cfg_attr(docsrs, doc(cfg(feature = "http")))]
407impl TryFrom<http::header::GetAll<'_, http::HeaderValue>> for PasswordClient {
408    type Error = String;
409
410    fn try_from(value: http::header::GetAll<'_, http::HeaderValue>) -> Result<Self, Self::Error> {
411        let mut builder = PasswordClient::builder();
412        for v in value {
413            builder = builder.header_value(v);
414        }
415        builder.build()
416    }
417}
418
419/// Tries to create a `PasswordClient` from the supplied `http::header::GetAll` challenge lists.
420///
421/// This is a convenience wrapper around [`PasswordClientBuilder`].
422#[cfg(feature = "http10")]
423#[cfg_attr(docsrs, doc(cfg(feature = "http10")))]
424impl TryFrom<http10::header::GetAll<'_, http10::HeaderValue>> for PasswordClient {
425    type Error = String;
426
427    fn try_from(
428        value: http10::header::GetAll<'_, http10::HeaderValue>,
429    ) -> Result<Self, Self::Error> {
430        let mut builder = PasswordClient::builder();
431        for v in value {
432            builder = builder.header_value(v);
433        }
434        builder.build()
435    }
436}
437
438impl PasswordClient {
439    /// Builds a new `PasswordClient`.
440    ///
441    /// See example at [`PasswordClientBuilder`].
442    pub fn builder() -> PasswordClientBuilder {
443        PasswordClientBuilder::default()
444    }
445
446    /// Responds to the challenge with the supplied parameters.
447    ///
448    /// The caller should use the returned string as an `Authorization` or
449    /// `Proxy-Authorization` header value.
450    #[allow(unused_variables)] // p is unused with no features.
451    pub fn respond(&mut self, p: &PasswordParams) -> Result<String, String> {
452        match self {
453            #[cfg(feature = "basic-scheme")]
454            Self::Basic(c) => Ok(c.respond(p.username, p.password)),
455            #[cfg(feature = "digest-scheme")]
456            Self::Digest(c) => c.respond(p),
457
458            // Rust 1.55 + --no-default-features produces a "non-exhaustive
459            // patterns" error without this. I think this is a rustc bug given
460            // that the enum is empty in this case. Work around it.
461            #[cfg(not(any(feature = "basic-scheme", feature = "digest-scheme")))]
462            _ => unreachable!(),
463        }
464    }
465}
466
467/// Parameters for responding to a password challenge.
468///
469/// This is cheap to construct; callers generally use a fresh `PasswordParams`
470/// for each request.
471///
472/// The caller is responsible for supplying parameters in the correct
473/// format. Servers may expect character data to be in Unicode Normalization
474/// Form C as noted in [RFC 7617 section
475/// 2.1](https://datatracker.ietf.org/doc/html/rfc7617#section-2.1) for the
476/// `Basic` scheme and [RFC 7616 section
477/// 4](https://datatracker.ietf.org/doc/html/rfc7616#section-4) for the `Digest`
478/// scheme.
479///
480/// Note that most of these fields are only needed for [`DigestClient`]. Callers
481/// that only care about the `Basic` challenge scheme can use
482/// [`BasicClient::respond`] directly with only username and password.
483#[derive(Copy, Clone, Debug, Eq, PartialEq)]
484pub struct PasswordParams<'a> {
485    pub username: &'a str,
486    pub password: &'a str,
487
488    /// The URI from the Request-URI of the Request-Line, as described in
489    /// [RFC 2617 section 3.2.2](https://datatracker.ietf.org/doc/html/rfc2617#section-3.2.2).
490    ///
491    /// [RFC 2617 section
492    /// 3.2.2.5](https://datatracker.ietf.org/doc/html/rfc2617#section-3.2.2.5),
493    /// which says the following:
494    /// > This may be `*`, an `absoluteURL` or an `abs_path` as specified in
495    /// > section 5.1.2 of [RFC 2616](https://datatracker.ietf.org/doc/html/rfc2616),
496    /// > but it MUST agree with the Request-URI. In particular, it MUST
497    /// > be an `absoluteURL` if the Request-URI is an `absoluteURL`.
498    ///
499    /// [RFC 7616 section 3.4](https://datatracker.ietf.org/doc/html/rfc7616#section-3.4)
500    /// describes this as the "Effective Request URI", which is *always* an
501    /// absolute form. This may be a mistake. [Section
502    /// 3.4.6](https://datatracker.ietf.org/doc/html/rfc7616#section-3.4.6)
503    /// matches RFC 2617 section 3.2.2.5, and [Appendix
504    /// A](https://datatracker.ietf.org/doc/html/rfc7616#appendix-A) doesn't
505    /// mention a change from RFC 2617.
506    pub uri: &'a str,
507
508    /// The HTTP method, such as `GET`.
509    ///
510    /// When using the `http` crate, use the return value of
511    /// [`http::Method::as_str`].
512    pub method: &'a str,
513
514    /// The entity body, if available. Use `Some(&[])` for HTTP methods with no
515    /// body.
516    ///
517    /// When `None`, `Digest` challenges will only be able to use
518    /// [`crate::digest::Qop::Auth`], not
519    /// [`crate::digest::Qop::AuthInt`].
520    pub body: Option<&'a [u8]>,
521}
522
523/// Parses a list of challenges into a `Vec`.
524///
525/// Most callers don't need to directly parse; see [`PasswordClient`] instead.
526///
527/// This is a shorthand for `parser::ChallengeParser::new(input).collect()`. Use
528/// [`crate::parser::ChallengeParser`] directly when you want to parse lazily,
529/// avoid allocation, and/or see any well-formed challenges before an error.
530///
531/// ## Example
532///
533/// ```rust
534/// use http_auth::{parse_challenges, ChallengeRef, ParamValue};
535///
536/// // When all challenges are well-formed, returns them.
537/// assert_eq!(
538///     parse_challenges("UnsupportedSchemeA, Basic realm=\"foo\"").unwrap(),
539///     vec![
540///         ChallengeRef {
541///             scheme: "UnsupportedSchemeA",
542///             params: vec![],
543///         },
544///         ChallengeRef {
545///             scheme: "Basic",
546///             params: vec![("realm", ParamValue::try_from_escaped("foo").unwrap())],
547///         },
548///     ],
549/// );
550///
551/// // Returns `Err` if there is a syntax error anywhere in the input.
552/// parse_challenges("UnsupportedSchemeA, Basic realm=\"foo\", error error").unwrap_err();
553/// ```
554#[inline]
555pub fn parse_challenges(input: &str) -> Result<Vec<ChallengeRef>, parser::Error> {
556    parser::ChallengeParser::new(input).collect()
557}
558
559/// Parsed challenge parameter value used within [`ChallengeRef`].
560#[derive(Copy, Clone, Eq, PartialEq)]
561pub struct ParamValue<'i> {
562    /// The number of backslash escapes in a quoted-text parameter; 0 for a plain token.
563    escapes: usize,
564
565    /// The escaped string, which must be pure ASCII (no bytes >= 128) and be
566    /// consistent with `escapes`.
567    escaped: &'i str,
568}
569
570impl<'i> ParamValue<'i> {
571    /// Tries to create a new `ParamValue` from an escaped sequence, primarily for testing.
572    ///
573    /// Validates the sequence and counts the number of escapes.
574    pub fn try_from_escaped(escaped: &'i str) -> Result<Self, String> {
575        let mut escapes = 0;
576        let mut pos = 0;
577        while pos < escaped.len() {
578            let slash = memchr::memchr(b'\\', &escaped.as_bytes()[pos..]).map(|off| pos + off);
579            for i in pos..slash.unwrap_or(escaped.len()) {
580                if (char_classes(escaped.as_bytes()[i]) & C_QDTEXT) == 0 {
581                    return Err(format!("{:?} has non-qdtext at byte {}", escaped, i));
582                }
583            }
584            if let Some(slash) = slash {
585                escapes += 1;
586                if escaped.len() <= slash + 1 {
587                    return Err(format!("{:?} ends at a quoted-pair escape", escaped));
588                }
589                if (char_classes(escaped.as_bytes()[slash + 1]) & C_ESCAPABLE) == 0 {
590                    return Err(format!(
591                        "{:?} has an invalid quote-pair escape at byte {}",
592                        escaped,
593                        slash + 1
594                    ));
595                }
596                pos = slash + 2;
597            } else {
598                break;
599            }
600        }
601        Ok(Self { escaped, escapes })
602    }
603
604    /// Creates a new param, panicking if invariants are not satisfied.
605    /// This is not part of the stable API; it's just for the fuzz tester to use.
606    #[doc(hidden)]
607    pub fn new(escapes: usize, escaped: &'i str) -> Self {
608        let mut pos = 0;
609        for escape in 0..escapes {
610            match memchr::memchr(b'\\', &escaped.as_bytes()[pos..]) {
611                Some(rel_pos) => pos += rel_pos + 2,
612                None => panic!(
613                    "expected {} backslashes in {:?}, ran out after {}",
614                    escapes, escaped, escape
615                ),
616            };
617        }
618        if memchr::memchr(b'\\', &escaped.as_bytes()[pos..]).is_some() {
619            panic!(
620                "expected {} backslashes in {:?}, are more",
621                escapes, escaped
622            );
623        }
624        ParamValue { escapes, escaped }
625    }
626
627    /// Appends the unescaped form of this parameter to the supplied string.
628    pub fn append_unescaped(&self, to: &mut String) {
629        to.reserve(self.escaped.len() - self.escapes);
630        let mut first_unwritten = 0;
631        for _ in 0..self.escapes {
632            let i = match memchr::memchr(b'\\', &self.escaped.as_bytes()[first_unwritten..]) {
633                Some(rel_i) => first_unwritten + rel_i,
634                None => panic!("bad ParamValues; not as many backslash escapes as promised"),
635            };
636            to.push_str(&self.escaped[first_unwritten..i]);
637            to.push_str(&self.escaped[i + 1..i + 2]);
638            first_unwritten = i + 2;
639        }
640        to.push_str(&self.escaped[first_unwritten..]);
641    }
642
643    /// Returns the unescaped length of this parameter; cheap.
644    #[inline]
645    pub fn unescaped_len(&self) -> usize {
646        self.escaped.len() - self.escapes
647    }
648
649    /// Returns the unescaped form of this parameter as a fresh `String`.
650    pub fn to_unescaped(&self) -> String {
651        let mut to = String::new();
652        self.append_unescaped(&mut to);
653        to
654    }
655
656    /// Returns the unescaped form of this parameter, possibly appending it to `scratch`.
657    #[cfg(feature = "digest-scheme")]
658    fn unescaped_with_scratch<'tmp>(&self, scratch: &'tmp mut String) -> &'tmp str
659    where
660        'i: 'tmp,
661    {
662        if self.escapes == 0 {
663            self.escaped
664        } else {
665            let start = scratch.len();
666            self.append_unescaped(scratch);
667            &scratch[start..]
668        }
669    }
670
671    /// Returns the escaped string, unquoted.
672    #[inline]
673    pub fn as_escaped(&self) -> &'i str {
674        self.escaped
675    }
676}
677
678impl<'i> std::fmt::Debug for ParamValue<'i> {
679    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
680        write!(f, "\"{}\"", self.escaped)
681    }
682}
683
684#[cfg(test)]
685mod tests {
686    use crate::ParamValue;
687    use crate::{C_ATTR, C_ESCAPABLE, C_OWS, C_QDTEXT, C_TCHAR};
688
689    /// Prints the character classes of all ASCII bytes from the table.
690    ///
691    /// ```console
692    /// $ cargo test -- --nocapture tests::table
693    /// ```
694    #[test]
695    fn table() {
696        // Print the table to allow human inspection.
697        println!("oct  dec  hex   char      tchar  qdtext  escapable  ows  attr");
698        for b in 0..128 {
699            let classes = crate::char_classes(b);
700            let if_class =
701                |class: u8, label: &'static str| if (classes & class) != 0 { label } else { "" };
702            println!(
703                "{:03o}  {:>3}  0x{:02x}  {:8}  {:5}  {:6}  {:9}  {:3}  {:4}",
704                b,
705                b,
706                b,
707                format!("{:?}", char::from(b)),
708                if_class(C_TCHAR, "tchar"),
709                if_class(C_QDTEXT, "qdtext"),
710                if_class(C_ESCAPABLE, "escapable"),
711                if_class(C_OWS, "ows"),
712                if_class(C_ATTR, "attr")
713            );
714
715            // Do basic sanity checks: all tchar and ows should be qdtext; all
716            // qdtext should be escapable.
717            assert!(classes & (C_TCHAR | C_QDTEXT) != C_TCHAR);
718            assert!(classes & (C_OWS | C_QDTEXT) != C_OWS);
719            assert!(classes & (C_QDTEXT | C_ESCAPABLE) != C_QDTEXT);
720        }
721    }
722
723    #[test]
724    fn try_from_escaped() {
725        assert_eq!(ParamValue::try_from_escaped("").unwrap().escapes, 0);
726        assert_eq!(ParamValue::try_from_escaped("foo").unwrap().escapes, 0);
727        assert_eq!(ParamValue::try_from_escaped("\\\"").unwrap().escapes, 1);
728        assert_eq!(
729            ParamValue::try_from_escaped("foo\\\"bar").unwrap().escapes,
730            1
731        );
732        assert_eq!(
733            ParamValue::try_from_escaped("foo\\\"bar\\\"baz")
734                .unwrap()
735                .escapes,
736            2
737        );
738        ParamValue::try_from_escaped("\\").unwrap_err(); // ends in slash
739        ParamValue::try_from_escaped("\"").unwrap_err(); // not valid qdtext
740        ParamValue::try_from_escaped("\n").unwrap_err(); // not valid qdtext
741        ParamValue::try_from_escaped("\\\n").unwrap_err(); // not valid escape
742    }
743
744    #[test]
745    fn unescape() {
746        assert_eq!(
747            &ParamValue {
748                escapes: 0,
749                escaped: ""
750            }
751            .to_unescaped(),
752            ""
753        );
754        assert_eq!(
755            &ParamValue {
756                escapes: 0,
757                escaped: "foo"
758            }
759            .to_unescaped(),
760            "foo"
761        );
762        assert_eq!(
763            &ParamValue {
764                escapes: 1,
765                escaped: "\\foo"
766            }
767            .to_unescaped(),
768            "foo"
769        );
770        assert_eq!(
771            &ParamValue {
772                escapes: 1,
773                escaped: "fo\\o"
774            }
775            .to_unescaped(),
776            "foo"
777        );
778        assert_eq!(
779            &ParamValue {
780                escapes: 1,
781                escaped: "foo\\bar"
782            }
783            .to_unescaped(),
784            "foobar"
785        );
786        assert_eq!(
787            &ParamValue {
788                escapes: 3,
789                escaped: "\\foo\\ba\\r"
790            }
791            .to_unescaped(),
792            "foobar"
793        );
794    }
795}