From 1a145c922c7541af1e9cda94bf827880a07f8d63 Mon Sep 17 00:00:00 2001 From: panicbit Date: Sat, 14 Nov 2020 01:45:16 +0100 Subject: [PATCH 1/4] add Meta::new_lossy --- src/types.rs | 53 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/src/types.rs b/src/types.rs index fb4dada..cf80661 100644 --- a/src/types.rs +++ b/src/types.rs @@ -218,12 +218,28 @@ impl StatusCategory { pub struct Meta(String); impl Meta { + /// Creates a new "Meta" string. Fails if `meta` contains `\n`. pub fn new(meta: impl AsRef + Into) -> Result { ensure!(!meta.as_ref().contains("\n"), "Meta must not contain newlines"); Ok(Self(meta.into())) } + /// Cretaes a new "Meta" string. Truncates `meta` to before the first occurrence of `\n`. + pub fn new_lossy(meta: impl AsRef + Into) -> Self { + let meta = meta.as_ref(); + let newline_pos = meta.char_indices().position(|(_i, ch)| ch == '\n'); + + match newline_pos { + None => Self(meta.into()), + Some(newline_pos) => { + let meta = meta.get(..newline_pos).expect("northstar BUG"); + + Self(meta.into()) + } + } + } + pub fn empty() -> Self { Self::default() } @@ -329,3 +345,40 @@ impl From for Body { Self::Reader(Box::new(file)) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn meta_new_lossy_truncates() { + let meta = "foo\r\nbar\nquux"; + let meta = Meta::new_lossy(meta); + + assert_eq!(meta.as_str(), "foo\r"); + } + + #[test] + fn meta_new_lossy_no_truncate() { + let meta = "foo bar\r"; + let meta = Meta::new_lossy(meta); + + assert_eq!(meta.as_str(), "foo bar\r"); + } + + #[test] + fn meta_new_lossy_empty() { + let meta = ""; + let meta = Meta::new_lossy(meta); + + assert_eq!(meta.as_str(), ""); + } + + #[test] + fn meta_new_lossy_truncates_to_empty() { + let meta = "\n\n\n"; + let meta = Meta::new_lossy(meta); + + assert_eq!(meta.as_str(), ""); + } +} From af614a06aa5672ace3bac0aa59385c2cee02ad61 Mon Sep 17 00:00:00 2001 From: panicbit Date: Sat, 14 Nov 2020 01:55:47 +0100 Subject: [PATCH 2/4] reduce number of functions that return Result --- examples/certificates.rs | 6 ++-- src/types.rs | 75 +++++++++++++++++++++++++--------------- src/util.rs | 10 +++--- 3 files changed, 55 insertions(+), 36 deletions(-) diff --git a/examples/certificates.rs b/examples/certificates.rs index 103aa7f..e3c18e7 100644 --- a/examples/certificates.rs +++ b/examples/certificates.rs @@ -32,7 +32,7 @@ fn handle_request(users: Arc>>, request: Reque if let Some(user) = users_read.get(cert_bytes) { // The user has already registered Ok( - Response::success(&GEMINI_MIME)? + Response::success(&GEMINI_MIME) .with_body(format!("Welcome {}!", user)) ) } else { @@ -44,7 +44,7 @@ fn handle_request(users: Arc>>, request: Reque let mut users_write = users.write().await; users_write.insert(cert_bytes.clone(), username.to_owned()); Ok( - Response::success(&GEMINI_MIME)? + Response::success(&GEMINI_MIME) .with_body(format!( "Your account has been created {}! Welcome!", username @@ -57,7 +57,7 @@ fn handle_request(users: Arc>>, request: Reque } } else { // The user didn't provide a certificate - Response::client_certificate_required() + Ok(Response::client_certificate_required()) } }.boxed() } diff --git a/src/types.rs b/src/types.rs index cf80661..6797dff 100644 --- a/src/types.rs +++ b/src/types.rs @@ -88,11 +88,18 @@ impl ResponseHeader { }) } - pub fn success(mime: &Mime) -> Result { - Ok(Self { + pub fn input_lossy(prompt: impl AsRef + Into) -> Self { + Self { + status: Status::INPUT, + meta: Meta::new_lossy(prompt), + } + } + + pub fn success(mime: &Mime) -> Self { + Self { status: Status::SUCCESS, - meta: Meta::new(mime.to_string())?, - }) + meta: Meta::new_lossy(mime.to_string()), + } } pub fn server_error(reason: impl AsRef + Into) -> Result { @@ -102,25 +109,32 @@ impl ResponseHeader { }) } - pub fn not_found() -> Result { - Ok(Self { + pub fn server_error_lossy(reason: impl AsRef + Into) -> Self { + Self { + status: Status::PERMANENT_FAILURE, + meta: Meta::new_lossy(reason), + } + } + + pub fn not_found() -> Self { + Self { status: Status::NOT_FOUND, - meta: Meta::new("Not found")?, - }) + meta: Meta::new_lossy("Not found"), + } } - pub fn client_certificate_required() -> Result { - Ok(Self { + pub fn client_certificate_required() -> Self { + Self { status: Status::CLIENT_CERTIFICATE_REQUIRED, - meta: Meta::new("No certificate provided")?, - }) + meta: Meta::new_lossy("No certificate provided"), + } } - pub fn certificate_not_authorized() -> Result { - Ok(Self { + pub fn certificate_not_authorized() -> Self { + Self { status: Status::CERTIFICATE_NOT_AUTHORIZED, - meta: Meta::new("Your certificate is not authorized to view this content")?, - }) + meta: Meta::new_lossy("Your certificate is not authorized to view this content"), + } } pub fn status(&self) -> &Status { @@ -272,9 +286,14 @@ impl Response { Ok(Self::new(header)) } - pub fn success(mime: &Mime) -> Result { - let header = ResponseHeader::success(&mime)?; - Ok(Self::new(header)) + pub fn input_lossy(prompt: impl AsRef + Into) -> Self { + let header = ResponseHeader::input_lossy(prompt); + Self::new(header) + } + + pub fn success(mime: &Mime) -> Self { + let header = ResponseHeader::success(&mime); + Self::new(header) } pub fn server_error(reason: impl AsRef + Into) -> Result { @@ -282,19 +301,19 @@ impl Response { Ok(Self::new(header)) } - pub fn not_found() -> Result { - let header = ResponseHeader::not_found()?; - Ok(Self::new(header)) + pub fn not_found() -> Self { + let header = ResponseHeader::not_found(); + Self::new(header) } - pub fn client_certificate_required() -> Result { - let header = ResponseHeader::client_certificate_required()?; - Ok(Self::new(header)) + pub fn client_certificate_required() -> Self { + let header = ResponseHeader::client_certificate_required(); + Self::new(header) } - pub fn certificate_not_authorized() -> Result { - let header = ResponseHeader::certificate_not_authorized()?; - Ok(Self::new(header)) + pub fn certificate_not_authorized() -> Self { + let header = ResponseHeader::certificate_not_authorized(); + Self::new(header) } pub fn with_body(mut self, body: impl Into) -> Self { diff --git a/src/util.rs b/src/util.rs index 8b388fc..0236fbd 100644 --- a/src/util.rs +++ b/src/util.rs @@ -15,12 +15,12 @@ pub async fn serve_file>(path: P, mime: &Mime) -> Result file, Err(err) => match err.kind() { - io::ErrorKind::NotFound => return Ok(Response::not_found()?), + io::ErrorKind::NotFound => return Ok(Response::not_found()), _ => return Err(err.into()), } }; - Ok(Response::success(&mime)?.with_body(file)) + Ok(Response::success(&mime).with_body(file)) } pub async fn serve_dir, P: AsRef>(dir: D, virtual_path: &[P]) -> Result { @@ -35,7 +35,7 @@ pub async fn serve_dir, P: AsRef>(dir: D, virtual_path: &[P let path = path.canonicalize()?; if !path.starts_with(&dir) { - return Ok(Response::not_found()?); + return Ok(Response::not_found()); } if !path.is_dir() { @@ -52,7 +52,7 @@ async fn serve_dir_listing, B: AsRef>(path: P, virtual_path let mut dir = match fs::read_dir(path).await { Ok(dir) => dir, Err(err) => match err.kind() { - io::ErrorKind::NotFound => return Ok(Response::not_found()?), + io::ErrorKind::NotFound => return Ok(Response::not_found()), _ => return Err(err.into()), } }; @@ -82,7 +82,7 @@ async fn serve_dir_listing, B: AsRef>(path: P, virtual_path )?; } - Ok(Response::success(&GEMINI_MIME)?.with_body(listing)) + Ok(Response::success(&GEMINI_MIME).with_body(listing)) } pub fn guess_mime_from_path>(path: P) -> Mime { From 73a5c4007d35b3e794a5e42c7f94756720c6675f Mon Sep 17 00:00:00 2001 From: panicbit Date: Sat, 14 Nov 2020 02:19:13 +0100 Subject: [PATCH 3/4] add changelog --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..bbcc1b1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] +### Added +- `GEMINI_MIME_STR`, the `&str` representation of the Gemini MIME +- `Meta::new_lossy` constructor that never fails +- "lossy" constructors for `Response` and `Status` (see `Meta::new_lossy`) + +### Changed +- Some `Response` and `Status` constructors are now infallible + +### Deprecated +- Instead of `gemini_mime()` use `GEMINI_MIME` + +## [0.2.0] - 2020-11-14 +### Added +- Access to client certificates by [@Alch-Emi](https://github.com/Alch-Emi) \ No newline at end of file From 82dc34b2c53a12ff8a879118dbe63aec15c59d4a Mon Sep 17 00:00:00 2001 From: panicbit Date: Sat, 14 Nov 2020 02:54:34 +0100 Subject: [PATCH 4/4] respect meta max len of 1024 in constructors --- CHANGELOG.md | 2 ++ src/types.rs | 69 +++++++++++++++++++++++++++++++++++++++++++++------- 2 files changed, 62 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbcc1b1..7e32277 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - `GEMINI_MIME_STR`, the `&str` representation of the Gemini MIME - `Meta::new_lossy` constructor that never fails +- `Meta::MAX_LEN`, which is `1024` - "lossy" constructors for `Response` and `Status` (see `Meta::new_lossy`) ### Changed +- `Meta::new` rejects strings exceeding `Meta::MAX_LEN` (`1024`) - Some `Response` and `Status` constructors are now infallible ### Deprecated diff --git a/src/types.rs b/src/types.rs index 6797dff..15316f5 100644 --- a/src/types.rs +++ b/src/types.rs @@ -232,9 +232,12 @@ impl StatusCategory { pub struct Meta(String); impl Meta { + pub const MAX_LEN: usize = 1024; + /// Creates a new "Meta" string. Fails if `meta` contains `\n`. pub fn new(meta: impl AsRef + Into) -> Result { ensure!(!meta.as_ref().contains("\n"), "Meta must not contain newlines"); + ensure!(meta.as_ref().len() <= Self::MAX_LEN, "Meta must not exceed {} bytes", Self::MAX_LEN); Ok(Self(meta.into())) } @@ -242,16 +245,19 @@ impl Meta { /// Cretaes a new "Meta" string. Truncates `meta` to before the first occurrence of `\n`. pub fn new_lossy(meta: impl AsRef + Into) -> Self { let meta = meta.as_ref(); - let newline_pos = meta.char_indices().position(|(_i, ch)| ch == '\n'); + let truncate_pos = meta.char_indices().position(|(i, ch)| { + let is_newline = ch == '\n'; + let exceeds_limit = (i + ch.len_utf8()) > Self::MAX_LEN; - match newline_pos { - None => Self(meta.into()), - Some(newline_pos) => { - let meta = meta.get(..newline_pos).expect("northstar BUG"); + is_newline || exceeds_limit + }); - Self(meta.into()) - } - } + let meta: String = match truncate_pos { + None => meta.into(), + Some(truncate_pos) => meta.get(..truncate_pos).expect("northstar BUG").into(), + }; + + Self(meta) } pub fn empty() -> Self { @@ -368,6 +374,31 @@ impl From for Body { #[cfg(test)] mod tests { use super::*; + use std::iter::repeat; + + #[test] + fn meta_new_rejects_newlines() { + let meta = "foo\nbar"; + let meta = Meta::new(meta); + + assert!(meta.is_err()); + } + + #[test] + fn meta_new_accepts_max_len() { + let meta: String = repeat('x').take(Meta::MAX_LEN).collect(); + let meta = Meta::new(meta); + + assert!(meta.is_ok()); + } + + #[test] + fn meta_new_rejects_exceeding_max_len() { + let meta: String = repeat('x').take(Meta::MAX_LEN + 1).collect(); + let meta = Meta::new(meta); + + assert!(meta.is_err()); + } #[test] fn meta_new_lossy_truncates() { @@ -393,11 +424,31 @@ mod tests { assert_eq!(meta.as_str(), ""); } - #[test] + #[test] fn meta_new_lossy_truncates_to_empty() { let meta = "\n\n\n"; let meta = Meta::new_lossy(meta); assert_eq!(meta.as_str(), ""); } + + #[test] + fn meta_new_lossy_truncates_to_max_len() { + let meta: String = repeat('x').take(Meta::MAX_LEN + 1).collect(); + let meta = Meta::new_lossy(meta); + + assert_eq!(meta.as_str().len(), Meta::MAX_LEN); + } + + #[test] + fn meta_new_lossy_truncates_multi_byte_sequences() { + let mut meta: String = repeat('x').take(Meta::MAX_LEN - 1).collect(); + meta.push('🦀'); + + assert_eq!(meta.len(), Meta::MAX_LEN + 3); + + let meta = Meta::new_lossy(meta); + + assert_eq!(meta.as_str().len(), Meta::MAX_LEN - 1); + } }