diff --git a/src/client/connection/response/input.rs b/src/client/connection/response/input.rs index b62276b..81988ff 100644 --- a/src/client/connection/response/input.rs +++ b/src/client/connection/response/input.rs @@ -1,12 +1,17 @@ +pub mod default; pub mod error; +pub mod sensitive; + +pub use default::Default; pub use error::Error; +pub use sensitive::Sensitive; -const DEFAULT: (u8, &str) = (10, "Input"); -const SENSITIVE: (u8, &str) = (11, "Sensitive input"); +const CODE: u8 = b'1'; +/// [Input expected](https://geminiprotocol.net/docs/protocol-specification.gmi#input-expected) pub enum Input { - Default { message: Option }, - Sensitive { message: Option }, + Default(Default), + Sensitive(Sensitive), } impl Input { @@ -14,97 +19,63 @@ impl Input { /// Create new `Self` from buffer include header bytes pub fn from_utf8(buffer: &[u8]) -> Result { - use std::str::FromStr; - match std::str::from_utf8(buffer) { - Ok(header) => Self::from_str(header), - Err(e) => Err(Error::Utf8Error(e)), + match buffer.first() { + Some(b) => match *b { + CODE => match buffer.get(1) { + Some(b) => match *b { + b'0' => Ok(Self::Default( + Default::from_utf8(buffer).map_err(Error::Default)?, + )), + b'1' => Ok(Self::Sensitive( + Sensitive::from_utf8(buffer).map_err(Error::Sensitive)?, + )), + b => Err(Error::SecondByte(b)), + }, + None => Err(Error::UndefinedSecondByte), + }, + b => Err(Error::FirstByte(b)), + }, + None => Err(Error::UndefinedFirstByte), } } // Getters - pub fn to_code(&self) -> u8 { - match self { - Self::Default { .. } => DEFAULT, - Self::Sensitive { .. } => SENSITIVE, - } - .0 - } - pub fn message(&self) -> Option<&str> { match self { - Self::Default { message } => message, - Self::Sensitive { message } => message, + Self::Default(default) => default.message(), + Self::Sensitive(sensitive) => sensitive.message(), } - .as_deref() } -} -impl std::fmt::Display for Input { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Default { .. } => DEFAULT, - Self::Sensitive { .. } => SENSITIVE, - } - .1 - ) - } -} - -impl std::str::FromStr for Input { - type Err = Error; - fn from_str(header: &str) -> Result { - if let Some(postfix) = header.strip_prefix("10") { - return Ok(Self::Default { - message: message(postfix), - }); + pub fn as_str(&self) -> &str { + match self { + Self::Default(default) => default.as_str(), + Self::Sensitive(sensitive) => sensitive.as_str(), } - if let Some(postfix) = header.strip_prefix("11") { - return Ok(Self::Sensitive { - message: message(postfix), - }); - } - Err(Error::Protocol) } -} -// Tools - -fn message(value: &str) -> Option { - let value = value.trim(); - if value.is_empty() { - None - } else { - Some(value.to_string()) + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Default(default) => default.as_bytes(), + Self::Sensitive(sensitive) => sensitive.as_bytes(), + } } } #[test] -fn test_from_str() { - use std::str::FromStr; - +fn test() { + fn t(source: &str, message: Option<&str>) { + let bytes = source.as_bytes(); + let input = Input::from_utf8(bytes).unwrap(); + assert_eq!(input.message(), message); + assert_eq!(input.as_str(), source); + assert_eq!(input.as_bytes(), bytes); + } // 10 - let default = Input::from_str("10 Default\r\n").unwrap(); - assert_eq!(default.message(), Some("Default")); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - - let default = Input::from_str("10\r\n").unwrap(); - assert_eq!(default.message(), None); - assert_eq!(default.to_code(), DEFAULT.0); - assert_eq!(default.to_string(), DEFAULT.1); - + t("10 Default\r\n", Some("Default")); + t("10\r\n", None); // 11 - let sensitive = Input::from_str("11 Sensitive\r\n").unwrap(); - assert_eq!(sensitive.message(), Some("Sensitive")); - assert_eq!(sensitive.to_code(), SENSITIVE.0); - assert_eq!(sensitive.to_string(), SENSITIVE.1); - - let sensitive = Input::from_str("11\r\n").unwrap(); - assert_eq!(sensitive.message(), None); - assert_eq!(sensitive.to_code(), SENSITIVE.0); - assert_eq!(sensitive.to_string(), SENSITIVE.1); + t("11 Sensitive\r\n", Some("Sensitive")); + t("11\r\n", None); } diff --git a/src/client/connection/response/input/default.rs b/src/client/connection/response/input/default.rs new file mode 100644 index 0000000..f0a04f3 --- /dev/null +++ b/src/client/connection/response/input/default.rs @@ -0,0 +1,61 @@ +pub mod error; +pub use error::Error; + +const CODE: &[u8] = b"10"; + +/// Hold header `String` for [10](https://geminiprotocol.net/docs/protocol-specification.gmi#status-10) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Default(String); + +impl Default { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let default = Default::from_utf8("10 Default\r\n".as_bytes()).unwrap(); + assert_eq!(default.message(), Some("Default")); + assert_eq!(default.as_str(), "10 Default\r\n"); + + let default = Default::from_utf8("10\r\n".as_bytes()).unwrap(); + assert_eq!(default.message(), None); + assert_eq!(default.as_str(), "10\r\n"); + + // err + assert!(Default::from_utf8("12 Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("22 Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(Default::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/input/default/error.rs b/src/client/connection/response/input/default/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/input/default/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code => { + write!(f, "Unexpected status code") + } + Self::Header(e) => { + write!(f, "Header error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +} diff --git a/src/client/connection/response/input/error.rs b/src/client/connection/response/input/error.rs index ae589e8..bcb7f8a 100644 --- a/src/client/connection/response/input/error.rs +++ b/src/client/connection/response/input/error.rs @@ -1,23 +1,40 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { + Default(super::default::Error), + FirstByte(u8), Protocol, - Utf8Error(Utf8Error), + SecondByte(u8), + Sensitive(super::sensitive::Error), + UndefinedFirstByte, + UndefinedSecondByte, } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Utf8Error(e) => { - write!(f, "UTF-8 error: {e}") + Self::Default(e) => { + write!(f, "Default parse error: {e}") + } + Self::FirstByte(b) => { + write!(f, "Unexpected first byte: {b}") } Self::Protocol => { write!(f, "Protocol error") } + Self::SecondByte(b) => { + write!(f, "Unexpected second byte: {b}") + } + Self::Sensitive(e) => { + write!(f, "Sensitive parse error: {e}") + } + Self::UndefinedFirstByte => { + write!(f, "Undefined first byte") + } + Self::UndefinedSecondByte => { + write!(f, "Undefined second byte") + } } } } diff --git a/src/client/connection/response/input/sensitive.rs b/src/client/connection/response/input/sensitive.rs new file mode 100644 index 0000000..d456490 --- /dev/null +++ b/src/client/connection/response/input/sensitive.rs @@ -0,0 +1,61 @@ +pub mod error; +pub use error::Error; + +const CODE: &[u8] = b"11"; + +/// Hold header `String` for [11](https://geminiprotocol.net/docs/protocol-specification.gmi#status-11-sensitive-input) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Sensitive(String); + +impl Sensitive { + // Constructors + + /// Parse `Self` from buffer contains header bytes + pub fn from_utf8(buffer: &[u8]) -> Result { + if !buffer.starts_with(CODE) { + return Err(Error::Code); + } + Ok(Self( + std::str::from_utf8( + crate::client::connection::response::header_bytes(buffer).map_err(Error::Header)?, + ) + .map_err(Error::Utf8Error)? + .to_string(), + )) + } + + // Getters + + /// Get optional message for `Self` + /// * return `None` if the message is empty + pub fn message(&self) -> Option<&str> { + self.0.get(2..).map(|s| s.trim()).filter(|x| !x.is_empty()) + } + + pub fn as_str(&self) -> &str { + &self.0 + } + + pub fn as_bytes(&self) -> &[u8] { + self.0.as_bytes() + } +} + +#[test] +fn test() { + // ok + let sensitive = Sensitive::from_utf8("11 Sensitive\r\n".as_bytes()).unwrap(); + assert_eq!(sensitive.message(), Some("Sensitive")); + assert_eq!(sensitive.as_str(), "11 Sensitive\r\n"); + + let sensitive = Sensitive::from_utf8("11\r\n".as_bytes()).unwrap(); + assert_eq!(sensitive.message(), None); + assert_eq!(sensitive.as_str(), "11\r\n"); + + // err + assert!(Sensitive::from_utf8("12 Fail\r\n".as_bytes()).is_err()); + assert!(Sensitive::from_utf8("22 Fail\r\n".as_bytes()).is_err()); + assert!(Sensitive::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(Sensitive::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/input/sensitive/error.rs b/src/client/connection/response/input/sensitive/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/input/sensitive/error.rs @@ -0,0 +1,24 @@ +use std::fmt::{Display, Formatter, Result}; + +#[derive(Debug)] +pub enum Error { + Code, + Header(crate::client::connection::response::HeaderBytesError), + Utf8Error(std::str::Utf8Error), +} + +impl Display for Error { + fn fmt(&self, f: &mut Formatter) -> Result { + match self { + Self::Code => { + write!(f, "Unexpected status code") + } + Self::Header(e) => { + write!(f, "Header error: {e}") + } + Self::Utf8Error(e) => { + write!(f, "UTF-8 decode error: {e}") + } + } + } +}