]> git.proxmox.com Git - rustc.git/blob - vendor/http-auth/src/lib.rs
New upstream version 1.70.0+dfsg2
[rustc.git] / vendor / http-auth / src / 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 |
26 //!
27 //! ## Example
28 //!
29 //! In most cases, callers only need to use [`PasswordClient`] and
30 //! [`PasswordParams`] to handle `Basic` and `Digest` authentication schemes.
31 //!
32 #![cfg_attr(
33 feature = "http",
34 doc = r##"
35 ```rust
36 use std::convert::TryFrom as _;
37 use http_auth::PasswordClient;
38
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 {
43 username: "Aladdin",
44 password: "open sesame",
45 uri: "/",
46 method: "GET",
47 body: Some(&[]),
48 }).unwrap();
49 assert_eq!(response, "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
50 ```
51 "##
52 )]
53 //!
54 //! The `http` feature allows parsing all `WWW-Authenticate` headers within a
55 //! [`http::HeaderMap`] in one call.
56 //!
57 #![cfg_attr(
58 feature = "http",
59 doc = r##"
60 ```rust
61 # use std::convert::TryFrom as _;
62 use http::header::{HeaderMap, WWW_AUTHENTICATE};
63 # use http_auth::PasswordClient;
64
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());
68
69 let mut pw_client = PasswordClient::try_from(headers.get_all(WWW_AUTHENTICATE)).unwrap();
70 assert!(matches!(pw_client, http_auth::PasswordClient::Basic(_)));
71 ```
72 "##
73 )]
74 #![cfg_attr(docsrs, feature(doc_cfg))]
75
76 use std::convert::TryFrom;
77
78 pub mod parser;
79
80 #[cfg(feature = "basic-scheme")]
81 #[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))]
82 pub mod basic;
83
84 #[cfg(feature = "digest-scheme")]
85 #[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))]
86 pub mod digest;
87
88 pub use parser::ChallengeParser;
89
90 #[cfg(feature = "basic-scheme")]
91 #[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))]
92 pub use crate::basic::BasicClient;
93
94 #[cfg(feature = "digest-scheme")]
95 #[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))]
96 pub use crate::digest::DigestClient;
97
98 // Must match build.rs exactly.
99 const C_TCHAR: u8 = 1;
100 const C_QDTEXT: u8 = 2;
101 const C_ESCAPABLE: u8 = 4;
102 const C_OWS: u8 = 8;
103
104 #[cfg_attr(not(feature = "digest-scheme"), allow(unused))]
105 const C_ATTR: u8 = 16;
106
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)
112 }
113
114 /// Parsed challenge (scheme and body) using references to the original header value.
115 /// Produced by [`crate::parser::ChallengeParser`].
116 ///
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`.
121 ///
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.
127 pub scheme: &'i str,
128
129 /// Zero or more parameters.
130 ///
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>>,
137 }
138
139 impl<'i> ChallengeRef<'i> {
140 pub fn new(scheme: &'i str) -> Self {
141 ChallengeRef {
142 scheme,
143 params: Vec::new(),
144 }
145 }
146 }
147
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))
153 .finish()
154 }
155 }
156
157 type ChallengeParamRef<'i> = (&'i str, ParamValue<'i>);
158
159 struct ParamsPrinter<'i>(&'i [ChallengeParamRef<'i>]);
160
161 impl<'i> std::fmt::Debug for ParamsPrinter<'i> {
162 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
163 f.debug_map()
164 .entries(self.0.iter().map(|&(ref k, ref v)| (k, v)))
165 .finish()
166 }
167 }
168
169 /// Builds a [`PasswordClient`] from the supplied challenges; create via
170 /// [`PasswordClient::builder`].
171 ///
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`]).
176 ///
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`].
181 ///
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.
185 ///
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.
191 ///
192 /// Ignores parse errors as long as there's at least one parseable, supported
193 /// challenge.
194 ///
195 /// ## Example
196 ///
197 #[cfg_attr(
198 feature = "digest",
199 doc = r##"
200 ```rust
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\", \
207 algorithm=MD5, \
208 nonce=\"7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v\", \
209 opaque=\"FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS\"")
210 .build()
211 .unwrap();
212 assert!(matches!(client, PasswordClient::Digest(_)));
213 ```
214 "##
215 )]
216 #[derive(Default)]
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>>,
223 );
224
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 {
230 if self.complete() {
231 return self;
232 }
233
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())),
237 _ => {}
238 }
239
240 self
241 }
242
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(_))))
247 }
248
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(_)))
253 }
254
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())),
262 _ => break,
263 }
264 }
265 self
266 }
267
268 /// Considers a single challenge.
269 pub fn challenge(mut self, challenge: &ChallengeRef<'_>) -> Self {
270 if self.complete() {
271 return self;
272 }
273
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)),
279 _ => {}
280 }
281 return self;
282 }
283
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)),
289 _ => {}
290 }
291 return self;
292 }
293
294 if self.0.is_none() {
295 self.0 = Some(Err(format!("Unsupported scheme {:?}", challenge.scheme)));
296 }
297
298 self
299 }
300
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()))
304 }
305 }
306
307 /// Client for responding to a password challenge.
308 ///
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).
313 ///
314 /// For more complex scenarios, see [`PasswordClientBuilder`].
315 #[derive(Debug, Eq, PartialEq)]
316 #[non_exhaustive]
317 pub enum PasswordClient {
318 #[cfg(feature = "basic-scheme")]
319 #[cfg_attr(docsrs, doc(cfg(feature = "basic-scheme")))]
320 Basic(BasicClient),
321
322 #[cfg(feature = "digest-scheme")]
323 #[cfg_attr(docsrs, doc(cfg(feature = "digest-scheme")))]
324 Digest(DigestClient),
325 }
326
327 /// Tries to create a `PasswordClient` from the single supplied challenge.
328 ///
329 /// This is a convenience wrapper around [`PasswordClientBuilder`].
330 impl TryFrom<&ChallengeRef<'_>> for PasswordClient {
331 type Error = String;
332
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)?));
337 }
338 #[cfg(feature = "digest-scheme")]
339 if value.scheme.eq_ignore_ascii_case("Digest") {
340 return Ok(PasswordClient::Digest(DigestClient::try_from(value)?));
341 }
342
343 Err(format!("unsupported challenge scheme {:?}", value.scheme))
344 }
345 }
346
347 /// Tries to create a `PasswordClient` forom the supplied `str` challenge list.
348 ///
349 /// This is a convenience wrapper around [`PasswordClientBuilder`].
350 impl TryFrom<&str> for PasswordClient {
351 type Error = String;
352
353 #[inline]
354 fn try_from(value: &str) -> Result<Self, Self::Error> {
355 PasswordClient::builder().challenges(value).build()
356 }
357 }
358
359 /// Tries to create a `PasswordClient` from the supplied `HeaderValue` challenge list.
360 ///
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 {
365 type Error = String;
366
367 #[inline]
368 fn try_from(value: &http::HeaderValue) -> Result<Self, Self::Error> {
369 PasswordClient::builder().header_value(value).build()
370 }
371 }
372
373 /// Tries to create a `PasswordClient` from the supplied `http::header::GetAll` challenge lists.
374 ///
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 {
379 type Error = String;
380
381 fn try_from(value: http::header::GetAll<'_, http::HeaderValue>) -> Result<Self, Self::Error> {
382 let mut builder = PasswordClient::builder();
383 for v in value {
384 builder = builder.header_value(v);
385 }
386 builder.build()
387 }
388 }
389
390 impl PasswordClient {
391 /// Builds a new `PasswordClient`.
392 ///
393 /// See example at [`PasswordClientBuilder`].
394 pub fn builder() -> PasswordClientBuilder {
395 PasswordClientBuilder::default()
396 }
397
398 /// Responds to the challenge with the supplied parameters.
399 ///
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> {
404 match self {
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),
409
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")))]
414 _ => unreachable!(),
415 }
416 }
417 }
418
419 /// Parameters for responding to a password challenge.
420 ///
421 /// This is cheap to construct; callers generally use a fresh `PasswordParams`
422 /// for each request.
423 ///
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`
430 /// scheme.
431 ///
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,
439
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).
442 ///
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`.
450 ///
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.
458 pub uri: &'a str,
459
460 /// The HTTP method, such as `GET`.
461 ///
462 /// When using the `http` crate, use the return value of
463 /// [`http::Method::as_str`].
464 pub method: &'a str,
465
466 /// The entity body, if available. Use `Some(&[])` for HTTP methods with no
467 /// body.
468 ///
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]>,
473 }
474
475 /// Parses a list of challenges into a `Vec`.
476 ///
477 /// Most callers don't need to directly parse; see [`PasswordClient`] instead.
478 ///
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.
482 ///
483 /// ## Example
484 ///
485 /// ```rust
486 /// use http_auth::{parse_challenges, ChallengeRef, ParamValue};
487 ///
488 /// // When all challenges are well-formed, returns them.
489 /// assert_eq!(
490 /// parse_challenges("UnsupportedSchemeA, Basic realm=\"foo\"").unwrap(),
491 /// vec![
492 /// ChallengeRef {
493 /// scheme: "UnsupportedSchemeA",
494 /// params: vec![],
495 /// },
496 /// ChallengeRef {
497 /// scheme: "Basic",
498 /// params: vec![("realm", ParamValue::try_from_escaped("foo").unwrap())],
499 /// },
500 /// ],
501 /// );
502 ///
503 /// // Returns `Err` if there is a syntax error anywhere in the input.
504 /// parse_challenges("UnsupportedSchemeA, Basic realm=\"foo\", error error").unwrap_err();
505 /// ```
506 #[inline]
507 pub fn parse_challenges(input: &str) -> Result<Vec<ChallengeRef>, parser::Error> {
508 parser::ChallengeParser::new(input).collect()
509 }
510
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.
515 escapes: usize,
516
517 /// The escaped string, which must be pure ASCII (no bytes >= 128) and be
518 /// consistent with `escapes`.
519 escaped: &'i str,
520 }
521
522 impl<'i> ParamValue<'i> {
523 /// Tries to create a new `ParamValue` from an escaped sequence, primarily for testing.
524 ///
525 /// Validates the sequence and counts the number of escapes.
526 pub fn try_from_escaped(escaped: &'i str) -> Result<Self, String> {
527 let mut escapes = 0;
528 let mut pos = 0;
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));
534 }
535 }
536 if let Some(slash) = slash {
537 escapes += 1;
538 if escaped.len() <= slash + 1 {
539 return Err(format!("{:?} ends at a quoted-pair escape", escaped));
540 }
541 if (char_classes(escaped.as_bytes()[slash + 1]) & C_ESCAPABLE) == 0 {
542 return Err(format!(
543 "{:?} has an invalid quote-pair escape at byte {}",
544 escaped,
545 slash + 1
546 ));
547 }
548 pos = slash + 2;
549 } else {
550 break;
551 }
552 }
553 Ok(Self { escaped, escapes })
554 }
555
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.
558 #[doc(hidden)]
559 pub fn new(escapes: usize, escaped: &'i str) -> Self {
560 let mut pos = 0;
561 for escape in 0..escapes {
562 match memchr::memchr(b'\\', &escaped.as_bytes()[pos..]) {
563 Some(rel_pos) => pos += rel_pos + 2,
564 None => panic!(
565 "expected {} backslashes in {:?}, ran out after {}",
566 escapes, escaped, escape
567 ),
568 };
569 }
570 if memchr::memchr(b'\\', &escaped.as_bytes()[pos..]).is_some() {
571 panic!(
572 "expected {} backslashes in {:?}, are more",
573 escapes, escaped
574 );
575 }
576 ParamValue { escapes, escaped }
577 }
578
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"),
587 };
588 to.push_str(&self.escaped[first_unwritten..i]);
589 to.push_str(&self.escaped[i + 1..i + 2]);
590 first_unwritten = i + 2;
591 }
592 to.push_str(&self.escaped[first_unwritten..]);
593 }
594
595 /// Returns the unescaped length of this parameter; cheap.
596 #[inline]
597 pub fn unescaped_len(&self) -> usize {
598 self.escaped.len() - self.escapes
599 }
600
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);
605 to
606 }
607
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
611 where
612 'i: 'tmp,
613 {
614 if self.escapes == 0 {
615 self.escaped
616 } else {
617 let start = scratch.len();
618 self.append_unescaped(scratch);
619 &scratch[start..]
620 }
621 }
622
623 /// Returns the escaped string, unquoted.
624 #[inline]
625 pub fn as_escaped(&self) -> &'i str {
626 self.escaped
627 }
628 }
629
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)
633 }
634 }
635
636 #[cfg(test)]
637 mod tests {
638 use crate::ParamValue;
639 use crate::{C_ATTR, C_ESCAPABLE, C_OWS, C_QDTEXT, C_TCHAR};
640
641 /// Prints the character classes of all ASCII bytes from the table.
642 ///
643 /// ```console
644 /// $ cargo test -- --nocapture tests::table
645 /// ```
646 #[test]
647 fn table() {
648 // Print the table to allow human inspection.
649 println!("oct dec hex char tchar qdtext escapable ows attr");
650 for b in 0..128 {
651 let classes = crate::char_classes(b);
652 let if_class =
653 |class: u8, label: &'static str| if (classes & class) != 0 { label } else { "" };
654 println!(
655 "{:03o} {:>3} 0x{:02x} {:8} {:5} {:6} {:9} {:3} {:4}",
656 b,
657 b,
658 b,
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")
665 );
666
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);
672 }
673 }
674
675 #[test]
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);
680 assert_eq!(
681 ParamValue::try_from_escaped("foo\\\"bar").unwrap().escapes,
682 1
683 );
684 assert_eq!(
685 ParamValue::try_from_escaped("foo\\\"bar\\\"baz")
686 .unwrap()
687 .escapes,
688 2
689 );
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
694 }
695
696 #[test]
697 fn unescape() {
698 assert_eq!(
699 &ParamValue {
700 escapes: 0,
701 escaped: ""
702 }
703 .to_unescaped(),
704 ""
705 );
706 assert_eq!(
707 &ParamValue {
708 escapes: 0,
709 escaped: "foo"
710 }
711 .to_unescaped(),
712 "foo"
713 );
714 assert_eq!(
715 &ParamValue {
716 escapes: 1,
717 escaped: "\\foo"
718 }
719 .to_unescaped(),
720 "foo"
721 );
722 assert_eq!(
723 &ParamValue {
724 escapes: 1,
725 escaped: "fo\\o"
726 }
727 .to_unescaped(),
728 "foo"
729 );
730 assert_eq!(
731 &ParamValue {
732 escapes: 1,
733 escaped: "foo\\bar"
734 }
735 .to_unescaped(),
736 "foobar"
737 );
738 assert_eq!(
739 &ParamValue {
740 escapes: 3,
741 escaped: "\\foo\\ba\\r"
742 }
743 .to_unescaped(),
744 "foobar"
745 );
746 }
747 }