Coverage Report

Created: 2026-10-28 06:40

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/rust/registry/src/index.crates.io-1949cf8c6b5b557f/ureq-3.5.0/src/util.rs
Line
Count
Source
1
use std::convert::TryFrom;
2
use std::io::{self, ErrorKind};
3
use std::{fmt, iter};
4
5
use http::header::{ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING};
6
use http::header::{CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, CONTENT_TYPE};
7
use http::header::{DATE, HOST, LOCATION, SERVER, TRANSFER_ENCODING, USER_AGENT};
8
use http::uri::{Authority, Scheme};
9
use http::{HeaderMap, HeaderName, HeaderValue, Method, Response, Uri, Version};
10
11
use crate::Error;
12
use crate::http;
13
use crate::proxy::ProxyProtocol;
14
15
pub(crate) mod private {
16
    pub trait Private {}
17
}
18
19
pub(crate) trait AuthorityExt {
20
    fn userinfo(&self) -> Option<&str>;
21
    fn username(&self) -> Option<&str>;
22
    fn password(&self) -> Option<&str>;
23
    #[allow(unused)]
24
    fn host_bare(&self) -> &str;
25
}
26
27
// NB: Treating &str with direct indexes is OK, since Uri parsed the Authority,
28
// and ensured it's all ASCII (or %-encoded).
29
impl AuthorityExt for Authority {
30
0
    fn userinfo(&self) -> Option<&str> {
31
0
        let s = self.as_str();
32
0
        s.rfind('@').map(|i| &s[..i])
33
0
    }
34
35
0
    fn username(&self) -> Option<&str> {
36
0
        self.userinfo()
37
0
            .map(|a| a.find(':').map(|i| &a[..i]).unwrap_or(a))
38
0
    }
39
40
0
    fn password(&self) -> Option<&str> {
41
0
        self.userinfo()
42
0
            .and_then(|a| a.find(':').map(|i| &a[i + 1..]))
43
0
    }
44
45
    // Per RFC 3986, IPv6 literals in a URI authority are wrapped in square
46
    // brackets (e.g. `[::1]`). `Authority::host()` returns that bracketed
47
    // form. Consumers like rustls' `ServerName` expect the bare form and
48
    // reject the brackets, so strip them here.
49
0
    fn host_bare(&self) -> &str {
50
0
        let host = self.host();
51
0
        host.strip_prefix('[')
52
0
            .and_then(|s| s.strip_suffix(']'))
53
0
            .unwrap_or(host)
54
0
    }
55
}
56
57
pub(crate) trait SchemeExt {
58
    fn default_port(&self) -> Option<u16>;
59
}
60
61
impl SchemeExt for Scheme {
62
0
    fn default_port(&self) -> Option<u16> {
63
0
        if *self == Scheme::HTTPS {
64
0
            Some(443)
65
0
        } else if *self == Scheme::HTTP {
66
0
            Some(80)
67
0
        } else if let Ok(proxy) = ProxyProtocol::try_from(self.as_str()) {
68
0
            Some(proxy.default_port())
69
        } else {
70
0
            debug!("Unknown scheme: {}", self);
71
0
            None
72
        }
73
0
    }
74
}
75
76
/// Windows causes kind `TimedOut` while unix does `WouldBlock`. Since we are not
77
/// using non-blocking streams, we normalize `WouldBlock` -> `TimedOut`.
78
pub(crate) trait IoResultExt {
79
    fn normalize_would_block(self) -> Self;
80
}
81
82
impl<T> IoResultExt for io::Result<T> {
83
0
    fn normalize_would_block(self) -> Self {
84
0
        match self {
85
0
            Ok(v) => Ok(v),
86
0
            Err(e) if e.kind() == ErrorKind::WouldBlock => {
87
0
                Err(io::Error::new(ErrorKind::TimedOut, e))
88
            }
89
0
            Err(e) => Err(e),
90
        }
91
0
    }
Unexecuted instantiation: <core::result::Result<std::net::tcp::TcpStream, std::io::error::Error> as ureq::util::IoResultExt>::normalize_would_block
Unexecuted instantiation: <core::result::Result<usize, std::io::error::Error> as ureq::util::IoResultExt>::normalize_would_block
Unexecuted instantiation: <core::result::Result<(), std::io::error::Error> as ureq::util::IoResultExt>::normalize_would_block
92
}
93
94
#[derive(Debug)]
95
pub(crate) struct ConsumeBuf {
96
    buf: Vec<u8>,
97
    filled: usize,
98
    consumed: usize,
99
}
100
101
impl ConsumeBuf {
102
0
    pub fn new(size: usize) -> Self {
103
0
        ConsumeBuf {
104
0
            buf: vec![0; size],
105
0
            filled: 0,
106
0
            consumed: 0,
107
0
        }
108
0
    }
109
110
0
    pub fn resize(&mut self, size: usize) {
111
0
        if size > 100 * 1024 * 1024 {
112
0
            panic!("ConsumeBuf grown to unreasonable size (>100MB)");
113
0
        }
114
0
        self.buf.resize(size, 0);
115
0
    }
116
117
0
    pub fn add_space(&mut self, size: usize) {
118
0
        if size == 0 {
119
0
            return;
120
0
        }
121
0
        let wanted = self.buf.len() + size;
122
0
        self.resize(wanted);
123
0
    }
124
125
0
    pub fn free_mut(&mut self) -> &mut [u8] {
126
0
        self.maybe_shift();
127
0
        &mut self.buf[self.filled..]
128
0
    }
129
130
0
    pub fn add_filled(&mut self, amount: usize) {
131
0
        self.filled += amount;
132
0
        assert!(self.filled <= self.buf.len());
133
0
    }
134
135
0
    pub fn unconsumed(&self) -> &[u8] {
136
0
        &self.buf[self.consumed..self.filled]
137
0
    }
138
139
0
    pub fn unconsumed_mut(&mut self) -> &mut [u8] {
140
0
        &mut self.buf[self.consumed..self.filled]
141
0
    }
142
143
0
    pub fn consume(&mut self, amount: usize) {
144
0
        self.consumed += amount;
145
0
        assert!(self.consumed <= self.filled);
146
0
    }
147
148
0
    fn maybe_shift(&mut self) {
149
0
        if self.consumed == 0 {
150
0
            return;
151
0
        }
152
153
0
        if self.consumed == self.filled {
154
0
            self.consumed = 0;
155
0
            self.filled = 0;
156
0
        } else if self.filled > self.buf.len() / 2 {
157
0
            self.buf.copy_within(self.consumed..self.filled, 0);
158
0
            self.filled -= self.consumed;
159
0
            self.consumed = 0;
160
0
        }
161
0
    }
162
}
163
164
/// Wrapper to only log non-sensitive data.
165
pub(crate) struct DebugRequest<'a> {
166
    pub method: &'a Method,
167
    pub uri: &'a Uri,
168
    pub version: Version,
169
    pub headers: HeaderMap<HeaderValue>,
170
}
171
172
impl<'a> fmt::Debug for DebugRequest<'a> {
173
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174
0
        f.debug_struct("Request")
175
0
            .field("method", &self.method)
176
0
            .field("uri", &DebugUri(self.uri))
177
0
            .field("version", &self.version)
178
0
            .field("headers", &DebugHeaders(&self.headers))
179
0
            .finish()
180
0
    }
181
}
182
183
/// Wrapper to only log non-sensitive data.
184
pub(crate) struct DebugResponse<'a, B>(pub &'a Response<B>);
185
186
impl<'a, B> fmt::Debug for DebugResponse<'a, B> {
187
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188
0
        f.debug_struct("Response")
189
0
            .field("status", &self.0.status())
190
0
            .field("version", &self.0.version())
191
0
            .field("headers", &DebugHeaders(self.0.headers()))
192
0
            .finish()
193
0
    }
194
}
195
196
pub(crate) struct DebugHeaders<'a>(pub &'a HeaderMap);
197
198
const NON_SENSITIVE_HEADERS: &[HeaderName] = &[
199
    DATE,
200
    CONTENT_TYPE,
201
    CONTENT_LENGTH,
202
    TRANSFER_ENCODING,
203
    CONNECTION,
204
    CONTENT_ENCODING,
205
    HOST,
206
    ACCEPT,
207
    ACCEPT_ENCODING,
208
    ACCEPT_CHARSET,
209
    SERVER,
210
    USER_AGENT,
211
    // LOCATION is also logged in a redacted form
212
];
213
214
impl<'a> fmt::Debug for DebugHeaders<'a> {
215
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216
0
        let mut debug = f.debug_map();
217
218
        static REDACTED_LOCATION: HeaderValue = HeaderValue::from_static("******");
219
0
        let has_location = self.0.has_location();
220
221
0
        let filtered_headers = self
222
0
            .0
223
0
            .iter()
224
0
            .filter(|(name, _)| NON_SENSITIVE_HEADERS.contains(name));
225
226
0
        if has_location {
227
0
            let location_header = if log_enabled!(log::Level::Trace) {
228
0
                iter::once((&LOCATION, self.0.get(LOCATION).unwrap()))
229
            } else {
230
0
                iter::once((&LOCATION, &REDACTED_LOCATION))
231
            };
232
0
            debug.entries(filtered_headers.chain(location_header));
233
0
        } else {
234
0
            debug.entries(filtered_headers);
235
0
        }
236
237
0
        let redact_count = self
238
0
            .0
239
0
            .iter()
240
0
            .filter(|(name, _)| {
241
                // println!("{}", name);
242
0
                !NON_SENSITIVE_HEADERS.contains(name)
243
0
            })
244
0
            .count()
245
            // location is logged, but redacted, so do not include in the count
246
0
            - if has_location { 1 } else { 0 };
247
248
0
        if redact_count > 0 {
249
0
            debug.entry(
250
0
                &"<NOTICE>",
251
0
                &format!("{} HEADERS ARE REDACTED", redact_count),
252
0
            );
253
0
        }
254
255
0
        debug.finish()
256
0
    }
257
}
258
259
/// Wrapper to only log non-sensitive data.
260
pub(crate) struct DebugUri<'a>(pub &'a Uri);
261
262
impl<'a> fmt::Debug for DebugUri<'a> {
263
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264
0
        if let Some(s) = self.0.scheme_str() {
265
0
            write!(f, "{}://", s)?;
266
0
        }
267
268
0
        if let Some(a) = self.0.authority() {
269
0
            write!(f, "{:?}", DebugAuthority(a))?;
270
0
        }
271
272
0
        if let Some(q) = self.0.path_and_query() {
273
0
            if log_enabled!(log::Level::Trace) {
274
0
                write!(f, "{}", q)?;
275
            } else {
276
0
                write!(f, "/******")?;
277
            }
278
0
        }
279
280
0
        Ok(())
281
0
    }
282
}
283
284
pub(crate) struct DebugAuthority<'a>(pub &'a Authority);
285
286
impl<'a> fmt::Debug for DebugAuthority<'a> {
287
0
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
288
0
        let mut at = false;
289
290
0
        if let Some(u) = self.0.username() {
291
0
            at = true;
292
0
            if let Some(x) = u.chars().next() {
293
0
                write!(f, "{}*****", x)?;
294
0
            }
295
0
        }
296
297
0
        if self.0.password().is_some() {
298
0
            at = true;
299
0
            write!(f, ":******")?;
300
0
        }
301
302
0
        if at {
303
0
            write!(f, "@")?;
304
0
        }
305
306
0
        write!(f, "{}", self.0.host())?;
307
308
0
        if let Some(p) = self.0.port_u16() {
309
0
            write!(f, ":{}", p)?;
310
0
        }
311
312
0
        Ok(())
313
0
    }
314
}
315
316
pub(crate) trait UriExt {
317
    fn ensure_valid_url(&self) -> Result<(), Error>;
318
319
    #[cfg(feature = "_url")]
320
    fn try_into_url(&self) -> Result<url::Url, Error>;
321
322
    #[allow(unused)]
323
    fn host_port(&self) -> (&str, u16);
324
}
325
326
impl UriExt for Uri {
327
0
    fn ensure_valid_url(&self) -> Result<(), Error> {
328
0
        let scheme = self
329
0
            .scheme()
330
0
            .ok_or_else(|| Error::BadUri(format!("{} is missing scheme", self)))?;
331
332
0
        scheme
333
0
            .default_port()
334
0
            .ok_or_else(|| Error::BadUri(format!("unknown scheme: {}", scheme)))?;
335
336
0
        self.authority()
337
0
            .ok_or_else(|| Error::BadUri(format!("{} is missing host", self)))?;
338
339
0
        Ok(())
340
0
    }
341
342
    #[cfg(feature = "_url")]
343
    fn try_into_url(&self) -> Result<url::Url, Error> {
344
        self.ensure_valid_url()?;
345
        let uri = self.to_string();
346
347
        // If ensure_full_url() works, we expect to be able to parse it to a url
348
        let url = url::Url::parse(&uri).expect("parsed url");
349
350
        Ok(url)
351
    }
352
353
0
    fn host_port(&self) -> (&str, u16) {
354
0
        (
355
0
            self.host().unwrap(),
356
0
            self.port_u16()
357
0
                .unwrap_or(self.scheme().unwrap().default_port().unwrap_or(80)),
358
0
        )
359
0
    }
360
}
361
362
pub(crate) trait HeaderMapExt {
363
    fn get_str(&self, k: HeaderName) -> Option<&str>;
364
    fn is_chunked(&self) -> bool;
365
    fn content_length(&self) -> Option<u64>;
366
    fn has_accept_encoding(&self) -> bool;
367
    fn has_user_agent(&self) -> bool;
368
0
    fn has_send_body_mode(&self) -> bool {
369
0
        self.is_chunked() || self.content_length().is_some()
370
0
    }
371
    fn has_accept(&self) -> bool;
372
    fn has_content_type(&self) -> bool;
373
    fn has_location(&self) -> bool;
374
}
375
376
impl HeaderMapExt for HeaderMap {
377
0
    fn get_str(&self, k: HeaderName) -> Option<&str> {
378
0
        self.get(k).and_then(|v| v.to_str().ok())
379
0
    }
380
381
0
    fn is_chunked(&self) -> bool {
382
0
        self.get_str(TRANSFER_ENCODING)
383
0
            .map(|v| v.contains("chunked"))
384
0
            .unwrap_or(false)
385
0
    }
386
387
0
    fn content_length(&self) -> Option<u64> {
388
0
        let h = self.get_str(CONTENT_LENGTH)?;
389
0
        let len: u64 = h.parse().ok()?;
390
0
        Some(len)
391
0
    }
392
393
0
    fn has_accept_encoding(&self) -> bool {
394
0
        self.contains_key(ACCEPT_ENCODING)
395
0
    }
396
397
0
    fn has_user_agent(&self) -> bool {
398
0
        self.contains_key(USER_AGENT)
399
0
    }
400
401
0
    fn has_accept(&self) -> bool {
402
0
        self.contains_key(ACCEPT)
403
0
    }
404
405
0
    fn has_content_type(&self) -> bool {
406
0
        self.contains_key(CONTENT_TYPE)
407
0
    }
408
409
0
    fn has_location(&self) -> bool {
410
0
        self.contains_key(LOCATION)
411
0
    }
412
}
413
414
#[cfg(test)]
415
mod tests {
416
    use super::*;
417
418
    fn authority(uri: &str) -> Authority {
419
        uri.parse::<Uri>().unwrap().authority().unwrap().clone()
420
    }
421
422
    #[test]
423
    fn host_bare_strips_ipv6_brackets() {
424
        assert_eq!(authority("https://[::1]/").host_bare(), "::1");
425
        assert_eq!(
426
            authority("https://[fe80::1000:ff:fe00:1234]:8443/").host_bare(),
427
            "fe80::1000:ff:fe00:1234"
428
        );
429
    }
430
431
    #[test]
432
    fn host_bare_leaves_dns_and_ipv4_untouched() {
433
        assert_eq!(authority("https://example.com/").host_bare(), "example.com");
434
        assert_eq!(
435
            authority("https://127.0.0.1:8443/").host_bare(),
436
            "127.0.0.1"
437
        );
438
    }
439
}