diff --git a/Cargo.lock b/Cargo.lock index fedc0d43..dea997fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,12 +44,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd" -[[package]] -name = "anyhow" -version = "1.0.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4668cab20f66d8d020e1fbc0ebe47217433c1b6c8f2040faf858554e394ace6" - [[package]] name = "async-stream" version = "0.3.5" @@ -269,6 +263,33 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2da6da31387c7e4ef160ffab6d5e7f00c42626fe39aea70a7b0f1773f7dd6c1b" +[[package]] +name = "color-eyre" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd6be1b2a7e382e2b98b43b2adcca6bb0e465af0bdd38123873ae61eb17a72c2" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -433,6 +454,16 @@ dependencies = [ "libc", ] +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fake-simd" version = "0.1.2" @@ -644,10 +675,10 @@ dependencies = [ name = "http-server" version = "0.8.8" dependencies = [ - "anyhow", "async-stream", "async-trait", "chrono", + "color-eyre", "criterion", "dhat", "flate2", @@ -756,6 +787,12 @@ dependencies = [ "cc", ] +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "2.0.0" @@ -999,6 +1036,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "parking_lot" version = "0.12.0" @@ -1390,6 +1433,15 @@ dependencies = [ "opaque-debug", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook-registry" version = "1.4.0" @@ -1517,6 +1569,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3bf63baf9f5039dadc247375c29eb13706706cfde997d0330d05aa63a77d8820" +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "tinytemplate" version = "1.2.1" @@ -1627,6 +1689,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f54c8ca710e81886d498c2fd3331b56c93aa248d49de2222ad2742247c60072f" dependencies = [ "lazy_static", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bc28f93baff38037f64e6f43d34cfa1605f27a49c34e8a04c5e78b0babf2596" +dependencies = [ + "sharded-slab", + "thread_local", + "tracing-core", ] [[package]] @@ -1680,6 +1764,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "version_check" version = "0.9.4" diff --git a/Cargo.toml b/Cargo.toml index 70f74211..88d16adf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,7 +28,6 @@ harness = false dhat-profiling = ["dhat"] [dependencies] -anyhow = "1.0.75" async-stream = "0.3.5" async-trait = "0.1.74" chrono = { version = "0.4.31", features = ["serde"] } @@ -58,6 +57,7 @@ tokio = { version = "1.29.1", features = [ tokio-rustls = "0.23.4" toml = "0.7.6" humansize = "2.1.3" +color-eyre = "0.6.2" [dev-dependencies] criterion = { version = "0.5.1", features = ["async_tokio", "html_reports"] } diff --git a/src/addon/compression/gzip.rs b/src/addon/compression/gzip.rs index cb26b075..e2ef74fc 100644 --- a/src/addon/compression/gzip.rs +++ b/src/addon/compression/gzip.rs @@ -1,4 +1,4 @@ -use anyhow::{Error, Result}; +use color_eyre::eyre::eyre; use flate2::write::GzEncoder; use http::{HeaderValue, Request, Response}; use hyper::body::aggregate; @@ -18,7 +18,7 @@ const IGNORED_CONTENT_TYPE: [&str; 6] = [ "video", ]; -pub async fn is_encoding_accepted(request: Arc>>) -> Result { +pub async fn is_encoding_accepted(request: Arc>>) -> color_eyre::Result { if let Some(accept_encoding) = request .lock() .await @@ -36,7 +36,9 @@ pub async fn is_encoding_accepted(request: Arc>>) -> Result< Ok(false) } -pub async fn is_compressable_content_type(response: Arc>>) -> Result { +pub async fn is_compressible_content_type( + response: Arc>>, +) -> color_eyre::Result { if let Some(content_type) = response .lock() .await @@ -58,26 +60,26 @@ pub async fn is_compressable_content_type(response: Arc>>) pub async fn should_compress( request: Arc>>, response: Arc>>, -) -> Result { +) -> color_eyre::Result { Ok(is_encoding_accepted(request).await? - && is_compressable_content_type(Arc::clone(&response)).await?) + && is_compressible_content_type(Arc::clone(&response)).await?) } -pub fn compress(bytes: &[u8]) -> Result> { +pub fn compress(bytes: &[u8]) -> color_eyre::Result> { let buffer: Vec = Vec::with_capacity(bytes.len()); let mut compressor: GzEncoder> = GzEncoder::new(buffer, flate2::Compression::default()); compressor.write_all(bytes)?; - compressor.finish().map_err(Error::from) + compressor.finish().map_err(|err| eyre!(err)) } pub async fn compress_http_response( request: Arc>>, response: Arc>>, -) -> Result<()> { - if let Ok(compressable) = should_compress(Arc::clone(&request), Arc::clone(&response)).await { - if compressable { +) -> color_eyre::Result<()> { + if let Ok(compressible) = should_compress(Arc::clone(&request), Arc::clone(&response)).await { + if compressible { let mut buffer: Vec = Vec::new(); { diff --git a/src/addon/cors.rs b/src/addon/cors.rs index ea51e816..92317996 100644 --- a/src/addon/cors.rs +++ b/src/addon/cors.rs @@ -1,6 +1,4 @@ -use anyhow::{Error, Result}; use hyper::header::{self, HeaderName, HeaderValue}; -use std::convert::TryFrom; use crate::config::cors::CorsConfig; @@ -211,10 +209,8 @@ impl CorsBuilder { } } -impl TryFrom for Cors { - type Error = Error; - - fn try_from(value: CorsConfig) -> Result { +impl From for Cors { + fn from(value: CorsConfig) -> Self { let mut builder = Cors::builder(); if value.allow_credentials { @@ -249,7 +245,7 @@ impl TryFrom for Cors { builder = builder.request_method(request_method); } - Ok(builder.build()) + builder.build() } } @@ -341,7 +337,7 @@ mod tests { "content-length".to_string(), "request-id".to_string(), ]; - let allow_mehtods = vec!["GET".to_string(), "POST".to_string(), "PUT".to_string()]; + let allow_methods = vec!["GET".to_string(), "POST".to_string(), "PUT".to_string()]; let allow_origin = String::from("github.com"); let expose_headers = vec!["content-type".to_string(), "request-id".to_string()]; let max_age = 5400; @@ -354,7 +350,7 @@ mod tests { let config = CorsConfig { allow_credentials: true, allow_headers: Some(allow_headers.clone()), - allow_methods: Some(allow_mehtods.clone()), + allow_methods: Some(allow_methods.clone()), allow_origin: Some(allow_origin.clone()), expose_headers: Some(expose_headers.clone()), max_age: Some(max_age), @@ -364,7 +360,7 @@ mod tests { let cors = Cors { allow_credentials: true, allow_headers: Some(allow_headers), - allow_methods: Some(allow_mehtods), + allow_methods: Some(allow_methods), allow_origin: Some(allow_origin), expose_headers: Some(expose_headers), max_age: Some(max_age), @@ -372,6 +368,6 @@ mod tests { request_method: Some(request_method), }; - assert_eq!(cors, Cors::try_from(config).unwrap()); + assert_eq!(cors, Cors::from(config)); } } diff --git a/src/addon/file_server/file.rs b/src/addon/file_server/file.rs index 8d003f54..c476a50f 100644 --- a/src/addon/file_server/file.rs +++ b/src/addon/file_server/file.rs @@ -1,5 +1,5 @@ -use anyhow::{Context, Result}; use chrono::{DateTime, Local}; +use color_eyre::eyre::Context; use futures::Stream; use hyper::body::Bytes; use mime_guess::{from_path, Mime}; @@ -40,7 +40,7 @@ impl File { self.metadata.len() } - pub fn last_modified(&self) -> Result> { + pub fn last_modified(&self) -> color_eyre::Result> { let modified = self .metadata .modified() @@ -80,7 +80,7 @@ impl From for ByteStream { } impl Stream for ByteStream { - type Item = Result; + type Item = color_eyre::Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { let ByteStream { diff --git a/src/addon/file_server/http_utils.rs b/src/addon/file_server/http_utils.rs index 6208ea5d..f4f68a5d 100644 --- a/src/addon/file_server/http_utils.rs +++ b/src/addon/file_server/http_utils.rs @@ -1,5 +1,5 @@ -use anyhow::{Context, Result}; use chrono::{DateTime, Local, Utc}; +use color_eyre::eyre::Context; use futures::Stream; use http::response::Builder as HttpResponseBuilder; use hyper::body::Body; @@ -69,8 +69,10 @@ impl ResponseHeaders { pub fn new( file: &File, cache_control_directive: CacheControlDirective, - ) -> Result { - let last_modified = file.last_modified()?; + ) -> color_eyre::Result { + let last_modified = file + .last_modified() + .context("Unable to get file's last modified date")?; Ok(ResponseHeaders { cache_control: cache_control_directive.to_string(), @@ -111,8 +113,9 @@ impl ResponseHeaders { pub async fn make_http_file_response( file: File, cache_control_directive: CacheControlDirective, -) -> Result> { - let headers = ResponseHeaders::new(&file, cache_control_directive)?; +) -> color_eyre::Result> { + let headers = ResponseHeaders::new(&file, cache_control_directive) + .context("Failed to construct response headers")?; let builder = HttpResponseBuilder::new() .header(http::header::CONTENT_LENGTH, headers.content_length) .header(http::header::CACHE_CONTROL, headers.cache_control) @@ -143,7 +146,7 @@ pub struct ByteStream { } impl Stream for ByteStream { - type Item = Result; + type Item = color_eyre::Result; fn poll_next(mut self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> Poll> { let ByteStream { diff --git a/src/addon/file_server/mod.rs b/src/addon/file_server/mod.rs index fdcf6ec2..15bee3a9 100644 --- a/src/addon/file_server/mod.rs +++ b/src/addon/file_server/mod.rs @@ -6,12 +6,13 @@ mod scoped_file_system; use chrono::{DateTime, Local}; +use color_eyre::eyre::{Context, ContextCompat}; +use color_eyre::Section; pub use file::File; use humansize::{format_size, DECIMAL}; pub use scoped_file_system::{Entry, ScopedFileSystem}; -use anyhow::{Context, Result}; use handlebars::{handlebars_helper, Handlebars}; use http::response::Builder as HttpResponseBuilder; use http::{StatusCode, Uri}; @@ -40,15 +41,16 @@ pub struct FileServer { impl<'a> FileServer { /// Creates a new instance of the `FileExplorer` with the provided `root_dir` - pub fn new(config: Arc) -> Self { + pub fn new(config: Arc) -> color_eyre::Result { let handlebars = FileServer::make_handlebars_engine(); - let scoped_file_system = ScopedFileSystem::new(config.root_dir.clone()).unwrap(); + let scoped_file_system = ScopedFileSystem::new(config.root_dir.clone()) + .context("Could not create scoped file system")?; - FileServer { + Ok(FileServer { handlebars, scoped_file_system, config, - } + }) } /// Creates a new `Handlebars` instance with templates registered @@ -88,14 +90,17 @@ impl<'a> FileServer { Arc::new(handlebars) } - fn parse_path(req_uri: &str) -> Result<(PathBuf, Option)> { - let uri = Uri::from_str(req_uri)?; + fn parse_path(req_uri: &str) -> color_eyre::Result<(PathBuf, Option)> { + let uri = Uri::from_str(req_uri).context("Cannot construct URI from request URI string")?; let uri_parts = uri.into_parts(); if let Some(path_and_query) = uri_parts.path_and_query { let path = path_and_query.path(); let query_params = if let Some(query_str) = path_and_query.query() { - Some(QueryParams::from_str(query_str)?) + Some( + QueryParams::from_str(query_str) + .context("Cannot construct QueryParams from query parameters string")?, + ) } else { None }; @@ -103,7 +108,7 @@ impl<'a> FileServer { return Ok((decode_uri(path), query_params)); } - Ok((PathBuf::from_str("/")?, None)) + Ok((PathBuf::from_str("/").unwrap(), None)) } /// Resolves a HTTP Request to a file or directory. @@ -130,8 +135,9 @@ impl<'a> FileServer { /// /// If the matched path resolves to a file, attempts to render it if the /// MIME type is supported, otherwise returns the binary (downloadable file) - pub async fn resolve(&self, req_path: String) -> Result> { - let (path, query_params) = FileServer::parse_path(req_path.as_str())?; + pub async fn resolve(&self, req_path: String) -> color_eyre::Result> { + let (path, query_params) = + FileServer::parse_path(req_path.as_str()).context("Failed to parse request path")?; match self.scoped_file_system.resolve(path).await { Ok(entry) => match entry { @@ -144,7 +150,10 @@ impl<'a> FileServer { return make_http_file_response( File { path: filepath, - metadata: file.metadata().await?, + metadata: file + .metadata() + .await + .context("Failed to get file metadata")?, file, }, CacheControlDirective::MaxAge(2500), @@ -166,7 +175,10 @@ impl<'a> FileServer { let mut path = self.config.root_dir.clone(); path.push("index.html"); - let file = tokio::fs::File::open(&path).await?; + let file = tokio::fs::File::open(&path) + .await + .with_context(|| format!("Failed to open index.html at {path:?}")) + .with_suggestion(|| "Create index.html in root")?; let metadata = file.metadata().await?; @@ -215,7 +227,7 @@ impl<'a> FileServer { &self, path: PathBuf, query_params: Option, - ) -> Result> { + ) -> color_eyre::Result> { let directory_index = FileServer::index_directory(self.config.root_dir.clone(), path, query_params)?; let html = self @@ -247,14 +259,17 @@ impl<'a> FileServer { utf8_percent_encode(component, PERCENT_ENCODE_SET).to_string() } - fn breadcrumbs_from_path(root_dir: &Path, path: &Path) -> Result> { + fn breadcrumbs_from_path( + root_dir: &Path, + path: &Path, + ) -> color_eyre::Result> { let root_dir_name = root_dir .components() .last() - .unwrap() + .context("Unable to get last root directory component")? .as_os_str() .to_str() - .expect("The first path component is not UTF-8 charset compliant."); + .context("The last path component is not UTF-8 charset compliant.")?; let stripped = path .strip_prefix(root_dir)? .components() @@ -290,14 +305,15 @@ impl<'a> FileServer { root_dir: PathBuf, path: PathBuf, query_params: Option, - ) -> Result { - let breadcrumbs = FileServer::breadcrumbs_from_path(&root_dir, &path)?; + ) -> color_eyre::Result { + let breadcrumbs = FileServer::breadcrumbs_from_path(&root_dir, &path) + .context("Failed to create breadcrumbs")?; let entries = read_dir(path).context("Unable to read directory")?; let mut directory_entries: Vec = Vec::new(); for entry in entries { let entry = entry.context("Unable to read entry")?; - let metadata = entry.metadata()?; + let metadata = entry.metadata().context("Unable to get entry metadata")?; let date_created = if let Ok(time) = metadata.created() { Some(time.into()) } else { @@ -317,7 +333,8 @@ impl<'a> FileServer { .to_string(), is_dir: metadata.is_dir(), size_bytes: metadata.len(), - entry_path: FileServer::make_dir_entry_link(&root_dir, &entry.path()), + entry_path: FileServer::make_dir_entry_link(&root_dir, &entry.path()) + .context("Failed to create directory entry link")?, date_created, date_modified, }); @@ -373,8 +390,10 @@ impl<'a> FileServer { /// /// This happens because links should behave relative to the `/` path /// which in this case is `http-server/src` instead of system's root path. - fn make_dir_entry_link(root_dir: &Path, entry_path: &Path) -> String { - let path = entry_path.strip_prefix(root_dir).unwrap(); + fn make_dir_entry_link(root_dir: &Path, entry_path: &Path) -> color_eyre::Result { + let path = entry_path.strip_prefix(root_dir).with_context(|| { + format!("Unable to construct relative path between entry path ({entry_path:?}) and root directory path ({root_dir:?})") + })?; encode_uri(path) } @@ -397,7 +416,7 @@ mod tests { assert_eq!( "/src/server/service/testing%20stuff/filename%20with%20spaces.txt", - FileServer::make_dir_entry_link(&root_dir, &entry_path) + FileServer::make_dir_entry_link(&root_dir, &entry_path).unwrap() ); } diff --git a/src/addon/file_server/query_params.rs b/src/addon/file_server/query_params.rs index c548b1a4..caefd9f2 100644 --- a/src/addon/file_server/query_params.rs +++ b/src/addon/file_server/query_params.rs @@ -1,4 +1,4 @@ -use anyhow::Error; +use color_eyre::eyre::{eyre, Report}; use serde::Serialize; use std::str::FromStr; @@ -11,7 +11,7 @@ pub enum SortBy { } impl FromStr for SortBy { - type Err = Error; + type Err = Report; fn from_str(s: &str) -> Result { let lower = s.to_ascii_lowercase(); @@ -22,7 +22,7 @@ impl FromStr for SortBy { "size" => Ok(Self::Size), "date_created" => Ok(Self::DateCreated), "date_modified" => Ok(Self::DateModified), - _ => Err(Error::msg("Value doesnt correspond")), + _ => Err(eyre!("Value doesn't correspond")), } } } @@ -33,7 +33,7 @@ pub struct QueryParams { } impl FromStr for QueryParams { - type Err = Error; + type Err = Report; fn from_str(s: &str) -> Result { let mut query_params = QueryParams::default(); diff --git a/src/addon/file_server/scoped_file_system.rs b/src/addon/file_server/scoped_file_system.rs index d1145148..8b162a6d 100644 --- a/src/addon/file_server/scoped_file_system.rs +++ b/src/addon/file_server/scoped_file_system.rs @@ -8,7 +8,6 @@ //! The `Entry` is a wrapper on OS file system entries such as `File` and //! `Directory`. Both `File` and `Directory` are primitive types for //! `ScopedFileSystem` -use anyhow::Result; use std::path::{Component, Path, PathBuf}; use tokio::fs::OpenOptions; @@ -58,8 +57,8 @@ impl ScopedFileSystem { /// Creates a new instance of `ScopedFileSystem` using the provided PathBuf /// as the root directory to serve files from. /// - /// Provided paths will resolve relartive to the provided `root` directory. - pub fn new(root: PathBuf) -> Result { + /// Provided paths will resolve relative to the provided `root` directory. + pub fn new(root: PathBuf) -> color_eyre::Result { Ok(ScopedFileSystem { root }) } diff --git a/src/addon/logger.rs b/src/addon/logger.rs index cdddeeac..a601704f 100644 --- a/src/addon/logger.rs +++ b/src/addon/logger.rs @@ -1,4 +1,3 @@ -use anyhow::Result; use chrono::{DateTime, Utc}; use http::header::USER_AGENT; use http::Method; @@ -19,7 +18,11 @@ impl Logger { Logger { buffer_writer } } - pub async fn log(&mut self, request: Request, response: Response) -> Result<()> { + pub async fn log( + &mut self, + request: Request, + response: Response, + ) -> color_eyre::Result<()> { let mut buffer = self.buffer_writer.buffer(); let request = request.lock().await; let response = response.lock().await; diff --git a/src/addon/proxy.rs b/src/addon/proxy.rs index fb841692..5dbb6a8a 100644 --- a/src/addon/proxy.rs +++ b/src/addon/proxy.rs @@ -1,4 +1,3 @@ -use std::str::FromStr; use std::sync::Arc; use http::header::USER_AGENT; @@ -15,14 +14,13 @@ pub struct Proxy { } impl Proxy { - pub fn new(upstream: &str) -> Self { + pub fn new(upstream: Uri) -> Self { let https_connector = HttpsConnectorBuilder::new() .with_webpki_roots() .https_or_http() .enable_http1() .build(); let client = Client::builder().build::<_, hyper::Body>(https_connector); - let upstream = Uri::from_str(upstream).unwrap(); Proxy { client, upstream } } @@ -153,7 +151,7 @@ impl Proxy { ); // Specify Proxy as User Agent - headers.remove(USER_AGENT).unwrap(); + headers.remove(USER_AGENT); headers.append(USER_AGENT, HeaderValue::from_static("Rust http-server/1.0")); request @@ -184,7 +182,9 @@ mod tests { CONNECTION, PROXY_AUTHENTICATE, PROXY_AUTHORIZATION, TE, TRAILER, TRANSFER_ENCODING, UPGRADE, }; + use http::Uri; use hyper::Body; + use std::str::FromStr; use std::sync::Arc; use tokio::sync::Mutex; @@ -192,7 +192,7 @@ mod tests { #[tokio::test] async fn adds_via_header_if_not_present() { - let proxy = Proxy::new("https://example.com"); + let proxy = Proxy::new(Uri::from_str("https://example.com").unwrap()); let request = http::Request::new(Body::empty()); let request = Arc::new(Mutex::new(request)); @@ -211,7 +211,7 @@ mod tests { #[tokio::test] async fn appends_via_header_if_another_is_present() { - let proxy = Proxy::new("https://example.com"); + let proxy = Proxy::new(Uri::from_str("https://example.com").unwrap()); let mut request = http::Request::new(Body::empty()); let headers = request.headers_mut(); @@ -240,7 +240,7 @@ mod tests { #[tokio::test] async fn removes_hbh_headers() { - let proxy = Proxy::new("https://example.com"); + let proxy = Proxy::new(Uri::from_str("https://example.com").unwrap()); let mut request = http::Request::new(Body::empty()); let headers = request.headers_mut(); let headers_to_add = vec![ diff --git a/src/bin/main.rs b/src/bin/main.rs index fc195ab8..3b8c1866 100644 --- a/src/bin/main.rs +++ b/src/bin/main.rs @@ -1,5 +1,5 @@ +use color_eyre::eyre::Context; use http_server_lib::make_server; -use std::process::exit; #[cfg(feature = "dhat-profiling")] use dhat::{Dhat, DhatAlloc}; @@ -9,17 +9,17 @@ use dhat::{Dhat, DhatAlloc}; static ALLOCATOR: DhatAlloc = DhatAlloc; #[tokio::main] -async fn main() { +async fn main() -> color_eyre::Result<()> { #[cfg(feature = "dhat-profiling")] let _dhat = Dhat::start_heap_profiling(); - match make_server() { - Ok(server) => { - server.run().await; - } - Err(error) => { - eprint!("{:?}", error); - exit(1); - } - } + color_eyre::install()?; + + make_server() + .context("Failed to create server")? + .run() + .await + .context("Error while running server")?; + + Ok(()) } diff --git a/src/config/file.rs b/src/config/file.rs index 659eca96..ffcb4f43 100644 --- a/src/config/file.rs +++ b/src/config/file.rs @@ -1,4 +1,4 @@ -use anyhow::{Error, Result}; +use color_eyre::eyre::{eyre, Context}; use serde::{Deserialize, Deserializer}; use std::fs; use std::net::IpAddr; @@ -31,31 +31,29 @@ pub struct ConfigFile { } impl ConfigFile { - pub fn from_file(file_path: PathBuf) -> Result { - let file = fs::read_to_string(file_path)?; - let config = ConfigFile::parse_toml(file.as_str())?; + pub fn from_file(file_path: PathBuf) -> color_eyre::Result { + let file = fs::read_to_string(file_path).context("Failed to read config file")?; + let config = + ConfigFile::parse_toml(file.as_str()).context("Failed to parse config file as TOML")?; Ok(config) } - fn parse_toml(content: &str) -> Result { + fn parse_toml(content: &str) -> color_eyre::Result { match toml::from_str(content) { Ok(config) => Ok(config), - Err(err) => Err(Error::msg(format!( - "Failed to parse config from file. {}", - err - ))), + Err(err) => Err(eyre!("Failed to parse config from file. {}", err)), } } } -fn canonicalize_some<'de, D>(deserializer: D) -> Result, D::Error> +fn canonicalize_some<'de, D>(deserializer: D) -> color_eyre::Result, D::Error> where D: Deserializer<'de>, { let value: &str = Deserialize::deserialize(deserializer)?; - let path = PathBuf::from_str(value).unwrap(); - let canon = fs::canonicalize(path).unwrap(); + let path = PathBuf::from_str(value).unwrap(); // Unreachable + let canon = fs::canonicalize(path).unwrap(); // Unreachable Ok(Some(canon)) } @@ -319,7 +317,7 @@ mod tests { let proxy = config.proxy.unwrap(); - assert_eq!(proxy.url, "https://example.com"); + assert_eq!(proxy.uri, "https://example.com"); } #[test] diff --git a/src/config/mod.rs b/src/config/mod.rs index eecc3852..ca9d70c4 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -6,11 +6,14 @@ pub mod proxy; pub mod tls; pub mod util; -use anyhow::{Error, Result}; +use color_eyre::eyre::{eyre, Context}; +use color_eyre::{Report, Section}; +use http::Uri; use std::convert::TryFrom; use std::env::current_dir; use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::path::PathBuf; +use std::str::FromStr; use crate::cli::Cli; @@ -44,7 +47,7 @@ impl Default for Config { let host = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let port = 7878; let address = SocketAddr::new(host, port); - let root_dir = current_dir().unwrap(); + let root_dir = current_dir().unwrap_or_default(); Self { host, @@ -66,27 +69,21 @@ impl Default for Config { } impl TryFrom for Config { - type Error = anyhow::Error; + type Error = Report; fn try_from(cli_arguments: Cli) -> Result { - let quiet = cli_arguments.quiet; - let root_dir = if cli_arguments.root_dir.to_str().unwrap() == "./" { - current_dir().unwrap() - } else { - let root_dir = cli_arguments.root_dir.to_str().unwrap(); - - cli_arguments - .root_dir - .canonicalize() - .unwrap_or_else(|_| panic!("Failed to find config on: {}", root_dir)) - }; + let root_dir = cli_arguments + .root_dir + .canonicalize() + .context("Cannot canonicalize root directory")?; let tls: Option = if cli_arguments.tls { - Some(TlsConfig::new( + TlsConfig::new( cli_arguments.tls_cert, cli_arguments.tls_key, cli_arguments.tls_key_algorithm, - )?) + ) + .ok() } else { None }; @@ -106,30 +103,34 @@ impl TryFrom for Config { None }; - let basic_auth: Option = - if cli_arguments.username.is_some() && cli_arguments.password.is_some() { - Some(BasicAuthConfig::new( - cli_arguments.username.unwrap(), - cli_arguments.password.unwrap(), - )) - } else { - None - }; + // If only one of both is given + if cli_arguments.username.is_some() ^ cli_arguments.password.is_some() { + return Err(eyre!( + "Only one of username or password is given. Expected none or both" + ).with_suggestion(|| "Use both the username and password flag.\nExample:\nhttp-server --username YOURUSERNAME --password YOURPASSWORD")); + } - let logger = if cli_arguments.logger { - Some(true) + let basic_auth: Option = if let (Some(username), Some(password)) = + (cli_arguments.username, cli_arguments.password) + { + Some(BasicAuthConfig::new(username, password)) } else { None }; - let proxy = if cli_arguments.proxy.is_some() { - let proxy_url = cli_arguments.proxy.unwrap(); - - Some(ProxyConfig::url(proxy_url)) + let logger = if cli_arguments.logger { + Some(true) } else { None }; + let proxy = match cli_arguments.proxy { + Some(proxy_url) => Some(ProxyConfig::url( + Uri::from_str(&proxy_url).context("Cannot construct URI from proxy URI string")?, + )), + None => None, + }; + let spa = cli_arguments.spa; let index = spa || cli_arguments.index; @@ -140,7 +141,7 @@ impl TryFrom for Config { index, spa, root_dir, - quiet, + quiet: cli_arguments.quiet, tls, cors, compression, @@ -153,7 +154,7 @@ impl TryFrom for Config { } impl TryFrom for Config { - type Error = Error; + type Error = Report; fn try_from(file: ConfigFile) -> Result { let root_dir = file.root_dir.unwrap_or_default(); diff --git a/src/config/proxy.rs b/src/config/proxy.rs index b57b1c2f..0682141c 100644 --- a/src/config/proxy.rs +++ b/src/config/proxy.rs @@ -1,16 +1,42 @@ -use serde::Deserialize; +use http::Uri; +use serde::{Deserialize, Serialize}; -#[derive(Clone, Debug, Deserialize, PartialEq, Eq)] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct ProxyConfig { - pub url: String, + #[serde(with = "uri_serde")] + pub uri: Uri, } impl ProxyConfig { - pub fn new(url: String) -> Self { - ProxyConfig { url } + pub fn new(uri: Uri) -> Self { + ProxyConfig { uri } } - pub fn url(url: String) -> Self { - ProxyConfig { url } + pub fn url(uri: Uri) -> Self { + ProxyConfig { uri } + } +} + +mod uri_serde { + use http::uri::InvalidUri; + use serde::{de::Error as _, Deserialize, Deserializer, Serializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let string = String::deserialize(deserializer)?; + let uri = string + .parse() + .map_err(|err: InvalidUri| D::Error::custom(err.to_string()))?; + + Ok(uri) + } + + pub fn serialize(uri: &http::Uri, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&uri.to_string()) } } diff --git a/src/config/tls.rs b/src/config/tls.rs index 8f900c22..9e566f6f 100644 --- a/src/config/tls.rs +++ b/src/config/tls.rs @@ -1,4 +1,4 @@ -use anyhow::Result; +use color_eyre::eyre::Context; use rustls::{Certificate, PrivateKey}; use serde::Deserialize; use std::path::PathBuf; @@ -26,9 +26,10 @@ impl TlsConfig { cert_path: PathBuf, key_path: PathBuf, key_algorithm: PrivateKeyAlgorithm, - ) -> Result { - let cert = load_cert(&cert_path)?; - let key = load_private_key(&key_path, &key_algorithm)?; + ) -> color_eyre::Result { + let cert = load_cert(&cert_path).context("Failed to load certificate")?; + let key = + load_private_key(&key_path, &key_algorithm).context("Failed to load private key")?; Ok(TlsConfig { cert, diff --git a/src/config/util/tls.rs b/src/config/util/tls.rs index 6b9dd1e3..50b9a90f 100644 --- a/src/config/util/tls.rs +++ b/src/config/util/tls.rs @@ -1,4 +1,7 @@ -use anyhow::{Context, Error, Result}; +use color_eyre::eyre::bail; +use color_eyre::eyre::eyre; +use color_eyre::eyre::Context; +use color_eyre::Report; use rustls::{Certificate, PrivateKey}; use rustls_pemfile::{pkcs8_private_keys, rsa_private_keys}; use serde::Deserialize; @@ -16,45 +19,37 @@ pub enum PrivateKeyAlgorithm { } impl FromStr for PrivateKeyAlgorithm { - type Err = Error; + type Err = Report; fn from_str(s: &str) -> Result { match s { "rsa" => Ok(PrivateKeyAlgorithm::Rsa), "pkcs8" => Ok(PrivateKeyAlgorithm::Pkcs8), - _ => anyhow::bail!("Invalid algorithm name provided for TLS key. {}", s), + _ => bail!("Invalid algorithm name provided for TLS key. {}", s), } } } /// Load certificate on the provided `path` and retrieve it /// as an instance of `Vec`. -pub fn load_cert(path: &Path) -> Result> { - let file = File::open(path).context(format!( - "Unable to find the TLS certificate on: {}", - path.to_str().unwrap() - ))?; +pub fn load_cert(path: &Path) -> color_eyre::Result> { + let file = + File::open(path).context(format!("Unable to find the TLS certificate on: {path:?}",))?; let mut buf_reader = BufReader::new(file); - let cert_bytes = &rustls_pemfile::certs(&mut buf_reader).unwrap()[0]; + let cert_bytes = &rustls_pemfile::certs(&mut buf_reader)?[0]; Ok(vec![Certificate(cert_bytes.to_vec())]) } -pub fn load_private_key(path: &Path, kind: &PrivateKeyAlgorithm) -> Result { - let file = File::open(path) - .with_context(|| format!("Unable to find the TLS keys on: {}", path.to_str().unwrap()))?; +pub fn load_private_key(path: &Path, kind: &PrivateKeyAlgorithm) -> color_eyre::Result { + let file = + File::open(path).with_context(|| format!("Unable to find the TLS keys on: {path:?}"))?; let mut reader = BufReader::new(file); let keys = match kind { - PrivateKeyAlgorithm::Rsa => rsa_private_keys(&mut reader).map_err(|_| { - let path = path.to_str().unwrap(); - - Error::msg(format!("Failed to read private (RSA) keys at {}", path)) - })?, - PrivateKeyAlgorithm::Pkcs8 => pkcs8_private_keys(&mut reader).map_err(|_| { - let path = path.to_str().unwrap(); - - Error::msg(format!("Failed to read private (PKCS8) keys at {}", path)) - })?, + PrivateKeyAlgorithm::Rsa => rsa_private_keys(&mut reader) + .map_err(|_| eyre!("Failed to read private (RSA) keys at {path:?}"))?, + PrivateKeyAlgorithm::Pkcs8 => pkcs8_private_keys(&mut reader) + .map_err(|_| eyre!("Failed to read private (PKCS8) keys at {path:?}"))?, }; Ok(PrivateKey(keys[0].clone())) diff --git a/src/lib.rs b/src/lib.rs index c33b90ad..9e980879 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,7 +4,7 @@ mod config; mod server; mod utils; -use anyhow::{Context, Result}; +use color_eyre::eyre::{eyre, Context}; use std::convert::TryFrom; use structopt::StructOpt; @@ -12,7 +12,7 @@ use crate::config::file::ConfigFile; use crate::config::Config; use crate::server::Server; -fn resolve_config(cli_arguments: cli::Cli) -> Result { +fn resolve_config(cli_arguments: cli::Cli) -> color_eyre::Result { if let Some(config_path) = cli_arguments.config { let config_file = ConfigFile::from_file(config_path)?; let config = Config::try_from(config_file)?; @@ -21,11 +21,10 @@ fn resolve_config(cli_arguments: cli::Cli) -> Result { } // Otherwise configuration is build from CLI arguments - Config::try_from(cli_arguments) - .with_context(|| anyhow::Error::msg("Failed to parse arguments from stdin")) + Config::try_from(cli_arguments).with_context(|| eyre!("Failed to parse arguments from stdin")) } -pub fn make_server() -> Result { +pub fn make_server() -> color_eyre::Result { let cli_arguments = cli::Cli::from_args(); let config = resolve_config(cli_arguments)?; let server = Server::new(config); diff --git a/src/server/handler/file_server.rs b/src/server/handler/file_server.rs index a5d7a01c..befdd002 100644 --- a/src/server/handler/file_server.rs +++ b/src/server/handler/file_server.rs @@ -29,7 +29,21 @@ impl RequestHandler for FileServerHandler { let req_method = request_lock.method(); if req_method == Method::GET { - let response = self.file_server.resolve(req_path).await.unwrap(); + let response = match self.file_server.resolve(req_path).await { + Ok(response) => response, + Err(err) => hyper::Response::builder() + .status(hyper::StatusCode::INTERNAL_SERVER_ERROR) + .header(http::header::CONTENT_TYPE, "text/html") + .body(hyper::Body::from( + handlebars::Handlebars::new() + .render_template( + include_str!("../../addon/file_server/template/error.hbs"), + &serde_json::json!({"error": err.to_string(), "code": 500}), + ) + .unwrap(), + )) + .unwrap(), + }; return Arc::new(Mutex::new(response)); } diff --git a/src/server/handler/mod.rs b/src/server/handler/mod.rs index 88c4c6fb..ed1d434f 100644 --- a/src/server/handler/mod.rs +++ b/src/server/handler/mod.rs @@ -1,11 +1,13 @@ mod file_server; mod proxy; -use anyhow::Result; use async_trait::async_trait; +use color_eyre::eyre::Context; +use color_eyre::Report; use http::{Request, Response}; use hyper::Body; use std::convert::TryFrom; + use std::sync::Arc; use tokio::sync::Mutex; @@ -32,7 +34,10 @@ pub struct HttpHandler { } impl HttpHandler { - pub async fn handle_request(self, request: Request) -> Result> { + pub async fn handle_request( + self, + request: Request, + ) -> color_eyre::Result> { let handler = Arc::clone(&self.request_handler); let middleware = Arc::clone(&self.middleware); let response = middleware.handle(request, handler).await; @@ -41,28 +46,31 @@ impl HttpHandler { } } -impl From> for HttpHandler { - fn from(config: Arc) -> Self { +impl TryFrom> for HttpHandler { + type Error = Report; + + fn try_from(config: Arc) -> Result { if let Some(proxy_config) = config.proxy.clone() { - let proxy = Proxy::new(&proxy_config.url); + let proxy = Proxy::new(proxy_config.uri); let request_handler = Arc::new(ProxyHandler::new(proxy)); - let middleware = Middleware::try_from(config).unwrap(); + let middleware = Middleware::from(config); let middleware = Arc::new(middleware); - return HttpHandler { + return Ok(HttpHandler { request_handler, middleware, - }; + }); } - let file_server = FileServer::new(config.clone()); + let file_server = + FileServer::new(config.clone()).context("Failed to create file server")?; let request_handler = Arc::new(FileServerHandler::new(file_server)); - let middleware = Middleware::try_from(config).unwrap(); + let middleware = Middleware::from(config); let middleware = Arc::new(middleware); - HttpHandler { + Ok(HttpHandler { request_handler, middleware, - } + }) } } diff --git a/src/server/https.rs b/src/server/https.rs index f069cf56..6fe7ee5e 100644 --- a/src/server/https.rs +++ b/src/server/https.rs @@ -1,5 +1,5 @@ -use anyhow::Result; use async_stream::stream; +use color_eyre::eyre::eyre; use futures::TryFutureExt; use hyper::server::accept::Accept; use hyper::server::Builder; @@ -21,13 +21,13 @@ impl Https { Https { cert, key } } - fn make_tls_cfg(&self) -> Result> { + fn make_tls_cfg(&self) -> color_eyre::Result> { let (certs, private_key) = (self.cert.clone(), self.key.clone()); let config = ServerConfig::builder() .with_safe_defaults() .with_no_client_auth() .with_single_cert(certs, private_key) - .map_err(anyhow::Error::new)?; + .map_err(|err| eyre!(err))?; Ok(Arc::new(config)) } @@ -35,7 +35,7 @@ impl Https { pub async fn make_server( &self, addr: SocketAddr, - ) -> Result, Error = Error>>> { + ) -> color_eyre::Result, Error = Error>>> { let tcp = TcpListener::bind(addr).await?; let tls_cfg = self.make_tls_cfg()?; let tls_acceptor = TlsAcceptor::from(tls_cfg); diff --git a/src/server/middleware/cors.rs b/src/server/middleware/cors.rs index 84d9059e..c6d0e185 100644 --- a/src/server/middleware/cors.rs +++ b/src/server/middleware/cors.rs @@ -1,6 +1,6 @@ use http::{Request, Response}; use hyper::Body; -use std::convert::TryFrom; + use std::sync::Arc; use tokio::sync::Mutex; @@ -24,7 +24,7 @@ use super::MiddlewareAfter; /// /// Also panics if any CORS header value is not a valid UTF-8 string pub fn make_cors_middleware(cors_config: CorsConfig) -> MiddlewareAfter { - let cors = Cors::try_from(cors_config).unwrap(); + let cors = Cors::from(cors_config); let cors_headers = cors.make_http_headers(); Box::new( diff --git a/src/server/middleware/mod.rs b/src/server/middleware/mod.rs index 41172258..5681b52c 100644 --- a/src/server/middleware/mod.rs +++ b/src/server/middleware/mod.rs @@ -3,10 +3,9 @@ pub mod cors; pub mod gzip; pub mod logger; -use anyhow::Error; use futures::Future; use hyper::Body; -use std::convert::TryFrom; + use std::pin::Pin; use std::sync::Arc; use tokio::sync::Mutex; @@ -96,10 +95,8 @@ impl Middleware { } } -impl TryFrom> for Middleware { - type Error = Error; - - fn try_from(config: Arc) -> std::result::Result { +impl From> for Middleware { + fn from(config: Arc) -> Self { let mut middleware = Middleware::default(); if let Some(basic_auth_config) = config.basic_auth.clone() { @@ -126,6 +123,6 @@ impl TryFrom> for Middleware { } } - Ok(middleware) + middleware } } diff --git a/src/server/mod.rs b/src/server/mod.rs index 5e0271f3..e058b98b 100644 --- a/src/server/mod.rs +++ b/src/server/mod.rs @@ -4,10 +4,11 @@ mod service; pub mod middleware; -use anyhow::Error; +use color_eyre::eyre::{eyre, Context, Report}; +use color_eyre::Section; use hyper::service::{make_service_fn, service_fn}; use std::net::{Ipv4Addr, SocketAddr}; -use std::process::exit; + use std::str::FromStr; use std::sync::Arc; @@ -25,27 +26,29 @@ impl Server { Server { config } } - pub async fn run(self) { + pub async fn run(self) -> color_eyre::Result<()> { let config = Arc::clone(&self.config); let address = config.address; - let handler = handler::HttpHandler::from(Arc::clone(&config)); + let handler = handler::HttpHandler::try_from(Arc::clone(&config)) + .context("Failed to create HTTP handler")?; let server = Arc::new(self); - let mut server_instances: Vec> = Vec::new(); + let mut server_instances: Vec>> = Vec::new(); if config.spa { let mut index_html = config.root_dir.clone(); index_html.push("index.html"); if !index_html.exists() { - eprintln!( - "SPA flag is enabled, but index.html in root does not exist. Quitting..." + return Err( + eyre!("SPA flag is enabled, but index.html in root does not exist") + .with_suggestion(|| { + format!("Create index.html in root ({:?})", config.root_dir) + }), ); - exit(1); } } - if config.tls.is_some() { - let https_config = config.tls.clone().unwrap(); + if let Some(tls_config) = config.tls.clone() { let handler = handler.clone(); let host = config.address.ip(); let port = config.address.port().saturating_add(1); @@ -54,7 +57,12 @@ impl Server { let task = tokio::spawn(async move { let server = Arc::clone(&server); - server.serve_https(address, handler, https_config).await; + server + .serve_https(address, handler, tls_config) + .await + .context("Failed to serve HTTPS")?; + + Ok(()) }); server_instances.push(task); @@ -65,13 +73,17 @@ impl Server { let server = Arc::clone(&server); server.serve(address, handler).await; + + Ok(()) }); server_instances.push(task); for server_task in server_instances { - server_task.await.unwrap(); + server_task.await?.context("Task failed")?; } + + Ok(()) } pub async fn serve(&self, address: SocketAddr, handler: handler::HttpHandler) { @@ -80,7 +92,7 @@ impl Server { let handler = handler.clone(); async { - Ok::<_, Error>(service_fn(move |req| { + Ok::<_, Report>(service_fn(move |req| { service::main_service(handler.to_owned(), req) })) } @@ -116,16 +128,19 @@ impl Server { address: SocketAddr, handler: handler::HttpHandler, https_config: TlsConfig, - ) { + ) -> color_eyre::Result<()> { let (cert, key) = https_config.parts(); let https_server_builder = https::Https::new(cert, key); - let server = https_server_builder.make_server(address).await.unwrap(); + let server = https_server_builder + .make_server(address) + .await + .context("Could not build an HTTPS server")?; let server = server.serve(make_service_fn(|_| { // Move a clone of `handler` into the `service_fn`. let handler = handler.clone(); async { - Ok::<_, Error>(service_fn(move |req| { + Ok::<_, Report>(service_fn(move |req| { service::main_service(handler.to_owned(), req) })) } @@ -144,15 +159,13 @@ impl Server { if self.config.graceful_shutdown { let graceful = server.with_graceful_shutdown(crate::utils::signal::shutdown_signal()); - if let Err(e) = graceful.await { - eprint!("Server Error: {}", e); - } + graceful.await.context("Server error")?; - return; + return Ok(()); } - if let Err(e) = server.await { - eprint!("Server Error: {}", e); - } + server.await.context("Server error")?; + + Ok(()) } } diff --git a/src/server/service.rs b/src/server/service.rs index b894069a..4ea1c1c0 100644 --- a/src/server/service.rs +++ b/src/server/service.rs @@ -1,9 +1,11 @@ -use anyhow::Result; use http::{Request, Response}; use hyper::Body; use super::handler::HttpHandler; -pub async fn main_service(handler: HttpHandler, req: Request) -> Result> { +pub async fn main_service( + handler: HttpHandler, + req: Request, +) -> color_eyre::Result> { handler.handle_request(req).await } diff --git a/src/utils/url_encode.rs b/src/utils/url_encode.rs index e2c56724..4903f63a 100644 --- a/src/utils/url_encode.rs +++ b/src/utils/url_encode.rs @@ -1,3 +1,4 @@ +use color_eyre::eyre::bail; use percent_encoding::{percent_decode, utf8_percent_encode, AsciiSet, NON_ALPHANUMERIC}; use std::path::{Path, PathBuf}; @@ -7,10 +8,12 @@ pub const PERCENT_ENCODE_SET: &AsciiSet = &NON_ALPHANUMERIC .remove(b'.') .remove(b'~'); -pub fn encode_uri(file_path: &Path) -> String { - assert!(!file_path.is_absolute()); +pub fn encode_uri(file_path: &Path) -> color_eyre::Result { + if file_path.is_absolute() { + bail!("File path is not absolute: {file_path:?}"); + } - file_path + Ok(file_path .iter() .flat_map(|component| { let component = component.to_str().unwrap(); @@ -18,7 +21,7 @@ pub fn encode_uri(file_path: &Path) -> String { std::iter::once("/").chain(segment) }) - .collect::() + .collect::()) } pub fn decode_uri(file_path: &str) -> PathBuf { @@ -44,7 +47,7 @@ mod tests { fn encodes_uri() { let file_path = "/these are important files/do_not_delete/file name.txt"; let file_path = PathBuf::from_str(file_path).unwrap(); - let file_path = encode_uri(&file_path); + let file_path = encode_uri(&file_path).unwrap(); assert_eq!( file_path,