From 3cca3f82c99cc6f22be28c93ea1b346e14df2010 Mon Sep 17 00:00:00 2001 From: jayjamesjay Date: Sun, 28 Jul 2024 09:03:31 +0200 Subject: [PATCH 1/4] RedirectPolicy init --- Cargo.lock | 2 +- Cargo.toml | 2 +- README.md | 11 +- ...builder_get.rs => advanced_request_get.rs} | 4 +- src/lib.rs | 2 +- src/request.rs | 147 +++++++++++------- src/response.rs | 14 +- src/stream.rs | 11 +- src/tls.rs | 8 +- 9 files changed, 125 insertions(+), 76 deletions(-) rename examples/{request_builder_get.rs => advanced_request_get.rs} (94%) diff --git a/Cargo.lock b/Cargo.lock index 46d9c60..b8c6ab2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -218,7 +218,7 @@ dependencies = [ [[package]] name = "http_req" -version = "0.11.0" +version = "0.12.0" dependencies = [ "native-tls", "rustls", diff --git a/Cargo.toml b/Cargo.toml index bb5d947..b5b37e5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "http_req" -version = "0.11.0" +version = "0.12.0" license = "MIT" description = "simple and lightweight HTTP client with built-in HTTPS support" repository = "https://github.com/jayjamesjay/http_req" diff --git a/README.md b/README.md index b5d6db8..f23c88d 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,15 @@ # http_req > [!CAUTION] -> v0.11.0 introduces major changes to design of `RequestBuilder` and `Request`. Please review [documentation](https://docs.rs/http_req/0.11.0/http_req/) before migrating from previous versions. +> v0.12.0 replaces `RequestBuilder` with `RequestMessage`. Please review [documentation](https://docs.rs/http_req/0.12.0/http_req/) before migrating from previous versions. [![Rust](https://github.com/jayjamesjay/http_req/actions/workflows/rust.yml/badge.svg)](https://github.com/jayjamesjay/http_req/actions/workflows/rust.yml) -[![Crates.io](https://img.shields.io/badge/crates.io-v0.11.0-orange.svg?longCache=true)](https://crates.io/crates/http_req) -[![Docs.rs](https://docs.rs/http_req/badge.svg)](https://docs.rs/http_req/0.11.0/http_req/) +[![Crates.io](https://img.shields.io/badge/crates.io-v0.12.0-orange.svg?longCache=true)](https://crates.io/crates/http_req) +[![Docs.rs](https://docs.rs/http_req/badge.svg)](https://docs.rs/http_req/0.12.0/http_req/) Simple and lightweight HTTP client with built-in HTTPS support. +- HTTP and HTTPS via [rust-native-tls](https://github.com/sfackler/rust-native-tls) (or optionally [rus-tls](https://crates.io/crates/rustls)) +- Small binary size (less than 0.7 MB for basic GET request) +- Minimal amount of dependencies ## Requirements http_req by default uses [rust-native-tls](https://github.com/sfackler/rust-native-tls), @@ -32,7 +35,7 @@ Take a look at [more examples](https://github.com/jayjamesjay/http_req/tree/mast In order to use `http_req` with `rustls` in your project, add the following lines to `Cargo.toml`: ```toml [dependencies] -http_req = {version="^0.11", default-features = false, features = ["rust-tls"]} +http_req = {version="^0.12", default-features = false, features = ["rust-tls"]} ``` ## License diff --git a/examples/request_builder_get.rs b/examples/advanced_request_get.rs similarity index 94% rename from examples/request_builder_get.rs rename to examples/advanced_request_get.rs index e52be18..abf6788 100644 --- a/examples/request_builder_get.rs +++ b/examples/advanced_request_get.rs @@ -1,5 +1,5 @@ use http_req::{ - request::RequestBuilder, + request::RequestMessage, response::Response, stream::{self, Stream}, uri::Uri, @@ -19,7 +19,7 @@ fn main() { let mut body = Vec::new(); // Prepares a request message. - let request_msg = RequestBuilder::new(&addr) + let request_msg = RequestMessage::new(&addr) .header("Connection", "Close") .parse(); diff --git a/src/lib.rs b/src/lib.rs index 2de2c9e..77ad98e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,5 @@ //! Simple HTTP client with built-in HTTPS support. -//! +//! //! By default uses [rust-native-tls](https://github.com/sfackler/rust-native-tls), //! which relies on TLS framework provided by OS on Windows and macOS, and OpenSSL //! on all other platforms. But it also supports [rus-tls](https://crates.io/crates/rustls). diff --git a/src/request.rs b/src/request.rs index c079892..17e31b8 100644 --- a/src/request.rs +++ b/src/request.rs @@ -78,21 +78,55 @@ impl fmt::Display for HttpVersion { } } -/// Raw HTTP request that can be sent to any stream +pub struct RequestBuilder {} + +#[deprecated( + since = "0.12.0", + note = "RequestBuilder was replaced with RequestMessage" +)] +impl<'a> RequestBuilder { + pub fn new(uri: &'a Uri<'a>) -> RequestMessage<'a> { + RequestMessage::new(uri) + } +} + +/// Allows to control redirects +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum RedirectPolicy bool> { + /// Follows redirect if limit is greater than 0. + Limit(usize), + /// Runs functions `F` to determine if redirect should be followed. + Custom(F), +} + +impl bool> RedirectPolicy { + /// Checks the policy againt specified conditions. + /// Returns `true` if redirect should be followed. + pub fn follow(&self) -> bool { + use self::RedirectPolicy::*; + + match self { + Limit(limit) => *limit > 0, + Custom(func) => func(), + } + } +} + +/// Raw HTTP request message that can be sent to any stream /// /// # Examples /// ``` /// use std::convert::TryFrom; -/// use http_req::{request::RequestBuilder, uri::Uri}; +/// use http_req::{request::RequestMessage, uri::Uri}; /// /// let addr: Uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// -/// let mut request_msg = RequestBuilder::new(&addr) +/// let mut request_msg = RequestMessage::new(&addr) /// .header("Connection", "Close") /// .parse(); /// ``` #[derive(Clone, Debug, PartialEq)] -pub struct RequestBuilder<'a> { +pub struct RequestMessage<'a> { uri: &'a Uri<'a>, method: Method, version: HttpVersion, @@ -100,21 +134,21 @@ pub struct RequestBuilder<'a> { body: Option<&'a [u8]>, } -impl<'a> RequestBuilder<'a> { - /// Creates a new `RequestBuilder` with default parameters +impl<'a> RequestMessage<'a> { + /// Creates a new `RequestMessage` with default parameters /// /// # Examples /// ``` /// use std::convert::TryFrom; - /// use http_req::{request::RequestBuilder, uri::Uri}; + /// use http_req::{request::RequestMessage, uri::Uri}; /// /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - /// let request_builder = RequestBuilder::new(&addr) + /// let request_msg = RequestMessage::new(&addr) /// .header("Connection", "Close"); /// ``` - pub fn new(uri: &'a Uri<'a>) -> RequestBuilder<'a> { - RequestBuilder { + pub fn new(uri: &'a Uri<'a>) -> RequestMessage<'a> { + RequestMessage { headers: Headers::default_http(uri), uri, method: Method::GET, @@ -128,11 +162,11 @@ impl<'a> RequestBuilder<'a> { /// # Examples /// ``` /// use std::convert::TryFrom; - /// use http_req::{request::{RequestBuilder, Method}, uri::Uri}; + /// use http_req::{request::{RequestMessage, Method}, uri::Uri}; /// /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - /// let request_builder = RequestBuilder::new(&addr) + /// let request_msg = RequestMessage::new(&addr) /// .method(Method::HEAD); /// ``` pub fn method(&mut self, method: T) -> &mut Self @@ -148,11 +182,11 @@ impl<'a> RequestBuilder<'a> { /// # Examples /// ``` /// use std::convert::TryFrom; - /// use http_req::{request::{RequestBuilder, HttpVersion}, uri::Uri}; + /// use http_req::{request::{RequestMessage, HttpVersion}, uri::Uri}; /// /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - /// let request_builder = RequestBuilder::new(&addr) + /// let request_msg = RequestMessage::new(&addr) /// .version(HttpVersion::Http10); /// ``` pub fn version(&mut self, version: T) -> &mut Self @@ -168,7 +202,7 @@ impl<'a> RequestBuilder<'a> { /// # Examples /// ``` /// use std::convert::TryFrom; - /// use http_req::{request::RequestBuilder, response::Headers, uri::Uri}; + /// use http_req::{request::RequestMessage, response::Headers, uri::Uri}; /// /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// @@ -178,7 +212,7 @@ impl<'a> RequestBuilder<'a> { /// headers.insert("Host", "rust-lang.org"); /// headers.insert("Connection", "Close"); /// - /// let request_builder = RequestBuilder::new(&addr) + /// let request_msg = RequestMessage::new(&addr) /// .headers(headers); /// ``` pub fn headers(&mut self, headers: T) -> &mut Self @@ -194,11 +228,11 @@ impl<'a> RequestBuilder<'a> { /// # Examples /// ``` /// use std::convert::TryFrom; - /// use http_req::{request::RequestBuilder, response::Headers, uri::Uri}; + /// use http_req::{request::RequestMessage, response::Headers, uri::Uri}; /// /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - /// let request_builder = RequestBuilder::new(&addr) + /// let request_msg = RequestMessage::new(&addr) /// .header("Connection", "Close"); /// ``` pub fn header(&mut self, key: &T, val: &U) -> &mut Self @@ -215,12 +249,12 @@ impl<'a> RequestBuilder<'a> { /// # Examples /// ``` /// use std::convert::TryFrom; - /// use http_req::{request::{RequestBuilder, Method}, response::Headers, uri::Uri}; + /// use http_req::{request::{RequestMessage, Method}, response::Headers, uri::Uri}; /// /// let addr = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// const BODY: &[u8; 27] = b"field1=value1&field2=value2"; /// - /// let request_builder = RequestBuilder::new(&addr) + /// let request_msg = RequestMessage::new(&addr) /// .method(Method::POST) /// .body(BODY) /// .header("Content-Length", &BODY.len()) @@ -231,16 +265,16 @@ impl<'a> RequestBuilder<'a> { self } - /// Parses the request message for this `RequestBuilder` + /// Parses the request message for this `RequestMessage` /// /// # Examples /// ``` /// use std::convert::TryFrom; - /// use http_req::{request::RequestBuilder, uri::Uri}; + /// use http_req::{request::RequestMessage, uri::Uri}; /// /// let addr: Uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); /// - /// let mut request_msg = RequestBuilder::new(&addr) + /// let mut request_msg = RequestMessage::new(&addr) /// .header("Connection", "Close") /// .parse(); /// ``` @@ -288,7 +322,7 @@ impl<'a> RequestBuilder<'a> { /// #[derive(Clone, Debug, PartialEq)] pub struct Request<'a> { - inner: RequestBuilder<'a>, + messsage: RequestMessage<'a>, connect_timeout: Option, read_timeout: Option, write_timeout: Option, @@ -309,11 +343,11 @@ impl<'a> Request<'a> { /// let request = Request::new(&uri); /// ``` pub fn new(uri: &'a Uri) -> Request<'a> { - let mut builder = RequestBuilder::new(&uri); - builder.header("Connection", "Close"); + let mut message = RequestMessage::new(&uri); + message.header("Connection", "Close"); Request { - inner: builder, + messsage: message, connect_timeout: Some(Duration::from_secs(60)), read_timeout: Some(Duration::from_secs(60)), write_timeout: Some(Duration::from_secs(60)), @@ -338,7 +372,7 @@ impl<'a> Request<'a> { where Method: From, { - self.inner.method(method); + self.messsage.method(method); self } @@ -359,7 +393,7 @@ impl<'a> Request<'a> { where HttpVersion: From, { - self.inner.version(version); + self.messsage.version(version); self } @@ -385,7 +419,7 @@ impl<'a> Request<'a> { where Headers: From, { - self.inner.headers(headers); + self.messsage.headers(headers); self } @@ -406,7 +440,7 @@ impl<'a> Request<'a> { T: ToString + ?Sized, U: ToString + ?Sized, { - self.inner.header(key, val); + self.messsage.header(key, val); self } @@ -426,7 +460,7 @@ impl<'a> Request<'a> { /// .body(body); /// ``` pub fn body(&mut self, body: &'a [u8]) -> &mut Self { - self.inner.body(body); + self.messsage.body(body); self } @@ -572,13 +606,13 @@ impl<'a> Request<'a> { T: Write, { // Set up a stream. - let mut stream = Stream::new(self.inner.uri, self.connect_timeout)?; + let mut stream = Stream::new(self.messsage.uri, self.connect_timeout)?; stream.set_read_timeout(self.read_timeout)?; stream.set_write_timeout(self.write_timeout)?; - stream = Stream::try_to_https(stream, self.inner.uri, self.root_cert_file_pem)?; + stream = Stream::try_to_https(stream, self.messsage.uri, self.root_cert_file_pem)?; // Send the request message to stream. - let request_msg = self.inner.parse(); + let request_msg = self.messsage.parse(); stream.write_all(&request_msg)?; // Set up variables @@ -608,16 +642,13 @@ impl<'a> Request<'a> { let response = Response::from_head(&raw_response_head)?; let content_len = response.content_len().unwrap_or(1); - let encoding = response.headers().get("Transfer-Encoding"); let mut params = Vec::with_capacity(5); - if let Some(encode) = encoding { - if encode == "chunked" { - params.push("chunked"); - } + if response.is_chunked() { + params.push("chunked"); } - if content_len > 0 && self.inner.method != Method::HEAD { + if content_len > 0 && self.messsage.method != Method::HEAD { params.push("non-empty"); } @@ -715,22 +746,22 @@ mod tests { } #[test] - fn request_b_new() { - RequestBuilder::new(&Uri::try_from(URI).unwrap()); - RequestBuilder::new(&Uri::try_from(URI_S).unwrap()); + fn request_m_new() { + RequestMessage::new(&Uri::try_from(URI).unwrap()); + RequestMessage::new(&Uri::try_from(URI_S).unwrap()); } #[test] - fn request_b_method() { + fn request_m_method() { let uri = Uri::try_from(URI).unwrap(); - let mut req = RequestBuilder::new(&uri); + let mut req = RequestMessage::new(&uri); let req = req.method(Method::HEAD); assert_eq!(req.method, Method::HEAD); } #[test] - fn request_b_headers() { + fn request_m_headers() { let mut headers = Headers::new(); headers.insert("Accept-Charset", "utf-8"); headers.insert("Accept-Language", "en-US"); @@ -738,16 +769,16 @@ mod tests { headers.insert("Connection", "Close"); let uri = Uri::try_from(URI).unwrap(); - let mut req = RequestBuilder::new(&uri); + let mut req = RequestMessage::new(&uri); let req = req.headers(headers.clone()); assert_eq!(req.headers, headers); } #[test] - fn request_b_header() { + fn request_m_header() { let uri = Uri::try_from(URI).unwrap(); - let mut req = RequestBuilder::new(&uri); + let mut req = RequestMessage::new(&uri); let k = "Connection"; let v = "Close"; @@ -761,18 +792,18 @@ mod tests { } #[test] - fn request_b_body() { + fn request_m_body() { let uri = Uri::try_from(URI).unwrap(); - let mut req = RequestBuilder::new(&uri); + let mut req = RequestMessage::new(&uri); let req = req.body(&BODY); assert_eq!(req.body, Some(BODY.as_ref())); } #[test] - fn request_b_parse() { + fn request_m_parse() { let uri = Uri::try_from(URI).unwrap(); - let req = RequestBuilder::new(&uri); + let req = RequestMessage::new(&uri); const DEFAULT_MSG: &str = "GET /std/string/index.html HTTP/1.1\r\n\ Host: doc.rust-lang.org\r\n\r\n"; @@ -800,7 +831,7 @@ mod tests { let mut req = Request::new(&uri); req.method(Method::HEAD); - assert_eq!(req.inner.method, Method::HEAD); + assert_eq!(req.messsage.method, Method::HEAD); } #[test] @@ -815,7 +846,7 @@ mod tests { let mut req = Request::new(&uri); let req = req.headers(headers.clone()); - assert_eq!(req.inner.headers, headers); + assert_eq!(req.messsage.headers, headers); } #[test] @@ -832,7 +863,7 @@ mod tests { let req = req.header(k, v); - assert_eq!(req.inner.headers, expect_headers); + assert_eq!(req.messsage.headers, expect_headers); } #[test] @@ -841,7 +872,7 @@ mod tests { let mut req = Request::new(&uri); let req = req.body(&BODY); - assert_eq!(req.inner.body, Some(BODY.as_ref())); + assert_eq!(req.messsage.body, Some(BODY.as_ref())); } #[test] diff --git a/src/response.rs b/src/response.rs index 895e6d6..6295c0d 100644 --- a/src/response.rs +++ b/src/response.rs @@ -60,7 +60,10 @@ impl Response { /// /// let response = Response::try_from(RESPONSE, &mut body).unwrap(); /// ``` - pub fn try_from(res: &[u8], writer: &mut T) -> Result { + pub fn try_from(res: &[u8], writer: &mut T) -> Result + where + T: Write, + { if res.is_empty() { Err(Error::Parse(ParseErr::Empty)) } else { @@ -152,7 +155,7 @@ impl Response { /// let response = Response::try_from(RESPONSE, &mut body).unwrap(); /// let headers = response.headers(); /// ``` - pub fn headers(&self) -> &Headers { + pub const fn headers(&self) -> &Headers { &self.headers } @@ -178,6 +181,13 @@ impl Response { .get("Content-Length") .and_then(|len| len.parse().ok()) } + + /// Checks if Transfer-Encoding includes "chunked". + pub fn is_chunked(&self) -> bool { + self.headers() + .get("Transfer-Encoding") + .is_some_and(|encodings| encodings.contains("chunked")) + } } /// Status of HTTP response diff --git a/src/stream.rs b/src/stream.rs index 1afe877..27b0e60 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -13,6 +13,7 @@ const BUF_SIZE: usize = 16 * 1000; /// Wrapper around TCP stream for HTTP and HTTPS protocols. /// Allows to perform common operations on underlying stream. +#[derive(Debug)] pub enum Stream { Http(TcpStream), Https(Conn), @@ -226,10 +227,10 @@ where /// Exexcutes a function in a loop until operation is completed or deadline is exceeded. /// /// It checks if a timeout was exceeded every iteration, therefore it limits -/// how many time a specific function can be called before deadline. -/// For the `execute_with_deadline` to meet the deadline, each call -/// to `func` needs finish before the deadline. -/// +/// how many time a specific function can be called before deadline. +/// For the `execute_with_deadline` to meet the deadline, each call +/// to `func` needs finish before the deadline. +/// /// Key information about function `func`: /// - is provided with information about remaining time /// - must ensure that its execution will not take more time than specified in `remaining_time` @@ -250,7 +251,7 @@ where /// Reads the head of HTTP response from `reader`. /// -/// Reads from `reader` (line by line) until a blank line is identified, +/// Reads from `reader` (line by line) until a blank line is identified, /// which indicates that all meta-information has been read, pub fn read_head(reader: &mut B) -> Vec where diff --git a/src/tls.rs b/src/tls.rs index 9cd771e..edb5732 100644 --- a/src/tls.rs +++ b/src/tls.rs @@ -18,9 +18,10 @@ use rustls_pki_types::ServerName; #[cfg(not(any(feature = "native-tls", feature = "rust-tls")))] compile_error!("one of the `native-tls` or `rust-tls` features must be enabled"); -/// Wrapper around TLS Stream, depends on selected TLS library (`S: io::Read + io::Write`): +/// Wrapper around TLS Stream, depends on selected TLS library: /// - native_tls: `TlsStream` /// - rustls: `StreamOwned` +#[derive(Debug)] pub struct Conn { #[cfg(feature = "native-tls")] stream: native_tls::TlsStream, @@ -70,7 +71,10 @@ where } } -impl io::Write for Conn { +impl io::Write for Conn +where + S: io::Read + io::Write, +{ fn write(&mut self, buf: &[u8]) -> Result { self.stream.write(buf) } From f46dd1c1d0f086ae6fa26a8c469113b75102037f Mon Sep 17 00:00:00 2001 From: jayjamesjay Date: Sun, 28 Jul 2024 20:51:19 +0200 Subject: [PATCH 2/4] basic redirect support --- Cargo.lock | 148 +++++++++++++++++++++++------------------------- src/request.rs | 66 ++++++++++++++++----- src/response.rs | 21 ++++++- src/uri.rs | 45 ++++++++++++--- 4 files changed, 178 insertions(+), 102 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b8c6ab2..a141821 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13,9 +13,9 @@ dependencies = [ [[package]] name = "aws-lc-rs" -version = "1.7.3" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7d844e282b4b56750b2d4e893b2205581ded8709fddd2b6aa5418c150ca877" +checksum = "4ae74d9bd0a7530e8afd1770739ad34b36838829d6ad61818f9230f683f5ad77" dependencies = [ "aws-lc-sys", "mirai-annotations", @@ -25,9 +25,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.18.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3a2c29203f6bf296d01141cc8bb9dbd5ecd4c27843f2ee0767bcd5985a927da" +checksum = "2e89b6941c2d1a7045538884d6e760ccfffdf8e1ffc2613d8efa74305e1f3752" dependencies = [ "bindgen", "cc", @@ -50,7 +50,7 @@ version = "0.69.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0" dependencies = [ - "bitflags 2.5.0", + "bitflags", "cexpr", "clang-sys", "itertools", @@ -69,25 +69,18 @@ dependencies = [ [[package]] name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bitflags" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "cc" -version = "1.0.95" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b" +checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" dependencies = [ "jobserver", "libc", - "once_cell", ] [[package]] @@ -155,9 +148,9 @@ checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0" [[package]] name = "errno" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", "windows-sys", @@ -165,9 +158,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.2" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" +checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a" [[package]] name = "foreign-types" @@ -192,9 +185,9 @@ checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" [[package]] name = "getrandom" -version = "0.2.14" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", "libc", @@ -240,18 +233,18 @@ dependencies = [ [[package]] name = "jobserver" -version = "0.1.31" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e" +checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0" dependencies = [ "libc", ] [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "lazycell" @@ -261,15 +254,15 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.153" +version = "0.2.155" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" [[package]] name = "libloading" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d" +checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", "windows-targets", @@ -277,15 +270,15 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "log" -version = "0.4.21" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" +checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" [[package]] name = "memchr" @@ -307,11 +300,10 @@ checksum = "c9be0862c1b3f26a88803c4a49de6889c10e608b3ee9344e6ef5b45fb37ad3d1" [[package]] name = "native-tls" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" +checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466" dependencies = [ - "lazy_static", "libc", "log", "openssl", @@ -341,11 +333,11 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openssl" -version = "0.10.64" +version = "0.10.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" +checksum = "9529f4786b70a3e8c61e11179af17ab6188ad8d0ded78c5529441ed39d4bd9c1" dependencies = [ - "bitflags 2.5.0", + "bitflags", "cfg-if", "foreign-types", "libc", @@ -373,9 +365,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.102" +version = "0.9.103" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" +checksum = "7f9e8deee91df40a943c71b917e5874b951d32a802526c85721ce3b776c929d6" dependencies = [ "cc", "libc", @@ -407,9 +399,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.81" +version = "1.0.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" dependencies = [ "unicode-ident", ] @@ -475,11 +467,11 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.32" +version = "0.38.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" +checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f" dependencies = [ - "bitflags 2.5.0", + "bitflags", "errno", "libc", "linux-raw-sys", @@ -488,9 +480,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.10" +version = "0.23.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05cff451f60db80f490f3c182b77c35260baace73209e9cdbbe526bfe3a4d402" +checksum = "c58f8c84392efc0a126acce10fa59ff7b3d2ac06ab451a33f2741989b806b044" dependencies = [ "aws-lc-rs", "log", @@ -519,9 +511,9 @@ checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" [[package]] name = "rustls-webpki" -version = "0.102.4" +version = "0.102.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff448f7e92e913c4b7d4c6d8e4540a1724b319b4152b8aef6d4cf8339712b33e" +checksum = "8e6b52d4fda176fd835fdc55a835d4a89b8499cad995885a21149d5ad62f852e" dependencies = [ "aws-lc-rs", "ring", @@ -540,11 +532,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 1.3.2", + "bitflags", "core-foundation", "core-foundation-sys", "libc", @@ -553,9 +545,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.10.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" +checksum = "75da29fe9b9b08fe9d6b22b5b4bcbc75d8db3aa31e639aa56bb62e9d46bfceaf" dependencies = [ "core-foundation-sys", "libc", @@ -581,9 +573,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.60" +version = "2.0.72" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3" +checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" dependencies = [ "proc-macro2", "quote", @@ -631,9 +623,9 @@ checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.4" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "wasi" @@ -683,9 +675,9 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ "windows_aarch64_gnullvm", "windows_aarch64_msvc", @@ -699,51 +691,51 @@ dependencies = [ [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" [[package]] name = "windows_aarch64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" [[package]] name = "windows_i686_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" [[package]] name = "windows_i686_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" [[package]] name = "windows_i686_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" [[package]] name = "windows_x86_64_gnu" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" [[package]] name = "windows_x86_64_msvc" -version = "0.52.5" +version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] name = "zeroize" diff --git a/src/request.rs b/src/request.rs index 17e31b8..2ed0a90 100644 --- a/src/request.rs +++ b/src/request.rs @@ -7,6 +7,7 @@ use crate::{ uri::Uri, }; use std::{ + convert::TryFrom, fmt, io::{BufReader, Write}, path::Path, @@ -61,7 +62,7 @@ pub enum HttpVersion { } impl HttpVersion { - pub const fn as_str(self) -> &'static str { + pub const fn as_str(&self) -> &str { use self::HttpVersion::*; match self { @@ -92,7 +93,7 @@ impl<'a> RequestBuilder { /// Allows to control redirects #[derive(Debug, PartialEq, Clone, Copy)] -pub enum RedirectPolicy bool> { +pub enum RedirectPolicy { /// Follows redirect if limit is greater than 0. Limit(usize), /// Runs functions `F` to determine if redirect should be followed. @@ -102,16 +103,28 @@ pub enum RedirectPolicy bool> { impl bool> RedirectPolicy { /// Checks the policy againt specified conditions. /// Returns `true` if redirect should be followed. - pub fn follow(&self) -> bool { + pub fn follow(&mut self) -> bool { use self::RedirectPolicy::*; match self { - Limit(limit) => *limit > 0, + Limit(limit) => match limit { + 0 => false, + _ => { + *limit = *limit - 1; + true + } + }, Custom(func) => func(), } } } +impl bool> Default for RedirectPolicy { + fn default() -> Self { + RedirectPolicy::Limit(5) + } +} + /// Raw HTTP request message that can be sent to any stream /// /// # Examples @@ -323,6 +336,7 @@ impl<'a> RequestMessage<'a> { #[derive(Clone, Debug, PartialEq)] pub struct Request<'a> { messsage: RequestMessage<'a>, + redirect_policy: RedirectPolicy bool>, connect_timeout: Option, read_timeout: Option, write_timeout: Option, @@ -348,6 +362,7 @@ impl<'a> Request<'a> { Request { messsage: message, + redirect_policy: RedirectPolicy::default(), connect_timeout: Some(Duration::from_secs(60)), read_timeout: Some(Duration::from_secs(60)), write_timeout: Some(Duration::from_secs(60)), @@ -586,6 +601,23 @@ impl<'a> Request<'a> { self } + /// Sets the redirect policy for the request. + /// + /// # Examples + /// ``` + /// use http_req::{request::{Request, RedirectPolicy}, uri::Uri}; + /// use std::{time::Duration, convert::TryFrom, path::Path}; + /// + /// let uri = Uri::try_from("https://www.rust-lang.org/learn").unwrap(); + /// + /// let request = Request::new(&uri) + /// .redirect_policy(RedirectPolicy::Limit(5)); + /// ``` + pub fn redirect_policy(&mut self, policy: RedirectPolicy bool>) -> &mut Self { + self.redirect_policy = policy; + self + } + /// Sends the HTTP request and returns `Response`. /// /// Creates `TcpStream` (and wraps it with `TlsStream` if needed). Writes request message @@ -601,7 +633,7 @@ impl<'a> Request<'a> { /// /// let response = Request::new(&uri).send(&mut writer).unwrap(); /// ``` - pub fn send(&self, writer: &mut T) -> Result + pub fn send(&mut self, writer: &mut T) -> Result where T: Write, { @@ -626,7 +658,7 @@ impl<'a> Request<'a> { thread::spawn(move || { buf_reader.send_head(&sender); - let params: Vec<&str> = receiver_supp.recv().unwrap(); + let params: Vec<&str> = receiver_supp.recv().unwrap_or(Vec::new()); if params.contains(&"non-empty") { if params.contains(&"chunked") { let mut buf_reader = ChunkReader::from(buf_reader); @@ -639,22 +671,28 @@ impl<'a> Request<'a> { // Receive and process `head` of the response. raw_response_head.receive(&receiver, deadline)?; - let response = Response::from_head(&raw_response_head)?; - let content_len = response.content_len().unwrap_or(1); - let mut params = Vec::with_capacity(5); - if response.is_chunked() { - params.push("chunked"); - } + if response.status_code().is_redirect() && self.redirect_policy.follow() { + if let Some(location) = response.headers().get("Location") { + let mut raw_uri = location.to_string(); + let uri = if Uri::is_relative(&raw_uri) { + self.messsage.uri.from_relative(&mut raw_uri) + } else { + Uri::try_from(raw_uri.as_str()) + }?; - if content_len > 0 && self.messsage.method != Method::HEAD { - params.push("non-empty"); + return Request::new(&uri) + .redirect_policy(self.redirect_policy) + .send(writer); + } } + let params = response.basic_info(&self.messsage.method).to_vec(); sender_supp.send(params).unwrap(); // Receive and process `body` of the response. + let content_len = response.content_len().unwrap_or(1); if content_len > 0 { writer.receive_all(&receiver, deadline)?; } diff --git a/src/response.rs b/src/response.rs index 6295c0d..68aff81 100644 --- a/src/response.rs +++ b/src/response.rs @@ -1,6 +1,7 @@ //! parsing server response use crate::{ error::{Error, ParseErr}, + request::Method, uri::Uri, }; use std::{ @@ -188,6 +189,24 @@ impl Response { .get("Transfer-Encoding") .is_some_and(|encodings| encodings.contains("chunked")) } + + /// Returns basic information about the response as an array, including: + /// - chunked -> Transfer-Encoding includes "chunked" + /// - non-empty -> Content-Length is greater than 0 (or unknown) and method is not HEAD + pub fn basic_info<'a>(&self, method: &Method) -> [&'a str; 2] { + let mut params = [""; 2]; + let content_len = self.content_len().unwrap_or(1); + + if self.is_chunked() { + params[0] = "chunked"; + } + + if content_len > 0 && method != &Method::HEAD { + params[1] = "non-empty"; + } + + params + } } /// Status of HTTP response @@ -512,7 +531,7 @@ impl StatusCode { /// const code: StatusCode = StatusCode::new(200); /// assert_eq!(code.reason(), Some("OK")) /// ``` - pub const fn reason(self) -> Option<&'static str> { + pub const fn reason(&self) -> Option<&str> { let reason = match self.0 { 100 => "Continue", 101 => "Switching Protocols", diff --git a/src/uri.rs b/src/uri.rs index ca76801..1877547 100644 --- a/src/uri.rs +++ b/src/uri.rs @@ -80,6 +80,11 @@ pub struct Uri<'a> { } impl<'a> Uri<'a> { + /// Returns a reference to the underlying &str. + pub fn get_ref(&self) -> &str { + self.inner + } + /// Returns scheme of this `Uri`. /// /// # Example @@ -95,7 +100,7 @@ impl<'a> Uri<'a> { } /// Returns information about the user included in this `Uri`. - /// + /// /// # Example /// ``` /// use http_req::uri::Uri; @@ -109,7 +114,7 @@ impl<'a> Uri<'a> { } /// Returns host of this `Uri`. - /// + /// /// # Example /// ``` /// use http_req::uri::Uri; @@ -123,7 +128,7 @@ impl<'a> Uri<'a> { } /// Returns host of this `Uri` to use in a header. - /// + /// /// # Example /// ``` /// use http_req::uri::Uri; @@ -140,7 +145,7 @@ impl<'a> Uri<'a> { } /// Returns port of this `Uri` - /// + /// /// # Example /// ``` /// use http_req::uri::Uri; @@ -155,7 +160,7 @@ impl<'a> Uri<'a> { /// Returns port corresponding to this `Uri`. /// Returns default port if it hasn't been set in the uri. - /// + /// /// # Example /// ``` /// use http_req::uri::Uri; @@ -177,7 +182,7 @@ impl<'a> Uri<'a> { } /// Returns path of this `Uri`. - /// + /// /// # Example /// ``` /// use http_req::uri::Uri; @@ -191,7 +196,7 @@ impl<'a> Uri<'a> { } /// Returns query of this `Uri`. - /// + /// /// # Example /// ``` /// use http_req::uri::Uri; @@ -205,7 +210,7 @@ impl<'a> Uri<'a> { } /// Returns fragment of this `Uri`. - /// + /// /// # Example /// ``` /// use http_req::uri::Uri; @@ -219,7 +224,7 @@ impl<'a> Uri<'a> { } /// Returns resource `Uri` points to. - /// + /// /// # Example /// ``` /// use http_req::uri::Uri; @@ -234,6 +239,28 @@ impl<'a> Uri<'a> { None => "/", } } + + /// Checks if &str is an relative uri. + pub fn is_relative(raw_uri: &str) -> bool { + raw_uri.starts_with("/") + || raw_uri.starts_with("?") + || raw_uri.starts_with("#") + || raw_uri.starts_with("@") + } + + /// Creates a new `Uri` from current uri and relative uri. + /// Transforms the relative uri into an absolute uri. + pub fn from_relative(&'a self, relative_uri: &'a mut String) -> Result, Error> { + let inner_uri = self.inner; + let mut relative_part = relative_uri.as_ref(); + + if inner_uri.ends_with("/") && relative_uri.starts_with("/") { + relative_part = relative_uri.trim_start_matches("/"); + } + + *relative_uri = inner_uri.to_owned() + relative_part; + Uri::try_from(relative_uri.as_str()) + } } impl<'a> fmt::Display for Uri<'a> { From ce69208a239725757ee83a6b92cf2a63efadff7f Mon Sep 17 00:00:00 2001 From: jayjamesjay Date: Thu, 15 Aug 2024 17:42:44 +0200 Subject: [PATCH 3/4] improve parsing relative uri --- Cargo.lock | 63 +++++++++++-------- examples/advanced_request_get.rs | 2 +- src/chunked.rs | 2 +- src/error.rs | 61 ++++++++++-------- src/request.rs | 22 ++++--- src/stream.rs | 28 +++++---- src/uri.rs | 105 ++++++++++++++++++++++++++++--- 7 files changed, 199 insertions(+), 84 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a141821..e3ade9a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -25,9 +25,9 @@ dependencies = [ [[package]] name = "aws-lc-sys" -version = "0.20.0" +version = "0.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e89b6941c2d1a7045538884d6e760ccfffdf8e1ffc2613d8efa74305e1f3752" +checksum = "0f0e249228c6ad2d240c2dc94b714d711629d52bad946075d8e9b2f5391f0703" dependencies = [ "bindgen", "cc", @@ -75,12 +75,13 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "cc" -version = "1.1.6" +version = "1.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aba8f4e9906c7ce3c73463f62a7f0c65183ada1a2d47e397cc8810827f9694f" +checksum = "68064e60dbf1f17005c2fde4d07c16d8baa506fd7ffed8ccab702d93617975c7" dependencies = [ "jobserver", "libc", + "shlex", ] [[package]] @@ -111,9 +112,9 @@ dependencies = [ [[package]] name = "cmake" -version = "0.1.50" +version = "0.1.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31c789563b815f77f4250caee12365734369f942439b7defd71e18a48197130" +checksum = "fb1e43aa7fd152b1f968787f7dbcdeb306d1867ff373c69955211876c053f91a" dependencies = [ "cc", ] @@ -130,15 +131,15 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "dunce" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" [[package]] name = "either" @@ -153,7 +154,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -206,7 +207,7 @@ version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -417,9 +418,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.5" +version = "1.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" dependencies = [ "aho-corasick", "memchr", @@ -456,7 +457,7 @@ dependencies = [ "libc", "spin", "untrusted", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -475,7 +476,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -495,9 +496,9 @@ dependencies = [ [[package]] name = "rustls-pemfile" -version = "2.1.2" +version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +checksum = "196fe16b00e106300d3e45ecfcb764fa292a535d7326a29a5875c579c7417425" dependencies = [ "base64", "rustls-pki-types", @@ -505,9 +506,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "976295e77ce332211c0d24d92c0e83e50f5c5f046d11082cea19f3df13a3562d" +checksum = "fc0a2ce646f8655401bb81e7927b812614bd5d91dbc968696be50603510fcaf0" [[package]] name = "rustls-webpki" @@ -527,7 +528,7 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -573,9 +574,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.72" +version = "2.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "1fceb41e3d546d0bd83421d3409b1460cc7444cd389341a4c880fe7a042cb3d7" dependencies = [ "proc-macro2", "quote", @@ -584,14 +585,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.10.1" +version = "3.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" +checksum = "04cbcdd0c794ebb0d4cf35e88edd2f7d2c4c3e9a5a6dab322839b321c6a87a64" dependencies = [ "cfg-if", "fastrand", + "once_cell", "rustix", - "windows-sys", + "windows-sys 0.59.0", ] [[package]] @@ -673,6 +675,15 @@ dependencies = [ "windows-targets", ] +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-targets" version = "0.52.6" diff --git a/examples/advanced_request_get.rs b/examples/advanced_request_get.rs index abf6788..6b60897 100644 --- a/examples/advanced_request_get.rs +++ b/examples/advanced_request_get.rs @@ -24,7 +24,7 @@ fn main() { .parse(); // Connects to a server. Uses information from `addr`. - let mut stream = Stream::new(&addr, Some(Duration::from_secs(60))).unwrap(); + let mut stream = Stream::connect(&addr, Some(Duration::from_secs(60))).unwrap(); stream = Stream::try_to_https(stream, &addr, None).unwrap(); // Makes a request to server. Sends the prepared message. diff --git a/src/chunked.rs b/src/chunked.rs index 9b4616e..25ac72d 100644 --- a/src/chunked.rs +++ b/src/chunked.rs @@ -175,7 +175,7 @@ fn is_ascii_space(b: u8) -> bool { } } -fn parse_hex_uint(data: Vec) -> Result { +fn parse_hex_uint<'a>(data: Vec) -> Result { let mut n = 0usize; for (i, v) in data.iter().enumerate() { if i == 16 { diff --git a/src/error.rs b/src/error.rs index 7168732..4e54570 100644 --- a/src/error.rs +++ b/src/error.rs @@ -29,13 +29,13 @@ impl fmt::Display for ParseErr { use self::ParseErr::*; let err = match self { - Utf8(_) => "invalid character", - Int(_) => "cannot parse number", - Invalid => "invalid value", - Empty => "nothing to parse", - StatusErr => "status line contains invalid values", - HeadersErr => "headers contain invalid values", - UriErr => "uri contains invalid characters", + Utf8(_) => "Invalid character", + Int(_) => "Cannot parse number", + Invalid => "Invalid value", + Empty => "Nothing to parse", + StatusErr => "Status line contains invalid values", + HeadersErr => "Headers contain invalid values", + UriErr => "URI contains invalid characters", }; write!(f, "ParseErr: {}", err) } @@ -57,8 +57,9 @@ impl From for ParseErr { pub enum Error { IO(io::Error), Parse(ParseErr), - Timeout(mpsc::RecvTimeoutError), + Timeout, Tls, + Thread, } impl error::Error for Error { @@ -68,8 +69,7 @@ impl error::Error for Error { match self { IO(e) => Some(e), Parse(e) => Some(e), - Timeout(e) => Some(e), - Tls => None, + Timeout | Tls | Thread => None, } } } @@ -81,27 +81,14 @@ impl fmt::Display for Error { let err = match self { IO(_) => "IO error", Parse(err) => return err.fmt(f), - Timeout(_) => "Timeout error", + Timeout => "Timeout error", Tls => "TLS error", + Thread => "Thread communication error", }; write!(f, "Error: {}", err) } } -#[cfg(feature = "native-tls")] -impl From for Error { - fn from(_e: native_tls::Error) -> Self { - Error::Tls - } -} - -#[cfg(feature = "native-tls")] -impl From> for Error { - fn from(_e: native_tls::HandshakeError) -> Self { - Error::Tls - } -} - impl From for Error { fn from(e: io::Error) -> Self { Error::IO(e) @@ -121,8 +108,8 @@ impl From for Error { } impl From for Error { - fn from(e: mpsc::RecvTimeoutError) -> Self { - Error::Timeout(e) + fn from(_e: mpsc::RecvTimeoutError) -> Self { + Error::Timeout } } @@ -132,3 +119,23 @@ impl From for Error { Error::Tls } } + +#[cfg(feature = "native-tls")] +impl From for Error { + fn from(_e: native_tls::Error) -> Self { + Error::Tls + } +} + +#[cfg(feature = "native-tls")] +impl From> for Error { + fn from(_e: native_tls::HandshakeError) -> Self { + Error::Tls + } +} + +impl From> for Error { + fn from(_e: mpsc::SendError) -> Self { + Error::Thread + } +} diff --git a/src/request.rs b/src/request.rs index 2ed0a90..3add606 100644 --- a/src/request.rs +++ b/src/request.rs @@ -18,6 +18,7 @@ use std::{ const CR_LF: &str = "\r\n"; const DEFAULT_REQ_TIMEOUT: u64 = 60 * 60; +const DEFAULT_CALL_TIMEOUT: u64 = 60; /// HTTP request methods #[derive(Debug, PartialEq, Clone, Copy)] @@ -308,8 +309,8 @@ impl<'a> RequestMessage<'a> { let mut request_msg = (request_line + &headers + CR_LF).as_bytes().to_vec(); - if let Some(b) = &self.body { - request_msg.extend(*b); + if let Some(b) = self.body { + request_msg.extend(b); } request_msg @@ -363,9 +364,9 @@ impl<'a> Request<'a> { Request { messsage: message, redirect_policy: RedirectPolicy::default(), - connect_timeout: Some(Duration::from_secs(60)), - read_timeout: Some(Duration::from_secs(60)), - write_timeout: Some(Duration::from_secs(60)), + connect_timeout: Some(Duration::from_secs(DEFAULT_CALL_TIMEOUT)), + read_timeout: Some(Duration::from_secs(DEFAULT_CALL_TIMEOUT)), + write_timeout: Some(Duration::from_secs(DEFAULT_CALL_TIMEOUT)), timeout: Duration::from_secs(DEFAULT_REQ_TIMEOUT), root_cert_file_pem: None, } @@ -613,8 +614,11 @@ impl<'a> Request<'a> { /// let request = Request::new(&uri) /// .redirect_policy(RedirectPolicy::Limit(5)); /// ``` - pub fn redirect_policy(&mut self, policy: RedirectPolicy bool>) -> &mut Self { - self.redirect_policy = policy; + pub fn redirect_policy(&mut self, policy: T) -> &mut Self + where + RedirectPolicy bool>: From, + { + self.redirect_policy = RedirectPolicy::from(policy); self } @@ -638,7 +642,7 @@ impl<'a> Request<'a> { T: Write, { // Set up a stream. - let mut stream = Stream::new(self.messsage.uri, self.connect_timeout)?; + let mut stream = Stream::connect(self.messsage.uri, self.connect_timeout)?; stream.set_read_timeout(self.read_timeout)?; stream.set_write_timeout(self.write_timeout)?; stream = Stream::try_to_https(stream, self.messsage.uri, self.root_cert_file_pem)?; @@ -689,7 +693,7 @@ impl<'a> Request<'a> { } let params = response.basic_info(&self.messsage.method).to_vec(); - sender_supp.send(params).unwrap(); + sender_supp.send(params)?; // Receive and process `body` of the response. let content_len = response.content_len().unwrap_or(1); diff --git a/src/stream.rs b/src/stream.rs index 27b0e60..6b60879 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -21,7 +21,7 @@ pub enum Stream { impl Stream { /// Opens a TCP connection to a remote host with a connection timeout (if specified). - pub fn new(uri: &Uri, connect_timeout: Option) -> Result { + pub fn connect(uri: &Uri, connect_timeout: Option) -> Result { let host = uri.host().unwrap_or(""); let port = uri.corr_port(); @@ -120,7 +120,7 @@ where { fn send_head(&mut self, sender: &Sender>) { let buf = read_head(self); - sender.send(buf).unwrap(); + sender.send(buf).unwrap_or(()); } fn send_all(&mut self, sender: &Sender>) { @@ -131,7 +131,9 @@ where Ok(0) | Err(_) => break, Ok(len) => { let filled_buf = buf[..len].to_vec(); - sender.send(filled_buf).unwrap(); + if let Err(_) = sender.send(filled_buf) { + break; + } } } } @@ -175,7 +177,7 @@ where Ok(data) => data, Err(e) => { if e == RecvTimeoutError::Timeout { - result = Err(Error::Timeout(RecvTimeoutError::Timeout)); + result = Err(Error::Timeout); } return true; } @@ -189,7 +191,7 @@ where is_complete }); - Ok(result?) + result } } @@ -298,13 +300,13 @@ mod tests { fn stream_new() { { let uri = Uri::try_from(URI).unwrap(); - let stream = Stream::new(&uri, None); + let stream = Stream::connect(&uri, None); assert!(stream.is_ok()); } { let uri = Uri::try_from(URI).unwrap(); - let stream = Stream::new(&uri, Some(TIMEOUT)); + let stream = Stream::connect(&uri, Some(TIMEOUT)); assert!(stream.is_ok()); } @@ -314,7 +316,7 @@ mod tests { fn stream_try_to_https() { { let uri = Uri::try_from(URI_S).unwrap(); - let stream = Stream::new(&uri, None).unwrap(); + let stream = Stream::connect(&uri, None).unwrap(); let https_stream = Stream::try_to_https(stream, &uri, None); assert!(https_stream.is_ok()); @@ -327,7 +329,7 @@ mod tests { } { let uri = Uri::try_from(URI).unwrap(); - let stream = Stream::new(&uri, None).unwrap(); + let stream = Stream::connect(&uri, None).unwrap(); let https_stream = Stream::try_to_https(stream, &uri, None); assert!(https_stream.is_ok()); @@ -344,7 +346,7 @@ mod tests { fn stream_set_read_timeot() { { let uri = Uri::try_from(URI).unwrap(); - let mut stream = Stream::new(&uri, None).unwrap(); + let mut stream = Stream::connect(&uri, None).unwrap(); stream.set_read_timeout(Some(TIMEOUT)).unwrap(); let inner_read_timeout = if let Stream::Http(inner) = stream { @@ -357,7 +359,7 @@ mod tests { } { let uri = Uri::try_from(URI_S).unwrap(); - let mut stream = Stream::new(&uri, None).unwrap(); + let mut stream = Stream::connect(&uri, None).unwrap(); stream = Stream::try_to_https(stream, &uri, None).unwrap(); stream.set_read_timeout(Some(TIMEOUT)).unwrap(); @@ -375,7 +377,7 @@ mod tests { fn stream_set_write_timeot() { { let uri = Uri::try_from(URI).unwrap(); - let mut stream = Stream::new(&uri, None).unwrap(); + let mut stream = Stream::connect(&uri, None).unwrap(); stream.set_write_timeout(Some(TIMEOUT)).unwrap(); let inner_read_timeout = if let Stream::Http(inner) = stream { @@ -388,7 +390,7 @@ mod tests { } { let uri = Uri::try_from(URI_S).unwrap(); - let mut stream = Stream::new(&uri, None).unwrap(); + let mut stream = Stream::connect(&uri, None).unwrap(); stream = Stream::try_to_https(stream, &uri, None).unwrap(); stream.set_write_timeout(Some(TIMEOUT)).unwrap(); diff --git a/src/uri.rs b/src/uri.rs index 1877547..7930dc0 100644 --- a/src/uri.rs +++ b/src/uri.rs @@ -240,27 +240,59 @@ impl<'a> Uri<'a> { } } - /// Checks if &str is an relative uri. + /// Checks if &str is a relative uri. pub fn is_relative(raw_uri: &str) -> bool { raw_uri.starts_with("/") || raw_uri.starts_with("?") || raw_uri.starts_with("#") - || raw_uri.starts_with("@") + || !raw_uri.contains(":") } /// Creates a new `Uri` from current uri and relative uri. /// Transforms the relative uri into an absolute uri. pub fn from_relative(&'a self, relative_uri: &'a mut String) -> Result, Error> { let inner_uri = self.inner; - let mut relative_part = relative_uri.as_ref(); + let mut resource = self.resource().to_string(); + + resource = match &relative_uri.get(..1) { + Some("#") => Uri::add_part(&resource, relative_uri, "#"), + Some("?") => Uri::add_part(&self.path().unwrap_or("/"), relative_uri, "?"), + Some("/") => Uri::add_part(&resource, relative_uri, "/"), + Some(_) | None => { + let part_idx = resource.rfind("/"); + + match part_idx { + Some(idx) => resource[..idx].to_string() + relative_uri, + None => { + if resource.starts_with("/") { + resource.to_string() + relative_uri + } else { + resource.to_string() + "/" + relative_uri + } + } + } + } + }; - if inner_uri.ends_with("/") && relative_uri.starts_with("/") { - relative_part = relative_uri.trim_start_matches("/"); - } + *relative_uri = if let Some(p) = self.path { + inner_uri[..p.start].to_string() + &resource + } else { + inner_uri.trim_end_matches("/").to_string() + &resource + }; - *relative_uri = inner_uri.to_owned() + relative_part; Uri::try_from(relative_uri.as_str()) } + + /// Adds a part to a base at the position definied by a separator. + /// If the separator is not found, concatenates 2 strings together. + fn add_part(base: &str, part: &str, separator: &str) -> String { + let part_idx = base.find(separator); + + match part_idx { + Some(idx) => base[..idx].to_string() + part, + None => base.to_string() + part, + } + } } impl<'a> fmt::Display for Uri<'a> { @@ -531,6 +563,16 @@ mod tests { "[4b10:bbb0:0:d0::ba7:8001]:443", ]; + const TEST_PARTS: [&str; 7] = [ + "?query123", + "/path", + "#fragment", + "other-path", + "#paragraph", + "/foo/bar/buz", + "?users#1551", + ]; + #[test] fn remove_space() { let mut text = String::from("Hello World !"); @@ -749,6 +791,55 @@ mod tests { } } + #[test] + fn uri_is_relative() { + for i in 0..TEST_URIS.len() { + assert!(!Uri::is_relative(TEST_URIS[i])); + } + + for i in 0..TEST_PARTS.len() { + assert!(Uri::is_relative(TEST_PARTS[i])); + } + } + + #[test] + fn uri_from_relative() { + let uris: Vec<_> = TEST_URIS + .iter() + .map(|&uri| Uri::try_from(uri).unwrap()) + .collect(); + + const RESULT: [&str; 7] = [ + "https://user:info@foo.com:12/bar/baz?query123", + "file:///path", + "https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol#fragment", + "mailto:John.Doe@example.com/other-path", + "https://[4b10:bbb0:0:d0::ba7:8001]:443/#paragraph", + "http://example.com/foo/bar/buz", + "https://example.com/?users#1551", + ]; + + for i in 0..RESULT.len() { + let mut uri_part = TEST_PARTS[i].to_string(); + + println!("{}", uris[i].resource()); + assert_eq!( + uris[i].from_relative(&mut uri_part).unwrap().inner, + RESULT[i] + ); + } + } + + #[test] + fn uri_add_part() { + const BASES: [&str; 2] = ["/bar/baz?query", "/bar/baz?query#some-fragment"]; + const RESULT: &str = "/bar/baz?query#another-fragment"; + + for i in 0..BASES.len() { + assert_eq!(Uri::add_part(BASES[i], "#another-fragment", "#"), RESULT); + } + } + #[test] fn uri_display() { let uris: Vec<_> = TEST_URIS From b3f828cb6782e1ff7184693a996660d9189fb2ec Mon Sep 17 00:00:00 2001 From: jayjamesjay Date: Sat, 17 Aug 2024 14:00:26 +0200 Subject: [PATCH 4/4] improve timeout error handling & relative uri support --- Cargo.lock | 8 ++-- src/stream.rs | 63 ++++++++++++++++-------------- src/uri.rs | 103 ++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 114 insertions(+), 60 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e3ade9a..5bc715d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -75,9 +75,9 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "cc" -version = "1.1.12" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68064e60dbf1f17005c2fde4d07c16d8baa506fd7ffed8ccab702d93617975c7" +checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" dependencies = [ "jobserver", "libc", @@ -255,9 +255,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" [[package]] name = "libc" -version = "0.2.155" +version = "0.2.156" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "a5f43f184355eefb8d17fc948dbecf6c13be3c141f20d834ae842193a448c72a" [[package]] name = "libloading" diff --git a/src/stream.rs b/src/stream.rs index 6b60879..e9840c6 100644 --- a/src/stream.rs +++ b/src/stream.rs @@ -20,6 +20,15 @@ pub enum Stream { } impl Stream { + /// Opens a TCP connection to a remote host with a connection timeout (if specified). + #[deprecated( + since = "0.12.0", + note = "Stream::new(uri, connect_timeout) was replaced with Stream::connect(uri, connect_timeout)" + )] + pub fn new(uri: &Uri, connect_timeout: Option) -> Result { + Stream::connect(uri, connect_timeout) + } + /// Opens a TCP connection to a remote host with a connection timeout (if specified). pub fn connect(uri: &Uri, connect_timeout: Option) -> Result { let host = uri.host().unwrap_or(""); @@ -168,30 +177,18 @@ where receiver: &Receiver>, deadline: Instant, ) -> Result<(), Error> { - let mut result = Ok(()); - execute_with_deadline(deadline, |remaining_time| { - let mut is_complete = false; - let data_read = match receiver.recv_timeout(remaining_time) { Ok(data) => data, - Err(e) => { - if e == RecvTimeoutError::Timeout { - result = Err(Error::Timeout); - } - return true; - } + Err(e) => match e { + RecvTimeoutError::Timeout => return Err(Error::Timeout), + RecvTimeoutError::Disconnected => return Ok(true), + }, }; - if let Err(e) = self.write_all(&data_read).map_err(|e| Error::IO(e)) { - result = Err(e); - is_complete = true; - } - - is_complete - }); - - result + self.write_all(&data_read).map_err(|e| Error::IO(e))?; + Ok(false) + }) } } @@ -236,19 +233,27 @@ where /// Key information about function `func`: /// - is provided with information about remaining time /// - must ensure that its execution will not take more time than specified in `remaining_time` -/// - needs to return `true` when the operation is complete -pub fn execute_with_deadline(deadline: Instant, mut func: F) +/// - needs to return `Some(true)` when the operation is complete, and `Some(false)` - when operation is in progress +pub fn execute_with_deadline(deadline: Instant, mut func: F) -> Result<(), Error> where - F: FnMut(Duration) -> bool, + F: FnMut(Duration) -> Result, { loop { let now = Instant::now(); let remaining_time = deadline - now; - if deadline < now || func(remaining_time) == true { - break; + if deadline < now { + return Err(Error::Timeout); + } + + match func(remaining_time) { + Ok(true) => break, + Ok(false) => continue, + Err(e) => return Err(e), } } + + Ok(()) } /// Reads the head of HTTP response from `reader`. @@ -475,17 +480,18 @@ mod tests { let star_time = Instant::now(); let deadline = star_time + TIMEOUT; - execute_with_deadline(deadline, |_| { + let timeout_err = execute_with_deadline(deadline, |_| { let sleep_time = Duration::from_millis(500); thread::sleep(sleep_time); - false + Ok(false) }); let end_time = Instant::now(); let total_time = end_time.duration_since(star_time).as_secs(); assert_eq!(total_time, TIMEOUT.as_secs()); + assert!(timeout_err.is_err()); } { let star_time = Instant::now(); @@ -495,8 +501,9 @@ mod tests { let sleep_time = Duration::from_secs(1); thread::sleep(sleep_time); - true - }); + Ok(true) + }) + .unwrap(); let end_time = Instant::now(); let total_time = end_time.duration_since(star_time).as_secs(); diff --git a/src/uri.rs b/src/uri.rs index 7930dc0..c4a65ce 100644 --- a/src/uri.rs +++ b/src/uri.rs @@ -255,23 +255,10 @@ impl<'a> Uri<'a> { let mut resource = self.resource().to_string(); resource = match &relative_uri.get(..1) { - Some("#") => Uri::add_part(&resource, relative_uri, "#"), - Some("?") => Uri::add_part(&self.path().unwrap_or("/"), relative_uri, "?"), - Some("/") => Uri::add_part(&resource, relative_uri, "/"), - Some(_) | None => { - let part_idx = resource.rfind("/"); - - match part_idx { - Some(idx) => resource[..idx].to_string() + relative_uri, - None => { - if resource.starts_with("/") { - resource.to_string() + relative_uri - } else { - resource.to_string() + "/" + relative_uri - } - } - } - } + Some("#") => Uri::add_part_start(&resource, relative_uri, "#"), + Some("?") => Uri::add_part_start(&self.path().unwrap_or("/"), relative_uri, "?"), + Some("/") => Uri::add_part_start(&resource, relative_uri, "/"), + Some(_) | None => Uri::add_part_end(&resource, relative_uri, "/"), }; *relative_uri = if let Some(p) = self.path { @@ -283,15 +270,43 @@ impl<'a> Uri<'a> { Uri::try_from(relative_uri.as_str()) } - /// Adds a part to a base at the position definied by a separator. - /// If the separator is not found, concatenates 2 strings together. - fn add_part(base: &str, part: &str, separator: &str) -> String { - let part_idx = base.find(separator); + /// Adds a part at the beggining of the base. + /// Finds the first occurance of a separator in a base and the first occurance of a separator in a part. + /// Joins all chars before the separator from the base, separator and all chars after the separator from the part. + fn add_part_start(base: &str, part: &str, separator: &str) -> String { + let base_idx = base.find(separator); + Uri::add_part(base, part, separator, base_idx) + } + + /// Adds a part at the end of the base. + /// Finds the last occurance of a separator in a base and the first occurance of a separator in a part. + /// Joins all chars before the separator from the base, separator and all chars after the separator from the part. + fn add_part_end(base: &str, part: &str, separator: &str) -> String { + let base_idx = base.rfind(separator); + Uri::add_part(base, part, separator, base_idx) + } + + /// Adds a part to the base with separator in between. + /// Base index defines where part should be added. + fn add_part(base: &str, part: &str, separator: &str, base_idx: Option) -> String { + let mut output = String::new(); + let part_idx = part.find(separator); + + if let Some(idx) = base_idx { + output += &base[..idx]; + } else { + output += base; + } + + output += separator; - match part_idx { - Some(idx) => base[..idx].to_string() + part, - None => base.to_string() + part, + if let Some(idx) = part_idx { + output += &part[idx + 1..]; + } else { + output += part; } + + output } } @@ -569,7 +584,7 @@ mod tests { "#fragment", "other-path", "#paragraph", - "/foo/bar/buz", + "./foo/bar/buz", "?users#1551", ]; @@ -832,11 +847,43 @@ mod tests { #[test] fn uri_add_part() { - const BASES: [&str; 2] = ["/bar/baz?query", "/bar/baz?query#some-fragment"]; - const RESULT: &str = "/bar/baz?query#another-fragment"; + const BASES: [&str; 2] = ["/bar/baz/fizz?query", "/bar/baz?query#some-fragment"]; + const RESULT: [&str; 2] = [ + "/bar/baz/fizz?query#another-fragment", + "/bar/baz?query#some-fragment#another-fragment", + ]; + + for i in 0..BASES.len() { + assert_eq!( + Uri::add_part(BASES[i], "#another-fragment", "#", Some(BASES[i].len())), + RESULT[i] + ); + } + } + + #[test] + fn uri_add_part_start() { + const BASES: [&str; 2] = ["/bar/baz/fizz?query", "/bar/baz?query#some-fragment"]; + const RESULT: [&str; 2] = [ + "/bar/baz/fizz?query#another-fragment", + "/bar/baz?query#another-fragment", + ]; + + for i in 0..BASES.len() { + assert_eq!( + Uri::add_part_start(BASES[i], "#another-fragment", "#"), + RESULT[i] + ); + } + } + + #[test] + fn uri_add_part_end() { + const BASES: [&str; 2] = ["/bar/baz/fizz?query", "/bar/baz?query#some-fragment"]; + const RESULT: [&str; 2] = ["/bar/baz/another", "/bar/another"]; for i in 0..BASES.len() { - assert_eq!(Uri::add_part(BASES[i], "#another-fragment", "#"), RESULT); + assert_eq!(Uri::add_part_end(BASES[i], "./another", "/"), RESULT[i]); } }