Repository: algesten/ureq
Pull request #14 (2019-10-07 22:51:57)

Removed IDE metadata files
Quick repository cleanup; added jetbrains-based and vscode metadata directories to .gitignore and removed `.vscode/` from repository root.


Patch:
diff --git a/.gitignore b/.gitignore
index 045a90c..c9c2a87 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
 Cargo.lock
 target
 rls
+.idea/
+.vscode/
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index 3b66410..0000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
-    "git.ignoreLimitWarning": true
-}
\ No newline at end of file
diff --git a/Cargo.toml b/Cargo.toml
index 48d3c48..3a0cff9 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -17,7 +17,6 @@ charset = ["encoding"]
 tls = ["rustls", "webpki", "webpki-roots"]
 
 [dependencies]
-ascii = "1"
 base64 = "0.10"
 chunked_transfer = "1"
 cookie = { version = "0.12", features = ["percent-encode"] }
@@ -29,3 +28,6 @@ webpki = { version = "0.21", optional = true }
 webpki-roots = { version = "0.17", optional = true }
 serde_json = { version = "1", optional = true }
 encoding = { version = "0.2", optional = true }
+
+[package.metadata."docs.rs"]
+all-features = true
diff --git a/README.md b/README.md
index fd8bb24..1882b16 100644
--- a/README.md
+++ b/README.md
@@ -67,24 +67,21 @@ dependencies, this library might change.
 - [ ] Expect 100-continue
 - [x] Use `rustls` when [ring with versioned asm symbols](https://github.com/briansmith/ring/pull/619) is released. (PR is not resolved, but most implementations have settled on 0.13)
 
-## License (MIT)
-
-Copyright (c) 2018 Martin Algesten
-
-Permission is hereby granted, free of charge, to any person obtaining a copy
-of this software and associated documentation files (the "Software"), to deal
-in the Software without restriction, including without limitation the rights
-to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is
-furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all
-copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE.
+## License
+
+Copyright (c) 2019 Martin Algesten
+
+Licensed under either of
+
+ * Apache License, Version 2.0
+   ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
+ * MIT license
+   ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
+
+at your option.
+
+## Contribution
+
+Unless you explicitly state otherwise, any contribution intentionally submitted
+for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
+dual licensed as above, without any additional terms or conditions.
diff --git a/src/agent.rs b/src/agent.rs
index 24cb6c9..b87ab6d 100644
--- a/src/agent.rs
+++ b/src/agent.rs
@@ -284,5 +284,4 @@ mod tests {
             request.set("Foo", "Bar");
         });
     }
-
 }
diff --git a/src/body.rs b/src/body.rs
index 2eba430..02db9fe 100644
--- a/src/body.rs
+++ b/src/body.rs
@@ -95,7 +95,7 @@ impl Payload {
                 let len = bytes.len();
                 let cursor = Cursor::new(bytes);
                 SizedReader::new(Some(len), Box::new(cursor))
-            },
+            }
         }
     }
 }
diff --git a/src/error.rs b/src/error.rs
index a25b73e..1ec8bfd 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -1,5 +1,5 @@
-use std::io::Error as IoError;
 use std::fmt;
+use std::io::Error as IoError;
 
 /// Errors that are translated to ["synthetic" responses](struct.Response.html#method.synthetic).
 #[derive(Debug)]
diff --git a/src/header.rs b/src/header.rs
index 9ebdf88..056a4b8 100644
--- a/src/header.rs
+++ b/src/header.rs
@@ -1,11 +1,10 @@
 use crate::error::Error;
-use ascii::{AsAsciiStr, AsciiString};
 use std::str::FromStr;
 
 #[derive(Clone)]
 /// Wrapper type for a header line.
 pub struct Header {
-    line: AsciiString,
+    line: String,
     index: usize,
 }
 
@@ -17,8 +16,7 @@ impl ::std::fmt::Debug for Header {
 
 impl Header {
     pub fn new(name: &str, value: &str) -> Self {
-        let s = format!("{}: {}", name, value);
-        let line = unsafe { s.as_ascii_str_unchecked().to_owned() };
+        let line = format!("{}: {}", name, value);
         let index = name.len();
         Header { line, index }
     }
@@ -77,8 +75,8 @@ pub fn has_header(headers: &[Header], name: &str) -> bool {
 }
 
 pub fn add_header(headers: &mut Vec<Header>, header: Header) {
-    if !header.name().to_lowercase().starts_with("x-") {
-        let name = header.name();
+    let name = header.name();
+    if !name.starts_with("x-") && !name.starts_with("X-") {
         headers.retain(|h| h.name() != name);
     }
     headers.push(header);
@@ -88,7 +86,7 @@ impl FromStr for Header {
     type Err = Error;
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         //
-        let line = AsciiString::from_str(s).map_err(|_| Error::BadHeader)?;
+        let line = s.to_string();
         let index = s.find(':').ok_or_else(|| Error::BadHeader)?;
 
         // no value?
@@ -96,6 +94,16 @@ impl FromStr for Header {
             return Err(Error::BadHeader);
         }
 
+        // https://www.w3.org/Protocols/rfc2616/rfc2616-sec4.html#sec4.2
+        // The field value MAY be preceded by any amount of LWS, though a single SP is preferred.
+        let value_from = &s[index..];
+        let voff = match value_from.find(|c: char| !c.is_whitespace()) {
+            Some(n) => n,
+            None => 0,
+        };
+
+        let index = index + voff;
+
         Ok(Header { line, index })
     }
 }
diff --git a/src/pool.rs b/src/pool.rs
index 6dff887..b611381 100644
--- a/src/pool.rs
+++ b/src/pool.rs
@@ -52,9 +52,20 @@ struct PoolKey {
 
 impl PoolKey {
     fn new(url: &Url) -> Self {
+        let port = if cfg!(test) {
+            if let Some(p) = url.port_or_known_default() {
+                Some(p)
+            } else if url.scheme() == "test" {
+                Some(42)
+            } else {
+                None
+            }
+        } else {
+            url.port_or_known_default()
+        };
         PoolKey {
             hostname: url.host_str().unwrap_or(DEFAULT_HOST).into(),
-            port: url.port_or_known_default().unwrap_or(0),
+            port: port.expect("Failed to get port for pool key"),
         }
     }
 }
diff --git a/src/response.rs b/src/response.rs
index 910c30f..3976cdc 100644
--- a/src/response.rs
+++ b/src/response.rs
@@ -1,7 +1,6 @@
 use std::io::{Cursor, Error as IoError, ErrorKind, Read, Result as IoResult};
 use std::str::FromStr;
 
-use ascii::AsciiString;
 use chunked_transfer::Decoder as ChunkDecoder;
 
 use crate::error::Error;
@@ -40,14 +39,21 @@ pub const DEFAULT_CHARACTER_SET: &str = "utf-8";
 pub struct Response {
     url: Option<String>,
     error: Option<Error>,
-    status_line: AsciiString,
-    index: (usize, usize), // index into status_line where we split: HTTP/1.1 200 OK
+    status_line: String,
+    index: ResponseStatusIndex,
     status: u16,
     headers: Vec<Header>,
     unit: Option<Unit>,
     stream: Option<Stream>,
 }
 
+/// index into status_line where we split: HTTP/1.1 200 OK
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+struct ResponseStatusIndex {
+    http_version: usize,
+    response_code: usize,
+}
+
 impl ::std::fmt::Debug for Response {
     fn fmt(&self, f: &mut ::std::fmt::Formatter) -> ::std::result::Result<(), ::std::fmt::Error> {
         write!(
@@ -91,7 +97,7 @@ impl Response {
 
     /// The http version: `HTTP/1.1`
     pub fn http_version(&self) -> &str {
-        &self.status_line.as_str()[0..self.index.0]
+        &self.status_line.as_str()[0..self.index.http_version]
     }
 
     /// The status as a u16: `200`
@@ -101,7 +107,7 @@ impl Response {
 
     /// The status text: `OK`
     pub fn status_text(&self) -> &str {
-        &self.status_line.as_str()[self.index.1 + 1..].trim()
+        &self.status_line.as_str()[self.index.response_code + 1..].trim()
     }
 
     /// The header corresponding header value for the give name, if any.
@@ -117,7 +123,7 @@ impl Response {
     pub fn headers_names(&self) -> Vec<String> {
         self.headers
             .iter()
-            .map(|h| h.name().to_ascii_lowercase())
+            .map(|h| h.name().to_lowercase())
             .collect()
     }
 
@@ -294,7 +300,7 @@ impl Response {
 
         let stream = Box::new(self.stream.expect("No reader in response?!"));
         let stream_ptr = Box::into_raw(stream);
-        let mut yolo = YoloRead {
+        let mut reclaiming_read = ReclaimingRead {
             stream: stream_ptr,
             dealloc: false,
         };
@@ -304,16 +310,16 @@ impl Response {
             (true, _) => Box::new(PoolReturnRead::new(
                 unit,
                 stream_ptr,
-                ChunkDecoder::new(yolo),
+                ChunkDecoder::new(reclaiming_read),
             )) as Box<dyn Read>,
             (false, Some(len)) => Box::new(PoolReturnRead::new(
                 unit,
                 stream_ptr,
-                LimitedRead::new(yolo, len),
+                LimitedRead::new(reclaiming_read, len),
             )),
             (false, None) => {
-                yolo.dealloc = true; // dealloc when read drops.
-                Box::new(yolo)
+                reclaiming_read.dealloc = true; // dealloc when read drops.
+                Box::new(reclaiming_read)
             }
         }
     }
@@ -446,7 +452,7 @@ impl Response {
 }
 
 /// parse a line like: HTTP/1.1 200 OK\r\n
-fn parse_status_line(line: &str) -> Result<((usize, usize), u16), Error> {
+fn parse_status_line(line: &str) -> Result<(ResponseStatusIndex, u16), Error> {
     //
 
     let mut split = line.splitn(3, ' ');
@@ -465,7 +471,13 @@ fn parse_status_line(line: &str) -> Result<((usize, usize), u16), Error> {
 
     let status = status.parse::<u16>().map_err(|_| Error::BadStatus)?;
 
-    Ok(((index1, index2), status))
+    Ok((
+        ResponseStatusIndex {
+            http_version: index1,
+            response_code: index2,
+        },
+        status,
+    ))
 }
 
 impl FromStr for Response {
@@ -513,7 +525,7 @@ pub(crate) fn set_stream(resp: &mut Response, url: String, unit: Option<Unit>, s
     resp.stream = Some(stream);
 }
 
-fn read_next_line<R: Read>(reader: &mut R) -> IoResult<AsciiString> {
+fn read_next_line<R: Read>(reader: &mut R) -> IoResult<String> {
     let mut buf = Vec::new();
     let mut prev_byte_was_cr = false;
 
@@ -527,7 +539,7 @@ fn read_next_line<R: Read>(reader: &mut R) -> IoResult<AsciiString> {
 
         if byte == b'\n' && prev_byte_was_cr {
             buf.pop(); // removing the '\r'
-            return AsciiString::from_ascii(buf)
+            return String::from_utf8(buf)
                 .map_err(|_| IoError::new(ErrorKind::InvalidInput, "Header is not in ASCII"));
         }
 
@@ -540,12 +552,16 @@ fn read_next_line<R: Read>(reader: &mut R) -> IoResult<AsciiString> {
 /// Read Wrapper around an (unsafe) pointer to a Stream.
 ///
 /// *Internal API*
-pub(crate) struct YoloRead {
+///
+/// The reason for this is that we wrap our reader in `ChunkDecoder::new` and
+/// that api provides no way for us to get the underlying stream back. We need
+/// to get the stream both for sending responses and for pooling.
+pub(crate) struct ReclaimingRead {
     stream: *mut Stream,
     dealloc: bool, // whether we are to dealloc stream on drop
 }
 
-impl Read for YoloRead {
+impl Read for ReclaimingRead {
     fn read(&mut self, buf: &mut [u8]) -> IoResult<usize> {
         unsafe {
             if self.stream.is_null() {
@@ -563,7 +579,7 @@ impl Read for YoloRead {
     }
 }
 
-impl Drop for YoloRead {
+impl Drop for ReclaimingRead {
     fn drop(&mut self) {
         if self.dealloc && !self.stream.is_null() {
             unsafe {
@@ -573,15 +589,15 @@ impl Drop for YoloRead {
     }
 }
 
-/// Limits a YoloRead to a content size (as set by a "Content-Length" header).
+/// Limits a ReclaimingRead to a content size (as set by a "Content-Length" header).
 struct LimitedRead {
-    reader: YoloRead,
+    reader: ReclaimingRead,
     limit: usize,
     position: usize,
 }
 
 impl LimitedRead {
-    fn new(reader: YoloRead, limit: usize) -> Self {
+    fn new(reader: ReclaimingRead, limit: usize) -> Self {
         LimitedRead {
             reader,
             limit,
diff --git a/src/stream.rs b/src/stream.rs
index 352d4c2..50b293b 100644
--- a/src/stream.rs
+++ b/src/stream.rs
@@ -4,10 +4,10 @@ use std::net::TcpStream;
 use std::net::ToSocketAddrs;
 use std::time::Duration;
 
-#[cfg(feature = "tls")]
-use rustls::StreamOwned;
 #[cfg(feature = "tls")]
 use rustls::ClientSession;
+#[cfg(feature = "tls")]
+use rustls::StreamOwned;
 
 use crate::error::Error;
 use crate::unit::Unit;
@@ -72,7 +72,10 @@ impl Read for Stream {
 }
 
 #[cfg(feature = "tls")]
-fn read_https(stream: &mut StreamOwned<ClientSession, TcpStream>, buf: &mut [u8]) -> IoResult<usize> {
+fn read_https(
+    stream: &mut StreamOwned<ClientSession, TcpStream>,
+    buf: &mut [u8],
+) -> IoResult<usize> {
     match stream.read(buf) {
         Ok(size) => Ok(size),
         Err(ref e) if is_close_notify(e) => Ok(0),
diff --git a/src/unit.rs b/src/unit.rs
index a4984e5..de3f738 100644
--- a/src/unit.rs
+++ b/src/unit.rs
@@ -131,14 +131,14 @@ pub(crate) fn connect(
 
     let send_result = send_prelude(&unit, &mut stream, redir);
 
-    if send_result.is_err() {
+    if let Err(err) = send_result {
         if is_recycled {
             // we try open a new connection, this time there will be
             // no connection in the pool. don't use it.
             return connect(req, unit, false, redirect_count, body, redir);
         } else {
             // not a pooled connection, propagate the error.
-            return Err(send_result.unwrap_err().into());
+            return Err(err.into());
         }
     }
 


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..8b77e36
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,22 @@
+on: [push]
+
+name: CI
+
+jobs:
+  build_and_test:
+    name: Rust project
+    runs-on: ubuntu-latest
+    env:
+      RUST_BACKTRACE: "1"
+    steps:
+      - uses: actions/checkout@master
+      - name: Install Rust
+        uses: actions-rs/toolchain@v1
+        with:
+          toolchain: stable
+          override: true
+      - name: Test
+        uses: actions-rs/cargo@v1
+        with:
+          command: test
+          args: --all-features
diff --git a/src/test/simple.rs b/src/test/simple.rs
index fe4c2c8..e596057 100644
--- a/src/test/simple.rs
+++ b/src/test/simple.rs
@@ -134,9 +134,10 @@ fn non_ascii_header() {
     let resp = get("test://host/non_ascii_header")
         .set("Bäd", "Headör")
         .call();
-    assert!(!resp.ok());
-    assert_eq!(resp.status(), 500);
-    assert_eq!(resp.status_text(), "Bad Header");
+    // surprisingly, this is ok, because this lib is not about enforcing standards.
+    assert!(resp.ok());
+    assert_eq!(resp.status(), 200);
+    assert_eq!(resp.status_text(), "OK");
 }
 
 #[test]
@@ -150,3 +151,16 @@ pub fn no_status_text() {
     assert!(resp.ok());
     assert_eq!(resp.status(), 200);
 }
+
+#[test]
+pub fn header_with_spaces_before_value() {
+    test::set_handler("/space_before_value", |unit| {
+        assert!(unit.has("X-Test"));
+        assert_eq!(unit.header("X-Test").unwrap(), "value");
+        test::make_response(200, "OK", vec![], vec![])
+    });
+    let resp = get("test://host/space_before_value")
+        .set("X-Test", "     value")
+        .call();
+    assert_eq!(resp.status(), 200);
+}
diff --git a/tests/https-agent.rs b/tests/https-agent.rs
index b246725..5e54a8f 100644
--- a/tests/https-agent.rs
+++ b/tests/https-agent.rs
@@ -4,7 +4,8 @@ use std::io::Read;
 #[test]
 fn tls_connection_close() {
     let agent = ureq::Agent::default().build();
-    let resp = agent.get("https://example.com/404")
+    let resp = agent
+        .get("https://example.com/404")
         .set("Connection", "close")
         .call();
     assert_eq!(resp.status(), 404);