diff --git a/src/client/connection/response/certificate.rs b/src/client/connection/response/certificate.rs index 423d0e8..e67ded4 100644 --- a/src/client/connection/response/certificate.rs +++ b/src/client/connection/response/certificate.rs @@ -1,19 +1,24 @@ pub mod error; -pub use error::Error; +pub mod not_authorized; +pub mod not_valid; +pub mod required; -const REQUIRED: (u8, &str) = (60, "Certificate required"); -const NOT_AUTHORIZED: (u8, &str) = (61, "Certificate not authorized"); -const NOT_VALID: (u8, &str) = (62, "Certificate not valid"); +pub use error::Error; +pub use not_authorized::NotAuthorized; +pub use not_valid::NotValid; +pub use required::Required; + +const CODE: u8 = b'6'; /// 6* status code group /// https://geminiprotocol.net/docs/protocol-specification.gmi#client-certificates pub enum Certificate { /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-60 - Required { message: Option }, + Required(Required), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-61-certificate-not-authorized - NotAuthorized { message: Option }, + NotAuthorized(NotAuthorized), /// https://geminiprotocol.net/docs/protocol-specification.gmi#status-62-certificate-not-valid - NotValid { message: Option }, + NotValid(NotValid), } impl Certificate { @@ -21,95 +26,72 @@ impl Certificate { /// 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::Required( + Required::from_utf8(buffer).map_err(Error::Required)?, + )), + b'1' => Ok(Self::NotAuthorized( + NotAuthorized::from_utf8(buffer).map_err(Error::NotAuthorized)?, + )), + b'2' => Ok(Self::NotValid( + NotValid::from_utf8(buffer).map_err(Error::NotValid)?, + )), + 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::Required { .. } => REQUIRED, - Self::NotAuthorized { .. } => NOT_AUTHORIZED, - Self::NotValid { .. } => NOT_VALID, - } - .0 - } - pub fn message(&self) -> Option<&str> { match self { - Self::Required { message } => message, - Self::NotAuthorized { message } => message, - Self::NotValid { message } => message, + Self::Required(required) => required.message(), + Self::NotAuthorized(not_authorized) => not_authorized.message(), + Self::NotValid(not_valid) => not_valid.message(), } - .as_deref() } -} -impl std::fmt::Display for Certificate { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!( - f, - "{}", - match self { - Self::Required { .. } => REQUIRED, - Self::NotAuthorized { .. } => NOT_AUTHORIZED, - Self::NotValid { .. } => NOT_VALID, - } - .1 - ) + pub fn as_str(&self) -> &str { + match self { + Self::Required(required) => required.as_str(), + Self::NotAuthorized(not_authorized) => not_authorized.as_str(), + Self::NotValid(not_valid) => not_valid.as_str(), + } } -} -impl std::str::FromStr for Certificate { - type Err = Error; - fn from_str(header: &str) -> Result { - if let Some(postfix) = header.strip_prefix("60") { - return Ok(Self::Required { - message: message(postfix), - }); + pub fn as_bytes(&self) -> &[u8] { + match self { + Self::Required(required) => required.as_bytes(), + Self::NotAuthorized(not_authorized) => not_authorized.as_bytes(), + Self::NotValid(not_valid) => not_valid.as_bytes(), } - if let Some(postfix) = header.strip_prefix("61") { - return Ok(Self::NotAuthorized { - message: message(postfix), - }); - } - if let Some(postfix) = header.strip_prefix("62") { - return Ok(Self::NotValid { - message: message(postfix), - }); - } - Err(Error::Code) - } -} - -// Tools - -fn message(value: &str) -> Option { - let value = value.trim(); - if value.is_empty() { - None - } else { - Some(value.to_string()) } } #[test] -fn test_from_str() { - use std::str::FromStr; - - let required = Certificate::from_str("60 Message\r\n").unwrap(); - - assert_eq!(required.message(), Some("Message")); - assert_eq!(required.to_code(), REQUIRED.0); - assert_eq!(required.to_string(), REQUIRED.1); - - let required = Certificate::from_str("60\r\n").unwrap(); - - assert_eq!(required.message(), None); - assert_eq!(required.to_code(), REQUIRED.0); - assert_eq!(required.to_string(), REQUIRED.1); +fn test() { + fn t(source: &str, message: Option<&str>) { + let b = source.as_bytes(); + let c = Certificate::from_utf8(b).unwrap(); + assert_eq!(c.message(), message); + assert_eq!(c.as_str(), source); + assert_eq!(c.as_bytes(), b); + } + // 60 + t("60 Required\r\n", Some("Required")); + t("60\r\n", None); + // 61 + t("61 Not Authorized\r\n", Some("Not Authorized")); + t("61\r\n", None); + // 62 + t("61 Not Valid\r\n", Some("Not Valid")); + t("61\r\n", None); } diff --git a/src/client/connection/response/certificate/error.rs b/src/client/connection/response/certificate/error.rs index 5cf1cf6..a710617 100644 --- a/src/client/connection/response/certificate/error.rs +++ b/src/client/connection/response/certificate/error.rs @@ -1,22 +1,39 @@ -use std::{ - fmt::{Display, Formatter, Result}, - str::Utf8Error, -}; +use std::fmt::{Display, Formatter, Result}; #[derive(Debug)] pub enum Error { - Code, - Utf8Error(Utf8Error), + FirstByte(u8), + NotAuthorized(super::not_authorized::Error), + NotValid(super::not_valid::Error), + Required(super::required::Error), + SecondByte(u8), + UndefinedFirstByte, + UndefinedSecondByte, } impl Display for Error { fn fmt(&self, f: &mut Formatter) -> Result { match self { - Self::Code => { - write!(f, "Status code error") + Self::FirstByte(b) => { + write!(f, "Unexpected first byte: {b}") } - Self::Utf8Error(e) => { - write!(f, "UTF-8 error: {e}") + Self::NotAuthorized(e) => { + write!(f, "NotAuthorized status parse error: {e}") + } + Self::NotValid(e) => { + write!(f, "NotValid status parse error: {e}") + } + Self::Required(e) => { + write!(f, "Required status parse error: {e}") + } + Self::SecondByte(b) => { + write!(f, "Unexpected second byte: {b}") + } + Self::UndefinedFirstByte => { + write!(f, "Undefined first byte") + } + Self::UndefinedSecondByte => { + write!(f, "Undefined second byte") } } } diff --git a/src/client/connection/response/certificate/not_authorized.rs b/src/client/connection/response/certificate/not_authorized.rs new file mode 100644 index 0000000..fe85d1e --- /dev/null +++ b/src/client/connection/response/certificate/not_authorized.rs @@ -0,0 +1,61 @@ +pub mod error; +pub use error::Error; + +const CODE: &[u8] = b"61"; + +/// Hold header `String` for [61](https://geminiprotocol.net/docs/protocol-specification.gmi#status-61) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct NotAuthorized(String); + +impl NotAuthorized { + // 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 not_authorized = NotAuthorized::from_utf8("61 Not Authorized\r\n".as_bytes()).unwrap(); + assert_eq!(not_authorized.message(), Some("Not Authorized")); + assert_eq!(not_authorized.as_str(), "61 Not Authorized\r\n"); + + let not_authorized = NotAuthorized::from_utf8("61\r\n".as_bytes()).unwrap(); + assert_eq!(not_authorized.message(), None); + assert_eq!(not_authorized.as_str(), "61\r\n"); + + // err + assert!(NotAuthorized::from_utf8("62 Fail\r\n".as_bytes()).is_err()); + assert!(NotAuthorized::from_utf8("62 Fail\r\n".as_bytes()).is_err()); + assert!(NotAuthorized::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(NotAuthorized::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/certificate/not_authorized/error.rs b/src/client/connection/response/certificate/not_authorized/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/certificate/not_authorized/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/certificate/not_valid.rs b/src/client/connection/response/certificate/not_valid.rs new file mode 100644 index 0000000..35ad475 --- /dev/null +++ b/src/client/connection/response/certificate/not_valid.rs @@ -0,0 +1,61 @@ +pub mod error; +pub use error::Error; + +const CODE: &[u8] = b"62"; + +/// Hold header `String` for [62](https://geminiprotocol.net/docs/protocol-specification.gmi#status-62) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct NotValid(String); + +impl NotValid { + // 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 not_valid = NotValid::from_utf8("62 Not Valid\r\n".as_bytes()).unwrap(); + assert_eq!(not_valid.message(), Some("Not Valid")); + assert_eq!(not_valid.as_str(), "62 Not Valid\r\n"); + + let not_valid = NotValid::from_utf8("62\r\n".as_bytes()).unwrap(); + assert_eq!(not_valid.message(), None); + assert_eq!(not_valid.as_str(), "62\r\n"); + + // err + // @TODO assert!(NotValid::from_utf8("62Fail\r\n".as_bytes()).is_err()); + assert!(NotValid::from_utf8("63 Fail\r\n".as_bytes()).is_err()); + assert!(NotValid::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(NotValid::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/certificate/not_valid/error.rs b/src/client/connection/response/certificate/not_valid/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/certificate/not_valid/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/certificate/required.rs b/src/client/connection/response/certificate/required.rs new file mode 100644 index 0000000..df0ef63 --- /dev/null +++ b/src/client/connection/response/certificate/required.rs @@ -0,0 +1,61 @@ +pub mod error; +pub use error::Error; + +const CODE: &[u8] = b"60"; + +/// Hold header `String` for [60](https://geminiprotocol.net/docs/protocol-specification.gmi#status-60) status code +/// * this response type does not contain body data +/// * the header member is closed to require valid construction +pub struct Required(String); + +impl Required { + // 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 required = Required::from_utf8("60 Required\r\n".as_bytes()).unwrap(); + assert_eq!(required.message(), Some("Required")); + assert_eq!(required.as_str(), "60 Required\r\n"); + + let required = Required::from_utf8("60\r\n".as_bytes()).unwrap(); + assert_eq!(required.message(), None); + assert_eq!(required.as_str(), "60\r\n"); + + // err + assert!(Required::from_utf8("62 Fail\r\n".as_bytes()).is_err()); + assert!(Required::from_utf8("62 Fail\r\n".as_bytes()).is_err()); + assert!(Required::from_utf8("Fail\r\n".as_bytes()).is_err()); + assert!(Required::from_utf8("Fail".as_bytes()).is_err()); +} diff --git a/src/client/connection/response/certificate/required/error.rs b/src/client/connection/response/certificate/required/error.rs new file mode 100644 index 0000000..4f0ee59 --- /dev/null +++ b/src/client/connection/response/certificate/required/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}") + } + } + } +}